Skip to content

Commit

Permalink
feat: subscribe to notifications websocket
Browse files Browse the repository at this point in the history
and update NotificationIsland
  • Loading branch information
SyedAhkam committed Sep 1, 2023
1 parent 53b4f8e commit f95cf10
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 83 deletions.
121 changes: 55 additions & 66 deletions frontend/src/components/header/NotificationIsland.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { useRouter } from "next/router";

import {
ActionIcon,
Expand All @@ -19,63 +20,77 @@ import { BsRecordCircle, BsThreeDots } from "react-icons/bs";

import { getColorByTag } from "@/utils";
import NotificationListItem from "./NotificationListItem";
import useNotificationsWS from "@/hooks/use-notifications-ws";
import Notification from "@/models/notification";

dayjs.extend(relativeTime);

export default function NotificationIsland() {
const [notifications, setNotifications] = useState<Array<Record<any, any>>>([
{
contract_name: "Alpha",
contract_id: 1,
alert_name: "Gas Threshold Exceed",
type: "gas_threshold_reached",
type_description: "Occurs when the gas threshold is reached",
tag: "success",
body: "Gas threshold of 1000 was reached",
executed_at: dayjs(),
},
{
contract_name: "Beta",
contract_id: 1,
alert_name: "Change in balance",
type: "owner_balance_changed",
type_description: "Occurs when the owner balance changes",
tag: "warn",
body: "Owner balance changed by 1 ETH",
executed_at: dayjs().subtract(1, "hour"),
},
{
contract_name: "Beta",
contract_id: 1,
alert_name: "Too many Failed Transactions",
type: "too_many_failed_transactions",
type_description:
"Occurs when there are too many failed transactions in the set duration",
tag: "error",
body: "Too many failed transactions in the last 24 hours",
executed_at: dayjs().subtract(3, "day"),
},
]);
const router = useRouter();
const organizationId = router.query.orgId as string;

const { notification, ...swr } = useNotificationsWS(organizationId);

const [notifications, setNotifications] = useState<Notification[]>([]);

// Detect a new notification
useEffect(() => {
if (!notification) return;

setNotifications((notifs) => [...notifs, notification]);
}, [notification]);

const [
isListDropdownOpen,
{ close: closeListDropdown, open: openListDropdown },
] = useDisclosure(false);

var latestNotification = notifications.at(0);
var latestNotification = notifications.at(-1);

const NotificationListHover = () => {
return (
<ScrollArea h="50vh">
{notifications
.filter((notif) => notif !== latestNotification)
.map((notif) => {
return <NotificationListItem key={notif.id} notif={notif} />;
.map((notif, idx) => {
return <NotificationListItem key={idx} notif={notif} />;
})}
</ScrollArea>
);
};

const LatestNotification = () => {
return (
<Group
pl="xs"
sx={{
"&:hover": {
opacity: 0.8,
},
}}
>
<BsRecordCircle
size="1.2em"
color={getColorByTag(latestNotification!.tag ?? "success")} // FIXME: not supplied by backend yet
/>

<Badge>{latestNotification!.contract_name}</Badge>
<Text size="sm" weight="bold" color="gray">
triggered alert
</Text>

<Code>{latestNotification!.alert_name}</Code>

<Text size="xs" color="gray">
</Text>
<Text size="xs" color="gray">
{dayjs(latestNotification!.created_at).fromNow()}
</Text>
</Group>
);
};

return (
<Paper
withBorder
Expand All @@ -90,7 +105,7 @@ export default function NotificationIsland() {
: theme.colors.gray[1],
})}
>
{notifications.length === 0 ? (
{!notifications || notifications.length === 0 ? (
<Flex direction="column" align="center" justify="center" h="100%">
<Text size="lg" weight="bold" color="gray">
Notifications will appear here
Expand All @@ -107,36 +122,10 @@ export default function NotificationIsland() {
h="100%"
onMouseLeave={closeListDropdown}
>
<Group
pl="xs"
sx={{
"&:hover": {
opacity: 0.8,
},
}}
>
<BsRecordCircle
size="1.2em"
color={getColorByTag(latestNotification!.tag)}
/>

<Badge>{latestNotification!.contract_name}</Badge>
<Text size="sm" weight="bold" color="gray">
triggered alert
</Text>

<Code>{latestNotification!.alert_name}</Code>

<Text size="xs" color="gray">
</Text>
<Text size="xs" color="gray">
{dayjs(latestNotification!.executed_at).fromNow()}
</Text>
</Group>
<LatestNotification />

<Group pr="sm">
<Indicator processing>
<Indicator size="lg" processing label={notifications.length}>
<ActionIcon onClick={openListDropdown}>
<BsThreeDots
size="1.2em"
Expand Down
15 changes: 5 additions & 10 deletions frontend/src/components/header/NotificationListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import { Box, Code, Flex, Stack, Text } from "@mantine/core";
import dayjs from "dayjs";
import { useRouter } from "next/router";
import { BsRecordCircle } from "react-icons/bs";

import { getColorByTag } from "@/utils";
import Notification from "@/models/notification";

// FIXME: construct a type for this
export default function NotificationListItem({ notif }: Record<string, any>) {
const router = useRouter();

if (!notif) return null;

export default function NotificationListItem({ notif }: { notif: Notification }) {
return (
<Box
p="md"
Expand All @@ -31,7 +26,7 @@ export default function NotificationListItem({ notif }: Record<string, any>) {
>
<Stack>
<Flex direction="row" justify="flex-start" gap="md">
<BsRecordCircle size="1.5em" color={getColorByTag(notif.tag)} />
<BsRecordCircle size="1.5em" color={getColorByTag("success")} /> {/* hardcoded tag right now */}

<Code>{notif.contract_name}</Code>
<Text size="sm" weight="bold" color="gray">
Expand All @@ -41,10 +36,10 @@ export default function NotificationListItem({ notif }: Record<string, any>) {
<Code>{notif.alert_name}</Code>
</Flex>

<Text>{notif.body}</Text>
<Text>{!notif.alert_description ? "No alert description" : notif.alert_description}</Text>

<Text size="sm" color="gray">
{dayjs(notif.executed_at).fromNow()}
{dayjs(notif.created_at).fromNow()}
</Text>
</Stack>
</Box>
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/hooks/use-notifications-ws.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import useSWRSubscription from "swr/subscription";

import { BACKEND_URL } from "@/constants";
import { getAccessToken } from "@/cookies";
import Notification from "@/models/notification";

export default function useNotificationsWS(organizationId: string) {
const { data, ...swr } = useSWRSubscription<Notification, Error, string>(
`ws://${BACKEND_URL}/ws/notifications/${organizationId}?token=${getAccessToken()}`,
(key, { next }) => {
const socket = new WebSocket(key)

socket.addEventListener('message', (event) => next(null, JSON.parse(event.data)))
// @ts-ignore
socket.addEventListener('error', (event) => next(event.error))

return () => socket.close()
})

return {
notification: data,
...swr,
};
}
15 changes: 8 additions & 7 deletions frontend/src/models/notification.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import Alert from "./alert";

enum NotificationType {
export enum NotificationType {
EMAIL = "EMAIL",
SMS = "SMS",
WEBHOOK = "WEBHOOK",
}

enum NotificationStatus {
export enum NotificationStatus {
PENDING = "PENDING",
SENT = "SENT",
FAILED = "FAILED",
}

interface Notification {
export default interface Notification {
id: number;
alert: Alert;
alert_name: string;
alert_description: string;
contract_name: string;
created_at: string;
notification_type: NotificationType;
notification_body: string;
notification_body: Map<string, string>;
notification_target: string;
trigger_transaction_hash: string;
meta_logs: string;
Expand Down

0 comments on commit f95cf10

Please sign in to comment.