Skip to content

Commit

Permalink
task/WG-236: Websocket notifications (#228)
Browse files Browse the repository at this point in the history
* task/WG-236: Websocket notifications

* adding nginx conf file

* adding toast

* refactoring Create Map button to be inside ProjectListings. Adding trigger buttons for notification examples

* updating unit test for main menu

* Adding auth and socket event for new notification

* removing create map button changes. These changes are in WG-276 instead

* Add To-do for wss in socketUtils

---------

Co-authored-by: Taylor Grafft <[email protected]>
Co-authored-by: Taylor Grafft <[email protected]>
Co-authored-by: Taylor Grafft <[email protected]>
Co-authored-by: Taylor Grafft <[email protected]>
  • Loading branch information
5 people authored Apr 30, 2024
1 parent 3d5ea2b commit 6d80b87
Show file tree
Hide file tree
Showing 6 changed files with 254 additions and 3 deletions.
101 changes: 101 additions & 0 deletions react/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@
"react-resize-detector": "^9.1.1",
"react-router-dom": "^6.3.0",
"react-table": "^7.8.0",
"react-toastify": "^10.0.5",
"reactstrap": "^9.2.1",
"socket.io-client": "^4.7.5",
"uuid": "^9.0.1",
"yup": "^1.3.3"
},
Expand Down
42 changes: 40 additions & 2 deletions react/src/components/AssetsPanel/AssetsPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import styles from './AssetsPanel.module.css';
import {
socket,
setupSocketListeners,
removeSocketListeners,
} from '../../utils/socketUtils';
import { ToastContainer } from 'react-toastify';
import { Button } from '../../core-components';

interface Props {
/**
Expand All @@ -12,8 +19,39 @@ interface Props {
* A component that displays a map project (a map and related data)
*/
const AssetsPanel: React.FC<Props> = ({ isPublic }) => {
const [connectionStatus, setConnectionStatus] = useState('Disconnected');

const triggerSuccess = () => {
socket.emit('trigger_asset_success', {
message: 'Hello from the client!',
});
};

const triggerFailure = () => {
socket.emit('trigger_asset_failure', {
message: 'Hello from the client!',
});
};

useEffect(() => {
setupSocketListeners(setConnectionStatus);
return () => {
removeSocketListeners();
};
}, []);

return (
<div className={styles.root}>Assets Panel TODO, isPublic: {isPublic}</div>
<>
<Button size="small" onClick={triggerSuccess}>
Asset Success
</Button>
<Button size="small" onClick={triggerFailure}>
Asset Failure
</Button>
<ToastContainer />
<div className={styles.root}>Assets Panel TODO, isPublic: {isPublic}</div>
Connection Status: {connectionStatus}
</>
);
};

Expand Down
19 changes: 19 additions & 0 deletions react/src/pages/MainMenu/MainMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ import { Provider } from 'react-redux';
import store from '../../redux/store';
import { BrowserRouter as Router } from 'react-router-dom';

jest.mock('socket.io-client', () => {
return {
__esModule: true,
default: () => {
return {
on: jest.fn(),
emit: jest.fn(),
off: jest.fn(),
};
},
};
});

jest.mock('react-toastify', () => ({
toast: {
info: jest.fn(),
},
}));

test('renders menu', () => {
const { getByText } = render(
<Provider store={store}>
Expand Down
34 changes: 33 additions & 1 deletion react/src/pages/MainMenu/MainMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import {
LoadingSpinner,
InlineMessage,
Expand All @@ -10,6 +12,11 @@ import useAuthenticatedUser from '../../hooks/user/useAuthenticatedUser';
import { SystemSelect } from '../../components/Systems';
import CreateMapModal from '../../components/CreateMapModal/CreateMapModal';
import { ProjectListing } from '../../components/Projects/ProjectListing';
import {
setupSocketListeners,
removeSocketListeners,
socket,
} from '../../utils/socketUtils';

function MainMenu() {
const {
Expand All @@ -18,13 +25,29 @@ function MainMenu() {
error: userError,
} = useAuthenticatedUser();
const [isModalOpen, setIsModalOpen] = useState(false);
const [connectionStatus, setConnectionStatus] = useState('Disconnected');

const toggleModal = () => {
setIsModalOpen(!isModalOpen);
};

const triggerNotification = () => {
socket.emit('trigger_notification', {
message: 'Hello from the client!',
});
};

const [selectedSystem, setSelectedSystem] = useState('');

useEffect(() => {
setupSocketListeners(setConnectionStatus);

// Clean up the event listeners when the component unmounts
return () => {
removeSocketListeners();
};
}, [setConnectionStatus]);

if (isUserLoading) {
return (
<>
Expand All @@ -47,6 +70,9 @@ function MainMenu() {
return (
<>
<SectionHeader isNestedHeader>Main Menu</SectionHeader>
<InlineMessage type="info">
WebSocket Status: {connectionStatus}
</InlineMessage>
<div>
<Button type="primary" size="small" onClick={toggleModal}>
Create Map
Expand All @@ -55,8 +81,14 @@ function MainMenu() {
<InlineMessage type="info">
Welcome, {userData?.username || 'User'} <Icon name="user"></Icon>
</InlineMessage>
<div>
<Button type="primary" onClick={triggerNotification}>
Trigger Notification
</Button>
</div>
<CreateMapModal isOpen={isModalOpen} toggle={toggleModal} />
<ProjectListing />
<ToastContainer />
{selectedSystem && <div>Current system selected: {selectedSystem}</div>}
<SystemSelect onSystemSelect={handleSelectChange}></SystemSelect>
</>
Expand Down
59 changes: 59 additions & 0 deletions react/src/utils/socketUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import io from 'socket.io-client';
import { toast } from 'react-toastify';
import { getTokenFromLocalStorage } from './authUtils';

const { token } = getTokenFromLocalStorage();

// TODO: REACT Point to active backend (where nginx is running) use wss:// for secure connection
export const socket = io('http://localhost:8888', {
auth: {
token,
},
});

export const setupSocketListeners = (updateConnectionStatus) => {
socket.on('connect', () => {
updateConnectionStatus('Connected');
console.log('Connected');
});

socket.on('disconnect', () => {
updateConnectionStatus('Disconnected');
console.log('Disconnected');
});

socket.on('connect_error', (error) => {
console.error('Connection Error:', error);
});

socket.on('notification', (data) => {
console.log('Notification received:', data.message);
toast.info(data.message);
});

socket.on('new_notification', (data) => {
console.log('New Notification received:', data.message);
toast.info(data.message);
});

socket.on('asset_success', (data) => {
console.log('Notification received:', data.message);
toast.success(data.message);
});

socket.on('asset_failure', (data) => {
console.log('Notification received:', data.message);
toast.error(data.message);
});
};

// Clean up function to remove all listeners
export const removeSocketListeners = () => {
socket.off('connect');
socket.off('disconnect');
socket.off('connect_error');
socket.off('notification');
socket.off('new_notification');
socket.off('asset_success');
socket.off('asset_failure');
};

0 comments on commit 6d80b87

Please sign in to comment.