node delete confirmation (#74)

* delete confirmation

* Fix unused icon remove

Co-authored-by: moon19960501@gmail.com <wenzhenhe1@gmail.com>
Co-authored-by: Hanbyeol Shin /  David Shin / 신한별 <76985229+shinhanbyeol@users.noreply.github.com>
diff --git a/frontend/src/app/reducers.js b/frontend/src/app/reducers.js
index 1c9e069..411eba8 100644
--- a/frontend/src/app/reducers.js
+++ b/frontend/src/app/reducers.js
@@ -26,6 +26,7 @@
 import CypherReducer from '../features/cypher/CypherSlice';
 import AlertReducer from '../features/alert/AlertSlice';
 import EditorSlice from '../features/editor/EditorSlice';
+import ModalSlice from '../features/modal/ModalSlice';
 import LayoutSlice from '../features/layout/LayoutSlice';
 
 const rootReducer = combineReducers({
@@ -37,6 +38,7 @@
   cypher: CypherReducer,
   alerts: AlertReducer,
   editor: EditorSlice,
+  modal: ModalSlice,
   layout: LayoutSlice,
 });
 
diff --git a/frontend/src/components/cypherresult/containers/CypherResultCytoscapeContainer.js b/frontend/src/components/cypherresult/containers/CypherResultCytoscapeContainer.js
index 157e118..07c701f 100644
--- a/frontend/src/components/cypherresult/containers/CypherResultCytoscapeContainer.js
+++ b/frontend/src/components/cypherresult/containers/CypherResultCytoscapeContainer.js
@@ -20,6 +20,7 @@
 import { connect } from 'react-redux';
 import CypherResultCytoscape from '../presentations/CypherResultCytoscape';
 import { setLabels } from '../../../features/cypher/CypherSlice';
+import { openModal, addGraphHistory, addElementHistory } from '../../../features/modal/ModalSlice';
 import { generateCytoscapeElement } from '../../../features/cypher/CypherUtil';
 
 const mapStateToProps = (state, ownProps) => {
@@ -54,7 +55,12 @@
   };
 };
 
-const mapDispatchToProps = { setLabels };
+const mapDispatchToProps = {
+  setLabels,
+  openModal,
+  addGraphHistory,
+  addElementHistory,
+};
 
 export default connect(
   mapStateToProps,
diff --git a/frontend/src/components/cypherresult/presentations/CypherResultCytoscape.jsx b/frontend/src/components/cypherresult/presentations/CypherResultCytoscape.jsx
index 049a86e..982b710 100644
--- a/frontend/src/components/cypherresult/presentations/CypherResultCytoscape.jsx
+++ b/frontend/src/components/cypherresult/presentations/CypherResultCytoscape.jsx
@@ -380,6 +380,9 @@
         addLegendData={addLegendData}
         maxDataOfGraph={maxDataOfGraph}
         graph={props.graph}
+        openModal={props.openModal}
+        addGraphHistory={props.addGraphHistory}
+        addElementHistory={props.addElementHistory}
       />
       <CypherResultCytoscapeFooter
         captions={captions}
@@ -421,6 +424,9 @@
   refKey: PropTypes.string.isRequired,
   setChartLegend: PropTypes.func.isRequired,
   graph: PropTypes.string.isRequired,
+  openModal: PropTypes.func.isRequired,
+  addGraphHistory: PropTypes.func.isRequired,
+  addElementHistory: PropTypes.func.isRequired,
   setIsTable: PropTypes.func.isRequired,
 };
 
diff --git a/frontend/src/components/cytoscape/CypherResultCytoscapeChart.jsx b/frontend/src/components/cytoscape/CypherResultCytoscapeChart.jsx
index a68c35b..17c9949 100644
--- a/frontend/src/components/cytoscape/CypherResultCytoscapeChart.jsx
+++ b/frontend/src/components/cytoscape/CypherResultCytoscapeChart.jsx
@@ -27,6 +27,7 @@
 import euler from 'cytoscape-euler';
 import avsdf from 'cytoscape-avsdf';
 import spread from 'cytoscape-spread';
