cleaned up displaying active users in frontend w/ burnettk
This commit is contained in:
parent
f7e8fd0022
commit
9320ec3cf9
|
@ -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):
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()}
|
||||||
|
|
Loading…
Reference in New Issue