Skip to content

Commit

Permalink
add list app monitors
Browse files Browse the repository at this point in the history
  • Loading branch information
BenoitSerrano committed Dec 13, 2024
1 parent 6991c05 commit 623a8d0
Show file tree
Hide file tree
Showing 13 changed files with 216 additions and 40 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"nodemon": "^3.1.4",
"ts-node": "^10.9.1",
"ts-jest": "^29.0.3",
"ts-node": "^10.9.1",
"typescript": "^4.6.4"
},
"dependencies": {
Expand Down
15 changes: 14 additions & 1 deletion src/client/src/lib/api/monitorsApi.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { performApiCall } from './utils';

const monitorsApi = { getMyMonitors, getMyMonitorSummary, createMonitor };
const monitorsApi = {
getMyMonitors,
getMyMonitorSummary,
fetchMonitorsFromUptimeRobot,
createMonitor,
};

async function createMonitor(params: { displayName: string; frequency: number; url: string }) {
const URI = `me/monitors`;
Expand All @@ -14,6 +19,13 @@ async function getMyMonitors() {
return performApiCall<monitorType[]>(URI, 'GET');
}

type appMonitorDtoType = { displayName: string; frequency: number; url: string };

async function fetchMonitorsFromUptimeRobot(uptimeRobotApiKey: string) {
const URI = `uptime-robot/monitors`;
return performApiCall<appMonitorDtoType[]>(URI, 'POST', { uptimeRobotApiKey });
}

type statusValueType = 'up' | 'down';

type eventType = { createdAt: string; kind: statusValueType; title: string; id: number };
Expand All @@ -30,3 +42,4 @@ async function getMyMonitorSummary(monitorId: string) {
}

export { monitorsApi };
export type { appMonitorDtoType };
28 changes: 12 additions & 16 deletions src/client/src/lib/api/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,19 @@ async function performApiCall<dataT>(
const url = `${BASE_URL}/${uri}`;
let response: Response;
const token = localStorage.jwtTokenHandler.get();

if (method === 'GET') {
response = await fetch(url, { method, headers: { Authorization: `Bearer ${token}` } });
} else {
const headers: Record<string, string> = {
Accept: 'application/json',
'Content-Type': 'application/json',
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
response = await fetch(url, {
method,
headers,
body: JSON.stringify(body),
});
const headers: Record<string, string> = {
Accept: 'application/json',
'Content-Type': 'application/json',
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}

response = await fetch(url, {
method,
headers,
body: JSON.stringify(body),
});
if (!response.ok) {
if (response.status === 401) {
localStorage.jwtTokenHandler.remove();
Expand Down
3 changes: 3 additions & 0 deletions src/client/src/locale/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ const fr = {
importFrom: {
uptimeRobot: {
fetchMonitorsButton: 'Récupérer les monitors',
list: {
frequencyLabel: 'toutes les {{frequency}} minutes',
},
},
},
monitors: {
Expand Down
37 changes: 37 additions & 0 deletions src/client/src/locale/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { variabilize } from './utils';

describe('utils', () => {
describe('variabilize', () => {
it('should return the original text if no variable pass', () => {
const text = 'truc';

const variabilizedText = variabilize(text, {});

expect(variabilizedText).toBe(text);
});

it('should return the variablized text if right variable pass', () => {
const text = 'truc {{count}} machin';

const variabilizedText = variabilize(text, { count: 2 });

expect(variabilizedText).toBe('truc 2 machin');
});

it('should return the variablized text if right variable passed twice', () => {
const text = 'truc {{count}} machin {{count}}';

const variabilizedText = variabilize(text, { count: 2 });

expect(variabilizedText).toBe('truc 2 machin 2');
});

it('should return the variablized text if several variables passed', () => {
const text = 'truc {{count}} machin {{double}}';

const variabilizedText = variabilize(text, { count: 2, double: 4 });

expect(variabilizedText).toBe('truc 2 machin 4');
});
});
});
9 changes: 9 additions & 0 deletions src/client/src/locale/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
function variabilize(text: string, variables: Record<string, string | number>) {
let variabilizedText = text;
for (const [key, value] of Object.entries(variables)) {
const REGEX = new RegExp(`{{${key}}}`, 'g');
variabilizedText = variabilizedText.replace(REGEX, `${value}`);
}
return variabilizedText;
}
export { variabilize };
60 changes: 60 additions & 0 deletions src/client/src/pages/ImportFrom/AppMonitorList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {
Checkbox,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
} from '@mui/material';
import { appMonitorDtoType } from '../../lib/api/monitorsApi';
import { useState } from 'react';
import { variabilize } from '../../locale/utils';
import { locale } from '../../locale';

function AppMonitorList(props: { appMonitorsDtos: appMonitorDtoType[] }) {
const initialSelectedIndexes = props.appMonitorsDtos.map((_, index) => index);
const [selectedIndexes, setSelectedIndexes] = useState<number[]>(initialSelectedIndexes);
return (
<List dense={true}>
{props.appMonitorsDtos.map((appMonitorDto, index) => (
<ListItemButton key={appMonitorDto.displayName} onClick={buildOnItemClick(index)}>
<ListItemIcon>
<Checkbox
edge="start"
checked={selectedIndexes.includes(index)}
disableRipple
/>
</ListItemIcon>
<ListItem>
<ListItemText
primary={computeListItemPrimaryText(appMonitorDto)}
secondary={appMonitorDto.url}
/>
</ListItem>
</ListItemButton>
))}
</List>
);

function buildOnItemClick(index: number) {
return () => {
const indexOfSelectedIndex = selectedIndexes.indexOf(index);
if (indexOfSelectedIndex === -1) {
setSelectedIndexes([...selectedIndexes, index]);
} else {
setSelectedIndexes(
selectedIndexes.filter((selectedIndex) => selectedIndex !== index),
);
}
};
}
}

function computeListItemPrimaryText(appMonitorDto: appMonitorDtoType) {
const frequencyText = variabilize(locale.importFrom.uptimeRobot.list.frequencyLabel, {
frequency: appMonitorDto.frequency,
});
return `${appMonitorDto.displayName} (${frequencyText})`;
}

export { AppMonitorList };
41 changes: 33 additions & 8 deletions src/client/src/pages/ImportFrom/ImportFromUptimeRobot.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,46 @@
import { TextField } from '@mui/material';
import { useState } from 'react';
import { FormEvent, useState } from 'react';
import { Button } from '../../components/Button';
import { locale } from '../../locale';
import { useApiCall } from '../../lib/useApiCall';
import { appMonitorDtoType, monitorsApi } from '../../lib/api/monitorsApi';
import { AppMonitorList } from './AppMonitorList';

function ImportFromUptimeRobot() {
const [apiKey, setApiKey] = useState('');
const [appMonitorsDtos, setAppMonitorDtos] = useState<appMonitorDtoType[] | undefined>();
const fetchMonitorsFromUptimeRobotApiCall = useApiCall({
apiCall: monitorsApi.fetchMonitorsFromUptimeRobot,
onSuccess: (data) => {
setAppMonitorDtos(data);
},
});
return (
<div>
<TextField
label="API key"
placeholder="API key"
value={apiKey}
onChange={(event) => setApiKey(event.target.value)}
/>
<Button>{locale.importFrom.uptimeRobot.fetchMonitorsButton}</Button>
<form onSubmit={fetchMonitors}>
<TextField
label="API key"
placeholder="API key"
value={apiKey}
onChange={(event) => setApiKey(event.target.value)}
/>
<Button
type="submit"
disabled={!apiKey}
isLoading={fetchMonitorsFromUptimeRobotApiCall.isLoading}
>
{locale.importFrom.uptimeRobot.fetchMonitorsButton}
</Button>
</form>
{appMonitorsDtos !== undefined && <AppMonitorList appMonitorsDtos={appMonitorsDtos} />}
</div>
);

function fetchMonitors(event: FormEvent<HTMLFormElement>) {
event.preventDefault();

fetchMonitorsFromUptimeRobotApiCall.perform(apiKey);
}
}

export { ImportFromUptimeRobot };
30 changes: 30 additions & 0 deletions src/lib/externalApis/uptimeRobotApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
type uptimeRobotMonitorApiType = {
id: number;
friendly_name: string;
url: string;
interval: number;
};

function buildUptimeRobotApi(uptimeRobotApiKey: string) {
const BASE_URL = `https://api.uptimerobot.com/v2`;

return { fetchMonitors };

async function fetchMonitors() {
const URL = `${BASE_URL}/getMonitors?format=json&api_key=${uptimeRobotApiKey}`;
try {
const response = await fetch(URL, { method: 'POST' });
const data = (await response.json()) as { monitors: uptimeRobotMonitorApiType[] };

return data.monitors.map((monitor) => ({
url: monitor.url,
frequency: monitor.interval / 60,
displayName: monitor.friendly_name,
}));
} catch (error) {
throw new Error(`Le serveur uptimeRobot a renvoyé l'erreur suivante : ${error}`);
}
}
}

export { buildUptimeRobotApi };
5 changes: 5 additions & 0 deletions src/modules/monitor/monitor.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ function buildMonitorController() {
getMyMonitorSummary,
pingCronMonitor,
createMonitor,
fetchMonitorsFromUptimeRobot,
};

return monitorController;
Expand All @@ -20,6 +21,10 @@ function buildMonitorController() {
return monitorService.assertIsMonitorUpByName(params.urlParams.monitorName);
}

async function fetchMonitorsFromUptimeRobot(params: { body: { uptimeRobotApiKey: string } }) {
return monitorService.fetchMonitorsFromUptimeRobot(params.body.uptimeRobotApiKey);
}

async function getMyMonitorSummary(params: { urlParams: { monitorId: string } }) {
return monitorService.getMyMonitorSummary(params.urlParams.monitorId);
}
Expand Down
18 changes: 5 additions & 13 deletions src/modules/monitor/monitor.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { dataSource } from '../../dataSource';
import { buildUptimeRobotApi } from '../../lib/externalApis/uptimeRobotApi';
import { slugify } from '../../lib/utils';
import { buildMonitorEventService } from '../monitorEvent';
import { eventKindType } from '../monitorEvent/types';
Expand All @@ -20,6 +21,7 @@ function buildMonitorService() {
pingAppMonitor,
checkAllCronMonitors,
pingCronMonitor,
fetchMonitorsFromUptimeRobot,
};

return monitorService;
Expand Down Expand Up @@ -224,19 +226,9 @@ function buildMonitorService() {
}
}

async function getMyCronMonitorSummary(monitorId: Monitor['id']) {
const monitorEventService = buildMonitorEventService();
async function fetchMonitorsFromUptimeRobot(uptimeRobotApiKey: string) {
const uptimeRobotApi = buildUptimeRobotApi(uptimeRobotApiKey);

const cronMonitor = (await monitorRepository.findOneByOrFail({
id: monitorId,
kind: 'cron',
})) as cronMonitorType;
const monitorEvents = await monitorEventService.getEventsForMonitor(monitorId);
const status = await getCronMonitorStatus(cronMonitor);
return {
name: cronMonitor.name,
status,
events: monitorEvents,
};
return uptimeRobotApi.fetchMonitors();
}
}
2 changes: 1 addition & 1 deletion src/modules/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ function buildUserService() {
const user = await userRepository.findOneOrFail({ where: { email } });

const isPasswordCorrect = hasher.verify(password, user.hashedPassword);
console.log(hasher.hash(password));

if (isPasswordCorrect) {
const token = createJwt({ userId: user.id, email: user.email });
const userInfo = { email };
Expand Down
6 changes: 6 additions & 0 deletions src/router/monitorRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ const monitorRoutes: Array<routeType<any, any, any>> = [
path: '/monitors/:monitorName/health',
controller: monitorController.assertIsMonitorUpByName,
},
{
method: 'POST',
kind: 'authenticated',
path: '/uptime-robot/monitors',
controller: monitorController.fetchMonitorsFromUptimeRobot,
},
];

export { monitorRoutes };

0 comments on commit 623a8d0

Please sign in to comment.