diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/active_user.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/active_user.py index 71b81bc9..943edc6f 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/active_user.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/active_user.py @@ -1,9 +1,10 @@ from __future__ import annotations -from spiffworkflow_backend.models.user import UserModel + from sqlalchemy import ForeignKey from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel +from spiffworkflow_backend.models.user import UserModel class ActiveUserModel(SpiffworkflowBaseDBModel): diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/active_users_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/active_users_controller.py index 5e9ba1ec..496d9b06 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/active_users_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/active_users_controller.py @@ -1,29 +1,22 @@ import json -import os import time -from spiffworkflow_backend.models.user import UserModel -from spiffworkflow_backend.models.db import db from typing import Generator -from flask import stream_with_context -import re -from typing import Any -from typing import Dict -from typing import Optional -from typing import Union -import connexion # type: ignore import flask.wrappers from flask import current_app from flask import g -from flask import jsonify -from flask import make_response +from flask import stream_with_context from flask.wrappers import Response + from spiffworkflow_backend.models.active_user import ActiveUserModel -from werkzeug.datastructures import FileStorage +from spiffworkflow_backend.models.db import db +from spiffworkflow_backend.models.user import UserModel def active_user_updates(last_visited_identifier: str) -> Response: - active_user = ActiveUserModel.query.filter_by(user_id=g.user.id, last_visited_identifier=last_visited_identifier).first() + active_user = ActiveUserModel.query.filter_by( + user_id=g.user.id, last_visited_identifier=last_visited_identifier + ).first() if active_user is None: active_user = ActiveUserModel(user_id=g.user.id, last_visited_identifier=last_visited_identifier) db.session.add(active_user) @@ -42,23 +35,23 @@ def _active_user_updates(last_visited_identifier: str, active_user: ActiveUserMo db.session.add(active_user) db.session.commit() - time.sleep(1) - cutoff_time_in_seconds = time.time() - 7 + cutoff_time_in_seconds = time.time() - 15 active_users = ( - UserModel.query - .join(ActiveUserModel) + UserModel.query.join(ActiveUserModel) .filter(ActiveUserModel.last_visited_identifier == last_visited_identifier) .filter(ActiveUserModel.last_seen_in_seconds > cutoff_time_in_seconds) - # .filter(UserModel.id != g.user.id) + .filter(UserModel.id != g.user.id) .all() ) yield f"data: {current_app.json.dumps(active_users)} \n\n" + time.sleep(5) -def active_user_unregister( - last_visited_identifier: str -) -> flask.wrappers.Response: - active_user = ActiveUserModel.query.filter_by(user_id=g.user.id, last_visited_identifier=last_visited_identifier).first() + +def active_user_unregister(last_visited_identifier: str) -> flask.wrappers.Response: + active_user = ActiveUserModel.query.filter_by( + user_id=g.user.id, last_visited_identifier=last_visited_identifier + ).first() if active_user is not None: db.session.delete(active_user) db.session.commit() diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/health_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/health_controller.py index 556ec6b4..05408a4f 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/health_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/health_controller.py @@ -9,10 +9,6 @@ def status() -> Response: """Status.""" ProcessInstanceModel.query.filter().first() return make_response({"ok": True}, 200) -def status2() -> Response: - """Status.""" - ProcessInstanceModel.query.filter().first() - return make_response({"ok": True}, 200) def test_raise_error() -> Response: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/logging_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/logging_service.py index d9732b0b..3a5ee845 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/logging_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/logging_service.py @@ -130,6 +130,7 @@ def setup_logger(app: Flask) -> None: # these loggers have been deemed too verbose to be useful garbage_loggers_to_exclude = ["connexion", "flask_cors.extension"] + loggers_to_exclude_from_debug = ["sqlalchemy"] # make all loggers act the same for name in logging.root.manager.loggerDict: @@ -149,8 +150,17 @@ def setup_logger(app: Flask) -> None: for garbage_logger in garbage_loggers_to_exclude: if name.startswith(garbage_logger): exclude_logger_name_from_logging = True + + exclude_logger_name_from_debug = False + for logger_to_exclude_from_debug in loggers_to_exclude_from_debug: + if name.startswith(logger_to_exclude_from_debug): + exclude_logger_name_from_debug = True + if exclude_logger_name_from_debug: + the_logger.setLevel("INFO") + if not exclude_logger_name_from_logging: the_logger.addHandler(logging.StreamHandler(sys.stdout)) + for the_handler in the_logger.handlers: the_handler.setFormatter(log_formatter) the_handler.setLevel(log_level) diff --git a/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx b/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx index cddb7211..1855f955 100644 --- a/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx +++ b/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx @@ -88,6 +88,7 @@ type OwnProps = { onElementsChanged?: (..._args: any[]) => any; url?: string; callers?: ProcessModelCaller[]; + activeUserElement?: React.ReactElement; }; // https://codesandbox.io/s/quizzical-lake-szfyo?file=/src/App.js was a handy reference @@ -114,6 +115,7 @@ export default function ReactDiagramEditor({ onElementsChanged, url, callers, + activeUserElement, }: OwnProps) { const [diagramXMLString, setDiagramXMLString] = useState(''); const [diagramModelerState, setDiagramModelerState] = useState(null); @@ -672,6 +674,7 @@ export default function ReactDiagramEditor({ )} {getReferencesButton()} + {activeUserElement || null} ); } @@ -679,9 +682,9 @@ export default function ReactDiagramEditor({ }; return ( -
+ <> {userActionOptions()} {showReferences()} -
+ ); } diff --git a/spiffworkflow-frontend/src/index.css b/spiffworkflow-frontend/src/index.css index 7b4d327e..7f815787 100644 --- a/spiffworkflow-frontend/src/index.css +++ b/spiffworkflow-frontend/src/index.css @@ -459,6 +459,7 @@ svg.notification-icon { .user-list { display: flex; align-items: center; + margin-left: 8px; } .user-circle { diff --git a/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx b/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx index 8bb199e4..9ea22a5c 100644 --- a/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx @@ -31,7 +31,11 @@ import HttpService, { getBasicHeaders } from '../services/HttpService'; import ReactDiagramEditor from '../components/ReactDiagramEditor'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; import useAPIError from '../hooks/UseApiError'; -import { makeid, modifyProcessIdentifierForPathParam } from '../helpers'; +import { + makeid, + modifyProcessIdentifierForPathParam, + encodeBase64, +} from '../helpers'; import { CarbonComboBoxProcessSelection, ProcessFile, @@ -128,8 +132,29 @@ export default function ProcessModelEditDiagram() { usePrompt('Changes you made may not be saved.', diagramHasChanges); + const lastVisitedIdentifier = encodeBase64(window.location.pathname); useEffect(() => { - const lastVisitedIdentifier = 'HEY'; + // Grab all available process models in case we need to search for them. + // Taken from the Process Group List + const processResults = (result: any) => { + const selectionArray = result.map((item: any) => { + const label = `${item.display_name} (${item.identifier})`; + Object.assign(item, { label }); + return item; + }); + setProcesses(selectionArray); + }; + HttpService.makeCallToBackend({ + path: `/processes`, + successCallback: processResults, + }); + + const unregisterUser = () => { + HttpService.makeCallToBackend({ + path: `/active-users/unregister/${lastVisitedIdentifier}`, + successCallback: setActiveUsers, + }); + }; fetchEventSource( `${BACKEND_BASE_URL}/active-users/updates/${lastVisitedIdentifier}`, { @@ -143,52 +168,31 @@ export default function ProcessModelEditDiagram() { } }, onclose() { - HttpService.makeCallToBackend({ - path: `/active-users/unregister/${lastVisitedIdentifier}`, - successCallback: setActiveUsers, - }); + unregisterUser(); }, onerror(err: any) { throw err; }, } ); + + // FIXME: this is not getting called when navigating away from this page. + // we do not know why yet. + return unregisterUser; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // it is critical to only run this once. - // useEffect(() => { - // // Grab all available process models in case we need to search for them. - // // Taken from the Process Group List - // const processResults = (result: any) => { - // const selectionArray = result.map((item: any) => { - // const label = `${item.display_name} (${item.identifier})`; - // Object.assign(item, { label }); - // return item; - // }); - // setProcesses(selectionArray); - // }; - // HttpService.makeCallToBackend({ - // path: `/processes`, - // successCallback: processResults, - // }); - // }, []); - - useEffect(() => { - const processResult = (result: ProcessModel) => { - setProcessModel(result); - }; - HttpService.makeCallToBackend({ - path: `/${processModelPath}?include_file_references=true`, - successCallback: processResult, - }); - }, [processModelPath]); - useEffect(() => { const fileResult = (result: any) => { setProcessModelFile(result); setBpmnXmlForDiagramRendering(result.file_contents); }; + HttpService.makeCallToBackend({ + path: `/${processModelPath}?include_file_references=true`, + successCallback: setProcessModel, + }); + if (params.file_name) { HttpService.makeCallToBackend({ path: `/${processModelPath}/files/${params.file_name}`, @@ -962,6 +966,20 @@ export default function ProcessModelEditDiagram() { return searchParams.get('file_type') === 'dmn' || fileName.endsWith('.dmn'); }; + const activeUserElement = () => { + const au = activeUsers.map((activeUser: User) => { + return ( +
+ {activeUser.username.charAt(0).toUpperCase()} +
+ ); + }); + return
{au}
; + }; + const appropriateEditor = () => { if (isDmn()) { return ( @@ -1006,6 +1024,7 @@ export default function ProcessModelEditDiagram() { onSearchProcessModels={onSearchProcessModels} onElementsChanged={onElementsChanged} callers={callers} + activeUserElement={activeUserElement()} /> ); }; @@ -1024,17 +1043,6 @@ export default function ProcessModelEditDiagram() { return null; }; - const activeUserElement = () => { - const au = activeUsers.map((activeUser: User) => { - return ( -
- {activeUser.username.charAt(0).toUpperCase()} -
- ); - }); - return
{au}
; - }; - // if a file name is not given then this is a new model and the ReactDiagramEditor component will handle it if ((bpmnXmlForDiagramRendering || !params.file_name) && processModel) { const processModelFileName = processModelFile ? processModelFile.name : ''; @@ -1055,7 +1063,6 @@ export default function ProcessModelEditDiagram() { Process Model File{processModelFile ? ': ' : ''} {processModelFileName} - {activeUserElement()} {saveFileMessage()} {appropriateEditor()} {newFileNameBox()}