diff --git a/app/actions/StatusActions.ts b/app/actions/StatusActions.ts new file mode 100644 index 0000000000..7debfa0c07 --- /dev/null +++ b/app/actions/StatusActions.ts @@ -0,0 +1,50 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; + +export type SystemStatus = { + status: 'operational' | 'degraded' | 'major'; + message: string; +}; + +type UptimeService = { + name: string; + status: 'up' | 'down' | 'degraded'; + uptime: string; +}; + +export const fetchSystemStatus = createAsyncThunk( + 'status/fetch', + async (): Promise => { + const response = await fetch( + 'https://raw.githubusercontent.com/webkom/uptime/master/history/summary.json', + ); + + if (!response.ok) { + throw new Error('Failed to fetch system status'); + } + + const data = (await response.json()) as UptimeService[]; + + const servicesDown = data.filter( + (service) => service.status === 'down', + ).length; + const servicesDegraded = data.filter( + (service) => service.status === 'degraded', + ).length; + + let status: SystemStatus['status']; + let message: string; + + if (servicesDown > 0) { + status = 'major'; + message = `${servicesDown} ${servicesDown === 1 ? 'tjeneste er' : 'tjenester er'} nede`; + } else if (servicesDegraded > 0) { + status = 'degraded'; + message = `${servicesDegraded} ${servicesDegraded === 1 ? 'tjeneste har' : 'tjenester har'} redusert ytelse`; + } else { + status = 'operational'; + message = `Alle tjenester opererer normalt`; + } + + return { status, message }; + }, +); diff --git a/app/components/Footer/Footer.module.css b/app/components/Footer/Footer.module.css index 29bdd1aa4c..977c300fce 100644 --- a/app/components/Footer/Footer.module.css +++ b/app/components/Footer/Footer.module.css @@ -26,12 +26,51 @@ } } +.statusDot { + position: relative; + display: inline-flex; + height: var(--spacing-sm); + width: var(--spacing-sm); +} + +.statusDotCore { + position: relative; + display: inline-flex; + height: 100%; + width: 100%; + border-radius: 50%; +} + +.statusDotPing { + position: absolute; + display: inline-flex; + height: 100%; + width: 100%; + border-radius: 50%; + opacity: 0.6; + animation: ping 1.5s cubic-bezier(0, 0, 0.2, 1) infinite; +} + +@keyframes ping { + 75%, + 100% { + transform: scale(2); + opacity: 0; + } +} + +.statusLink { + margin-top: var(--spacing-sm); + margin-left: calc(-1 * var(--spacing-sm)); + font-weight: 400; +} + /* stylelint-disable no-descending-specificity */ .footerContent a { color: var(--color-red-8); margin-bottom: var(--spacing-sm); - &:hover { + &:hover:not(.statusLink) { color: var(--color-red-7); } } @@ -43,7 +82,7 @@ html[data-theme='dark'] .footerContent a { color: var(--color-red-2); - &:hover { + &:hover:not(.statusLink) { color: var(--color-red-3); } } diff --git a/app/components/Footer/index.tsx b/app/components/Footer/index.tsx index 88e50cb0e7..73685f37b0 100644 --- a/app/components/Footer/index.tsx +++ b/app/components/Footer/index.tsx @@ -1,17 +1,42 @@ -import { Flex, Icon, Image } from '@webkom/lego-bricks'; +import { Flex, Icon, Image, LinkButton } from '@webkom/lego-bricks'; +import { usePreparedEffect } from '@webkom/react-prepare'; import cx from 'classnames'; import { Facebook, Instagram, Linkedin, Slack } from 'lucide-react'; import moment from 'moment-timezone'; import { Link } from 'react-router-dom'; +import { fetchSystemStatus } from 'app/actions/StatusActions'; import bekk from 'app/assets/bekk_short_white.svg'; import octocat from 'app/assets/octocat.png'; import { useIsLoggedIn } from 'app/reducers/auth'; +import { useAppDispatch, useAppSelector } from 'app/store/hooks'; import utilityStyles from 'app/styles/utilities.css'; import Circle from '../Circle'; import styles from './Footer.module.css'; const Footer = () => { + const dispatch = useAppDispatch(); + const systemStatus = useAppSelector((state) => state.status.systemStatus); const loggedIn = useIsLoggedIn(); + + usePreparedEffect( + 'fetchSystemStatus', + () => dispatch(fetchSystemStatus()), + [], + ); + + const getStatusColor = (status?: string) => { + switch (status) { + case 'operational': + return 'var(--success-color)'; + case 'degraded': + return 'var(--color-orange-6)'; + case 'major': + return 'var(--danger-color)'; + default: + return 'var(--color-gray-6)'; + } + }; + return (