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 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):

View File

@ -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()

View File

@ -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:

View File

@ -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)

View File

@ -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({
)}
</Can>
{getReferencesButton()}
{activeUserElement || null}
</ButtonSet>
);
}
@ -679,9 +682,9 @@ export default function ReactDiagramEditor({
};
return (
<div>
<>
{userActionOptions()}
{showReferences()}
</div>
</>
);
}

View File

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

View File

@ -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 (
<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 = () => {
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 (
<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 ((bpmnXmlForDiagramRendering || !params.file_name) && processModel) {
const processModelFileName = processModelFile ? processModelFile.name : '';
@ -1055,7 +1063,6 @@ export default function ProcessModelEditDiagram() {
Process Model File{processModelFile ? ': ' : ''}
{processModelFileName}
</h1>
{activeUserElement()}
{saveFileMessage()}
{appropriateEditor()}
{newFileNameBox()}