+import { useDispatch } from 'react-redux';
 import CytoscapeComponent from 'react-cytoscapejs';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import {
@@ -53,10 +54,12 @@
 
 const CypherResultCytoscapeCharts = ({
   elements, cytoscapeObject, setCytoscapeObject, cytoscapeLayout, maxDataOfGraph,
-  onElementsMouseover, addLegendData, graph,
+  onElementsMouseover, addLegendData, graph, openModal,
+  addGraphHistory, addElementHistory,
 }) => {
   const [cytoscapeMenu, setCytoscapeMenu] = useState(null);
   const [initialized, setInitialized] = useState(false);
+  const dispatch = useDispatch();
   const addEventOnElements = (targetElements) => {
     targetElements.bind('mouseover', (e) => {
       onElementsMouseover({ type: 'elements', data: e.target.data() });
@@ -234,6 +237,17 @@
                 });
             },
           },
+
+          {
+            content: ReactDOMServer.renderToString(
+              (<FontAwesomeIcon icon={faTrash} size="lg" />),
+            ),
+            select(ele) {
+              dispatch(openModal());
+              dispatch(addGraphHistory(graph));
+              dispatch(addElementHistory(ele.id()));
+            },
+          },
         ],
         fillColor: 'rgba(210, 213, 218, 1)',
         activeFillColor: 'rgba(166, 166, 166, 1)',
@@ -310,6 +324,9 @@
   onElementsMouseover: PropTypes.func.isRequired,
   addLegendData: PropTypes.func.isRequired,
   graph: PropTypes.string.isRequired,
+  openModal: PropTypes.func.isRequired,
+  addGraphHistory: PropTypes.func.isRequired,
+  addElementHistory: PropTypes.func.isRequired,
 };
 
 export default CypherResultCytoscapeCharts;
