Merge pull request #70 from sartography/feature/git-integration
Feature/git integration
This commit is contained in:
commit
9c59426fbd
5
.flake8
5
.flake8
|
@ -27,3 +27,8 @@ per-file-ignores =
|
||||||
# this file overwrites methods from the logging library so we can't change them
|
# this file overwrites methods from the logging library so we can't change them
|
||||||
# and ignore long comment line
|
# and ignore long comment line
|
||||||
spiffworkflow-backend/src/spiffworkflow_backend/services/logging_service.py:N802,B950
|
spiffworkflow-backend/src/spiffworkflow_backend/services/logging_service.py:N802,B950
|
||||||
|
|
||||||
|
# TODO: fix the S issues:
|
||||||
|
# S607 Starting a process with a partial executable path
|
||||||
|
# S605 Starting a process with a shell: Seems safe, but may be changed in the future, consider rewriting without shell
|
||||||
|
spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py:S607,S101,D103,S605
|
||||||
|
|
|
@ -27,3 +27,5 @@ per-file-ignores =
|
||||||
# this file overwrites methods from the logging library so we can't change them
|
# this file overwrites methods from the logging library so we can't change them
|
||||||
# and ignore long comment line
|
# and ignore long comment line
|
||||||
src/spiffworkflow_backend/services/logging_service.py:N802,B950
|
src/spiffworkflow_backend/services/logging_service.py:N802,B950
|
||||||
|
|
||||||
|
tests/spiffworkflow_backend/integration/test_process_api.py:S607,S101,D103,S605
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
"""Grabs tickets from csv and makes process instances."""
|
"""Grabs tickets from csv and makes process instances."""
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from spiffworkflow_backend import get_hacked_up_app_for_script
|
from spiffworkflow_backend import create_app
|
||||||
from spiffworkflow_backend.services.data_setup_service import DataSetupService
|
from spiffworkflow_backend.services.data_setup_service import DataSetupService
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
"""Main."""
|
"""Main."""
|
||||||
app = get_hacked_up_app_for_script()
|
app = create_app()
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
failing_process_models = DataSetupService.save_all_process_models()
|
failing_process_models = DataSetupService.save_all_process_models()
|
||||||
for bpmn_errors in failing_process_models:
|
for bpmn_errors in failing_process_models:
|
||||||
|
|
|
@ -445,6 +445,32 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/ProcessModel"
|
$ref: "#/components/schemas/ProcessModel"
|
||||||
|
|
||||||
|
/process-models/{modified_process_model_identifier}/publish:
|
||||||
|
parameters:
|
||||||
|
- name: modified_process_model_identifier
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: the modified process model id
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: branch_to_update
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
description: the name of the branch we want to merge into
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
post:
|
||||||
|
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_model_publish
|
||||||
|
summary: Merge changes from this model to another branch.
|
||||||
|
tags:
|
||||||
|
- Process Models
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The process model was published.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
|
||||||
/processes:
|
/processes:
|
||||||
get:
|
get:
|
||||||
|
|
|
@ -38,6 +38,17 @@ def setup_database_uri(app: Flask) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_config_file(app: Flask, env_config_module: str) -> None:
|
||||||
|
"""Load_config_file."""
|
||||||
|
try:
|
||||||
|
app.config.from_object(env_config_module)
|
||||||
|
except ImportStringError as exception:
|
||||||
|
if os.environ.get("TERRAFORM_DEPLOYED_ENVIRONMENT") != "true":
|
||||||
|
raise ModuleNotFoundError(
|
||||||
|
f"Cannot find config module: {env_config_module}"
|
||||||
|
) from exception
|
||||||
|
|
||||||
|
|
||||||
def setup_config(app: Flask) -> None:
|
def setup_config(app: Flask) -> None:
|
||||||
"""Setup_config."""
|
"""Setup_config."""
|
||||||
# ensure the instance folder exists
|
# ensure the instance folder exists
|
||||||
|
@ -53,19 +64,14 @@ def setup_config(app: Flask) -> None:
|
||||||
app.config.from_object("spiffworkflow_backend.config.default")
|
app.config.from_object("spiffworkflow_backend.config.default")
|
||||||
|
|
||||||
env_config_prefix = "spiffworkflow_backend.config."
|
env_config_prefix = "spiffworkflow_backend.config."
|
||||||
|
if (
|
||||||
|
os.environ.get("TERRAFORM_DEPLOYED_ENVIRONMENT") == "true"
|
||||||
|
and os.environ.get("SPIFFWORKFLOW_BACKEND_ENV") is not None
|
||||||
|
):
|
||||||
|
load_config_file(app, f"{env_config_prefix}terraform_deployed_environment")
|
||||||
|
|
||||||
env_config_module = env_config_prefix + app.config["ENV_IDENTIFIER"]
|
env_config_module = env_config_prefix + app.config["ENV_IDENTIFIER"]
|
||||||
try:
|
load_config_file(app, env_config_module)
|
||||||
app.config.from_object(env_config_module)
|
|
||||||
except ImportStringError as exception:
|
|
||||||
if (
|
|
||||||
os.environ.get("TERRAFORM_DEPLOYED_ENVIRONMENT") == "true"
|
|
||||||
and os.environ.get("SPIFFWORKFLOW_BACKEND_ENV") is not None
|
|
||||||
):
|
|
||||||
app.config.from_object(f"{env_config_prefix}terraform_deployed_environment")
|
|
||||||
else:
|
|
||||||
raise ModuleNotFoundError(
|
|
||||||
f"Cannot find config module: {env_config_module}"
|
|
||||||
) from exception
|
|
||||||
|
|
||||||
# This allows config/testing.py or instance/config.py to override the default config
|
# This allows config/testing.py or instance/config.py to override the default config
|
||||||
if "ENV_IDENTIFIER" in app.config and app.config["ENV_IDENTIFIER"] == "testing":
|
if "ENV_IDENTIFIER" in app.config and app.config["ENV_IDENTIFIER"] == "testing":
|
||||||
|
|
|
@ -61,6 +61,10 @@ SPIFFWORKFLOW_BACKEND_LOG_LEVEL = environ.get(
|
||||||
"SPIFFWORKFLOW_BACKEND_LOG_LEVEL", default="info"
|
"SPIFFWORKFLOW_BACKEND_LOG_LEVEL", default="info"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# When a user clicks on the `Publish` button, this is the default branch this server merges into.
|
||||||
|
# I.e., dev server could have `staging` here. Staging server might have `production` here.
|
||||||
|
GIT_MERGE_BRANCH = environ.get("GIT_MERGE_BRANCH", default="staging")
|
||||||
|
|
||||||
# Datbase Configuration
|
# Datbase Configuration
|
||||||
SPIFF_DATABASE_TYPE = environ.get(
|
SPIFF_DATABASE_TYPE = environ.get(
|
||||||
"SPIFF_DATABASE_TYPE", default="mysql"
|
"SPIFF_DATABASE_TYPE", default="mysql"
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
from os import environ
|
from os import environ
|
||||||
|
|
||||||
GIT_COMMIT_ON_SAVE = True
|
GIT_COMMIT_ON_SAVE = True
|
||||||
GIT_COMMIT_USERNAME = "demo"
|
GIT_USERNAME = "demo"
|
||||||
GIT_COMMIT_EMAIL = "demo@example.com"
|
GIT_USER_EMAIL = "demo@example.com"
|
||||||
SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME = environ.get(
|
SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME = environ.get(
|
||||||
"SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME",
|
"SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME",
|
||||||
default="terraform_deployed_environment.yml",
|
default="terraform_deployed_environment.yml",
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
"""Dev."""
|
||||||
|
from os import environ
|
||||||
|
|
||||||
|
GIT_MERGE_BRANCH = environ.get("GIT_MERGE_BRANCH", default="staging")
|
||||||
|
GIT_USERNAME = environ.get("GIT_USERNAME", default="sartography-automated-committer")
|
||||||
|
GIT_USER_EMAIL = environ.get(
|
||||||
|
"GIT_USER_EMAIL", default="sartography-automated-committer@users.noreply.github.com"
|
||||||
|
)
|
|
@ -5,8 +5,8 @@ from os import environ
|
||||||
environment_identifier_for_this_config_file_only = environ["SPIFFWORKFLOW_BACKEND_ENV"]
|
environment_identifier_for_this_config_file_only = environ["SPIFFWORKFLOW_BACKEND_ENV"]
|
||||||
|
|
||||||
GIT_COMMIT_ON_SAVE = True
|
GIT_COMMIT_ON_SAVE = True
|
||||||
GIT_COMMIT_USERNAME = environment_identifier_for_this_config_file_only
|
GIT_USERNAME = environment_identifier_for_this_config_file_only
|
||||||
GIT_COMMIT_EMAIL = f"{environment_identifier_for_this_config_file_only}@example.com"
|
GIT_USER_EMAIL = f"{environment_identifier_for_this_config_file_only}@example.com"
|
||||||
SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME = environ.get(
|
SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME = environ.get(
|
||||||
"SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME",
|
"SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME",
|
||||||
default="terraform_deployed_environment.yml",
|
default="terraform_deployed_environment.yml",
|
||||||
|
|
|
@ -370,6 +370,20 @@ def process_model_move(
|
||||||
return make_response(jsonify(new_process_model), 201)
|
return make_response(jsonify(new_process_model), 201)
|
||||||
|
|
||||||
|
|
||||||
|
def process_model_publish(
|
||||||
|
modified_process_model_identifier: str, branch_to_update: Optional[str] = None
|
||||||
|
) -> flask.wrappers.Response:
|
||||||
|
"""Process_model_publish."""
|
||||||
|
if branch_to_update is None:
|
||||||
|
branch_to_update = current_app.config["GIT_MERGE_BRANCH"]
|
||||||
|
process_model_identifier = un_modify_modified_process_model_id(
|
||||||
|
modified_process_model_identifier
|
||||||
|
)
|
||||||
|
pr_url = GitService().publish(process_model_identifier, branch_to_update)
|
||||||
|
data = {"ok": True, "pr_url": pr_url}
|
||||||
|
return Response(json.dumps(data), status=200, mimetype="application/json")
|
||||||
|
|
||||||
|
|
||||||
def process_model_list(
|
def process_model_list(
|
||||||
process_group_identifier: Optional[str] = None,
|
process_group_identifier: Optional[str] = None,
|
||||||
recursive: Optional[bool] = False,
|
recursive: Optional[bool] = False,
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
"""File_system_service."""
|
"""File_system_service."""
|
||||||
import os
|
import os
|
||||||
|
from contextlib import contextmanager
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List
|
from typing import Generator, List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
|
@ -23,13 +24,25 @@ class FileSystemService:
|
||||||
PROCESS_GROUP_JSON_FILE = "process_group.json"
|
PROCESS_GROUP_JSON_FILE = "process_group.json"
|
||||||
PROCESS_MODEL_JSON_FILE = "process_model.json"
|
PROCESS_MODEL_JSON_FILE = "process_model.json"
|
||||||
|
|
||||||
|
# https://stackoverflow.com/a/24176022/6090676
|
||||||
|
@staticmethod
|
||||||
|
@contextmanager
|
||||||
|
def cd(newdir: str) -> Generator:
|
||||||
|
"""Cd."""
|
||||||
|
prevdir = os.getcwd()
|
||||||
|
os.chdir(os.path.expanduser(newdir))
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
os.chdir(prevdir)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def root_path() -> str:
|
def root_path() -> str:
|
||||||
"""Root_path."""
|
"""Root_path."""
|
||||||
# fixme: allow absolute files
|
# fixme: allow absolute files
|
||||||
dir_name = current_app.config["BPMN_SPEC_ABSOLUTE_DIR"]
|
dir_name = current_app.config["BPMN_SPEC_ABSOLUTE_DIR"]
|
||||||
app_root = current_app.root_path
|
app_root = current_app.root_path
|
||||||
return os.path.join(app_root, "..", dir_name)
|
return os.path.abspath(os.path.join(app_root, "..", dir_name))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def id_string_to_relative_path(id_string: str) -> str:
|
def id_string_to_relative_path(id_string: str) -> str:
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
"""Git_service."""
|
"""Git_service."""
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
|
import uuid
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
from flask import g
|
||||||
|
|
||||||
from spiffworkflow_backend.models.process_model import ProcessModelInfo
|
from spiffworkflow_backend.models.process_model import ProcessModelInfo
|
||||||
from spiffworkflow_backend.services.file_system_service import FileSystemService
|
from spiffworkflow_backend.services.file_system_service import FileSystemService
|
||||||
|
@ -40,17 +44,74 @@ class GitService:
|
||||||
return file_contents.encode("utf-8")
|
return file_contents.encode("utf-8")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def commit(message: str) -> str:
|
def commit(message: str, repo_path: Optional[str] = None) -> str:
|
||||||
"""Commit."""
|
"""Commit."""
|
||||||
bpmn_spec_absolute_dir = current_app.config["BPMN_SPEC_ABSOLUTE_DIR"]
|
repo_path_to_use = repo_path
|
||||||
|
if repo_path is None:
|
||||||
|
repo_path_to_use = current_app.config["BPMN_SPEC_ABSOLUTE_DIR"]
|
||||||
|
|
||||||
git_username = ""
|
git_username = ""
|
||||||
git_email = ""
|
git_email = ""
|
||||||
if (
|
if current_app.config["GIT_USERNAME"] and current_app.config["GIT_USER_EMAIL"]:
|
||||||
current_app.config["GIT_COMMIT_USERNAME"]
|
git_username = current_app.config["GIT_USERNAME"]
|
||||||
and current_app.config["GIT_COMMIT_EMAIL"]
|
git_email = current_app.config["GIT_USER_EMAIL"]
|
||||||
):
|
shell_command_path = os.path.join(
|
||||||
git_username = current_app.config["GIT_COMMIT_USERNAME"]
|
current_app.root_path, "..", "..", "bin", "git_commit_bpmn_models_repo"
|
||||||
git_email = current_app.config["GIT_COMMIT_EMAIL"]
|
)
|
||||||
shell_command = f"./bin/git_commit_bpmn_models_repo '{bpmn_spec_absolute_dir}' '{message}' '{git_username}' '{git_email}'"
|
shell_command = f"{shell_command_path} '{repo_path_to_use}' '{message}' '{git_username}' '{git_email}'"
|
||||||
output = os.popen(shell_command).read() # noqa: S605
|
output = os.popen(shell_command).read() # noqa: S605
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def publish(cls, process_model_id: str, branch_to_update: str) -> str:
|
||||||
|
"""Publish."""
|
||||||
|
source_process_model_root = FileSystemService.root_path()
|
||||||
|
source_process_model_path = os.path.join(
|
||||||
|
source_process_model_root, process_model_id
|
||||||
|
)
|
||||||
|
unique_hex = uuid.uuid4().hex
|
||||||
|
clone_dir = f"sample-process-models.{unique_hex}"
|
||||||
|
|
||||||
|
# clone new instance of sample-process-models, checkout branch_to_update
|
||||||
|
# we are adding a guid to this so the flake8 issue has been mitigated
|
||||||
|
destination_process_root = f"/tmp/{clone_dir}" # noqa
|
||||||
|
|
||||||
|
cmd = (
|
||||||
|
f"git clone https://{current_app.config['GIT_USERNAME']}:{current_app.config['GIT_USER_PASSWORD']}"
|
||||||
|
f"@github.com/sartography/sample-process-models.git {destination_process_root}"
|
||||||
|
)
|
||||||
|
os.system(cmd) # noqa: S605
|
||||||
|
with FileSystemService.cd(destination_process_root):
|
||||||
|
# create publish branch from branch_to_update
|
||||||
|
os.system(f"git checkout {branch_to_update}") # noqa: S605
|
||||||
|
publish_branch = f"publish-{process_model_id}"
|
||||||
|
command = f"git show-ref --verify refs/remotes/origin/{publish_branch}"
|
||||||
|
output = os.popen(command).read() # noqa: S605
|
||||||
|
if output:
|
||||||
|
os.system(f"git checkout {publish_branch}") # noqa: S605
|
||||||
|
else:
|
||||||
|
os.system(f"git checkout -b {publish_branch}") # noqa: S605
|
||||||
|
|
||||||
|
# copy files from process model into the new publish branch
|
||||||
|
destination_process_model_path = os.path.join(
|
||||||
|
destination_process_root, process_model_id
|
||||||
|
)
|
||||||
|
if os.path.exists(destination_process_model_path):
|
||||||
|
shutil.rmtree(destination_process_model_path)
|
||||||
|
shutil.copytree(source_process_model_path, destination_process_model_path)
|
||||||
|
|
||||||
|
# add and commit files to publish_branch, then push
|
||||||
|
commit_message = f"Request to publish changes to {process_model_id}, from {g.user.username}"
|
||||||
|
cls.commit(commit_message, destination_process_root)
|
||||||
|
os.system("git push") # noqa
|
||||||
|
|
||||||
|
# build url for github page to open PR
|
||||||
|
output = os.popen("git config --get remote.origin.url").read() # noqa
|
||||||
|
remote_url = output.strip().replace(".git", "")
|
||||||
|
pr_url = f"{remote_url}/compare/{publish_branch}?expand=1"
|
||||||
|
|
||||||
|
# try to clean up
|
||||||
|
if os.path.exists(destination_process_root):
|
||||||
|
shutil.rmtree(destination_process_root)
|
||||||
|
|
||||||
|
return pr_url
|
||||||
|
|
|
@ -2555,6 +2555,123 @@ class TestProcessApi(BaseTest):
|
||||||
new_process_group = ProcessModelService.get_process_group(new_sub_path)
|
new_process_group = ProcessModelService.get_process_group(new_sub_path)
|
||||||
assert new_process_group.id == new_sub_path
|
assert new_process_group.id == new_sub_path
|
||||||
|
|
||||||
|
def test_process_model_publish(
|
||||||
|
self,
|
||||||
|
app: Flask,
|
||||||
|
client: FlaskClient,
|
||||||
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
|
with_super_admin_user: UserModel,
|
||||||
|
) -> None:
|
||||||
|
"""Test_process_model_publish."""
|
||||||
|
bpmn_root = FileSystemService.root_path()
|
||||||
|
shell_command = f"git init {bpmn_root}"
|
||||||
|
output = os.popen(shell_command).read() # noqa: S605
|
||||||
|
assert output == f"Initialized empty Git repository in {bpmn_root}/.git/\n"
|
||||||
|
os.chdir(bpmn_root)
|
||||||
|
output = os.popen("git status").read() # noqa: S605
|
||||||
|
assert "On branch main" in output
|
||||||
|
assert "No commits yet" in output
|
||||||
|
assert (
|
||||||
|
'nothing to commit (create/copy files and use "git add" to track)' in output
|
||||||
|
)
|
||||||
|
|
||||||
|
process_group_id = "test_group"
|
||||||
|
self.create_process_group(
|
||||||
|
client, with_super_admin_user, process_group_id, process_group_id
|
||||||
|
)
|
||||||
|
|
||||||
|
sub_process_group_id = "test_group/test_sub_group"
|
||||||
|
process_model_id = "hello_world"
|
||||||
|
bpmn_file_name = "hello_world.bpmn"
|
||||||
|
bpmn_file_location = "hello_world"
|
||||||
|
process_model_identifier = self.create_group_and_model_with_bpmn(
|
||||||
|
client=client,
|
||||||
|
user=with_super_admin_user,
|
||||||
|
process_group_id=sub_process_group_id,
|
||||||
|
process_model_id=process_model_id,
|
||||||
|
bpmn_file_name=bpmn_file_name,
|
||||||
|
bpmn_file_location=bpmn_file_location,
|
||||||
|
)
|
||||||
|
process_model_absolute_dir = os.path.join(bpmn_root, process_model_identifier)
|
||||||
|
|
||||||
|
output = os.popen("git status").read() # noqa: S605
|
||||||
|
test_string = 'Untracked files:\n (use "git add <file>..." to include in what will be committed)\n\ttest_group'
|
||||||
|
assert test_string in output
|
||||||
|
|
||||||
|
os.system("git add .")
|
||||||
|
output = os.popen("git commit -m 'Initial Commit'").read()
|
||||||
|
assert "Initial Commit" in output
|
||||||
|
assert "4 files changed" in output
|
||||||
|
assert "test_group/process_group.json" in output
|
||||||
|
assert "test_group/test_sub_group/hello_world/hello_world.bpmn" in output
|
||||||
|
assert "test_group/test_sub_group/hello_world/process_model.json" in output
|
||||||
|
assert "test_group/test_sub_group/process_group.json" in output
|
||||||
|
|
||||||
|
output = os.popen("git status").read() # noqa: S605
|
||||||
|
assert "On branch main" in output
|
||||||
|
assert "nothing to commit" in output
|
||||||
|
assert "working tree clean" in output
|
||||||
|
|
||||||
|
output = os.popen("git branch --list").read() # noqa: S605
|
||||||
|
assert output == "* main\n"
|
||||||
|
os.system("git branch staging")
|
||||||
|
output = os.popen("git branch --list").read() # noqa: S605
|
||||||
|
assert output == "* main\n staging\n"
|
||||||
|
|
||||||
|
os.system("git checkout staging")
|
||||||
|
|
||||||
|
output = os.popen("git status").read() # noqa: S605
|
||||||
|
assert "On branch staging" in output
|
||||||
|
assert "nothing to commit" in output
|
||||||
|
assert "working tree clean" in output
|
||||||
|
|
||||||
|
# process_model = ProcessModelService.get_process_model(process_model_identifier)
|
||||||
|
|
||||||
|
listing = os.listdir(process_model_absolute_dir)
|
||||||
|
assert len(listing) == 2
|
||||||
|
assert "hello_world.bpmn" in listing
|
||||||
|
assert "process_model.json" in listing
|
||||||
|
|
||||||
|
os.system("git checkout main")
|
||||||
|
|
||||||
|
output = os.popen("git status").read() # noqa: S605
|
||||||
|
assert "On branch main" in output
|
||||||
|
assert "nothing to commit" in output
|
||||||
|
assert "working tree clean" in output
|
||||||
|
|
||||||
|
file_data = b"abc123"
|
||||||
|
new_file_path = os.path.join(process_model_absolute_dir, "new_file.txt")
|
||||||
|
with open(new_file_path, "wb") as f_open:
|
||||||
|
f_open.write(file_data)
|
||||||
|
|
||||||
|
output = os.popen("git status").read() # noqa: S605
|
||||||
|
assert "On branch main" in output
|
||||||
|
assert "Untracked files:" in output
|
||||||
|
assert "test_group/test_sub_group/hello_world/new_file.txt" in output
|
||||||
|
|
||||||
|
os.system(
|
||||||
|
"git add test_group/test_sub_group/hello_world/new_file.txt"
|
||||||
|
) # noqa: S605
|
||||||
|
output = os.popen("git commit -m 'add new_file.txt'").read() # noqa: S605
|
||||||
|
|
||||||
|
assert "add new_file.txt" in output
|
||||||
|
assert "1 file changed, 1 insertion(+)" in output
|
||||||
|
assert "test_group/test_sub_group/hello_world/new_file.txt" in output
|
||||||
|
|
||||||
|
listing = os.listdir(process_model_absolute_dir)
|
||||||
|
assert len(listing) == 3
|
||||||
|
assert "hello_world.bpmn" in listing
|
||||||
|
assert "process_model.json" in listing
|
||||||
|
assert "new_file.txt" in listing
|
||||||
|
|
||||||
|
# modified_process_model_id = process_model_identifier.replace("/", ":")
|
||||||
|
# response = client.post(
|
||||||
|
# f"/v1.0/process-models/{modified_process_model_id}/publish?branch_to_update=staging",
|
||||||
|
# headers=self.logged_in_headers(with_super_admin_user),
|
||||||
|
# )
|
||||||
|
|
||||||
|
print("test_process_model_publish")
|
||||||
|
|
||||||
def test_can_get_process_instance_list_with_report_metadata(
|
def test_can_get_process_instance_list_with_report_metadata(
|
||||||
self,
|
self,
|
||||||
app: Flask,
|
app: Flask,
|
||||||
|
|
|
@ -29,4 +29,4 @@ cypress/screenshots
|
||||||
/test*.json
|
/test*.json
|
||||||
|
|
||||||
# Editors
|
# Editors
|
||||||
.idea
|
.idea
|
||||||
|
|
|
@ -41,4 +41,3 @@
|
||||||
-->
|
-->
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
import React from 'react';
|
||||||
|
// @ts-ignore
|
||||||
|
import { Close, CheckmarkFilled } from '@carbon/icons-react';
|
||||||
|
// @ts-ignore
|
||||||
|
import { Button } from '@carbon/react';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
title: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
onClose: (..._args: any[]) => any;
|
||||||
|
type?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Notification({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
onClose,
|
||||||
|
type = 'success',
|
||||||
|
}: OwnProps) {
|
||||||
|
let iconClassName = 'green-icon';
|
||||||
|
if (type === 'error') {
|
||||||
|
iconClassName = 'red-icon';
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="status"
|
||||||
|
className={`with-bottom-margin cds--inline-notification cds--inline-notification--low-contrast cds--inline-notification--${type}`}
|
||||||
|
>
|
||||||
|
<div className="cds--inline-notification__details">
|
||||||
|
<div className="cds--inline-notification__text-wrapper">
|
||||||
|
<CheckmarkFilled className={`${iconClassName} notification-icon`} />
|
||||||
|
<div className="cds--inline-notification__title">{title}</div>
|
||||||
|
<div className="cds--inline-notification__subtitle">{children}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
data-qa="close-publish-notification"
|
||||||
|
renderIcon={Close}
|
||||||
|
iconDescription="Close Notification"
|
||||||
|
className="cds--inline-notification__close-button"
|
||||||
|
hasIconOnly
|
||||||
|
size="sm"
|
||||||
|
kind=""
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -22,7 +22,6 @@ import {
|
||||||
TableRow,
|
TableRow,
|
||||||
TimePicker,
|
TimePicker,
|
||||||
Tag,
|
Tag,
|
||||||
InlineNotification,
|
|
||||||
Stack,
|
Stack,
|
||||||
Modal,
|
Modal,
|
||||||
ComboBox,
|
ComboBox,
|
||||||
|
@ -65,6 +64,7 @@ import ProcessModelSearch from './ProcessModelSearch';
|
||||||
import ProcessInstanceReportSearch from './ProcessInstanceReportSearch';
|
import ProcessInstanceReportSearch from './ProcessInstanceReportSearch';
|
||||||
import ProcessInstanceListSaveAsReport from './ProcessInstanceListSaveAsReport';
|
import ProcessInstanceListSaveAsReport from './ProcessInstanceListSaveAsReport';
|
||||||
import { FormatProcessModelDisplayName } from './MiniComponents';
|
import { FormatProcessModelDisplayName } from './MiniComponents';
|
||||||
|
import { Notification } from './Notification';
|
||||||
|
|
||||||
const REFRESH_INTERVAL = 5;
|
const REFRESH_INTERVAL = 5;
|
||||||
const REFRESH_TIMEOUT = 600;
|
const REFRESH_TIMEOUT = 600;
|
||||||
|
@ -372,18 +372,16 @@ export default function ProcessInstanceListTable({
|
||||||
titleOperation = 'Created';
|
titleOperation = 'Created';
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<Notification
|
||||||
<InlineNotification
|
title={`Perspective: ${titleOperation}`}
|
||||||
title={`Perspective ${titleOperation}:`}
|
onClose={() => setProcessInstanceReportJustSaved(null)}
|
||||||
subtitle={`'${
|
>
|
||||||
processInstanceReportSelection
|
<span>{`'${
|
||||||
? processInstanceReportSelection.identifier
|
processInstanceReportSelection
|
||||||
: ''
|
? processInstanceReportSelection.identifier
|
||||||
}'`}
|
: ''
|
||||||
kind="success"
|
}'`}</span>
|
||||||
/>
|
</Notification>
|
||||||
<br />
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -18,6 +18,7 @@ export const useUriListForPermissions = () => {
|
||||||
processModelCreatePath: `/v1.0/process-models/${params.process_group_id}`,
|
processModelCreatePath: `/v1.0/process-models/${params.process_group_id}`,
|
||||||
processModelFileCreatePath: `/v1.0/process-models/${params.process_model_id}/files`,
|
processModelFileCreatePath: `/v1.0/process-models/${params.process_model_id}/files`,
|
||||||
processModelFileShowPath: `/v1.0/process-models/${params.process_model_id}/files/${params.file_name}`,
|
processModelFileShowPath: `/v1.0/process-models/${params.process_model_id}/files/${params.file_name}`,
|
||||||
|
processModelPublishPath: `/v1.0/process-models/${params.process_model_id}/publish`,
|
||||||
processModelShowPath: `/v1.0/process-models/${params.process_model_id}`,
|
processModelShowPath: `/v1.0/process-models/${params.process_model_id}`,
|
||||||
secretListPath: `/v1.0/secrets`,
|
secretListPath: `/v1.0/secrets`,
|
||||||
};
|
};
|
||||||
|
|
|
@ -332,6 +332,14 @@ td.actions-cell {
|
||||||
fill: red;
|
fill: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
svg.green-icon {
|
||||||
|
fill: #198038;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg.notification-icon {
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.failure-string {
|
.failure-string {
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
@ -358,7 +366,7 @@ td.actions-cell {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* lime green */
|
||||||
.tag-type-green:hover {
|
.tag-type-green:hover {
|
||||||
background-color: #00FF00;
|
background-color: #80ee90;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,7 @@ import ProcessInstanceListTable from '../components/ProcessInstanceListTable';
|
||||||
import { usePermissionFetcher } from '../hooks/PermissionService';
|
import { usePermissionFetcher } from '../hooks/PermissionService';
|
||||||
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
|
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
|
||||||
import ProcessInstanceRun from '../components/ProcessInstanceRun';
|
import ProcessInstanceRun from '../components/ProcessInstanceRun';
|
||||||
|
import { Notification } from '../components/Notification';
|
||||||
|
|
||||||
export default function ProcessModelShow() {
|
export default function ProcessModelShow() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
@ -60,11 +61,14 @@ export default function ProcessModelShow() {
|
||||||
const [filesToUpload, setFilesToUpload] = useState<any>(null);
|
const [filesToUpload, setFilesToUpload] = useState<any>(null);
|
||||||
const [showFileUploadModal, setShowFileUploadModal] =
|
const [showFileUploadModal, setShowFileUploadModal] =
|
||||||
useState<boolean>(false);
|
useState<boolean>(false);
|
||||||
|
const [processModelPublished, setProcessModelPublished] = useState<any>(null);
|
||||||
|
const [publishDisabled, setPublishDisabled] = useState<boolean>(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { targetUris } = useUriListForPermissions();
|
const { targetUris } = useUriListForPermissions();
|
||||||
const permissionRequestData: PermissionsToCheck = {
|
const permissionRequestData: PermissionsToCheck = {
|
||||||
[targetUris.processModelShowPath]: ['PUT', 'DELETE'],
|
[targetUris.processModelShowPath]: ['PUT', 'DELETE'],
|
||||||
|
[targetUris.processModelPublishPath]: ['POST'],
|
||||||
[targetUris.processInstanceListPath]: ['GET'],
|
[targetUris.processInstanceListPath]: ['GET'],
|
||||||
[targetUris.processInstanceCreatePath]: ['POST'],
|
[targetUris.processInstanceCreatePath]: ['POST'],
|
||||||
[targetUris.processModelFileCreatePath]: ['POST', 'PUT', 'GET', 'DELETE'],
|
[targetUris.processModelFileCreatePath]: ['POST', 'PUT', 'GET', 'DELETE'],
|
||||||
|
@ -91,19 +95,17 @@ export default function ProcessModelShow() {
|
||||||
const processInstanceRunResultTag = () => {
|
const processInstanceRunResultTag = () => {
|
||||||
if (processInstance) {
|
if (processInstance) {
|
||||||
return (
|
return (
|
||||||
<div className="alert alert-success with-top-margin" role="alert">
|
<Notification
|
||||||
<p>
|
title="Process Instance Kicked Off:"
|
||||||
Process Instance {processInstance.id} kicked off (
|
onClose={() => setProcessInstance(null)}
|
||||||
<Link
|
>
|
||||||
to={`/admin/process-instances/${modifiedProcessModelId}/${processInstance.id}`}
|
<Link
|
||||||
data-qa="process-instance-show-link"
|
to={`/admin/process-instances/${modifiedProcessModelId}/${processInstance.id}`}
|
||||||
>
|
data-qa="process-instance-show-link"
|
||||||
view
|
>
|
||||||
</Link>
|
view
|
||||||
).
|
</Link>
|
||||||
</p>
|
</Notification>
|
||||||
<br />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -203,6 +205,21 @@ export default function ProcessModelShow() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const postPublish = (value: any) => {
|
||||||
|
setPublishDisabled(false);
|
||||||
|
setProcessModelPublished(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const publishProcessModel = () => {
|
||||||
|
setPublishDisabled(true);
|
||||||
|
setProcessModelPublished(null);
|
||||||
|
HttpService.makeCallToBackend({
|
||||||
|
path: `/process-models/${modifiedProcessModelId}/publish`,
|
||||||
|
successCallback: postPublish,
|
||||||
|
httpMethod: 'POST',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const navigateToFileEdit = (processModelFile: ProcessFile) => {
|
const navigateToFileEdit = (processModelFile: ProcessFile) => {
|
||||||
const url = profileModelFileEditUrl(processModelFile);
|
const url = profileModelFileEditUrl(processModelFile);
|
||||||
if (url) {
|
if (url) {
|
||||||
|
@ -510,6 +527,23 @@ export default function ProcessModelShow() {
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const processModelPublishMessage = () => {
|
||||||
|
if (processModelPublished) {
|
||||||
|
const prUrl: string = processModelPublished.pr_url;
|
||||||
|
return (
|
||||||
|
<Notification
|
||||||
|
title="Model Published:"
|
||||||
|
onClose={() => setProcessModelPublished(false)}
|
||||||
|
>
|
||||||
|
<a href={prUrl} target="_void()">
|
||||||
|
view the changes and create a Pull Request
|
||||||
|
</a>
|
||||||
|
</Notification>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
if (processModel) {
|
if (processModel) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -523,6 +557,8 @@ export default function ProcessModelShow() {
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
{processModelPublishMessage()}
|
||||||
|
{processInstanceRunResultTag()}
|
||||||
<Stack orientation="horizontal" gap={1}>
|
<Stack orientation="horizontal" gap={1}>
|
||||||
<h1 className="with-icons">
|
<h1 className="with-icons">
|
||||||
Process Model: {processModel.display_name}
|
Process Model: {processModel.display_name}
|
||||||
|
@ -568,8 +604,16 @@ export default function ProcessModelShow() {
|
||||||
<br />
|
<br />
|
||||||
</>
|
</>
|
||||||
</Can>
|
</Can>
|
||||||
|
<Can
|
||||||
|
I="POST"
|
||||||
|
a={targetUris.processModelPublishPath}
|
||||||
|
ability={ability}
|
||||||
|
>
|
||||||
|
<Button disabled={publishDisabled} onClick={publishProcessModel}>
|
||||||
|
Publish Changes
|
||||||
|
</Button>
|
||||||
|
</Can>
|
||||||
</Stack>
|
</Stack>
|
||||||
{processInstanceRunResultTag()}
|
|
||||||
{processModelFilesSection()}
|
{processModelFilesSection()}
|
||||||
<Can I="GET" a={targetUris.processInstanceListPath} ability={ability}>
|
<Can I="GET" a={targetUris.processInstanceListPath} ability={ability}>
|
||||||
{processInstanceListTableButton()}
|
{processInstanceListTableButton()}
|
||||||
|
|
Loading…
Reference in New Issue