mirror of
https://github.com/tormachris/cf-workers-status-page.git
synced 2024-11-23 22:45:43 +01:00
commit
a89a3737e5
Binary file not shown.
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 450 KiB |
7
.github/workflows/deploy.yml
vendored
7
.github/workflows/deploy.yml
vendored
@ -28,11 +28,18 @@ jobs:
|
||||
echo "[env.production]" >> wrangler.toml
|
||||
echo "kv_namespaces = [{binding=\"KV_STATUS_PAGE\", id=\"${KV_NAMESPACE_ID}\"}]" >> wrangler.toml
|
||||
[ -z "$SECRET_SLACK_WEBHOOK_URL" ] && echo "Secret SECRET_SLACK_WEBHOOK_URL not set, creating dummy one..." && SECRET_SLACK_WEBHOOK_URL="default-gh-action-secret" || true
|
||||
[ -z "$SECRET_TELEGRAM_API_TOKEN" ] && echo "Secret SECRET_TELEGRAM_API_TOKEN not set, creating dummy one..." && SECRET_TELEGRAM_API_TOKEN="default-gh-action-secret" || true
|
||||
[ -z "$SECRET_TELEGRAM_CHAT_ID" ] && echo "Secret SECRET_TELEGRAM_CHAT_ID not set, creating dummy one..." && SECRET_TELEGRAM_CHAT_ID="default-gh-action-secret" || true
|
||||
postCommands: |
|
||||
yarn kv-gc
|
||||
secrets: |
|
||||
SECRET_SLACK_WEBHOOK_URL
|
||||
SECRET_TELEGRAM_API_TOKEN
|
||||
SECRET_TELEGRAM_CHAT_ID
|
||||
environment: production
|
||||
env:
|
||||
NODE_ENV: production
|
||||
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
SECRET_SLACK_WEBHOOK_URL: ${{secrets.SECRET_SLACK_WEBHOOK_URL}}
|
||||
SECRET_TELEGRAM_API_TOKEN: ${{secrets.SECRET_TELEGRAM_API_TOKEN}}
|
||||
SECRET_TELEGRAM_CHAT_ID: ${{secrets.SECRET_TELEGRAM_CHAT_ID}}
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -133,3 +133,4 @@ worker/
|
||||
.direnv/
|
||||
out/
|
||||
package-lock.json
|
||||
public/style.css
|
||||
|
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@ -0,0 +1,3 @@
|
||||
# Ignore generated files
|
||||
out
|
||||
public
|
@ -14,22 +14,22 @@ appearance, race, religion, or sexual identity and orientation.
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
- Using welcoming and inclusive language
|
||||
- Being respectful of differing viewpoints and experiences
|
||||
- Gracefully accepting constructive criticism
|
||||
- Focusing on what is best for the community
|
||||
- Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
- The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
|
48
README.md
48
README.md
@ -10,15 +10,15 @@ Monitor your websites, showcase status including daily history, and get Slack no
|
||||
|
||||
You'll need a [Cloudflare Workers account](https://dash.cloudflare.com/sign-up/workers) with
|
||||
|
||||
* A workers domain set up
|
||||
* The Workers Bundled subscription \($5/mo\)
|
||||
* [It works with Workers Free!](https://blog.cloudflare.com/workers-kv-free-tier/) Check [more info](#workers-kv-free-tier) on how to run on Workers Free.
|
||||
* Some websites/APIs to watch 🙂
|
||||
- A workers domain set up
|
||||
- The Workers Bundled subscription \($5/mo\)
|
||||
- [It works with Workers Free!](https://blog.cloudflare.com/workers-kv-free-tier/) Check [more info](#workers-kv-free-tier) on how to run on Workers Free.
|
||||
- Some websites/APIs to watch 🙂
|
||||
|
||||
Also, prepare the following secrets
|
||||
|
||||
* Cloudflare API token with `Edit Cloudflare Workers` permissions
|
||||
* Slack incoming webhook \(optional\)
|
||||
- Cloudflare API token with `Edit Cloudflare Workers` permissions
|
||||
- Slack incoming webhook \(optional\)
|
||||
|
||||
## Getting started
|
||||
|
||||
@ -39,6 +39,7 @@ You can either deploy with **Cloudflare Deploy Button** using GitHub Actions or
|
||||
- Name: SECRET_SLACK_WEBHOOK_URL (optional)
|
||||
- Value: your-slack-webhook-url
|
||||
```
|
||||
|
||||
3. Navigate to the **Actions** settings in your repository and enable them
|
||||
4. Edit [config.yaml](./config.yaml) to adjust configuration and list all of your websites/APIs you want to monitor
|
||||
|
||||
@ -48,6 +49,7 @@ You can either deploy with **Cloudflare Deploy Button** using GitHub Actions or
|
||||
url: 'https://status-page.eidam.dev' # used for Slack messages
|
||||
logo: logo-192x192.png # image in ./public/ folder
|
||||
daysInHistogram: 90 # number of days you want to display in histogram
|
||||
collectResponseTimes: false # experimental feature, enable only for <5 monitors or on paid plans
|
||||
|
||||
# configurable texts across the status page
|
||||
allmonitorsOperational: 'All Systems Operational'
|
||||
@ -73,32 +75,46 @@ You can either deploy with **Cloudflare Deploy Button** using GitHub Actions or
|
||||
5. Push to `main` branch to trigger the deployment
|
||||
6. 🎉
|
||||
7. _\(optional\)_ Go to [Cloudflare Workers settings](https://dash.cloudflare.com/?to=/workers) and assign custom domain/route
|
||||
* e.g. `status-page.eidam.dev/*` _\(make sure you include `/*` as the Worker also serve static files\)_
|
||||
- e.g. `status-page.eidam.dev/*` _\(make sure you include `/*` as the Worker also serve static files\)_
|
||||
8. _\(optional\)_ Edit [wrangler.toml](./wrangler.toml) to adjust Worker settings or CRON Trigger schedule, especially if you are on [Workers Free plan](#workers-kv-free-tier)
|
||||
|
||||
### Telegram notifications
|
||||
|
||||
To enable telegram notifications, you'll need to take a few additional steps.
|
||||
|
||||
1. [Create a new Bot](https://core.telegram.org/bots#creating-a-new-bot)
|
||||
2. Set the api token you received when creating the bot as content of the `SECRET_TELEGRAM_API_TOKEN` secret in your github repository.
|
||||
3. Send a message to the bot from the telegram account which should receive the alerts (Something more than `/start`)
|
||||
4. Get the chat id with `curl https://api.telegram.org/bot<YOUR TELEGRAM API TOKEN>/getUpdates | jq '.result[0] .message .chat .id'`
|
||||
5. Set the retrieved chat id in the `SECRET_TELEGRAM_CHAT_ID` secret variable
|
||||
6. Redeploy the status page using the github action
|
||||
|
||||
### Deploy on your own
|
||||
|
||||
You can clone the repository yourself and use Wrangler CLI to develop/deploy, extra list of things you need to take care of:
|
||||
|
||||
* create KV namespace and add the `KV_STATUS_PAGE` binding to [wrangler.toml](./wrangler.toml)
|
||||
* create Worker secrets _\(optional\)_
|
||||
* `SECRET_SLACK_WEBHOOK_URL`
|
||||
- create KV namespace and add the `KV_STATUS_PAGE` binding to [wrangler.toml](./wrangler.toml)
|
||||
- create Worker secrets _\(optional\)_
|
||||
- `SECRET_SLACK_WEBHOOK_URL`
|
||||
|
||||
## Workers KV free tier
|
||||
|
||||
The Workers Free plan includes limited KV usage, but the quota is sufficient for 2-minute checks only
|
||||
* Change the CRON trigger to 2 minutes interval (`crons = ["*/2 * * * *"]`) in [wrangler.toml](./wrangler.toml)
|
||||
|
||||
- Change the CRON trigger to 2 minutes interval (`crons = ["*/2 * * * *"]`) in [wrangler.toml](./wrangler.toml)
|
||||
|
||||
## Known issues
|
||||
|
||||
* **Max 25 monitors to watch in case you are using Slack notifications**, due to the limit of subrequests Cloudflare Worker can make \(50\).
|
||||
- **Max 25 monitors to watch in case you are using Slack notifications**, due to the limit of subrequests Cloudflare Worker can make \(50\).
|
||||
|
||||
The plan is to support up to 49 by sending only one Slack notification per scheduled run.
|
||||
|
||||
* **KV replication lag** - You might get Slack notification instantly, however it may take couple of more seconds to see the change on your status page as [Cron Triggers are usually running on underutilized quiet hours machines](https://blog.cloudflare.com/introducing-cron-triggers-for-cloudflare-workers/#how-are-you-able-to-offer-this-feature-at-no-additional-cost).
|
||||
- **KV replication lag** - You might get Slack notification instantly, however it may take couple of more seconds to see the change on your status page as [Cron Triggers are usually running on underutilized quiet hours machines](https://blog.cloudflare.com/introducing-cron-triggers-for-cloudflare-workers/#how-are-you-able-to-offer-this-feature-at-no-additional-cost).
|
||||
|
||||
* **Initial delay (no data)** - It takes couple of minutes to schedule and run CRON Triggers for the first time
|
||||
- **Initial delay (no data)** - It takes couple of minutes to schedule and run CRON Triggers for the first time
|
||||
|
||||
## Future plans
|
||||
|
||||
Stay tuned for more features coming in, like leveraging the fact that CRON instances are scheduled around the world during the day
|
||||
so we can monitor the response times. However, we will most probably wait for the [Durable Objects](https://blog.cloudflare.com/introducing-workers-durable-objects/) to be in open beta
|
||||
as they are better fit to reliably store such info.
|
||||
so we can monitor the response times. However, we will most probably wait for the [Durable Objects](https://blog.cloudflare.com/introducing-workers-durable-objects/) to be in open beta
|
||||
as they are better fit to reliably store such info.
|
||||
|
@ -1,4 +1,3 @@
|
||||
# Table of contents
|
||||
|
||||
* [Cloudflare Worker - Status Page](README.md)
|
||||
|
||||
- [Cloudflare Worker - Status Page](README.md)
|
||||
|
@ -3,6 +3,7 @@ settings:
|
||||
url: 'https://status-page.eidam.dev' # used for Slack messages
|
||||
logo: logo-192x192.png # image in ./public/ folder
|
||||
daysInHistogram: 90 # number of days you want to display in histogram
|
||||
collectResponseTimes: false # experimental feature, enable only for <5 monitors or on paid plans
|
||||
|
||||
allmonitorsOperational: 'All Systems Operational'
|
||||
notAllmonitorsOperational: 'Not All Systems Operational'
|
||||
@ -11,7 +12,7 @@ settings:
|
||||
monitorLabelNoData: 'No data'
|
||||
dayInHistogramNoData: 'No data'
|
||||
dayInHistogramOperational: 'All good'
|
||||
dayInHistogramNotOperational: 'Some checks failed'
|
||||
dayInHistogramNotOperational: ' incident(s)' # xx incident(s) recorded
|
||||
|
||||
monitors:
|
||||
- id: workers-cloudflare-com # unique identifier
|
||||
|
4
index.js
4
index.js
@ -10,7 +10,7 @@ import { processCronTrigger } from './src/functions/cronTrigger'
|
||||
*/
|
||||
const DEBUG = false
|
||||
|
||||
addEventListener('fetch', event => {
|
||||
addEventListener('fetch', (event) => {
|
||||
try {
|
||||
event.respondWith(
|
||||
handleEvent(event, require.context('./pages/', true, /\.js$/), DEBUG),
|
||||
@ -27,6 +27,6 @@ addEventListener('fetch', event => {
|
||||
}
|
||||
})
|
||||
|
||||
addEventListener('scheduled', event => {
|
||||
addEventListener('scheduled', (event) => {
|
||||
event.waitUntil(processCronTrigger(event))
|
||||
})
|
||||
|
24
package.json
24
package.json
@ -7,21 +7,29 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "flareact dev",
|
||||
"build": "flareact build",
|
||||
"deploy": "flareact publish",
|
||||
"build": "yarn css && flareact build",
|
||||
"deploy": "yarn build && flareact publish",
|
||||
"kv-gc": "node ./src/cli/gcMonitors.js",
|
||||
"format": "prettier --write '**/*.{js,css,json,md}'"
|
||||
"format": "prettier --write '**/*.{js,css,json,md}'",
|
||||
"css": "postcss public/tailwind.css -o public/style.css",
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"dependencies": {
|
||||
"flareact": "^0.9.0",
|
||||
"flareact": "0.9.0",
|
||||
"laco": "^1.2.1",
|
||||
"laco-react": "^1.1.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1"
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.0.2",
|
||||
"node-fetch": "^2.6.1",
|
||||
"prettier": "^1.18.2",
|
||||
"yaml-loader": "^0.6.0"
|
||||
"postcss": "^8.1.8",
|
||||
"postcss-cli": "^8.3.0",
|
||||
"prettier": "^2.2.0",
|
||||
"tailwindcss": "^2.0.1",
|
||||
"yaml-loader": "^0.6.0",
|
||||
"patch-package": "^6.2.2",
|
||||
"postinstall-postinstall": "^2.1.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { processCronTrigger } from '../../src/functions/cronTrigger'
|
||||
|
||||
export default async event => {
|
||||
export default async (event) => {
|
||||
// used only for local debugging
|
||||
//return processCronTrigger(event)
|
||||
}
|
||||
|
145
pages/index.js
145
pages/index.js
@ -1,115 +1,94 @@
|
||||
import Head from 'flareact/head'
|
||||
import MonitorHistogram from '../src/components/monitorHistogram'
|
||||
|
||||
import {
|
||||
getMonitors,
|
||||
useKeyPress,
|
||||
} from '../src/functions/helpers'
|
||||
|
||||
import config from '../config.yaml'
|
||||
import MonitorStatusLabel from '../src/components/monitorStatusLabel'
|
||||
import MonitorStatusHeader from '../src/components/monitorStatusHeader'
|
||||
import MonitorFilter from '../src/components/monitorFilter'
|
||||
|
||||
import { Store } from 'laco'
|
||||
import { useStore } from 'laco-react'
|
||||
import Head from 'flareact/head'
|
||||
|
||||
const MonitorStore = new Store(
|
||||
{
|
||||
monitors: config.monitors,
|
||||
visible: config.monitors,
|
||||
activeFilter: false
|
||||
}
|
||||
)
|
||||
import { getKVMonitors, useKeyPress } from '../src/functions/helpers'
|
||||
import config from '../config.yaml'
|
||||
import MonitorCard from '../src/components/monitorCard'
|
||||
import MonitorFilter from '../src/components/monitorFilter'
|
||||
import MonitorStatusHeader from '../src/components/monitorStatusHeader'
|
||||
import ThemeSwitcher from '../src/components/themeSwitcher'
|
||||
|
||||
const filterByTerm = (term) => MonitorStore.set(
|
||||
state => ({ visible: state.monitors.filter((monitor) => monitor.name.toLowerCase().includes(term)) })
|
||||
)
|
||||
const MonitorStore = new Store({
|
||||
monitors: config.monitors,
|
||||
visible: config.monitors,
|
||||
activeFilter: false,
|
||||
})
|
||||
|
||||
const filterByTerm = (term) =>
|
||||
MonitorStore.set((state) => ({
|
||||
visible: state.monitors.filter((monitor) =>
|
||||
monitor.name.toLowerCase().includes(term),
|
||||
),
|
||||
}))
|
||||
|
||||
export async function getEdgeProps() {
|
||||
// get KV data
|
||||
const {value: kvMonitors, metadata: kvMonitorsMetadata } = await getMonitors()
|
||||
const kvMonitors = await getKVMonitors()
|
||||
|
||||
return {
|
||||
props: {
|
||||
config,
|
||||
kvMonitors: kvMonitors || {},
|
||||
kvMonitorsMetadata: kvMonitorsMetadata || {}
|
||||
kvMonitors: kvMonitors ? kvMonitors.monitors : {},
|
||||
kvMonitorsLastUpdate: kvMonitors ? kvMonitors.lastUpdate : {},
|
||||
},
|
||||
// Revalidate these props once every x seconds
|
||||
revalidate: 5,
|
||||
}
|
||||
}
|
||||
|
||||
export default function Index({
|
||||
config,
|
||||
kvMonitors,
|
||||
kvMonitorsMetadata,
|
||||
}) {
|
||||
export default function Index({ config, kvMonitors, kvMonitorsLastUpdate }) {
|
||||
const state = useStore(MonitorStore)
|
||||
const slash = useKeyPress('/')
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="min-h-screen">
|
||||
<Head>
|
||||
<title>{config.settings.title}</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.8.7/semantic.min.css"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link rel="stylesheet" href="./main.css" />
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
<script>
|
||||
{`
|
||||
function setTheme(theme) {
|
||||
document.documentElement.classList.remove("dark", "light")
|
||||
document.documentElement.classList.add(theme)
|
||||
localStorage.theme = theme
|
||||
}
|
||||
(() => {
|
||||
const query = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
query.addListener(() => {
|
||||
setTheme(query.matches ? "dark" : "light")
|
||||
})
|
||||
if (["dark", "light"].includes(localStorage.theme)) {
|
||||
setTheme(localStorage.theme)
|
||||
} else {
|
||||
setTheme(query.matches ? "dark" : "light")
|
||||
}
|
||||
})()
|
||||
`}
|
||||
</script>
|
||||
</Head>
|
||||
<div className="ui basic segment container">
|
||||
<div className="horizontal flex between">
|
||||
<h1 className="ui huge marginless title header">
|
||||
<img
|
||||
className="ui middle aligned tiny image"
|
||||
src={config.settings.logo}
|
||||
/>
|
||||
{config.settings.title}
|
||||
</h1>
|
||||
<MonitorFilter
|
||||
active={slash}
|
||||
callback={filterByTerm}
|
||||
/>
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex flex-row justify-between items-center p-4">
|
||||
<div className="flex flex-row items-center">
|
||||
<img className="h-8 w-auto" src={config.settings.logo} />
|
||||
<h1 className="ml-4 text-3xl">{config.settings.title}</h1>
|
||||
</div>
|
||||
<div className="flex flex-row items-center">
|
||||
{typeof window !== 'undefined' && <ThemeSwitcher />}
|
||||
<MonitorFilter active={slash} callback={filterByTerm} />
|
||||
</div>
|
||||
</div>
|
||||
<MonitorStatusHeader
|
||||
kvMonitorsMetadata={kvMonitorsMetadata}
|
||||
/>
|
||||
<MonitorStatusHeader kvMonitorsLastUpdate={kvMonitorsLastUpdate} />
|
||||
{state.visible.map((monitor, key) => {
|
||||
return (
|
||||
<div key={key} className="ui segment">
|
||||
<div
|
||||
className="ui horizontal flex between"
|
||||
style={{ marginBottom: '8px' }}
|
||||
>
|
||||
<div className="ui marginless header">
|
||||
{monitor.description && (
|
||||
<span data-tooltip={monitor.description}>
|
||||
<i className="blue small info circle icon" />
|
||||
</span>
|
||||
)}
|
||||
<div className="content">{monitor.name}</div>
|
||||
</div>
|
||||
<MonitorStatusLabel
|
||||
kvMonitor={kvMonitors[monitor.id]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<MonitorHistogram
|
||||
monitorId={monitor.id}
|
||||
kvMonitor={kvMonitors[monitor.id]}
|
||||
/>
|
||||
|
||||
<div className="horizontal flex between grey-text">
|
||||
<div>{config.settings.daysInHistogram} days ago</div>
|
||||
<div>Today</div>
|
||||
</div>
|
||||
</div>
|
||||
<MonitorCard
|
||||
key={key}
|
||||
monitor={monitor}
|
||||
data={kvMonitors[monitor.id]}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<div className="horizontal flex between grey-text">
|
||||
<div className="flex flex-row justify-between mt-4 text-sm">
|
||||
<div>
|
||||
Powered by{' '}
|
||||
<a href="https://workers.cloudflare.com/" target="_blank">
|
||||
|
12
patches/flareact+0.9.0.patch
Normal file
12
patches/flareact+0.9.0.patch
Normal file
@ -0,0 +1,12 @@
|
||||
diff --git a/node_modules/flareact/src/components/_document.js b/node_modules/flareact/src/components/_document.js
|
||||
index 3494b60..206b493 100644
|
||||
--- a/node_modules/flareact/src/components/_document.js
|
||||
+++ b/node_modules/flareact/src/components/_document.js
|
||||
@@ -61,6 +61,7 @@ export function FlareactHead({ helmet, page, buildManifest }) {
|
||||
{helmet.title.toComponent()}
|
||||
{helmet.meta.toComponent()}
|
||||
{helmet.link.toComponent()}
|
||||
+ {helmet.script.toComponent()}
|
||||
|
||||
{[...links].map((link) => (
|
||||
<link href={`/_flareact/static/${link}`} rel="stylesheet" />
|
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
body {
|
||||
background: #eeeeee;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.flex.horizontal {
|
||||
flex-direction: row;
|
||||
}
|
||||
.flex.vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
.flex.between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.marginless {
|
||||
margin: 0 !important;
|
||||
}
|
||||
.paddingless {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.black-text {
|
||||
color: #000 !important;
|
||||
}
|
||||
.grey-text {
|
||||
color: #a0a0a0 !important;
|
||||
}
|
||||
.white-text {
|
||||
color: #fff !important;
|
||||
}
|
||||
.histogram {
|
||||
height: 24px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.hitbox {
|
||||
align-items: flex-end;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 1px;
|
||||
border-radius: 3.75px;
|
||||
}
|
||||
.bar {
|
||||
background: #dcddde;
|
||||
padding-bottom: 1px;
|
||||
height: 100%;
|
||||
width: 85%;
|
||||
border-radius: 100px;
|
||||
}
|
||||
.bar.green {
|
||||
background: #21ba45;
|
||||
}
|
||||
.bar.red {
|
||||
background: #db2828;
|
||||
}
|
||||
.bar.orange {
|
||||
background: #f2711c;
|
||||
}
|
||||
span i.icon {
|
||||
margin: 0 .25em .25em 0 !important;
|
||||
}
|
||||
.ui.title.header .ui.image {
|
||||
margin-top: -.5em !important;
|
||||
}
|
68
public/tailwind.css
Normal file
68
public/tailwind.css
Normal file
@ -0,0 +1,68 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-50;
|
||||
}
|
||||
|
||||
a {
|
||||
@apply text-blue-500 dark:text-blue-400;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.card {
|
||||
@apply p-4 bg-white border border-gray-200 dark:bg-gray-700 dark:border-gray-600 shadow rounded-lg p-4 mb-2;
|
||||
}
|
||||
|
||||
.pill {
|
||||
@apply px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full;
|
||||
}
|
||||
|
||||
.histogram {
|
||||
@apply h-6 w-full mx-auto;
|
||||
}
|
||||
.hitbox {
|
||||
align-items: flex-end;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 1px;
|
||||
border-radius: 3.75px;
|
||||
}
|
||||
.bar {
|
||||
@apply bg-gray-300 dark:bg-gray-600;
|
||||
padding-bottom: 1px;
|
||||
height: 100%;
|
||||
width: 85%;
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
.bar.green {
|
||||
@apply bg-green-400 dark:bg-green-700;
|
||||
}
|
||||
|
||||
.bar.red {
|
||||
@apply bg-red-400 dark:bg-red-700;
|
||||
}
|
||||
|
||||
.bar.yellow {
|
||||
@apply bg-yellow-400 dark:bg-yellow-700;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.tooltip .content {
|
||||
@apply invisible absolute z-50 inline-block;
|
||||
@apply rounded-lg py-1 px-2 bg-gray-100 dark:bg-gray-800 shadow;
|
||||
@apply opacity-0 transition-all duration-200 scale-50;
|
||||
}
|
||||
|
||||
.tooltip:hover .content {
|
||||
@apply visible opacity-100 scale-100;
|
||||
}
|
||||
}
|
@ -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,67 @@
|
||||
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, then check the rest
|
||||
if (kvMonitor && kvMonitor.firstCheck <= dayInHistogram) {
|
||||
if (
|
||||
kvMonitor.checks.hasOwnProperty(dayInHistogram) &&
|
||||
kvMonitor.checks[dayInHistogram].fails > 0
|
||||
) {
|
||||
bg = 'yellow'
|
||||
dayInHistogramLabel = `${kvMonitor.checks[dayInHistogram].fails} ${config.settings.dayInHistogramNotOperational}`
|
||||
} else {
|
||||
bg = 'green'
|
||||
dayInHistogramLabel = config.settings.dayInHistogramOperational
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
{kvMonitor &&
|
||||
kvMonitor.checks.hasOwnProperty(dayInHistogram) &&
|
||||
Object.keys(kvMonitor.checks[dayInHistogram].res).map((key) => {
|
||||
return (
|
||||
<>
|
||||
<br />
|
||||
{key}: {kvMonitor.checks[dayInHistogram].res[key].a}ms
|
||||
</>
|
||||
)
|
||||
})}
|
||||
</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,32 @@
|
||||
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',
|
||||
}
|
||||
|
||||
if (!kvMonitorsMetadata.monitorsOperational) {
|
||||
backgroundColor = 'yellow'
|
||||
headerText = config.settings.notAllmonitorsOperational
|
||||
export default function MonitorStatusHeader({ kvMonitorsLastUpdate }) {
|
||||
let color = 'green'
|
||||
let text = config.settings.allmonitorsOperational
|
||||
|
||||
if (!kvMonitorsLastUpdate.allOperational) {
|
||||
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>
|
||||
{kvMonitorsLastUpdate.time && typeof window !== 'undefined' && (
|
||||
<div className="text-xs font-light">
|
||||
checked{' '}
|
||||
{Math.round((Date.now() - kvMonitorsLastUpdate.time) / 1000)} sec
|
||||
ago (from {kvMonitorsLastUpdate.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
|
||||
if (kvMonitor.lastCheck.operational) {
|
||||
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,9 +1,11 @@
|
||||
import config from '../../config.yaml'
|
||||
|
||||
import {
|
||||
setKV,
|
||||
getKVWithMetadata,
|
||||
notifySlack,
|
||||
notifyTelegram,
|
||||
getCheckLocation,
|
||||
getKVMonitors,
|
||||
setKVMonitors,
|
||||
} from './helpers'
|
||||
|
||||
function getDate() {
|
||||
@ -11,24 +13,29 @@ function getDate() {
|
||||
}
|
||||
|
||||
export async function processCronTrigger(event) {
|
||||
// Get Worker PoP and save it to monitorsStateMetadata
|
||||
const checkLocation = await getCheckLocation()
|
||||
const checkDay = getDate()
|
||||
|
||||
// Get monitors state from KV
|
||||
let {value: monitorsState, metadata: monitorsStateMetadata} = await getKVWithMetadata('monitors_data', 'json')
|
||||
let monitorsState = await getKVMonitors()
|
||||
|
||||
// Create empty state objects if not exists in KV storage yet
|
||||
if (!monitorsState) {
|
||||
monitorsState = {}
|
||||
}
|
||||
if (!monitorsStateMetadata) {
|
||||
monitorsStateMetadata = {}
|
||||
monitorsState = { lastUpdate: {}, monitors: {} }
|
||||
}
|
||||
|
||||
// Reset default all monitors state to true
|
||||
monitorsStateMetadata.monitorsOperational = true
|
||||
monitorsState.lastUpdate.allOperational = true
|
||||
|
||||
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: []}
|
||||
if (typeof monitorsState.monitors[monitor.id] === 'undefined') {
|
||||
monitorsState.monitors[monitor.id] = {
|
||||
firstCheck: checkDay,
|
||||
lastCheck: {},
|
||||
checks: {},
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Checking ${monitor.name} ...`)
|
||||
@ -42,41 +49,101 @@ export async function processCronTrigger(event) {
|
||||
},
|
||||
}
|
||||
|
||||
// Perform a check and measure time
|
||||
const requestStartTime = Date.now()
|
||||
const checkResponse = await fetch(monitor.url, init)
|
||||
const monitorOperational = checkResponse.status === (monitor.expectStatus || 200)
|
||||
const requestTime = Math.round(Date.now() - requestStartTime)
|
||||
|
||||
// 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))
|
||||
// Determine whether operational and status changed
|
||||
const monitorOperational =
|
||||
checkResponse.status === (monitor.expectStatus || 200)
|
||||
const monitorStatusChanged =
|
||||
monitorsState.monitors[monitor.id].lastCheck.operational !==
|
||||
monitorOperational
|
||||
|
||||
// Save monitor's last check response status
|
||||
monitorsState.monitors[monitor.id].lastCheck = {
|
||||
status: checkResponse.status,
|
||||
statusText: checkResponse.statusText,
|
||||
operational: monitorOperational,
|
||||
}
|
||||
|
||||
monitorsState[monitor.id].operational = checkResponse.status === (monitor.expectStatus || 200)
|
||||
monitorsState[monitor.id].firstCheck = monitorsState[monitor.id].firstCheck || getDate()
|
||||
// Send Slack message on monitor change
|
||||
if (
|
||||
monitorStatusChanged &&
|
||||
typeof SECRET_SLACK_WEBHOOK_URL !== 'undefined' &&
|
||||
SECRET_SLACK_WEBHOOK_URL !== 'default-gh-action-secret'
|
||||
) {
|
||||
event.waitUntil(notifySlack(monitor, monitorOperational))
|
||||
}
|
||||
|
||||
// Set monitorsOperational and push current day to failedDays
|
||||
if (!monitorOperational) {
|
||||
monitorsStateMetadata.monitorsOperational = false
|
||||
// Send Telegram message on monitor change
|
||||
if (
|
||||
monitorStatusChanged &&
|
||||
typeof SECRET_TELEGRAM_API_TOKEN !== 'undefined' &&
|
||||
SECRET_TELEGRAM_API_TOKEN !== 'default-gh-action-secret' &&
|
||||
typeof SECRET_TELEGRAM_CHAT_ID !== 'undefined' &&
|
||||
SECRET_TELEGRAM_CHAT_ID !== 'default-gh-action-secret'
|
||||
) {
|
||||
event.waitUntil(notifyTelegram(monitor, monitorOperational))
|
||||
}
|
||||
|
||||
const failedDay = getDate()
|
||||
if (!monitorsState[monitor.id].failedDays.includes(failedDay)) {
|
||||
console.log('Saving new failed daily status ...')
|
||||
monitorsState[monitor.id].failedDays.push(failedDay)
|
||||
// make sure checkDay exists in checks in cases when needed
|
||||
if (
|
||||
(config.settings.collectResponseTimes || !monitorOperational) &&
|
||||
!monitorsState.monitors[monitor.id].checks.hasOwnProperty(checkDay)
|
||||
) {
|
||||
monitorsState.monitors[monitor.id].checks[checkDay] = {
|
||||
fails: 0,
|
||||
res: {},
|
||||
}
|
||||
}
|
||||
|
||||
if (config.settings.collectResponseTimes && monitorOperational) {
|
||||
// make sure location exists in current checkDay
|
||||
if (
|
||||
!monitorsState.monitors[monitor.id].checks[checkDay].res.hasOwnProperty(
|
||||
checkLocation,
|
||||
)
|
||||
) {
|
||||
monitorsState.monitors[monitor.id].checks[checkDay].res[
|
||||
checkLocation
|
||||
] = {
|
||||
n: 0,
|
||||
ms: 0,
|
||||
a: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// increment number of checks and sum of ms
|
||||
const no = ++monitorsState.monitors[monitor.id].checks[checkDay].res[
|
||||
checkLocation
|
||||
].n
|
||||
const ms = (monitorsState.monitors[monitor.id].checks[checkDay].res[
|
||||
checkLocation
|
||||
].ms += requestTime)
|
||||
|
||||
// save new average ms
|
||||
monitorsState.monitors[monitor.id].checks[checkDay].res[
|
||||
checkLocation
|
||||
].a = Math.round(ms / no)
|
||||
} else if (!monitorOperational) {
|
||||
// Save allOperational to false
|
||||
monitorsState.lastUpdate.allOperational = false
|
||||
|
||||
// Increment failed checks, only on status change (maybe call it .incidents instead?)
|
||||
if (monitorStatusChanged) {
|
||||
monitorsState.monitors[monitor.id].checks[checkDay].fails++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get Worker PoP and save it to monitorsStateMetadata
|
||||
const res = await fetch('https://cloudflare-dns.com/dns-query', {
|
||||
method: 'OPTIONS',
|
||||
})
|
||||
const loc = res.headers.get('cf-ray').split('-')[1]
|
||||
monitorsStateMetadata.lastUpdate = {
|
||||
loc,
|
||||
time: Date.now()
|
||||
}
|
||||
// Save last update information
|
||||
monitorsState.lastUpdate.time = Date.now()
|
||||
monitorsState.lastUpdate.loc = checkLocation
|
||||
|
||||
// Save monitorsState and monitorsStateMetadata to KV storage
|
||||
await setKV('monitors_data', JSON.stringify(monitorsState), monitorsStateMetadata)
|
||||
// Save monitorsState to KV storage
|
||||
await setKVMonitors(monitorsState)
|
||||
|
||||
return new Response('OK')
|
||||
}
|
||||
|
@ -1,18 +1,28 @@
|
||||
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")
|
||||
const kvDataKey = 'monitors_data_v1_1'
|
||||
|
||||
export async function getKVMonitors() {
|
||||
// trying both to see performance difference
|
||||
return KV_STATUS_PAGE.get(kvDataKey, 'json')
|
||||
//return JSON.parse(await KV_STATUS_PAGE.get(kvDataKey, 'text'))
|
||||
}
|
||||
|
||||
export async function setKVMonitors(data) {
|
||||
return setKV(kvDataKey, JSON.stringify(data))
|
||||
}
|
||||
|
||||
const getOperationalLabel = (operational) => {
|
||||
return operational
|
||||
? config.settings.monitorLabelOperational
|
||||
: config.settings.monitorLabelNotOperational
|
||||
}
|
||||
|
||||
export async function setKV(key, value, metadata, expirationTtl) {
|
||||
return KV_STATUS_PAGE.put(key, value, { metadata, expirationTtl })
|
||||
}
|
||||
|
||||
export async function getKVWithMetadata(key, type = 'text') {
|
||||
return KV_STATUS_PAGE.getWithMetadata(key, type)
|
||||
}
|
||||
|
||||
export async function notifySlack(monitor, operational) {
|
||||
const payload = {
|
||||
attachments: [
|
||||
@ -23,11 +33,9 @@ export async function notifySlack(monitor, operational) {
|
||||
type: 'section',
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: `Monitor *${monitor.name}* changed status to *${
|
||||
operational
|
||||
? config.settings.monitorLabelOperational
|
||||
: config.settings.monitorLabelNotOperational
|
||||
}*`,
|
||||
text: `Monitor *${
|
||||
monitor.name
|
||||
}* changed status to *${getOperationalLabel(operational)}*`,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -35,9 +43,9 @@ export async function notifySlack(monitor, operational) {
|
||||
elements: [
|
||||
{
|
||||
type: 'mrkdwn',
|
||||
text: `${
|
||||
operational ? ':white_check_mark:' : ':x:'
|
||||
} \`${monitor.method ? monitor.method : "GET"} ${monitor.url}\` - :eyes: <${
|
||||
text: `${operational ? ':white_check_mark:' : ':x:'} \`${
|
||||
monitor.method ? monitor.method : 'GET'
|
||||
} ${monitor.url}\` - :eyes: <${
|
||||
config.settings.url
|
||||
}|Status Page>`,
|
||||
},
|
||||
@ -54,30 +62,58 @@ export async function notifySlack(monitor, operational) {
|
||||
})
|
||||
}
|
||||
|
||||
export async function notifyTelegram(monitor, operational) {
|
||||
const text = `Monitor *${monitor.name.replace(
|
||||
'-',
|
||||
'\\-',
|
||||
)}* changed status to *${getOperationalLabel(operational)}*
|
||||
${operational ? '✅' : '❌'} \`${monitor.method ? monitor.method : 'GET'} ${
|
||||
monitor.url
|
||||
}\` \\- 👀 [Status Page](${config.settings.url})`
|
||||
|
||||
const payload = new FormData()
|
||||
payload.append('chat_id', SECRET_TELEGRAM_CHAT_ID)
|
||||
payload.append('parse_mode', 'MarkdownV2')
|
||||
payload.append('text', text)
|
||||
|
||||
const telegramUrl = `https://api.telegram.org/bot${SECRET_TELEGRAM_API_TOKEN}/sendMessage`
|
||||
return fetch(telegramUrl, {
|
||||
body: payload,
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
export function useKeyPress(targetKey) {
|
||||
const [keyPressed, setKeyPressed] = useState(false)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
export async function getCheckLocation() {
|
||||
const res = await fetch('https://cloudflare-dns.com/dns-query', {
|
||||
method: 'OPTIONS',
|
||||
})
|
||||
return res.headers.get('cf-ray').split('-')[1]
|
||||
}
|
||||
|
918
tailwind.config.js
Normal file
918
tailwind.config.js
Normal file
@ -0,0 +1,918 @@
|
||||
const colors = require('tailwindcss/colors')
|
||||
|
||||
module.exports = {
|
||||
purge: {
|
||||
content: ['./src/**/*.js', './pages/**/*.js', './public/tailwind.css'],
|
||||
},
|
||||
presets: [],
|
||||
darkMode: 'class', // or 'media' or 'class'
|
||||
theme: {
|
||||
screens: {
|
||||
sm: '640px',
|
||||
md: '768px',
|
||||
lg: '1024px',
|
||||
xl: '1280px',
|
||||
'2xl': '1536px',
|
||||
},
|
||||
colors: {
|
||||
transparent: 'transparent',
|
||||
current: 'currentColor',
|
||||
|
||||
black: colors.black,
|
||||
white: colors.white,
|
||||
gray: colors.coolGray,
|
||||
red: colors.red,
|
||||
yellow: colors.yellow,
|
||||
green: colors.green,
|
||||
blue: colors.lightBlue,
|
||||
indigo: colors.indigo,
|
||||
purple: colors.violet,
|
||||
pink: colors.pink,
|
||||
},
|
||||
spacing: {
|
||||
px: '1px',
|
||||
0: '0px',
|
||||
0.5: '0.125rem',
|
||||
1: '0.25rem',
|
||||
1.5: '0.375rem',
|
||||
2: '0.5rem',
|
||||
2.5: '0.625rem',
|
||||
3: '0.75rem',
|
||||
3.5: '0.875rem',
|
||||
4: '1rem',
|
||||
5: '1.25rem',
|
||||
6: '1.5rem',
|
||||
7: '1.75rem',
|
||||
8: '2rem',
|
||||
9: '2.25rem',
|
||||
10: '2.5rem',
|
||||
11: '2.75rem',
|
||||
12: '3rem',
|
||||
14: '3.5rem',
|
||||
16: '4rem',
|
||||
20: '5rem',
|
||||
24: '6rem',
|
||||
28: '7rem',
|
||||
32: '8rem',
|
||||
36: '9rem',
|
||||
40: '10rem',
|
||||
44: '11rem',
|
||||
48: '12rem',
|
||||
52: '13rem',
|
||||
56: '14rem',
|
||||
60: '15rem',
|
||||
64: '16rem',
|
||||
72: '18rem',
|
||||
80: '20rem',
|
||||
96: '24rem',
|
||||
},
|
||||
animation: {
|
||||
none: 'none',
|
||||
spin: 'spin 1s linear infinite',
|
||||
ping: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
|
||||
pulse: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||
bounce: 'bounce 1s infinite',
|
||||
},
|
||||
backgroundColor: (theme) => theme('colors'),
|
||||
backgroundImage: {
|
||||
none: 'none',
|
||||
'gradient-to-t': 'linear-gradient(to top, var(--tw-gradient-stops))',
|
||||
'gradient-to-tr':
|
||||
'linear-gradient(to top right, var(--tw-gradient-stops))',
|
||||
'gradient-to-r': 'linear-gradient(to right, var(--tw-gradient-stops))',
|
||||
'gradient-to-br':
|
||||
'linear-gradient(to bottom right, var(--tw-gradient-stops))',
|
||||
'gradient-to-b': 'linear-gradient(to bottom, var(--tw-gradient-stops))',
|
||||
'gradient-to-bl':
|
||||
'linear-gradient(to bottom left, var(--tw-gradient-stops))',
|
||||
'gradient-to-l': 'linear-gradient(to left, var(--tw-gradient-stops))',
|
||||
'gradient-to-tl':
|
||||
'linear-gradient(to top left, var(--tw-gradient-stops))',
|
||||
},
|
||||
backgroundOpacity: (theme) => theme('opacity'),
|
||||
backgroundPosition: {
|
||||
bottom: 'bottom',
|
||||
center: 'center',
|
||||
left: 'left',
|
||||
'left-bottom': 'left bottom',
|
||||
'left-top': 'left top',
|
||||
right: 'right',
|
||||
'right-bottom': 'right bottom',
|
||||
'right-top': 'right top',
|
||||
top: 'top',
|
||||
},
|
||||
backgroundSize: {
|
||||
auto: 'auto',
|
||||
cover: 'cover',
|
||||
contain: 'contain',
|
||||
},
|
||||
borderColor: (theme) => ({
|
||||
...theme('colors'),
|
||||
DEFAULT: theme('colors.gray.200', 'currentColor'),
|
||||
}),
|
||||
borderOpacity: (theme) => theme('opacity'),
|
||||
borderRadius: {
|
||||
none: '0px',
|
||||
sm: '0.125rem',
|
||||
DEFAULT: '0.25rem',
|
||||
md: '0.375rem',
|
||||
lg: '0.5rem',
|
||||
xl: '0.75rem',
|
||||
'2xl': '1rem',
|
||||
'3xl': '1.5rem',
|
||||
full: '9999px',
|
||||
},
|
||||
borderWidth: {
|
||||
DEFAULT: '1px',
|
||||
0: '0px',
|
||||
2: '2px',
|
||||
4: '4px',
|
||||
8: '8px',
|
||||
},
|
||||
boxShadow: {
|
||||
sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
||||
DEFAULT:
|
||||
'0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
|
||||
md:
|
||||
'0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
|
||||
lg:
|
||||
'0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
|
||||
xl:
|
||||
'0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
|
||||
'2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
|
||||
inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)',
|
||||
none: 'none',
|
||||
},
|
||||
container: {},
|
||||
cursor: {
|
||||
auto: 'auto',
|
||||
default: 'default',
|
||||
pointer: 'pointer',
|
||||
wait: 'wait',
|
||||
text: 'text',
|
||||
move: 'move',
|
||||
'not-allowed': 'not-allowed',
|
||||
},
|
||||
divideColor: (theme) => theme('borderColor'),
|
||||
divideOpacity: (theme) => theme('borderOpacity'),
|
||||
divideWidth: (theme) => theme('borderWidth'),
|
||||
fill: { current: 'currentColor' },
|
||||
flex: {
|
||||
1: '1 1 0%',
|
||||
auto: '1 1 auto',
|
||||
initial: '0 1 auto',
|
||||
none: 'none',
|
||||
},
|
||||
flexGrow: {
|
||||
0: '0',
|
||||
DEFAULT: '1',
|
||||
},
|
||||
flexShrink: {
|
||||
0: '0',
|
||||
DEFAULT: '1',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: [
|
||||
'ui-sans-serif',
|
||||
'system-ui',
|
||||
'-apple-system',
|
||||
'BlinkMacSystemFont',
|
||||
'"Segoe UI"',
|
||||
'Roboto',
|
||||
'"Helvetica Neue"',
|
||||
'Arial',
|
||||
'"Noto Sans"',
|
||||
'sans-serif',
|
||||
'"Apple Color Emoji"',
|
||||
'"Segoe UI Emoji"',
|
||||
'"Segoe UI Symbol"',
|
||||
'"Noto Color Emoji"',
|
||||
],
|
||||
serif: [
|
||||
'ui-serif',
|
||||
'Georgia',
|
||||
'Cambria',
|
||||
'"Times New Roman"',
|
||||
'Times',
|
||||
'serif',
|
||||
],
|
||||
mono: [
|
||||
'ui-monospace',
|
||||
'SFMono-Regular',
|
||||
'Menlo',
|
||||
'Monaco',
|
||||
'Consolas',
|
||||
'"Liberation Mono"',
|
||||
'"Courier New"',
|
||||
'monospace',
|
||||
],
|
||||
},
|
||||
fontSize: {
|
||||
xs: ['0.75rem', { lineHeight: '1rem' }],
|
||||
sm: ['0.875rem', { lineHeight: '1.25rem' }],
|
||||
base: ['1rem', { lineHeight: '1.5rem' }],
|
||||
lg: ['1.125rem', { lineHeight: '1.75rem' }],
|
||||
xl: ['1.25rem', { lineHeight: '1.75rem' }],
|
||||
'2xl': ['1.5rem', { lineHeight: '2rem' }],
|
||||
'3xl': ['1.875rem', { lineHeight: '2.25rem' }],
|
||||
'4xl': ['2.25rem', { lineHeight: '2.5rem' }],
|
||||
'5xl': ['3rem', { lineHeight: '1' }],
|
||||
'6xl': ['3.75rem', { lineHeight: '1' }],
|
||||
'7xl': ['4.5rem', { lineHeight: '1' }],
|
||||
'8xl': ['6rem', { lineHeight: '1' }],
|
||||
'9xl': ['8rem', { lineHeight: '1' }],
|
||||
},
|
||||
fontWeight: {
|
||||
thin: '100',
|
||||
extralight: '200',
|
||||
light: '300',
|
||||
normal: '400',
|
||||
medium: '500',
|
||||
semibold: '600',
|
||||
bold: '700',
|
||||
extrabold: '800',
|
||||
black: '900',
|
||||
},
|
||||
gap: (theme) => theme('spacing'),
|
||||
gradientColorStops: (theme) => theme('colors'),
|
||||
gridAutoColumns: {
|
||||
auto: 'auto',
|
||||
min: 'min-content',
|
||||
max: 'max-content',
|
||||
fr: 'minmax(0, 1fr)',
|
||||
},
|
||||
gridAutoRows: {
|
||||
auto: 'auto',
|
||||
min: 'min-content',
|
||||
max: 'max-content',
|
||||
fr: 'minmax(0, 1fr)',
|
||||
},
|
||||
gridColumn: {
|
||||
auto: 'auto',
|
||||
'span-1': 'span 1 / span 1',
|
||||
'span-2': 'span 2 / span 2',
|
||||
'span-3': 'span 3 / span 3',
|
||||
'span-4': 'span 4 / span 4',
|
||||
'span-5': 'span 5 / span 5',
|
||||
'span-6': 'span 6 / span 6',
|
||||
'span-7': 'span 7 / span 7',
|
||||
'span-8': 'span 8 / span 8',
|
||||
'span-9': 'span 9 / span 9',
|
||||
'span-10': 'span 10 / span 10',
|
||||
'span-11': 'span 11 / span 11',
|
||||
'span-12': 'span 12 / span 12',
|
||||
'span-full': '1 / -1',
|
||||
},
|
||||
gridColumnEnd: {
|
||||
auto: 'auto',
|
||||
1: '1',
|
||||
2: '2',
|
||||
3: '3',
|
||||
4: '4',
|
||||
5: '5',
|
||||
6: '6',
|
||||
7: '7',
|
||||
8: '8',
|
||||
9: '9',
|
||||
10: '10',
|
||||
11: '11',
|
||||
12: '12',
|
||||
13: '13',
|
||||
},
|
||||
gridColumnStart: {
|
||||
auto: 'auto',
|
||||
1: '1',
|
||||
2: '2',
|
||||
3: '3',
|
||||
4: '4',
|
||||
5: '5',
|
||||
6: '6',
|
||||
7: '7',
|
||||
8: '8',
|
||||
9: '9',
|
||||
10: '10',
|
||||
11: '11',
|
||||
12: '12',
|
||||
13: '13',
|
||||
},
|
||||
gridRow: {
|
||||
auto: 'auto',
|
||||
'span-1': 'span 1 / span 1',
|
||||
'span-2': 'span 2 / span 2',
|
||||
'span-3': 'span 3 / span 3',
|
||||
'span-4': 'span 4 / span 4',
|
||||
'span-5': 'span 5 / span 5',
|
||||
'span-6': 'span 6 / span 6',
|
||||
'span-full': '1 / -1',
|
||||
},
|
||||
gridRowStart: {
|
||||
auto: 'auto',
|
||||
1: '1',
|
||||
2: '2',
|
||||
3: '3',
|
||||
4: '4',
|
||||
5: '5',
|
||||
6: '6',
|
||||
7: '7',
|
||||
},
|
||||
gridRowEnd: {
|
||||
auto: 'auto',
|
||||
1: '1',
|
||||
2: '2',
|
||||
3: '3',
|
||||
4: '4',
|
||||
5: '5',
|
||||
6: '6',
|
||||
7: '7',
|
||||
},
|
||||
transformOrigin: {
|
||||
center: 'center',
|
||||
top: 'top',
|
||||
'top-right': 'top right',
|
||||
right: 'right',
|
||||
'bottom-right': 'bottom right',
|
||||
bottom: 'bottom',
|
||||
'bottom-left': 'bottom left',
|
||||
left: 'left',
|
||||
'top-left': 'top left',
|
||||
},
|
||||
gridTemplateColumns: {
|
||||
none: 'none',
|
||||
1: 'repeat(1, minmax(0, 1fr))',
|
||||
2: 'repeat(2, minmax(0, 1fr))',
|
||||
3: 'repeat(3, minmax(0, 1fr))',
|
||||
4: 'repeat(4, minmax(0, 1fr))',
|
||||
5: 'repeat(5, minmax(0, 1fr))',
|
||||
6: 'repeat(6, minmax(0, 1fr))',
|
||||
7: 'repeat(7, minmax(0, 1fr))',
|
||||
8: 'repeat(8, minmax(0, 1fr))',
|
||||
9: 'repeat(9, minmax(0, 1fr))',
|
||||
10: 'repeat(10, minmax(0, 1fr))',
|
||||
11: 'repeat(11, minmax(0, 1fr))',
|
||||
12: 'repeat(12, minmax(0, 1fr))',
|
||||
},
|
||||
gridTemplateRows: {
|
||||
none: 'none',
|
||||
1: 'repeat(1, minmax(0, 1fr))',
|
||||
2: 'repeat(2, minmax(0, 1fr))',
|
||||
3: 'repeat(3, minmax(0, 1fr))',
|
||||
4: 'repeat(4, minmax(0, 1fr))',
|
||||
5: 'repeat(5, minmax(0, 1fr))',
|
||||
6: 'repeat(6, minmax(0, 1fr))',
|
||||
},
|
||||
height: (theme) => ({
|
||||
auto: 'auto',
|
||||
...theme('spacing'),
|
||||
'1/2': '50%',
|
||||
'1/3': '33.333333%',
|
||||
'2/3': '66.666667%',
|
||||
'1/4': '25%',
|
||||
'2/4': '50%',
|
||||
'3/4': '75%',
|
||||
'1/5': '20%',
|
||||
'2/5': '40%',
|
||||
'3/5': '60%',
|
||||
'4/5': '80%',
|
||||
'1/6': '16.666667%',
|
||||
'2/6': '33.333333%',
|
||||
'3/6': '50%',
|
||||
'4/6': '66.666667%',
|
||||
'5/6': '83.333333%',
|
||||
full: '100%',
|
||||
screen: '100vh',
|
||||
}),
|
||||
inset: (theme, { negative }) => ({
|
||||
auto: 'auto',
|
||||
...theme('spacing'),
|
||||
...negative(theme('spacing')),
|
||||
'1/2': '50%',
|
||||
'1/3': '33.333333%',
|
||||
'2/3': '66.666667%',
|
||||
'1/4': '25%',
|
||||
'2/4': '50%',
|
||||
'3/4': '75%',
|
||||
full: '100%',
|
||||
'-1/2': '-50%',
|
||||
'-1/3': '-33.333333%',
|
||||
'-2/3': '-66.666667%',
|
||||
'-1/4': '-25%',
|
||||
'-2/4': '-50%',
|
||||
'-3/4': '-75%',
|
||||
'-full': '-100%',
|
||||
}),
|
||||
keyframes: {
|
||||
spin: {
|
||||
to: {
|
||||
transform: 'rotate(360deg)',
|
||||
},
|
||||
},
|
||||
ping: {
|
||||
'75%, 100%': {
|
||||
transform: 'scale(2)',
|
||||
opacity: '0',
|
||||
},
|
||||
},
|
||||
pulse: {
|
||||
'50%': {
|
||||
opacity: '.5',
|
||||
},
|
||||
},
|
||||
bounce: {
|
||||
'0%, 100%': {
|
||||
transform: 'translateY(-25%)',
|
||||
animationTimingFunction: 'cubic-bezier(0.8,0,1,1)',
|
||||
},
|
||||
'50%': {
|
||||
transform: 'none',
|
||||
animationTimingFunction: 'cubic-bezier(0,0,0.2,1)',
|
||||
},
|
||||
},
|
||||
},
|
||||
letterSpacing: {
|
||||
tighter: '-0.05em',
|
||||
tight: '-0.025em',
|
||||
normal: '0em',
|
||||
wide: '0.025em',
|
||||
wider: '0.05em',
|
||||
widest: '0.1em',
|
||||
},
|
||||
lineHeight: {
|
||||
none: '1',
|
||||
tight: '1.25',
|
||||
snug: '1.375',
|
||||
normal: '1.5',
|
||||
relaxed: '1.625',
|
||||
loose: '2',
|
||||
3: '.75rem',
|
||||
4: '1rem',
|
||||
5: '1.25rem',
|
||||
6: '1.5rem',
|
||||
7: '1.75rem',
|
||||
8: '2rem',
|
||||
9: '2.25rem',
|
||||
10: '2.5rem',
|
||||
},
|
||||
listStyleType: {
|
||||
none: 'none',
|
||||
disc: 'disc',
|
||||
decimal: 'decimal',
|
||||
},
|
||||
margin: (theme, { negative }) => ({
|
||||
auto: 'auto',
|
||||
...theme('spacing'),
|
||||
...negative(theme('spacing')),
|
||||
}),
|
||||
maxHeight: (theme) => ({
|
||||
...theme('spacing'),
|
||||
full: '100%',
|
||||
screen: '100vh',
|
||||
}),
|
||||
maxWidth: (theme, { breakpoints }) => ({
|
||||
none: 'none',
|
||||
0: '0rem',
|
||||
xs: '20rem',
|
||||
sm: '24rem',
|
||||
md: '28rem',
|
||||
lg: '32rem',
|
||||
xl: '36rem',
|
||||
'2xl': '42rem',
|
||||
'3xl': '48rem',
|
||||
'4xl': '56rem',
|
||||
'5xl': '64rem',
|
||||
'6xl': '72rem',
|
||||
'7xl': '80rem',
|
||||
full: '100%',
|
||||
min: 'min-content',
|
||||
max: 'max-content',
|
||||
prose: '65ch',
|
||||
...breakpoints(theme('screens')),
|
||||
}),
|
||||
minHeight: {
|
||||
0: '0px',
|
||||
full: '100%',
|
||||
screen: '100vh',
|
||||
},
|
||||
minWidth: {
|
||||
0: '0px',
|
||||
full: '100%',
|
||||
min: 'min-content',
|
||||
max: 'max-content',
|
||||
},
|
||||
objectPosition: {
|
||||
bottom: 'bottom',
|
||||
center: 'center',
|
||||
left: 'left',
|
||||
'left-bottom': 'left bottom',
|
||||
'left-top': 'left top',
|
||||
right: 'right',
|
||||
'right-bottom': 'right bottom',
|
||||
'right-top': 'right top',
|
||||
top: 'top',
|
||||
},
|
||||
opacity: {
|
||||
0: '0',
|
||||
5: '0.05',
|
||||
10: '0.1',
|
||||
20: '0.2',
|
||||
25: '0.25',
|
||||
30: '0.3',
|
||||
40: '0.4',
|
||||
50: '0.5',
|
||||
60: '0.6',
|
||||
70: '0.7',
|
||||
75: '0.75',
|
||||
80: '0.8',
|
||||
90: '0.9',
|
||||
95: '0.95',
|
||||
100: '1',
|
||||
},
|
||||
order: {
|
||||
first: '-9999',
|
||||
last: '9999',
|
||||
none: '0',
|
||||
1: '1',
|
||||
2: '2',
|
||||
3: '3',
|
||||
4: '4',
|
||||
5: '5',
|
||||
6: '6',
|
||||
7: '7',
|
||||
8: '8',
|
||||
9: '9',
|
||||
10: '10',
|
||||
11: '11',
|
||||
12: '12',
|
||||
},
|
||||
outline: {
|
||||
none: ['2px solid transparent', '2px'],
|
||||
white: ['2px dotted white', '2px'],
|
||||
black: ['2px dotted black', '2px'],
|
||||
},
|
||||
padding: (theme) => theme('spacing'),
|
||||
placeholderColor: (theme) => theme('colors'),
|
||||
placeholderOpacity: (theme) => theme('opacity'),
|
||||
ringColor: (theme) => ({
|
||||
DEFAULT: theme('colors.blue.500', '#3b82f6'),
|
||||
...theme('colors'),
|
||||
}),
|
||||
ringOffsetColor: (theme) => theme('colors'),
|
||||
ringOffsetWidth: {
|
||||
0: '0px',
|
||||
1: '1px',
|
||||
2: '2px',
|
||||
4: '4px',
|
||||
8: '8px',
|
||||
},
|
||||
ringOpacity: (theme) => ({
|
||||
DEFAULT: '0.5',
|
||||
...theme('opacity'),
|
||||
}),
|
||||
ringWidth: {
|
||||
DEFAULT: '3px',
|
||||
0: '0px',
|
||||
1: '1px',
|
||||
2: '2px',
|
||||
4: '4px',
|
||||
8: '8px',
|
||||
},
|
||||
rotate: {
|
||||
'-180': '-180deg',
|
||||
'-90': '-90deg',
|
||||
'-45': '-45deg',
|
||||
'-12': '-12deg',
|
||||
'-6': '-6deg',
|
||||
'-3': '-3deg',
|
||||
'-2': '-2deg',
|
||||
'-1': '-1deg',
|
||||
0: '0deg',
|
||||
1: '1deg',
|
||||
2: '2deg',
|
||||
3: '3deg',
|
||||
6: '6deg',
|
||||
12: '12deg',
|
||||
45: '45deg',
|
||||
90: '90deg',
|
||||
180: '180deg',
|
||||
},
|
||||
scale: {
|
||||
0: '0',
|
||||
50: '.5',
|
||||
75: '.75',
|
||||
90: '.9',
|
||||
95: '.95',
|
||||
100: '1',
|
||||
105: '1.05',
|
||||
110: '1.1',
|
||||
125: '1.25',
|
||||
150: '1.5',
|
||||
},
|
||||
skew: {
|
||||
'-12': '-12deg',
|
||||
'-6': '-6deg',
|
||||
'-3': '-3deg',
|
||||
'-2': '-2deg',
|
||||
'-1': '-1deg',
|
||||
0: '0deg',
|
||||
1: '1deg',
|
||||
2: '2deg',
|
||||
3: '3deg',
|
||||
6: '6deg',
|
||||
12: '12deg',
|
||||
},
|
||||
space: (theme, { negative }) => ({
|
||||
...theme('spacing'),
|
||||
...negative(theme('spacing')),
|
||||
}),
|
||||
stroke: {
|
||||
current: 'currentColor',
|
||||
},
|
||||
strokeWidth: {
|
||||
0: '0',
|
||||
1: '1',
|
||||
2: '2',
|
||||
},
|
||||
textColor: (theme) => theme('colors'),
|
||||
textOpacity: (theme) => theme('opacity'),
|
||||
transitionDuration: {
|
||||
DEFAULT: '150ms',
|
||||
75: '75ms',
|
||||
100: '100ms',
|
||||
150: '150ms',
|
||||
200: '200ms',
|
||||
300: '300ms',
|
||||
500: '500ms',
|
||||
700: '700ms',
|
||||
1000: '1000ms',
|
||||
},
|
||||
transitionDelay: {
|
||||
75: '75ms',
|
||||
100: '100ms',
|
||||
150: '150ms',
|
||||
200: '200ms',
|
||||
300: '300ms',
|
||||
500: '500ms',
|
||||
700: '700ms',
|
||||
1000: '1000ms',
|
||||
},
|
||||
transitionProperty: {
|
||||
none: 'none',
|
||||
all: 'all',
|
||||
DEFAULT:
|
||||
'background-color, border-color, color, fill, stroke, opacity, box-shadow, transform',
|
||||
colors: 'background-color, border-color, color, fill, stroke',
|
||||
opacity: 'opacity',
|
||||
shadow: 'box-shadow',
|
||||
transform: 'transform',
|
||||
},
|
||||
transitionTimingFunction: {
|
||||
DEFAULT: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
linear: 'linear',
|
||||
in: 'cubic-bezier(0.4, 0, 1, 1)',
|
||||
out: 'cubic-bezier(0, 0, 0.2, 1)',
|
||||
'in-out': 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
},
|
||||
translate: (theme, { negative }) => ({
|
||||
...theme('spacing'),
|
||||
...negative(theme('spacing')),
|
||||
'1/2': '50%',
|
||||
'1/3': '33.333333%',
|
||||
'2/3': '66.666667%',
|
||||
'1/4': '25%',
|
||||
'2/4': '50%',
|
||||
'3/4': '75%',
|
||||
full: '100%',
|
||||
'-1/2': '-50%',
|
||||
'-1/3': '-33.333333%',
|
||||
'-2/3': '-66.666667%',
|
||||
'-1/4': '-25%',
|
||||
'-2/4': '-50%',
|
||||
'-3/4': '-75%',
|
||||
'-full': '-100%',
|
||||
}),
|
||||
width: (theme) => ({
|
||||
auto: 'auto',
|
||||
...theme('spacing'),
|
||||
'1/2': '50%',
|
||||
'1/3': '33.333333%',
|
||||
'2/3': '66.666667%',
|
||||
'1/4': '25%',
|
||||
'2/4': '50%',
|
||||
'3/4': '75%',
|
||||
'1/5': '20%',
|
||||
'2/5': '40%',
|
||||
'3/5': '60%',
|
||||
'4/5': '80%',
|
||||
'1/6': '16.666667%',
|
||||
'2/6': '33.333333%',
|
||||
'3/6': '50%',
|
||||
'4/6': '66.666667%',
|
||||
'5/6': '83.333333%',
|
||||
'1/12': '8.333333%',
|
||||
'2/12': '16.666667%',
|
||||
'3/12': '25%',
|
||||
'4/12': '33.333333%',
|
||||
'5/12': '41.666667%',
|
||||
'6/12': '50%',
|
||||
'7/12': '58.333333%',
|
||||
'8/12': '66.666667%',
|
||||
'9/12': '75%',
|
||||
'10/12': '83.333333%',
|
||||
'11/12': '91.666667%',
|
||||
full: '100%',
|
||||
screen: '100vw',
|
||||
min: 'min-content',
|
||||
max: 'max-content',
|
||||
}),
|
||||
zIndex: {
|
||||
auto: 'auto',
|
||||
0: '0',
|
||||
10: '10',
|
||||
20: '20',
|
||||
30: '30',
|
||||
40: '40',
|
||||
50: '50',
|
||||
},
|
||||
},
|
||||
variantOrder: [
|
||||
'first',
|
||||
'last',
|
||||
'odd',
|
||||
'even',
|
||||
'visited',
|
||||
'checked',
|
||||
'group-hover',
|
||||
'group-focus',
|
||||
'focus-within',
|
||||
'hover',
|
||||
'focus',
|
||||
'focus-visible',
|
||||
'active',
|
||||
'disabled',
|
||||
],
|
||||
variants: {
|
||||
accessibility: ['responsive', 'focus-within', 'focus'],
|
||||
alignContent: ['responsive'],
|
||||
alignItems: ['responsive'],
|
||||
alignSelf: ['responsive'],
|
||||
animation: ['responsive'],
|
||||
appearance: ['responsive'],
|
||||
backgroundAttachment: ['responsive'],
|
||||
backgroundClip: ['responsive'],
|
||||
backgroundColor: [
|
||||
'responsive',
|
||||
'dark',
|
||||
'group-hover',
|
||||
'focus-within',
|
||||
'hover',
|
||||
'focus',
|
||||
],
|
||||
backgroundImage: ['responsive'],
|
||||
backgroundOpacity: [
|
||||
'responsive',
|
||||
'group-hover',
|
||||
'focus-within',
|
||||
'hover',
|
||||
'focus',
|
||||
],
|
||||
backgroundPosition: ['responsive'],
|
||||
backgroundRepeat: ['responsive'],
|
||||
backgroundSize: ['responsive'],
|
||||
borderCollapse: ['responsive'],
|
||||
borderColor: [
|
||||
'responsive',
|
||||
'dark',
|
||||
'group-hover',
|
||||
'focus-within',
|
||||
'hover',
|
||||
'focus',
|
||||
],
|
||||
borderOpacity: [
|
||||
'responsive',
|
||||
'group-hover',
|
||||
'focus-within',
|
||||
'hover',
|
||||
'focus',
|
||||
],
|
||||
borderRadius: ['responsive'],
|
||||
borderStyle: ['responsive'],
|
||||
borderWidth: ['responsive'],
|
||||
boxShadow: ['responsive', 'group-hover', 'focus-within', 'hover', 'focus'],
|
||||
boxSizing: ['responsive'],
|
||||
clear: ['responsive'],
|
||||
container: ['responsive'],
|
||||
cursor: ['responsive'],
|
||||
display: ['responsive'],
|
||||
divideColor: ['responsive', 'dark'],
|
||||
divideOpacity: ['responsive'],
|
||||
divideStyle: ['responsive'],
|
||||
divideWidth: ['responsive'],
|
||||
fill: ['responsive'],
|
||||
flex: ['responsive'],
|
||||
flexDirection: ['responsive'],
|
||||
flexGrow: ['responsive'],
|
||||
flexShrink: ['responsive'],
|
||||
flexWrap: ['responsive'],
|
||||
float: ['responsive'],
|
||||
fontFamily: ['responsive'],
|
||||
fontSize: ['responsive'],
|
||||
fontSmoothing: ['responsive'],
|
||||
fontStyle: ['responsive'],
|
||||
fontVariantNumeric: ['responsive'],
|
||||
fontWeight: ['responsive'],
|
||||
gap: ['responsive'],
|
||||
gradientColorStops: ['responsive', 'dark', 'hover', 'focus'],
|
||||
gridAutoColumns: ['responsive'],
|
||||
gridAutoFlow: ['responsive'],
|
||||
gridAutoRows: ['responsive'],
|
||||
gridColumn: ['responsive'],
|
||||
gridColumnEnd: ['responsive'],
|
||||
gridColumnStart: ['responsive'],
|
||||
gridRow: ['responsive'],
|
||||
gridRowEnd: ['responsive'],
|
||||
gridRowStart: ['responsive'],
|
||||
gridTemplateColumns: ['responsive'],
|
||||
gridTemplateRows: ['responsive'],
|
||||
height: ['responsive'],
|
||||
inset: ['responsive'],
|
||||
justifyContent: ['responsive'],
|
||||
justifyItems: ['responsive'],
|
||||
justifySelf: ['responsive'],
|
||||
letterSpacing: ['responsive'],
|
||||
lineHeight: ['responsive'],
|
||||
listStylePosition: ['responsive'],
|
||||
listStyleType: ['responsive'],
|
||||
margin: ['responsive'],
|
||||
maxHeight: ['responsive'],
|
||||
maxWidth: ['responsive'],
|
||||
minHeight: ['responsive'],
|
||||
minWidth: ['responsive'],
|
||||
objectFit: ['responsive'],
|
||||
objectPosition: ['responsive'],
|
||||
opacity: ['responsive', 'group-hover', 'focus-within', 'hover', 'focus'],
|
||||
order: ['responsive'],
|
||||
outline: ['responsive', 'focus-within', 'focus'],
|
||||
overflow: ['responsive'],
|
||||
overscrollBehavior: ['responsive'],
|
||||
padding: ['responsive'],
|
||||
placeContent: ['responsive'],
|
||||
placeItems: ['responsive'],
|
||||
placeSelf: ['responsive'],
|
||||
placeholderColor: ['responsive', 'dark', 'focus'],
|
||||
placeholderOpacity: ['responsive', 'focus'],
|
||||
pointerEvents: ['responsive'],
|
||||
position: ['responsive'],
|
||||
resize: ['responsive'],
|
||||
ringColor: ['responsive', 'dark', 'focus-within', 'focus'],
|
||||
ringOffsetColor: ['responsive', 'dark', 'focus-within', 'focus'],
|
||||
ringOffsetWidth: ['responsive', 'focus-within', 'focus'],
|
||||
ringOpacity: ['responsive', 'focus-within', 'focus'],
|
||||
ringWidth: ['responsive', 'focus-within', 'focus'],
|
||||
rotate: ['responsive', 'hover', 'focus'],
|
||||
scale: ['responsive', 'hover', 'focus'],
|
||||
skew: ['responsive', 'hover', 'focus'],
|
||||
space: ['responsive'],
|
||||
stroke: ['responsive'],
|
||||
strokeWidth: ['responsive'],
|
||||
tableLayout: ['responsive'],
|
||||
textAlign: ['responsive'],
|
||||
textColor: [
|
||||
'responsive',
|
||||
'dark',
|
||||
'group-hover',
|
||||
'focus-within',
|
||||
'hover',
|
||||
'focus',
|
||||
],
|
||||
textDecoration: [
|
||||
'responsive',
|
||||
'group-hover',
|
||||
'focus-within',
|
||||
'hover',
|
||||
'focus',
|
||||
],
|
||||
textOpacity: [
|
||||
'responsive',
|
||||
'group-hover',
|
||||
'focus-within',
|
||||
'hover',
|
||||
'focus',
|
||||
],
|
||||
textOverflow: ['responsive'],
|
||||
textTransform: ['responsive'],
|
||||
transform: ['responsive'],
|
||||
transformOrigin: ['responsive'],
|
||||
transitionDelay: ['responsive'],
|
||||
transitionDuration: ['responsive'],
|
||||
transitionProperty: ['responsive'],
|
||||
transitionTimingFunction: ['responsive'],
|
||||
translate: ['responsive', 'hover', 'focus'],
|
||||
userSelect: ['responsive'],
|
||||
verticalAlign: ['responsive'],
|
||||
visibility: ['responsive'],
|
||||
whitespace: ['responsive'],
|
||||
width: ['responsive'],
|
||||
wordBreak: ['responsive'],
|
||||
zIndex: ['responsive', 'focus-within', 'focus'],
|
||||
},
|
||||
plugins: [],
|
||||
}
|
Loading…
Reference in New Issue
Block a user