diff --git a/frontend/src/components/modal/containers/Modal.js b/frontend/src/components/modal/containers/Modal.js
new file mode 100644
index 0000000..f34aed1
--- /dev/null
+++ b/frontend/src/components/modal/containers/Modal.js
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { connect } from 'react-redux';
+import { closeModal, removeGraphHistory, removeElementHistory } from '../../../features/modal/ModalSlice';
+import Modal from '../presentations/Modal';
+
+const mapStateToProps = (state) => ({
+  graphHistory: state.modal.graphHistory,
+  elementHistory: state.modal.elementHistory,
+});
+const mapDispatchToProps = { closeModal, removeGraphHistory, removeElementHistory };
+
+export default connect(mapStateToProps, mapDispatchToProps)(Modal);
diff --git a/frontend/src/components/modal/presentations/Modal.jsx b/frontend/src/components/modal/presentations/Modal.jsx
new file mode 100644
index 0000000..71854cc
--- /dev/null
+++ b/frontend/src/components/modal/presentations/Modal.jsx
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import { useDispatch } from 'react-redux';
+
+const Modal = ({
+  closeModal,
+  graphHistory,
+  elementHistory,
+  removeGraphHistory,
+  removeElementHistory,
+}) => {
+  const dispatch = useDispatch();
+
+  const removeNode = () => {
+    fetch('/api/v1/cypher',
+      {
+        method: 'POST',
+        headers: {
+          Accept: 'application/json',
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({ cmd: `SELECT * FROM cypher('${graphHistory[0]}', $$ MATCH (S) WHERE id(S) = ${elementHistory[0]} DETACH DELETE S $$) as (S agtype);` }),
+      })
+      .then((res) => {
+        if (res.ok) {
+          dispatch(removeGraphHistory());
+          dispatch(removeElementHistory());
+          dispatch(closeModal());
+          alert('The node has been deleted from your database. Please refresh the page or frame.');
+        }
+      });
+  };
+
+  return (
+    <div className="modal-container">
+      <div className="modal-wrapper">
+        <h4>
+          After clicking on confirm, the node and related edge will be deleted from the database.
+        </h4>
+        <div className="btn-container">
+          <button type="button" className="btn confirm-btn" onClick={() => { dispatch(closeModal()); }}>
+            cancel
+          </button>
+          <button type="button" className="btn clear-btn" onClick={() => { removeNode(); }}>
+            confirm
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+Modal.propTypes = {
+  closeModal: PropTypes.func.isRequired,
+  // eslint-disable-next-line react/forbid-prop-types
+  graphHistory: PropTypes.any.isRequired,
+  // eslint-disable-next-line react/forbid-prop-types
+  elementHistory: PropTypes.any.isRequired,
+  removeGraphHistory: PropTypes.func.isRequired,
+  removeElementHistory: PropTypes.func.isRequired,
+};
+
+export default Modal;
diff --git a/frontend/src/components/template/DefaultTemplate.js b/frontend/src/components/template/DefaultTemplate.js
index 880d09d..80e8643 100644
--- a/frontend/src/components/template/DefaultTemplate.js
+++ b/frontend/src/components/template/DefaultTemplate.js
@@ -27,6 +27,7 @@
   maxNumOfHistories: state.setting.maxNumOfHistories,
   maxDataOfGraph: state.setting.maxDataOfGraph,
   maxDataOfTable: state.setting.maxDataOfTable,
+  isOpen: state.modal.isOpen,
 });
 
 const mapDispatchToProps = { changeSettings };
diff --git a/frontend/src/components/template/presentations/DefaultTemplate.jsx b/frontend/src/components/template/presentations/DefaultTemplate.jsx
index d24ffcc..323e1c9 100644
--- a/frontend/src/components/template/presentations/DefaultTemplate.jsx
+++ b/frontend/src/components/template/presentations/DefaultTemplate.jsx
@@ -23,6 +23,7 @@
 import EditorContainer from '../../contents/containers/Editor';
 import Sidebar from '../../sidebar/containers/Sidebar';
 import Contents from '../../contents/containers/Contents';
+import Modal from '../../modal/containers/Modal';
 import { loadFromCookie, saveToCookie } from '../../../features/cookie/CookieUtil';
 
 const DefaultTemplate = ({
@@ -32,6 +33,7 @@
   maxDataOfGraph,
   maxDataOfTable,
   changeSettings,
+  isOpen,
 }) => {
   const dispatch = useDispatch();
   const [stateValues] = useState({
@@ -74,6 +76,7 @@
 
   return (
     <div className="default-template">
+      { isOpen && <Modal /> }
       <input
         type="radio"
         className="theme-switch"
@@ -109,6 +112,7 @@
   maxDataOfGraph: PropTypes.number.isRequired,
   maxDataOfTable: PropTypes.number.isRequired,
   changeSettings: PropTypes.func.isRequired,
+  isOpen: PropTypes.bool.isRequired,
 };
 
 export default DefaultTemplate;
diff --git a/frontend/src/features/modal/ModalSlice.js b/frontend/src/features/modal/ModalSlice.js
new file mode 100644
index 0000000..a9003df
--- /dev/null
+++ b/frontend/src/features/modal/ModalSlice.js
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/* eslint-disable no-param-reassign */
+import { createSlice } from '@reduxjs/toolkit';
+
+const ModalSlice = createSlice({
+  name: 'modal',
+  initialState: {
+    isOpen: false,
+    graphHistory: [],
+    elementHistory: [],
+  },
+  reducers: {
+    openModal: {
+      reducer: (state) => {
+        state.isOpen = true;
+      },
+    },
+    closeModal: {
+      reducer: (state) => {
+        state.isOpen = false;
+      },
+    },
+    addGraphHistory: {
+      reducer: (state, action) => {
+        state.graphHistory.push(action.payload.graph);
+      },
+      prepare: (graph) => ({ payload: { graph } }),
+    },
+    addElementHistory: {
+      reducer: (state, action) => {
+        state.elementHistory.push(action.payload.element);
+      },
+      prepare: (element) => ({ payload: { element } }),
+    },
+    removeGraphHistory: {
+      reducer: (state) => {
+        state.graphHistory = [];
+      },
+    },
+    removeElementHistory: {
+      reducer: (state) => {
+        state.elementHistory = [];
+      },
+    },
+  },
+});
+
+export const {
+  openModal,
+  closeModal,
+  addGraphHistory,
+  addElementHistory,
+  removeGraphHistory,
+  removeElementHistory,
+} = ModalSlice.actions;
+
+export default ModalSlice.reducer;
diff --git a/frontend/src/static/style.css b/frontend/src/static/style.css
index 8d5ad43..19e4186 100644
--- a/frontend/src/static/style.css
+++ b/frontend/src/static/style.css
@@ -797,4 +797,41 @@
 }
 .refresh_button:hover {
     opacity: 0.6;
-}
\ No newline at end of file
+}
+
+.modal-container {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background: rgba(0, 0, 0, 0.7);
+    z-index: 9999;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+  
+  .modal-wrapper {
+    background: #F8F8F8;
+    width: 80vw;
+    max-width: 400px;
+    border-radius: 10px;
+    padding: 2rem 1rem;
+    text-align: center;
+  }
+  .modal-wrapper h4 {
+    margin-bottom: 0;
+    line-height: 1.5;
+    font-size: 1.2rem;
+  }
+  .modal-wrapper .clear-btn,
+  .modal-wrapper .confirm-btn {
+    margin-top: 1rem;
+    cursor: pointer;
+  }
+  .btn-container {
+    display: flex;
+    justify-content: space-around;
+  }
+  
\ No newline at end of file