Merge branch 'main' into upcoming-and-active-maint
commit
8391bc6b68
|
|
@ -57,7 +57,7 @@ Please refer to [Wiki](https://github.com/lyc8503/UptimeFlare/wiki)
|
|||
- [x] Remove old incidents
|
||||
- [x] ~~Known issue~~: `fetch` doesn't support non-standard port (resolved after CF update)
|
||||
- [x] Compatibility date update
|
||||
- [x] Scheduled Maintenance
|
||||
- [x] Scheduled Maintenance
|
||||
- [ ] Update wiki/README and add docs for dev
|
||||
- [ ] Migration to Terraform Cloudflare provider version 5.x
|
||||
- [ ] Cloudflare D1 database
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
import { Divider, Text } from '@mantine/core'
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<>
|
||||
<Divider mt="lg" />
|
||||
<Text
|
||||
size="xs"
|
||||
mt="xs"
|
||||
mb="xs"
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
Open-source monitoring and status page powered by{' '}
|
||||
<a href="https://github.com/lyc8503/UptimeFlare" target="_blank">
|
||||
Uptimeflare
|
||||
</a>{' '}
|
||||
and{' '}
|
||||
<a href="https://www.cloudflare.com/" target="_blank">
|
||||
Cloudflare
|
||||
</a>
|
||||
, made with ❤ by{' '}
|
||||
<a href="https://github.com/lyc8503" target="_blank">
|
||||
lyc8503
|
||||
</a>
|
||||
.
|
||||
</Text>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -3,13 +3,13 @@ import classes from '@/styles/Header.module.css'
|
|||
import { pageConfig } from '@/uptime.config'
|
||||
import { PageConfigLink } from '@/types/config'
|
||||
|
||||
export default function Header() {
|
||||
const linkToElement = (link: PageConfigLink) => {
|
||||
export default function Header({ style }: { style?: React.CSSProperties }) {
|
||||
const linkToElement = (link: PageConfigLink, i: number) => {
|
||||
return (
|
||||
<a
|
||||
key={link.label}
|
||||
key={i}
|
||||
href={link.link}
|
||||
target="_blank"
|
||||
target={link.link.startsWith('/') ? undefined : '_blank'}
|
||||
className={classes.link}
|
||||
data-active={link.highlight}
|
||||
>
|
||||
|
|
@ -18,11 +18,16 @@ export default function Header() {
|
|||
)
|
||||
}
|
||||
|
||||
const links = [{ label: 'Incident History', link: '/incidents' }, ...(pageConfig.links || [])]
|
||||
|
||||
return (
|
||||
<header className={classes.header}>
|
||||
<header className={classes.header} style={style}>
|
||||
<Container size="md" className={classes.inner}>
|
||||
<div>
|
||||
<a href="https://github.com/lyc8503/UptimeFlare" target="_blank">
|
||||
<a
|
||||
href={location.pathname == '/' ? 'https://github.com/lyc8503/UptimeFlare' : '/'}
|
||||
target={location.pathname == '/' ? '_blank' : undefined}
|
||||
>
|
||||
<Text size="xl" span>
|
||||
🕒
|
||||
</Text>
|
||||
|
|
@ -39,11 +44,11 @@ export default function Header() {
|
|||
</div>
|
||||
|
||||
<Group gap={5} visibleFrom="sm">
|
||||
{pageConfig.links?.map(linkToElement)}
|
||||
{links?.map(linkToElement)}
|
||||
</Group>
|
||||
|
||||
<Group gap={5} hiddenFrom="sm">
|
||||
{pageConfig.links?.filter((link) => link.highlight).map(linkToElement)}
|
||||
{links?.filter((link) => link.highlight || link.link.startsWith('/')).map(linkToElement)}
|
||||
</Group>
|
||||
</Container>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Alert, List, Text, Group, Box } from '@mantine/core'
|
||||
import { Alert, List, Text, Group, Box, useMantineTheme } from '@mantine/core'
|
||||
import { useMediaQuery } from '@mantine/hooks'
|
||||
import { IconAlertTriangle } from '@tabler/icons-react'
|
||||
import { MaintenanceConfig, MonitorTarget } from '@/types/config'
|
||||
import { pageConfig } from '@/uptime.config'
|
||||
|
|
@ -12,6 +13,9 @@ export default function MaintenanceAlert({
|
|||
style?: React.CSSProperties
|
||||
upcoming?: boolean
|
||||
}) {
|
||||
const theme = useMantineTheme()
|
||||
const isDesktop = useMediaQuery(`(min-width: ${theme.breakpoints.sm})`)
|
||||
|
||||
return (
|
||||
<Alert
|
||||
icon={<IconAlertTriangle />}
|
||||
|
|
@ -20,7 +24,7 @@ export default function MaintenanceAlert({
|
|||
? `Upcoming Maintenance: ${maintenance.title || 'Scheduled Maintenance'}`
|
||||
: maintenance.title || 'Scheduled Maintenance'
|
||||
}
|
||||
color={upcoming ? (pageConfig.maintenances?.upcomingColor ?? 'gray') : maintenance.color || '#f29030'}
|
||||
color={upcoming ? (pageConfig.maintenances?.upcomingColor ?? "gray") : maintenance.color || '#f29030'}
|
||||
withCloseButton={false}
|
||||
style={{ margin: '16px auto', ...style }}
|
||||
>
|
||||
|
|
@ -34,7 +38,7 @@ export default function MaintenanceAlert({
|
|||
[`@media (max-width: 600px)`]: { gap: 8 },
|
||||
}}
|
||||
>
|
||||
{/* Maintenance description and affected components */}
|
||||
{/* Maintenance description and affected components */}
|
||||
<Box style={{ flex: '1 1 300px', minWidth: 200 }}>
|
||||
<Text style={{ whiteSpace: 'pre-line' }}>{maintenance.body}</Text>
|
||||
|
||||
|
|
@ -55,18 +59,18 @@ export default function MaintenanceAlert({
|
|||
style={{
|
||||
flex: '0 0 auto',
|
||||
fontSize: '0.85rem',
|
||||
textAlign: 'right',
|
||||
textAlign: isDesktop ? 'right' : 'left',
|
||||
minWidth: 120,
|
||||
}}
|
||||
>
|
||||
<b>{upcoming ? 'Scheduled for' : 'From'}:</b> {new Date(maintenance.start).toLocaleString()}
|
||||
<br />
|
||||
<b>{upcoming ? 'Expected end' : 'To'}:</b>{' '}
|
||||
{maintenance.end
|
||||
? new Date(maintenance.end).toLocaleString()
|
||||
: 'Until further notice'}
|
||||
<b>{upcoming ? 'Scheduled for' : 'From'}:</b>{' '}{new Date(maintenance.start).toLocaleString()}
|
||||
<br />
|
||||
<b>{upcoming ? 'Expected end' : 'To'}:</b>{' '}
|
||||
{maintenance.end
|
||||
? new Date(maintenance.end).toLocaleString()
|
||||
: 'Until further notice'}
|
||||
</Box>
|
||||
</Group>
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -42,7 +42,9 @@ export default function MonitorList({
|
|||
|
||||
// Load expanded groups from localStorage
|
||||
const savedExpandedGroups = localStorage.getItem('expandedGroups')
|
||||
const expandedInitial = savedExpandedGroups ? JSON.parse(savedExpandedGroups) : Object.keys(group || {})
|
||||
const expandedInitial = savedExpandedGroups
|
||||
? JSON.parse(savedExpandedGroups)
|
||||
: Object.keys(group || {})
|
||||
const [expandedGroups, setExpandedGroups] = useState<string[]>(expandedInitial)
|
||||
useEffect(() => {
|
||||
localStorage.setItem('expandedGroups', JSON.stringify(expandedGroups))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
import { Alert, Text } from '@mantine/core'
|
||||
import { IconInfoCircle } from '@tabler/icons-react'
|
||||
|
||||
export default function NoIncidentsAlert({ style }: { style?: React.CSSProperties }) {
|
||||
return (
|
||||
<Alert
|
||||
icon={<IconInfoCircle />}
|
||||
title={
|
||||
<span
|
||||
style={{
|
||||
fontSize: '1rem',
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{'No incidents in this month'}
|
||||
</span>
|
||||
}
|
||||
color="gray"
|
||||
withCloseButton={false}
|
||||
style={{
|
||||
position: 'relative',
|
||||
margin: '16px auto 0 auto',
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<Text>There are no incidents for this month.</Text>
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
|
@ -58,6 +58,7 @@ export default function OverallStatus({
|
|||
})
|
||||
|
||||
const now = new Date()
|
||||
|
||||
const activeMaintenances: (Omit<MaintenanceConfig, 'monitors'> & { monitors?: MonitorTarget[] })[] = maintenances
|
||||
.filter((m) => now >= new Date(m.start) && (!m.end || now <= new Date(m.end)))
|
||||
.map((maintenance) => ({
|
||||
|
|
|
|||
|
|
@ -0,0 +1,131 @@
|
|||
import Head from 'next/head'
|
||||
|
||||
import { Inter } from 'next/font/google'
|
||||
import { MaintenanceConfig, MonitorTarget } from '@/types/config'
|
||||
import { maintenances, pageConfig, workerConfig } from '@/uptime.config'
|
||||
import Header from '@/components/Header'
|
||||
import { Box, Button, Center, Container, Group, Select } from '@mantine/core'
|
||||
import Footer from '@/components/Footer'
|
||||
import { useEffect, useState } from 'react'
|
||||
import MaintenanceAlert from '@/components/MaintenanceAlert'
|
||||
import NoIncidentsAlert from '@/components/NoIncidents'
|
||||
|
||||
export const runtime = 'experimental-edge'
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
|
||||
function getSelectedMonth() {
|
||||
const hash = window.location.hash.replace('#', '')
|
||||
if (!hash) {
|
||||
const now = new Date()
|
||||
return now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0')
|
||||
}
|
||||
return hash.split('-').splice(0, 2).join('-')
|
||||
}
|
||||
|
||||
function filterIncidentsByMonth(
|
||||
incidents: MaintenanceConfig[],
|
||||
monthStr: string
|
||||
): (Omit<MaintenanceConfig, 'monitors'> & { monitors: MonitorTarget[] })[] {
|
||||
return incidents
|
||||
.filter((incident) => {
|
||||
const d = new Date(incident.start)
|
||||
const incidentMonth = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0')
|
||||
return incidentMonth === monthStr
|
||||
})
|
||||
.map((e) => ({
|
||||
...e,
|
||||
monitors: (e.monitors || []).map((e) => workerConfig.monitors.find((mon) => mon.id === e)!),
|
||||
}))
|
||||
.sort((a, b) => (new Date(a.start) > new Date(b.start) ? -1 : 1))
|
||||
}
|
||||
|
||||
function getPrevNextMonth(monthStr: string) {
|
||||
const [year, month] = monthStr.split('-').map(Number)
|
||||
const date = new Date(year, month - 1)
|
||||
const prev = new Date(date)
|
||||
prev.setMonth(prev.getMonth() - 1)
|
||||
const next = new Date(date)
|
||||
next.setMonth(next.getMonth() + 1)
|
||||
return {
|
||||
prev: prev.getFullYear() + '-' + String(prev.getMonth() + 1).padStart(2, '0'),
|
||||
next: next.getFullYear() + '-' + String(next.getMonth() + 1).padStart(2, '0'),
|
||||
}
|
||||
}
|
||||
|
||||
export default function IncidentsPage() {
|
||||
const [selectedMonitor, setSelectedMonitor] = useState<string | null>('')
|
||||
const [selectedMonth, setSelectedMonth] = useState(getSelectedMonth())
|
||||
|
||||
useEffect(() => {
|
||||
const onHashChange = () => setSelectedMonth(getSelectedMonth())
|
||||
window.addEventListener('hashchange', onHashChange)
|
||||
return () => window.removeEventListener('hashchange', onHashChange)
|
||||
}, [])
|
||||
|
||||
const filteredIncidents = filterIncidentsByMonth(maintenances, selectedMonth)
|
||||
const monitorFilteredIncidents = selectedMonitor
|
||||
? filteredIncidents.filter((i) => i.monitors.find((e) => e.id === selectedMonitor))
|
||||
: filteredIncidents
|
||||
|
||||
const { prev, next } = getPrevNextMonth(selectedMonth)
|
||||
|
||||
const monitorOptions = [
|
||||
{ value: '', label: 'All' },
|
||||
...workerConfig.monitors.map((monitor) => ({
|
||||
value: monitor.id,
|
||||
label: monitor.name,
|
||||
})),
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{pageConfig.title}</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
<main className={inter.className}>
|
||||
<Header
|
||||
style={{
|
||||
marginBottom: '40px',
|
||||
}}
|
||||
/>
|
||||
<Center>
|
||||
<Container size="md" style={{ width: '100%' }}>
|
||||
<Group justify="end" mb="md">
|
||||
<Select
|
||||
placeholder="Select monitor"
|
||||
data={monitorOptions}
|
||||
value={selectedMonitor}
|
||||
onChange={setSelectedMonitor}
|
||||
clearable
|
||||
style={{ maxWidth: 300, float: 'right' }}
|
||||
/>
|
||||
</Group>
|
||||
<Box>
|
||||
{monitorFilteredIncidents.length === 0 ? (
|
||||
<NoIncidentsAlert />
|
||||
) : (
|
||||
monitorFilteredIncidents.map((incident, i) => (
|
||||
<MaintenanceAlert key={i} maintenance={incident} />
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
<Group justify="space-between" mt="md">
|
||||
<Button variant="default" onClick={() => (window.location.hash = prev)}>
|
||||
← Backwards
|
||||
</Button>
|
||||
<Box style={{ alignSelf: 'center', fontWeight: 500, fontSize: 18 }}>
|
||||
{selectedMonth}
|
||||
</Box>
|
||||
<Button variant="default" onClick={() => (window.location.hash = next)}>
|
||||
Forward →
|
||||
</Button>
|
||||
</Group>
|
||||
</Container>
|
||||
</Center>
|
||||
<Footer />
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -7,8 +7,9 @@ import { maintenances, pageConfig, workerConfig } from '@/uptime.config'
|
|||
import OverallStatus from '@/components/OverallStatus'
|
||||
import Header from '@/components/Header'
|
||||
import MonitorList from '@/components/MonitorList'
|
||||
import { Center, Divider, Text } from '@mantine/core'
|
||||
import { Center, Text } from '@mantine/core'
|
||||
import MonitorDetail from '@/components/MonitorDetail'
|
||||
import Footer from '@/components/Footer'
|
||||
|
||||
export const runtime = 'experimental-edge'
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
|
|
@ -65,29 +66,7 @@ export default function Home({
|
|||
</div>
|
||||
)}
|
||||
|
||||
<Divider mt="lg" />
|
||||
<Text
|
||||
size="xs"
|
||||
mt="xs"
|
||||
mb="xs"
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
Open-source monitoring and status page powered by{' '}
|
||||
<a href="https://github.com/lyc8503/UptimeFlare" target="_blank">
|
||||
Uptimeflare
|
||||
</a>{' '}
|
||||
and{' '}
|
||||
<a href="https://www.cloudflare.com/" target="_blank">
|
||||
Cloudflare
|
||||
</a>
|
||||
, made with ❤ by{' '}
|
||||
<a href="https://github.com/lyc8503" target="_blank">
|
||||
lyc8503
|
||||
</a>
|
||||
.
|
||||
</Text>
|
||||
<Footer />
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
.header {
|
||||
height: rem(56px);
|
||||
margin-bottom: rem(120px);
|
||||
margin-bottom: rem(100px);
|
||||
background-color: var(--mantine-color-body);
|
||||
border-bottom: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ const Worker = {
|
|||
new Date(timeNow * 1000) >= new Date(m.start) &&
|
||||
(!m.end || new Date(timeNow * 1000) <= new Date(m.end))
|
||||
)
|
||||
.map((e) => (e.monitors || []))
|
||||
.map((e) => e.monitors || [])
|
||||
.flat()
|
||||
|
||||
if (maintenanceList.includes(monitor.id)) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue