Allow client side request cancelling (#55)
* prevent graph select from wrapping and removed unnecessary iteration for id
* remove debug output statement
* add signal to request
* track active cypher requests
* allow long requests to be cancelled
* cleaned up code and removed logging statements
* merge with main update
* move styling to separate module and update select to fit/shrink when necessary
* Update SidebarComponents.jsx
Fix eslint issue
Co-authored-by: Hanbyeol Shin / David Shin / 신한별 <76985229+shinhanbyeol@users.noreply.github.com>
diff --git a/frontend/src/components/contents/containers/Editor.js b/frontend/src/components/contents/containers/Editor.js
index a17cc3d..4b1c3e4 100644
--- a/frontend/src/components/contents/containers/Editor.js
+++ b/frontend/src/components/contents/containers/Editor.js
@@ -33,6 +33,7 @@
database: state.database,
command: state.editor.command,
isActive: state.navigator.isActive,
+ activeRequests: state.cypher.activeRequests,
});
const mapDispatchToProps = {
diff --git a/frontend/src/components/contents/presentations/Editor.jsx b/frontend/src/components/contents/presentations/Editor.jsx
index 3411dbc..9b50d8c 100644
--- a/frontend/src/components/contents/presentations/Editor.jsx
+++ b/frontend/src/components/contents/presentations/Editor.jsx
@@ -31,6 +31,7 @@
const Editor = ({
setCommand,
+ activeRequests,
command,
addFrame,
trimFrame,
@@ -41,11 +42,11 @@
executeCypherQuery,
addCommandHistory,
toggleMenu,
- getMetaData,
// addCommandFavorites,
}) => {
const dispatch = useDispatch();
const [alerts, setAlerts] = useState([]);
+ const [activePromises, setPromises] = useState({});
// const favoritesCommand = () => {
// dispatch(() => addCommandFavorites(command));
@@ -56,7 +57,6 @@
};
const onClick = () => {
- console.log('in editor presentation command is ', command);
const refKey = uuid();
if (command.toUpperCase().startsWith(':PLAY')) {
dispatch(() => addFrame(command, 'Contents', refKey));
@@ -91,17 +91,31 @@
}
} else if (database.status === 'connected') {
addFrame(command, 'CypherResultFrame', refKey);
- dispatch(() => executeCypherQuery([refKey, command]).then((response) => {
+ const req = dispatch(() => executeCypherQuery([refKey, command]));
+ req.then((response) => {
if (response.type === 'cypher/executeCypherQuery/rejected') {
- dispatch(() => addAlert('ErrorCypherQuery'));
- } else { dispatch(() => getMetaData()); }
- }));
+ if (response.error.name !== 'AbortError') {
+ dispatch(() => addAlert('ErrorCypherQuery'));
+ }
+ }
+ });
+ activePromises[refKey] = req;
+ setPromises({ ...activePromises });
}
dispatch(() => addCommandHistory(command));
clearCommand();
};
useEffect(() => {
+ const reqCancel = Object.keys(activePromises).filter((ref) => !activeRequests.includes(ref));
+ reqCancel.forEach((ref) => {
+ activePromises[ref].abort();
+ delete activePromises[ref];
+ });
+ setPromises({ ...activePromises });
+ }, [activeRequests]);
+
+ useEffect(() => {
setAlerts(
alertList.map((alert) => (
<AlertContainers
@@ -190,6 +204,7 @@
Editor.propTypes = {
setCommand: PropTypes.func.isRequired,
+ activeRequests: PropTypes.arrayOf(PropTypes.string).isRequired,
command: PropTypes.string.isRequired,
addFrame: PropTypes.func.isRequired,
trimFrame: PropTypes.func.isRequired,
@@ -210,7 +225,6 @@
executeCypherQuery: PropTypes.func.isRequired,
addCommandHistory: PropTypes.func.isRequired,
toggleMenu: PropTypes.func.isRequired,
- getMetaData: PropTypes.func.isRequired,
// addCommandFavorites: PropTypes.func.isRequired,
};
diff --git a/frontend/src/components/frame/Frame.jsx b/frontend/src/components/frame/Frame.jsx
index aa585fa..c4be9a0 100644
--- a/frontend/src/components/frame/Frame.jsx
+++ b/frontend/src/components/frame/Frame.jsx
@@ -27,6 +27,7 @@
import { useDispatch } from 'react-redux';
import styles from './Frame.module.scss';
import { removeFrame } from '../../features/frame/FrameSlice';
+import { removeActiveRequests } from '../../features/cypher/CypherSlice';
import EdgeWeight from '../../icons/EdgeWeight';
import IconFilter from '../../icons/IconFilter';
import IconSearchCancel from '../../icons/IconSearchCancel';
@@ -176,7 +177,10 @@
size="large"
type="link"
className={`${styles.FrameButton}`}
- onClick={() => dispatch(removeFrame(refKey))}
+ onClick={() => {
+ dispatch(removeFrame(refKey));
+ dispatch(removeActiveRequests(refKey));
+ }}
title="Close Window"
>
<FontAwesomeIcon
diff --git a/frontend/src/components/sidebar/presentations/Components.scss b/frontend/src/components/sidebar/presentations/Components.scss
new file mode 100644
index 0000000..927d2cd
--- /dev/null
+++ b/frontend/src/components/sidebar/presentations/Components.scss
@@ -0,0 +1,13 @@
+
+
+#graphSelectionContainer {
+ position: relative;
+}
+
+#graphSelection {
+ display: flex;
+}
+
+.ant-select{
+ display: flex;
+}
\ No newline at end of file
diff --git a/frontend/src/components/sidebar/presentations/SidebarComponents.jsx b/frontend/src/components/sidebar/presentations/SidebarComponents.jsx
index 5cfa812..59151bb 100644
--- a/frontend/src/components/sidebar/presentations/SidebarComponents.jsx
+++ b/frontend/src/components/sidebar/presentations/SidebarComponents.jsx
@@ -19,7 +19,9 @@
import React from 'react';
import { Select } from 'antd';
+import { Col } from 'react-bootstrap';
import PropTypes from 'prop-types';
+import './Components.scss';
const StyleTextRight = {
marginBottom: '10px', textAlign: 'right', fontSize: '13px', fontWeight: 'bold',
@@ -96,17 +98,18 @@
marginTop: '1rem',
display: 'block',
};
- const handleGraphClick = (e) => {
- const graphName = graphs.find((graph) => graph[1] === e)[0];
- changeCurrentGraph({ id: e });
- changeGraphDB({ graphName });
+ const handleGraphClick = (_, e) => {
+ changeCurrentGraph({ id: e['data-gid'] });
+ changeGraphDB({ graphName: e.value });
};
const options = (
- graphs.map(([gname, graphId]) => (<option value={graphId}>{gname}</option>))
+ graphs.map(([gname, graphId]) => (
+ <Select.Option value={gname} data-gid={graphId}>{gname}</Select.Option>
+ ))
);
return (
- <div id="graphSelectDropdown">
+ <Col id="graphSelectionContainer">
<Select onChange={handleGraphClick} placeholder="Select Graph" style={selectStyle} value={currentGraph}>
{options}
</Select>
@@ -114,7 +117,7 @@
<b>
Current Graph
</b>
- </div>
+ </Col>
);
};
diff --git a/frontend/src/features/alert/AlertSlice.js b/frontend/src/features/alert/AlertSlice.js
index 2e55f9e..c1ae501 100644
--- a/frontend/src/features/alert/AlertSlice.js
+++ b/frontend/src/features/alert/AlertSlice.js
@@ -26,13 +26,10 @@
reducers: {
addAlert: {
reducer: (state, action) => {
- const { alertName } = action.payload;
+ const { alertName, message: errorMessage = '' } = action.payload;
let alertType = 'Notice';
- let errorMessage = '';
-
if (['ErrorServerConnectFail', 'ErrorNoDatabaseConnected', 'ErrorPlayLoadFail'].includes(alertName)) {
alertType = 'Error';
- errorMessage = action.payload.message;
}
state.push({ alertName, alertProps: { key: uuid(), alertType, errorMessage } });
diff --git a/frontend/src/features/cypher/CypherSlice.js b/frontend/src/features/cypher/CypherSlice.js
index 78c2950..0b89909 100644
--- a/frontend/src/features/cypher/CypherSlice.js
+++ b/frontend/src/features/cypher/CypherSlice.js
@@ -48,11 +48,8 @@
export const executeCypherQuery = createAsyncThunk(
'cypher/executeCypherQuery',
- async (args) => {
+ async (args, thunkAPI) => {
try {
- // validateSamePathVariableReturn(args[1]);
- // validateVlePathVariableReturn(args[1]);
-
const response = await fetch('/api/v1/cypher',
{
method: 'POST',
@@ -61,6 +58,7 @@
'Content-Type': 'application/json',
},
body: JSON.stringify({ cmd: args[1] }),
+ signal: thunkAPI.signal,
});
if (response.ok) {
const res = await response.json();
@@ -80,10 +78,15 @@
);
+const removeActive = (state, key) => {
+ state.activeRequests = state.activeRequests.filter((ref) => ref !== key);
+};
+
const CypherSlice = createSlice({
name: 'cypher',
initialState: {
queryResult: {},
+ activeRequests: [],
labels: { nodeLabels: {}, edgeLabels: {} },
},
reducers: {
@@ -105,26 +108,30 @@
},
prepare: (elementType, label, property) => ({ payload: { elementType, label, property } }),
},
+ removeActiveRequests: (state, action) => removeActive(state, action.payload),
},
extraReducers: {
[executeCypherQuery.fulfilled]: (state, action) => {
- // state.queryResult[action.payload.key].response = action.payload
Object.assign(state.queryResult[action.payload.key], {
...action.payload,
complete: true,
});
+ removeActive(state, action.payload.key);
},
[executeCypherQuery.pending]: (state, action) => {
const key = action.meta.arg[0];
const command = action.meta.arg[1];
+ const rid = action.meta.requestId;
state.queryResult[key] = {};
+ state.activeRequests = [...state.activeRequests, key];
Object.assign(state.queryResult[key], {
command,
complete: false,
- requestId: action.meta.requestId,
+ requestId: rid,
});
},
[executeCypherQuery.rejected]: (state, action) => {
+ removeActive(state, action.meta.arg[0]);
state.queryResult[action.meta.arg[0]] = {
command: 'ERROR',
query: action.meta.arg[1],
@@ -136,6 +143,6 @@
},
});
-export const { setLabels } = CypherSlice.actions;
+export const { setLabels, removeActiveRequests } = CypherSlice.actions;
export default CypherSlice.reducer;