diff --git a/spiffworkflow-backend/bin/git_commit_bpmn_models_repo b/spiffworkflow-backend/bin/git_commit_bpmn_models_repo index 13e18da9c..62fc0cab0 100755 --- a/spiffworkflow-backend/bin/git_commit_bpmn_models_repo +++ b/spiffworkflow-backend/bin/git_commit_bpmn_models_repo @@ -11,11 +11,12 @@ set -o errtrace -o errexit -o nounset -o pipefail bpmn_models_absolute_dir="$1" git_commit_message="$2" -git_commit_username="$3" -git_commit_email="$4" +git_branch="$3" +git_commit_username="$4" +git_commit_email="$5" -if [[ -z "${2:-}" ]]; then - >&2 echo "usage: $(basename "$0") [bpmn_models_absolute_dir] [git_commit_message]" +if [[ -z "${5:-}" ]]; then + >&2 echo "usage: $(basename "$0") [bpmn_models_absolute_dir] [git_commit_message] [git_branch] [git_commit_username] [git_commit_email]" exit 1 fi @@ -26,11 +27,8 @@ git add . if [ -z "$(git status --porcelain)" ]; then echo "No changes to commit" else - if [[ -n "$git_commit_username" ]]; then - git config --local user.name "$git_commit_username" - fi - if [[ -n "$git_commit_email" ]]; then - git config --local user.email "$git_commit_email" - fi + git config --local user.name "$git_commit_username" + git config --local user.email "$git_commit_email" git commit -m "$git_commit_message" + git push --set-upstream origin "$git_branch" fi diff --git a/spiffworkflow-backend/bin/start_keycloak b/spiffworkflow-backend/bin/start_keycloak index 32b502ca0..f76347da7 100755 --- a/spiffworkflow-backend/bin/start_keycloak +++ b/spiffworkflow-backend/bin/start_keycloak @@ -18,7 +18,19 @@ set -o errtrace -o errexit -o nounset -o pipefail if ! docker network inspect spiffworkflow > /dev/null 2>&1; then docker network create spiffworkflow fi -docker rm keycloak 2>/dev/null || echo 'no keycloak container found, safe to start new container' + +# https://stackoverflow.com/a/60579344/6090676 +container_name="keycloak" +if [[ -n "$(docker ps -qa -f name=$container_name)" ]]; then + echo ":: Found container - $container_name" + if [[ -n "$(docker ps -q -f name=$container_name)" ]]; then + echo ":: Stopping running container - $container_name" + docker stop $container_name + fi + echo ":: Removing stopped container - $container_name" + docker rm $container_name +fi + docker run \ -p 7002:8080 \ -d \ diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/development.py b/spiffworkflow-backend/src/spiffworkflow_backend/config/development.py index 15cbead83..39e10cb58 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/development.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/development.py @@ -17,5 +17,3 @@ GIT_CLONE_URL_FOR_PUBLISHING = environ.get( ) GIT_USERNAME = "sartography-automated-committer" GIT_USER_EMAIL = f"{GIT_USERNAME}@users.noreply.github.com" -GIT_BRANCH_TO_PUBLISH_TO = "main" -GIT_BRANCH = "main" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/testing.py b/spiffworkflow-backend/src/spiffworkflow_backend/config/testing.py index bbda9db9a..605c1bccc 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/testing.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/testing.py @@ -15,6 +15,7 @@ SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME = environ.get( SPIFFWORKFLOW_BACKEND_LOG_LEVEL = environ.get( "SPIFFWORKFLOW_BACKEND_LOG_LEVEL", default="debug" ) +GIT_COMMIT_ON_SAVE = False # NOTE: set this here since nox shoves tests and src code to # different places and this allows us to know exactly where we are at the start diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py index 9a6168361..cf41767bd 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -1,5 +1,6 @@ """APIs for dealing with process groups, process models, and process instances.""" import json +import os import random import re import string @@ -75,6 +76,7 @@ from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignme from spiffworkflow_backend.routes.user import verify_token from spiffworkflow_backend.services.authorization_service import AuthorizationService from spiffworkflow_backend.services.error_handling_service import ErrorHandlingService +from spiffworkflow_backend.services.file_system_service import FileSystemService from spiffworkflow_backend.services.git_service import GitService from spiffworkflow_backend.services.message_service import MessageService from spiffworkflow_backend.services.process_instance_processor import ( @@ -168,6 +170,9 @@ def process_group_add(body: dict) -> flask.wrappers.Response: """Add_process_group.""" process_group = ProcessGroup(**body) ProcessModelService.add_process_group(process_group) + commit_and_push_to_git( + f"User: {g.user.username} added process group {process_group.id}" + ) return make_response(jsonify(process_group), 201) @@ -175,6 +180,9 @@ def process_group_delete(modified_process_group_id: str) -> flask.wrappers.Respo """Process_group_delete.""" process_group_id = un_modify_modified_process_model_id(modified_process_group_id) ProcessModelService().process_group_delete(process_group_id) + commit_and_push_to_git( + f"User: {g.user.username} deleted process group {process_group_id}" + ) return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") @@ -192,6 +200,9 @@ def process_group_update( process_group_id = un_modify_modified_process_model_id(modified_process_group_id) process_group = ProcessGroup(id=process_group_id, **body_filtered) ProcessModelService.update_process_group(process_group) + commit_and_push_to_git( + f"User: {g.user.username} updated process group {process_group_id}" + ) return make_response(jsonify(process_group), 200) @@ -256,7 +267,10 @@ def process_group_move( new_process_group = ProcessModelService().process_group_move( original_process_group_id, new_location ) - return make_response(jsonify(new_process_group), 201) + commit_and_push_to_git( + f"User: {g.user.username} moved process group {original_process_group_id} to {new_process_group.id}" + ) + return make_response(jsonify(new_process_group), 200) def process_model_create( @@ -304,6 +318,9 @@ def process_model_create( ) ProcessModelService.add_process_model(process_model_info) + commit_and_push_to_git( + f"User: {g.user.username} created process model {process_model_info.id}" + ) return Response( json.dumps(ProcessModelInfoSchema().dump(process_model_info)), status=201, @@ -317,6 +334,9 @@ def process_model_delete( """Process_model_delete.""" process_model_identifier = modified_process_model_identifier.replace(":", "/") ProcessModelService().process_model_delete(process_model_identifier) + commit_and_push_to_git( + f"User: {g.user.username} deleted process model {process_model_identifier}" + ) return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") @@ -340,6 +360,9 @@ def process_model_update( process_model = get_process_model(process_model_identifier) ProcessModelService.update_process_model(process_model, body_filtered) + commit_and_push_to_git( + f"User: {g.user.username} updated process model {process_model_identifier}" + ) return ProcessModelInfoSchema().dump(process_model) @@ -371,7 +394,10 @@ def process_model_move( new_process_model = ProcessModelService().process_model_move( original_process_model_id, new_location ) - return make_response(jsonify(new_process_model), 201) + commit_and_push_to_git( + f"User: {g.user.username} moved process model {original_process_model_id} to {new_process_model.id}" + ) + return make_response(jsonify(new_process_model), 200) def process_model_publish( @@ -467,14 +493,9 @@ def process_model_file_update( ) SpecFileService.update_file(process_model, file_name, request_file_contents) - - if current_app.config["GIT_COMMIT_ON_SAVE"]: - git_output = GitService.commit( - message=f"User: {g.user.username} clicked save for {process_model_identifier}/{file_name}" - ) - current_app.logger.info(f"git output: {git_output}") - else: - current_app.logger.info("Git commit on save is disabled") + commit_and_push_to_git( + f"User: {g.user.username} clicked save for {process_model_identifier}/{file_name}" + ) return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") @@ -496,6 +517,9 @@ def process_model_file_delete( ) ) from exception + commit_and_push_to_git( + f"User: {g.user.username} deleted process model file {process_model_identifier}/{file_name}" + ) return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") @@ -517,6 +541,9 @@ def add_file(modified_process_model_identifier: str) -> flask.wrappers.Response: file_contents = SpecFileService.get_data(process_model, file.name) file.file_contents = file_contents file.process_model_id = process_model.id + commit_and_push_to_git( + f"User: {g.user.username} added process model file {process_model_identifier}/{file.name}" + ) return Response( json.dumps(FileSchema().dump(file)), status=201, mimetype="application/json" ) @@ -1484,7 +1511,25 @@ def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response task.data = spiff_task.data task.process_model_display_name = process_model.display_name task.process_model_identifier = process_model.id + process_model_with_form = process_model + refs = SpecFileService.get_references_for_process(process_model_with_form) + all_processes = [i.identifier for i in refs] + if task.process_identifier not in all_processes: + bpmn_file_full_path = ( + ProcessInstanceProcessor.bpmn_file_full_path_from_bpmn_process_identifier( + task.process_identifier + ) + ) + relative_path = os.path.relpath( + bpmn_file_full_path, start=FileSystemService.root_path() + ) + process_model_relative_path = os.path.dirname(relative_path) + process_model_with_form = ( + ProcessModelService.get_process_model_from_relative_path( + process_model_relative_path + ) + ) if task.type == "User Task": if not form_schema_file_name: @@ -1657,7 +1702,7 @@ def script_unit_test_create( extension_elements = None extension_elements_array = script_task_element.xpath( - "//bpmn:extensionElements", + ".//bpmn:extensionElements", namespaces={"bpmn": "http://www.omg.org/spec/BPMN/20100524/MODEL"}, ) if len(extension_elements_array) == 0: @@ -2038,3 +2083,12 @@ def update_task_data( status=200, mimetype="application/json", ) + + +def commit_and_push_to_git(message: str) -> None: + """Commit_and_push_to_git.""" + if current_app.config["GIT_COMMIT_ON_SAVE"]: + git_output = GitService.commit(message=message) + current_app.logger.info(f"git output: {git_output}") + else: + current_app.logger.info("Git commit on save is disabled") diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/git_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/git_service.py index 152aab1c0..8ef952c3c 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/git_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/git_service.py @@ -68,8 +68,17 @@ class GitService: return cls.run_shell_command_to_get_stdout(shell_command) @classmethod - def commit(cls, message: str, repo_path: Optional[str] = None) -> str: + def commit( + cls, + message: str, + repo_path: Optional[str] = None, + branch_name: Optional[str] = None, + ) -> str: """Commit.""" + cls.check_for_basic_configs() + branch_name_to_use = branch_name + if branch_name_to_use is None: + branch_name_to_use = current_app.config["GIT_BRANCH"] repo_path_to_use = repo_path if repo_path is None: repo_path_to_use = current_app.config["BPMN_SPEC_ABSOLUTE_DIR"] @@ -88,14 +97,25 @@ class GitService: shell_command_path, repo_path_to_use, message, + branch_name_to_use, git_username, git_email, ] return cls.run_shell_command_to_get_stdout(shell_command) @classmethod - def check_for_configs(cls) -> None: + def check_for_basic_configs(cls) -> None: + """Check_for_basic_configs.""" + if current_app.config["GIT_BRANCH"] is None: + raise MissingGitConfigsError( + "Missing config for GIT_BRANCH. " + "This is required for publishing process models" + ) + + @classmethod + def check_for_publish_configs(cls) -> None: """Check_for_configs.""" + cls.check_for_basic_configs() if current_app.config["GIT_BRANCH_TO_PUBLISH_TO"] is None: raise MissingGitConfigsError( "Missing config for GIT_BRANCH_TO_PUBLISH_TO. " @@ -148,7 +168,7 @@ class GitService: @classmethod def handle_web_hook(cls, webhook: dict) -> bool: """Handle_web_hook.""" - cls.check_for_configs() + cls.check_for_publish_configs() if "repository" not in webhook or "clone_url" not in webhook["repository"]: raise InvalidGitWebhookBodyError( @@ -184,7 +204,7 @@ class GitService: @classmethod def publish(cls, process_model_id: str, branch_to_update: str) -> str: """Publish.""" - cls.check_for_configs() + cls.check_for_publish_configs() source_process_model_root = FileSystemService.root_path() source_process_model_path = os.path.join( source_process_model_root, process_model_id @@ -233,10 +253,7 @@ class GitService: f"Request to publish changes to {process_model_id}, " f"from {g.user.username} on {current_app.config['ENV_IDENTIFIER']}" ) - cls.commit(commit_message, destination_process_root) - cls.run_shell_command( - ["git", "push", "--set-upstream", "origin", branch_to_pull_request] - ) + cls.commit(commit_message, destination_process_root, branch_to_pull_request) # build url for github page to open PR git_remote = cls.run_shell_command_to_get_stdout( diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index 64b0a7e90..a21baec0d 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -843,8 +843,8 @@ export default function ProcessInstanceListTable({ return null; }} shouldFilterItem={shouldFilterReportColumn} - placeholder="Choose a report column" - titleText="Report Column" + placeholder="Choose a column to show" + titleText="Column" /> ); } @@ -893,7 +893,7 @@ export default function ProcessInstanceListTable({ kind="ghost" size="sm" className={`button-tag-icon ${tagTypeClass}`} - title={`Edit ${reportColumnForEditing.accessor}`} + title={`Edit ${reportColumnForEditing.accessor} column`} onClick={() => { setReportColumnToOperateOn(reportColumnForEditing); setShowReportColumnForm(true); @@ -921,7 +921,7 @@ export default function ProcessInstanceListTable({ + + {canViewXml && ( + + )} + ); } diff --git a/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx b/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx index 1a5c751f7..4ef758480 100644 --- a/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx @@ -6,11 +6,11 @@ import { useSearchParams, } from 'react-router-dom'; // @ts-ignore -import { Button, Modal, Stack, Content } from '@carbon/react'; +import { Button, Modal, Content, Tabs, TabList, Tab, TabPanels, TabPanel } from '@carbon/react'; import Row from 'react-bootstrap/Row'; import Col from 'react-bootstrap/Col'; -import Editor from '@monaco-editor/react'; +import Editor, { DiffEditor } from '@monaco-editor/react'; import MDEditor from '@uiw/react-md-editor'; import ReactDiagramEditor from '../components/ReactDiagramEditor'; @@ -397,6 +397,13 @@ export default function ProcessModelEditDiagram() { }; }; + const jsonEditorOptions = () => { + return Object.assign(generalEditorOptions(), { + minimap: { enabled: false }, + folding: true + }); + } + const setPreviousScriptUnitTest = () => { resetUnitTextResult(); const newScriptIndex = currentScriptUnitTestIndex - 1; @@ -491,11 +498,32 @@ export default function ProcessModelEditDiagram() { } let errorContextElement = null; if (scriptUnitTestResult.context) { + errorStringElement = ( + Unexpected result. Please see the comparison below. + ); + let outputJson = '{}'; + if (currentScriptUnitTest) { + outputJson = JSON.stringify( + JSON.parse(currentScriptUnitTest.expectedOutputJson.value), + null, + ' ' + ); + } + const contextJson = JSON.stringify( + scriptUnitTestResult.context, + null, + ' ' + ); errorContextElement = ( - - Received unexpected output:{' '} - {JSON.stringify(scriptUnitTestResult.context)} - + ); } return ( @@ -539,19 +567,29 @@ export default function ProcessModelEditDiagram() { ); } + const inputJson = JSON.stringify( + JSON.parse(currentScriptUnitTest.inputJson.value), + null, + ' ' + ); + const outputJson = JSON.stringify( + JSON.parse(currentScriptUnitTest.expectedOutputJson.value), + null, + ' ' + ); + return (
-
); } return null; }; - const scriptEditor = () => { + return ( + + ); + }; + const scriptEditorAndTests = () => { let scriptName = ''; if (scriptElement) { scriptName = (scriptElement as any).di.bpmnElement.name; } - return ( - - {scriptUnitTestEditorElement()} + + + Script Editor + Unit Tests + + + {scriptEditor()} + {scriptUnitTestEditorElement()} + + ); }; @@ -858,7 +905,7 @@ export default function ProcessModelEditDiagram() { {appropriateEditor()} {newFileNameBox()} - {scriptEditor()} + {scriptEditorAndTests()} {markdownEditor()} {processModelSelector()}
diff --git a/spiffworkflow-frontend/src/routes/ReactFormEditor.tsx b/spiffworkflow-frontend/src/routes/ReactFormEditor.tsx index 5a4da3878..5d1f55279 100644 --- a/spiffworkflow-frontend/src/routes/ReactFormEditor.tsx +++ b/spiffworkflow-frontend/src/routes/ReactFormEditor.tsx @@ -36,7 +36,20 @@ export default function ReactFormEditor() { return searchParams.get('file_ext') ?? 'json'; })(); - const editorDefaultLanguage = fileExtension === 'md' ? 'markdown' : 'json'; + const hasDiagram = fileExtension === 'bpmn' || fileExtension === 'dmn'; + + const editorDefaultLanguage = (() => { + if (fileExtension === 'json') { + return 'json'; + } + if (hasDiagram) { + return 'xml'; + } + if (fileExtension === 'md') { + return 'markdown'; + } + return 'text'; + })(); const modifiedProcessModelId = modifyProcessIdentifierForPathParam( `${params.process_model_id}` @@ -193,6 +206,19 @@ export default function ReactFormEditor() { buttonLabel="Delete" /> ) : null} + {hasDiagram ? ( + + ) : null}