Skip to content

Commit

Permalink
Implement and show upptime status in footer
Browse files Browse the repository at this point in the history
Fetching data from the readme of the repo hehe - couldn't find a better api
  • Loading branch information
ivarnakken committed Dec 25, 2024
1 parent 300d8a7 commit 2659bc4
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 3 deletions.
50 changes: 50 additions & 0 deletions app/actions/StatusActions.ts
Original file line number Diff line number Diff line change
@@ -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<SystemStatus> => {
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 };
},
);
43 changes: 41 additions & 2 deletions app/components/Footer/Footer.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand All @@ -43,7 +82,7 @@
html[data-theme='dark'] .footerContent a {
color: var(--color-red-2);

&:hover {
&:hover:not(.statusLink) {
color: var(--color-red-3);
}
}
Expand Down
53 changes: 52 additions & 1 deletion app/components/Footer/index.tsx
Original file line number Diff line number Diff line change
@@ -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 netcompany from 'app/assets/netcompany_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 (
<footer className={styles.footer}>
<div className={styles.footerContent}>
Expand Down Expand Up @@ -54,6 +79,32 @@ const Footer = () => {
Backend
</a>
</Flex>
{systemStatus?.status && systemStatus?.message && (
<LinkButton
flat
size="small"
href="https://status.abakus.no"
rel="noopener noreferrer"
target="_blank"
className={styles.statusLink}
>
<span className={styles.statusDot}>
<span
className={styles.statusDotPing}
style={{
backgroundColor: getStatusColor(systemStatus.status),
}}
/>
<span
className={styles.statusDotCore}
style={{
backgroundColor: getStatusColor(systemStatus.status),
}}
/>
</span>
{systemStatus.message}
</LinkButton>
)}
</div>

<div className={cx(styles.section, styles.rightSection)}>
Expand Down
32 changes: 32 additions & 0 deletions app/reducers/status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { createSlice } from '@reduxjs/toolkit';
import { fetchSystemStatus } from 'app/actions/StatusActions';
import { EntityType } from 'app/store/models/entities';
import createLegoAdapter from 'app/utils/legoAdapter/createLegoAdapter';
import type { SystemStatus } from 'app/actions/StatusActions';
import type { RootState } from 'app/store/createRootReducer';

type ExtraStatusState = {
systemStatus: SystemStatus | null;
};

const legoAdapter = createLegoAdapter(EntityType.SystemStatus);

const statusSlice = createSlice({
name: EntityType.SystemStatus,
initialState: legoAdapter.getInitialState({
systemStatus: null,
} as ExtraStatusState),
reducers: {},
extraReducers: legoAdapter.buildReducers({
extraCases: (addCase) => {
addCase(fetchSystemStatus.fulfilled, (state, action) => {
state.systemStatus = action.payload;
});
},
}),
});

export default statusSlice.reducer;

export const selectSystemStatus = (state: RootState) =>
state.status.systemStatus;
2 changes: 2 additions & 0 deletions app/store/createRootReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import registrations from 'app/reducers/registrations';
import restrictedMails from 'app/reducers/restrictedMails';
import routing from 'app/reducers/routing';
import search from 'app/reducers/search';
import status from 'app/reducers/status';
import surveySubmissions from 'app/reducers/surveySubmissions';
import surveys from 'app/reducers/surveys';
import tags from 'app/reducers/tags';
Expand Down Expand Up @@ -94,6 +95,7 @@ const createRootReducer = () => {
threads,
toasts,
users,
status,
});
};

Expand Down
3 changes: 3 additions & 0 deletions app/store/models/entities.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type OAuth2Grant from './OAuth2Grant';
import type { EntityId } from '@reduxjs/toolkit';
import type { SystemStatus } from 'app/actions/StatusActions';
import type { UnknownAnnouncement } from 'app/store/models/Announcement';
import type { UnknownArticle } from 'app/store/models/Article';
import type Comment from 'app/store/models/Comment';
Expand Down Expand Up @@ -74,6 +75,7 @@ export enum EntityType {
Tags = 'tags',
Thread = 'threads',
Users = 'users',
SystemStatus = 'systemStatus',
}

// Most fetch success redux actions are normalized such that payload.entities is a subset of this interface.
Expand Down Expand Up @@ -115,6 +117,7 @@ export default interface Entities {
[EntityType.Tags]: Record<EntityId, UnknownTag>;
[EntityType.Thread]: Record<EntityId, UnknownThread>;
[EntityType.Users]: Record<EntityId, UnknownUser>;
[EntityType.SystemStatus]: Record<EntityId, SystemStatus>;
}

type InferEntityType<T> = {
Expand Down

0 comments on commit 2659bc4

Please sign in to comment.