1
0
mirror of https://github.com/tormachris/cf-workers-status-page.git synced 2025-07-05 11:32:48 +02:00

refactor: switch css framework to tailwind

This commit is contained in:
Alex
2020-11-20 17:05:13 +01:00
parent 42f422c455
commit fb134bbf74
20 changed files with 1997 additions and 416 deletions

View File

@ -9,7 +9,9 @@ const apiToken = process.env.CF_API_TOKEN
const kvPrefix = 's_'
if (!accountId || !namespaceId || !apiToken) {
console.error("Missing required environment variables: CF_ACCOUNT_ID, KV_NAMESPACE_ID, CF_API_TOKEN")
console.error(
'Missing required environment variables: CF_ACCOUNT_ID, KV_NAMESPACE_ID, CF_API_TOKEN',
)
process.exit(0)
}
@ -51,24 +53,26 @@ function loadConfig() {
return JSON.parse(config)
}
getKvMonitors(kvPrefix).then(async kvMonitors => {
const config = loadConfig()
const monitors = config.monitors.map(key => {
return key.id
})
const kvState = kvMonitors.map(key => {
return key.name
})
const keysForRemoval = kvState.filter(
x => !monitors.includes(x.replace(kvPrefix, '')),
)
if (keysForRemoval.length > 0) {
console.log(
`Removing following keys from KV storage as they are no longer in the config: ${keysForRemoval.join(
', ',
)}`,
getKvMonitors(kvPrefix)
.then(async (kvMonitors) => {
const config = loadConfig()
const monitors = config.monitors.map((key) => {
return key.id
})
const kvState = kvMonitors.map((key) => {
return key.name
})
const keysForRemoval = kvState.filter(
(x) => !monitors.includes(x.replace(kvPrefix, '')),
)
await deleteKvBulk(keysForRemoval)
}
}).catch(e => console.log(e))
if (keysForRemoval.length > 0) {
console.log(
`Removing following keys from KV storage as they are no longer in the config: ${keysForRemoval.join(
', ',
)}`,
)
await deleteKvBulk(keysForRemoval)
}
})
.catch((e) => console.log(e))

View File

@ -0,0 +1,46 @@
import config from '../../config.yaml'
import MonitorStatusLabel from './monitorStatusLabel'
import MonitorHistogram from './monitorHistogram'
const infoIcon = (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
className="h-5 mr-2 mx-auto text-blue-500 dark:text-blue-400"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clipRule="evenodd"
/>
</svg>
)
export default function MonitorCard({ key, monitor, data }) {
return (
<div key={key} className="card">
<div className="flex flex-row justify-between items-center mb-2">
<div className="flex flex-row items-center align-center">
{monitor.description && (
<div className="tooltip">
{infoIcon}
<div className="content text-center transform -translate-y-1/2 top-1/2 ml-8 w-72 text-sm object-left">
{monitor.description}
</div>
</div>
)}
<div className="text-xl">{monitor.name}</div>
</div>
<MonitorStatusLabel kvMonitor={data} />
</div>
<MonitorHistogram monitorId={monitor.id} kvMonitor={data} />
<div className="flex flex-row justify-between items-center text-gray-400 text-sm">
<div>{config.settings.daysInHistogram} days ago</div>
<div>Today</div>
</div>
</div>
)
}

View File

@ -1,6 +1,21 @@
import config from '../../config.yaml'
import { useState } from 'react'
const searchIcon = (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
className="h-7 mx-auto text-gray-300 dark:text-gray-600"
fill="currentColor"
>
<path d="M9 9a2 2 0 114 0 2 2 0 01-4 0z" />
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-13a4 4 0 00-3.446 6.032l-2.261 2.26a1 1 0 101.414 1.415l2.261-2.261A4 4 0 1011 5z"
clipRule="evenodd"
/>
</svg>
)
export default function MonitorFilter({ active, callback }) {
const [input, setInput] = useState('')
@ -21,21 +36,19 @@ export default function MonitorFilter({ active, callback }) {
}
return (
<div className="ui search">
<div className="ui icon input">
<input
className="prompt"
type="text"
value={input}
onInput={handleInput}
onKeyDown={handleKeyDown}
placeholder="Tap '/' to search"
tabIndex={0}
ref={
(e) => e && active && e.focus()
}
/>
<i className="search icon"></i>
<div className="col-span-6 sm:col-span-3 relative">
<input
className="block w-full py-2 px-3 border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 rounded-full shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
type="text"
value={input}
onInput={handleInput}
onKeyDown={handleKeyDown}
placeholder="Tap '/' to search"
tabIndex={0}
ref={(e) => e && active && e.focus()}
/>
<div className="absolute inset-y-1 right-1 flex z-1 items-center">
{searchIcon}
</div>
</div>
)

View File

@ -1,56 +1,54 @@
import config from '../../config.yaml'
export default function MonitorHistogram({
monitorId,
kvMonitor,
}) {
export default function MonitorHistogram({ monitorId, kvMonitor }) {
// create date and set date - daysInHistogram for the first day of the histogram
let date = new Date()
date.setDate(date.getDate() - config.settings.daysInHistogram)
let content = null
if (typeof window !== 'undefined') {
return (
<div
key={`${monitorId}-histogram`}
className="horizontal flex histogram"
>
{Array.from(Array(config.settings.daysInHistogram).keys()).map(key => {
date.setDate(date.getDate() + 1)
const dayInHistogram = date.toISOString().split('T')[0]
content = Array.from(Array(config.settings.daysInHistogram).keys()).map(
(key) => {
date.setDate(date.getDate() + 1)
const dayInHistogram = date.toISOString().split('T')[0]
let bg = ''
let dayInHistogramLabel = config.settings.dayInHistogramNoData
let bg = ''
let dayInHistogramLabel = config.settings.dayInHistogramNoData
// filter all dates before first check, check the rest
if (kvMonitor && kvMonitor.firstCheck <= dayInHistogram) {
if (!kvMonitor.failedDays.includes(dayInHistogram)) {
bg = 'green'
dayInHistogramLabel = config.settings.dayInHistogramOperational
} else {
bg = 'orange'
dayInHistogramLabel = config.settings.dayInHistogramNotOperational
}
// filter all dates before first check, check the rest
if (kvMonitor && kvMonitor.firstCheck <= dayInHistogram) {
if (!kvMonitor.failedDays.includes(dayInHistogram)) {
bg = 'green'
dayInHistogramLabel = config.settings.dayInHistogramOperational
} else {
bg = 'yellow'
dayInHistogramLabel = config.settings.dayInHistogramNotOperational
}
}
return (
<div key={key} className="hitbox">
<div
className={`${bg} bar`}
data-tooltip={`${dayInHistogram} - ${dayInHistogramLabel}`}
/>
return (
<div key={key} className="hitbox tooltip">
<div className={`${bg} bar`} />
<div className="content text-center py-1 px-2 mt-2 left-1/2 -ml-20 w-40 text-xs">
{dayInHistogram}
<br />
<span className="font-semibold text-sm">
{dayInHistogramLabel}
</span>
</div>
)
})}
</div>
)
} else {
return (
<div
key={`${monitorId}-histogram`}
className="horizontal flex histogram"
>
<div className="grey-text">Loading histogram ...</div>
</div>
</div>
)
},
)
}
return (
<div
key={`${monitorId}-histogram`}
className="flex flex-row items-center histogram"
>
{content}
</div>
)
}

View File

@ -1,28 +1,34 @@
import config from '../../config.yaml'
export default function MonitorStatusHeader({kvMonitorsMetadata}) {
let backgroundColor = 'green'
let headerText = config.settings.allmonitorsOperational
let textColor = 'black'
const classes = {
green:
'bg-green-200 text-green-700 dark:bg-green-700 dark:text-green-200 border-green-300 dark:border-green-600',
yellow:
'bg-yellow-200 text-yellow-700 dark:bg-yellow-700 dark:text-yellow-200 border-yellow-300 dark:border-yellow-600',
}
export default function MonitorStatusHeader({ kvMonitorsMetadata }) {
let color = 'green'
let text = config.settings.allmonitorsOperational
if (!kvMonitorsMetadata.monitorsOperational) {
backgroundColor = 'yellow'
headerText = config.settings.notAllmonitorsOperational
color = 'yellow'
text = config.settings.notAllmonitorsOperational
}
return (
<div className={`ui inverted segment ${backgroundColor}`}>
<div className="horizontal flex between">
<div className={`ui marginless header ${textColor}-text`}>
{headerText}
</div>
{
kvMonitorsMetadata.lastUpdate && typeof window !== 'undefined' && (
<div className={`${textColor}-text`}>
checked {Math.round((Date.now() - kvMonitorsMetadata.lastUpdate.time) / 1000)} sec ago (from {kvMonitorsMetadata.lastUpdate.loc})
<div className={`card mb-4 font-semibold ${classes[color]}`}>
<div className="flex flex-row justify-between items-center">
<div>{text}</div>
{kvMonitorsMetadata.lastUpdate && typeof window !== 'undefined' && (
<div className="text-xs font-light">
checked{' '}
{Math.round(
(Date.now() - kvMonitorsMetadata.lastUpdate.time) / 1000,
)}{' '}
sec ago (from {kvMonitorsMetadata.lastUpdate.loc})
</div>
)
}
)}
</div>
</div>
)

View File

@ -1,18 +1,25 @@
import config from '../../config.yaml'
const classes = {
gray: 'bg-gray-200 text-gray-800 dark:bg-gray-800 dark:text-gray-200',
green: 'bg-green-200 text-green-800 dark:bg-green-800 dark:text-green-200',
yellow:
'bg-yellow-200 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-200',
}
export default function MonitorStatusLabel({ kvMonitor }) {
let labelColor = 'grey'
let labelText = 'No data'
let color = 'gray'
let text = 'No data'
if (typeof kvMonitor !== 'undefined') {
if (kvMonitor.operational) {
labelColor = 'green'
labelText = config.settings.monitorLabelOperational
color = 'green'
text = config.settings.monitorLabelOperational
} else {
labelColor = 'orange'
labelText = config.settings.monitorLabelNotOperational
color = 'yellow'
text = config.settings.monitorLabelNotOperational
}
}
return <div className={`ui ${labelColor} horizontal label`}>{labelText}</div>
return <div className={`pill leading-5 ${classes[color]}`}>{text}</div>
}

View File

@ -0,0 +1,58 @@
import { useEffect, useState } from 'react'
const moonIcon = (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
className="h-5 mx-auto"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
/>
</svg>
)
const sunIcon = (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
className="h-5 mx-auto"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
/>
</svg>
)
export default function ThemeSwitcher() {
const [darkmode, setDark] = useState(localStorage.getItem('theme') === 'dark')
useEffect(() => {
setTheme(darkmode ? 'dark' : 'light')
}, [darkmode])
const changeTheme = () => {
setDark(!darkmode)
}
const buttonColor = darkmode ? 'bg-gray-700' : 'bg-gray-200'
return (
<button
className={`${buttonColor} rounded-full h-7 w-7 mr-4`}
onClick={changeTheme}
>
{darkmode ? sunIcon : moonIcon}
</button>
)
}

View File

@ -1,10 +1,6 @@
import config from '../../config.yaml'
import {
setKV,
getKVWithMetadata,
notifySlack,
} from './helpers'
import { setKV, getKVWithMetadata, notifySlack } from './helpers'
function getDate() {
return new Date().toISOString().split('T')[0]
@ -12,7 +8,10 @@ function getDate() {
export async function processCronTrigger(event) {
// Get monitors state from KV
let {value: monitorsState, metadata: monitorsStateMetadata} = await getKVWithMetadata('monitors_data', 'json')
let {
value: monitorsState,
metadata: monitorsStateMetadata,
} = await getKVWithMetadata('monitors_data', 'json')
// Create empty state objects if not exists in KV storage yet
if (!monitorsState) {
@ -28,7 +27,7 @@ export async function processCronTrigger(event) {
for (const monitor of config.monitors) {
// Create default monitor state if does not exist yet
if (typeof monitorsState[monitor.id] === 'undefined') {
monitorsState[monitor.id] = {failedDays: []}
monitorsState[monitor.id] = { failedDays: [] }
}
console.log(`Checking ${monitor.name} ...`)
@ -43,15 +42,22 @@ export async function processCronTrigger(event) {
}
const checkResponse = await fetch(monitor.url, init)
const monitorOperational = checkResponse.status === (monitor.expectStatus || 200)
const monitorOperational =
checkResponse.status === (monitor.expectStatus || 200)
// Send Slack message on monitor change
if (monitorsState[monitor.id].operational !== monitorOperational && typeof SECRET_SLACK_WEBHOOK_URL !== 'undefined' && SECRET_SLACK_WEBHOOK_URL !== 'default-gh-action-secret') {
event.waitUntil(notifySlack(monitor, monitorOperational))
if (
monitorsState[monitor.id].operational !== monitorOperational &&
typeof SECRET_SLACK_WEBHOOK_URL !== 'undefined' &&
SECRET_SLACK_WEBHOOK_URL !== 'default-gh-action-secret'
) {
event.waitUntil(notifySlack(monitor, monitorOperational))
}
monitorsState[monitor.id].operational = checkResponse.status === (monitor.expectStatus || 200)
monitorsState[monitor.id].firstCheck = monitorsState[monitor.id].firstCheck || getDate()
monitorsState[monitor.id].operational =
checkResponse.status === (monitor.expectStatus || 200)
monitorsState[monitor.id].firstCheck =
monitorsState[monitor.id].firstCheck || getDate()
// Set monitorsOperational and push current day to failedDays
if (!monitorOperational) {
@ -72,11 +78,15 @@ export async function processCronTrigger(event) {
const loc = res.headers.get('cf-ray').split('-')[1]
monitorsStateMetadata.lastUpdate = {
loc,
time: Date.now()
time: Date.now(),
}
// Save monitorsState and monitorsStateMetadata to KV storage
await setKV('monitors_data', JSON.stringify(monitorsState), monitorsStateMetadata)
await setKV(
'monitors_data',
JSON.stringify(monitorsState),
monitorsStateMetadata,
)
return new Response('OK')
}

View File

@ -1,8 +1,8 @@
import config from '../../config.yaml'
import {useEffect, useState} from 'react'
import { useEffect, useState } from 'react'
export async function getMonitors() {
return await getKVWithMetadata('monitors_data', "json")
return await getKVWithMetadata('monitors_data', 'json')
}
export async function setKV(key, value, metadata, expirationTtl) {
@ -23,11 +23,10 @@ export async function notifySlack(monitor, operational) {
type: 'section',
text: {
type: 'mrkdwn',
text: `Monitor *${monitor.name}* changed status to *${
operational
text: `Monitor *${monitor.name}* changed status to *${operational
? config.settings.monitorLabelOperational
: config.settings.monitorLabelNotOperational
}*`,
}*`,
},
},
{
@ -35,11 +34,9 @@ export async function notifySlack(monitor, operational) {
elements: [
{
type: 'mrkdwn',
text: `${
operational ? ':white_check_mark:' : ':x:'
} \`${monitor.method ? monitor.method : "GET"} ${monitor.url}\` - :eyes: <${
config.settings.url
}|Status Page>`,
text: `${operational ? ':white_check_mark:' : ':x:'} \`${monitor.method ? monitor.method : 'GET'
} ${monitor.url}\` - :eyes: <${config.settings.url
}|Status Page>`,
},
],
},
@ -59,24 +56,24 @@ export function useKeyPress(targetKey) {
function downHandler({ key }) {
if (key === targetKey) {
setKeyPressed(true);
setKeyPressed(true)
}
}
const upHandler = ({ key }) => {
if (key === targetKey) {
setKeyPressed(false);
setKeyPressed(false)
}
}
useEffect(() => {
window.addEventListener('keydown', downHandler);
window.addEventListener('keyup', upHandler);
window.addEventListener('keydown', downHandler)
window.addEventListener('keyup', upHandler)
return () => {
window.removeEventListener('keydown', downHandler);
window.removeEventListener('keyup', upHandler);
};
window.removeEventListener('keydown', downHandler)
window.removeEventListener('keyup', upHandler)
}
}, [])
return keyPressed