├── .dockerignore ├── packages ├── react │ ├── .env │ ├── public │ │ ├── robots.txt │ │ ├── favicon.ico │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── index.html │ ├── src │ │ ├── react-app-env.d.ts │ │ ├── setupTests.ts │ │ ├── app │ │ │ ├── constants │ │ │ │ └── FrontendOptions.ts │ │ │ ├── hooks │ │ │ │ ├── useSeverityColor.ts │ │ │ │ ├── useGlobalSeverity.ts │ │ │ │ ├── useMetricRange.ts │ │ │ │ ├── useStatusifyEvent.ts │ │ │ │ ├── useComponentInfo.ts │ │ │ │ ├── useIncidents.ts │ │ │ │ ├── useSeverity.ts │ │ │ │ ├── useUptimePercentage.ts │ │ │ │ ├── useComponentMetric.ts │ │ │ │ └── useSeverityTicks.ts │ │ │ ├── interfaces │ │ │ │ └── ISeverityTick.ts │ │ │ ├── utils │ │ │ │ ├── dayjs.ts │ │ │ │ ├── reportWebVitals.ts │ │ │ │ └── i18n.ts │ │ │ ├── theme │ │ │ │ ├── LaminarThemeOptions.ts │ │ │ │ └── Theme.ts │ │ │ ├── components │ │ │ │ ├── elements │ │ │ │ │ ├── componentGroup │ │ │ │ │ │ ├── ComponentGroupHeaderToggleIndicator.tsx │ │ │ │ │ │ ├── ComponentGroupBody.tsx │ │ │ │ │ │ ├── ComponentGroup.tsx │ │ │ │ │ │ └── ComponentGroupHeader.tsx │ │ │ │ │ ├── util │ │ │ │ │ │ └── CirclePulse.tsx │ │ │ │ │ ├── global │ │ │ │ │ │ ├── StatusBanner.tsx │ │ │ │ │ │ └── header │ │ │ │ │ │ │ └── PageHeader.tsx │ │ │ │ │ ├── incident │ │ │ │ │ │ ├── IncidentUpdate.tsx │ │ │ │ │ │ ├── IncidentBannerUpdate.tsx │ │ │ │ │ │ └── IncidentBanner.tsx │ │ │ │ │ └── component │ │ │ │ │ │ ├── Component.tsx │ │ │ │ │ │ ├── ComponentHeader.tsx │ │ │ │ │ │ └── metrics │ │ │ │ │ │ ├── tickChart │ │ │ │ │ │ ├── ComponentTickChartTooltip.tsx │ │ │ │ │ │ └── ComponentTickChart.tsx │ │ │ │ │ │ └── latency │ │ │ │ │ │ └── ComponentLatencyGraph.tsx │ │ │ │ └── layouts │ │ │ │ │ └── DefaultLayout.tsx │ │ │ └── contexts │ │ │ │ ├── StatusifyContext.tsx │ │ │ │ ├── ComponentContext.tsx │ │ │ │ ├── LaminarContext.tsx │ │ │ │ ├── ResponsiveViewboxContext.tsx │ │ │ │ ├── AutoRefreshContext.tsx │ │ │ │ └── ComponentGroupContext.tsx │ │ ├── index.tsx │ │ ├── lang │ │ │ └── en.json │ │ ├── pages │ │ │ ├── _app.tsx │ │ │ ├── index.tsx │ │ │ └── IncidentPage.tsx │ │ ├── logo.svg │ │ └── StatusifyInstance.ts │ ├── .prettierrc │ ├── nginx.conf │ ├── .gitignore │ ├── .eslintrc │ ├── Dockerfile │ ├── tsconfig.json │ ├── README.md │ └── package.json ├── core │ ├── .gitignore │ ├── lib │ │ ├── Types │ │ │ └── constructor.d.ts │ │ ├── Metric │ │ │ ├── IMetricRange.ts │ │ │ ├── MetricRecord.ts │ │ │ ├── ILatencyMetricRecord.ts │ │ │ ├── IDowntimeMetricRecord.ts │ │ │ └── Metric.ts │ │ ├── Builder │ │ │ ├── index.ts │ │ │ ├── AttributeStorageBuilder.ts │ │ │ ├── Builder.ts │ │ │ ├── SeverityBuilder.ts │ │ │ └── ComponentBuilder.ts │ │ ├── Util │ │ │ ├── StatusifyEvents.ts │ │ │ ├── IInjectStatusify.ts │ │ │ ├── ApplyMixins.ts │ │ │ ├── WorstSeverity.ts │ │ │ └── AttributeStorage.ts │ │ ├── Severity │ │ │ ├── IProvidesSeverities.ts │ │ │ ├── Severity.ts │ │ │ ├── ICalculatesSeverities.ts │ │ │ ├── RunnableSeverity.ts │ │ │ ├── SeverityMultiplexer.ts │ │ │ ├── AchievedSeverityCalculator.ts │ │ │ └── IncidentSeverityCalculator.ts │ │ ├── Incident │ │ │ ├── IIncidentUpdate.ts │ │ │ ├── IProvidesIncidents.ts │ │ │ ├── IIncident.ts │ │ │ └── ArrayIncidentProvider.ts │ │ ├── Component │ │ │ ├── IProvidesComponents.ts │ │ │ ├── Component.ts │ │ │ └── ComponentGroup.ts │ │ └── index.ts │ ├── babel.config.js │ ├── tsconfig.json │ ├── package.json │ ├── __tests__ │ │ └── builder.test.ts │ ├── __TestUtils │ │ └── MockBuilder.ts │ └── README.md ├── uptimerobot │ ├── .gitignore │ ├── lib │ │ ├── Util │ │ │ ├── CachedEntry.ts │ │ │ ├── useSomewhatSingleton.ts │ │ │ └── useCache.ts │ │ ├── constants.ts │ │ ├── Types │ │ │ ├── IUptimeRobotResponseTime.ts │ │ │ ├── UptimeRobotMonitorType.ts │ │ │ ├── UptimeRobotMonitorStatus.ts │ │ │ ├── IUptimeRobotResponse.ts │ │ │ ├── IUptimeRobotMonitorResponse.ts │ │ │ └── IUptimeRobotMonitor.ts │ │ ├── Metric │ │ │ ├── GenericUptimeRobotMetric.ts │ │ │ ├── UptimeRobotDowntime.ts │ │ │ └── UptimeRobotLatency.ts │ │ └── index.ts │ ├── babel.config.js │ ├── README.md │ ├── tsconfig.json │ ├── example.ts │ ├── __tests__ │ │ └── uptimerobot.test.ts │ └── package.json ├── loki │ ├── .gitignore │ ├── __tests__ │ │ └── loki.test.js │ ├── README.md │ ├── tsconfig.json │ ├── package.json │ ├── yarn.lock │ └── lib │ │ └── LokiIncidentProvider.ts └── tsconfig.settings.json ├── example ├── .gitignore ├── tsconfig.json ├── package.json ├── incidents │ └── 2021-09-01-test-incident.json ├── yarn.lock ├── index.ts └── LokiIncidentProvider.ts ├── .github ├── statusify.png ├── react-preview.png ├── statusify-core.png └── statusify-react.png ├── lerna.json ├── .prettierrc ├── .gitignore ├── docker-compose.yml ├── workspace.code-workspace ├── CONTRIBUTING.md ├── LICENSE ├── package.json └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules -------------------------------------------------------------------------------- /packages/react/.env: -------------------------------------------------------------------------------- 1 | BROWSER=none -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn-error.log -------------------------------------------------------------------------------- /packages/core/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /packages/uptimerobot/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /packages/loki/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | tsconfig.tsbuildinfo 3 | yarn-error.log -------------------------------------------------------------------------------- /packages/core/lib/Types/constructor.d.ts: -------------------------------------------------------------------------------- 1 | declare type Constructor = new (...args: any[]) => {}; -------------------------------------------------------------------------------- /.github/statusify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LegendEffects/Statusify/HEAD/.github/statusify.png -------------------------------------------------------------------------------- /.github/react-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LegendEffects/Statusify/HEAD/.github/react-preview.png -------------------------------------------------------------------------------- /.github/statusify-core.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LegendEffects/Statusify/HEAD/.github/statusify-core.png -------------------------------------------------------------------------------- /.github/statusify-react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LegendEffects/Statusify/HEAD/.github/statusify-react.png -------------------------------------------------------------------------------- /packages/react/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../packages/tsconfig.settings.json", 3 | "include": [ 4 | "./" 5 | ] 6 | } -------------------------------------------------------------------------------- /packages/core/lib/Metric/IMetricRange.ts: -------------------------------------------------------------------------------- 1 | export default interface IMetricRange { 2 | start: Date 3 | end: Date 4 | } -------------------------------------------------------------------------------- /packages/core/lib/Metric/MetricRecord.ts: -------------------------------------------------------------------------------- 1 | export default interface MetricRecord { 2 | time: Date, 3 | value: number 4 | } -------------------------------------------------------------------------------- /packages/react/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LegendEffects/Statusify/HEAD/packages/react/public/favicon.ico -------------------------------------------------------------------------------- /packages/react/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LegendEffects/Statusify/HEAD/packages/react/public/logo192.png -------------------------------------------------------------------------------- /packages/react/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LegendEffects/Statusify/HEAD/packages/react/public/logo512.png -------------------------------------------------------------------------------- /packages/uptimerobot/lib/Util/CachedEntry.ts: -------------------------------------------------------------------------------- 1 | export default interface CachedEntry { 2 | time: Date, 3 | entry: T 4 | } -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "independent", 6 | "npmClient": "yarn" 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/lib/Builder/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Builder' 2 | export * from './ComponentBuilder' 3 | export * from './SeverityBuilder' -------------------------------------------------------------------------------- /packages/uptimerobot/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const CACHE_LIFETIME = 60 * 5; // 5 Mins 2 | export const MILLISECONDS_IN_DAY = 3600 * 24 * 1000; -------------------------------------------------------------------------------- /packages/loki/__tests__/loki.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const loki = require('..'); 4 | 5 | describe('loki', () => { 6 | it('needs tests'); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/uptimerobot/lib/Types/IUptimeRobotResponseTime.ts: -------------------------------------------------------------------------------- 1 | export default interface IUptimeRobotResponseTime { 2 | datetime: number; 3 | value: number; 4 | } -------------------------------------------------------------------------------- /packages/loki/README.md: -------------------------------------------------------------------------------- 1 | # `loki` 2 | 3 | > TODO: description 4 | 5 | ## Usage 6 | 7 | ``` 8 | const loki = require('loki'); 9 | 10 | // TODO: DEMONSTRATE API 11 | ``` 12 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "es5", 4 | "useTabs": false, 5 | "singleQuote": true, 6 | "arrowParens": "always", 7 | "endOfLine": "auto", 8 | "tabWidth": 2 9 | } -------------------------------------------------------------------------------- /packages/core/babel.config.js: -------------------------------------------------------------------------------- 1 | // babel.config.js 2 | module.exports = { 3 | presets: [ 4 | ['@babel/preset-env', {targets: {node: 'current'}}], 5 | '@babel/preset-typescript', 6 | ], 7 | }; -------------------------------------------------------------------------------- /packages/core/lib/Util/StatusifyEvents.ts: -------------------------------------------------------------------------------- 1 | import IIncident from "../Incident/IIncident"; 2 | 3 | export default interface StatusifyEvents { 4 | 'incidents::updated': (incidents: IIncident[]) => void; 5 | } -------------------------------------------------------------------------------- /packages/uptimerobot/babel.config.js: -------------------------------------------------------------------------------- 1 | // babel.config.js 2 | module.exports = { 3 | presets: [ 4 | ['@babel/preset-env', {targets: {node: 'current'}}], 5 | '@babel/preset-typescript', 6 | ], 7 | }; -------------------------------------------------------------------------------- /packages/uptimerobot/lib/Types/UptimeRobotMonitorType.ts: -------------------------------------------------------------------------------- 1 | enum UptimeRobotMonitorType { 2 | HTTP, 3 | Keyword, 4 | Ping, 5 | Port, 6 | Heartbeat, 7 | } 8 | 9 | export default UptimeRobotMonitorType; -------------------------------------------------------------------------------- /packages/uptimerobot/README.md: -------------------------------------------------------------------------------- 1 | # `uptimerobot` 2 | 3 | > TODO: description 4 | 5 | ## Usage 6 | 7 | ``` 8 | const uptimerobot = require('uptimerobot'); 9 | 10 | // TODO: DEMONSTRATE API 11 | ``` 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Dependencies 10 | node_modules 11 | 12 | # Builds 13 | dist 14 | .tsbuildinfo -------------------------------------------------------------------------------- /packages/uptimerobot/lib/Types/UptimeRobotMonitorStatus.ts: -------------------------------------------------------------------------------- 1 | enum UptimeRobotMonitorStatus { 2 | Paused, 3 | NotCheckedYet, 4 | Up, 5 | SeemsDown, 6 | Down 7 | } 8 | 9 | export default UptimeRobotMonitorStatus; -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.settings.json", 3 | "compilerOptions": { 4 | "rootDir": "lib", 5 | "outDir": "dist" 6 | }, 7 | "include": [ 8 | "./lib/**/*" 9 | ] 10 | } -------------------------------------------------------------------------------- /packages/loki/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.settings.json", 3 | "compilerOptions": { 4 | "rootDir": "lib", 5 | "outDir": "dist" 6 | }, 7 | "include": [ 8 | "./lib/**/*" 9 | ] 10 | } -------------------------------------------------------------------------------- /packages/react/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | -------------------------------------------------------------------------------- /packages/uptimerobot/lib/Types/IUptimeRobotResponse.ts: -------------------------------------------------------------------------------- 1 | export default interface IUptimeRobotResponse { 2 | stat: "ok" | "fail"; 3 | pagination: { 4 | offset: number; 5 | limit: number; 6 | total: number; 7 | } 8 | } -------------------------------------------------------------------------------- /packages/uptimerobot/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.settings.json", 3 | "compilerOptions": { 4 | "rootDir": "lib", 5 | "outDir": "dist" 6 | }, 7 | "include": [ 8 | "./lib/**/*" 9 | ] 10 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.6' 2 | 3 | services: 4 | 5 | front-end: 6 | image: statusify 7 | ports: 8 | - "8080:8080" 9 | restart: on-failure 10 | build: 11 | context: ./ 12 | dockerfile: ./packages/react/Dockerfile 13 | -------------------------------------------------------------------------------- /packages/react/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "es5", 4 | "useTabs": false, 5 | "singleQuote": true, 6 | "arrowParens": "always", 7 | "endOfLine": "auto", 8 | "tabWidth": 2, 9 | "bracketSpacing": false, 10 | "eslintIntegration": true 11 | } -------------------------------------------------------------------------------- /packages/react/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "@types/lokijs": "^1.5.3", 8 | "@types/node": "^14.14.20", 9 | "lokijs": "^1.5.11", 10 | "path": "^0.12.7" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/uptimerobot/lib/Types/IUptimeRobotMonitorResponse.ts: -------------------------------------------------------------------------------- 1 | import IUptimeRobotMonitor from "./IUptimeRobotMonitor"; 2 | import IUptimeRobotResponse from "./IUptimeRobotResponse"; 3 | 4 | export default interface IUptimeRobotMonitorResponse extends IUptimeRobotResponse { 5 | monitors: IUptimeRobotMonitor[] 6 | } -------------------------------------------------------------------------------- /packages/core/lib/Metric/ILatencyMetricRecord.ts: -------------------------------------------------------------------------------- 1 | import MetricRecord from "./MetricRecord"; 2 | 3 | export default interface ILatencyMetricRecord extends MetricRecord { 4 | /** 5 | * When the reading was taken 6 | */ 7 | time: Date 8 | 9 | /** 10 | * Latency (MS) 11 | */ 12 | value: number 13 | } -------------------------------------------------------------------------------- /packages/core/lib/Metric/IDowntimeMetricRecord.ts: -------------------------------------------------------------------------------- 1 | import MetricRecord from "./MetricRecord"; 2 | 3 | export default interface IDowntimeMetricRecord extends MetricRecord { 4 | /** 5 | * When the downtime started 6 | */ 7 | time: Date 8 | 9 | /** 10 | * Time in MS of how long it was down 11 | */ 12 | value: number 13 | } -------------------------------------------------------------------------------- /packages/core/lib/Builder/AttributeStorageBuilder.ts: -------------------------------------------------------------------------------- 1 | import { AttributeStorageType } from "../Util/AttributeStorage"; 2 | 3 | export default class AttributeStorageBuilder { 4 | protected _attributes: AttributeStorageType = {}; 5 | 6 | public attribute(key: string, value: any) { 7 | this._attributes[key] = value; 8 | return this; 9 | } 10 | } -------------------------------------------------------------------------------- /packages/react/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | 3 | listen 8080; 4 | 5 | location / { 6 | root /usr/share/nginx/html; 7 | index index.html index.htm; 8 | try_files $uri $uri/ /index.html; 9 | } 10 | 11 | error_page 500 502 503 504 /50x.html; 12 | 13 | location = /50x.html { 14 | root /usr/share/nginx/html; 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /packages/react/src/app/constants/FrontendOptions.ts: -------------------------------------------------------------------------------- 1 | export const ANONYMOUS = 'R_FRONTEND_ANONYMOUS'; 2 | export const COLLAPSED = 'R_FRONTEND_COLLAPSED'; 3 | export const COLLAPSIBLE = 'R_FRONTEND_COLLAPSIBLE'; 4 | 5 | // Graphs 6 | export const CHART_TITLE = 'R_FRONTEND_CHART_TITLE'; 7 | export const CHART_AVERAGE = 'R_FRONTEND_CHART_AVERAGE'; 8 | export const CHART_SERIES_NAME = 'R_FRONTEND_CHART_SERIES_NAME'; -------------------------------------------------------------------------------- /packages/core/lib/Builder/Builder.ts: -------------------------------------------------------------------------------- 1 | import { applyMixins } from "../Util/ApplyMixins" 2 | import { ComponentBuilderMixin } from "./ComponentBuilder" 3 | import { SeverityBuilderMixin } from "./SeverityBuilder" 4 | 5 | export class Builder {} 6 | 7 | export interface Builder extends ComponentBuilderMixin, SeverityBuilderMixin {} 8 | 9 | applyMixins(Builder, [ ComponentBuilderMixin, SeverityBuilderMixin ]) 10 | 11 | -------------------------------------------------------------------------------- /packages/core/lib/Util/IInjectStatusify.ts: -------------------------------------------------------------------------------- 1 | import Statusify from ".."; 2 | 3 | /** 4 | * Used to inject a statusify instance into anything statusify interacts with 5 | * Such as providers, calculators, and plugins 6 | */ 7 | export default interface IInjectStatusify { 8 | /** 9 | * Called upon instantiation of the entity. 10 | * @param statusify Statusify Instance 11 | */ 12 | inject(statusify: Statusify): void; 13 | } -------------------------------------------------------------------------------- /workspace.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "name": "core", 5 | "path": "packages\\core" 6 | }, 7 | { 8 | "name": "uptimerobot", 9 | "path": "packages\\uptimerobot" 10 | }, 11 | { 12 | "name": "loki", 13 | "path": "packages\\loki" 14 | }, 15 | { 16 | "name": "example", 17 | "path": "example" 18 | }, 19 | { 20 | "path": "packages\\react" 21 | } 22 | ], 23 | "settings": {} 24 | } -------------------------------------------------------------------------------- /packages/react/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /packages/react/src/app/hooks/useSeverityColor.ts: -------------------------------------------------------------------------------- 1 | import Severity from "@statusify/core/dist/Severity/Severity"; 2 | import { useLaminar } from "../contexts/LaminarContext"; 3 | 4 | export default function useSeverityColor(severity?: Severity, fallback: string = 'transparent') { 5 | const { severityColors } = useLaminar(); 6 | 7 | if(!severity) { 8 | return fallback; 9 | } 10 | 11 | return severityColors[severity.id] || fallback; 12 | } -------------------------------------------------------------------------------- /packages/react/src/app/interfaces/ISeverityTick.ts: -------------------------------------------------------------------------------- 1 | import IDowntimeMetricRecord from "@statusify/core/dist/Metric/IDowntimeMetricRecord"; 2 | import IIncident from "@statusify/core/dist/Incident/IIncident"; 3 | import Severity from "@statusify/core/dist/Severity/Severity"; 4 | 5 | export default interface ISeverityTick { 6 | date: Date; 7 | severity: Severity; 8 | relatedIncidents: IIncident[]; 9 | relatedDowntimes: IDowntimeMetricRecord[]; 10 | } -------------------------------------------------------------------------------- /packages/core/lib/Util/ApplyMixins.ts: -------------------------------------------------------------------------------- 1 | export function applyMixins(derivedCtor: any, constructors: any[]) { 2 | constructors.forEach((baseCtor) => { 3 | Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => { 4 | Object.defineProperty( 5 | derivedCtor.prototype, 6 | name, 7 | Object.getOwnPropertyDescriptor(baseCtor.prototype, name) || 8 | Object.create(null) 9 | ); 10 | }); 11 | }); 12 | } -------------------------------------------------------------------------------- /packages/react/src/app/utils/dayjs.ts: -------------------------------------------------------------------------------- 1 | import Duration from "dayjs/plugin/duration"; 2 | import IsBetweenPlugin from "dayjs/plugin/isBetween" 3 | import LocalizedFormat from "dayjs/plugin/localizedFormat"; 4 | import RelativeTime from "dayjs/plugin/relativeTime"; 5 | import dayjs from 'dayjs'; 6 | 7 | dayjs.extend(IsBetweenPlugin); 8 | dayjs.extend(LocalizedFormat); 9 | dayjs.extend(RelativeTime); 10 | dayjs.extend(Duration); 11 | 12 | export default dayjs; 13 | -------------------------------------------------------------------------------- /packages/react/src/index.tsx: -------------------------------------------------------------------------------- 1 | import reportWebVitals from './app/utils/reportWebVitals'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './pages/_app'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | 7 | // If you want to start measuring performance in your app, pass a function 8 | // to log results (for example: reportWebVitals(console.log)) 9 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 10 | reportWebVitals(); 11 | -------------------------------------------------------------------------------- /packages/react/src/app/theme/LaminarThemeOptions.ts: -------------------------------------------------------------------------------- 1 | import { ThemeOverride } from "@chakra-ui/react" 2 | import { ViewboxEntry } from "../contexts/ResponsiveViewboxContext"; 3 | 4 | export default interface LaminarThemeOptions extends ThemeOverride { 5 | // Maps a severity ID to a Chakra Color name 6 | severityColors: {[id: string]: string}; 7 | 8 | // Maps an downtime length (seconds) to a severity 9 | downtimeSeverities: {[id: number]: string}; 10 | 11 | viewboxes: ViewboxEntry[]; 12 | } -------------------------------------------------------------------------------- /packages/react/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ "react-app", "prettier" ], 3 | "rules": { 4 | "sort-imports": ["error", { 5 | "ignoreCase": false, 6 | "ignoreDeclarationSort": true, 7 | "ignoreMemberSort": false, 8 | "memberSyntaxSortOrder": ["none", "all", "multiple", "single"], 9 | "allowSeparatedGroups": false 10 | }], 11 | "array-bracket-spacing": ["error", "always", { "objectsInArrays": false }], 12 | "prettier/prettier": "off" 13 | }, 14 | "plugins": [ "prettier" ] 15 | } -------------------------------------------------------------------------------- /packages/tsconfig.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "sourceMap": true, 5 | "target": "esnext", 6 | "module": "esnext", 7 | "moduleResolution": "node", 8 | "allowJs": false, 9 | "strict": true, 10 | "experimentalDecorators": true, 11 | "resolveJsonModule": true, 12 | "esModuleInterop": true, 13 | "emitDecoratorMetadata": true, 14 | "removeComments": false, 15 | "allowSyntheticDefaultImports": true, 16 | "declaration": true, 17 | } 18 | } -------------------------------------------------------------------------------- /packages/react/src/app/hooks/useGlobalSeverity.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Severity from '@statusify/core/dist/Severity/Severity'; 3 | import { useStatusify } from '../contexts/StatusifyContext'; 4 | 5 | export default function useGlobalSeverity() { 6 | const [ severity, setSeverity ] = React.useState(); 7 | 8 | const statusify = useStatusify(); 9 | 10 | React.useEffect(() => { 11 | statusify?.getGlobalSeverity().then(setSeverity); 12 | }, [ statusify ]); 13 | 14 | return severity; 15 | } 16 | -------------------------------------------------------------------------------- /packages/react/src/app/utils/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /packages/react/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM node:16 as build 3 | 4 | WORKDIR /app 5 | ENV PATH /app/node_modules/.bin:$PATH 6 | 7 | COPY . ./ 8 | 9 | RUN yarn global add lerna 10 | RUN yarn bootstrap 11 | RUN yarn build 12 | 13 | WORKDIR /app/packages/react 14 | RUN yarn build 15 | 16 | # Production stage 17 | FROM nginx:stable-alpine 18 | COPY --from=build /app/packages/react/build /usr/share/nginx/html 19 | # new 20 | COPY packages/react/nginx.conf /etc/nginx/conf.d/default.conf 21 | EXPOSE 8080 22 | CMD ["nginx", "-g", "daemon off;"] -------------------------------------------------------------------------------- /packages/react/src/app/hooks/useMetricRange.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import dayjs from "../utils/dayjs"; 3 | import { useResponsiveViewbox } from "../contexts/ResponsiveViewboxContext"; 4 | 5 | export default function useMetricRange() { 6 | const viewbox = useResponsiveViewbox(); 7 | 8 | const range = React.useMemo(() => { 9 | return { 10 | start: dayjs().subtract(viewbox.days, 'days').startOf('day').toDate(), 11 | end: dayjs().endOf('day').toDate() 12 | } 13 | }, [ viewbox ]); 14 | 15 | return range; 16 | } -------------------------------------------------------------------------------- /packages/core/lib/Util/WorstSeverity.ts: -------------------------------------------------------------------------------- 1 | import Statusify from ".."; 2 | import Severity from "../Severity/Severity"; 3 | 4 | export default async function WorstSeverity(severities: Severity[], statusify: Statusify) { 5 | const allSeverities = await statusify.getSeverities() 6 | let currentWorst = 0 7 | 8 | for(const severity of severities) { 9 | const index = allSeverities.findIndex((cSev) => cSev.id === severity.id) 10 | currentWorst = (index > currentWorst) ? index : currentWorst 11 | } 12 | 13 | return allSeverities[currentWorst] 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/lib/Severity/IProvidesSeverities.ts: -------------------------------------------------------------------------------- 1 | import Statusify from ".."; 2 | import Severity from "./Severity"; 3 | 4 | export default interface IProvidesSeverities { 5 | /** 6 | * Gets all of the registered severities 7 | * @param statusify Statusify Core 8 | */ 9 | getSeverities(statusify: Statusify): Promise 10 | 11 | /** 12 | * Gets a specific severity that matches an id 13 | * @param statusify Statusify Core 14 | * @param id ID of the severity to get 15 | */ 16 | getSeverity(statusify: Statusify, id: string): Promise 17 | } -------------------------------------------------------------------------------- /packages/react/src/app/utils/i18n.ts: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs"; 2 | import i18n from "i18next" 3 | import { initReactI18next } from "react-i18next" 4 | 5 | i18n.use(initReactI18next) 6 | .init({ 7 | resources: { 8 | en: require('../../lang/en.json'), 9 | }, 10 | lng: 'en', 11 | fallbackLng: 'en', 12 | interpolation: { 13 | format: (value, format, lng) => { 14 | if(value instanceof Date) { 15 | return dayjs(value).format(format); 16 | } 17 | 18 | return value; 19 | } 20 | } 21 | }) 22 | 23 | export default i18n; -------------------------------------------------------------------------------- /packages/react/src/app/components/elements/componentGroup/ComponentGroupHeaderToggleIndicator.tsx: -------------------------------------------------------------------------------- 1 | import { AiOutlineClose, AiOutlinePlus } from "react-icons/ai" 2 | import Icon, { IconProps } from "@chakra-ui/icon" 3 | 4 | interface ComponentGroupHeaderToggleIndicatorProps extends IconProps { 5 | collapsed: boolean; 6 | } 7 | 8 | export default function ComponentGroupHeaderToggleIndicator({ collapsed, ...props }: ComponentGroupHeaderToggleIndicatorProps) { 9 | if(collapsed) { 10 | return 11 | } 12 | 13 | return 14 | } -------------------------------------------------------------------------------- /packages/core/lib/Incident/IIncidentUpdate.ts: -------------------------------------------------------------------------------- 1 | import Severity from "../Severity/Severity" 2 | 3 | export default interface IIncidentUpdate { 4 | /** 5 | * The severity this update makes the incident 6 | */ 7 | severity: Severity 8 | 9 | /** 10 | * The body of the incident message 11 | */ 12 | body: string 13 | 14 | /** 15 | * The status the incident message 16 | */ 17 | bodyStatus: string 18 | 19 | /** 20 | * When the incident update was created 21 | */ 22 | createdAt: Date 23 | 24 | /** 25 | * When the incident update was last updated at 26 | */ 27 | updatedAt: Date 28 | } -------------------------------------------------------------------------------- /packages/react/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /packages/uptimerobot/lib/Util/useSomewhatSingleton.ts: -------------------------------------------------------------------------------- 1 | export default function useSomewhatSingleton(resolver: (...args: any[]) => Promise) { 2 | let currentPromise: Promise | undefined = undefined; 3 | 4 | const fetch = (...args: any[]) => { 5 | // Check if there is a current promise being processed 6 | if(currentPromise !== undefined) { 7 | return currentPromise; 8 | } 9 | 10 | currentPromise = resolver(...args); 11 | 12 | // Reset the promise 13 | currentPromise.then(() => { 14 | currentPromise = undefined; 15 | }) 16 | 17 | return currentPromise; 18 | } 19 | 20 | return fetch; 21 | } -------------------------------------------------------------------------------- /packages/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": false, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /packages/react/src/app/hooks/useStatusifyEvent.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import StatusifyEvents from "@statusify/core/dist/Util/StatusifyEvents"; 3 | import { useStatusify } from "../contexts/StatusifyContext"; 4 | 5 | export default function useStatusifyEvent(event: keyof StatusifyEvents, callback: StatusifyEvents[typeof event]) { 6 | const statusify = useStatusify(); 7 | 8 | const memoizedCallback = React.useMemo(() => { 9 | return callback; 10 | }, [ callback ]); 11 | 12 | React.useEffect(() => { 13 | statusify.on(event, memoizedCallback); 14 | 15 | return () => { 16 | statusify.removeListener(event, memoizedCallback); 17 | } 18 | }, [ statusify, event, memoizedCallback ]); 19 | } -------------------------------------------------------------------------------- /packages/react/src/app/components/layouts/DefaultLayout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { Container, Flex, Link } from "@chakra-ui/layout"; 4 | 5 | import PageHeader from "../elements/global/header/PageHeader"; 6 | export interface DefaultLayoutProps { 7 | children?: React.ReactNode 8 | } 9 | 10 | export default function DefaultLayout({ children }: DefaultLayoutProps) { 11 | return ( 12 | 13 | 14 | 15 | {children} 16 | 17 | 18 | Powered by Statusify 19 | 20 | 21 | ) 22 | } -------------------------------------------------------------------------------- /packages/react/src/app/components/elements/componentGroup/ComponentGroupBody.tsx: -------------------------------------------------------------------------------- 1 | import Component from "../component/Component"; 2 | import { Collapse } from "@chakra-ui/transition"; 3 | import { Stack } from "@chakra-ui/layout"; 4 | import { useComponentGroup } from "../../../contexts/ComponentGroupContext" 5 | 6 | export default function ComponentGroupBody() { 7 | const [{ group, isAnonymous, isCollapsible, collapsed }] = useComponentGroup(); 8 | 9 | return ( 10 | 11 | 12 | { group.components.map((component, i) => ) } 13 | 14 | 15 | ) 16 | } -------------------------------------------------------------------------------- /packages/core/lib/Component/IProvidesComponents.ts: -------------------------------------------------------------------------------- 1 | import Statusify from ".."; 2 | import Component from "./Component"; 3 | import ComponentGroup from "./ComponentGroup"; 4 | 5 | export default interface IProvidesComponents { 6 | /** 7 | * Gets the component groups for the service 8 | * @param statusify Statusify core 9 | */ 10 | getComponentGroups(statusify: Statusify): Promise 11 | 12 | /** 13 | * Gets the components for the service 14 | * @param statusify Statusify core 15 | */ 16 | getComponents(statusify: Statusify): Promise 17 | 18 | /** 19 | * Gets a component by its ID 20 | * @param statusify Statusify core 21 | * @param id Component ID 22 | */ 23 | getComponent(statusify: Statusify, id: string): Promise 24 | } -------------------------------------------------------------------------------- /packages/uptimerobot/lib/Metric/GenericUptimeRobotMetric.ts: -------------------------------------------------------------------------------- 1 | import Metric, { MetricCParams, MetricType } from "@statusify/core/dist/Metric/Metric"; 2 | import MetricRecord from "@statusify/core/dist/Metric/MetricRecord"; 3 | import UptimeRobotCore from ".."; 4 | 5 | export interface UptimeRobotGenericMetricCParams extends Omit { 6 | monitorID: number 7 | } 8 | 9 | export abstract class GenericUptimeRobotMetric extends Metric { 10 | protected readonly urc: UptimeRobotCore; 11 | protected readonly monitorID: number; 12 | 13 | constructor(type: MetricType, urc: UptimeRobotCore, { monitorID, ...inherited }: UptimeRobotGenericMetricCParams) { 14 | super({type, ...inherited}); 15 | 16 | this.urc = urc; 17 | this.monitorID = monitorID; 18 | } 19 | } -------------------------------------------------------------------------------- /packages/react/src/app/theme/Theme.ts: -------------------------------------------------------------------------------- 1 | import LaminarThemeOptions from "./LaminarThemeOptions"; 2 | 3 | const theme: LaminarThemeOptions = { 4 | config: { 5 | initialColorMode: 'dark', 6 | useSystemColorMode: true 7 | }, 8 | 9 | viewboxes: [ 10 | {width: 1200, box: "0 0 448 40", days: 90}, 11 | {width: 900, box: "0 0 298 40", days: 60}, 12 | {width: 0, box: "0 0 148 40", days: 30} 13 | ], 14 | 15 | severityColors: { 16 | operational: 'green', 17 | partial: 'orange', 18 | minor: 'yellow', 19 | major: 'red', 20 | }, 21 | 22 | downtimeSeverities: { 23 | 0: 'operational', 24 | 1: 'partial', 25 | 300: 'minor', 26 | 1800: 'major' 27 | }, 28 | 29 | sizes: { 30 | container: { 31 | xl: '1140px' 32 | } 33 | } 34 | } 35 | 36 | export default theme; -------------------------------------------------------------------------------- /packages/react/src/app/contexts/StatusifyContext.tsx: -------------------------------------------------------------------------------- 1 | import Statusify from "@statusify/core"; 2 | import React from "react"; 3 | 4 | const StatusifyContext = React.createContext(null); 5 | 6 | export function StatusifyProvider({ children, statusify }: { children?: React.ReactNode, statusify: Statusify}) { 7 | const computedStatusify = React.useMemo(() => { 8 | return statusify; 9 | }, [ statusify ]); 10 | return ( 11 | 12 | {children} 13 | 14 | ) 15 | } 16 | 17 | export function useStatusify() { 18 | const context = React.useContext(StatusifyContext); 19 | 20 | if(context === undefined) { 21 | throw new Error('useStatusify must be used within a StatusifyProvider') 22 | } 23 | 24 | return context; 25 | } -------------------------------------------------------------------------------- /packages/core/lib/Util/AttributeStorage.ts: -------------------------------------------------------------------------------- 1 | export type AttributeStorageType = {[key: string]: any}; 2 | 3 | export default class AttributeStorage { 4 | /** 5 | * Custom attributes, useful for integration settings 6 | */ 7 | public readonly attributes: AttributeStorageType 8 | 9 | constructor(attributes?: AttributeStorageType) { 10 | this.attributes = attributes || {}; 11 | } 12 | 13 | // 14 | // Attribute modification 15 | // 16 | async setAttribute(key: string, value: any) { 17 | this.attributes[key] = value; 18 | } 19 | 20 | async getAttribute(key: string, def?: any) { 21 | return this.attributes[key] || def; 22 | } 23 | 24 | async removeAttribute(key: string) { 25 | delete this.attributes[key]; 26 | } 27 | 28 | async getAttributes() { 29 | return this.attributes; 30 | } 31 | } -------------------------------------------------------------------------------- /packages/react/src/app/hooks/useComponentInfo.ts: -------------------------------------------------------------------------------- 1 | import Component from "@statusify/core/dist/Component/Component"; 2 | import { MetricType } from "@statusify/core/dist/Metric/Metric"; 3 | import React from "react"; 4 | 5 | export interface ComponentInfo { 6 | hasDowntime: boolean 7 | hasLatency: boolean 8 | } 9 | 10 | export default function useComponentInfo(component: Component) { 11 | const [ state, setState ] = React.useState({ hasDowntime: false, hasLatency: false}); 12 | 13 | React.useEffect(() => { 14 | setState(state => ({ 15 | ...state, 16 | hasDowntime: component.metrics?.find(m => m.type === MetricType.DOWNTIME) !== undefined, 17 | hasLatency: component.metrics?.find(m => m.type === MetricType.LATENCY) !== undefined, 18 | })); 19 | }, [ component ]); 20 | 21 | return state; 22 | } -------------------------------------------------------------------------------- /packages/react/src/app/contexts/ComponentContext.tsx: -------------------------------------------------------------------------------- 1 | import Component from "@statusify/core/dist/Component/Component"; 2 | import React from "react"; 3 | 4 | const ComponentContext = React.createContext(undefined as any); 5 | 6 | export function ComponentProvider({ component, children }: {component: Component, children?: React.ReactNode}) { 7 | const value = React.useMemo(() => component, [ component ]); 8 | 9 | return ( 10 | 11 | {children} 12 | 13 | ); 14 | } 15 | 16 | export function useComponent() { 17 | const context = React.useContext(ComponentContext); 18 | 19 | if(context === undefined) { 20 | throw new Error('useComponent must be used within a ComponentProvider'); 21 | } 22 | 23 | return context; 24 | } 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /packages/uptimerobot/example.ts: -------------------------------------------------------------------------------- 1 | import IMetricRange from "@statusify/core/lib/Metric/IMetricRange"; 2 | import UptimeRobotCore from "./lib"; 3 | import UptimeRobotLatency from "./lib/Metric/UptimeRobotLatency"; 4 | import UptimeRobotDowntime from "./lib/Metric/UptimeRobotDowntime"; 5 | 6 | import * as moment from "moment" 7 | 8 | 9 | async function bootstrap() { 10 | const core = new UptimeRobotCore('ur488195-bd46852677deb5ca10988538'); 11 | const testMonitor = new UptimeRobotDowntime(core, { 12 | monitorID: 780071088, 13 | id: 'ur-latency', 14 | name: 'Latency' 15 | }) 16 | 17 | const range: IMetricRange = { 18 | start: moment().subtract(90, 'days').toDate(), 19 | end: moment().toDate() 20 | } 21 | 22 | console.log(await testMonitor.getPeriod(range)) 23 | console.log(await testMonitor.getAverage(range)) 24 | } 25 | 26 | bootstrap(); -------------------------------------------------------------------------------- /packages/core/lib/Severity/Severity.ts: -------------------------------------------------------------------------------- 1 | import AttributeStorage, { AttributeStorageType } from "../Util/AttributeStorage"; 2 | 3 | import Component from "../Component/Component" 4 | 5 | export interface SeverityCParams { 6 | id: string; 7 | name: string; 8 | attributes?: AttributeStorageType; 9 | } 10 | 11 | export default abstract class Severity extends AttributeStorage { 12 | /** 13 | * ID of the severity (For Lookups and Reference) 14 | */ 15 | public readonly id: string 16 | 17 | /** 18 | * Friendly Name 19 | */ 20 | public readonly name: string 21 | 22 | // 23 | // Constructor 24 | // 25 | constructor({ id, name, attributes }: SeverityCParams) { 26 | super(attributes); 27 | this.id = id; 28 | this.name = name; 29 | } 30 | 31 | /** 32 | * Checks if the severity has been achieved 33 | */ 34 | abstract achieved(component: Component): Promise 35 | } -------------------------------------------------------------------------------- /packages/core/lib/Severity/ICalculatesSeverities.ts: -------------------------------------------------------------------------------- 1 | import Component from "../Component/Component"; 2 | import ComponentGroup from "../Component/ComponentGroup"; 3 | import Severity from "./Severity"; 4 | import Statusify from ".."; 5 | 6 | export default interface ICalculatesSeverities { 7 | /** 8 | * Gets a global severity for all groups 9 | * @param statusify Statusify Core 10 | */ 11 | getGlobalSeverity(statusify: Statusify): Promise 12 | 13 | /** 14 | * Gets the severity for a group 15 | * @param group Group to get the severity for 16 | * @param statusify Statusify Core 17 | */ 18 | getSeverityForGroup(group: ComponentGroup, statusify: Statusify): Promise 19 | 20 | /** 21 | * Gets the severity for a component 22 | * @param component Component to get the severity for 23 | * @param statusify Statusify Core 24 | */ 25 | getSeverityForComponent(component: Component, statusify: Statusify): Promise 26 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Statusify 2 | 3 | Hey, thanks for taking the time to contribute! 4 | 5 | The following is a set of guidelines for contributing to Statusify and its packages. These are mostly guidelines and not rules. Use your best judgement are propose changes. 6 | 7 | #### Table of Contents 8 | [Styleguides](#styleguides) 9 | * [Git Commit Messages](#git-commit-messages) 10 | 11 | # Styleguides 12 | ## Git Commit Messages 13 | *This specification uses part of the [Angular commit message](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#commit) format* 14 | 15 | #### Commit Message Header 16 | ``` 17 | (): 18 | │ │ │ 19 | │ │ └─⫸ Summary in present tense. Not capitalized. No period at the end. 20 | │ │ 21 | │ └─⫸ The package being committed to, if multiple are affected then split the commits. 22 | │ 23 | └─⫸ Commit Type: build|ci|docs|feat|fix|perf|refactor|test 24 | ``` 25 | -------------------------------------------------------------------------------- /packages/react/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |
6 | 7 |

8 | React Frontend for Statusify 9 |

10 | 11 |

12 | 13 |

14 | 15 |

16 | Getting Started 17 |

18 | 19 | --- 20 | 21 | ## Features 22 | - Serverless Focused (Hash Router Support) 23 | - Latency and Downtime Metric Types Supported 24 | - Internationalization Support ([i18next](i18next.com)) 25 | - [Day.JS](https://day.js.org/) Formatting 26 | - Light and Dark mode Support 27 | 28 | ## Technologies 29 | - [Chakra UI](https://chakra-ui.com/) 30 | - [React Router](https://reactrouter.com/) 31 | ## Getting Started 32 | 33 | Development: 34 | ```bash 35 | $ yarn install 36 | $ yarn start 37 | ``` 38 | 39 | Production 40 | ```bash 41 | $ yarn install 42 | $ yarn build 43 | ``` 44 | 45 | -------------------------------------------------------------------------------- /packages/react/src/app/hooks/useIncidents.ts: -------------------------------------------------------------------------------- 1 | import IIncident from "@statusify/core/dist/Incident/IIncident"; 2 | import { IncidentsQuery } from "@statusify/core/dist/Incident/IProvidesIncidents"; 3 | import React from "react"; 4 | import { useStatusify } from "../contexts/StatusifyContext"; 5 | import useStatusifyEvent from "./useStatusifyEvent"; 6 | 7 | export default function useIncidents(query?: IncidentsQuery) { 8 | const statusify = useStatusify(); 9 | const [ incidents, setIncidents ] = React.useState([]); 10 | 11 | React.useEffect(() => { 12 | let isMounted = true; 13 | 14 | statusify.getIncidents(query).then((incidents) => { 15 | if(isMounted) { 16 | setIncidents(incidents); 17 | } 18 | }); 19 | 20 | return () => { 21 | isMounted = false; 22 | }; 23 | 24 | }, [ query, statusify ]); 25 | 26 | useStatusifyEvent('incidents::updated', () => { 27 | statusify.getIncidents(query).then(setIncidents); 28 | }) 29 | 30 | return incidents; 31 | } -------------------------------------------------------------------------------- /packages/react/src/app/hooks/useSeverity.ts: -------------------------------------------------------------------------------- 1 | import Component from "@statusify/core/dist/Component/Component"; 2 | import ComponentGroup from "@statusify/core/dist/Component/ComponentGroup"; 3 | import Severity from "@statusify/core/dist/Severity/Severity"; 4 | import React from "react"; 5 | import { useStatusify } from "../contexts/StatusifyContext"; 6 | 7 | export default function useSeverity(target: Component | ComponentGroup){ 8 | 9 | const statusify = useStatusify(); 10 | const [ severity, setSeverity ] = React.useState(); 11 | 12 | React.useEffect(() => { 13 | let isMounted = true; 14 | 15 | const promise = (target instanceof Component) ? statusify.getSeverityForComponent(target) : statusify.getSeverityForGroup(target); 16 | 17 | promise.then((severity) => { 18 | if(isMounted) { 19 | setSeverity(severity); 20 | } 21 | }) 22 | 23 | return () => { 24 | isMounted = false; 25 | }; 26 | }, [ target, statusify ]); 27 | 28 | return severity; 29 | } -------------------------------------------------------------------------------- /packages/loki/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@statusify/loki", 3 | "version": "1.1.1-alpha.0", 4 | "description": "Loki Incidents Provider", 5 | "author": "Legend ", 6 | "homepage": "https://github.com/LegendEffects/Statusify#readme", 7 | "license": "MIT", 8 | "main": "dist/index.js", 9 | "directories": { 10 | "lib": "lib", 11 | "test": "__tests__" 12 | }, 13 | "files": [ 14 | "dist" 15 | ], 16 | "publishConfig": { 17 | "access": "public" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/LegendEffects/Statusify.git" 22 | }, 23 | "scripts": { 24 | "test": "echo \"Error: run tests from root\" && exit 1" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/LegendEffects/Statusify/issues" 28 | }, 29 | "dependencies": { 30 | "@statusify/core": "^1.1.1-alpha.0", 31 | "lokijs": "^1.5.11", 32 | "path": "^0.12.7" 33 | }, 34 | "devDependencies": { 35 | "@types/lokijs": "^1.5.3", 36 | "@types/node": "^14.14.22" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/react/src/app/components/elements/util/CirclePulse.tsx: -------------------------------------------------------------------------------- 1 | import { Circle, SquareProps } from "@chakra-ui/layout"; 2 | import { ThemeTypings } from "@chakra-ui/styled-system"; 3 | import { keyframes } from "@chakra-ui/system"; 4 | 5 | export interface CirclePulseProps extends SquareProps { 6 | colorScheme: ThemeTypings["colorSchemes"] | (string & {}) 7 | duration?: number 8 | frozen?: boolean 9 | } 10 | 11 | const pulseAnimation = keyframes` 12 | from { 13 | transform: scale(0); 14 | opacity: 1; 15 | } 16 | 17 | to { 18 | transform: scale(1); 19 | opacity: 0; 20 | } 21 | ` 22 | 23 | export default function CirclePulse({ colorScheme, duration, frozen, ...props }: CirclePulseProps) { 24 | return ( 25 | 29 | {!frozen && ( 30 | 36 | )} 37 | 38 | ) 39 | } -------------------------------------------------------------------------------- /packages/uptimerobot/__tests__/uptimerobot.test.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import IMetricRange from '@statusify/core/lib/Metric/IMetricRange'; 3 | import moment from 'moment'; 4 | import UptimeRobotCore from '../lib' 5 | import UptimeRobotLatency from '../lib/Metric/UptimeRobotLatency' 6 | 7 | const core = new UptimeRobotCore('ur488195-bd46852677deb5ca10988538'); 8 | 9 | describe('UptimeRobot/Latency', () => { 10 | const testMonitor = new UptimeRobotLatency(core, { 11 | monitorID: 780071088, 12 | id: 'ur-latency', 13 | name: 'Latency' 14 | }) 15 | 16 | // test('Fetches Downtimes', async () => { 17 | // // Get 30 days worth 18 | // const ranges: IMetricRange[] = Array(30).map((_, i) => { 19 | 20 | // const m = moment().subtract(i, 'days'); 21 | // return { 22 | // start: m.startOf('day').toDate(), 23 | // end: m.endOf('day').toDate() 24 | // } 25 | // }) 26 | 27 | // console.log(ranges) 28 | 29 | // await testMonitor.fetchPeriods(ranges) 30 | // }) 31 | }); 32 | -------------------------------------------------------------------------------- /packages/react/src/app/components/elements/componentGroup/ComponentGroup.tsx: -------------------------------------------------------------------------------- 1 | import { Box, BoxProps } from "@chakra-ui/layout"; 2 | 3 | import ComponentGroupBody from "./ComponentGroupBody"; 4 | import ComponentGroupHeader from "./ComponentGroupHeader"; 5 | import { ComponentGroupProvider, useComponentGroup } from "../../../contexts/ComponentGroupContext"; 6 | import StatusifyComponentGroup from "@statusify/core/dist/Component/ComponentGroup"; 7 | 8 | export interface ComponentGroupProps extends BoxProps { 9 | group: StatusifyComponentGroup 10 | } 11 | 12 | export const ComponentGroupDisplay: React.FC = ({ ...props }) => { 13 | const [{ isCollapsible, isAnonymous }] = useComponentGroup(); 14 | 15 | return ( 16 | 17 | {(isCollapsible || !isAnonymous) && } 18 | 19 | 20 | ) 21 | } 22 | 23 | export default function ComponentGroup({ group, ...props }: ComponentGroupProps) { 24 | return ( 25 | 26 | 27 | 28 | ) 29 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-present LegendEffects and Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /example/incidents/2021-09-01-test-incident.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "test-incident", 3 | "name": "Test Incident", 4 | 5 | "severity": "major", 6 | "components": [ 7 | "component-1", 8 | "component-3" 9 | ], 10 | 11 | "body": "We have identified an issue with one of our upstream providers. A loss of power at a datacenter is causing a disruption with our external media proxies. External images are not loading at this time.", 12 | "body_status": "Investigating", 13 | 14 | "updates": [ 15 | { 16 | "severity": "minor", 17 | "body_status": "Monitoring", 18 | "body": "We've restored functionality to all connected bots, and will continue to monitor.", 19 | "created_at": "2021-01-09T02:44:02.163Z", 20 | "updated_at": "2021-01-09T02:44:02.163Z" 21 | }, 22 | 23 | { 24 | "severity": "operational", 25 | "body_status": "Resolved", 26 | "body": "This incident has been resolved.", 27 | "created_at": "2021-01-09T03:03:19.639Z", 28 | "updated_at": "2021-01-09T03:03:19.639Z" 29 | } 30 | ], 31 | 32 | "created_at": "2021-01-09T02:29:04.153Z", 33 | "updated_at": "2021-01-09T02:29:04.153Z" 34 | } -------------------------------------------------------------------------------- /packages/react/src/app/components/elements/global/StatusBanner.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, FlexProps, Heading } from "@chakra-ui/layout"; 2 | import { useTranslation } from "react-i18next"; 3 | import useGlobalSeverity from "../../../hooks/useGlobalSeverity" 4 | import useSeverityColor from "../../../hooks/useSeverityColor"; 5 | import CirclePulse from "../util/CirclePulse"; 6 | 7 | export interface StatusBannerProps extends FlexProps { } 8 | 9 | export default function StatusBanner({ ...props }: StatusBannerProps) { 10 | const { t } = useTranslation(); 11 | const severity = useGlobalSeverity(); 12 | const severityColor = useSeverityColor(severity, 'gray'); 13 | 14 | return ( 15 | 24 | 29 | 30 | 31 | { severity ? t(`statusBanner.${severity.id}`) : t('polling') } 32 | 33 | 34 | ) 35 | } -------------------------------------------------------------------------------- /packages/uptimerobot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@statusify/uptimerobot", 3 | "version": "1.1.1-alpha.0", 4 | "description": "UptimeRobot Statusify Integration", 5 | "author": "Legend ", 6 | "homepage": "https://github.com/LegendEffects/Statusify#readme", 7 | "license": ",OT", 8 | "main": "dist/index.js", 9 | "directories": { 10 | "lib": "lib", 11 | "test": "__tests__" 12 | }, 13 | "files": [ 14 | "dist" 15 | ], 16 | "publishConfig": { 17 | "access": "public" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/LegendEffects/Statusify.git" 22 | }, 23 | "scripts": { 24 | "test": "jest" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/LegendEffects/Statusify/issues" 28 | }, 29 | "dependencies": { 30 | "@statusify/core": "^1.1.1-alpha.0", 31 | "axios": "^0.21.1", 32 | "dayjs": "^1.10.4" 33 | }, 34 | "devDependencies": { 35 | "@babel/core": "^7.12.10", 36 | "@babel/preset-env": "^7.12.11", 37 | "@babel/preset-typescript": "^7.12.7", 38 | "@types/jest": "^26.0.20", 39 | "babel-jest": "^26.6.3", 40 | "jest": "^26.6.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/core/lib/Component/Component.ts: -------------------------------------------------------------------------------- 1 | import AttributeStorage, { AttributeStorageType } from "../Util/AttributeStorage"; 2 | 3 | import Metric from "../Metric/Metric"; 4 | import MetricRecord from "../Metric/MetricRecord"; 5 | 6 | export interface ComponentCParams { 7 | id: string 8 | name?: string 9 | description?: string 10 | metrics?: Metric[]; 11 | attributes?: AttributeStorageType; 12 | } 13 | 14 | export default class Component extends AttributeStorage { 15 | /** 16 | * ID of the component 17 | */ 18 | public readonly id: string; 19 | 20 | /** 21 | * Name of the component 22 | */ 23 | public readonly name?: string; 24 | 25 | /** 26 | * Description of the component 27 | */ 28 | public readonly description?: string; 29 | 30 | /** 31 | * Metrics of the component 32 | */ 33 | public readonly metrics?: Metric[]; 34 | 35 | 36 | // 37 | // Constructor 38 | // 39 | constructor({ id, name, description, metrics, attributes }: ComponentCParams) { 40 | super(attributes); 41 | 42 | this.id = id; 43 | this.name = name; 44 | this.description = description; 45 | this.metrics = metrics; 46 | } 47 | } -------------------------------------------------------------------------------- /packages/react/src/app/components/elements/global/header/PageHeader.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Container, Flex, Heading, Text } from "@chakra-ui/layout"; 2 | import { useTranslation } from "react-i18next"; 3 | import { useAutoRefresh } from "../../../../contexts/AutoRefreshContext"; 4 | 5 | export interface PageHeaderProps { 6 | lastUpdated: Date 7 | } 8 | 9 | export default function PageHeader() { 10 | const { t } = useTranslation(); 11 | const [{ lastUpdate }] = useAutoRefresh(); 12 | 13 | return ( 14 | 20 | 21 | 22 | 23 | {t('header.title')} 24 | 25 | 26 | 31 | 32 | {t((lastUpdate === null) ? 'header.polling' : 'header.lastUpdated', { date: lastUpdate })} 33 | 34 | 35 | 36 | 37 | 38 | ) 39 | } -------------------------------------------------------------------------------- /packages/react/src/app/hooks/useUptimePercentage.ts: -------------------------------------------------------------------------------- 1 | import Component from "@statusify/core/dist/Component/Component"; 2 | import IMetricRange from "@statusify/core/dist/Metric/IMetricRange"; 3 | import { MetricType } from "@statusify/core/dist/Metric/Metric"; 4 | import React from "react"; 5 | import dayjs from "../utils/dayjs"; 6 | 7 | export default function useUptimePercentage(range: IMetricRange, component: Component) { 8 | const [ percent, setPercent ] = React.useState(1); 9 | 10 | React.useMemo(() => { 11 | new Promise(async (resolve, reject) => { 12 | 13 | const downtimeMetric = component.metrics?.find(m => m.type === MetricType.DOWNTIME); 14 | if(downtimeMetric === undefined) { 15 | return reject(); 16 | } 17 | 18 | const downtimes = await downtimeMetric.getPeriod(range); 19 | 20 | const totalTimeRange = dayjs(range.end).diff(range.start, 'milliseconds') 21 | const totalDowntime = downtimes 22 | .map(v => v.value) 23 | .reduce((a, b) => a + b, 0) 24 | 25 | resolve((totalTimeRange - totalDowntime) / totalTimeRange ) 26 | }).then(setPercent).catch(() => {}); 27 | }, [ range, component ]) 28 | 29 | return percent; 30 | } -------------------------------------------------------------------------------- /packages/react/src/app/hooks/useComponentMetric.ts: -------------------------------------------------------------------------------- 1 | import Metric, { MetricType } from "@statusify/core/dist/Metric/Metric"; 2 | import MetricRecord from "@statusify/core/dist/Metric/MetricRecord"; 3 | import { AttributeStorageType } from "@statusify/core/dist/Util/AttributeStorage"; 4 | import React from "react"; 5 | import { useComponent } from "../contexts/ComponentContext"; 6 | 7 | export default function useComponentMetric(type: MetricType): undefined | [ Metric, AttributeStorageType ] { 8 | const component = useComponent(); 9 | 10 | const [ attributes, setAttributes ] = React.useState([]); 11 | 12 | const metric = React.useMemo(() => { 13 | if(!component || !component.metrics) { 14 | return undefined; 15 | } 16 | 17 | return component.metrics.find(m => m.type === type); 18 | }, [ component, type ]); 19 | 20 | React.useEffect(() => { 21 | let isMounted = true; 22 | 23 | metric.getAttributes().then((attributes) => { 24 | if(isMounted) { 25 | setAttributes(attributes); 26 | } 27 | }); 28 | 29 | return () => { 30 | isMounted = false; 31 | } 32 | }, [ metric ]); 33 | 34 | return [ 35 | (metric as Metric), 36 | attributes 37 | ]; 38 | } -------------------------------------------------------------------------------- /packages/core/lib/Builder/SeverityBuilder.ts: -------------------------------------------------------------------------------- 1 | import AttributeStorageBuilder from "./AttributeStorageBuilder" 2 | import IProvidesSeverities from "../Severity/IProvidesSeverities" 3 | import Severity from "../Severity/Severity" 4 | import Statusify from ".." 5 | 6 | export class SeverityBuilderMixin implements IProvidesSeverities { 7 | _severities: SeverityBuilder[] = [] 8 | 9 | severities(builders: SeverityBuilder[]) { 10 | this._severities = builders 11 | return this 12 | } 13 | 14 | async getSeverities(_statusify: Statusify): Promise { 15 | return this._severities.map(s => s.build()) 16 | } 17 | 18 | async getSeverity(statusify: Statusify, id: string): Promise { 19 | const found = (await this.getSeverities(statusify)).find(s => s.id === id) 20 | return (found === undefined) ? null : found 21 | } 22 | } 23 | 24 | export abstract class SeverityBuilder extends AttributeStorageBuilder { 25 | protected _name: string = ''; 26 | protected _id: string; 27 | 28 | constructor(id: string) { 29 | super(); 30 | this._id = id 31 | } 32 | 33 | public name(name: string) { 34 | this._name = name 35 | return this 36 | } 37 | 38 | /** 39 | * @ignore 40 | */ 41 | public abstract build(): Severity 42 | } -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@statusify/core", 3 | "version": "1.1.1-alpha.0", 4 | "description": "The core of Statusify", 5 | "keywords": [ 6 | "status", 7 | "platform", 8 | "agnostic" 9 | ], 10 | "author": "Legend ", 11 | "homepage": "https://github.com/LegendEffects/Statusify#readme", 12 | "license": "ISC", 13 | "main": "dist/index.js", 14 | "directories": { 15 | "lib": "lib", 16 | "test": "__tests__" 17 | }, 18 | "files": [ 19 | "dist" 20 | ], 21 | "publishConfig": { 22 | "access": "public" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/LegendEffects/Statusify.git" 27 | }, 28 | "scripts": { 29 | "test": "jest", 30 | "main": "ts-node lib/index.ts" 31 | }, 32 | "bugs": { 33 | "url": "https://github.com/LegendEffects/Statusify/issues" 34 | }, 35 | "dependencies": { 36 | "@types/events": "^3.0.0", 37 | "axios": "^0.21.1", 38 | "events": "^3.2.0", 39 | "tiny-typed-emitter": "^2.0.3" 40 | }, 41 | "devDependencies": { 42 | "@babel/core": "^7.12.10", 43 | "@babel/preset-env": "^7.12.11", 44 | "@babel/preset-typescript": "^7.12.7", 45 | "@types/jest": "^26.0.20", 46 | "babel-jest": "^26.6.3", 47 | "jest": "^26.6.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/react/src/app/components/elements/incident/IncidentUpdate.tsx: -------------------------------------------------------------------------------- 1 | import {Box, Circle, Flex, Heading, Text} from '@chakra-ui/layout'; 2 | import IIncidentUpdate from '@statusify/core/dist/Incident/IIncidentUpdate'; 3 | import dayjs from 'dayjs'; 4 | import React from 'react'; 5 | import { useTranslation } from 'react-i18next'; 6 | import useSeverityColor from '../../../hooks/useSeverityColor'; 7 | 8 | export interface IncidentUpdateProps { 9 | update: IIncidentUpdate; 10 | } 11 | 12 | const IncidentUpdate: React.FC = ({update}) => { 13 | const severityColor = useSeverityColor(update.severity); 14 | const { t } = useTranslation(); 15 | 16 | return ( 17 | <> 18 | 19 | 20 | 21 | 22 | 23 | {update.bodyStatus} 24 | 25 | 26 | 27 | 28 | 29 | 30 | {update.body} 31 | 32 | 35 | 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | 43 | export default IncidentUpdate; -------------------------------------------------------------------------------- /packages/core/lib/Incident/IProvidesIncidents.ts: -------------------------------------------------------------------------------- 1 | import Component from "../Component/Component"; 2 | import IIncident from "./IIncident"; 3 | import Statusify from ".."; 4 | 5 | export type DateQuery = null | { 6 | after?: Date 7 | before?: Date 8 | } 9 | 10 | export type IncidentsQuery = { 11 | createdAt?: DateQuery, 12 | updatedAt?: DateQuery, 13 | resolvedAt?: DateQuery | null, 14 | 15 | limit?: number, 16 | offset?: number, 17 | component?: string, 18 | 19 | id?: string 20 | } 21 | 22 | export default interface IProvidesIncidents { 23 | /** 24 | * Gets all incidents matching the query 25 | * @param statusify Statusify Core 26 | * @param query Query 27 | */ 28 | getIncidents(statusify: Statusify, query?: IncidentsQuery): Promise 29 | 30 | /** 31 | * Get all incidents for a component matching the query 32 | * @param statusify Statusify Core 33 | * @param component Component instance 34 | * @param query Query to match 35 | */ 36 | getIncidentsFor(statusify: Statusify, component: Component, query?: IncidentsQuery): Promise 37 | 38 | /** 39 | * Gets a specific incident matching an ID 40 | * @param statusify Statusify Core 41 | * @param id Incident ID to get 42 | */ 43 | getIncident(statusify: Statusify, id: string): Promise 44 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "statusify", 3 | "description": "A library connecting status platforms into a single glorious integration", 4 | "version": "1.0.0", 5 | "license": "MIT", 6 | "author": { 7 | "name": "LegendEffects", 8 | "url": "https://github.com/legendeffects" 9 | }, 10 | "private": true, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/LegendEffects/Statusify.git" 14 | }, 15 | "scripts": { 16 | "clean:build": "rimraf packages/*/dist packages/**/tsconfig.tsbuildinfo", 17 | "clean:install": "lerna clean --ci && rimraf node_modules", 18 | "bootstrap": "yarn install --production=false && lerna bootstrap", 19 | "setup:react": "yarn build && (cd packages/core && yarn link) && (cd ../../packages/uptimerobot && yarn link @statusify/core && yarn link) && (cd ../../packages/react && yarn link @statusify/core && yarn link @statusify/uptimerobot)", 20 | "build": "yarn clean:build && tsc -b packages/core packages/loki packages/uptimerobot", 21 | "dev": "yarn clean:build && tsc -b packages/core packages/loki packages/uptimerobot --watch", 22 | "release": "yarn build && lerna publish --force-publish" 23 | }, 24 | "devDependencies": { 25 | "lerna": "^3.22.1", 26 | "prettier": "^2.2.1", 27 | "rimraf": "^3.0.2", 28 | "typescript": "^4.2.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/react/src/app/contexts/LaminarContext.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { ChakraProvider, ChakraTheme, ColorModeScript, extendTheme } from "@chakra-ui/react"; 4 | 5 | import LaminarThemeOptions from "../theme/LaminarThemeOptions"; 6 | import { ResponsiveViewboxProvider } from "./ResponsiveViewboxContext"; 7 | 8 | const LaminarContext = React.createContext(null as any); 9 | 10 | export function LaminarProvider({ children, theme }: { children?: React.ReactNode, theme: LaminarThemeOptions }) { 11 | const chakraTheme = React.useMemo(() => { 12 | return extendTheme(theme) as ChakraTheme 13 | }, [ theme ]); 14 | 15 | return ( 16 | <> 17 | 18 | 19 | 20 | 21 | {children} 22 | 23 | 24 | 25 | 26 | ) 27 | } 28 | 29 | export function useLaminar() { 30 | const context = React.useContext(LaminarContext); 31 | 32 | if(context === undefined) { 33 | throw new Error('useLaminar must be used within a LaminarProvider'); 34 | } 35 | 36 | return context; 37 | } -------------------------------------------------------------------------------- /packages/react/src/app/components/elements/component/Component.tsx: -------------------------------------------------------------------------------- 1 | import StatusifyComponent from "@statusify/core/dist/Component/Component"; 2 | import ComponentHeader from "./ComponentHeader"; 3 | import { ComponentProvider } from "../../../contexts/ComponentContext"; 4 | import { Flex } from "@chakra-ui/layout"; 5 | import ComponentTickChart from "./metrics/tickChart/ComponentTickChart"; 6 | import ComponentLatencyGraph from "./metrics/latency/ComponentLatencyGraph"; 7 | import useComponentInfo from "../../../hooks/useComponentInfo"; 8 | 9 | export interface ComponentProps { 10 | component: StatusifyComponent; 11 | } 12 | 13 | export default function Component({ component }: ComponentProps) { 14 | const info = useComponentInfo(component); 15 | 16 | return ( 17 | 18 | 24 | 30 | 31 | 32 | 33 | 34 | {info.hasLatency && } 35 | 36 | 37 | ) 38 | } -------------------------------------------------------------------------------- /packages/core/lib/Severity/RunnableSeverity.ts: -------------------------------------------------------------------------------- 1 | import Severity, { SeverityCParams } from "./Severity"; 2 | 3 | import Component from "../Component/Component"; 4 | import { SeverityBuilder } from "../Builder"; 5 | 6 | export type SeverityRunnable = (component: Component) => Promise 7 | 8 | export class RunnableSeverity extends Severity { 9 | private runnable: SeverityRunnable 10 | 11 | constructor({ id, name, runnable }: { runnable: SeverityRunnable } & SeverityCParams) { 12 | super({ id, name }) 13 | this.runnable = runnable 14 | } 15 | 16 | achieved(component: Component): Promise { 17 | return this.runnable(component) 18 | } 19 | } 20 | 21 | export class RunnableSeverityBuilder extends SeverityBuilder { 22 | private _runnable!: SeverityRunnable; 23 | 24 | public runnable(runnable: SeverityRunnable) { 25 | this._runnable = runnable 26 | return this 27 | } 28 | 29 | public build(): Severity { 30 | if(this.runnable === undefined) { 31 | throw new Error('RunnableSeverity: No runnable provided.'); 32 | } 33 | 34 | return new RunnableSeverity({ 35 | id: this._id, 36 | name: this._name, 37 | attributes: this._attributes, 38 | runnable: this._runnable, 39 | }) 40 | } 41 | } 42 | 43 | export function runnableSeverity(id: string) { 44 | return new RunnableSeverityBuilder(id) 45 | } -------------------------------------------------------------------------------- /packages/core/lib/Incident/IIncident.ts: -------------------------------------------------------------------------------- 1 | import Component from "../Component/Component"; 2 | import IIncidentUpdate from "./IIncidentUpdate"; 3 | import Severity from "../Severity/Severity"; 4 | 5 | export default interface IIncident { 6 | /** 7 | * Identifier for the incident 8 | */ 9 | id: string 10 | 11 | /** 12 | * The name of the incident 13 | */ 14 | name: string 15 | 16 | /** 17 | * The body of the incident message 18 | */ 19 | body: string 20 | 21 | /** 22 | * The status the incident message 23 | */ 24 | bodyStatus: string 25 | 26 | /** 27 | * Updates for the incident 28 | */ 29 | updates: IIncidentUpdate[] 30 | 31 | /** 32 | * Severity that the incident inflicts 33 | */ 34 | severity: Severity 35 | 36 | /** 37 | * When the incident was resolved (undefined if unresolved) 38 | */ 39 | resolvedAt?: Date 40 | 41 | /** 42 | * Components that are affected by the incident 43 | */ 44 | components: Component[] 45 | 46 | /** 47 | * When the incident is scheduled to be created 48 | */ 49 | scheduledFor?: Date 50 | 51 | /** 52 | * When the incident is scheduled to be resolved 53 | */ 54 | scheduledUntil?: Date 55 | 56 | /** 57 | * When the incident was created 58 | */ 59 | createdAt: Date 60 | 61 | /** 62 | * When the incident was last updated at 63 | */ 64 | updatedAt: Date 65 | } -------------------------------------------------------------------------------- /packages/core/lib/Severity/SeverityMultiplexer.ts: -------------------------------------------------------------------------------- 1 | import lib from ".."; 2 | import Component from "../Component/Component"; 3 | import ComponentGroup from "../Component/ComponentGroup"; 4 | import WorstSeverity from "../Util/WorstSeverity"; 5 | import ICalculatesSeverities from "./ICalculatesSeverities"; 6 | import Severity from "./Severity"; 7 | 8 | /** 9 | * Combines multiple severity calculators into one 10 | */ 11 | export default class CombinedSeverityCalculator implements ICalculatesSeverities { 12 | private calculators: ICalculatesSeverities[]; 13 | 14 | constructor(calculators: ICalculatesSeverities[]) { 15 | this.calculators = calculators; 16 | } 17 | 18 | async getGlobalSeverity(statusify: lib): Promise { 19 | return WorstSeverity( 20 | await Promise.all(this.calculators.map(c => c.getGlobalSeverity(statusify))), 21 | statusify 22 | ) 23 | } 24 | 25 | async getSeverityForGroup(group: ComponentGroup, statusify: lib): Promise { 26 | return WorstSeverity( 27 | await Promise.all(this.calculators.map(c => c.getSeverityForGroup(group, statusify))), 28 | statusify 29 | ) 30 | } 31 | 32 | async getSeverityForComponent(component: Component, statusify: lib): Promise { 33 | return WorstSeverity( 34 | await Promise.all(this.calculators.map(c => c.getSeverityForComponent(component, statusify))), 35 | statusify 36 | ) 37 | } 38 | } -------------------------------------------------------------------------------- /packages/react/src/app/components/elements/incident/IncidentBannerUpdate.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { Box, Circle, Flex, Grid } from "@chakra-ui/layout"; 4 | 5 | import IIncidentUpdate from "@statusify/core/dist/Incident/IIncidentUpdate"; 6 | import dayjs from "../../../utils/dayjs"; 7 | import { useLaminar } from "../../../contexts/LaminarContext"; 8 | import { useTranslation } from "react-i18next"; 9 | 10 | interface IncidentBannerUpdateProps { 11 | update: IIncidentUpdate; 12 | } 13 | 14 | const IncidentBannerUpdate: React.FC = ({ update }) => { 15 | const { severityColors } = useLaminar(); 16 | const { t } = useTranslation(); 17 | 18 | return ( 19 | 23 | 24 | 25 | {dayjs(update.createdAt).format(t("incidents.banner.timeFormat"))} 26 | 27 | 28 | {dayjs(update.createdAt).format(t("incidents.banner.dateFormat"))} 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {update.body} 38 | 39 | 40 | ); 41 | } 42 | 43 | export default IncidentBannerUpdate; -------------------------------------------------------------------------------- /packages/react/src/app/contexts/ResponsiveViewboxContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | 3 | export interface ViewboxEntry { 4 | width: number; 5 | days: number; 6 | box: string; 7 | } 8 | 9 | export interface ResponsiveViewboxProviderProps { 10 | children?: React.ReactNode; 11 | viewboxes: ViewboxEntry[]; 12 | } 13 | 14 | 15 | const ResponsiveViewboxContext = React.createContext(undefined); 16 | 17 | export function ResponsiveViewboxProvider({ children, viewboxes }: ResponsiveViewboxProviderProps) { 18 | const [ viewbox, setViewbox ] = React.useState(); 19 | 20 | useEffect(() => { 21 | const handleResize = () => { 22 | setViewbox(viewboxes.find(v => window.innerWidth >= v.width) || viewboxes[0]); 23 | } 24 | 25 | handleResize(); 26 | window.addEventListener('resize', handleResize); 27 | return () => { 28 | window.removeEventListener('resize', handleResize); 29 | } 30 | }, [ viewboxes ]); 31 | 32 | return ( 33 | 34 | {children} 35 | 36 | ) 37 | } 38 | 39 | export function useResponsiveViewbox() { 40 | const context = React.useContext(ResponsiveViewboxContext); 41 | 42 | if(context === undefined) { 43 | throw new Error('useResponsiveViewbox must be used within a ResponsiveViewboxProvider'); 44 | } 45 | 46 | return context; 47 | } 48 | 49 | -------------------------------------------------------------------------------- /packages/core/lib/Component/ComponentGroup.ts: -------------------------------------------------------------------------------- 1 | import AttributeStorage, { AttributeStorageType } from "../Util/AttributeStorage"; 2 | 3 | import Component from "./Component" 4 | 5 | export interface ComponentGroupCParams { 6 | name?: string; 7 | description?: string; 8 | attributes?: AttributeStorageType; 9 | } 10 | 11 | export default class ComponentGroup extends AttributeStorage { 12 | /** 13 | * Name of the group 14 | */ 15 | public readonly name?: string 16 | 17 | /** 18 | * Optional description of the group 19 | */ 20 | public readonly description?: string 21 | 22 | /** 23 | * Components in the group 24 | */ 25 | public readonly components: Component[] = [] 26 | 27 | // 28 | // Constructor 29 | // 30 | constructor({ name, description, attributes }: ComponentGroupCParams) { 31 | super(attributes); 32 | 33 | this.name = name; 34 | this.description = description; 35 | } 36 | 37 | // 38 | // Component Registration 39 | // 40 | async addComponent(component: Component) { 41 | this.components.push(component); 42 | } 43 | 44 | async addComponents(components: Component[]) { 45 | for(const component of components) { 46 | this.addComponent(component); 47 | } 48 | } 49 | 50 | // 51 | // Getters 52 | // 53 | async getName() { 54 | return this.name; 55 | } 56 | 57 | async getDescription() { 58 | return this.description; 59 | } 60 | 61 | async getComponents() { 62 | return this.components; 63 | } 64 | } -------------------------------------------------------------------------------- /packages/react/src/lang/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "translation": { 3 | "header": { 4 | "title": "Status Page", 5 | "status": "Service Status", 6 | "polling": "Fetching...", 7 | "lastUpdated": "Last Updated: {{date, LLL}}" 8 | }, 9 | 10 | "statusBanner": { 11 | "operational": "All Services Operational", 12 | "partial": "Partial Outages", 13 | "minor": "Minor Outages", 14 | "major": "Major Outages" 15 | }, 16 | 17 | "components": { 18 | "group": { 19 | "overallStatusTooltip": "Groups take on the status of their most degraded child component. Click to see the status of individual children." 20 | }, 21 | 22 | "metrics": { 23 | "tickChart": { 24 | "dateFormat": "ll", 25 | "noIncidents": "No incidents reported on this day.", 26 | "noDowntime": "No downtime reported on this day.", 27 | "downtime": "{{duration}} of downtime." 28 | }, 29 | "latency": { 30 | "seriesName": "Latency (ms)", 31 | "title": "Latency", 32 | "average": "Averaging: {{average}}ms" 33 | } 34 | } 35 | }, 36 | 37 | "incidents": { 38 | "banner": { 39 | "timeFormat": "HH:mm", 40 | "dateFormat": "MMM DD" 41 | }, 42 | 43 | "overallFormat": "LLL", 44 | "updateFormat": "LLL", 45 | "resolvedAfter": "Resolved after {{duration}} of downtime.", 46 | 47 | "badge": { 48 | "open": "Unresolved", 49 | "closed": "Resolved" 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /packages/react/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../app/utils/i18n"; 2 | 3 | import StatusifyInstance from "../StatusifyInstance"; 4 | import theme from "../app/theme/Theme"; 5 | import * as React from "react"; 6 | import Home from "."; 7 | 8 | import { HashRouter, Route, Switch } from "react-router-dom"; 9 | import { LaminarProvider } from "../app/contexts/LaminarContext"; 10 | import { StatusifyProvider } from "../app/contexts/StatusifyContext"; 11 | import IncidentPage from "./IncidentPage"; 12 | import { AutoRefreshProvider } from "../app/contexts/AutoRefreshContext"; 13 | 14 | export default function App() { 15 | const [ statusify, setStatusify ] = React.useState(StatusifyInstance); 16 | const [ refresh, setRefresh ] = React.useState(false); 17 | 18 | React.useEffect(() => { 19 | if(refresh === true) { 20 | setStatusify(StatusifyInstance); 21 | } 22 | }, [ refresh ]) 23 | 24 | return ( 25 | 26 | 27 | 28 | setRefresh(true)} > 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ) 47 | } -------------------------------------------------------------------------------- /packages/core/lib/Metric/Metric.ts: -------------------------------------------------------------------------------- 1 | import AttributeStorage, { AttributeStorageType } from "../Util/AttributeStorage"; 2 | 3 | import IMetricRange from "./IMetricRange"; 4 | import MetricRecord from "./MetricRecord"; 5 | 6 | export enum MetricType { 7 | DOWNTIME = 'downtime', 8 | LATENCY = 'latency', 9 | CUSTOM = 'custom' 10 | } 11 | 12 | export interface MetricCParams { 13 | type: MetricType 14 | id: string 15 | name: string 16 | description?: string 17 | attributes?: AttributeStorageType 18 | } 19 | 20 | export abstract class Metric extends AttributeStorage { 21 | /** 22 | * Type of Metric 23 | */ 24 | public readonly type: MetricType; 25 | 26 | /** 27 | * ID of the Metric 28 | */ 29 | public readonly id: string; 30 | 31 | /** 32 | * Name of the Metric 33 | */ 34 | public readonly name: string; 35 | 36 | /** 37 | * Description of the Metric 38 | */ 39 | public readonly description?: string; 40 | 41 | // 42 | // Constructor 43 | // 44 | constructor({ type, id, name, description, attributes }: MetricCParams) { 45 | super(attributes); 46 | 47 | this.type = type; 48 | this.id = id; 49 | this.name = name; 50 | this.description = description; 51 | } 52 | 53 | // 54 | // Public 55 | // 56 | public attribute(key: string, value: any) { 57 | this.setAttribute(key, value); 58 | return this; 59 | } 60 | 61 | // 62 | // Public:Abstract 63 | // 64 | abstract getPeriod(range: IMetricRange): Promise; 65 | 66 | abstract getAverage(range: IMetricRange): Promise 67 | } 68 | 69 | export default Metric; -------------------------------------------------------------------------------- /packages/uptimerobot/lib/Util/useCache.ts: -------------------------------------------------------------------------------- 1 | import CachedEntry from "./CachedEntry"; 2 | 3 | function objKeyComparison(map: Map, key: K): K | false { 4 | const keys = map.keys(); 5 | let compareKey: K; 6 | 7 | while(compareKey = keys.next().value) { 8 | if (JSON.stringify(key) === JSON.stringify(compareKey)) return compareKey; 9 | } 10 | 11 | return false; 12 | } 13 | 14 | /** 15 | * 16 | * @param lifetime Lifetime of the cache in MS 17 | * @param fetcher Function to retrieve new elements 18 | */ 19 | export default function useCache(lifetime: number, fetcher: (key: K) => Promise) { 20 | const cache: Map> = new Map(); 21 | 22 | /** 23 | * Fetches the data through the cache 24 | * @param key Key to fetch from 25 | * @param ignoreCache If the cache should be ignored or not 26 | */ 27 | const fetch = async (key: K, ignoreCache?: boolean): Promise => { 28 | const cacheKey = ignoreCache === true ? false : objKeyComparison(cache, key); 29 | 30 | if(cacheKey) { 31 | const cacheEntry = cache.get(cacheKey); 32 | 33 | if(cacheEntry) { 34 | 35 | if(Date.now() - cacheEntry.time.getTime() > lifetime) { 36 | // Entry has expired, delete it 37 | cache.delete(key) 38 | } else { 39 | // Entry is valid, send it off 40 | return cacheEntry.entry 41 | } 42 | 43 | } 44 | } 45 | 46 | if(cache.size > 20) cache.clear(); 47 | 48 | // Fetch a new value 49 | const fetched = await fetcher(key) 50 | 51 | // Populate it into cache 52 | cache.set(key, {time: new Date(), entry: fetched}); 53 | 54 | // Return it 55 | return fetched; 56 | } 57 | 58 | return [ fetch ] 59 | } -------------------------------------------------------------------------------- /packages/core/__tests__/builder.test.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Builder, component, group } from "../lib/Builder"; 4 | import Component, { ComponentCParams } from '../lib/Component/Component' 5 | import ComponentGroup from "../lib/Component/ComponentGroup"; 6 | 7 | 8 | describe('Builder/ComponentBuilder', () => { 9 | test('Component Builder Builds', () => { 10 | const attrs = { 11 | id: 'component-1', 12 | name: 'Component 1', 13 | description: 'Test Description' 14 | } 15 | 16 | 17 | expect(component(attrs.id).name(attrs.name).description(attrs.description).build()).toStrictEqual(new Component(attrs)); 18 | }) 19 | 20 | test('Component Group Builder Builds', () => { 21 | const builderGroup = new Builder() 22 | .groups([ 23 | group() 24 | .name('group 1') 25 | .description('group 1 description') 26 | .components([ 27 | component('component-1') 28 | .name('g1-c1') 29 | .description('g1-c1 description'), 30 | component('component-2') 31 | .name('g1-c2') 32 | ]) 33 | ]) 34 | 35 | const comparisonGroup = new ComponentGroup({name: 'group 1', description: 'group 1 description'}); 36 | comparisonGroup.addComponents([ 37 | new Component({ id: 'component-1', name: 'g1-c1', description: 'g1-c1 description' }), 38 | new Component({ id: 'component-2', name: 'g1-c2' }) 39 | ]) 40 | 41 | expect(builderGroup._groups.map(g => g.build())).toStrictEqual([ 42 | comparisonGroup 43 | ]) 44 | }) 45 | }); 46 | -------------------------------------------------------------------------------- /packages/core/lib/Severity/AchievedSeverityCalculator.ts: -------------------------------------------------------------------------------- 1 | import lib from ".."; 2 | import Component from "../Component/Component"; 3 | import ComponentGroup from "../Component/ComponentGroup"; 4 | import WorstSeverity from "../Util/WorstSeverity"; 5 | import ICalculatesSeverities from "./ICalculatesSeverities"; 6 | import Severity from "./Severity"; 7 | 8 | export default class AchievedSeverityCalculator implements ICalculatesSeverities { 9 | async getGlobalSeverity(statusify: lib): Promise { 10 | const components = await statusify.getComponents(); 11 | 12 | const compSeverities = ( 13 | await Promise.all(components.map(async c_ => { 14 | return await this.getAchievedSeveritiesForComponent(c_, statusify) 15 | })) 16 | ).flat() 17 | 18 | return WorstSeverity(compSeverities, statusify) 19 | } 20 | 21 | async getSeverityForGroup(group: ComponentGroup, statusify: lib): Promise { 22 | const compSeverities = ( 23 | await Promise.all(group.components.map(async c_ => { 24 | return await this.getAchievedSeveritiesForComponent(c_, statusify) 25 | })) 26 | ).flat() 27 | 28 | return WorstSeverity(compSeverities, statusify) 29 | } 30 | 31 | async getSeverityForComponent(component: Component, statusify: lib): Promise { 32 | return WorstSeverity(await this.getAchievedSeveritiesForComponent(component, statusify), statusify) 33 | } 34 | 35 | // 36 | // Protected 37 | // 38 | protected async getAchievedSeveritiesForComponent(component: Component, statusify: lib) { 39 | const severities = await statusify.getSeverities(); 40 | 41 | return (await Promise.all(severities.map(async s => ({s, a: await s.achieved(component)}) ))) 42 | .filter(e => e.a === true) 43 | .map(e => e.s) 44 | } 45 | } -------------------------------------------------------------------------------- /packages/react/src/app/components/elements/component/ComponentHeader.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Flex, Text } from "@chakra-ui/layout"; 2 | import { Tooltip } from "@chakra-ui/tooltip"; 3 | import { useComponent } from "../../../contexts/ComponentContext"; 4 | import { AiOutlineQuestionCircle } from "react-icons/ai"; 5 | import useSeverityColor from "../../../hooks/useSeverityColor"; 6 | import useSeverity from "../../../hooks/useSeverity"; 7 | import useUptimePercentage from "../../../hooks/useUptimePercentage"; 8 | import useMetricRange from "../../../hooks/useMetricRange"; 9 | 10 | export default function ComponentHeader() { 11 | const component = useComponent(); 12 | const severity = useSeverity(component); 13 | const severityColor = useSeverityColor(severity); 14 | const range = useMetricRange(); 15 | const uptimePercentage = useUptimePercentage(range, component); 16 | 17 | return ( 18 | 25 | 26 | 27 | {component.name} 28 | 29 | 30 | {component.description && ( 31 | 32 | 33 | 34 | 35 | 36 | )} 37 | 38 | 39 | 40 | 41 | { Math.round(uptimePercentage * 10000) / 100 }% 42 | 43 | 44 | 45 | ) 46 | } -------------------------------------------------------------------------------- /packages/core/__TestUtils/MockBuilder.ts: -------------------------------------------------------------------------------- 1 | import { Builder, group, component } from '../lib/Builder' 2 | import { runnableSeverity } from '../lib/Severity/RunnableSeverity' 3 | 4 | 5 | const built = new Builder() 6 | .groups([ 7 | group() 8 | .name('Test Group') 9 | .description('Test Groups Description') 10 | .components([ 11 | component('component-1') 12 | .name('Test Component 1') 13 | .description('Test Component 1 Description'), 14 | 15 | component('component-2') 16 | .name('Test Component 2') 17 | ]), 18 | 19 | group() 20 | .name('Test Group 2') 21 | .components([ 22 | component('component-3') 23 | .name('Test Component 3') 24 | .description('Test Component 3 Description'), 25 | 26 | component('component-4') 27 | .name('Test Component 4') 28 | ]), 29 | ]) 30 | 31 | .severities([ 32 | runnableSeverity('operational') 33 | .name('Operational') 34 | .runnable(async (component) => { 35 | return true 36 | }), 37 | 38 | runnableSeverity('partial') 39 | .name('Partial') 40 | .runnable(async (component) => { 41 | return false 42 | }), 43 | 44 | runnableSeverity('minor') 45 | .name('Minor') 46 | .runnable(async (component) => { 47 | return false 48 | }), 49 | 50 | runnableSeverity('major') 51 | .name('Major') 52 | .runnable(async (component) => { 53 | return false 54 | }), 55 | ]) 56 | ; 57 | 58 | export default built; -------------------------------------------------------------------------------- /packages/react/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /example/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/lokijs@^1.5.3": 6 | version "1.5.3" 7 | resolved "https://registry.yarnpkg.com/@types/lokijs/-/lokijs-1.5.3.tgz#e45db0b35e53ec09a4c2ff4f253037abd56be151" 8 | integrity sha512-ssiP6F7rpDqlaOzo+0Y/0dkVH2JiAjc+o7n6M4pFnfAMscf+7FthqS0cobvGJEJvxmzp4PWfMOfe0q3VETtrgQ== 9 | 10 | "@types/node@^14.14.20": 11 | version "14.14.20" 12 | resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.20.tgz#f7974863edd21d1f8a494a73e8e2b3658615c340" 13 | integrity sha512-Y93R97Ouif9JEOWPIUyU+eyIdyRqQR0I8Ez1dzku4hDx34NWh4HbtIc3WNzwB1Y9ULvNGeu5B8h8bVL5cAk4/A== 14 | 15 | inherits@2.0.3: 16 | version "2.0.3" 17 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 18 | integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= 19 | 20 | lokijs@^1.5.11: 21 | version "1.5.11" 22 | resolved "https://registry.yarnpkg.com/lokijs/-/lokijs-1.5.11.tgz#2b2ea82ec66050e4b112c6cfc588dac22d362b13" 23 | integrity sha512-YYyuBPxMn/oS0tFznQDbIX5XL1ltMcwFqCboDr8voYE4VCDzR5vAsrvQDhlnua4lBeqMqHmLvUXRTmRUzUKH1Q== 24 | 25 | path@^0.12.7: 26 | version "0.12.7" 27 | resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f" 28 | integrity sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8= 29 | dependencies: 30 | process "^0.11.1" 31 | util "^0.10.3" 32 | 33 | process@^0.11.1: 34 | version "0.11.10" 35 | resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" 36 | integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= 37 | 38 | util@^0.10.3: 39 | version "0.10.4" 40 | resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" 41 | integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A== 42 | dependencies: 43 | inherits "2.0.3" 44 | -------------------------------------------------------------------------------- /packages/loki/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/lokijs@^1.5.3": 6 | version "1.5.3" 7 | resolved "https://registry.yarnpkg.com/@types/lokijs/-/lokijs-1.5.3.tgz#e45db0b35e53ec09a4c2ff4f253037abd56be151" 8 | integrity sha512-ssiP6F7rpDqlaOzo+0Y/0dkVH2JiAjc+o7n6M4pFnfAMscf+7FthqS0cobvGJEJvxmzp4PWfMOfe0q3VETtrgQ== 9 | 10 | "@types/node@^14.14.22": 11 | version "14.14.22" 12 | resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.22.tgz#0d29f382472c4ccf3bd96ff0ce47daf5b7b84b18" 13 | integrity sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw== 14 | 15 | inherits@2.0.3: 16 | version "2.0.3" 17 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 18 | integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= 19 | 20 | lokijs@^1.5.11: 21 | version "1.5.11" 22 | resolved "https://registry.yarnpkg.com/lokijs/-/lokijs-1.5.11.tgz#2b2ea82ec66050e4b112c6cfc588dac22d362b13" 23 | integrity sha512-YYyuBPxMn/oS0tFznQDbIX5XL1ltMcwFqCboDr8voYE4VCDzR5vAsrvQDhlnua4lBeqMqHmLvUXRTmRUzUKH1Q== 24 | 25 | path@^0.12.7: 26 | version "0.12.7" 27 | resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f" 28 | integrity sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8= 29 | dependencies: 30 | process "^0.11.1" 31 | util "^0.10.3" 32 | 33 | process@^0.11.1: 34 | version "0.11.10" 35 | resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" 36 | integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= 37 | 38 | util@^0.10.3: 39 | version "0.10.4" 40 | resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" 41 | integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A== 42 | dependencies: 43 | inherits "2.0.3" 44 | -------------------------------------------------------------------------------- /packages/uptimerobot/lib/Metric/UptimeRobotDowntime.ts: -------------------------------------------------------------------------------- 1 | import { GenericUptimeRobotMetric, UptimeRobotGenericMetricCParams } from "./GenericUptimeRobotMetric"; 2 | 3 | import IDowntimeMetricRecord from "@statusify/core/dist/Metric/IDowntimeMetricRecord" 4 | import IMetricRange from "@statusify/core/dist/Metric/IMetricRange"; 5 | import { MILLISECONDS_IN_DAY } from "../constants"; 6 | import { MetricType } from "@statusify/core/dist/Metric/Metric"; 7 | import UptimeRobotCore from ".."; 8 | import dayjs from "dayjs"; 9 | 10 | export default class UptimeRobotDowntime extends GenericUptimeRobotMetric { 11 | 12 | // 13 | // Constructor 14 | // 15 | constructor(urc: UptimeRobotCore, params: UptimeRobotGenericMetricCParams) { 16 | super(MetricType.DOWNTIME, urc, params); 17 | this.urc.useMonitor(this.monitorID, MetricType.DOWNTIME); 18 | } 19 | 20 | /** 21 | * @inheritdoc 22 | */ 23 | getPeriod(range: IMetricRange): Promise { 24 | return this.fetchData(range); 25 | } 26 | 27 | /** 28 | * @inheritdoc 29 | */ 30 | async getAverage(range: IMetricRange): Promise { 31 | const downtimes = await this.getPeriod(range); 32 | 33 | return { 34 | time: range.start, 35 | value: downtimes.map(v => v.value).reduce((a, b) => a + b) / downtimes.length 36 | } 37 | } 38 | 39 | // 40 | // Private 41 | // 42 | private async fetchData(range: IMetricRange) { 43 | const data = await this.urc.getMonitor(range, this.monitorID, MetricType.DOWNTIME); 44 | 45 | if(!data) { 46 | return []; 47 | } 48 | 49 | return (data.custom_uptime_ranges as string).split('-') 50 | .map((pr: string, i: number): IDowntimeMetricRecord => { 51 | return { 52 | time: dayjs(range.end).subtract(i, 'days').toDate(), 53 | value: MILLISECONDS_IN_DAY - (MILLISECONDS_IN_DAY * (parseFloat(pr) / 100)) 54 | } 55 | }) 56 | .filter(r => r.value > 0) 57 | } 58 | } -------------------------------------------------------------------------------- /packages/react/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Stack } from "@chakra-ui/layout"; 2 | 3 | import ComponentGroup from "../app/components/elements/componentGroup/ComponentGroup"; 4 | import DefaultLayout from "../app/components/layouts/DefaultLayout"; 5 | import IncidentBanner from "../app/components/elements/incident/IncidentBanner"; 6 | import { IncidentsQuery } from "@statusify/core/dist/Incident/IProvidesIncidents"; 7 | import React from "react"; 8 | import StatusifyComponentGroup from "@statusify/core/dist/Component/ComponentGroup"; 9 | import useIncidents from "../app/hooks/useIncidents"; 10 | import { useStatusify } from "../app/contexts/StatusifyContext"; 11 | import StatusBanner from "../app/components/elements/global/StatusBanner"; 12 | 13 | const activeIncidentsQuery: IncidentsQuery = { resolvedAt: null } 14 | 15 | export default function Home() { 16 | const [ groups, setGroups ] = React.useState([]); 17 | const statusify = useStatusify(); 18 | const incidents = useIncidents(activeIncidentsQuery); 19 | 20 | React.useEffect(() => { 21 | let isMounted = true; 22 | 23 | statusify.getComponentGroups().then((groups) => { 24 | if(isMounted) { 25 | setGroups(groups); 26 | } 27 | }); 28 | 29 | return () => { 30 | isMounted = false; 31 | } 32 | }, [ statusify ]); 33 | 34 | return ( 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | {incidents.map(incident => )} 45 | 46 | 47 | 48 | {groups.map((group, i) => )} 49 | 50 | 51 | 52 | 53 | 54 | ) 55 | } -------------------------------------------------------------------------------- /packages/react/src/app/components/elements/incident/IncidentBanner.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { Badge, Box, Flex, Heading, Stack } from "@chakra-ui/layout"; 4 | 5 | import IIncident from "@statusify/core/dist/Incident/IIncident"; 6 | import IncidentBannerUpdate from "./IncidentBannerUpdate"; 7 | import { Link } from "react-router-dom"; 8 | import Severity from "@statusify/core/dist/Severity/Severity"; 9 | import useSeverityColor from "../../../hooks/useSeverityColor"; 10 | 11 | export interface InterfaceBannerProps { 12 | incident: IIncident; 13 | } 14 | 15 | const IncidentBanner: React.FC = ({ incident }) => { 16 | const [ lastSeverity, setLastSeverity ] = React.useState(); 17 | const severityColor = useSeverityColor(lastSeverity, 'gray'); 18 | 19 | React.useEffect(() => { 20 | setLastSeverity((incident.updates.length === 0) ? incident.severity : incident.updates[0].severity) 21 | }, [ incident ]); 22 | 23 | return ( 24 | 31 | 32 |
33 | 39 | {lastSeverity && lastSeverity.name} 40 | 41 |
42 | 43 | 44 | {incident.name} 45 | 46 |
47 | 48 | 49 | {incident.updates.map((update, i) => )} 50 | 51 | 52 | 53 |
54 | ); 55 | } 56 | 57 | export default IncidentBanner; -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |
6 | 7 |

