replace alerts with toast 86/15086/1
authorKwonYongHyun <a48209952@gmail.com>
Thu, 25 Sep 2025 13:58:23 +0000 (22:58 +0900)
committerKwonYongHyun <a48209952@gmail.com>
Thu, 25 Sep 2025 13:58:23 +0000 (22:58 +0900)
- 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 <a48209952@gmail.com>
src/App.js
src/components/home/form/CreateFeatureGroupForm.js
src/components/home/form/CreateOrEditTrainingJobForm.js
src/components/home/pipelines/UploadPipeline.js
src/components/home/status/API_STATUS.js
src/components/home/status/ListFeatureGroup.js
src/components/home/status/StatusPageRows.js
src/components/toast/Toaster.jsx [new file with mode: 0644]
src/utils/toast-bus.js [new file with mode: 0644]

index 53cde0e..df4ec3c 100644 (file)
@@ -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 (
     <div className='App'>
       <HomePage />
+      <Toaster />
     </div>
   );
 }
index 618d645..e3e2002 100644 (file)
@@ -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 () {
index c71b673..a4545e8 100644 (file)
@@ -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
index 972501a..56c00dd 100644 (file)
@@ -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
index a6644e3..28dc97b 100644 (file)
@@ -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);
   }
index 65b26e3..6f57bd8 100644 (file)
@@ -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');
     }
   };
 
index 02ff1cd..9211a83 100644 (file)
@@ -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 (file)
index 0000000..c9bbc66
--- /dev/null
@@ -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 (
+    <ToastContainer position='top-end' className='p-3'>
+      {items.map(t => (
+        <Toast key={t.id} bg={t.bg} onClose={() => setItems(prev => prev.filter(i => i.id !== t.id))} autohide={false}>
+          {t.title ? (
+            <Toast.Header>
+              <strong className='me-auto'>{t.title}</strong>
+            </Toast.Header>
+          ) : null}
+          <Toast.Body style={{ color: 'white' }}>
+            {typeof t.msg === 'string' ? t.msg : JSON.stringify(t.msg)}
+          </Toast.Body>
+        </Toast>
+      ))}
+    </ToastContainer>
+  );
+}
diff --git a/src/utils/toast-bus.js b/src/utils/toast-bus.js
new file mode 100644 (file)
index 0000000..bb8c3f0
--- /dev/null
@@ -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' }),
+};