feat: incident history (#113)
* feat: add incident history link to links in header * fix: lower margin-bottom on header to have not that much wasted space * chore: move footer to own component * feat: add NoIncidents component * feat: add incidents history page * fix: add sorting by date * fix: fix sorting * feat: hash linking to individual incidents * fix: always truthy title * feat: remove hash * feat: some link improvements * feat: improve styles for alert, support mobile devices * fix: remove unused vars, larger margin --------- Co-authored-by: lyc8503 <me@lyc8503.net>pull/142/head
parent
5769a52f0c
commit
41257c6ae9
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -4,12 +4,12 @@ import { pageConfig } from '@/uptime.config'
|
||||||
import { PageConfigLink } from '@/types/config'
|
import { PageConfigLink } from '@/types/config'
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const linkToElement = (link: PageConfigLink) => {
|
const linkToElement = (link: PageConfigLink, i: number) => {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
key={link.label}
|
key={i}
|
||||||
href={link.link}
|
href={link.link}
|
||||||
target="_blank"
|
target={link.link.startsWith('/') ? undefined : '_blank'}
|
||||||
className={classes.link}
|
className={classes.link}
|
||||||
data-active={link.highlight}
|
data-active={link.highlight}
|
||||||
>
|
>
|
||||||
|
|
@ -18,11 +18,16 @@ export default function Header() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const links = [{ label: 'Incident History', link: '/incidents' }, ...(pageConfig.links || [])]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={classes.header}>
|
<header className={classes.header}>
|
||||||
<Container size="md" className={classes.inner}>
|
<Container size="md" className={classes.inner}>
|
||||||
<div>
|
<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 size="xl" span>
|
||||||
🕒
|
🕒
|
||||||
</Text>
|
</Text>
|
||||||
|
|
@ -39,11 +44,11 @@ export default function Header() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Group gap={5} visibleFrom="sm">
|
<Group gap={5} visibleFrom="sm">
|
||||||
{pageConfig.links?.map(linkToElement)}
|
{links?.map(linkToElement)}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Group gap={5} hiddenFrom="sm">
|
<Group gap={5} hiddenFrom="sm">
|
||||||
{pageConfig.links?.filter((link) => link.highlight).map(linkToElement)}
|
{links?.filter((link) => (link.highlight || link.link.startsWith('/'))).map(linkToElement)}
|
||||||
</Group>
|
</Group>
|
||||||
</Container>
|
</Container>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Alert, List, Text } from '@mantine/core'
|
import { Alert, List, Text, useMantineTheme } from '@mantine/core'
|
||||||
|
import { useMediaQuery } from '@mantine/hooks'
|
||||||
import { IconAlertTriangle } from '@tabler/icons-react'
|
import { IconAlertTriangle } from '@tabler/icons-react'
|
||||||
import { MaintenanceConfig, MonitorTarget } from '@/types/config'
|
import { MaintenanceConfig, MonitorTarget } from '@/types/config'
|
||||||
|
|
||||||
|
|
@ -9,24 +10,42 @@ export default function MaintenanceAlert({
|
||||||
maintenance: Omit<MaintenanceConfig, 'monitors'> & { monitors?: MonitorTarget[] }
|
maintenance: Omit<MaintenanceConfig, 'monitors'> & { monitors?: MonitorTarget[] }
|
||||||
style?: React.CSSProperties
|
style?: React.CSSProperties
|
||||||
}) {
|
}) {
|
||||||
|
const theme = useMantineTheme()
|
||||||
|
const isDesktop = useMediaQuery(`(min-width: ${theme.breakpoints.sm})`)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
icon={<IconAlertTriangle />}
|
icon={<IconAlertTriangle />}
|
||||||
title={maintenance.title || 'Scheduled Maintenance'}
|
title={
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: 700,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{maintenance.title || 'Scheduled Maintenance'}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
color={maintenance.color || 'yellow'}
|
color={maintenance.color || 'yellow'}
|
||||||
withCloseButton={false}
|
withCloseButton={false}
|
||||||
style={{ position: 'relative', margin: '16px auto 0 auto', ...style }}
|
style={{ position: 'relative', margin: '16px auto 0 auto', ...style }}
|
||||||
>
|
>
|
||||||
{/* Date range in top right */}
|
{/* Date range in top right (desktop) or inline (mobile) */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
...{
|
||||||
top: 10,
|
top: 10,
|
||||||
right: 10,
|
fontSize: '0.85rem',
|
||||||
fontSize: '0.85rem',
|
borderRadius: 6,
|
||||||
textAlign: 'right',
|
},
|
||||||
padding: '2px 8px',
|
...(isDesktop
|
||||||
borderRadius: 6,
|
? {
|
||||||
|
position: 'absolute',
|
||||||
|
right: 10,
|
||||||
|
padding: '2px 8px',
|
||||||
|
textAlign: 'right',
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<b>From:</b> {new Date(maintenance.start).toLocaleString()}
|
<b>From:</b> {new Date(maintenance.start).toLocaleString()}
|
||||||
|
|
@ -35,7 +54,7 @@ export default function MaintenanceAlert({
|
||||||
{maintenance.end ? new Date(maintenance.end).toLocaleString() : 'Until further notice'}
|
{maintenance.end ? new Date(maintenance.end).toLocaleString() : 'Until further notice'}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Text>{maintenance.body}</Text>
|
<Text style={{ paddingTop: '3px' }}>{maintenance.body}</Text>
|
||||||
{maintenance.monitors && maintenance.monitors.length > 0 && (
|
{maintenance.monitors && maintenance.monitors.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Text mt="xs">
|
<Text mt="xs">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
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 />
|
||||||
|
<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 OverallStatus from '@/components/OverallStatus'
|
||||||
import Header from '@/components/Header'
|
import Header from '@/components/Header'
|
||||||
import MonitorList from '@/components/MonitorList'
|
import MonitorList from '@/components/MonitorList'
|
||||||
import { Center, Divider, Text } from '@mantine/core'
|
import { Center, Text } from '@mantine/core'
|
||||||
import MonitorDetail from '@/components/MonitorDetail'
|
import MonitorDetail from '@/components/MonitorDetail'
|
||||||
|
import Footer from '@/components/Footer'
|
||||||
|
|
||||||
export const runtime = 'experimental-edge'
|
export const runtime = 'experimental-edge'
|
||||||
const inter = Inter({ subsets: ['latin'] })
|
const inter = Inter({ subsets: ['latin'] })
|
||||||
|
|
@ -65,29 +66,7 @@ export default function Home({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Divider mt="lg" />
|
<Footer />
|
||||||
<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>
|
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
.header {
|
.header {
|
||||||
height: rem(56px);
|
height: rem(56px);
|
||||||
margin-bottom: rem(120px);
|
margin-bottom: rem(100px);
|
||||||
background-color: var(--mantine-color-body);
|
background-color: var(--mantine-color-body);
|
||||||
border-bottom: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
border-bottom: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue