From 09d1cdd6576dc8c1deb7d06e19af6a1300479f6f Mon Sep 17 00:00:00 2001 From: KwonYongHyun Date: Thu, 25 Sep 2025 22:58:23 +0900 Subject: [PATCH] replace alerts with toast - replace blocking alerts with non-blocking toasts - add React Toast (react-bootstrap) and event bus API - mount single Toaster at app root; cap to 3 Items Issue-ID: AIMLFW-238 Change-Id: I5514b23a8d5a04c957669a275112a4dd88017499 Signed-off-by: KwonYongHyun --- src/App.js | 2 + src/components/home/form/CreateFeatureGroupForm.js | 7 ++-- .../home/form/CreateOrEditTrainingJobForm.js | 11 +++--- src/components/home/pipelines/UploadPipeline.js | 5 ++- src/components/home/status/API_STATUS.js | 7 ++-- src/components/home/status/ListFeatureGroup.js | 3 +- src/components/home/status/StatusPageRows.js | 8 ++-- src/components/toast/Toaster.jsx | 43 ++++++++++++++++++++++ src/utils/toast-bus.js | 22 +++++++++++ 9 files changed, 90 insertions(+), 18 deletions(-) create mode 100644 src/components/toast/Toaster.jsx create mode 100644 src/utils/toast-bus.js diff --git a/src/App.js b/src/App.js index 53cde0e..df4ec3c 100644 --- a/src/App.js +++ b/src/App.js @@ -20,11 +20,13 @@ import React from 'react'; import './App.css'; import HomePage from './components/home/HomePage'; import 'bootstrap/dist/css/bootstrap.min.css'; +import Toaster from './components/toast/Toaster'; function App() { return (
+
); } diff --git a/src/components/home/form/CreateFeatureGroupForm.js b/src/components/home/form/CreateFeatureGroupForm.js index 618d645..e3e2002 100644 --- a/src/components/home/form/CreateFeatureGroupForm.js +++ b/src/components/home/form/CreateFeatureGroupForm.js @@ -24,6 +24,7 @@ import './CreateFeatureGroupForm.css'; import { Row, Col } from 'react-bootstrap'; import { instance, UCMgr_baseUrl } from '../../../states'; import { featureGroupAPI } from '../../../apis'; +import { toast } from '../../../utils/toast-bus'; class CreateFeatureGroup extends React.Component { constructor(props) { @@ -72,7 +73,7 @@ class CreateFeatureGroup extends React.Component { handleFGNameChange = event => { if (this.regName.test(event.target.value)) { event.preventDefault(); - alert('Please use alphabet, number, and underscore to Feature Group Name.'); + toast.info('Please use alphabet, number, and underscore to Feature Group Name.'); } else { this.setState( { @@ -217,13 +218,13 @@ class CreateFeatureGroup extends React.Component { .then(res => { this.logger('featureGroup Created', res.data); if (res.status === 201) { - alert('FeatureGroup Created'); + toast.success('FeatureGroup Created', 'Feature Group'); this.resetForm(); } }) .catch(error => { this.logger('Error creating featureGroup', error); - alert('Failed: ' + error.response.data.Exception); + toast.error('Failed: ' + error.response.data.Exception, 'Feature Group'); event.preventDefault(); }) .then(function () { diff --git a/src/components/home/form/CreateOrEditTrainingJobForm.js b/src/components/home/form/CreateOrEditTrainingJobForm.js index c71b673..a4545e8 100644 --- a/src/components/home/form/CreateOrEditTrainingJobForm.js +++ b/src/components/home/form/CreateOrEditTrainingJobForm.js @@ -27,6 +27,7 @@ import { } from '../common/CommonMethods'; import { instance, UCMgr_baseUrl } from '../../../states'; import { featureGroupAPI, pipelineAPI, trainingJobAPI } from '../../../apis'; +import { toast } from '../../../utils/toast-bus'; class CreateTrainingJob extends React.Component { constructor(props) { @@ -401,7 +402,7 @@ class CreateTrainingJob extends React.Component { .then(res => { this.logger('Training responsed ', res); if (res.status === 201) { - alert('Training Job created and training initiated'); + toast.success('Training Job created and training initiated', 'Training Job'); this.resetFrom(); this.props.onHideCreatePopup(); this.props.fetchTrainingJobs(); @@ -409,7 +410,7 @@ class CreateTrainingJob extends React.Component { }) .catch(error => { this.logger('Error creating Training Job', error); - alert('Failed: ' + error.response.data.Exception); + toast.error('Failed: ' + error.response.data.Exception, 'Training Job'); event.preventDefault(); }); } @@ -442,7 +443,7 @@ class CreateTrainingJob extends React.Component { .catch(error => { // handle error this.logger('Error creating Use case', error); - alert('Failed: ' + error.response.data.Exception); + toast.error('Failed: ' + error.response.data.Exception, 'Training Job'); event.preventDefault(); }) .then(function () { @@ -459,7 +460,7 @@ class CreateTrainingJob extends React.Component { .then(res => { this.logger('Training responsed ', res); if (res.status === 200) { - alert('Training Job edited and training initiated'); + toast.success('Training Job edited and training initiated', 'Training Job'); this.props.onHideEditPopup(); this.props.fetchTrainingJobs(); } else { @@ -468,7 +469,7 @@ class CreateTrainingJob extends React.Component { }) .catch(error => { this.logger('Error in training api,response', error.response.data); - alert('Training failed: ' + error.response.data.Exception); + toast.error('Training failed: ' + error.response.data.Exception, 'Training Job'); }) .then(function () { // always executed diff --git a/src/components/home/pipelines/UploadPipeline.js b/src/components/home/pipelines/UploadPipeline.js index 972501a..56c00dd 100644 --- a/src/components/home/pipelines/UploadPipeline.js +++ b/src/components/home/pipelines/UploadPipeline.js @@ -24,6 +24,7 @@ import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; import Popover from 'react-bootstrap/Popover'; import { notebook_url, UCMgr_baseUrl } from '../../../states'; import { pipelineAPI } from '../../../apis/pipeline'; +import { toast } from '../../../utils/toast-bus'; class UploadPipelineForm extends Component { constructor(props) { @@ -85,7 +86,7 @@ class UploadPipelineForm extends Component { console.log('Status responsed ', res.status); if (res.status === 200) { console.log('Pipeline uploaded ', res.status); - alert(res.data.result); + toast.success(res.data.result, 'Pipeline'); this.resetFrom(event); } else { console.log('Upload pipeline error:', res); @@ -93,7 +94,7 @@ class UploadPipelineForm extends Component { }) .catch(error => { console.log('Error in uploading pipeline', error.response); - alert('Pipeline upload failed: ' + error.response.data.result); + toast.error('Pipeline upload failed: ' + error.response.data.result, 'Pipeline'); }) .then(function () { // always executed diff --git a/src/components/home/status/API_STATUS.js b/src/components/home/status/API_STATUS.js index a6644e3..28dc97b 100644 --- a/src/components/home/status/API_STATUS.js +++ b/src/components/home/status/API_STATUS.js @@ -18,6 +18,7 @@ import { featureGroupAPI } from '../../../apis/feature-group'; import { trainingJobAPI } from '../../../apis/training-job'; +import { toast } from '../../../utils/toast-bus'; export const invokeStartTraining = async trainingjobNames => { console.log('Retraining called ', trainingjobNames); @@ -25,7 +26,7 @@ export const invokeStartTraining = async trainingjobNames => { let res = await trainingJobAPI.invokeTrainingJob({ data: { trainingjobs_list: trainingjobNames } }); console.log('Retraining response', res); let result = 'Retraining initiated for selected trainingjob(s),Result' + '\n' + JSON.stringify(res.data); - alert(result); + toast.success(result, 'Training Job'); } catch (error) { console.log(error); } @@ -38,7 +39,7 @@ export const deleteTrainingjobs = async deleteTJList => { let res = await trainingJobAPI.deleteTrainingJob({ params: {trainingJobId: job.id} }); console.log('Delete API response', res); let result = `trainingjob deletion initiated for selected trainingjob ${job.id}, Result` + '\n' + JSON.stringify(res.status); - alert(result); + toast.success(result, 'Training Job'); } } catch (error) { console.log(error); @@ -51,7 +52,7 @@ export const deleteFeatureGroups = async featureGroup_names => { let res = await featureGroupAPI.deleteFeatureGroup({ data: { data: { featuregroups_list: featureGroup_names } } }); console.log('Deletion response', res); let result = 'FeatureGroup deletion initiated for selected featureGroups ,Result' + '\n' + JSON.stringify(res.data); - alert(result); + toast.success(result, 'Feature Group'); } catch (error) { console.log('error is : ', error); } diff --git a/src/components/home/status/ListFeatureGroup.js b/src/components/home/status/ListFeatureGroup.js index 65b26e3..6f57bd8 100644 --- a/src/components/home/status/ListFeatureGroup.js +++ b/src/components/home/status/ListFeatureGroup.js @@ -28,6 +28,7 @@ import { featureGroupAPI } from '../../../apis'; import FeatureGroupInfo from './FeatureGroupInfo'; import CreateFeatureGroup from '../create/CreateFeatureGroup'; import { deleteFeatureGroups } from './API_STATUS'; +import { toast } from '../../../utils/toast-bus'; const ListFeatureGroup = props => { const logger = props.logger; @@ -90,7 +91,7 @@ const ListFeatureGroup = props => { } toggleAllRowsSelected(false); } else { - alert('Please select more than one row'); + toast.info('Please select more than one row'); } }; diff --git a/src/components/home/status/StatusPageRows.js b/src/components/home/status/StatusPageRows.js index 02ff1cd..9211a83 100644 --- a/src/components/home/status/StatusPageRows.js +++ b/src/components/home/status/StatusPageRows.js @@ -24,7 +24,7 @@ import { useTable, useRowSelect } from 'react-table'; import { Checkbox, Popup, StepsState, TrainingJobInfo } from '../../../components'; import { UCMgr_baseUrl } from '../../../states'; import { trainingJobAPI } from '../../../apis'; - +import { toast } from '../../../utils/toast-bus'; import { invokeStartTraining, deleteTrainingjobs } from './API_STATUS'; import CreateOrEditTrainingJobForm from '../form/CreateOrEditTrainingJobForm'; import CreateTrainingJob from '../create/CreateTrainingJob'; @@ -87,7 +87,7 @@ const StatusPageRows = props => { } toggleAllRowsSelected(false); } else { - alert('Please select atleast one trainingjob'); + toast.info('Please select atleast one trainingjob'); } }; @@ -103,7 +103,7 @@ const StatusPageRows = props => { setEditPopup(true); toggleAllRowsSelected(false); } else { - alert('Please select exactly one trainingjob'); + toast.info('Please select exactly one trainingjob'); } }; @@ -123,7 +123,7 @@ const StatusPageRows = props => { } toggleAllRowsSelected(false); } else { - alert('Please select atleast one trainingjob'); + toast.info('Please select atleast one trainingjob'); } }; diff --git a/src/components/toast/Toaster.jsx b/src/components/toast/Toaster.jsx new file mode 100644 index 0000000..c9bbc66 --- /dev/null +++ b/src/components/toast/Toaster.jsx @@ -0,0 +1,43 @@ +import React, { useEffect, useState } from 'react'; +import { Toast, ToastContainer } from 'react-bootstrap'; +import { toastBus } from '../../utils/toast-bus'; + +export default function Toaster() { + const [items, setItems] = useState([]); + const MAX_TOASTS = 3; + + useEffect(() => { + const handler = ({ msg, title, bg = 'dark', delay = 4000 }) => { + const id = Date.now() + Math.random(); + setItems(prev => { + const next = [...prev, { id, msg, title, bg, delay }]; + while (next.length > MAX_TOASTS) next.shift(); + return next; + }); + if (delay > 0) { + setTimeout(() => { + setItems(prev => prev.filter(i => i.id !== id)); + }, delay); + } + }; + toastBus.on(handler); + return () => toastBus.off(handler); + }, []); + + return ( + + {items.map(t => ( + setItems(prev => prev.filter(i => i.id !== t.id))} autohide={false}> + {t.title ? ( + + {t.title} + + ) : null} + + {typeof t.msg === 'string' ? t.msg : JSON.stringify(t.msg)} + + + ))} + + ); +} diff --git a/src/utils/toast-bus.js b/src/utils/toast-bus.js new file mode 100644 index 0000000..bb8c3f0 --- /dev/null +++ b/src/utils/toast-bus.js @@ -0,0 +1,22 @@ +export const toastBus = { + _handlers: new Set(), + on(handler) { + this._handlers.add(handler); + }, + off(handler) { + this._handlers.delete(handler); + }, + emit(payload) { + for (const h of Array.from(this._handlers)) { + try { h(payload); } catch (e) { /* no-op */ } + } + } +}; + +export const toast = { + show: (msg, opts) => toastBus.emit({ msg, ...(opts || {}) }), + success: (msg, title = 'Success') => toastBus.emit({ msg, title, bg: 'success' }), + error: (msg, title = 'Error') => toastBus.emit({ msg, title, bg: 'danger', delay: 6000 }), + info: (msg, title = 'Info') => toastBus.emit({ msg, title, bg: 'info' }), + warning: (msg, title = 'Warning') => toastBus.emit({ msg, title, bg: 'warning' }), +}; -- 2.16.6