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:
@ -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))
|
||||
|
46
src/components/monitorCard.js
Normal file
46
src/components/monitorCard.js
Normal 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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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>
|
||||
}
|
||||
|
58
src/components/themeSwitcher.js
Normal file
58
src/components/themeSwitcher.js
Normal 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>
|
||||
)
|
||||
}
|
@ -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')
|
||||
}
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user