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