cleaned up displaying active users in frontend w/ burnettk

This commit is contained in:
jasquat 2023-05-04 12:44:04 -04:00
parent f7e8fd0022
commit 9320ec3cf9
7 changed files with 86 additions and 75 deletions

View File

@ -1,9 +1,10 @@
from __future__ import annotations from __future__ import annotations
from spiffworkflow_backend.models.user import UserModel
from sqlalchemy import ForeignKey from sqlalchemy import ForeignKey
from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel
from spiffworkflow_backend.models.user import UserModel
class ActiveUserModel(SpiffworkflowBaseDBModel): class ActiveUserModel(SpiffworkflowBaseDBModel):

View File

@ -1,29 +1,22 @@
import json import json
import os
import time import time
from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.models.db import db
from typing import Generator 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 import flask.wrappers
from flask import current_app from flask import current_app
from flask import g from flask import g
from flask import jsonify from flask import stream_with_context
from flask import make_response
from flask.wrappers import Response from flask.wrappers import Response
from spiffworkflow_backend.models.active_user import ActiveUserModel 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: 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: if active_user is None:
active_user = ActiveUserModel(user_id=g.user.id, last_visited_identifier=last_visited_identifier) active_user = ActiveUserModel(user_id=g.user.id, last_visited_identifier=last_visited_identifier)
db.session.add(active_user) 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.add(active_user)
db.session.commit() db.session.commit()
time.sleep(1) cutoff_time_in_seconds = time.time() - 15
cutoff_time_in_seconds = time.time() - 7
active_users = ( active_users = (
UserModel.query UserModel.query.join(ActiveUserModel)
.join(ActiveUserModel)
.filter(ActiveUserModel.last_visited_identifier == last_visited_identifier) .filter(ActiveUserModel.last_visited_identifier == last_visited_identifier)
.filter(ActiveUserModel.last_seen_in_seconds > cutoff_time_in_seconds) .filter(ActiveUserModel.last_seen_in_seconds > cutoff_time_in_seconds)
# .filter(UserModel.id != g.user.id) .filter(UserModel.id != g.user.id)
.all() .all()
) )
yield f"data: {current_app.json.dumps(active_users)} \n\n" yield f"data: {current_app.json.dumps(active_users)} \n\n"
time.sleep(5)
def active_user_unregister(
last_visited_identifier: str def active_user_unregister(last_visited_identifier: str) -> flask.wrappers.Response:
) -> flask.wrappers.Response: active_user = ActiveUserModel.query.filter_by(
active_user = ActiveUserModel.query.filter_by(user_id=g.user.id, last_visited_identifier=last_visited_identifier).first() user_id=g.user.id, last_visited_identifier=last_visited_identifier
).first()
if active_user is not None: if active_user is not None:
db.session.delete(active_user) db.session.delete(active_user)
db.session.commit() db.session.commit()

View File

@ -9,10 +9,6 @@ def status() -> Response:
"""Status.""" """Status."""
ProcessInstanceModel.query.filter().first() ProcessInstanceModel.query.filter().first()
return make_response({"ok": True}, 200) 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: def test_raise_error() -> Response:

View File

@ -130,6 +130,7 @@ def setup_logger(app: Flask) -> None:
# these loggers have been deemed too verbose to be useful # these loggers have been deemed too verbose to be useful
garbage_loggers_to_exclude = ["connexion", "flask_cors.extension"] garbage_loggers_to_exclude = ["connexion", "flask_cors.extension"]
loggers_to_exclude_from_debug = ["sqlalchemy"]
# make all loggers act the same # make all loggers act the same
for name in logging.root.manager.loggerDict: 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: for garbage_logger in garbage_loggers_to_exclude:
if name.startswith(garbage_logger): if name.startswith(garbage_logger):
exclude_logger_name_from_logging = True 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: if not exclude_logger_name_from_logging:
the_logger.addHandler(logging.StreamHandler(sys.stdout)) the_logger.addHandler(logging.StreamHandler(sys.stdout))
for the_handler in the_logger.handlers: for the_handler in the_logger.handlers:
the_handler.setFormatter(log_formatter) the_handler.setFormatter(log_formatter)
the_handler.setLevel(log_level) the_handler.setLevel(log_level)

View File

@ -88,6 +88,7 @@ type OwnProps = {
onElementsChanged?: (..._args: any[]) => any; onElementsChanged?: (..._args: any[]) => any;
url?: string; url?: string;
callers?: ProcessModelCaller[]; callers?: ProcessModelCaller[];
activeUserElement?: React.ReactElement;
}; };
// https://codesandbox.io/s/quizzical-lake-szfyo?file=/src/App.js was a handy reference // https://codesandbox.io/s/quizzical-lake-szfyo?file=/src/App.js was a handy reference
@ -114,6 +115,7 @@ export default function ReactDiagramEditor({
onElementsChanged, onElementsChanged,
url, url,
callers, callers,
activeUserElement,
}: OwnProps) { }: OwnProps) {
const [diagramXMLString, setDiagramXMLString] = useState(''); const [diagramXMLString, setDiagramXMLString] = useState('');
const [diagramModelerState, setDiagramModelerState] = useState(null); const [diagramModelerState, setDiagramModelerState] = useState(null);
@ -672,6 +674,7 @@ export default function ReactDiagramEditor({
)} )}
</Can> </Can>
{getReferencesButton()} {getReferencesButton()}
{activeUserElement || null}
</ButtonSet> </ButtonSet>
); );
} }
@ -679,9 +682,9 @@ export default function ReactDiagramEditor({
}; };
return ( return (
<div> <>
{userActionOptions()} {userActionOptions()}
{showReferences()} {showReferences()}
</div> </>
); );
} }