8 | Core for Statusify 9 |

10 | 11 | --- 12 | 13 | ## Features 14 | - Incidents 15 | - Metrics 16 | - Severities 17 | - Builder Pattern 18 | - ASynchronous API 19 | 20 | ## Technologies 21 | - [Chakra UI](https://chakra-ui.com/) 22 | - [React Router](https://reactrouter.com/) 23 | ## Building 24 | ```bash 25 | $ tsc 26 | ``` 27 | 28 | ## Usage 29 | 30 | ### Constructing 31 | Statusify needs to be initialized with Providers and a Severity Calculator, below is a list of included providers and calculators. 32 | 33 | #### Providers 34 | - [Builder](./lib/Builder/Builder.ts) (Component + Severity) 35 | - [ArrayIncidentProvider](./lib/Incident/ArrayIncidentProvider.ts) (Incident) 36 | 37 | #### Calculators 38 | - [AchievedSeverityCalculator](./lib/Severity/AchievedSeverityCalculator.ts) 39 | - [SeverityMultiplexer](./lib/Severity/SeverityMultiplexer.ts) 40 | 41 | ```ts 42 | const statusify = new Statusify({ 43 | componentProvider: , 44 | incidentProvider: , 45 | severityProvider: , 46 | severityCalculator: , 47 | }); 48 | ``` 49 | 50 | ### Fetching Data 51 | All APIs are asynchronous and must therefore be used with Promise#then or await, read more [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). 52 | 53 | Methods for fetching data can be found within the [Statusify class](./lib/index.ts). 54 | 55 | ## Creating Custom Providers and Calculators 56 | Custom providers can be created by implementing one or more of the following interfaces: 57 | - [IProvidesIncidents](./lib/Incident/IProvidesIncidents.ts) 58 | - [IProvidesComponents](./lib/Component/IProvidesComponents.ts) 59 | - [IProvidesSeverities](./lib/Severity/IProvidesSeverities.ts) 60 | 61 | Custom calculators can be created by implementing the [ICalculatesSeverities](./lib/Severity/ICalculatesSeverities.ts) interface. -------------------------------------------------------------------------------- /packages/core/lib/Severity/IncidentSeverityCalculator.ts: -------------------------------------------------------------------------------- 1 | import Component from "../Component/Component"; 2 | import ComponentGroup from "../Component/ComponentGroup"; 3 | import ICalculatesSeverities from "./ICalculatesSeverities"; 4 | import Severity from "./Severity"; 5 | import Statusify from ".."; 6 | import WorstSeverity from "../Util/WorstSeverity"; 7 | 8 | /** 9 | * This take into account severities only, it is expected to be extended to implement extra functionality 10 | */ 11 | export default class IncidentSeverityProvider implements ICalculatesSeverities { 12 | /** 13 | * Gets a global severity for all groups 14 | * Uses the severity of the most degraded child 15 | * @param statusify Statusify Core 16 | */ 17 | async getGlobalSeverity(statusify: Statusify): Promise { 18 | const componentSeverities = Promise.all( 19 | (await statusify.getComponents()).map(async (c) => { 20 | return this.getSeverityForComponent(c, statusify) 21 | }) 22 | ) 23 | 24 | return await WorstSeverity(await componentSeverities, statusify) 25 | } 26 | 27 | /** 28 | * Gets the severity for a group 29 | * Uses the severity of the most degraded child in the group 30 | * @param group Group to get the severity for 31 | * @param statusify Statusify Core 32 | */ 33 | async getSeverityForGroup(group: ComponentGroup, statusify: Statusify): Promise { 34 | const componentSeverities = Promise.all( 35 | group.components.map(async (c) => { 36 | return this.getSeverityForComponent(c, statusify) 37 | }) 38 | ) 39 | 40 | return await WorstSeverity(await componentSeverities, statusify) 41 | } 42 | 43 | /** 44 | * Gets the severity for a component 45 | * In this instance it gets all of the active incidents and finds the worst severity from those 46 | * @param component Component to get the severity for 47 | * @param statusify Statusify Core 48 | */ 49 | async getSeverityForComponent(component: Component, statusify: Statusify): Promise { 50 | const activeIncidents = await statusify.getIncidentsFor(component, { 51 | resolvedAt: null 52 | }) 53 | 54 | return await WorstSeverity(activeIncidents.map(i => i.severity), statusify) 55 | } 56 | } -------------------------------------------------------------------------------- /packages/uptimerobot/lib/Metric/UptimeRobotLatency.ts: -------------------------------------------------------------------------------- 1 | import { GenericUptimeRobotMetric, UptimeRobotGenericMetricCParams } from './GenericUptimeRobotMetric'; 2 | 3 | import ILatencyMetricRecord from '@statusify/core/dist/Metric/ILatencyMetricRecord'; 4 | import IMetricRange from '@statusify/core/dist/Metric/IMetricRange'; 5 | import { MetricType } from '@statusify/core/dist/Metric/Metric' 6 | import UptimeRobotCore from '..'; 7 | import dayjs from 'dayjs'; 8 | 9 | interface UptimeRobotResponseTime { 10 | datetime: number, 11 | value: number 12 | } 13 | 14 | export default class UptimeRobotLatency extends GenericUptimeRobotMetric { 15 | constructor(urc: UptimeRobotCore, params: UptimeRobotGenericMetricCParams) { 16 | super(MetricType.LATENCY, urc, params) 17 | this.urc.useMonitor(this.monitorID, MetricType.LATENCY); 18 | } 19 | 20 | /** 21 | * @inheritdoc 22 | */ 23 | async getPeriod(range: IMetricRange): Promise { 24 | const data = await this.fetchData(range); 25 | 26 | if(!data) { 27 | return []; 28 | } 29 | 30 | return (data.response_times as UptimeRobotResponseTime[]).map((t) => ({ 31 | time: new Date(t.datetime * 1000), 32 | value: t.value 33 | })); 34 | } 35 | 36 | /** 37 | * @inheritdoc 38 | * This simply uses the get period and then averages the values 39 | */ 40 | async getAverage(range: IMetricRange): Promise { 41 | const data = await this.urc.getMonitor(range, this.monitorID, MetricType.LATENCY); 42 | 43 | if(!data) { 44 | throw new Error('UptimeRobotLatency: Unable to fetch average response time.') 45 | } 46 | 47 | return { 48 | time: new Date(), 49 | value: parseFloat(data.average_response_time as string) 50 | }; 51 | } 52 | 53 | // 54 | // Private 55 | // 56 | private async fetchData(range: IMetricRange) { 57 | let computedRange = range; 58 | 59 | if(dayjs(range.end).diff(range.start, 'days') > 7) { 60 | console.warn("Range cannot go past 7 days.") 61 | computedRange.start = dayjs(range.end).startOf('day').subtract(7, 'days').toDate(); 62 | } 63 | 64 | return await this.urc.getMonitor(computedRange, this.monitorID, MetricType.LATENCY); 65 | } 66 | } -------------------------------------------------------------------------------- /packages/react/src/app/components/elements/componentGroup/ComponentGroupHeader.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Flex, Text } from "@chakra-ui/layout"; 2 | 3 | import { AiOutlineQuestionCircle } from "react-icons/ai" 4 | import ComponentGroupHeaderToggleIndicator from "./ComponentGroupHeaderToggleIndicator"; 5 | import { SlideFade } from "@chakra-ui/transition"; 6 | import { Tooltip } from "@chakra-ui/tooltip"; 7 | import { useComponentGroup } from "../../../contexts/ComponentGroupContext" 8 | import useSeverity from "../../../hooks/useSeverity"; 9 | import useSeverityColor from "../../../hooks/useSeverityColor"; 10 | import { useTranslation } from "react-i18next"; 11 | 12 | export default function ComponentGroupHeader() { 13 | const { t } = useTranslation(); 14 | const [{ group, collapsed, isAnonymous, isCollapsible }, dispatch ] = useComponentGroup(); 15 | const severity = useSeverity(group); 16 | const severityColor = useSeverityColor(severity); 17 | 18 | return ( 19 | { dispatch({type: 'toggle'}) }} 26 | > 27 | {!isAnonymous ? ( 28 | 29 | 30 | 31 | {group.name} 32 | 33 | 34 | {group.description && ( 35 | 36 | 37 | 38 | 39 | 40 | )} 41 | 42 | ) : (
)} 43 | 44 | 45 | 46 | {severity && ( 47 | 48 | 49 | {severity.name} 50 | 51 | 52 | )} 53 | 54 | 55 | {isCollapsible && ( 56 | 61 | )} 62 | 63 | 64 | ) 65 | } -------------------------------------------------------------------------------- /packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@statusify/react", 3 | "version": "1.0.1-alpha.0", 4 | "private": true, 5 | "dependencies": { 6 | "@chakra-ui/react": "^1.4.2", 7 | "@emotion/react": "11", 8 | "@emotion/styled": "11", 9 | "@popperjs/core": "^2.4.0", 10 | "@statusify/core": "^1.1.1-alpha.0", 11 | "@statusify/uptimerobot": "^1.1.1-alpha.0", 12 | "@testing-library/jest-dom": "^5.11.4", 13 | "@testing-library/react": "^11.1.0", 14 | "@testing-library/user-event": "^12.1.10", 15 | "@types/jest": "^26.0.15", 16 | "@types/node": "^12.0.0", 17 | "@types/react": "^17.0.0", 18 | "@types/react-dom": "^17.0.0", 19 | "apexcharts": "^3.26.0", 20 | "dayjs": "^1.10.4", 21 | "framer-motion": "4", 22 | "i18next": "^20.1.0", 23 | "react": "^17.0.2", 24 | "react-apexcharts": "^1.3.7", 25 | "react-dom": "^17.0.2", 26 | "react-i18next": "^11.8.12", 27 | "react-icons": "^4.1.0", 28 | "react-popper": "^2.2.3", 29 | "react-router-dom": "^5.2.0", 30 | "react-scripts": "4.0.3", 31 | "typescript": "^4.1.2", 32 | "web-vitals": "^1.0.1" 33 | }, 34 | "scripts": { 35 | "start": "react-scripts start", 36 | "build": "react-scripts build", 37 | "test": "react-scripts test", 38 | "eject": "react-scripts eject", 39 | "lint": "eslint . --fix" 40 | }, 41 | "eslintConfig": { 42 | "extends": [ 43 | "react-app", 44 | "react-app/jest" 45 | ] 46 | }, 47 | "browserslist": { 48 | "production": [ 49 | ">0.2%", 50 | "not dead", 51 | "not op_mini all" 52 | ], 53 | "development": [ 54 | "last 1 chrome version", 55 | "last 1 firefox version", 56 | "last 1 safari version" 57 | ] 58 | }, 59 | "devDependencies": { 60 | "@types/react-router-dom": "^5.1.7", 61 | "@typescript-eslint/eslint-plugin": "4.0.0", 62 | "@typescript-eslint/parser": "4.0.0", 63 | "@typescript-eslint/typescript-estree": "^4.23.0", 64 | "eslint-config-prettier": "^8.3.0", 65 | "eslint-config-react-app": "^6.0.0", 66 | "eslint-plugin-flowtype": "5.2.0", 67 | "eslint-plugin-import": "2.22.0", 68 | "eslint-plugin-jsx-a11y": "6.3.1", 69 | "eslint-plugin-prettier": "^3.4.0", 70 | "eslint-plugin-react": "7.20.3", 71 | "eslint-plugin-react-hooks": "4.0.8" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/react/src/app/contexts/AutoRefreshContext.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export interface IAutoRefreshState { 4 | interval: number; 5 | seconds: number; 6 | lastUpdate: Date; 7 | } 8 | 9 | type AutoRefreshAction = 10 | | { type: 'INCREMENT' } 11 | | { type: 'SET_SECONDS', value: number } 12 | | { type: 'SET_LAST_UPDATE', value: Date } 13 | ; 14 | 15 | const AutoRefreshContext = React.createContext<[ 16 | IAutoRefreshState, 17 | React.Dispatch 18 | ]>(undefined as any); 19 | 20 | function autoRefreshReducer(state: IAutoRefreshState, action: AutoRefreshAction): IAutoRefreshState { 21 | switch(action.type) { 22 | case 'INCREMENT': 23 | return { 24 | ...state, 25 | seconds: (state.seconds === 0 ? state.interval : state.seconds - 1) 26 | }; 27 | case 'SET_SECONDS': 28 | return { 29 | ...state, 30 | seconds: action.value 31 | }; 32 | case 'SET_LAST_UPDATE': 33 | return { 34 | ...state, 35 | lastUpdate: action.value 36 | } 37 | default: 38 | return state; 39 | } 40 | } 41 | 42 | export interface AutoRefreshProviderProps { 43 | interval: number; 44 | refreshListener: () => void; 45 | } 46 | 47 | export const AutoRefreshProvider: React.FC = ({ children, interval, refreshListener }) => { 48 | const [ state, dispatch ] = React.useReducer(autoRefreshReducer, { 49 | interval: interval, 50 | seconds: interval, 51 | lastUpdate: new Date(), 52 | }); 53 | 54 | React.useEffect(() => { 55 | const interval = setInterval(() => { 56 | dispatch({ type: 'INCREMENT' }); 57 | }, 1000); 58 | 59 | return () => { 60 | clearInterval(interval); 61 | } 62 | }, []); 63 | 64 | React.useEffect(() => { 65 | if(state.seconds === 0) { 66 | dispatch({ type: 'SET_LAST_UPDATE', value: new Date() }); 67 | refreshListener(); 68 | } 69 | }, [ state.seconds, refreshListener ]); 70 | 71 | return ( 72 | 73 | {children} 74 | 75 | ) 76 | } 77 | 78 | export function useAutoRefresh() { 79 | const context = React.useContext(AutoRefreshContext); 80 | 81 | if(context === undefined) { 82 | throw new Error('useAutoRefresh must be used within a UsedRefreshProvider'); 83 | } 84 | 85 | return context; 86 | } -------------------------------------------------------------------------------- /packages/react/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/uptimerobot/lib/Types/IUptimeRobotMonitor.ts: -------------------------------------------------------------------------------- 1 | import UptimeRobotMonitorType from "./UptimeRobotMonitorType"; 2 | import IUptimeRobotResponseTime from "./IUptimeRobotResponseTime"; 3 | import UptimeRobotMonitorStatus from "./UptimeRobotMonitorStatus"; 4 | 5 | /** 6 | * (Partially) Defines the response schema for a monitor 7 | * @see https://uptimerobot.com/api/ 8 | */ 9 | export default interface IUptimeRobotMonitor { 10 | /** 11 | * ID of the monitor 12 | */ 13 | id: number; 14 | 15 | /** 16 | * The friendly name of the monitor 17 | */ 18 | friendly_name: string; 19 | 20 | /** 21 | * The URL or IP Address of the monitor 22 | */ 23 | url: string; 24 | 25 | /** 26 | * The type of monitor 27 | */ 28 | type: UptimeRobotMonitorType; 29 | 30 | /** 31 | * Used for port monitoring (type 4), shows which service is monitored or a custom port. 32 | */ 33 | sub_type: number | string; 34 | 35 | /** 36 | * The rate at which the monitor is checked in seconds 37 | */ 38 | interval: number; 39 | 40 | /** 41 | * The status of the monitor 42 | */ 43 | status: UptimeRobotMonitorStatus; 44 | 45 | /** 46 | * UNIX Timestamp of when the monitor was created. 47 | */ 48 | create_datetime: number; 49 | 50 | /** 51 | * All time uptime ratio calculated since the monitor was created 52 | * Formatted As: up-down-paused 53 | */ 54 | all_time_uptime_ratio?: string; 55 | 56 | /** 57 | * The durations of all time up-down-paused events in seconds 58 | * !!ASSUMED!! Formatted As: up-down-paused 59 | */ 60 | all_time_uptime_durations?: string; 61 | 62 | /** 63 | * The uptime ratio of the monitor for the given periods 64 | * If there is more than 1 period then values are separated with "-" 65 | */ 66 | custom_uptime_ratios?: string; 67 | 68 | /** 69 | * The down durations for the given periods in seconds 70 | * If there is more than 1 period then values are separated with "-" 71 | */ 72 | custom_down_durations?: string; 73 | 74 | /** 75 | * The uptime ratio of the monitor for the given ranges 76 | * If there is more than 1 range then values are separated with "-" 77 | */ 78 | custom_uptime_ranges?: string; 79 | 80 | /** 81 | * The average value of the response times 82 | * Requires response_times=1 in request 83 | */ 84 | average_response_time?: string; 85 | 86 | /** 87 | * Response times for the monitor 88 | * Range is maxed at 7 days and if no range is provided then the last 24 hours are returned 89 | */ 90 | response_times?: IUptimeRobotResponseTime[]; 91 | } -------------------------------------------------------------------------------- /packages/react/src/app/components/elements/component/metrics/tickChart/ComponentTickChartTooltip.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Link, Text } from "@chakra-ui/layout"; 2 | 3 | import ISeverityTick from "../../../../../interfaces/ISeverityTick"; 4 | import React from "react"; 5 | import { Link as RouterLink } from "react-router-dom"; 6 | import dayjs from "../../../../../utils/dayjs"; 7 | import { usePopper } from "react-popper"; 8 | import { useTranslation } from "react-i18next"; 9 | 10 | export interface ComponentTickChartTooltipProps { 11 | reference: HTMLElement; 12 | tick: ISeverityTick; 13 | } 14 | 15 | export default function ComponentTickChartTooltip({ tick, reference, ...props }: ComponentTickChartTooltipProps) { 16 | const { t } = useTranslation(); 17 | const [ popperElement, setPopperElement ] = React.useState(null); 18 | const [ arrowElement, setArrowElement ] = React.useState(null); 19 | 20 | const offsetModifier = React.useMemo(() => ({ 21 | name: 'offset', 22 | options: { 23 | offset: (o) => { 24 | return [ 0, (o.reference.y - document.scrollingElement.scrollTop) - (reference.parentElement ? reference.parentElement.getBoundingClientRect().y : 0) ]; 25 | } 26 | } 27 | }), [ reference ]); 28 | 29 | const { styles, attributes } = usePopper(reference, popperElement, { 30 | modifiers: [ 31 | { name: 'arrow', options: { element: arrowElement } }, 32 | offsetModifier 33 | ] 34 | }); 35 | 36 | return ( 37 | 38 |
39 | 40 | {dayjs(tick.date).format(t('components.metrics.tickChart.dateFormat'))} 41 | 42 | {/* Downtimes */} 43 | {tick.relatedDowntimes.length === 0 ? ( 44 | 45 | {t('components.metrics.tickChart.noDowntime')} 46 | 47 | ) : ( 48 | tick.relatedDowntimes.map((downtime, i) => ( 49 | 50 | {t('components.metrics.tickChart.downtime', { duration: dayjs.duration(downtime.value, 'ms').humanize() })} 51 | 52 | )) 53 | )} 54 | 55 | 56 | {/* Incidents */} 57 | {tick.relatedIncidents.length === 0 ?( 58 | 59 | {t('components.metrics.tickChart.noIncidents')} 60 | 61 | ) : ( 62 | tick.relatedIncidents.map((incident, i) => ( 63 | 64 | {incident.name} 65 | 66 | )) 67 | )} 68 | 69 | 70 | ) 71 | } -------------------------------------------------------------------------------- /packages/react/src/app/contexts/ComponentGroupContext.tsx: -------------------------------------------------------------------------------- 1 | import { ANONYMOUS, COLLAPSED, COLLAPSIBLE } from "../constants/FrontendOptions"; 2 | 3 | import { AttributeStorageType } from "@statusify/core/dist/Util/AttributeStorage"; 4 | import ComponentGroup from "@statusify/core/dist/Component/ComponentGroup"; 5 | import React from "react"; 6 | 7 | export interface ComponentGroupState { 8 | group: ComponentGroup; 9 | attributes: AttributeStorageType; 10 | 11 | isAnonymous: boolean; 12 | isCollapsible: boolean; 13 | collapsed: boolean; 14 | } 15 | 16 | type ComponentGroupAction = 17 | | { type: 'expand' } 18 | | { type: 'collapse' } 19 | | { type: 'toggle' } 20 | | { type: 'setCollapsed', value: boolean } 21 | | { type: 'setAttributes', value: AttributeStorageType } 22 | ; 23 | 24 | const ComponentGroupContext = React.createContext<[ 25 | ComponentGroupState, 26 | React.Dispatch, 27 | ]>(undefined as any); 28 | 29 | function componentGroupReducer(state: ComponentGroupState, action: ComponentGroupAction): ComponentGroupState { 30 | switch(action.type) { 31 | case 'expand': 32 | return { ...state, collapsed: false }; 33 | case 'collapse': 34 | return { ...state, collapsed: true }; 35 | case 'toggle': 36 | return { ...state, collapsed: !state.collapsed }; 37 | case 'setCollapsed': 38 | return { ...state, collapsed: action.value }; 39 | case 'setAttributes': 40 | return { 41 | ...state, 42 | attributes: action.value, 43 | 44 | isCollapsible: action.value[COLLAPSIBLE] ?? true, 45 | isAnonymous: action.value[ANONYMOUS] ?? false, 46 | }; 47 | } 48 | } 49 | 50 | export function ComponentGroupProvider({ group, children }: {group: ComponentGroup, children?: React.ReactNode}) { 51 | const [ state, dispatch ] = React.useReducer(componentGroupReducer, { 52 | group, 53 | attributes: {}, 54 | 55 | isAnonymous: false, 56 | isCollapsible: true, 57 | collapsed: true 58 | }); 59 | 60 | React.useEffect(() => { 61 | let isMounted = true; 62 | 63 | group.getAttributes().then((attributes) => { 64 | if(!isMounted) { 65 | return; 66 | } 67 | 68 | dispatch({ type: 'setAttributes', value: attributes }); 69 | 70 | // Set the collapsed flag since we know we've just loaded it 71 | if(attributes[COLLAPSED] !== undefined) { 72 | dispatch({ type: 'setCollapsed', value: attributes[COLLAPSED] }); 73 | } 74 | }) 75 | 76 | return () => { 77 | isMounted = false; 78 | } 79 | }, [ group ]); 80 | 81 | return ( 82 | 83 | {children} 84 | 85 | ) 86 | } 87 | 88 | export function useComponentGroup() { 89 | const context = React.useContext(ComponentGroupContext); 90 | 91 | if(context === undefined) { 92 | throw new Error('useComponentGroup must be used within a ComponentGroupProvider'); 93 | } 94 | 95 | return context; 96 | } -------------------------------------------------------------------------------- /packages/core/lib/Incident/ArrayIncidentProvider.ts: -------------------------------------------------------------------------------- 1 | import IProvidesIncidents, { DateQuery, IncidentsQuery } from "./IProvidesIncidents"; 2 | 3 | import Component from "../Component/Component"; 4 | import IIncident from "./IIncident"; 5 | import lib from ".."; 6 | import IInjectStatusify from "../Util/IInjectStatusify"; 7 | import Statusify from ".."; 8 | 9 | export default class ArrayIncidentProviders implements IProvidesIncidents, IInjectStatusify { 10 | private statusify!: Statusify; 11 | private incidentFactory?: (statusify: Statusify) => Promise; 12 | 13 | public incidents: IIncident[] = new Proxy([], { 14 | set: (target: IIncident[], prop, value, _receiver) => { 15 | if(prop === 'length') { 16 | return true; 17 | } 18 | 19 | target[prop as unknown as number] = value; 20 | 21 | if(this.statusify !== null) { 22 | this.statusify.emit('incidents::updated', target); 23 | } 24 | return true; 25 | } 26 | }) 27 | 28 | // 29 | // Constructor 30 | // 31 | constructor(incidentFactory?: (statusify: Statusify) => Promise) { 32 | this.incidentFactory = incidentFactory; 33 | } 34 | 35 | inject(statusify: lib): void { 36 | this.statusify = statusify; 37 | 38 | if(this.incidentFactory) { 39 | this.incidentFactory(statusify).then(incidents => { 40 | incidents.forEach(incident => this.incidents.push(incident)); 41 | }); 42 | } 43 | } 44 | 45 | /** 46 | * @inheritdoc 47 | */ 48 | async getIncidents(_statusify: lib, query: IncidentsQuery = {}): Promise { 49 | return this.filterThroughQuery(this.incidents, query) 50 | } 51 | 52 | /** 53 | * @inheritdoc 54 | */ 55 | async getIncidentsFor(_statusify: lib, component: Component, query?: IncidentsQuery): Promise { 56 | return this.filterThroughQuery(this.incidents, {...query, component: component.id }) 57 | } 58 | 59 | /** 60 | * @inheritdoc 61 | */ 62 | async getIncident(_statusify: lib, id: string): Promise { 63 | return this.incidents.find((i) => i.id === id) || null; 64 | } 65 | 66 | // 67 | // Private 68 | // 69 | private filterThroughQuery(incidents: IIncident[], query: IncidentsQuery): IIncident[] { 70 | const qDate = (value: Date, query: DateQuery) => { 71 | if(query === null) { 72 | return query === value; 73 | } 74 | 75 | if(query.before && !(value.getTime() < query.before.getTime()) ) { 76 | return false; 77 | } 78 | if(query.after && !(value.getTime() > query.after.getTime()) ) { 79 | return false; 80 | } 81 | } 82 | 83 | return incidents.filter((i) => { 84 | if(query.createdAt !== undefined && qDate(i.createdAt, query.createdAt) === false) return false; 85 | if(query.updatedAt !== undefined && qDate(i.createdAt, query.updatedAt) === false) return false; 86 | if(query.resolvedAt !== undefined && qDate(i.createdAt, query.resolvedAt) === false) return false; 87 | 88 | if(query.component && i.components.find(c => c.id === query.component) === undefined) return false; 89 | if(query.id && query.id !== i.id) return false; 90 | 91 | return true; 92 | }) 93 | } 94 | } -------------------------------------------------------------------------------- /example/index.ts: -------------------------------------------------------------------------------- 1 | import Statusify from '../packages/core/lib' 2 | import { Builder, group, component } from '../packages/core/dist/Builder' 3 | import { runnableSeverity } from '../packages/core/dist/Severity/RunnableSeverity' 4 | import IncidentSeverityCalculator from '../packages/core/dist/Severity/IncidentSeverityCalculator' 5 | import LokiIncidentProvider from './LokiIncidentProvider'; 6 | 7 | import UptimeRobotCore from '../packages/uptimerobot/dist/index' 8 | import UptimeRobotLatency from '../packages/uptimerobot/dist/Metric/UptimeRobotLatency' 9 | import UptimeRobotDowntime from '../packages/uptimerobot/dist/Metric/UptimeRobotDowntime' 10 | 11 | 12 | const uptimeRobotCore = new UptimeRobotCore('ur488195-bd46852677deb5ca10988538'); 13 | 14 | const built = new Builder() 15 | .groups([ 16 | group() 17 | .name('Test Group') 18 | .description('Test Groups Description') 19 | .components([ 20 | component('component-1') 21 | .name('Test Component 1') 22 | .description('Test Component 1 Description') 23 | .metric(new UptimeRobotLatency(uptimeRobotCore, {name: 'Latency', monitorID: 780071088, id: 'ur-latency-1'})) 24 | .metric(new UptimeRobotDowntime(uptimeRobotCore, {name: 'Downtime', monitorID: 780071088, id: 'ur-downtime-1'})), 25 | 26 | 27 | component('component-2') 28 | .name('Test Component 2') 29 | ]), 30 | 31 | group() 32 | .name('Test Group 2') 33 | .components([ 34 | component('component-3') 35 | .name('Test Component 3') 36 | .description('Test Component 3 Description'), 37 | 38 | component('component-4') 39 | .name('Test Component 4') 40 | ]), 41 | ]) 42 | 43 | .severities([ 44 | runnableSeverity('operational') 45 | .name('Operational') 46 | .runnable(async (component) => { 47 | return true 48 | }), 49 | 50 | runnableSeverity('partial') 51 | .name('Partial') 52 | .runnable(async (component) => { 53 | return false 54 | }), 55 | 56 | runnableSeverity('minor') 57 | .name('Minor') 58 | .runnable(async (component) => { 59 | return false 60 | }), 61 | 62 | runnableSeverity('major') 63 | .name('Major') 64 | .runnable(async (component) => { 65 | return false 66 | }), 67 | ]) 68 | ; 69 | 70 | export const statusify = new Statusify({ 71 | componentProvider: built, 72 | severityProvider: built, 73 | incidentProvider: new LokiIncidentProvider(), 74 | severityCalculator: new IncidentSeverityCalculator() 75 | }); 76 | 77 | export default statusify; 78 | 79 | // const groups = await statusify.getComponentGroups() 80 | 81 | // console.log(await statusify.getIncidentsFor(groups[0].components[0], {})) 82 | // console.log(groups) 83 | // console.log(await statusify.getIncidents()) 84 | // console.log(await statusify.getSeverityForGroup(groups[0])) 85 | // console.log(await statusify.getSeverityForComponent(groups[0].components[0])) 86 | // console.log((await groups[0].getComponents())[0].metrics) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |
6 | 7 |

8 | A library connecting status platforms into a single glorious integration 9 |

10 | 11 |

12 | TODO: Insert Badges 13 |

14 | 15 |

16 | Contribute 17 | · 18 | Getting Started 19 |

20 | 21 | 22 | --- 23 | 24 | ## Status of This Project 25 | This branch of the project is currently under construction, production usage isn't recommended but possible with some work. Contributions are welcome. 26 | 27 | ## Here for a Status Page? 28 | If you're here for a status page then you'll want to look at our [React Frontend](./packages/react) 29 | 30 |

31 | 32 |

33 | 34 | ## Structure 35 | This project uses a Monorepo structure managed with [LernaJS](https://lerna.js.org/) 36 | 37 | | Package | Description | 38 | | :---------------------------------- | :---------------------------: | 39 | | [core](packages/core) | Core Connection Library | 40 | | [loki](packages/loki) | Loki Incident Provider | 41 | | [next](packages/next) | Deprecated NextJS Frontend | 42 | | [react](packages/react) | React Frontend | 43 | | [uptimerobot](packages/uptimerobot) | UptimeRobot Metric Providers | 44 | 45 | 46 | ## Features 47 | - Platform Agnostic Structure 48 | - Incident Reporting 49 | - Component Grouping 50 | - Support for Downtime, Latency, and Custom Metrics 51 | - Support for Custom Attributes 52 | 53 | ## Installation 54 | ```bash 55 | $ yarn bootstrap 56 | $ yarn build 57 | ``` 58 | 59 | Then follow individual package documentation. 60 | 61 | ## Contributing 62 | Read through our [contributing guidelines](./CONTRIBUTING.md) to learn about the contribution process and style guides. 63 | 64 | ## Configuration 65 | Configuration is currently mainly supported through a builder pattern; however, this is not enforced. 66 | ```ts 67 | const builder = new Builder() 68 | .groups([ 69 | group() 70 | .name('Group 1') 71 | .description('Group 1 Description') 72 | .components([ 73 | component('component-1') 74 | .name('Component 1') 75 | .description('Component 1 Description'), 76 | component('component-2') 77 | .name('Component 2') 78 | ]), 79 | group() 80 | .name('Group 2') 81 | .components([ 82 | component('component-3') 83 | .name('Component 3') 84 | .description('Test Component 3 Description'), 85 | ]), 86 | ]) 87 | .severities([ 88 | runnableSeverity('operational') 89 | .name('Operational') 90 | .runnable(async (component) => true), 91 | 92 | runnableSeverity('partial') 93 | .name('Partial') 94 | .runnable(async (component) => false), 95 | 96 | runnableSeverity('minor') 97 | .name('Minor') 98 | .runnable(async (component) => false), 99 | 100 | runnableSeverity('major') 101 | .name('Major') 102 | .runnable(async (component) => false), 103 | ]) 104 | ; 105 | 106 | export const statusify = new Statusify({ 107 | componentProvider: builder, 108 | severityProvider: builder, 109 | incidentProvider: new ArrayIncidentProvider(), 110 | severityCalculator: new AchievedSeverityCalculator(), 111 | }); 112 | ``` -------------------------------------------------------------------------------- /packages/core/lib/Builder/ComponentBuilder.ts: -------------------------------------------------------------------------------- 1 | import AttributeStorageBuilder from "./AttributeStorageBuilder" 2 | import Component from "../Component/Component" 3 | import ComponentGroup from "../Component/ComponentGroup" 4 | import IProvidesComponents from "../Component/IProvidesComponents" 5 | import Metric from "../Metric/Metric" 6 | import MetricRecord from "../Metric/MetricRecord" 7 | import Statusify from ".." 8 | 9 | export class ComponentBuilderMixin implements IProvidesComponents { 10 | _groups: ComponentGroupBuilder[] = []; 11 | 12 | groups(builders: ComponentGroupBuilder[]) { 13 | this._groups = builders; 14 | return this; 15 | } 16 | 17 | /** 18 | * Gets the component groups for the service 19 | * @param statusify Statusify core 20 | */ 21 | async getComponentGroups(_statusify: Statusify): Promise { 22 | return this._groups.map(g => g.build()); 23 | } 24 | 25 | /** 26 | * Gets the components for the service 27 | * @param statusify Statusify core 28 | */ 29 | async getComponents(statusify: Statusify): Promise { 30 | const components: Component[] = []; 31 | 32 | (await this.getComponentGroups(statusify)).forEach(async (group) => { 33 | components.push(...(await group.getComponents())); 34 | }) 35 | 36 | return components; 37 | } 38 | 39 | /** 40 | * Gets a component by its ID 41 | * @param statusify Statusify core 42 | * @param id Component ID 43 | */ 44 | async getComponent(statusify: Statusify, id: string): Promise { 45 | const found = (await this.getComponents(statusify)).find(c => c.id === id); 46 | return (found === undefined) ? null : found; 47 | } 48 | } 49 | 50 | /** 51 | * Group Builder 52 | */ 53 | export class ComponentGroupBuilder extends AttributeStorageBuilder { 54 | protected _name?: string 55 | protected _description?: string 56 | protected _components: ComponentBuilder[] = [] 57 | 58 | public name(name: string) { 59 | this._name = name; 60 | return this; 61 | } 62 | 63 | public description(description: string) { 64 | this._description = description; 65 | return this; 66 | } 67 | 68 | public components(builders: ComponentBuilder[]) { 69 | this._components = builders; 70 | return this; 71 | } 72 | 73 | /** 74 | * @ignore 75 | */ 76 | public build() { 77 | const group = new ComponentGroup({ 78 | name: this._name, 79 | description: this._description, 80 | attributes: this._attributes, 81 | }) 82 | 83 | group.addComponents(this._components.map(c => c.build())); 84 | return group; 85 | } 86 | } 87 | 88 | export function group() { 89 | return new ComponentGroupBuilder(); 90 | } 91 | 92 | /** 93 | * Component Builder 94 | */ 95 | export class ComponentBuilder extends AttributeStorageBuilder { 96 | protected _id: string 97 | protected _name?: string 98 | protected _description?: string 99 | protected _metrics?: Metric[] 100 | 101 | constructor(id: string) { 102 | super(); 103 | this._id = id; 104 | } 105 | 106 | public name(name: string) { 107 | this._name = name; 108 | return this; 109 | } 110 | 111 | public description(description: string) { 112 | this._description = description; 113 | return this; 114 | } 115 | 116 | public metric(metric: Metric) { 117 | if(this._metrics === undefined) this._metrics = []; 118 | this._metrics.push(metric); 119 | return this; 120 | } 121 | 122 | /** 123 | * @ignore 124 | */ 125 | public build() { 126 | return new Component({ 127 | id: this._id, 128 | name: this._name, 129 | description: this._description, 130 | metrics: this._metrics, 131 | attributes: this._attributes, 132 | }); 133 | } 134 | } 135 | 136 | export function component(id: string) { 137 | return new ComponentBuilder(id) 138 | } -------------------------------------------------------------------------------- /packages/uptimerobot/lib/index.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance } from "axios"; 2 | 3 | import { CACHE_LIFETIME } from "./constants"; 4 | import IMetricRange from "@statusify/core/dist/Metric/IMetricRange"; 5 | import IUptimeRobotMonitorResponse from "./Types/IUptimeRobotMonitorResponse"; 6 | import { MetricType } from "@statusify/core/dist/Metric/Metric"; 7 | import dayjs from "dayjs"; 8 | import useCache from "./Util/useCache"; 9 | import useSomewhatSingleton from "./Util/useSomewhatSingleton"; 10 | 11 | export default class UptimeRobotCore { 12 | // Constants 13 | private static readonly API_URL = 'https://api.uptimerobot.com/v2/'; 14 | 15 | // Instance 16 | public readonly axios: AxiosInstance; 17 | public readonly getDowntimeRes: (key: IMetricRange, ignoreCache?: boolean) => Promise; 18 | public readonly getLatencyRes: (key: IMetricRange, ignoreCache?: boolean) => Promise; 19 | 20 | // Stores monitor IDs that are in use so that they can all be done in 1 request 21 | private monitorIds: {[index in MetricType]: number[]} = { 22 | downtime: [], 23 | latency: [], 24 | custom: [] 25 | } 26 | 27 | // 28 | // Constructor 29 | // 30 | constructor(apiKey: string, cacheLifetime: number = CACHE_LIFETIME) { 31 | this.axios = axios.create({ 32 | baseURL: UptimeRobotCore.API_URL, 33 | params: { 34 | api_key: apiKey, 35 | format: 'json' 36 | } 37 | }); 38 | 39 | this.getDowntimeRes = useSomewhatSingleton( 40 | useCache(cacheLifetime * 1000, this.getDowntimes.bind(this))[0] 41 | ); 42 | this.getLatencyRes = useSomewhatSingleton( 43 | useCache(cacheLifetime * 1000, this.getLatencies.bind(this))[0] 44 | ); 45 | } 46 | 47 | // 48 | // Public 49 | // 50 | public useMonitor(id: number, type: MetricType) { 51 | this.monitorIds[type].push(id); 52 | } 53 | 54 | public async getMonitor(key: IMetricRange, id: number, type: MetricType, ignoreCache?: boolean) { 55 | let res: IUptimeRobotMonitorResponse; 56 | 57 | switch(type) { 58 | case MetricType.DOWNTIME: 59 | res = await this.getDowntimeRes(key, ignoreCache); 60 | break; 61 | case MetricType.LATENCY: 62 | res = await this.getLatencyRes(key, ignoreCache); 63 | break; 64 | default: 65 | throw new Error("Attempted to get unsupported UptimeRobot monitor type."); 66 | } 67 | 68 | return res.monitors.find(m => m.id === id); 69 | } 70 | 71 | // 72 | // Private 73 | // 74 | private async getDowntimes(range: IMetricRange) { 75 | const days = Math.round(dayjs(range.end).diff(dayjs(range.start), 'days')); 76 | 77 | const ranges = [...Array(days)].map((_, i) => { 78 | const d = dayjs().subtract(i, 'days'); 79 | return `${d.startOf('day').unix()}_${d.endOf('day').unix()}` 80 | }).join('-'); 81 | 82 | 83 | const { data } = await this.axios.post('getMonitors', { 84 | monitors: this.monitorIds.downtime.join('-'), 85 | custom_uptime_ranges: ranges, 86 | custom_down_durations: 1 87 | }); 88 | 89 | return data as IUptimeRobotMonitorResponse; 90 | } 91 | 92 | private async getLatencies(range: IMetricRange) { 93 | const { data } = await this.axios.post('getMonitors', { 94 | monitors: this.monitorIds.latency.join('-'), 95 | response_times: 1, 96 | response_times_start_date: (range.start.getTime() / 1000), 97 | response_times_end_date: (range.end.getTime() / 1000) 98 | }); 99 | 100 | return data as IUptimeRobotMonitorResponse; 101 | } 102 | } -------------------------------------------------------------------------------- /packages/react/src/app/components/elements/component/metrics/tickChart/ComponentTickChart.tsx: -------------------------------------------------------------------------------- 1 | import { Box, BoxProps } from "@chakra-ui/layout"; 2 | 3 | import ComponentTickChartTooltip from "./ComponentTickChartTooltip"; 4 | import ISeverityTick from "../../../../../interfaces/ISeverityTick"; 5 | import React from "react"; 6 | import { useLaminar } from "../../../../../contexts/LaminarContext"; 7 | import useMetricRange from "../../../../../hooks/useMetricRange"; 8 | import { useResponsiveViewbox } from "../../../../../contexts/ResponsiveViewboxContext"; 9 | import useSeverityTicks from "../../../../../hooks/useSeverityTicks"; 10 | 11 | const TickRectStyle: BoxProps['sx'] = { 12 | height: '20px', 13 | width: '3px', 14 | transition: 'all .1s ease', 15 | transform: 'scaleY(1)', 16 | transformOrigin: 'center center', 17 | 18 | '&.hvr-1': { 19 | transform: 'scaleY(1.4)' 20 | }, 21 | '&.hvr-2': { 22 | transform: 'scaleY(1.2)' 23 | }, 24 | '&.hvr-3': { 25 | transform: 'scaleY(1.1)' 26 | }, 27 | }; 28 | 29 | interface ICurrentTick extends ISeverityTick { 30 | element: HTMLElement; 31 | } 32 | 33 | export default function ComponentTickChart() { 34 | const [ isFocused, setIsFocused ] = React.useState(false); 35 | const [ currentTick, setCurrentTick ] = React.useState(undefined); 36 | const svgContainerRef = React.createRef(); 37 | 38 | const { severityColors } = useLaminar(); 39 | const viewbox = useResponsiveViewbox(); 40 | const range = useMetricRange(); 41 | const ticks = useSeverityTicks(range); 42 | 43 | // Hover Effect 44 | const clearHoverEffect = React.useCallback(() => { 45 | if(!svgContainerRef) return; 46 | const svgRef = svgContainerRef.current?.firstChild as HTMLElement; 47 | 48 | for(let i = 0; i < svgRef?.children.length; i++) { 49 | svgRef.children[i].classList.remove('hvr-1', 'hvr-2', 'hvr-3'); 50 | } 51 | }, [ svgContainerRef ]); 52 | 53 | const mouseEnter = (event: React.MouseEvent, i: number) => { 54 | clearHoverEffect(); 55 | 56 | event.currentTarget.classList.add('hvr-1'); 57 | 58 | event.currentTarget.previousElementSibling?.classList.add('hvr-2') 59 | event.currentTarget.previousElementSibling?.previousElementSibling?.classList.add('hvr-3') 60 | 61 | event.currentTarget.nextElementSibling?.classList.add('hvr-2') 62 | event.currentTarget.nextElementSibling?.nextElementSibling?.classList.add('hvr-3') 63 | 64 | // Set as last tick 65 | setCurrentTick({ 66 | ...ticks[i], 67 | element: event.currentTarget as unknown as HTMLElement 68 | }); 69 | } 70 | 71 | // Clear Effects on Unfocus 72 | React.useEffect(() => { 73 | if(isFocused === false) { 74 | clearHoverEffect(); 75 | } 76 | }, [ isFocused, clearHoverEffect ]); 77 | 78 | return ( 79 | { setIsFocused(true) }} 86 | onMouseLeave={() => { setIsFocused(false); setCurrentTick(undefined) }} 87 | ref={svgContainerRef} 88 | > 89 | 96 | { 97 | ticks.map((tick, i) => ( 98 | { 107 | mouseEnter(e, i) 108 | }) as any} 109 | /> 110 | )) 111 | } 112 | 113 | {isFocused && currentTick !== undefined && } 114 | 115 | ) 116 | } -------------------------------------------------------------------------------- /packages/react/src/app/hooks/useSeverityTicks.ts: -------------------------------------------------------------------------------- 1 | import IDowntimeMetricRecord from "@statusify/core/dist/Metric/IDowntimeMetricRecord"; 2 | import IMetricRange from "@statusify/core/dist/Metric/IMetricRange"; 3 | import ISeverityTick from "../interfaces/ISeverityTick"; 4 | import { MetricType } from "@statusify/core/dist/Metric/Metric"; 5 | import React from "react"; 6 | import Severity from "@statusify/core/dist/Severity/Severity"; 7 | import WorstSeverity from "@statusify/core/dist/Util/WorstSeverity"; 8 | import dayjs from "../utils/dayjs"; 9 | import { useComponent } from "../contexts/ComponentContext"; 10 | import { useLaminar } from "../contexts/LaminarContext"; 11 | import { useStatusify } from "../contexts/StatusifyContext"; 12 | import useIncidents from "./useIncidents"; 13 | import { IncidentsQuery } from "@statusify/core/dist/Incident/IProvidesIncidents"; 14 | 15 | export default function useSeverityTicks(range: IMetricRange) { 16 | const statusify = useStatusify(); 17 | const component = useComponent(); 18 | const { downtimeSeverities } = useLaminar(); 19 | const [ ticks, setTicks ] = React.useState([]); 20 | 21 | const incidentsQuery = React.useMemo((): IncidentsQuery => { 22 | return { 23 | component: component.id, 24 | createdAt: { 25 | after: range.start, 26 | before: range.end 27 | }, 28 | } 29 | }, [ range, component ]); 30 | 31 | const incidents = useIncidents(incidentsQuery); 32 | 33 | React.useMemo(() => { 34 | new Promise(async (resolve, _reject) => { 35 | // Find a downtime metric 36 | const downtimeMetric = component.metrics?.find(m => m.type === MetricType.DOWNTIME); 37 | const downtimes: IDowntimeMetricRecord[] = (downtimeMetric) ? await downtimeMetric.getPeriod(range) : []; 38 | const severities = await statusify.getSeverities(); 39 | 40 | // Calculate some dates 41 | const nStart = dayjs(range.start).startOf('day'); 42 | const nEnd = dayjs(range.end).endOf('day'); 43 | const daysBetween = nEnd.diff(nStart, 'days'); 44 | 45 | // Work on each day to find its tick 46 | const dayTicks = [ ...Array(daysBetween) ].map(async (_, i) => { 47 | const day = dayjs(nStart).add(i + 1, 'days').startOf('day'); 48 | const daySeverities: Severity[] = []; 49 | 50 | const dayIncidents = incidents 51 | .filter((incident) => { 52 | return day.isBetween(incident.createdAt, incident.resolvedAt, 'day', '[]'); 53 | }).map((v) => { 54 | // Add to the severities 55 | daySeverities.push(v.severity); 56 | return v; 57 | }); 58 | 59 | const dayDowntimes: IDowntimeMetricRecord[] = (downtimes === undefined) ? [] : downtimes.filter((downtime) => { 60 | const startedAt = dayjs(downtime.time); 61 | const endedAt = startedAt.add(downtime.value, 'millisecond'); 62 | return day.isBetween(startedAt, endedAt, 'day', '(]') 63 | }).map((v) => { 64 | // Add to the severities based upon the config 65 | const index = Number(Object.keys(downtimeSeverities).reduce((a, b) => { 66 | if(Number(b) > Number(a) && (v.value / 1000 >= Number(b))) { 67 | return b; 68 | } 69 | return a; 70 | })) 71 | 72 | // Use the index to get the severity id and then get the actual severity instance 73 | const foundSeverity = severities.find(s => s.id === downtimeSeverities[index]); 74 | if(foundSeverity !== undefined) { 75 | daySeverities.push(foundSeverity); 76 | } 77 | 78 | return v; 79 | }); 80 | 81 | return { 82 | date: day.toDate(), 83 | severity: await WorstSeverity(daySeverities, statusify), 84 | relatedIncidents: dayIncidents, 85 | relatedDowntimes: dayDowntimes 86 | } 87 | }); 88 | 89 | // Do everything, set the ticks and resolve for some reason 90 | Promise.all(dayTicks).then(setTicks).then(resolve); 91 | }) 92 | }, [ component, incidents, downtimeSeverities, range, statusify ]); 93 | 94 | return ticks; 95 | } -------------------------------------------------------------------------------- /packages/react/src/app/components/elements/component/metrics/latency/ComponentLatencyGraph.tsx: -------------------------------------------------------------------------------- 1 | import { AiOutlineArrowDown, AiOutlineArrowUp } from "react-icons/ai"; 2 | import { Box, Flex, Text } from "@chakra-ui/layout"; 3 | 4 | import Chart from "react-apexcharts"; 5 | import { Collapse } from "@chakra-ui/transition"; 6 | import ILatencyMetricRecord from "@statusify/core/dist/Metric/ILatencyMetricRecord" 7 | import { IconButton } from "@chakra-ui/button"; 8 | import { MetricType } from "@statusify/core/dist/Metric/Metric" 9 | import React from "react"; 10 | import useComponentMetric from "../../../../../hooks/useComponentMetric" 11 | import useMetricRange from "../../../../../hooks/useMetricRange"; 12 | import { useTheme } from "@chakra-ui/system"; 13 | import { useTranslation } from "react-i18next"; 14 | import { ApexOptions } from "apexcharts"; 15 | import { CHART_AVERAGE, CHART_SERIES_NAME, CHART_TITLE } from "../../../../../constants/FrontendOptions"; 16 | 17 | export default function ComponentLatencyGraph() { 18 | const [ lastUpdated, setLastUpdated ] = React.useState(undefined); 19 | const [ visible, setVisible ] = React.useState(false); 20 | 21 | const [ datapoints, setDatapoints ] = React.useState([]); 22 | const [ average, setAverage ] = React.useState(0); 23 | 24 | const { t } = useTranslation(); 25 | const [ latencyMetric, latencyMetricAttributes ] = useComponentMetric(MetricType.LATENCY); 26 | const range = useMetricRange(); 27 | const theme = useTheme(); 28 | 29 | const toggleVisibility = () => setVisible(!visible); 30 | 31 | React.useEffect(() => { 32 | // Don't do anything if it's not visible or we have valid metrics 33 | if(!visible || (lastUpdated && (new Date().getTime() - lastUpdated) < 300 * 1000)) { 34 | return; 35 | } 36 | 37 | // Update the last updated timestamp 38 | setLastUpdated(new Date().getTime()); 39 | latencyMetric.getPeriod(range) 40 | .then(setDatapoints) 41 | .then(() => { 42 | // Prevent double queueing requests 43 | latencyMetric.getAverage(range).then(r => { 44 | setAverage(Math.round(r.value * 100) / 100) 45 | }); 46 | }); 47 | }, [ range, latencyMetric, visible, lastUpdated ]); 48 | 49 | const chartOptions = React.useMemo(() => ({ 50 | chart: { 51 | background: 'transparent', 52 | toolbar: { 53 | tools: { 54 | download: false, 55 | } 56 | }, 57 | foreColor: theme.colors.gray[500], 58 | }, 59 | colors: [ 60 | theme.colors.gray[500], 61 | ], 62 | dataLabels: { 63 | enabled: false 64 | }, 65 | theme: { 66 | mode: 'dark' 67 | }, 68 | xaxis: { 69 | type: 'datetime' 70 | }, 71 | fill: { 72 | type: "gradient", 73 | gradient: { 74 | shadeIntensity: 0, 75 | opacityFrom: .6, 76 | opacityTo: 1, 77 | type: 'vertical' 78 | } 79 | }, 80 | stroke: { 81 | curve: 'smooth', 82 | show: false 83 | } 84 | }), [ theme ]); 85 | 86 | const series = React.useMemo(() => { 87 | return [{ 88 | name: latencyMetricAttributes[CHART_SERIES_NAME] ?? t('components.metrics.latency.seriesName'), 89 | data: datapoints.map(point => ({x: point.time.getTime(), y: point.value })) 90 | }] 91 | }, [ datapoints, latencyMetricAttributes, t ]) 92 | 93 | 94 | return ( 95 | 96 | 97 | 98 | 99 | 100 | {latencyMetricAttributes[CHART_TITLE] ?? t('components.metrics.latency.title')} 101 | 102 | 103 | {latencyMetricAttributes[CHART_AVERAGE] ? latencyMetricAttributes[CHART_AVERAGE].replaceAll('{{average}}', average) : t('components.metrics.latency.average', { average })} 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | : } 117 | 118 | onClick={toggleVisibility} 119 | 120 | _hover={{ 121 | bg: "blackAlpha.300" 122 | }} 123 | _focus={{ 124 | bg: "blackAlpha.300", 125 | border: "1px", 126 | borderColor: "blue.500" 127 | }} 128 | _active={{ 129 | bg: "blackAlpha.400" 130 | }} 131 | /> 132 | 133 | ) 134 | } -------------------------------------------------------------------------------- /packages/core/lib/index.ts: -------------------------------------------------------------------------------- 1 | import IProvidesIncidents, { IncidentsQuery } from './Incident/IProvidesIncidents' 2 | 3 | import Component from "./Component/Component"; 4 | import ComponentGroup from "./Component/ComponentGroup"; 5 | import ICalculatesSeverities from "./Severity/ICalculatesSeverities"; 6 | import IProvidesComponents from "./Component/IProvidesComponents"; 7 | import IProvidesSeverities from "./Severity/IProvidesSeverities"; 8 | import IInjectStatusify from "./Util/IInjectStatusify"; 9 | import StatusifyEvents from "./Util/StatusifyEvents"; 10 | import { TypedEmitter } from "tiny-typed-emitter"; 11 | 12 | export interface StatusifyOptions { 13 | componentProvider: IProvidesComponents; 14 | incidentProvider: IProvidesIncidents; 15 | severityProvider: IProvidesSeverities; 16 | severityCalculator: ICalculatesSeverities; 17 | } 18 | 19 | export default class Statusify extends TypedEmitter { 20 | private componentProvider: IProvidesComponents; 21 | private incidentProvider: IProvidesIncidents; 22 | private severityProvider: IProvidesSeverities; 23 | private severityCalculator: ICalculatesSeverities; 24 | 25 | constructor(options: StatusifyOptions) { 26 | super(); 27 | 28 | this.componentProvider = this.inject(options.componentProvider); 29 | this.severityProvider = this.inject(options.severityProvider); 30 | this.incidentProvider = this.inject(options.incidentProvider); 31 | this.severityCalculator = this.inject(options.severityCalculator); 32 | } 33 | 34 | // 35 | // Components 36 | // 37 | /** 38 | * Gets all component groups 39 | */ 40 | async getComponentGroups() { 41 | return this.componentProvider.getComponentGroups(this); 42 | } 43 | 44 | /** 45 | * Gets all components from component groups 46 | */ 47 | async getComponents(): Promise { 48 | return this.componentProvider.getComponents(this); 49 | } 50 | 51 | /** 52 | * Gets a specific component 53 | * @param id ID of the component 54 | * @return Component if found, otherwise null 55 | */ 56 | async getComponent(id: string) { 57 | return this.componentProvider.getComponent(this, id); 58 | } 59 | 60 | // 61 | // Incidents 62 | // 63 | /** 64 | * Gets a specific incident 65 | * @param id ID of the incident 66 | * @returns Incident if found, otherwise null 67 | */ 68 | async getIncident(id: string) { 69 | return this.incidentProvider.getIncident(this, id); 70 | } 71 | 72 | /** 73 | * Gets all incidents or incidents matching the query 74 | * @param query Optional query to match incidents to 75 | * @returns Array of incidents 76 | */ 77 | async getIncidents(query?: IncidentsQuery) { 78 | return this.incidentProvider.getIncidents(this, query); 79 | } 80 | 81 | /** 82 | * Gets all incidents for a specific component 83 | * @param component Component to get incidents for 84 | * @param query Optional query to match incidents to 85 | * @returns Array of incidents 86 | */ 87 | async getIncidentsFor(component: Component, query?: IncidentsQuery) { 88 | return this.incidentProvider.getIncidentsFor(this, component, query); 89 | } 90 | 91 | // 92 | // Severities 93 | // 94 | /** 95 | * Gets all severities 96 | * @returns Array of severities 97 | */ 98 | async getSeverities() { 99 | return this.severityProvider.getSeverities(this); 100 | } 101 | 102 | /** 103 | * Gets a specific severity 104 | * @param id ID of the severity 105 | * @returns Severity if found, otherwise null 106 | */ 107 | async getSeverity(id: string) { 108 | return this.severityProvider.getSeverity(this, id); 109 | } 110 | 111 | /** 112 | * Gets the current severity for a component 113 | * @param component Component to get the severity for 114 | * @returns Severity of the component 115 | */ 116 | async getSeverityForComponent(component: Component) { 117 | return this.severityCalculator.getSeverityForComponent(component, this); 118 | } 119 | 120 | /** 121 | * Gets the current severity for a group 122 | * @param group Component group to get the severity for 123 | * @returns Severity of the component group 124 | */ 125 | async getSeverityForGroup(group: ComponentGroup) { 126 | return this.severityCalculator.getSeverityForGroup(group, this); 127 | } 128 | 129 | /** 130 | * Gets the global severity 131 | * @returns Global severity 132 | */ 133 | async getGlobalSeverity() { 134 | return this.severityCalculator.getGlobalSeverity(this); 135 | } 136 | 137 | // 138 | // Private 139 | // 140 | private isInjectable(object: any): object is IInjectStatusify { 141 | return 'inject' in object; 142 | } 143 | 144 | private inject(instance: any) { 145 | if(this.isInjectable(instance)) { 146 | instance.inject(this); 147 | } 148 | 149 | return instance; 150 | } 151 | } -------------------------------------------------------------------------------- /packages/react/src/pages/IncidentPage.tsx: -------------------------------------------------------------------------------- 1 | import { Badge, Box, Container, Flex, Grid, Heading, Link } from "@chakra-ui/layout"; 2 | import IIncident from "@statusify/core/dist/Incident/IIncident"; 3 | import Severity from "@statusify/core/dist/Severity/Severity"; 4 | import dayjs from "dayjs"; 5 | import React from "react"; 6 | import { useTranslation } from "react-i18next"; 7 | import { useParams } from "react-router"; 8 | import { Link as RouterLink } from "react-router-dom"; 9 | import IncidentUpdate from "../app/components/elements/incident/IncidentUpdate"; 10 | import DefaultLayout from "../app/components/layouts/DefaultLayout"; 11 | import { useStatusify } from "../app/contexts/StatusifyContext"; 12 | import useSeverityColor from "../app/hooks/useSeverityColor"; 13 | import useStatusifyEvent from "../app/hooks/useStatusifyEvent"; 14 | 15 | const IncidentPage: React.FC = () => { 16 | const statusify = useStatusify(); 17 | const { t } = useTranslation(); 18 | 19 | const { id } = useParams<{ id: string }>(); 20 | const [ incident, setIncident ] = React.useState(undefined); 21 | 22 | const [ lastSeverity, setLastSeverity ] = React.useState(); 23 | const severityColor = useSeverityColor(lastSeverity, 'gray'); 24 | 25 | const fetchIncident = React.useCallback(async () => { 26 | statusify.getIncident(id).then((incident) => { 27 | setIncident(incident); 28 | 29 | if(incident) { 30 | setLastSeverity((incident.updates.length === 0) ? incident.severity : incident.updates[0].severity); 31 | } 32 | }); 33 | }, [ id, statusify ]); 34 | 35 | React.useEffect(() => { 36 | let mounted = true; 37 | 38 | if(mounted) { 39 | fetchIncident(); 40 | } 41 | 42 | return () => { 43 | mounted = false; 44 | } 45 | }, [ fetchIncident ]); 46 | 47 | useStatusifyEvent('incidents::updated', fetchIncident); 48 | 49 | return ( 50 | 51 | {incident && ( 52 | <> 53 | 54 | 64 | 65 | 72 | {t(`incidents.badge.${incident.resolvedAt ? 'closed' : 'open'}`)} 73 | 74 | 75 | 76 | {incident.name} 77 | 78 | 79 | 80 | 81 | {incident.resolvedAt && ( 82 | 83 | {t('incidents.resolvedAfter', {duration: dayjs.duration(incident.resolvedAt.getTime() - incident.createdAt.getTime()).humanize() })} 84 | 85 | )} 86 | 87 | 88 | 89 | {dayjs(incident.createdAt).format(t('incidents.overallFormat'))} 90 | {incident.resolvedAt && ( 91 | <> - {dayjs(incident.resolvedAt).format(t('incidents.overallFormat'))} 92 | )} 93 | 94 | 95 | 96 | 97 | 98 | {incident.components 99 | .filter((component) => component.name !== undefined) 100 | .map((component) => ( 101 | 111 | {component.name} 112 | 113 | ))} 114 | 115 | 116 | 117 | 118 | 119 | 120 | 125 | {incident.updates.map((update, i) => )} 126 | 127 | 128 | 129 | )} 130 | 131 | 132 | ); 133 | } 134 | 135 | 136 | 137 | export default IncidentPage; -------------------------------------------------------------------------------- /example/LokiIncidentProvider.ts: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | const loki = require('lokijs') 4 | import Component from "../packages/core/lib/Component/Component"; 5 | import IIncident from "../packages/core/lib/Incident/IIncident"; 6 | import IIncidentUpdate from "../packages/core/lib/Incident/IIncidentUpdate"; 7 | import IProvidesIncidents, { DateQuery, IncidentsQuery } from "../packages/core/lib/Incident/IProvidesIncidents"; 8 | import Statusify from "../packages/core/lib"; 9 | 10 | export default class LokiIncidentProvider implements IProvidesIncidents { 11 | private db = new loki('incidents') 12 | 13 | private incidents: Collection 14 | 15 | /** 16 | * LokiIncidentProvider Constructor 17 | */ 18 | constructor() { 19 | this.incidents = this.db.addCollection('incidents') 20 | 21 | // Load files 22 | const files = fs.readdirSync(path.join(__dirname, 'incidents')) 23 | for(const file of files) { 24 | this.incidents.insert( require(path.join(__dirname, 'incidents', file)) ) 25 | } 26 | } 27 | 28 | /** 29 | * Gets incidents matching the query 30 | * @param statusify Statusify Instance 31 | * @param query Query 32 | */ 33 | async getIncidents(statusify: Statusify, query: IncidentsQuery = {}): Promise { 34 | return Promise.all( 35 | this.incidents 36 | .find(this.buildQuery(query)) 37 | .map(i => this.parseIncident(i, statusify)) 38 | ) 39 | } 40 | 41 | /** 42 | * Gets incidents matching the query 43 | * @param statusify Statusify Instance 44 | * @param query Query 45 | */ 46 | async getIncidentsFor(statusify: Statusify, component: Component, query: IncidentsQuery): Promise { 47 | return Promise.all( 48 | this.incidents 49 | .find({...this.buildQuery(query), components: {$contains: component.id} }) 50 | .map(i => this.parseIncident(i, statusify)) 51 | ) 52 | } 53 | 54 | /** 55 | * Gets an incident matching the query 56 | * @param statusify Statusify Instance 57 | * @param query Query 58 | */ 59 | async getIncident(statusify: Statusify, id: string): Promise { 60 | const found = this.incidents.findOne({ id }) 61 | return (found === null) ? null : await this.parseIncident(found, statusify) 62 | } 63 | 64 | /** 65 | * Converts a loki entry for an incident into a Statusify Incident interface object 66 | * @param incident 67 | * @param statusify 68 | */ 69 | private async parseIncident(incident: any, statusify: Statusify): Promise { 70 | const updates = incident.updates.map(async (u) => { 71 | const update: IIncidentUpdate = { 72 | severity: await statusify.getSeverity(u.severity), 73 | body: u.body, 74 | bodyStatus: u.body_status, 75 | createdAt: new Date(u.created_at), 76 | updatedAt: new Date(u.updated_at) 77 | } 78 | return update 79 | }) as IIncidentUpdate[] 80 | 81 | const parsed: IIncident = { 82 | id: incident.id, 83 | name: incident.name, 84 | 85 | body: incident.body, 86 | bodyStatus: incident.body_status, 87 | 88 | severity: await statusify.getSeverity(incident.severity), 89 | components: (await statusify.getComponents()).filter(c => incident.components.includes(c.id)), 90 | updates: await Promise.all(updates), 91 | 92 | scheduledFor: this.nullableDate(incident.scheduled_for), 93 | scheduledUntil: this.nullableDate(incident.schedule_until), 94 | resolvedAt: this.nullableDate(incident.resolved_at), 95 | 96 | createdAt: new Date(incident.created_at), 97 | updatedAt: new Date(incident.updated_at) 98 | } 99 | 100 | return parsed as IIncident 101 | } 102 | 103 | /** 104 | * Builds a loki Mongo type query based on the incident query 105 | * @param query 106 | */ 107 | private buildQuery(query: IncidentsQuery): any { 108 | const qComp = {} 109 | 110 | if(query.createdAt !== undefined) { 111 | qComp['created_at'] = this.buildDateQuery(query.createdAt) 112 | } 113 | 114 | if(query.updatedAt !== undefined) { 115 | qComp['updated_at'] = this.buildDateQuery(query.updatedAt) 116 | } 117 | 118 | if(query.resolvedAt !== undefined) { 119 | qComp['resolved_at'] = this.buildDateQuery(query.resolvedAt) 120 | } 121 | 122 | if(query.id !== undefined) { 123 | qComp['id'] = query.id 124 | } 125 | 126 | return qComp 127 | } 128 | 129 | private buildDateQuery(query: DateQuery): any { 130 | const qComp = {} 131 | 132 | if(query === undefined || query === null) { 133 | return undefined 134 | } 135 | 136 | if(query.after === undefined) { 137 | qComp['$gt'] = query.after 138 | } 139 | 140 | if(query.before === undefined) { 141 | qComp['$lt'] = query.after 142 | } 143 | } 144 | 145 | private nullableDate(val: any) { 146 | if(val === null || val === undefined) { 147 | return undefined 148 | } 149 | 150 | return new Date(val) 151 | } 152 | } -------------------------------------------------------------------------------- /packages/react/src/StatusifyInstance.ts: -------------------------------------------------------------------------------- 1 | import { ANONYMOUS, CHART_AVERAGE, CHART_SERIES_NAME, CHART_TITLE, COLLAPSED, COLLAPSIBLE } from './app/constants/FrontendOptions' 2 | import { Builder, component, group } from '@statusify/core/dist/Builder' 3 | 4 | import AchievedSeverityCalculator from '@statusify/core/dist/Severity/AchievedSeverityCalculator' 5 | import ArrayIncidentProvider from '@statusify/core/dist/Incident/ArrayIncidentProvider' 6 | import IncidentSeverityCalculator from '@statusify/core/dist/Severity/IncidentSeverityCalculator' 7 | import SeverityMultiplexer from '@statusify/core/dist/Severity/SeverityMultiplexer' 8 | import Statusify from '@statusify/core/dist' 9 | import UptimeRobotCore from '@statusify/uptimerobot/dist' 10 | import UptimeRobotDowntime from '@statusify/uptimerobot/dist/Metric/UptimeRobotDowntime' 11 | import UptimeRobotLatency from '@statusify/uptimerobot/dist/Metric/UptimeRobotLatency' 12 | import { runnableSeverity } from '@statusify/core/dist/Severity/RunnableSeverity' 13 | 14 | const uptimeRobotCore = new UptimeRobotCore('ur488195-bd46852677deb5ca10988538'); 15 | 16 | const built = new Builder() 17 | .groups([ 18 | group() 19 | .name('Servers') 20 | .description('Our core services are hosted here.') 21 | .attribute(COLLAPSED, false) // Make the group expanded automatically 22 | .components([ 23 | component('vps') 24 | .name('VPS') 25 | .description('The core of our services.') 26 | .metric( 27 | new UptimeRobotLatency(uptimeRobotCore, {name: 'Latency', monitorID: 780071088, id: 'vps-latency'}) 28 | .attribute(CHART_TITLE, 'Latency') 29 | .attribute(CHART_SERIES_NAME, 'Latency (ms)') 30 | .attribute(CHART_AVERAGE, 'Averaging {{average}}ms') 31 | ) 32 | .metric(new UptimeRobotDowntime(uptimeRobotCore, {name: 'Downtime', monitorID: 780071088, id: 'vps-downtime'})), 33 | 34 | 35 | component('demo-pages') 36 | .name('Demo Pages') 37 | .metric(new UptimeRobotLatency(uptimeRobotCore, {name: 'Latency', monitorID: 779382341, id: 'demo-pages-latency'})) 38 | .metric(new UptimeRobotDowntime(uptimeRobotCore, {name: 'Downtime', monitorID: 779382341, id: 'demo-pages-downtime'})) 39 | ]), 40 | 41 | group() 42 | .name('Test Group 2') 43 | // If a group is both noncollapsible and anonymous then the header is hidden 44 | .attribute(COLLAPSIBLE, false) // An noncollapsible group is automatically expanded 45 | .attribute(ANONYMOUS, true) // Anonymous groups are automatically expanded 46 | .components([ 47 | component('component-3') 48 | .name('Test Component 3') 49 | .description('Test Component 3 Description'), 50 | 51 | component('component-4') 52 | .name('Test Component 4') 53 | ]), 54 | ]) 55 | 56 | .severities([ 57 | runnableSeverity('operational') 58 | .name('Operational') 59 | .runnable(async (component) => { 60 | return true 61 | }), 62 | 63 | runnableSeverity('partial') 64 | .name('Partial') 65 | .runnable(async (component) => { 66 | return false 67 | }), 68 | 69 | runnableSeverity('minor') 70 | .name('Minor') 71 | .runnable(async (component) => { 72 | return false 73 | }), 74 | 75 | runnableSeverity('major') 76 | .name('Major') 77 | .runnable(async (component) => { 78 | return false 79 | }), 80 | ]) 81 | ; 82 | 83 | const incidentProvider = new ArrayIncidentProvider(async (statusify) => [ 84 | { 85 | id: 'test-incident', 86 | name: 'Incident on Cluster SBG-1', 87 | body: 'We are encountering a partial outage that impacts some API calls.', 88 | bodyStatus: 'Partial', 89 | updates: [ 90 | { 91 | body: 'The incident has been resolved.', 92 | bodyStatus: 'Operational', 93 | severity: await statusify.getSeverity('operational'), 94 | createdAt: new Date(1620405538852), 95 | updatedAt: new Date(), 96 | }, 97 | { 98 | body: 'All API calls are now impacted, our teams are working hard to get everything back on track.', 99 | bodyStatus: 'Major', 100 | severity: await statusify.getSeverity('major'), 101 | createdAt: new Date(1620403538852), 102 | updatedAt: new Date(), 103 | }, 104 | ], 105 | resolvedAt: new Date(1620405538852), 106 | severity: await statusify.getSeverity('partial'), 107 | components: [ 108 | await statusify.getComponent('vps') 109 | ], 110 | createdAt: new Date(1620403138852), 111 | updatedAt: new Date(), 112 | } 113 | ]); 114 | 115 | export const statusify = new Statusify({ 116 | componentProvider: built, 117 | severityProvider: built, 118 | incidentProvider: incidentProvider, 119 | severityCalculator: new SeverityMultiplexer([ 120 | new IncidentSeverityCalculator(), 121 | new AchievedSeverityCalculator() 122 | ]) 123 | }); 124 | 125 | export default statusify; -------------------------------------------------------------------------------- /packages/loki/lib/LokiIncidentProvider.ts: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | const loki = require('lokijs') 4 | 5 | import IProvidesIncidents, { DateQuery, IncidentsQuery } from "@statusify/core/dist/Incident/IProvidesIncidents"; 6 | 7 | import Component from "@statusify/core/dist/Component/Component"; 8 | import IIncident from "@statusify/core/dist/Incident/IIncident"; 9 | import IIncidentUpdate from "@statusify/core/dist/Incident/IIncidentUpdate"; 10 | import Statusify from "@statusify/core/dist"; 11 | 12 | export interface LokiIncidentProviderOptions { 13 | autoLoadPath?: string // Path to load files from 14 | } 15 | 16 | export default class LokiIncidentProvider implements IProvidesIncidents { 17 | protected options: LokiIncidentProviderOptions 18 | public db = new loki('incidents') 19 | public incidents: Collection 20 | 21 | /** 22 | * LokiIncidentProvider Constructor 23 | */ 24 | constructor(options: LokiIncidentProviderOptions = {}) { 25 | this.incidents = this.db.addCollection('incidents') 26 | 27 | this.options = options 28 | 29 | // Load files 30 | if(options.autoLoadPath !== undefined) { 31 | this.load(); 32 | } 33 | } 34 | 35 | load() { 36 | const files = fs.readdirSync(this.options.autoLoadPath) 37 | for(const file of files) { 38 | this.incidents.insert( require(path.join(this.options.autoLoadPath, file)) ) 39 | } 40 | } 41 | 42 | /** 43 | * Gets incidents matching the query 44 | * @param statusify Statusify Instance 45 | * @param query Query 46 | */ 47 | async getIncidents(statusify: Statusify, query: IncidentsQuery = {}): Promise { 48 | const chain = this.incidents.chain() 49 | .find(this.buildQuery(query)); 50 | 51 | if(query.limit) { 52 | chain.limit(query.limit); 53 | } 54 | 55 | if(query.offset) { 56 | chain.offset(query.offset); 57 | } 58 | 59 | return Promise.all(chain.data().map(i => this.parseIncident(i, statusify))) 60 | } 61 | 62 | /** 63 | * Gets incidents matching the query 64 | * @param statusify Statusify Instance 65 | * @param query Query 66 | */ 67 | async getIncidentsFor(statusify: Statusify, component: Component, query: IncidentsQuery = {}): Promise { 68 | const chain = this.incidents.chain() 69 | .find({...this.buildQuery(query), components: {$contains: component.id} }); 70 | 71 | if(query.limit) { 72 | chain.limit(query.limit); 73 | } 74 | 75 | if(query.offset) { 76 | chain.offset(query.offset); 77 | } 78 | 79 | return Promise.all(chain.data().map(i => this.parseIncident(i, statusify))); 80 | } 81 | 82 | /** 83 | * Gets an incident matching the query 84 | * @param statusify Statusify Instance 85 | * @param query Query 86 | */ 87 | async getIncident(statusify: Statusify, id: string): Promise { 88 | const found = this.incidents.findOne({ id }) 89 | return (found === null) ? null : await this.parseIncident(found, statusify) 90 | } 91 | 92 | /** 93 | * Converts a loki entry for an incident into a Statusify Incident interface object 94 | * @param incident 95 | * @param statusify 96 | */ 97 | private async parseIncident(incident: any, statusify: Statusify): Promise { 98 | const updates = incident.updates.map(async (u: any) => { 99 | const severity = await statusify.getSeverity(u.severity); 100 | if(!severity) { 101 | throw new Error(`LokiIncidentProvider: Incident "${incident.name}", update "${u.name}" references unknown severity "${u.severity}"`); 102 | } 103 | 104 | const update: IIncidentUpdate = { 105 | severity, 106 | body: u.body, 107 | bodyStatus: u.body_status, 108 | createdAt: new Date(u.created_at), 109 | updatedAt: new Date(u.updated_at) 110 | } 111 | return update 112 | }) as IIncidentUpdate[] 113 | 114 | const severity = await statusify.getSeverity(incident.severity); 115 | if(!severity) { 116 | throw new Error(`LokiIncidentProvider: Incident "${incident.name}" references unknown severity "${incident.severity}"`); 117 | } 118 | 119 | const parsed: IIncident = { 120 | id: incident.id, 121 | name: incident.name, 122 | 123 | body: incident.body, 124 | bodyStatus: incident.body_status, 125 | 126 | severity, 127 | components: (await statusify.getComponents()).filter(c => incident.components.includes(c.id)), 128 | updates: await Promise.all(updates), 129 | 130 | scheduledFor: this.nullableDate(incident.scheduled_for), 131 | scheduledUntil: this.nullableDate(incident.schedule_until), 132 | resolvedAt: this.nullableDate(incident.resolved_at), 133 | 134 | createdAt: new Date(incident.created_at), 135 | updatedAt: new Date(incident.updated_at) 136 | } 137 | 138 | return parsed as IIncident 139 | } 140 | 141 | /** 142 | * Builds a loki Mongo type query based on the incident query 143 | * @param query 144 | */ 145 | private buildQuery(query: IncidentsQuery): any { 146 | const qComp: any = {} 147 | 148 | if(query.createdAt !== undefined) { 149 | qComp['created_at'] = this.buildDateQuery(query.createdAt) 150 | } 151 | 152 | if(query.updatedAt !== undefined) { 153 | qComp['updated_at'] = this.buildDateQuery(query.updatedAt) 154 | } 155 | 156 | if(query.resolvedAt !== undefined) { 157 | qComp['resolved_at'] = this.buildDateQuery(query.resolvedAt) 158 | } 159 | 160 | if(query.id !== undefined) { 161 | qComp['id'] = query.id 162 | } 163 | 164 | return qComp 165 | } 166 | 167 | private buildDateQuery(query?: DateQuery): any { 168 | const qComp: any = {} 169 | 170 | if(query === undefined || query === null) { 171 | return undefined 172 | } 173 | 174 | if(query.after === undefined) { 175 | qComp['$gt'] = query.after 176 | } 177 | 178 | if(query.before === undefined) { 179 | qComp['$lt'] = query.after 180 | } 181 | } 182 | 183 | private nullableDate(val: any) { 184 | if(val === null || val === undefined) { 185 | return undefined 186 | } 187 | 188 | return new Date(val) 189 | } 190 | } --------------------------------------------------------------------------------