Skip to content

Commit

Permalink
Migrate to react-toastify
Browse files Browse the repository at this point in the history
Remove eventEmitter for notifications, use react-toastify

Trying to style with PF alert, which will apply CSS from the theme
selection.

The alert is still getting wrapped in a container that I haven't been
able to style appropriately or remove.

beforeUpload toast is working, but I had issues with the others and I'm
adding them in one by one
  • Loading branch information
mshriver committed Jan 9, 2025
1 parent fa2a9bb commit ed9195d
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 98 deletions.
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"react-router-dom": "^6.28.0",
"react-scripts": "^5.0.1",
"react-simple-oauth2-login": "^0.5.4",
"react-toastify": "^11.0.2",
"serve": "^12.0.1",
"typescript": "^4.9.5",
"wolfy87-eventemitter": "^5.2.9"
Expand Down
105 changes: 67 additions & 38 deletions frontend/src/components/ibutsu-header.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';

import { ToastContainer, toast } from 'react-toastify';
import {
AboutModal,
AlertActionLink,

Check failure on line 6 in frontend/src/components/ibutsu-header.js

View workflow job for this annotation

GitHub Actions / lint-test-frontend

'AlertActionLink' is defined but never used

Check failure on line 6 in frontend/src/components/ibutsu-header.js

View workflow job for this annotation

GitHub Actions / lint-test-frontend

'AlertActionLink' is defined but never used
Expand Down Expand Up @@ -34,12 +34,13 @@ import {
import { BarsIcon, MoonIcon, ServerIcon, TimesIcon, QuestionCircleIcon, UploadIcon } from '@patternfly/react-icons';

import { FileUpload, UserDropdown } from '../components';
import { MONITOR_UPLOAD_TIMEOUT, VERSION } from '../constants';
import { ALERT_TIMEOUT, MONITOR_UPLOAD_TIMEOUT, VERSION } from '../constants';
import { HttpClient } from '../services/http';
import { Settings } from '../settings';
import { getTheme, setTheme } from '../utilities';
import { IbutsuContext } from '../services/context';
import { useNavigate, useParams } from 'react-router-dom';
import ToastWrapper from './toast-wrapper';


function IbutsuHeader (props) {
Expand All @@ -63,7 +64,7 @@ function IbutsuHeader (props) {

// values from hooks
const { project_id } = params;
const { setPrimaryObject, setPrimaryType, setDefaultDashboard } = context;
const { setPrimaryObject, setPrimaryType, setDefaultDashboard, primaryObject } = context;

useEffect(() => {
// update projects/portals when the filter input changes
Expand Down Expand Up @@ -101,16 +102,54 @@ function IbutsuHeader (props) {
}
}, [project_id, selectedProject, setDefaultDashboard, setPrimaryObject, setPrimaryType])

useEffect(() => {
if (importId) {
setMonitorUploadId(setInterval(() => {
HttpClient.get([Settings.serverUrl, 'import', importId])
.then(response => HttpClient.handleResponse(response))
.then(data => {
if (data['status'] === 'done') {
clearInterval(monitorUploadId);
setMonitorUploadId();
// let action = null;
// if (data.metadata.run_id) {
// const RunButton = () => (
// <AlertActionLink onClick={() => {navigate('/project/' + (data.metadata.project_id || primaryObject.id) + '/runs/' + data.metadata.run_id)}}>
// Go to Run
// </AlertActionLink>
// )
// //action = <RunButton />;
// }
// toast(<ToastWrapper />,
// {
// data: {
// type:'success',
// title:'Import Complete',
// message: `${data.filename} has been successfully imported as run ${data.metadata.run_id}`,
// action: action
// }
// }
// )
}
});
}, MONITOR_UPLOAD_TIMEOUT));
}
}, [importId, monitorUploadId, navigate, primaryObject])

function showNotification(type, title, message, action = null, timeout = null, key = null) {
// TODO replace with notification refactor
props?.eventEmitter?.emit('showNotification', type, title, message, action, timeout, key);
}

// TODO: separate functional upload component
function onBeforeUpload(files) {
for (var i = 0; i < files.length; i++) {
showNotification('info', 'File Uploaded', files[i].name + ' has been uploaded, importing will start momentarily.');
toast(ToastWrapper,
{
data: {
type: 'info',
title: 'File Uploaded',
message: files[i].name + ' has been uploaded, importing will start momentarily.'
},
toasClassName: isDarkTheme ? 'black-background' : 'white-background'
}
);
}
}

Expand All @@ -120,42 +159,31 @@ function IbutsuHeader (props) {
console.dir(response);
if (response.status >= 200 && response.status < 400) {
response.json().then((importObject) => {
showNotification('info', 'Import Starting', importObject.filename + ' is being imported...');
setImportId(importObject['id'], () => {
setMonitorUploadId(setInterval(monitorUpload, MONITOR_UPLOAD_TIMEOUT))
});
// toast(<ToastWrapper />,
// {
// data: {
// type: 'info',
// title: 'Import Starting',
// message: importObject.filename + ' is being imported...'
// }
// }
// );
setImportId(importObject['id']);
})
}
else {
showNotification('danger', 'Import Error', 'There was a problem importing your file');
// toast(<ToastWrapper />,
// {
// data: {
// type: 'danger',
// title: 'Import Error',
// message: 'There was a problem importing your file'
// }
// }
// );
}
}

function monitorUpload() {
if (!importId) {
console.warn('IbutsuHeader.monitorUpload is running with no importId set');
return;
}
const { primaryObject } = context;
HttpClient.get([Settings.serverUrl, 'import', importId])
.then(response => HttpClient.handleResponse(response))
.then(data => {
if (data['status'] === 'done') {
clearInterval(monitorUploadId);
setMonitorUploadId();
let action = null;
if (data.metadata.run_id) {
const RunButton = () => (
<AlertActionLink onClick={() => {navigate('/project/' + (data.metadata.project_id || primaryObject.id) + '/runs/' + data.metadata.run_id)}}>
Go to Run
</AlertActionLink>
)
action = <RunButton />;
}
showNotification('success', 'Import Complete', `${data.filename} has been successfully imported as run ${data.metadata.run_id}`, action);
}
});
}

function onProjectSelect(_event, value) {
const {
Expand Down Expand Up @@ -384,6 +412,7 @@ function IbutsuHeader (props) {
{headerTools}
</MastheadContent>
</Masthead>
<ToastContainer autoclose={ALERT_TIMEOUT}/>
</React.Fragment>
)
}
Expand Down
45 changes: 1 addition & 44 deletions frontend/src/components/ibutsu-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ import PropTypes from 'prop-types';
import { Outlet } from 'react-router-dom';

import {
Alert,
AlertGroup,
AlertVariant,
EmptyState,
EmptyStateBody,
EmptyStateHeader,
Expand All @@ -17,8 +14,6 @@ import {
} from '@patternfly/react-core';

import IbutsuHeader from './ibutsu-header';
import { ALERT_TIMEOUT } from '../constants';
import { getDateString } from '../utilities';
import { IbutsuContext } from '../services/context';
import IbutsuSidebar from './sidebar';
import { ArchiveIcon } from '@patternfly/react-icons';
Expand All @@ -42,55 +37,17 @@ export class IbutsuPage extends React.Component {
notifications: [],
views: []
};
this.props.eventEmitter.on('showNotification', (type, title, message, action, timeout, key) => {
this.showNotification(type, title, message, action, timeout, key);
});
// TODO: empty state props.children override

}

showNotification(type, title, message, action, timeout, key) {
let notifications = this.state.notifications;
let alertKey = key || getDateString();
timeout = timeout !== undefined ? timeout : true
if (notifications.find(element => element.key === alertKey) !== undefined) {
return;
}
let notification = {
'key': alertKey,
'type': AlertVariant[type],
'title': title,
'message': message,
'action': action
};
notifications.push(notification);
this.setState({notifications}, () => {
if (timeout === true) {
setTimeout(() => {
let notifs = this.state.notifications.filter((n) => {
if (n.type === type && n.title === title && n.message === message) {
return false;
}
return true;
});
this.setState({notifications: notifs});
}, ALERT_TIMEOUT);
}
});
}


render() {
document.title = this.props.title || 'Ibutsu';
const { primaryObject } = this.context;
return (
<React.Fragment>
<AlertGroup isToast>
{this.state.notifications.map((notification) => (
<Alert key={notification.key} variant={notification.type} title={notification.title} actionLinks={notification.action} timeout={ALERT_TIMEOUT} isLiveRegion>
{notification.message}
</Alert>
))}
</AlertGroup>
<Page
header={<IbutsuHeader eventEmitter={this.props.eventEmitter}/>}
sidebar={<IbutsuSidebar eventEmitter={this.props.eventEmitter} />}
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/components/toast-wrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Alert } from '@patternfly/react-core'
import PropTypes from 'prop-types';


const ToastWrapper = ({data}) => (
<Alert key={data.key} variant={data.type} title={data.title} actionLinks={data.action} isLiveRegion>
{data.message}
</Alert>
);

ToastWrapper.propTypes = {
data: PropTypes.object,
}

export default ToastWrapper;
18 changes: 11 additions & 7 deletions frontend/src/pages/profile/tokens.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ToastContainer, toast } from 'react-toastify';

import {
Button,
Expand All @@ -19,6 +20,8 @@ import { HttpClient } from '../../services/http';
import { Settings } from '../../settings';
import { getSpinnerRow } from '../../utilities';
import { FilterTable, AddTokenModal, DeleteModal } from '../../components';
import ToastWrapper from '../../components/toast-wrapper';
import { ALERT_TIMEOUT } from '../../constants';


export class UserTokens extends React.Component {
Expand Down Expand Up @@ -59,16 +62,16 @@ export class UserTokens extends React.Component {
};
}

showNotification(type, title, message, action?, timeout?, key?) {
if (!this.eventEmitter) {
return;
}
this.eventEmitter.emit('showNotification', type, title, message, action, timeout, key);
}

copyToClipboard = (text) => {
navigator.clipboard.writeText(text);
this.showNotification('info', 'Copied to clipboard', 'Your token has been copied to the clipboard');
toast(<ToastWrapper />,
{
data: {
type: 'info',
title: 'Copied to clipboard',
message: 'Your token has been copied to the clipboard'
}});
}

tokenToRow(token) {
Expand Down Expand Up @@ -224,6 +227,7 @@ export class UserTokens extends React.Component {
onDelete={this.onDeleteToken}
onClose={this.onDeleteTokenClose}
/>
<ToastContainer autoClose={ALERT_TIMEOUT} stacked/>
</React.Fragment>
);
}
Expand Down
11 changes: 2 additions & 9 deletions frontend/src/pages/profile/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,6 @@ export class UserProfile extends React.Component {
};
}

showNotification(type, title, message, action?, timeout?, key?) {
if (!this.eventEmitter) {
return;
}
this.eventEmitter.emit('showNotification', type, title, message, action, timeout, key);
}

updateUserName(userName) {
if (!this.eventEmitter) {
return;
Expand Down Expand Up @@ -87,12 +80,12 @@ export class UserProfile extends React.Component {
let tempUser = Object.assign({}, user, {name: tempName});
this.saveUser(tempUser).then(response => {
if (response !== undefined) {
this.showNotification('success', 'Name Updated', 'Your name has been updated.');
// this.showNotification('success', 'Name Updated', 'Your name has been updated.');
this.setState({user: tempUser, isEditing: false});
this.updateUserName(tempName);
}
else {
this.showNotification('danger', 'Error Updating', 'There was an error trying to save your name');
// this.showNotification('danger', 'Error Updating', 'There was an error trying to save your name');
this.setState({isEditing: false});
}
});
Expand Down
12 changes: 12 additions & 0 deletions frontend/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3707,6 +3707,11 @@ cliui@^7.0.2:
strip-ansi "^6.0.0"
wrap-ansi "^7.0.0"

clsx@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==

co@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
Expand Down Expand Up @@ -9522,6 +9527,13 @@ react-simple-oauth2-login@^0.5.4:
resolved "https://registry.yarnpkg.com/react-simple-oauth2-login/-/react-simple-oauth2-login-0.5.4.tgz#5d9b254fbc83484bd994f463c1f9e3643f0c639b"
integrity sha512-BkVpgGVFGpZEDK6lr1ao+hJXuPvK56aSA0t8jKST0Q+Aya504bSLXNk9A0eU8XaF2GeQqW9n/M0aWNkezvS4Pg==

react-toastify@^11.0.2:
version "11.0.2"
resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-11.0.2.tgz#5c5ae745f3c7240015a8746217595cd26701fdc3"
integrity sha512-GjHuGaiXMvbls3ywqv8XdWONwrcO4DXCJIY1zVLkHU73gEElKvTTXNI5Vom3s/k/M8hnkrfsqgBSX3OwmlonbA==
dependencies:
clsx "^2.1.1"

react@^18.3.1:
version "18.3.1"
resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"
Expand Down

0 comments on commit ed9195d

Please sign in to comment.