View File

@ -459,6 +459,7 @@ svg.notification-icon {
.user-list { .user-list {
display: flex; display: flex;
align-items: center; align-items: center;
margin-left: 8px;
} }
.user-circle { .user-circle {

View File

@ -31,7 +31,11 @@ import HttpService, { getBasicHeaders } from '../services/HttpService';
import ReactDiagramEditor from '../components/ReactDiagramEditor'; import ReactDiagramEditor from '../components/ReactDiagramEditor';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import useAPIError from '../hooks/UseApiError'; import useAPIError from '../hooks/UseApiError';
import { makeid, modifyProcessIdentifierForPathParam } from '../helpers'; import {
makeid,
modifyProcessIdentifierForPathParam,
encodeBase64,
} from '../helpers';
import { import {
CarbonComboBoxProcessSelection, CarbonComboBoxProcessSelection,
ProcessFile, ProcessFile,
@ -128,8 +132,29 @@ export default function ProcessModelEditDiagram() {
usePrompt('Changes you made may not be saved.', diagramHasChanges); usePrompt('Changes you made may not be saved.', diagramHasChanges);
const lastVisitedIdentifier = encodeBase64(window.location.pathname);
useEffect(() => { 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( fetchEventSource(
`${BACKEND_BASE_URL}/active-users/updates/${lastVisitedIdentifier}`, `${BACKEND_BASE_URL}/active-users/updates/${lastVisitedIdentifier}`,
{ {
@ -143,52 +168,31 @@ export default function ProcessModelEditDiagram() {
} }
}, },
onclose() { onclose() {
HttpService.makeCallToBackend({ unregisterUser();
path: `/active-users/unregister/${lastVisitedIdentifier}`,
successCallback: setActiveUsers,
});
}, },
onerror(err: any) { onerror(err: any) {
throw err; 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 // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // it is critical to only run this once. }, []); // 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(() => { useEffect(() => {
const fileResult = (result: any) => { const fileResult = (result: any) => {
setProcessModelFile(result); setProcessModelFile(result);
setBpmnXmlForDiagramRendering(result.file_contents); setBpmnXmlForDiagramRendering(result.file_contents);
}; };
HttpService.makeCallToBackend({
path: `/${processModelPath}?include_file_references=true`,
successCallback: setProcessModel,
});
if (params.file_name) { if (params.file_name) {
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: `/${processModelPath}/files/${params.file_name}`, path: `/${processModelPath}/files/${params.file_name}`,
@ -962,6 +966,20 @@ export default function ProcessModelEditDiagram() {
return searchParams.get('file_type') === 'dmn' || fileName.endsWith('.dmn'); return searchParams.get('file_type') === 'dmn' || fileName.endsWith('.dmn');
}; };
const activeUserElement = () => {
const au = activeUsers.map((activeUser: User) => {
return (
<div
title={`${activeUser.username} is also viewing this page`}
className="user-circle"
>
{activeUser.username.charAt(0).toUpperCase()}
</div>
);
});
return <div className="user-list">{au}</div>;
};
const appropriateEditor = () => { const appropriateEditor = () => {
if (isDmn()) { if (isDmn()) {
return ( return (
@ -1006,6 +1024,7 @@ export default function ProcessModelEditDiagram() {
onSearchProcessModels={onSearchProcessModels} onSearchProcessModels={onSearchProcessModels}
onElementsChanged={onElementsChanged} onElementsChanged={onElementsChanged}
callers={callers} callers={callers}
activeUserElement={activeUserElement()}
/> />
); );
}; };
@ -1024,17 +1043,6 @@ export default function ProcessModelEditDiagram() {
return null; return null;
}; };
const activeUserElement = () => {
const au = activeUsers.map((activeUser: User) => {
return (
<div title={activeUser.username} className="user-circle">
{activeUser.username.charAt(0).toUpperCase()}
</div>
);
});
return <div className="user-list">{au}</div>;
};
// if a file name is not given then this is a new model and the ReactDiagramEditor component will handle it // 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) { if ((bpmnXmlForDiagramRendering || !params.file_name) && processModel) {
const processModelFileName = processModelFile ? processModelFile.name : ''; const processModelFileName = processModelFile ? processModelFile.name : '';
@ -1055,7 +1063,6 @@ export default function ProcessModelEditDiagram() {
Process Model File{processModelFile ? ': ' : ''} Process Model File{processModelFile ? ': ' : ''}
{processModelFileName} {processModelFileName}
</h1> </h1>
{activeUserElement()}
{saveFileMessage()} {saveFileMessage()}
{appropriateEditor()} {appropriateEditor()}
{newFileNameBox()} {newFileNameBox()}