diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py index 7d33b67d7..1234aaadc 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py @@ -530,6 +530,8 @@ def _create_or_update_process_model_file( file_contents = SpecFileService.get_data(process_model, file.name) file.file_contents = file_contents file.process_model_id = process_model.id + file_contents_hash = sha256(file_contents).hexdigest() + file.file_contents_hash = file_contents_hash _commit_and_push_to_git(f"{message_for_git_commit} {process_model_identifier}/{file.name}") return make_response(jsonify(file), http_status_to_return) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py index d5842d317..71823a8e9 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py @@ -581,6 +581,7 @@ class AuthorizationService: permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/processes/callers")) permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/service-tasks")) permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/user-groups/for-current-user")) + permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/active-users/*")) permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/users/exists/by-username")) permissions_to_assign.append( PermissionToAssign(permission="read", target_uri="/process-instances/find-by-id/*") diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py index 2d43963df..4d945c3fe 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py @@ -289,6 +289,7 @@ class TestAuthorizationService(BaseTest): ) -> None: """Test_explode_permissions_basic.""" expected_permissions = [ + ("/active-users/*", "read"), ("/process-instances/find-by-id/*", "read"), ("/process-instances/for-me", "create"), ("/process-instances/report-metadata", "read"), diff --git a/spiffworkflow-frontend/src/components/ActiveUsers.tsx b/spiffworkflow-frontend/src/components/ActiveUsers.tsx new file mode 100644 index 000000000..012f9f675 --- /dev/null +++ b/spiffworkflow-frontend/src/components/ActiveUsers.tsx @@ -0,0 +1,52 @@ +import { useEffect, useState } from 'react'; + +import HttpService from '../services/HttpService'; +import { + encodeBase64, + refreshAtInterval, + REFRESH_TIMEOUT_SECONDS, +} from '../helpers'; +import { User } from '../interfaces'; + +export default function ActiveUsers() { + // Handles getting and displaying active users. + const [activeUsers, setActiveUsers] = useState([]); + + const lastVisitedIdentifier = encodeBase64(window.location.pathname); + useEffect(() => { + const updateActiveUsers = () => { + HttpService.makeCallToBackend({ + path: `/active-users/updates/${lastVisitedIdentifier}`, + successCallback: setActiveUsers, + }); + }; + + const unregisterUser = () => { + HttpService.makeCallToBackend({ + path: `/active-users/unregister/${lastVisitedIdentifier}`, + successCallback: setActiveUsers, + }); + }; + updateActiveUsers(); + + return refreshAtInterval( + 15, + REFRESH_TIMEOUT_SECONDS, + updateActiveUsers, + unregisterUser + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); // it is critical to only run this once. + + const au = activeUsers.map((activeUser: User) => { + return ( +
+ {activeUser.username.charAt(0).toUpperCase()} +
+ ); + }); + return
{au}
; +} diff --git a/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx b/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx index 1855f955e..a95c43b7e 100644 --- a/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx +++ b/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx @@ -674,7 +674,14 @@ export default function ReactDiagramEditor({ )} {getReferencesButton()} - {activeUserElement || null} + {/* only show other users if the current user can save the current diagram */} + + {activeUserElement || null} + ); } diff --git a/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx b/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx index 152fc179e..0ba15f14d 100644 --- a/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx @@ -29,24 +29,18 @@ import HttpService from '../services/HttpService'; import ReactDiagramEditor from '../components/ReactDiagramEditor'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; import useAPIError from '../hooks/UseApiError'; -import { - makeid, - modifyProcessIdentifierForPathParam, - encodeBase64, - refreshAtInterval, - REFRESH_TIMEOUT_SECONDS, -} from '../helpers'; +import { makeid, modifyProcessIdentifierForPathParam } from '../helpers'; import { CarbonComboBoxProcessSelection, ProcessFile, ProcessModel, ProcessModelCaller, ProcessReference, - User, } from '../interfaces'; import ProcessSearch from '../components/ProcessSearch'; import { Notification } from '../components/Notification'; import { usePrompt } from '../hooks/UsePrompt'; +import ActiveUsers from '../components/ActiveUsers'; export default function ProcessModelEditDiagram() { const [showFileNameEditor, setShowFileNameEditor] = useState(false); @@ -73,7 +67,6 @@ export default function ProcessModelEditDiagram() { useState(false); const [processModelFileInvalidText, setProcessModelFileInvalidText] = useState(''); - const [activeUsers, setActiveUsers] = useState([]); const handleShowMarkdownEditor = () => setShowMarkdownEditor(true); @@ -132,14 +125,7 @@ export default function ProcessModelEditDiagram() { usePrompt('Changes you made may not be saved.', diagramHasChanges); - const lastVisitedIdentifier = encodeBase64(window.location.pathname); useEffect(() => { - const updateActiveUsers = () => { - HttpService.makeCallToBackend({ - path: `/active-users/updates/${lastVisitedIdentifier}`, - successCallback: setActiveUsers, - }); - }; // Grab all available process models in case we need to search for them. // Taken from the Process Group List const processResults = (result: any) => { @@ -154,21 +140,6 @@ export default function ProcessModelEditDiagram() { path: `/processes`, successCallback: processResults, }); - - const unregisterUser = () => { - HttpService.makeCallToBackend({ - path: `/active-users/unregister/${lastVisitedIdentifier}`, - successCallback: setActiveUsers, - }); - }; - updateActiveUsers(); - - return refreshAtInterval( - 15, - REFRESH_TIMEOUT_SECONDS, - updateActiveUsers, - unregisterUser - ); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // it is critical to only run this once. @@ -206,8 +177,11 @@ export default function ProcessModelEditDiagram() { setProcessModelFileInvalidText(''); }; - const navigateToProcessModelFile = (_result: any) => { + const navigateToProcessModelFile = (file: ProcessFile) => { setDisplaySaveFileMessage(true); + if (file.file_contents_hash) { + setProcessModelFile(file); + } if (!params.file_name) { const fileNameWithExtension = `${newFileName}.${searchParams.get( 'file_type' @@ -956,20 +930,6 @@ 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 ( @@ -1014,7 +974,7 @@ export default function ProcessModelEditDiagram() { onSearchProcessModels={onSearchProcessModels} onElementsChanged={onElementsChanged} callers={callers} - activeUserElement={activeUserElement()} + activeUserElement={} /> ); }; diff --git a/spiffworkflow-frontend/src/routes/ReactFormEditor.tsx b/spiffworkflow-frontend/src/routes/ReactFormEditor.tsx index e03253dad..e4e72290f 100644 --- a/spiffworkflow-frontend/src/routes/ReactFormEditor.tsx +++ b/spiffworkflow-frontend/src/routes/ReactFormEditor.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'; import Editor from '@monaco-editor/react'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; // @ts-ignore -import { Button, Modal } from '@carbon/react'; +import { Button, ButtonSet, Modal } from '@carbon/react'; import { Can } from '@casl/react'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; import HttpService from '../services/HttpService'; @@ -15,6 +15,7 @@ import { Notification } from '../components/Notification'; import useAPIError from '../hooks/UseApiError'; import { usePermissionFetcher } from '../hooks/PermissionService'; import { useUriListForPermissions } from '../hooks/UriListForPermissions'; +import ActiveUsers from '../components/ActiveUsers'; // NOTE: This is mostly the same as ProcessModelEditDiagram and if we go this route could // possibly be merged into it. I'm leaving as a separate file now in case it does // end up diverging greatly @@ -87,8 +88,11 @@ export default function ReactFormEditor() { } }, [params, modifiedProcessModelId]); - const navigateToProcessModelFile = (_result: any) => { + const navigateToProcessModelFile = (file: ProcessFile) => { setDisplaySaveFileMessage(true); + if (file.file_contents_hash) { + setProcessModelFile(file); + } if (!params.file_name) { const fileNameWithExtension = `${newFileName}.${fileExtension}`; navigate( @@ -110,6 +114,9 @@ export default function ReactFormEditor() { httpMethod = 'POST'; } else { url += `/${fileNameWithExtension}`; + if (processModelFile && processModelFile.file_contents_hash) { + url += `?file_contents_hash=${processModelFile.file_contents_hash}`; + } } if (!fileNameWithExtension) { handleShowFileNameEditor(); @@ -225,59 +232,80 @@ export default function ReactFormEditor() { {newFileNameBox()} {saveFileMessage()} - - - - - {params.file_name ? ( - - ) : null} - - - {hasFormBuilder ? ( - ) : null} - - - {hasDiagram ? ( - - ) : null} - + + + {params.file_name ? ( + + ) : null} + + + {hasFormBuilder ? ( + + ) : null} + + + {hasDiagram ? ( + + ) : null} + + + + +