Merge branch 'main' into feature/create_containers

This commit is contained in:
Dan 2022-12-06 18:06:09 -05:00
commit d80e4b3456
46 changed files with 2058 additions and 505 deletions

View File

@ -1,5 +1,5 @@
pip==22.2.2
nox==2022.8.7
nox-poetry==1.0.1
nox==2022.11.21
nox-poetry==1.0.2
poetry==1.2.2
virtualenv==20.16.5

View File

@ -1,8 +1,8 @@
"""empty message
Revision ID: 3f049fa4d8ac
Revision ID: 4d75421c0af0
Revises:
Create Date: 2022-11-30 16:49:54.805372
Create Date: 2022-12-06 17:42:56.417673
"""
from alembic import op
@ -10,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '3f049fa4d8ac'
revision = '4d75421c0af0'
down_revision = None
branch_labels = None
depends_on = None
@ -248,6 +248,7 @@ def upgrade():
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('process_instance_id', 'key', name='process_instance_metadata_unique')
)
op.create_index(op.f('ix_process_instance_metadata_key'), 'process_instance_metadata', ['key'], unique=False)
op.create_table('spiff_step_details',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('process_instance_id', sa.Integer(), nullable=False),
@ -294,6 +295,7 @@ def downgrade():
op.drop_index(op.f('ix_active_task_user_active_task_id'), table_name='active_task_user')
op.drop_table('active_task_user')
op.drop_table('spiff_step_details')
op.drop_index(op.f('ix_process_instance_metadata_key'), table_name='process_instance_metadata')
op.drop_table('process_instance_metadata')
op.drop_table('permission_assignment')
op.drop_table('message_instance')

View File

@ -1851,7 +1851,7 @@ lxml = "*"
type = "git"
url = "https://github.com/sartography/SpiffWorkflow"
reference = "main"
resolved_reference = "062eaf15d28c66f8cf07f68409429560251b12c7"
resolved_reference = "ffb1686757f944065580dd2db8def73d6c1f0134"
[[package]]
name = "SQLAlchemy"
@ -2989,7 +2989,18 @@ psycopg2 = [
{file = "psycopg2-2.9.4.tar.gz", hash = "sha256:d529926254e093a1b669f692a3aa50069bc71faf5b0ecd91686a78f62767d52f"},
]
pyasn1 = [
{file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"},
{file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"},
{file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"},
{file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"},
{file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"},
{file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"},
{file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"},
{file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"},
{file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"},
{file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"},
{file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"},
{file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"},
{file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"},
]
pycodestyle = [

View File

@ -108,11 +108,14 @@ def create_app() -> flask.app.Flask:
app.register_blueprint(admin_blueprint, url_prefix="/admin")
app.register_blueprint(openid_blueprint, url_prefix="/openid")
# preflight options requests will be allowed if they meet the requirements of the url regex.
# we will add an Access-Control-Max-Age header to the response to tell the browser it doesn't
# need to continually keep asking for the same path.
origins_re = [
r"^https?:\/\/%s(.*)" % o.replace(".", r"\.")
for o in app.config["CORS_ALLOW_ORIGINS"]
]
CORS(app, origins=origins_re)
CORS(app, origins=origins_re, max_age=3600)
connexion_app.add_api("api.yml", base_path="/v1.0")

View File

@ -338,9 +338,9 @@ paths:
schema:
$ref: "#/components/schemas/ProcessModel"
/process-models/{modified_process_model_id}/files:
/process-models/{modified_process_model_identifier}/files:
parameters:
- name: modified_process_model_id
- name: modified_process_model_identifier
in: path
required: true
description: The process_model_id, modified to replace slashes (/)
@ -544,6 +544,12 @@ paths:
description: Specifies the identifier of a report to use, if any
schema:
type: string
- name: report_id
in: query
required: false
description: Specifies the identifier of a report to use, if any
schema:
type: integer
get:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_list
summary: Returns a list of process instances for a given process model
@ -559,33 +565,6 @@ paths:
items:
$ref: "#/components/schemas/Workflow"
/process-instances/{process_instance_id}/task/{task_id}/update:
parameters:
- name: process_instance_id
in: path
required: true
description: The unique id of the process instance
schema:
type: string
- name: task_id
in: path
required: true
description: The unique id of the task
schema:
type: string
post:
operationId: spiffworkflow_backend.routes.process_api_blueprint.update_task_data
summary: Update the task data for requested instance and task
tags:
- Process Instances
responses:
"200":
description: Task Updated Successfully
content:
application/json:
schema:
$ref: "#/components/schemas/Workflow"
/process-models/{process_group_id}/{process_model_id}/script-unit-tests:
parameters:
- name: process_group_id
@ -640,15 +619,14 @@ paths:
schema:
$ref: "#/components/schemas/Workflow"
/process-models/{modified_process_model_id}/process-instances:
/process-instances/{modified_process_model_identifier}:
parameters:
- name: modified_process_model_id
- name: modified_process_model_identifier
in: path
required: true
description: The unique id of an existing process model.
schema:
type: string
# process_instance_create
post:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_create
summary: Creates an process instance from a process model and returns the instance
@ -662,28 +640,7 @@ paths:
schema:
$ref: "#/components/schemas/Workflow"
/process-instances/{process_instance_id}:
parameters:
- name: process_instance_id
in: path
required: true
description: The unique id of an existing process instance.
schema:
type: integer
delete:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_delete
summary: Deletes a single process instance
tags:
- Process Instances
responses:
"200":
description: The process instance was deleted.
content:
application/json:
schema:
$ref: "#/components/schemas/OkTrue"
/process-models/{modified_process_model_identifier}/process-instances/{process_instance_id}:
/process-instances/{modified_process_model_identifier}/{process_instance_id}:
parameters:
- name: modified_process_model_identifier
in: path
@ -709,6 +666,18 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Workflow"
delete:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_delete
summary: Deletes a single process instance
tags:
- Process Instances
responses:
"200":
description: The process instance was deleted.
content:
application/json:
schema:
$ref: "#/components/schemas/OkTrue"
/process-instances/{modified_process_model_identifier}/{process_instance_id}/run:
parameters:
@ -737,7 +706,7 @@ paths:
schema:
$ref: "#/components/schemas/Workflow"
/process-instances/{process_instance_id}/terminate:
/process-instances/{modified_process_model_identifier}/{process_instance_id}/terminate:
parameters:
- name: process_instance_id
in: path
@ -758,7 +727,7 @@ paths:
schema:
$ref: "#/components/schemas/OkTrue"
/process-instances/{process_instance_id}/suspend:
/process-instances/{modified_process_model_identifier}/{process_instance_id}/suspend:
parameters:
- name: process_instance_id
in: path
@ -779,7 +748,7 @@ paths:
schema:
$ref: "#/components/schemas/OkTrue"
/process-instances/{process_instance_id}/resume:
/process-instances/{modified_process_model_identifier}/{process_instance_id}/resume:
parameters:
- name: process_instance_id
in: path
@ -841,14 +810,30 @@ paths:
schema:
$ref: "#/components/schemas/OkTrue"
/process-instances/reports/{report_identifier}:
/process-instances/reports/columns:
get:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_report_column_list
summary: Returns all available columns for a process instance report.
tags:
- Process Instances
responses:
"200":
description: Workflow.
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Workflow"
/process-instances/reports/{report_id}:
parameters:
- name: report_identifier
- name: report_id
in: path
required: true
description: The unique id of an existing report
schema:
type: string
type: integer
- name: page
in: query
required: false
@ -900,9 +885,9 @@ paths:
schema:
$ref: "#/components/schemas/OkTrue"
/process-models/{modified_process_model_id}/files/{file_name}:
/process-models/{modified_process_model_identifier}/files/{file_name}:
parameters:
- name: modified_process_model_id
- name: modified_process_model_identifier
in: path
required: true
description: The modified process model id
@ -1079,9 +1064,9 @@ paths:
items:
$ref: "#/components/schemas/Task"
/process-instances/{modified_process_model_id}/{process_instance_id}/tasks:
/task-data/{modified_process_model_identifier}/{process_instance_id}:
parameters:
- name: modified_process_model_id
- name: modified_process_model_identifier
in: path
required: true
description: The modified id of an existing process model
@ -1120,11 +1105,44 @@ paths:
items:
$ref: "#/components/schemas/Task"
/service_tasks:
/task-data/{modified_process_model_identifier}/{process_instance_id}/{task_id}:
parameters:
- name: modified_process_model_identifier
in: path
required: true
description: The modified id of an existing process model
schema:
type: string
- name: process_instance_id
in: path
required: true
description: The unique id of an existing process instance.
schema:
type: integer
- name: task_id
in: path
required: true
description: The unique id of the task.
schema:
type: string
put:
operationId: spiffworkflow_backend.routes.process_api_blueprint.update_task_data
summary: Update the task data for requested instance and task
tags:
- Process Instances
responses:
"200":
description: Task Updated Successfully
content:
application/json:
schema:
$ref: "#/components/schemas/Workflow"
/service-tasks:
get:
tags:
- Service Tasks
operationId: spiffworkflow_backend.routes.process_api_blueprint.service_tasks_show
operationId: spiffworkflow_backend.routes.process_api_blueprint.service_task_list
summary: Gets all available service task connectors
responses:
"200":
@ -1304,7 +1322,7 @@ paths:
schema:
$ref: "#/components/schemas/Workflow"
/process-instances/{process_instance_id}/logs:
/logs/{modified_process_model_identifier}/{process_instance_id}:
parameters:
- name: process_instance_id
in: path
@ -1324,6 +1342,12 @@ paths:
description: The number of items to show per page. Defaults to page 10.
schema:
type: integer
- name: detailed
in: query
required: false
description: Show the detailed view, which includes all log entries
schema:
type: boolean
get:
tags:
- Process Instances

View File

@ -18,7 +18,6 @@ groups:
mike,
jason,
j,
amir,
jarrad,
elizabeth,
jon,
@ -76,6 +75,12 @@ permissions:
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/tasks/*
service-tasks:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/service-tasks
# read all for everybody
read-all-process-groups:
@ -104,6 +109,12 @@ permissions:
allowed_permissions: [read]
uri: /v1.0/processes
task-data-read:
groups: [demo]
users: []
allowed_permissions: [read]
uri: /v1.0/task-data/*
manage-procurement-admin:
groups: ["Project Lead"]
@ -176,17 +187,17 @@ permissions:
uri: /v1.0/process-instances/manage-procurement:vendor-lifecycle-management:*
core1-admin-models-instantiate:
groups: ["core-contributor"]
groups: ["core-contributor", "Finance Team"]
users: []
allowed_permissions: [create]
uri: /v1.0/process-models/misc:category_number_one:process-model-with-form/process-instances
core1-admin-instances:
groups: ["core-contributor"]
groups: ["core-contributor", "Finance Team"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/process-instances/misc:category_number_one:process-model-with-form:*
core1-admin-instances-slash:
groups: ["core-contributor"]
groups: ["core-contributor", "Finance Team"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/process-instances/misc:category_number_one:process-model-with-form/*

View File

@ -12,7 +12,6 @@ groups:
mike,
jason,
j,
amir,
jarrad,
elizabeth,
jon,
@ -71,6 +70,13 @@ permissions:
allowed_permissions: [create, read, update, delete]
uri: /v1.0/tasks/*
service-tasks:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/service-tasks
# read all for everybody
read-all-process-groups:
groups: [everybody]
@ -98,6 +104,12 @@ permissions:
allowed_permissions: [read]
uri: /v1.0/processes
task-data-read:
groups: [demo]
users: []
allowed_permissions: [read]
uri: /v1.0/task-data/*
manage-procurement-admin:
groups: ["Project Lead"]

View File

@ -23,7 +23,7 @@ class ProcessInstanceMetadataModel(SpiffworkflowBaseDBModel):
process_instance_id: int = db.Column(
ForeignKey(ProcessInstanceModel.id), nullable=False # type: ignore
)
key: str = db.Column(db.String(255), nullable=False)
key: str = db.Column(db.String(255), nullable=False, index=True)
value: str = db.Column(db.String(255), nullable=False)
updated_at_in_seconds: int = db.Column(db.Integer, nullable=False)

View File

@ -26,6 +26,10 @@ from spiffworkflow_backend.services.process_instance_processor import (
ReportMetadata = dict[str, Any]
class ProcessInstanceReportAlreadyExistsError(Exception):
"""ProcessInstanceReportAlreadyExistsError."""
class ProcessInstanceReportResult(TypedDict):
"""ProcessInstanceReportResult."""
@ -63,7 +67,7 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel):
),
)
id = db.Column(db.Integer, primary_key=True)
id: int = db.Column(db.Integer, primary_key=True)
identifier: str = db.Column(db.String(50), nullable=False, index=True)
report_metadata: dict = deferred(db.Column(db.JSON)) # type: ignore
created_by_id = db.Column(ForeignKey(UserModel.id), nullable=False, index=True)
@ -71,6 +75,11 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel):
created_at_in_seconds = db.Column(db.Integer)
updated_at_in_seconds = db.Column(db.Integer)
@classmethod
def default_order_by(cls) -> list[str]:
"""Default_order_by."""
return ["-start_in_seconds", "-id"]
@classmethod
def add_fixtures(cls) -> None:
"""Add_fixtures."""
@ -120,21 +129,27 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel):
identifier: str,
user: UserModel,
report_metadata: ReportMetadata,
) -> None:
) -> ProcessInstanceReportModel:
"""Make_fixture_report."""
process_instance_report = ProcessInstanceReportModel.query.filter_by(
identifier=identifier,
created_by_id=user.id,
).first()
if process_instance_report is None:
process_instance_report = cls(
identifier=identifier,
created_by_id=user.id,
report_metadata=report_metadata,
if process_instance_report is not None:
raise ProcessInstanceReportAlreadyExistsError(
f"Process instance report with identifier already exists: {identifier}"
)
db.session.add(process_instance_report)
db.session.commit()
process_instance_report = cls(
identifier=identifier,
created_by_id=user.id,
report_metadata=report_metadata,
)
db.session.add(process_instance_report)
db.session.commit()
return process_instance_report # type: ignore
@classmethod
def ticket_for_month_report(cls) -> dict:
@ -204,18 +219,8 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel):
user: UserModel,
) -> ProcessInstanceReportModel:
"""Create_with_attributes."""
# <<<<<<< HEAD
# process_model = ProcessModelService.get_process_model(
# process_model_id=f"{process_model_identifier}"
# )
# process_instance_report = cls(
# identifier=identifier,
# process_group_identifier="process_model.process_group_id",
# process_model_identifier=process_model.id,
# =======
process_instance_report = cls(
identifier=identifier,
# >>>>>>> main
created_by_id=user.id,
report_metadata=report_metadata,
)

View File

@ -38,6 +38,7 @@ class ProcessModelInfo:
fault_or_suspend_on_exception: str = NotificationType.fault.value
exception_notification_addresses: list[str] = field(default_factory=list)
parent_groups: list[dict] | None = None
metadata_extraction_paths: list[dict[str, str]] | None = None
def __post_init__(self) -> None:
"""__post_init__."""
@ -76,6 +77,13 @@ class ProcessModelInfoSchema(Schema):
exception_notification_addresses = marshmallow.fields.List(
marshmallow.fields.String
)
metadata_extraction_paths = marshmallow.fields.List(
marshmallow.fields.Dict(
keys=marshmallow.fields.Str(required=False),
values=marshmallow.fields.Str(required=False),
required=False,
)
)
@post_load
def make_spec(

View File

@ -1,6 +1,7 @@
"""APIs for dealing with process groups, process models, and process instances."""
import json
import random
import re
import string
import uuid
from flask_bpmn.api.api_error import ApiError
@ -30,6 +31,9 @@ from SpiffWorkflow.task import TaskState
from sqlalchemy import and_
from sqlalchemy import asc
from sqlalchemy import desc
from sqlalchemy import func
from sqlalchemy.orm import aliased
from sqlalchemy.orm import joinedload
from sqlalchemy.orm import selectinload
from spiffworkflow_backend.exceptions.process_entity_not_found_error import (
@ -52,6 +56,9 @@ from spiffworkflow_backend.models.process_instance import ProcessInstanceApiSche
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModelSchema
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
from spiffworkflow_backend.models.process_instance_metadata import (
ProcessInstanceMetadataModel,
)
from spiffworkflow_backend.models.process_instance_report import (
ProcessInstanceReportModel,
)
@ -152,9 +159,9 @@ def modify_process_model_id(process_model_id: str) -> str:
return process_model_id.replace("/", ":")
def un_modify_modified_process_model_id(modified_process_model_id: str) -> str:
def un_modify_modified_process_model_id(modified_process_model_identifier: str) -> str:
"""Un_modify_modified_process_model_id."""
return modified_process_model_id.replace(":", "/")
return modified_process_model_identifier.replace(":", "/")
def process_group_add(body: dict) -> flask.wrappers.Response:
@ -256,19 +263,26 @@ def process_model_create(
modified_process_group_id: str, body: Dict[str, Union[str, bool, int]]
) -> flask.wrappers.Response:
"""Process_model_create."""
process_model_info = ProcessModelInfoSchema().load(body)
body_include_list = [
"id",
"display_name",
"primary_file_name",
"primary_process_id",
"description",
"metadata_extraction_paths",
]
body_filtered = {
include_item: body[include_item]
for include_item in body_include_list
if include_item in body
}
if modified_process_group_id is None:
raise ApiError(
error_code="process_group_id_not_specified",
message="Process Model could not be created when process_group_id path param is unspecified",
status_code=400,
)
if process_model_info is None:
raise ApiError(
error_code="process_model_could_not_be_created",
message=f"Process Model could not be created from given body: {body}",
status_code=400,
)
unmodified_process_group_id = un_modify_modified_process_model_id(
modified_process_group_id
@ -281,6 +295,14 @@ def process_model_create(
status_code=400,
)
process_model_info = ProcessModelInfo(**body_filtered) # type: ignore
if process_model_info is None:
raise ApiError(
error_code="process_model_could_not_be_created",
message=f"Process Model could not be created from given body: {body}",
status_code=400,
)
ProcessModelService.add_process_model(process_model_info)
return Response(
json.dumps(ProcessModelInfoSchema().dump(process_model_info)),
@ -294,7 +316,6 @@ def process_model_delete(
) -> flask.wrappers.Response:
"""Process_model_delete."""
process_model_identifier = modified_process_model_identifier.replace(":", "/")
# process_model_identifier = f"{process_group_id}/{process_model_id}"
ProcessModelService().process_model_delete(process_model_identifier)
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
@ -309,6 +330,7 @@ def process_model_update(
"primary_file_name",
"primary_process_id",
"description",
"metadata_extraction_paths",
]
body_filtered = {
include_item: body[include_item]
@ -316,7 +338,6 @@ def process_model_update(
if include_item in body
}
# process_model_identifier = f"{process_group_id}/{process_model_id}"
process_model = get_process_model(process_model_identifier)
ProcessModelService.update_process_model(process_model, body_filtered)
return ProcessModelInfoSchema().dump(process_model)
@ -325,10 +346,7 @@ def process_model_update(
def process_model_show(modified_process_model_identifier: str) -> Any:
"""Process_model_show."""
process_model_identifier = modified_process_model_identifier.replace(":", "/")
# process_model_identifier = f"{process_group_id}/{process_model_id}"
process_model = get_process_model(process_model_identifier)
# TODO: Temporary. Should not need the next line once models have correct ids
# process_model.id = process_model_identifier
files = sorted(SpecFileService.get_files(process_model))
process_model.files = files
for file in process_model.files:
@ -394,9 +412,9 @@ def process_list() -> Any:
return SpecReferenceSchema(many=True).dump(references)
def get_file(modified_process_model_id: str, file_name: str) -> Any:
def get_file(modified_process_model_identifier: str, file_name: str) -> Any:
"""Get_file."""
process_model_identifier = modified_process_model_id.replace(":", "/")
process_model_identifier = modified_process_model_identifier.replace(":", "/")
process_model = get_process_model(process_model_identifier)
files = SpecFileService.get_files(process_model, file_name)
if len(files) == 0:
@ -416,11 +434,10 @@ def get_file(modified_process_model_id: str, file_name: str) -> Any:
def process_model_file_update(
modified_process_model_id: str, file_name: str
modified_process_model_identifier: str, file_name: str
) -> flask.wrappers.Response:
"""Process_model_file_update."""
process_model_identifier = modified_process_model_id.replace(":", "/")
# process_model_identifier = f"{process_group_id}/{process_model_id}"
process_model_identifier = modified_process_model_identifier.replace(":", "/")
process_model = get_process_model(process_model_identifier)
request_file = get_file_from_request()
@ -446,10 +463,10 @@ def process_model_file_update(
def process_model_file_delete(
modified_process_model_id: str, file_name: str
modified_process_model_identifier: str, file_name: str
) -> flask.wrappers.Response:
"""Process_model_file_delete."""
process_model_identifier = modified_process_model_id.replace(":", "/")
process_model_identifier = modified_process_model_identifier.replace(":", "/")
process_model = get_process_model(process_model_identifier)
try:
SpecFileService.delete_file(process_model, file_name)
@ -465,9 +482,9 @@ def process_model_file_delete(
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
def add_file(modified_process_model_id: str) -> flask.wrappers.Response:
def add_file(modified_process_model_identifier: str) -> flask.wrappers.Response:
"""Add_file."""
process_model_identifier = modified_process_model_id.replace(":", "/")
process_model_identifier = modified_process_model_identifier.replace(":", "/")
process_model = get_process_model(process_model_identifier)
request_file = get_file_from_request()
if not request_file.filename:
@ -488,10 +505,12 @@ def add_file(modified_process_model_id: str) -> flask.wrappers.Response:
)
def process_instance_create(modified_process_model_id: str) -> flask.wrappers.Response:
def process_instance_create(
modified_process_model_identifier: str,
) -> flask.wrappers.Response:
"""Create_process_instance."""
process_model_identifier = un_modify_modified_process_model_id(
modified_process_model_id
modified_process_model_identifier
)
process_instance = (
ProcessInstanceService.create_process_instance_from_process_model_identifier(
@ -549,6 +568,7 @@ def process_instance_run(
def process_instance_terminate(
process_instance_id: int,
modified_process_model_identifier: str,
) -> flask.wrappers.Response:
"""Process_instance_run."""
process_instance = ProcessInstanceService().get_process_instance(
@ -561,6 +581,7 @@ def process_instance_terminate(
def process_instance_suspend(
process_instance_id: int,
modified_process_model_identifier: str,
) -> flask.wrappers.Response:
"""Process_instance_suspend."""
process_instance = ProcessInstanceService().get_process_instance(
@ -573,6 +594,7 @@ def process_instance_suspend(
def process_instance_resume(
process_instance_id: int,
modified_process_model_identifier: str,
) -> flask.wrappers.Response:
"""Process_instance_resume."""
process_instance = ProcessInstanceService().get_process_instance(
@ -584,19 +606,24 @@ def process_instance_resume(
def process_instance_log_list(
modified_process_model_identifier: str,
process_instance_id: int,
page: int = 1,
per_page: int = 100,
detailed: bool = False,
) -> flask.wrappers.Response:
"""Process_instance_log_list."""
# to make sure the process instance exists
process_instance = find_process_instance_by_id_or_raise(process_instance_id)
log_query = SpiffLoggingModel.query.filter(
SpiffLoggingModel.process_instance_id == process_instance.id
)
if not detailed:
log_query = log_query.filter(SpiffLoggingModel.message.in_(["State change to COMPLETED"])) # type: ignore
logs = (
SpiffLoggingModel.query.filter(
SpiffLoggingModel.process_instance_id == process_instance.id
)
.order_by(SpiffLoggingModel.timestamp.desc()) # type: ignore
log_query.order_by(SpiffLoggingModel.timestamp.desc()) # type: ignore
.join(
UserModel, UserModel.id == SpiffLoggingModel.current_user_id, isouter=True
) # isouter since if we don't have a user, we still want the log
@ -642,6 +669,7 @@ def message_instance_list(
.add_columns(
MessageModel.identifier.label("message_identifier"),
ProcessInstanceModel.process_model_identifier,
ProcessInstanceModel.process_model_display_name,
)
.paginate(page=page, per_page=per_page, error_out=False)
)
@ -776,10 +804,11 @@ def process_instance_list(
with_tasks_completed_by_my_group: Optional[bool] = None,
user_filter: Optional[bool] = False,
report_identifier: Optional[str] = None,
report_id: Optional[int] = None,
) -> flask.wrappers.Response:
"""Process_instance_list."""
process_instance_report = ProcessInstanceReportService.report_with_identifier(
g.user, report_identifier
g.user, report_id, report_identifier
)
if user_filter:
@ -810,7 +839,6 @@ def process_instance_list(
)
)
# process_model_identifier = un_modify_modified_process_model_id(modified_process_model_identifier)
process_instance_query = ProcessInstanceModel.query
# Always join that hot user table for good performance at serialization time.
process_instance_query = process_instance_query.options(
@ -928,28 +956,78 @@ def process_instance_list(
UserGroupAssignmentModel.user_id == g.user.id
)
try:
process_instances = (
process_instance_query.group_by(ProcessInstanceModel.id)
.order_by(
ProcessInstanceModel.start_in_seconds.desc(), ProcessInstanceModel.id.desc() # type: ignore
)
.paginate(page=page, per_page=per_page, error_out=False)
)
except Exception as e:
print(e)
results = list(
map(
ProcessInstanceService.serialize_flat_with_task_data,
process_instances.items,
)
instance_metadata_aliases = {}
stock_columns = ProcessInstanceReportService.get_column_names_for_model(
ProcessInstanceModel
)
for column in process_instance_report.report_metadata["columns"]:
if column["accessor"] in stock_columns:
continue
instance_metadata_alias = aliased(ProcessInstanceMetadataModel)
instance_metadata_aliases[column["accessor"]] = instance_metadata_alias
filter_for_column = None
if "filter_by" in process_instance_report.report_metadata:
filter_for_column = next(
(
f
for f in process_instance_report.report_metadata["filter_by"]
if f["field_name"] == column["accessor"]
),
None,
)
isouter = True
conditions = [
ProcessInstanceModel.id == instance_metadata_alias.process_instance_id,
instance_metadata_alias.key == column["accessor"],
]
if filter_for_column:
isouter = False
conditions.append(
instance_metadata_alias.value == filter_for_column["field_value"]
)
process_instance_query = process_instance_query.join(
instance_metadata_alias, and_(*conditions), isouter=isouter
).add_columns(func.max(instance_metadata_alias.value).label(column["accessor"]))
order_by_query_array = []
order_by_array = process_instance_report.report_metadata["order_by"]
if len(order_by_array) < 1:
order_by_array = ProcessInstanceReportModel.default_order_by()
for order_by_option in order_by_array:
attribute = re.sub("^-", "", order_by_option)
if attribute in stock_columns:
if order_by_option.startswith("-"):
order_by_query_array.append(
getattr(ProcessInstanceModel, attribute).desc()
)
else:
order_by_query_array.append(
getattr(ProcessInstanceModel, attribute).asc()
)
elif attribute in instance_metadata_aliases:
if order_by_option.startswith("-"):
order_by_query_array.append(
instance_metadata_aliases[attribute].value.desc()
)
else:
order_by_query_array.append(
instance_metadata_aliases[attribute].value.asc()
)
process_instances = (
process_instance_query.group_by(ProcessInstanceModel.id)
.add_columns(ProcessInstanceModel.id)
.order_by(*order_by_query_array)
.paginate(page=page, per_page=per_page, error_out=False)
)
results = ProcessInstanceReportService.add_metadata_columns_to_process_instance(
process_instances.items, process_instance_report.report_metadata["columns"]
)
report_metadata = process_instance_report.report_metadata
response_json = {
"report_identifier": process_instance_report.identifier,
"report_metadata": report_metadata,
"report": process_instance_report,
"results": results,
"filters": report_filter.to_dict(),
"pagination": {
@ -962,6 +1040,22 @@ def process_instance_list(
return make_response(jsonify(response_json), 200)
def process_instance_report_column_list() -> flask.wrappers.Response:
"""Process_instance_report_column_list."""
table_columns = ProcessInstanceReportService.builtin_column_options()
columns_for_metadata = (
db.session.query(ProcessInstanceMetadataModel.key)
.order_by(ProcessInstanceMetadataModel.key)
.distinct() # type: ignore
.all()
)
columns_for_metadata_strings = [
{"Header": i[0], "accessor": i[0], "filterable": True}
for i in columns_for_metadata
]
return make_response(jsonify(table_columns + columns_for_metadata_strings), 200)
def process_instance_show(
modified_process_model_identifier: str, process_instance_id: int
) -> flask.wrappers.Response:
@ -988,7 +1082,9 @@ def process_instance_show(
return make_response(jsonify(process_instance), 200)
def process_instance_delete(process_instance_id: int) -> flask.wrappers.Response:
def process_instance_delete(
process_instance_id: int, modified_process_model_identifier: str
) -> flask.wrappers.Response:
"""Create_process_instance."""
process_instance = find_process_instance_by_id_or_raise(process_instance_id)
@ -1018,22 +1114,22 @@ def process_instance_report_list(
def process_instance_report_create(body: Dict[str, Any]) -> flask.wrappers.Response:
"""Process_instance_report_create."""
ProcessInstanceReportModel.create_report(
process_instance_report = ProcessInstanceReportModel.create_report(
identifier=body["identifier"],
user=g.user,
report_metadata=body["report_metadata"],
)
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
return make_response(jsonify(process_instance_report), 201)
def process_instance_report_update(
report_identifier: str,
report_id: int,
body: Dict[str, Any],
) -> flask.wrappers.Response:
"""Process_instance_report_create."""
process_instance_report = ProcessInstanceReportModel.query.filter_by(
identifier=report_identifier,
id=report_id,
created_by_id=g.user.id,
).first()
if process_instance_report is None:
@ -1046,15 +1142,15 @@ def process_instance_report_update(
process_instance_report.report_metadata = body["report_metadata"]
db.session.commit()
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
return make_response(jsonify(process_instance_report), 201)
def process_instance_report_delete(
report_identifier: str,
report_id: int,
) -> flask.wrappers.Response:
"""Process_instance_report_create."""
process_instance_report = ProcessInstanceReportModel.query.filter_by(
identifier=report_identifier,
id=report_id,
created_by_id=g.user.id,
).first()
if process_instance_report is None:
@ -1070,11 +1166,9 @@ def process_instance_report_delete(
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
def service_tasks_show() -> flask.wrappers.Response:
"""Service_tasks_show."""
def service_task_list() -> flask.wrappers.Response:
"""Service_task_list."""
available_connectors = ServiceTaskService.available_connectors()
print(available_connectors)
return Response(
json.dumps(available_connectors), status=200, mimetype="application/json"
)
@ -1108,19 +1202,17 @@ def authentication_callback(
def process_instance_report_show(
report_identifier: str,
report_id: int,
page: int = 1,
per_page: int = 100,
) -> flask.wrappers.Response:
"""Process_instance_list."""
process_instances = ProcessInstanceModel.query.order_by( # .filter_by(process_model_identifier=process_model.id)
"""Process_instance_report_show."""
process_instances = ProcessInstanceModel.query.order_by(
ProcessInstanceModel.start_in_seconds.desc(), ProcessInstanceModel.id.desc() # type: ignore
).paginate(
page=page, per_page=per_page, error_out=False
)
).paginate(page=page, per_page=per_page, error_out=False)
process_instance_report = ProcessInstanceReportModel.query.filter_by(
identifier=report_identifier,
id=report_id,
created_by_id=g.user.id,
).first()
if process_instance_report is None:
@ -1282,7 +1374,7 @@ def get_tasks(
def process_instance_task_list(
modified_process_model_id: str,
modified_process_model_identifier: str,
process_instance_id: int,
all_tasks: bool = False,
spiff_step: int = 0,
@ -1397,9 +1489,6 @@ def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response
task.form_ui_schema = ui_form_contents
if task.properties and task.data and "instructionsForEndUser" in task.properties:
print(
f"task.properties['instructionsForEndUser']: {task.properties['instructionsForEndUser']}"
)
if task.properties["instructionsForEndUser"]:
task.properties["instructionsForEndUser"] = render_jinja_template(
task.properties["instructionsForEndUser"], task.data
@ -1846,7 +1935,12 @@ def _update_form_schema_with_task_data_as_needed(
_update_form_schema_with_task_data_as_needed(o, task_data)
def update_task_data(process_instance_id: str, task_id: str, body: Dict) -> Response:
def update_task_data(
process_instance_id: str,
modified_process_model_identifier: str,
task_id: str,
body: Dict,
) -> Response:
"""Update task data."""
process_instance = ProcessInstanceModel.query.filter(
ProcessInstanceModel.id == int(process_instance_id)

View File

@ -1,4 +1,6 @@
"""Get_env."""
"""Save process instance metadata."""
from typing import Any
from flask_bpmn.models.db import db
from typing import Any

View File

@ -186,8 +186,9 @@ class AuthenticationService:
refresh_token_object: RefreshTokenModel = RefreshTokenModel.query.filter(
RefreshTokenModel.user_id == user_id
).first()
assert refresh_token_object # noqa: S101
return refresh_token_object.token
if refresh_token_object:
return refresh_token_object.token
return None
@classmethod
def get_auth_token_from_refresh_token(cls, refresh_token: str) -> dict:

View File

@ -81,6 +81,9 @@ from spiffworkflow_backend.models.message_instance import MessageInstanceModel
from spiffworkflow_backend.models.message_instance import MessageModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
from spiffworkflow_backend.models.process_instance_metadata import (
ProcessInstanceMetadataModel,
)
from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.models.script_attributes_context import (
ScriptAttributesContext,
@ -179,7 +182,12 @@ class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore
)
return Script.generate_augmented_list(script_attributes_context)
def evaluate(self, task: SpiffTask, expression: str, external_methods: Any = None) -> Any:
def evaluate(
self,
task: SpiffTask,
expression: str,
external_methods: Optional[dict[str, Any]] = None,
) -> Any:
"""Evaluate."""
return self._evaluate(expression, task.data, task, external_methods)
@ -572,6 +580,36 @@ class ProcessInstanceProcessor:
db.session.add(details_model)
db.session.commit()
def extract_metadata(self, process_model_info: ProcessModelInfo) -> None:
"""Extract_metadata."""
metadata_extraction_paths = process_model_info.metadata_extraction_paths
if metadata_extraction_paths is None:
return
if len(metadata_extraction_paths) <= 0:
return
current_data = self.get_current_data()
for metadata_extraction_path in metadata_extraction_paths:
key = metadata_extraction_path["key"]
path = metadata_extraction_path["path"]
path_segments = path.split(".")
data_for_key = current_data
for path_segment in path_segments:
data_for_key = data_for_key[path_segment]
pim = ProcessInstanceMetadataModel.query.filter_by(
process_instance_id=self.process_instance_model.id,
key=key,
).first()
if pim is None:
pim = ProcessInstanceMetadataModel(
process_instance_id=self.process_instance_model.id,
key=key,
)
pim.value = data_for_key
db.session.add(pim)
db.session.commit()
def save(self) -> None:
"""Saves the current state of this processor to the database."""
self.process_instance_model.bpmn_json = self.serialize()
@ -598,6 +636,15 @@ class ProcessInstanceProcessor:
process_instance_id=self.process_instance_model.id
).all()
ready_or_waiting_tasks = self.get_all_ready_or_waiting_tasks()
process_model_display_name = ""
process_model_info = self.process_model_service.get_process_model(
self.process_instance_model.process_model_identifier
)
if process_model_info is not None:
process_model_display_name = process_model_info.display_name
self.extract_metadata(process_model_info)
for ready_or_waiting_task in ready_or_waiting_tasks:
# filter out non-usertasks
task_spec = ready_or_waiting_task.task_spec
@ -616,13 +663,6 @@ class ProcessInstanceProcessor:
if "formUiSchemaFilename" in properties:
ui_form_file_name = properties["formUiSchemaFilename"]
process_model_display_name = ""
process_model_info = self.process_model_service.get_process_model(
self.process_instance_model.process_model_identifier
)
if process_model_info is not None:
process_model_display_name = process_model_info.display_name
active_task = None
for at in active_tasks:
if at.task_id == str(ready_or_waiting_task.id):
@ -1147,8 +1187,8 @@ class ProcessInstanceProcessor:
def get_current_data(self) -> dict[str, Any]:
"""Get the current data for the process.
Return either most recent task data or the process data
if the process instance is complete
Return either the most recent task data or--if the process instance is complete--
the process data.
"""
if self.process_instance_model.status == "complete":
return self.get_data()

View File

@ -2,6 +2,9 @@
from dataclasses import dataclass
from typing import Optional
import sqlalchemy
from flask_bpmn.models.db import db
from spiffworkflow_backend.models.process_instance_report import (
ProcessInstanceReportModel,
)
@ -57,12 +60,21 @@ class ProcessInstanceReportService:
@classmethod
def report_with_identifier(
cls, user: UserModel, report_identifier: Optional[str] = None
cls,
user: UserModel,
report_id: Optional[int] = None,
report_identifier: Optional[str] = None,
) -> ProcessInstanceReportModel:
"""Report_with_filter."""
if report_id is not None:
process_instance_report = ProcessInstanceReportModel.query.filter_by(
id=report_id, created_by_id=user.id
).first()
if process_instance_report is not None:
return process_instance_report # type: ignore
if report_identifier is None:
report_identifier = "default"
process_instance_report = ProcessInstanceReportModel.query.filter_by(
identifier=report_identifier, created_by_id=user.id
).first()
@ -73,17 +85,9 @@ class ProcessInstanceReportService:
# TODO replace with system reports that are loaded on launch (or similar)
temp_system_metadata_map = {
"default": {
"columns": [
{"Header": "id", "accessor": "id"},
{
"Header": "process_model_display_name",
"accessor": "process_model_display_name",
},
{"Header": "start_in_seconds", "accessor": "start_in_seconds"},
{"Header": "end_in_seconds", "accessor": "end_in_seconds"},
{"Header": "username", "accessor": "username"},
{"Header": "status", "accessor": "status"},
],
"columns": cls.builtin_column_options(),
"filter_by": [],
"order_by": ["-start_in_seconds", "-id"],
},
"system_report_instances_initiated_by_me": {
"columns": [
@ -97,48 +101,31 @@ class ProcessInstanceReportService:
{"Header": "status", "accessor": "status"},
],
"filter_by": [{"field_name": "initiated_by_me", "field_value": True}],
"order_by": ["-start_in_seconds", "-id"],
},
"system_report_instances_with_tasks_completed_by_me": {
"columns": [
{"Header": "id", "accessor": "id"},
{
"Header": "process_model_display_name",
"accessor": "process_model_display_name",
},
{"Header": "start_in_seconds", "accessor": "start_in_seconds"},
{"Header": "end_in_seconds", "accessor": "end_in_seconds"},
{"Header": "username", "accessor": "username"},
{"Header": "status", "accessor": "status"},
],
"columns": cls.builtin_column_options(),
"filter_by": [
{"field_name": "with_tasks_completed_by_me", "field_value": True}
],
"order_by": ["-start_in_seconds", "-id"],
},
"system_report_instances_with_tasks_completed_by_my_groups": {
"columns": [
{"Header": "id", "accessor": "id"},
{
"Header": "process_model_display_name",
"accessor": "process_model_display_name",
},
{"Header": "start_in_seconds", "accessor": "start_in_seconds"},
{"Header": "end_in_seconds", "accessor": "end_in_seconds"},
{"Header": "username", "accessor": "username"},
{"Header": "status", "accessor": "status"},
],
"columns": cls.builtin_column_options(),
"filter_by": [
{
"field_name": "with_tasks_completed_by_my_group",
"field_value": True,
}
],
"order_by": ["-start_in_seconds", "-id"],
},
}
process_instance_report = ProcessInstanceReportModel(
identifier=report_identifier,
created_by_id=user.id,
report_metadata=temp_system_metadata_map[report_identifier], # type: ignore
report_metadata=temp_system_metadata_map[report_identifier],
)
return process_instance_report # type: ignore
@ -241,3 +228,43 @@ class ProcessInstanceReportService:
)
return report_filter
@classmethod
def add_metadata_columns_to_process_instance(
cls,
process_instance_sqlalchemy_rows: list[sqlalchemy.engine.row.Row], # type: ignore
metadata_columns: list[dict],
) -> list[dict]:
"""Add_metadata_columns_to_process_instance."""
results = []
for process_instance in process_instance_sqlalchemy_rows:
process_instance_dict = process_instance["ProcessInstanceModel"].serialized
for metadata_column in metadata_columns:
if metadata_column["accessor"] not in process_instance_dict:
process_instance_dict[
metadata_column["accessor"]
] = process_instance[metadata_column["accessor"]]
results.append(process_instance_dict)
return results
@classmethod
def get_column_names_for_model(cls, model: db.Model) -> list[str]: # type: ignore
"""Get_column_names_for_model."""
return [i.name for i in model.__table__.columns]
@classmethod
def builtin_column_options(cls) -> list[dict]:
"""Builtin_column_options."""
return [
{"Header": "Id", "accessor": "id", "filterable": False},
{
"Header": "Process",
"accessor": "process_model_display_name",
"filterable": False,
},
{"Header": "Start", "accessor": "start_in_seconds", "filterable": False},
{"Header": "End", "accessor": "end_in_seconds", "filterable": False},
{"Header": "Username", "accessor": "username", "filterable": False},
{"Header": "Status", "accessor": "status", "filterable": False},
]

View File

@ -322,18 +322,3 @@ class ProcessInstanceService:
)
return task
@staticmethod
def serialize_flat_with_task_data(
process_instance: ProcessInstanceModel,
) -> dict[str, Any]:
"""NOTE: This is crazy slow. Put the latest task data in the database."""
"""Serialize_flat_with_task_data."""
# results = {}
# try:
# processor = ProcessInstanceProcessor(process_instance)
# process_instance.data = processor.get_current_data()
# results = process_instance.serialized_flat
# except ApiError:
results = process_instance.serialized
return results

View File

@ -19,7 +19,11 @@
<bpmn:scriptTask id="hot_script_task_OH_YEEEEEEEEEEEEEEEEEEEEAH" name="OHHHHHHHHHHYEEEESSSSSSSSSS">
<bpmn:incoming>Flow_0bazl8x</bpmn:incoming>
<bpmn:outgoing>Flow_1mcaszp</bpmn:outgoing>
<bpmn:script>a = 1</bpmn:script>
<bpmn:script>a = 1
b = 2
outer = {}
outer["inner"] = 'sweet1'
</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="Event_1vch1y0">
<bpmn:incoming>Flow_1mcaszp</bpmn:incoming>

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
<bpmn:process id="Process_hk6nsfl" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_1ohrjz9</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_1ohrjz9" sourceRef="StartEvent_1" targetRef="Activity_0fah9rm" />
<bpmn:endEvent id="Event_1tk4dsv">
<bpmn:incoming>Flow_1flxgry</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_18gs4jt" sourceRef="Activity_0fah9rm" targetRef="Activity_1bvyv67" />
<bpmn:scriptTask id="Activity_0fah9rm" name="First setting of data">
<bpmn:incoming>Flow_1ohrjz9</bpmn:incoming>
<bpmn:outgoing>Flow_18gs4jt</bpmn:outgoing>
<bpmn:script>outer = {}
invoice_number = 123
outer["inner"] = 'sweet1'
outer['time'] = time.time_ns()</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_1flxgry" sourceRef="Activity_1bvyv67" targetRef="Event_1tk4dsv" />
<bpmn:scriptTask id="Activity_1bvyv67" name="First setting of data">
<bpmn:incoming>Flow_18gs4jt</bpmn:incoming>
<bpmn:outgoing>Flow_1flxgry</bpmn:outgoing>
<bpmn:script>outer["inner"] = 'sweet2'</bpmn:script>
</bpmn:scriptTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_hk6nsfl">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1c5bi8c_di" bpmnElement="Activity_0fah9rm">
<dc:Bounds x="270" y="137" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1tk4dsv_di" bpmnElement="Event_1tk4dsv">
<dc:Bounds x="612" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1ay4o3w_di" bpmnElement="Activity_1bvyv67">
<dc:Bounds x="430" y="137" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1ohrjz9_di" bpmnElement="Flow_1ohrjz9">
<di:waypoint x="215" y="177" />
<di:waypoint x="270" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_18gs4jt_di" bpmnElement="Flow_18gs4jt">
<di:waypoint x="370" y="177" />
<di:waypoint x="430" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1flxgry_di" bpmnElement="Flow_1flxgry">
<di:waypoint x="530" y="177" />
<di:waypoint x="612" y="177" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -265,7 +265,7 @@ class BaseTest:
)
modified_process_model_id = test_process_model_id.replace("/", ":")
response = client.post(
f"/v1.0/process-models/{modified_process_model_id}/process-instances",
f"/v1.0/process-instances/{modified_process_model_id}",
headers=headers,
)
assert response.status_code == 201

View File

@ -57,7 +57,7 @@ class TestLoggingService(BaseTest):
assert response.status_code == 200
log_response = client.get(
f"/v1.0/process-instances/{process_instance_id}/logs",
f"/v1.0/logs/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}",
headers=headers,
)
assert log_response.status_code == 200

View File

@ -20,6 +20,9 @@ from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.process_group import ProcessGroup
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
from spiffworkflow_backend.models.process_instance_metadata import (
ProcessInstanceMetadataModel,
)
from spiffworkflow_backend.models.process_instance_report import (
ProcessInstanceReportModel,
)
@ -330,6 +333,9 @@ class TestProcessApi(BaseTest):
process_model.display_name = "Updated Display Name"
process_model.primary_file_name = "superduper.bpmn"
process_model.primary_process_id = "superduper"
process_model.metadata_extraction_paths = [
{"key": "extraction1", "path": "path1"}
]
modified_process_model_identifier = process_model_identifier.replace("/", ":")
response = client.put(
@ -343,6 +349,9 @@ class TestProcessApi(BaseTest):
assert response.json["display_name"] == "Updated Display Name"
assert response.json["primary_file_name"] == "superduper.bpmn"
assert response.json["primary_process_id"] == "superduper"
assert response.json["metadata_extraction_paths"] == [
{"key": "extraction1", "path": "path1"}
]
def test_process_model_list_all(
self,
@ -903,7 +912,7 @@ class TestProcessApi(BaseTest):
modified_process_model_identifier = process_model_identifier.replace("/", ":")
response = client.post(
f"/v1.0/process-models/{modified_process_model_identifier}/process-instances",
f"/v1.0/process-instances/{modified_process_model_identifier}",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 201
@ -1145,10 +1154,11 @@ class TestProcessApi(BaseTest):
headers=self.logged_in_headers(with_super_admin_user),
)
show_response = client.get(
f"/v1.0/process-models/{modified_process_model_identifier}/process-instances/{process_instance_id}",
f"/v1.0/process-instances/{modified_process_model_identifier}/{process_instance_id}",
headers=self.logged_in_headers(with_super_admin_user),
)
assert show_response.json is not None
assert show_response.status_code == 200
file_system_root = FileSystemService.root_path()
file_path = (
f"{file_system_root}/{process_model_identifier}/{process_model_id}.bpmn"
@ -1311,7 +1321,7 @@ class TestProcessApi(BaseTest):
assert response.json is not None
response = client.post(
f"/v1.0/process-instances/{process_instance_id}/terminate",
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/terminate",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
@ -1358,7 +1368,7 @@ class TestProcessApi(BaseTest):
assert response.json is not None
delete_response = client.delete(
f"/v1.0/process-instances/{process_instance_id}",
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}",
headers=self.logged_in_headers(with_super_admin_user),
)
assert delete_response.status_code == 200
@ -1723,14 +1733,14 @@ class TestProcessApi(BaseTest):
],
}
ProcessInstanceReportModel.create_with_attributes(
report = ProcessInstanceReportModel.create_with_attributes(
identifier="sure",
report_metadata=report_metadata,
user=with_super_admin_user,
)
response = client.get(
"/v1.0/process-instances/reports/sure",
f"/v1.0/process-instances/reports/{report.id}",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
@ -1769,14 +1779,14 @@ class TestProcessApi(BaseTest):
],
}
ProcessInstanceReportModel.create_with_attributes(
report = ProcessInstanceReportModel.create_with_attributes(
identifier="sure",
report_metadata=report_metadata,
user=with_super_admin_user,
)
response = client.get(
"/v1.0/process-instances/reports/sure?grade_level=1",
f"/v1.0/process-instances/reports/{report.id}?grade_level=1",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
@ -1791,9 +1801,9 @@ class TestProcessApi(BaseTest):
with_super_admin_user: UserModel,
setup_process_instances_for_reports: list[ProcessInstanceModel],
) -> None:
"""Test_process_instance_report_show_with_default_list."""
"""Test_process_instance_report_show_with_bad_identifier."""
response = client.get(
"/v1.0/process-instances/reports/sure?grade_level=1",
"/v1.0/process-instances/reports/13000000?grade_level=1",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 404
@ -2357,7 +2367,7 @@ class TestProcessApi(BaseTest):
assert process_instance.status == "user_input_required"
client.post(
f"/v1.0/process-instances/{process_instance_id}/suspend",
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/suspend",
headers=self.logged_in_headers(with_super_admin_user),
)
process_instance = ProcessInstanceService().get_process_instance(
@ -2544,3 +2554,191 @@ class TestProcessApi(BaseTest):
# make sure the new subgroup does exist
new_process_group = ProcessModelService.get_process_group(new_sub_path)
assert new_process_group.id == new_sub_path
def test_can_get_process_instance_list_with_report_metadata(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
"""Test_can_get_process_instance_list_with_report_metadata."""
process_model = load_test_spec(
process_model_id="save_process_instance_metadata/save_process_instance_metadata",
bpmn_file_name="save_process_instance_metadata.bpmn",
process_model_source_directory="save_process_instance_metadata",
)
process_instance = self.create_process_instance_from_process_model(
process_model=process_model, user=with_super_admin_user
)
processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True)
process_instance_metadata = ProcessInstanceMetadataModel.query.filter_by(
process_instance_id=process_instance.id
).all()
assert len(process_instance_metadata) == 3
report_metadata = {
"columns": [
{"Header": "ID", "accessor": "id"},
{"Header": "Status", "accessor": "status"},
{"Header": "Key One", "accessor": "key1"},
{"Header": "Key Two", "accessor": "key2"},
],
"order_by": ["status"],
"filter_by": [],
}
process_instance_report = ProcessInstanceReportModel.create_with_attributes(
identifier="sure",
report_metadata=report_metadata,
user=with_super_admin_user,
)
response = client.get(
f"/v1.0/process-instances?report_identifier={process_instance_report.identifier}",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.json is not None
assert response.status_code == 200
assert len(response.json["results"]) == 1
assert response.json["results"][0]["status"] == "complete"
assert response.json["results"][0]["id"] == process_instance.id
assert response.json["results"][0]["key1"] == "value1"
assert response.json["results"][0]["key2"] == "value2"
assert response.json["pagination"]["count"] == 1
assert response.json["pagination"]["pages"] == 1
assert response.json["pagination"]["total"] == 1
def test_can_get_process_instance_report_column_list(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
"""Test_can_get_process_instance_list_with_report_metadata."""
process_model = load_test_spec(
process_model_id="save_process_instance_metadata/save_process_instance_metadata",
bpmn_file_name="save_process_instance_metadata.bpmn",
process_model_source_directory="save_process_instance_metadata",
)
process_instance = self.create_process_instance_from_process_model(
process_model=process_model, user=with_super_admin_user
)
processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True)
process_instance_metadata = ProcessInstanceMetadataModel.query.filter_by(
process_instance_id=process_instance.id
).all()
assert len(process_instance_metadata) == 3
response = client.get(
"/v1.0/process-instances/reports/columns",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.json is not None
assert response.status_code == 200
assert response.json == [
{"Header": "Id", "accessor": "id", "filterable": False},
{
"Header": "Process",
"accessor": "process_model_display_name",
"filterable": False,
},
{"Header": "Start", "accessor": "start_in_seconds", "filterable": False},
{"Header": "End", "accessor": "end_in_seconds", "filterable": False},
{"Header": "Username", "accessor": "username", "filterable": False},
{"Header": "Status", "accessor": "status", "filterable": False},
{"Header": "key1", "accessor": "key1", "filterable": True},
{"Header": "key2", "accessor": "key2", "filterable": True},
{"Header": "key3", "accessor": "key3", "filterable": True},
]
def test_process_instance_list_can_order_by_metadata(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
"""Test_process_instance_list_can_order_by_metadata."""
self.create_process_group(
client, with_super_admin_user, "test_group", "test_group"
)
process_model = load_test_spec(
"test_group/hello_world",
process_model_source_directory="nested-task-data-structure",
)
ProcessModelService.update_process_model(
process_model,
{
"metadata_extraction_paths": [
{"key": "time_ns", "path": "outer.time"},
]
},
)
process_instance_one = self.create_process_instance_from_process_model(
process_model
)
processor = ProcessInstanceProcessor(process_instance_one)
processor.do_engine_steps(save=True)
assert process_instance_one.status == "complete"
process_instance_two = self.create_process_instance_from_process_model(
process_model
)
processor = ProcessInstanceProcessor(process_instance_two)
processor.do_engine_steps(save=True)
assert process_instance_two.status == "complete"
report_metadata = {
"columns": [
{"Header": "id", "accessor": "id"},
{"Header": "Time", "accessor": "time_ns"},
],
"order_by": ["time_ns"],
}
report_one = ProcessInstanceReportModel.create_with_attributes(
identifier="report_one",
report_metadata=report_metadata,
user=with_super_admin_user,
)
response = client.get(
f"/v1.0/process-instances?report_id={report_one.id}",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
assert response.json is not None
assert len(response.json["results"]) == 2
assert response.json["results"][0]["id"] == process_instance_one.id
assert response.json["results"][1]["id"] == process_instance_two.id
report_metadata = {
"columns": [
{"Header": "id", "accessor": "id"},
{"Header": "Time", "accessor": "time_ns"},
],
"order_by": ["-time_ns"],
}
report_two = ProcessInstanceReportModel.create_with_attributes(
identifier="report_two",
report_metadata=report_metadata,
user=with_super_admin_user,
)
response = client.get(
f"/v1.0/process-instances?report_id={report_two.id}",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
assert response.json is not None
assert len(response.json["results"]) == 2
assert response.json["results"][1]["id"] == process_instance_one.id
assert response.json["results"][0]["id"] == process_instance_two.id

View File

@ -37,7 +37,7 @@ def test_generate_report_with_filter_by_with_variable_substitution(
with_db_and_bpmn_file_cleanup: None,
setup_process_instances_for_reports: list[ProcessInstanceModel],
) -> None:
"""Test_user_can_be_given_permission_to_administer_process_group."""
"""Test_generate_report_with_filter_by_with_variable_substitution."""
process_instances = setup_process_instances_for_reports
report_metadata = {
"filter_by": [
@ -61,7 +61,7 @@ def test_generate_report_with_order_by_and_one_field(
with_db_and_bpmn_file_cleanup: None,
setup_process_instances_for_reports: list[ProcessInstanceModel],
) -> None:
"""Test_user_can_be_given_permission_to_administer_process_group."""
"""Test_generate_report_with_order_by_and_one_field."""
process_instances = setup_process_instances_for_reports
report_metadata = {"order_by": ["test_score"]}
results = do_report_with_metadata_and_instances(report_metadata, process_instances)
@ -75,7 +75,7 @@ def test_generate_report_with_order_by_and_two_fields(
with_db_and_bpmn_file_cleanup: None,
setup_process_instances_for_reports: list[ProcessInstanceModel],
) -> None:
"""Test_user_can_be_given_permission_to_administer_process_group."""
"""Test_generate_report_with_order_by_and_two_fields."""
process_instances = setup_process_instances_for_reports
report_metadata = {"order_by": ["grade_level", "test_score"]}
results = do_report_with_metadata_and_instances(report_metadata, process_instances)
@ -89,7 +89,7 @@ def test_generate_report_with_order_by_desc(
with_db_and_bpmn_file_cleanup: None,
setup_process_instances_for_reports: list[ProcessInstanceModel],
) -> None:
"""Test_user_can_be_given_permission_to_administer_process_group."""
"""Test_generate_report_with_order_by_desc."""
process_instances = setup_process_instances_for_reports
report_metadata = {"order_by": ["grade_level", "-test_score"]}
results = do_report_with_metadata_and_instances(report_metadata, process_instances)
@ -103,7 +103,7 @@ def test_generate_report_with_columns(
with_db_and_bpmn_file_cleanup: None,
setup_process_instances_for_reports: list[ProcessInstanceModel],
) -> None:
"""Test_user_can_be_given_permission_to_administer_process_group."""
"""Test_generate_report_with_columns."""
process_instances = setup_process_instances_for_reports
report_metadata = {
"columns": [

View File

@ -6,12 +6,16 @@ from flask.testing import FlaskClient
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
from spiffworkflow_backend.models.process_instance_metadata import (
ProcessInstanceMetadataModel,
)
from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.models.spec_reference import SpecReferenceCache
from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.services.process_instance_processor import (
ProcessInstanceProcessor,
)
from spiffworkflow_backend.services.process_model_service import ProcessModelService
class TestProcessModel(BaseTest):
@ -123,6 +127,53 @@ class TestProcessModel(BaseTest):
processor.do_engine_steps(save=True)
assert process_instance.status == "complete"
def test_extract_metadata(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
"""Test_can_run_process_model_with_call_activities."""
self.create_process_group(
client, with_super_admin_user, "test_group", "test_group"
)
process_model = load_test_spec(
"test_group/hello_world",
process_model_source_directory="nested-task-data-structure",
)
ProcessModelService.update_process_model(
process_model,
{
"metadata_extraction_paths": [
{"key": "awesome_var", "path": "outer.inner"},
{"key": "invoice_number", "path": "invoice_number"},
]
},
)
process_instance = self.create_process_instance_from_process_model(
process_model
)
processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True)
assert process_instance.status == "complete"
process_instance_metadata_awesome_var = (
ProcessInstanceMetadataModel.query.filter_by(
process_instance_id=process_instance.id, key="awesome_var"
).first()
)
assert process_instance_metadata_awesome_var is not None
assert process_instance_metadata_awesome_var.value == "sweet2"
process_instance_metadata_awesome_var = (
ProcessInstanceMetadataModel.query.filter_by(
process_instance_id=process_instance.id, key="invoice_number"
).first()
)
assert process_instance_metadata_awesome_var is not None
assert process_instance_metadata_awesome_var.value == "123"
def create_test_process_model(self, id: str, display_name: str) -> ProcessModelInfo:
"""Create_test_process_model."""
return ProcessModelInfo(

View File

@ -0,0 +1,22 @@
import { Link } from 'react-router-dom';
import { modifyProcessIdentifierForPathParam } from '../helpers';
import { MessageInstance, ProcessInstance } from '../interfaces';
export function FormatProcessModelDisplayName(
instanceObject: ProcessInstance | MessageInstance
) {
const {
process_model_identifier: processModelIdentifier,
process_model_display_name: processModelDisplayName,
} = instanceObject;
return (
<Link
to={`/admin/process-models/${modifyProcessIdentifierForPathParam(
processModelIdentifier
)}`}
title={processModelIdentifier}
>
{processModelDisplayName}
</Link>
);
}

View File

@ -115,7 +115,6 @@ export default function ProcessGroupForm({
labelText="Display Name*"
value={processGroup.display_name}
onChange={(event: any) => onDisplayNameChanged(event.target.value)}
onBlur={(event: any) => console.log('event', event)}
/>,
];

View File

@ -0,0 +1,205 @@
import { useState } from 'react';
import {
Button,
TextInput,
Stack,
Modal,
// @ts-ignore
} from '@carbon/react';
import {
ReportFilter,
ProcessInstanceReport,
ProcessModel,
ReportColumn,
ReportMetadata,
} from '../interfaces';
import HttpService from '../services/HttpService';
type OwnProps = {
onSuccess: (..._args: any[]) => any;
columnArray: ReportColumn[];
orderBy: string;
processModelSelection: ProcessModel | null;
processStatusSelection: string[];
startFromSeconds: string | null;
startToSeconds: string | null;
endFromSeconds: string | null;
endToSeconds: string | null;
buttonText?: string;
buttonClassName?: string;
processInstanceReportSelection?: ProcessInstanceReport | null;
reportMetadata: ReportMetadata;
};
export default function ProcessInstanceListSaveAsReport({
onSuccess,
columnArray,
orderBy,
processModelSelection,
processInstanceReportSelection,
processStatusSelection,
startFromSeconds,
startToSeconds,
endFromSeconds,
endToSeconds,
buttonClassName,
buttonText = 'Save as Perspective',
reportMetadata,
}: OwnProps) {
const [identifier, setIdentifier] = useState<string>(
processInstanceReportSelection?.identifier || ''
);
const [showSaveForm, setShowSaveForm] = useState<boolean>(false);
const isEditMode = () => {
return (
processInstanceReportSelection &&
processInstanceReportSelection.identifier === identifier
);
};
const responseHandler = (result: any) => {
if (result) {
onSuccess(result, isEditMode() ? 'edit' : 'new');
}
};
const handleSaveFormClose = () => {
setIdentifier(processInstanceReportSelection?.identifier || '');
setShowSaveForm(false);
};
const addProcessInstanceReport = (event: any) => {
event.preventDefault();
// TODO: make a field to set this
let orderByArray = ['-start_in_seconds', '-id'];
if (orderBy) {
orderByArray = orderBy.split(',').filter((n) => n);
}
const filterByArray: any = [];
if (processModelSelection) {
filterByArray.push({
field_name: 'process_model_identifier',
field_value: processModelSelection.id,
});
}
if (processStatusSelection.length > 0) {
filterByArray.push({
field_name: 'process_status',
field_value: processStatusSelection.join(','),
operator: 'in',
});
}
if (startFromSeconds) {
filterByArray.push({
field_name: 'start_from',
field_value: startFromSeconds,
});
}
if (startToSeconds) {
filterByArray.push({
field_name: 'start_to',
field_value: startToSeconds,
});
}
if (endFromSeconds) {
filterByArray.push({
field_name: 'end_from',
field_value: endFromSeconds,
});
}
if (endToSeconds) {
filterByArray.push({
field_name: 'end_to',
field_value: endToSeconds,
});
}
reportMetadata.filter_by.forEach((reportFilter: ReportFilter) => {
columnArray.forEach((reportColumn: ReportColumn) => {
if (
reportColumn.accessor === reportFilter.field_name &&
reportColumn.filterable
) {
filterByArray.push(reportFilter);
}
});
});
let path = `/process-instances/reports`;
let httpMethod = 'POST';
if (isEditMode() && processInstanceReportSelection) {
httpMethod = 'PUT';
path = `${path}/${processInstanceReportSelection.id}`;
}
HttpService.makeCallToBackend({
path,
successCallback: responseHandler,
httpMethod,
postBody: {
identifier,
report_metadata: {
columns: columnArray,
order_by: orderByArray,
filter_by: filterByArray,
},
},
});
handleSaveFormClose();
};
let textInputComponent = null;
textInputComponent = (
<TextInput
id="identifier"
name="identifier"
labelText="Identifier"
className="no-wrap"
inline
value={identifier}
onChange={(e: any) => setIdentifier(e.target.value)}
/>
);
let descriptionText =
'Save the current columns and filters as a perspective so you can come back to this view in the future.';
if (processInstanceReportSelection) {
descriptionText =
'Keep the identifier the same and click Save to update the current perspective. Change the identifier if you want to save the current view with a new name.';
}
return (
<Stack gap={5} orientation="horizontal">
<Modal
open={showSaveForm}
modalHeading="Save Perspective"
primaryButtonText="Save"
primaryButtonDisabled={!identifier}
onRequestSubmit={addProcessInstanceReport}
onRequestClose={handleSaveFormClose}
hasScrollingContent
>
<p className="data-table-description">{descriptionText}</p>
{textInputComponent}
</Modal>
<Button
kind=""
className={buttonClassName}
onClick={() => {
setIdentifier(processInstanceReportSelection?.identifier || '');
setShowSaveForm(true);
}}
>
{buttonText}
</Button>
</Stack>
);
}

View File

@ -7,7 +7,7 @@ import {
} from 'react-router-dom';
// @ts-ignore
import { Filter } from '@carbon/icons-react';
import { Filter, Close, AddAlt } from '@carbon/icons-react';
import {
Button,
ButtonSet,
@ -21,6 +21,13 @@ import {
TableHead,
TableRow,
TimePicker,
Tag,
InlineNotification,
Stack,
Modal,
ComboBox,
TextInput,
FormLabel,
// @ts-ignore
} from '@carbon/react';
import { PROCESS_STATUSES, DATE_FORMAT, DATE_FORMAT_CARBON } from '../config';
@ -49,9 +56,15 @@ import {
ProcessModel,
ProcessInstanceReport,
ProcessInstance,
ReportColumn,
ReportColumnForEditing,
ReportMetadata,
ReportFilter,
} from '../interfaces';
import ProcessModelSearch from './ProcessModelSearch';
import ProcessInstanceReportSearch from './ProcessInstanceReportSearch';
import ProcessInstanceListSaveAsReport from './ProcessInstanceListSaveAsReport';
import { FormatProcessModelDisplayName } from './MiniComponents';
const REFRESH_INTERVAL = 5;
const REFRESH_TIMEOUT = 600;
@ -88,7 +101,7 @@ export default function ProcessInstanceListTable({
const navigate = useNavigate();
const [processInstances, setProcessInstances] = useState([]);
const [reportMetadata, setReportMetadata] = useState({});
const [reportMetadata, setReportMetadata] = useState<ReportMetadata | null>();
const [pagination, setPagination] = useState<PaginationObject | null>(null);
const [processInstanceFilters, setProcessInstanceFilters] = useState({});
@ -125,6 +138,17 @@ export default function ProcessInstanceListTable({
const [processInstanceReportSelection, setProcessInstanceReportSelection] =
useState<ProcessInstanceReport | null>(null);
const [availableReportColumns, setAvailableReportColumns] = useState<
ReportColumn[]
>([]);
const [processInstanceReportJustSaved, setProcessInstanceReportJustSaved] =
useState<string | null>(null);
const [showReportColumnForm, setShowReportColumnForm] =
useState<boolean>(false);
const [reportColumnToOperateOn, setReportColumnToOperateOn] =
useState<ReportColumnForEditing | null>(null);
const [reportColumnFormMode, setReportColumnFormMode] = useState<string>('');
const dateParametersToAlwaysFilterBy: dateParameters = useMemo(() => {
return {
start_from: [setStartFromDate, setStartFromTime],
@ -155,16 +179,12 @@ export default function ProcessInstanceListTable({
function setProcessInstancesFromResult(result: any) {
const processInstancesFromApi = result.results;
setProcessInstances(processInstancesFromApi);
setReportMetadata(result.report_metadata);
setPagination(result.pagination);
setProcessInstanceFilters(result.filters);
// TODO: need to iron out this interaction some more
if (result.report_identifier !== 'default') {
setProcessInstanceReportSelection({
id: result.report_identifier,
display_name: result.report_identifier,
});
setReportMetadata(result.report.report_metadata);
if (result.report.id) {
setProcessInstanceReportSelection(result.report);
}
}
function getProcessInstances() {
@ -186,14 +206,10 @@ export default function ProcessInstanceListTable({
queryParamString += `&user_filter=${userAppliedFilter}`;
}
let reportIdentifierToUse: any = reportIdentifier;
if (!reportIdentifierToUse) {
reportIdentifierToUse = searchParams.get('report_identifier');
}
if (reportIdentifierToUse) {
queryParamString += `&report_identifier=${reportIdentifierToUse}`;
if (searchParams.get('report_id')) {
queryParamString += `&report_id=${searchParams.get('report_id')}`;
} else if (reportIdentifier) {
queryParamString += `&report_identifier=${reportIdentifier}`;
}
Object.keys(dateParametersToAlwaysFilterBy).forEach(
@ -349,6 +365,30 @@ export default function ProcessInstanceListTable({
processModelAvailableItems,
]);
const processInstanceReportSaveTag = () => {
if (processInstanceReportJustSaved) {
let titleOperation = 'Updated';
if (processInstanceReportJustSaved === 'new') {
titleOperation = 'Created';
}
return (
<>
<InlineNotification
title={`Perspective ${titleOperation}:`}
subtitle={`'${
processInstanceReportSelection
? processInstanceReportSelection.identifier
: ''
}'`}
kind="success"
/>
<br />
</>
);
}
return null;
};
// does the comparison, but also returns false if either argument
// is not truthy and therefore not comparable.
const isTrueComparison = (param1: any, operation: any, param2: any) => {
@ -366,16 +406,8 @@ export default function ProcessInstanceListTable({
}
};
const applyFilter = (event: any) => {
event.preventDefault();
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
undefined,
undefined,
paginationQueryParamPrefix
);
let queryParamString = `per_page=${perPage}&page=${page}&user_filter=true`;
// TODO: after factoring this out page hangs when invalid date ranges and applying the filter
const calculateStartAndEndSeconds = () => {
const startFromSeconds = convertDateAndTimeStringsToSeconds(
startFromDate,
startFromTime || '00:00:00'
@ -392,28 +424,59 @@ export default function ProcessInstanceListTable({
endToDate,
endToTime || '00:00:00'
);
let valid = true;
if (isTrueComparison(startFromSeconds, '>', startToSeconds)) {
setErrorMessage({
message: '"Start date from" cannot be after "start date to"',
});
return;
valid = false;
}
if (isTrueComparison(endFromSeconds, '>', endToSeconds)) {
setErrorMessage({
message: '"End date from" cannot be after "end date to"',
});
return;
valid = false;
}
if (isTrueComparison(startFromSeconds, '>', endFromSeconds)) {
setErrorMessage({
message: '"Start date from" cannot be after "end date from"',
});
return;
valid = false;
}
if (isTrueComparison(startToSeconds, '>', endToSeconds)) {
setErrorMessage({
message: '"Start date to" cannot be after "end date to"',
});
valid = false;
}
return {
valid,
startFromSeconds,
startToSeconds,
endFromSeconds,
endToSeconds,
};
};
const applyFilter = (event: any) => {
event.preventDefault();
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
undefined,
undefined,
paginationQueryParamPrefix
);
let queryParamString = `per_page=${perPage}&page=${page}&user_filter=true`;
const {
valid,
startFromSeconds,
startToSeconds,
endFromSeconds,
endToSeconds,
} = calculateStartAndEndSeconds();
if (!valid) {
return;
}
@ -438,10 +501,11 @@ export default function ProcessInstanceListTable({
}
if (processInstanceReportSelection) {
queryParamString += `&report_identifier=${processInstanceReportSelection.id}`;
queryParamString += `&report_id=${processInstanceReportSelection.id}`;
}
setErrorMessage(null);
setProcessInstanceReportJustSaved(null);
navigate(`/admin/process-instances?${queryParamString}`);
};
@ -529,12 +593,369 @@ export default function ProcessInstanceListTable({
setEndToTime('');
};
const processInstanceReportDidChange = (selection: any, mode?: string) => {
clearFilters();
const selectedReport = selection.selectedItem;
setProcessInstanceReportSelection(selectedReport);
let queryParamString = '';
if (selectedReport) {
queryParamString = `?report_id=${selectedReport.id}`;
}
setErrorMessage(null);
setProcessInstanceReportJustSaved(mode || null);
navigate(`/admin/process-instances${queryParamString}`);
};
const reportColumns = () => {
return (reportMetadata as any).columns;
};
const reportColumnAccessors = () => {
return reportColumns().map((reportColumn: ReportColumn) => {
return reportColumn.accessor;
});
};
// TODO onSuccess reload/select the new report in the report search
const onSaveReportSuccess = (result: any, mode: string) => {
processInstanceReportDidChange(
{
selectedItem: result,
},
mode
);
};
const saveAsReportComponent = () => {
const {
valid,
startFromSeconds,
startToSeconds,
endFromSeconds,
endToSeconds,
} = calculateStartAndEndSeconds();
if (!valid || !reportMetadata) {
return null;
}
return (
<ProcessInstanceListSaveAsReport
onSuccess={onSaveReportSuccess}
buttonClassName="button-white-background narrow-button"
columnArray={reportColumns()}
orderBy=""
buttonText="Save"
processModelSelection={processModelSelection}
processStatusSelection={processStatusSelection}
processInstanceReportSelection={processInstanceReportSelection}
reportMetadata={reportMetadata}
startFromSeconds={startFromSeconds}
startToSeconds={startToSeconds}
endFromSeconds={endFromSeconds}
endToSeconds={endToSeconds}
/>
);
};
const removeColumn = (reportColumn: ReportColumn) => {
if (reportMetadata) {
const reportMetadataCopy = { ...reportMetadata };
const newColumns = reportColumns().filter(
(rc: ReportColumn) => rc.accessor !== reportColumn.accessor
);
Object.assign(reportMetadataCopy, { columns: newColumns });
setReportMetadata(reportMetadataCopy);
}
};
const handleColumnFormClose = () => {
setShowReportColumnForm(false);
setReportColumnFormMode('');
setReportColumnToOperateOn(null);
};
const getFilterByFromReportMetadata = (reportColumnAccessor: string) => {
if (reportMetadata) {
return reportMetadata.filter_by.find((reportFilter: ReportFilter) => {
return reportColumnAccessor === reportFilter.field_name;
});
}
return null;
};
const getNewFiltersFromReportForEditing = (
reportColumnForEditing: ReportColumnForEditing
) => {
if (!reportMetadata) {
return null;
}
const reportMetadataCopy = { ...reportMetadata };
let newReportFilters = reportMetadataCopy.filter_by;
if (reportColumnForEditing.filterable) {
const newReportFilter: ReportFilter = {
field_name: reportColumnForEditing.accessor,
field_value: reportColumnForEditing.filter_field_value,
operator: reportColumnForEditing.filter_operator || 'equals',
};
const existingReportFilter = getFilterByFromReportMetadata(
reportColumnForEditing.accessor
);
if (existingReportFilter) {
const existingReportFilterIndex =
reportMetadataCopy.filter_by.indexOf(existingReportFilter);
if (reportColumnForEditing.filter_field_value) {
newReportFilters[existingReportFilterIndex] = newReportFilter;
} else {
newReportFilters.splice(existingReportFilterIndex, 1);
}
} else if (reportColumnForEditing.filter_field_value) {
newReportFilters = newReportFilters.concat([newReportFilter]);
}
}
return newReportFilters;
};
const handleUpdateReportColumn = () => {
if (reportColumnToOperateOn && reportMetadata) {
const reportMetadataCopy = { ...reportMetadata };
let newReportColumns = null;
if (reportColumnFormMode === 'new') {
newReportColumns = reportColumns().concat([reportColumnToOperateOn]);
} else {
newReportColumns = reportColumns().map((rc: ReportColumn) => {
if (rc.accessor === reportColumnToOperateOn.accessor) {
return reportColumnToOperateOn;
}
return rc;
});
}
Object.assign(reportMetadataCopy, {
columns: newReportColumns,
filter_by: getNewFiltersFromReportForEditing(reportColumnToOperateOn),
});
setReportMetadata(reportMetadataCopy);
setReportColumnToOperateOn(null);
setShowReportColumnForm(false);
setShowReportColumnForm(false);
}
};
const reportColumnToReportColumnForEditing = (reportColumn: ReportColumn) => {
const reportColumnForEditing: ReportColumnForEditing = Object.assign(
reportColumn,
{ filter_field_value: '', filter_operator: '' }
);
const reportFilter = getFilterByFromReportMetadata(
reportColumnForEditing.accessor
);
if (reportFilter) {
reportColumnForEditing.filter_field_value = reportFilter.field_value;
reportColumnForEditing.filter_operator =
reportFilter.operator || 'equals';
}
return reportColumnForEditing;
};
const updateReportColumn = (event: any) => {
const reportColumnForEditing = reportColumnToReportColumnForEditing(
event.selectedItem
);
setReportColumnToOperateOn(reportColumnForEditing);
};
// options includes item and inputValue
const shouldFilterReportColumn = (options: any) => {
const reportColumn: ReportColumn = options.item;
const { inputValue } = options;
return (
!reportColumnAccessors().includes(reportColumn.accessor) &&
(reportColumn.accessor || '')
.toLowerCase()
.includes((inputValue || '').toLowerCase())
);
};
const setReportColumnConditionValue = (event: any) => {
if (reportColumnToOperateOn) {
const reportColumnToOperateOnCopy = {
...reportColumnToOperateOn,
};
reportColumnToOperateOnCopy.filter_field_value = event.target.value;
setReportColumnToOperateOn(reportColumnToOperateOnCopy);
}
};
const reportColumnForm = () => {
if (reportColumnFormMode === '') {
return null;
}
const formElements = [
<TextInput
id="report-column-display-name"
name="report-column-display-name"
labelText="Display Name"
disabled={!reportColumnToOperateOn}
value={reportColumnToOperateOn ? reportColumnToOperateOn.Header : ''}
onChange={(event: any) => {
if (reportColumnToOperateOn) {
const reportColumnToOperateOnCopy = {
...reportColumnToOperateOn,
};
reportColumnToOperateOnCopy.Header = event.target.value;
setReportColumnToOperateOn(reportColumnToOperateOnCopy);
}
}}
/>,
];
if (reportColumnToOperateOn && reportColumnToOperateOn.filterable) {
formElements.push(
<TextInput
id="report-column-condition-value"
name="report-column-condition-value"
labelText="Condition Value"
value={
reportColumnToOperateOn
? reportColumnToOperateOn.filter_field_value
: ''
}
onChange={setReportColumnConditionValue}
/>
);
}
if (reportColumnFormMode === 'new') {
formElements.push(
<ComboBox
onChange={updateReportColumn}
className="combo-box-in-modal"
id="report-column-selection"
data-qa="report-column-selection"
data-modal-primary-focus
items={availableReportColumns}
itemToString={(reportColumn: ReportColumn) => {
if (reportColumn) {
return reportColumn.accessor;
}
return null;
}}
shouldFilterItem={shouldFilterReportColumn}
placeholder="Choose a report column"
titleText="Report Column"
/>
);
}
const modalHeading =
reportColumnFormMode === 'new'
? 'Add Column'
: `Edit ${
reportColumnToOperateOn ? reportColumnToOperateOn.accessor : ''
} column`;
return (
<Modal
open={showReportColumnForm}
modalHeading={modalHeading}
primaryButtonText="Save"
primaryButtonDisabled={!reportColumnToOperateOn}
onRequestSubmit={handleUpdateReportColumn}
onRequestClose={handleColumnFormClose}
hasScrollingContent
>
{formElements}
</Modal>
);
};
const columnSelections = () => {
if (reportColumns()) {
const tags: any = [];
(reportColumns() as any).forEach((reportColumn: ReportColumn) => {
const reportColumnForEditing =
reportColumnToReportColumnForEditing(reportColumn);
let tagType = 'cool-gray';
let tagTypeClass = '';
if (reportColumnForEditing.filterable) {
tagType = 'green';
tagTypeClass = 'tag-type-green';
}
let reportColumnLabel = reportColumnForEditing.Header;
if (reportColumnForEditing.filter_field_value) {
reportColumnLabel = `${reportColumnLabel}=${reportColumnForEditing.filter_field_value}`;
}
tags.push(
<Tag type={tagType} size="sm">
<Button
kind="ghost"
size="sm"
className={`button-tag-icon ${tagTypeClass}`}
title={`Edit ${reportColumnForEditing.accessor}`}
onClick={() => {
setReportColumnToOperateOn(reportColumnForEditing);
setShowReportColumnForm(true);
setReportColumnFormMode('edit');
}}
>
{reportColumnLabel}
</Button>
<Button
data-qa="remove-report-column"
renderIcon={Close}
iconDescription="Remove Column"
className={`button-tag-icon ${tagTypeClass}`}
hasIconOnly
size="sm"
kind="ghost"
onClick={() => removeColumn(reportColumnForEditing)}
/>
</Tag>
);
});
return (
<Stack orientation="horizontal">
{tags}
<Button
data-qa="add-column-button"
renderIcon={AddAlt}
iconDescription="Filter Options"
className="with-tiny-top-margin"
kind="ghost"
hasIconOnly
size="sm"
onClick={() => {
setShowReportColumnForm(true);
setReportColumnFormMode('new');
}}
/>
</Stack>
);
}
return null;
};
const filterOptions = () => {
if (!showFilterOptions) {
return null;
}
// get the columns anytime we display the filter options if they are empty
if (availableReportColumns.length < 1) {
HttpService.makeCallToBackend({
path: `/process-instances/reports/columns`,
successCallback: setAvailableReportColumns,
});
}
return (
<>
<Grid fullWidth className="with-bottom-margin">
<Column md={8} lg={16} sm={4}>
<FormLabel>Columns</FormLabel>
<br />
{columnSelections()}
</Column>
</Grid>
<Grid fullWidth className="with-bottom-margin">
<Column md={8}>
<ProcessModelSearch
@ -598,11 +1019,11 @@ export default function ProcessInstanceListTable({
</Column>
</Grid>
<Grid fullWidth className="with-bottom-margin">
<Column md={4}>
<Column sm={4} md={4} lg={8}>
<ButtonSet>
<Button
kind=""
className="button-white-background"
className="button-white-background narrow-button"
onClick={clearFilters}
>
Clear
@ -611,11 +1032,15 @@ export default function ProcessInstanceListTable({
kind="secondary"
onClick={applyFilter}
data-qa="filter-button"
className="narrow-button"
>
Filter
</Button>
</ButtonSet>
</Column>
<Column sm={4} md={4} lg={8}>
{saveAsReportComponent()}
</Column>
</Grid>
</>
);
@ -635,7 +1060,7 @@ export default function ProcessInstanceListTable({
const getHeaderLabel = (header: string) => {
return headerLabels[header] ?? header;
};
const headers = (reportMetadata as any).columns.map((column: any) => {
const headers = reportColumns().map((column: any) => {
// return <th>{getHeaderLabel((column as any).Header)}</th>;
return getHeaderLabel((column as any).Header);
});
@ -646,7 +1071,7 @@ export default function ProcessInstanceListTable({
return (
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${id}`}
to={`/admin/process-instances/${modifiedProcessModelId}/${id}`}
title={`View process instance ${id}`}
>
{id}
@ -665,22 +1090,6 @@ export default function ProcessInstanceListTable({
);
};
const formatProcessModelDisplayName = (
row: ProcessInstance,
displayName: string
) => {
return (
<Link
to={`/admin/process-models/${modifyProcessIdentifierForPathParam(
row.process_model_identifier
)}`}
title={row.process_model_identifier}
>
{displayName}
</Link>
);
};
const formatSecondsForDisplay = (_row: any, seconds: any) => {
return convertSecondsToFormattedDateTime(seconds) || '-';
};
@ -688,15 +1097,16 @@ export default function ProcessInstanceListTable({
return value;
};
const columnFormatters: Record<string, any> = {
const reportColumnFormatters: Record<string, any> = {
id: formatProcessInstanceId,
process_model_identifier: formatProcessModelIdentifier,
process_model_display_name: formatProcessModelDisplayName,
process_model_display_name: FormatProcessModelDisplayName,
start_in_seconds: formatSecondsForDisplay,
end_in_seconds: formatSecondsForDisplay,
};
const formattedColumn = (row: any, column: any) => {
const formatter = columnFormatters[column.accessor] ?? defaultFormatter;
const formatter =
reportColumnFormatters[column.accessor] ?? defaultFormatter;
const value = row[column.accessor];
if (column.accessor === 'status') {
return (
@ -709,7 +1119,7 @@ export default function ProcessInstanceListTable({
};
const rows = processInstances.map((row: any) => {
const currentRow = (reportMetadata as any).columns.map((column: any) => {
const currentRow = reportColumns().map((column: any) => {
return formattedColumn(row, column);
});
return <tr key={row.id}>{currentRow}</tr>;
@ -738,27 +1148,20 @@ export default function ProcessInstanceListTable({
setShowFilterOptions(!showFilterOptions);
};
const processInstanceReportDidChange = (selection: any) => {
clearFilters();
const selectedReport = selection.selectedItem;
setProcessInstanceReportSelection(selectedReport);
const queryParamString = selectedReport
? `&report_identifier=${selectedReport.id}`
: '';
setErrorMessage(null);
navigate(`/admin/process-instances?${queryParamString}`);
};
const reportSearchComponent = () => {
if (showReports) {
const columns = [
<Column sm={2} md={4} lg={7}>
<ProcessInstanceReportSearch
onChange={processInstanceReportDidChange}
selectedItem={processInstanceReportSelection}
/>
</Column>,
];
return (
<ProcessInstanceReportSearch
onChange={processInstanceReportDidChange}
selectedItem={processInstanceReportSelection}
/>
<Grid className="with-tiny-bottom-margin" fullWidth>
{columns}
</Grid>
);
}
return null;
@ -771,6 +1174,9 @@ export default function ProcessInstanceListTable({
return (
<>
<Grid fullWidth>
<Column sm={2} md={4} lg={7}>
{reportSearchComponent()}
</Column>
<Column
className="filterIcon"
sm={{ span: 1, offset: 3 }}
@ -806,8 +1212,9 @@ export default function ProcessInstanceListTable({
}
return (
<>
{reportColumnForm()}
{processInstanceReportSaveTag()}
{filterComponent()}
{reportSearchComponent()}
<PaginationForTable
page={page}
perPage={perPage}

View File

@ -1,8 +1,11 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import {
ComboBox,
Stack,
FormLabel,
// @ts-ignore
} from '@carbon/react';
import { useSearchParams } from 'react-router-dom';
import { truncateString } from '../helpers';
import { ProcessInstanceReport } from '../interfaces';
import HttpService from '../services/HttpService';
@ -22,52 +25,61 @@ export default function ProcessInstanceReportSearch({
ProcessInstanceReport[] | null
>(null);
function setProcessInstanceReportsFromResult(result: any) {
const processInstanceReportsFromApi = result.map((item: any) => {
return { id: item.identifier, display_name: item.identifier };
});
setProcessInstanceReports(processInstanceReportsFromApi);
}
const [searchParams] = useSearchParams();
const reportId = searchParams.get('report_id');
useEffect(() => {
function setProcessInstanceReportsFromResult(
result: ProcessInstanceReport[]
) {
setProcessInstanceReports(result);
}
if (processInstanceReports === null) {
setProcessInstanceReports([]);
HttpService.makeCallToBackend({
path: `/process-instances/reports`,
successCallback: setProcessInstanceReportsFromResult,
});
}
}, [reportId]);
const reportSelectionString = (
processInstanceReport: ProcessInstanceReport
) => {
return `${truncateString(processInstanceReport.identifier, 20)} (Id: ${
processInstanceReport.id
})`;
};
const shouldFilterProcessInstanceReport = (options: any) => {
const processInstanceReport: ProcessInstanceReport = options.item;
const { inputValue } = options;
return `${processInstanceReport.id} (${processInstanceReport.display_name})`.includes(
inputValue
);
return reportSelectionString(processInstanceReport).includes(inputValue);
};
const reportsAvailable = () => {
return processInstanceReports && processInstanceReports.length > 0;
};
return reportsAvailable() ? (
<ComboBox
onChange={onChange}
id="process-instance-report-select"
data-qa="process-instance-report-selection"
items={processInstanceReports}
itemToString={(processInstanceReport: ProcessInstanceReport) => {
if (processInstanceReport) {
return `${processInstanceReport.id} (${truncateString(
processInstanceReport.display_name,
20
)})`;
}
return null;
}}
shouldFilterItem={shouldFilterProcessInstanceReport}
placeholder="Choose a process instance perspective"
titleText={titleText}
selectedItem={selectedItem}
/>
) : null;
if (reportsAvailable()) {
return (
<Stack orientation="horizontal" gap={2}>
<FormLabel className="with-top-margin">{titleText}</FormLabel>
<ComboBox
onChange={onChange}
id="process-instance-report-select"
data-qa="process-instance-report-selection"
items={processInstanceReports}
itemToString={(processInstanceReport: ProcessInstanceReport) => {
if (processInstanceReport) {
return reportSelectionString(processInstanceReport);
}
return null;
}}
shouldFilterItem={shouldFilterProcessInstanceReport}
placeholder="Choose a process instance perspective"
selectedItem={selectedItem}
/>
</Stack>
);
}
return null;
}

View File

@ -83,9 +83,9 @@ export default function ProcessInstanceRun({
processModel.id
);
const processInstanceActionPath = `/v1.0/process-models/${modifiedProcessModelId}/process-instances`;
const processInstanceCreatePath = `/v1.0/process-instances/${modifiedProcessModelId}`;
let permissionRequestData: PermissionsToCheck = {
[processInstanceActionPath]: ['POST'],
[processInstanceCreatePath]: ['POST'],
};
if (!checkPermissions) {
@ -117,14 +117,14 @@ export default function ProcessInstanceRun({
const processInstanceCreateAndRun = () => {
HttpService.makeCallToBackend({
path: processInstanceActionPath,
path: processInstanceCreatePath,
successCallback: processModelRun,
httpMethod: 'POST',
});
};
if (checkPermissions) {
return (
<Can I="POST" a={processInstanceActionPath} ability={ability}>
<Can I="POST" a={processInstanceCreatePath} ability={ability}>
<Button onClick={processInstanceCreateAndRun} className={className}>
Start
</Button>

View File

@ -1,10 +1,20 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Button,
ButtonSet,
Form,
Stack,
TextInput,
Grid,
Column,
// @ts-ignore
} from '@carbon/react';
// @ts-ignore
import { Button, ButtonSet, Form, Stack, TextInput } from '@carbon/react';
import { AddAlt, TrashCan } from '@carbon/icons-react';
import { modifyProcessIdentifierForPathParam, slugifyString } from '../helpers';
import HttpService from '../services/HttpService';
import { ProcessModel } from '../interfaces';
import { MetadataExtractionPath, ProcessModel } from '../interfaces';
type OwnProps = {
mode: string;
@ -23,6 +33,7 @@ export default function ProcessModelForm({
const [idHasBeenUpdatedByUser, setIdHasBeenUpdatedByUser] =
useState<boolean>(false);
const [displayNameInvalid, setDisplayNameInvalid] = useState<boolean>(false);
useState<boolean>(false);
const navigate = useNavigate();
const navigateToProcessModel = (result: ProcessModel) => {
@ -64,6 +75,7 @@ export default function ProcessModelForm({
const postBody = {
display_name: processModel.display_name,
description: processModel.description,
metadata_extraction_paths: processModel.metadata_extraction_paths,
};
if (mode === 'new') {
Object.assign(postBody, {
@ -87,6 +99,80 @@ export default function ProcessModelForm({
setProcessModel(processModelToCopy);
};
const metadataExtractionPathForm = (
index: number,
metadataExtractionPath: MetadataExtractionPath
) => {
return (
<Grid>
<Column md={3} lg={7} sm={1}>
<TextInput
id={`process-model-metadata-extraction-path-key-${index}`}
labelText="Extraction Key"
value={metadataExtractionPath.key}
onChange={(event: any) => {
const cep: MetadataExtractionPath[] =
processModel.metadata_extraction_paths || [];
const newMeta = { ...metadataExtractionPath };
newMeta.key = event.target.value;
cep[index] = newMeta;
updateProcessModel({ metadata_extraction_paths: cep });
}}
/>
</Column>
<Column md={4} lg={8} sm={2}>
<TextInput
id={`process-model-metadata-extraction-path-${index}`}
labelText="Extraction Path"
value={metadataExtractionPath.path}
onChange={(event: any) => {
const cep: MetadataExtractionPath[] =
processModel.metadata_extraction_paths || [];
const newMeta = { ...metadataExtractionPath };
newMeta.path = event.target.value;
cep[index] = newMeta;
updateProcessModel({ metadata_extraction_paths: cep });
}}
/>
</Column>
<Column md={1} lg={1} sm={1}>
<Button
kind="ghost"
renderIcon={TrashCan}
iconDescription="Remove Key"
hasIconOnly
size="lg"
className="with-extra-top-margin"
onClick={() => {
const cep: MetadataExtractionPath[] =
processModel.metadata_extraction_paths || [];
cep.splice(index, 1);
updateProcessModel({ metadata_extraction_paths: cep });
}}
/>
</Column>
</Grid>
);
};
const metadataExtractionPathFormArea = () => {
if (processModel.metadata_extraction_paths) {
return processModel.metadata_extraction_paths.map(
(metadataExtractionPath: MetadataExtractionPath, index: number) => {
return metadataExtractionPathForm(index, metadataExtractionPath);
}
);
}
return null;
};
const addBlankMetadataExtractionPath = () => {
const cep: MetadataExtractionPath[] =
processModel.metadata_extraction_paths || [];
cep.push({ key: '', path: '' });
updateProcessModel({ metadata_extraction_paths: cep });
};
const onDisplayNameChanged = (newDisplayName: any) => {
setDisplayNameInvalid(false);
const updateDict = { display_name: newDisplayName };
@ -108,7 +194,6 @@ export default function ProcessModelForm({
onChange={(event: any) => {
onDisplayNameChanged(event.target.value);
}}
onBlur={(event: any) => console.log('event', event)}
/>,
];
@ -145,6 +230,38 @@ export default function ProcessModelForm({
/>
);
textInputs.push(<h2>Metadata Extractions</h2>);
textInputs.push(
<Grid>
<Column md={8} lg={16} sm={4}>
<p className="data-table-description">
You can provide one or more metadata extractions to pull data from
your process instances to provide quick access in searches and
perspectives.
</p>
</Column>
</Grid>
);
textInputs.push(<>{metadataExtractionPathFormArea()}</>);
textInputs.push(
<Grid>
<Column md={4} lg={8} sm={2}>
<Button
data-qa="add-metadata-extraction-path-button"
renderIcon={AddAlt}
className="button-white-background"
kind=""
size="sm"
onClick={() => {
addBlankMetadataExtractionPath();
}}
>
Add Metadata Extraction Path
</Button>
</Column>
</Grid>
);
return textInputs;
};

View File

@ -54,9 +54,9 @@ export default function ProcessModelListTiles({
<p>
Process Instance {processInstance.id} kicked off (
<Link
to={`/admin/process-models/${modifyProcessIdentifierForPathParam(
to={`/admin/process-instances/${modifyProcessIdentifierForPathParam(
processInstance.process_model_identifier
)}/process-instances/${processInstance.id}`}
)}/${processInstance.id}`}
data-qa="process-instance-show-link"
>
view

View File

@ -55,7 +55,7 @@ export default function MyOpenProcesses() {
<td>
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}/process-instances/${rowToUse.process_instance_id}`}
to={`/admin/process-instances/${modifiedProcessModelIdentifier}/${rowToUse.process_instance_id}`}
title={`View process instance ${rowToUse.process_instance_id}`}
>
{rowToUse.process_instance_id}

View File

@ -47,7 +47,7 @@ export default function TasksWaitingForMe() {
<td>
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}/process-instances/${rowToUse.process_instance_id}`}
to={`/admin/${modifiedProcessModelIdentifier}/${rowToUse.process_instance_id}`}
title={`View process instance ${rowToUse.process_instance_id}`}
>
{rowToUse.process_instance_id}

View File

@ -55,7 +55,7 @@ export default function TasksWaitingForMyGroups() {
<td>
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}/process-instances/${rowToUse.process_instance_id}`}
to={`/admin/process-instances/${modifiedProcessModelIdentifier}/${rowToUse.process_instance_id}`}
title={`View process instance ${rowToUse.process_instance_id}`}
>
{rowToUse.process_instance_id}

View File

@ -14,6 +14,7 @@ export const PROCESS_STATUSES = [
'complete',
'error',
'suspended',
'terminated',
];
// with time: yyyy-MM-dd HH:mm:ss

View File

@ -9,10 +9,12 @@ export const useUriListForPermissions = () => {
messageInstanceListPath: '/v1.0/messages',
processGroupListPath: '/v1.0/process-groups',
processGroupShowPath: `/v1.0/process-groups/${params.process_group_id}`,
processInstanceActionPath: `/v1.0/process-models/${params.process_model_id}/process-instances`,
processInstanceCreatePath: `/v1.0/process-instances/${params.process_model_id}`,
processInstanceActionPath: `/v1.0/process-instances/${params.process_model_id}/${params.process_instance_id}`,
processInstanceListPath: '/v1.0/process-instances',
processInstanceTaskListPath: `/v1.0/process-instances/${params.process_model_id}/${params.process_instance_id}/tasks`,
processInstanceLogListPath: `/v1.0/logs/${params.process_model_id}/${params.process_instance_id}`,
processInstanceReportListPath: '/v1.0/process-instances/reports',
processInstanceTaskListPath: `/v1.0/task-data/${params.process_model_id}/${params.process_instance_id}`,
processModelCreatePath: `/v1.0/process-models/${params.process_group_id}`,
processModelFileCreatePath: `/v1.0/process-models/${params.process_model_id}/files`,
processModelFileShowPath: `/v1.0/process-models/${params.process_model_id}/files/${params.file_name}`,

View File

@ -69,6 +69,24 @@ h2 {
color: black;
}
/* match normal link colors */
.cds--btn--ghost.button-link {
color: #0062fe;
padding-left: 0;
}
.cds--btn--ghost.button-link:visited {
color: #0062fe;
padding-left: 0;
}
.cds--btn--ghost.button-link:hover {
color: #0062fe;
padding-left: 0;
}
.cds--btn--ghost.button-link:visited:hover {
color: #0062fe;
padding-left: 0;
}
.cds--header__global .cds--btn--primary {
background-color: #161616
}
@ -151,10 +169,22 @@ h1.with-icons {
margin-top: 1em;
}
.with-extra-top-margin {
margin-top: 1.3em;
}
.with-tiny-top-margin {
margin-top: 4px;
}
.with-large-bottom-margin {
margin-bottom: 3em;
}
.with-tiny-bottom-margin {
margin-bottom: 4px;
}
.diagram-viewer-canvas {
border:1px solid #000000;
height:70vh;
@ -297,3 +327,38 @@ td.actions-cell {
text-align: right;
padding-bottom: 10px;
}
.cds--btn--ghost:not([disabled]) svg.red-icon {
fill: red;
}
.failure-string {
color: red;
}
.cds--btn--ghost.cds--btn--sm.button-tag-icon {
padding-left: 0;
padding-right: 0;
padding-top: 0;
}
/* .no-wrap cds--label cds--label--inline cds--label--inline--md{ */
.no-wrap .cds--label--inline{
word-break: normal;
}
.combo-box-in-modal {
height: 300px;
}
.cds--btn.narrow-button {
max-width: 10rem;
min-width: 5rem;
word-break: normal;
}
.tag-type-green:hover {
background-color: #00FF00;
}

View File

@ -12,9 +12,8 @@ export interface RecentProcessModel {
}
export interface ProcessReference {
id: string; // The unique id of the process or decision table.
name: string; // The process or decision Display name.
identifier: string;
identifier: string; // The unique id of the process
display_name: string;
process_group_id: string;
process_model_id: string;
@ -39,11 +38,58 @@ export interface ProcessFile {
export interface ProcessInstance {
id: number;
process_model_identifier: string;
process_model_display_name: string;
}
export interface MessageCorrelationProperties {
[key: string]: string;
}
export interface MessageCorrelations {
[key: string]: MessageCorrelationProperties;
}
export interface MessageInstance {
id: number;
process_model_identifier: string;
process_model_display_name: string;
process_instance_id: number;
message_identifier: string;
message_type: string;
failure_cause: string;
status: string;
created_at_in_seconds: number;
message_correlations?: MessageCorrelations;
}
export interface ReportFilter {
field_name: string;
field_value: string;
operator?: string;
}
export interface ReportColumn {
Header: string;
accessor: string;
filterable: boolean;
}
export interface ReportColumnForEditing extends ReportColumn {
filter_field_value: string;
filter_operator: string;
}
export interface ReportMetadata {
columns: ReportColumn[];
filter_by: ReportFilter[];
order_by: string[];
}
export interface ProcessInstanceReport {
id: string;
display_name: string;
id: number;
identifier: string;
name: string;
report_metadata: ReportMetadata;
}
export interface ProcessGroupLite {
@ -51,6 +97,11 @@ export interface ProcessGroupLite {
display_name: string;
}
export interface MetadataExtractionPath {
key: string;
path: string;
}
export interface ProcessModel {
id: string;
description: string;
@ -58,6 +109,7 @@ export interface ProcessModel {
primary_file_name: string;
files: ProcessFile[];
parent_groups?: ProcessGroupLite[];
metadata_extraction_paths?: MetadataExtractionPath[];
}
export interface ProcessGroup {

View File

@ -71,11 +71,11 @@ export default function AdminRoutes() {
element={<ProcessModelEdit />}
/>
<Route
path="process-models/:process_model_id/process-instances/:process_instance_id"
path="process-instances/:process_model_id/:process_instance_id"
element={<ProcessInstanceShow />}
/>
<Route
path="process-models/:process_model_id/process-instances/:process_instance_id/:spiff_step"
path="process-instances/:process_model_id/:process_instance_id/:spiff_step"
element={<ProcessInstanceShow />}
/>
<Route
@ -103,7 +103,7 @@ export default function AdminRoutes() {
element={<ReactFormEditor />}
/>
<Route
path="process-models/:process_model_id/process-instances/:process_instance_id/logs"
path="logs/:process_model_id/:process_instance_id"
element={<ProcessInstanceLogList />}
/>
<Route path="process-instances" element={<ProcessInstanceList />} />

View File

@ -1,15 +1,19 @@
import { useEffect, useState } from 'react';
// @ts-ignore
import { Table } from '@carbon/react';
import { ErrorOutline } from '@carbon/icons-react';
// @ts-ignore
import { Table, Modal, Button } from '@carbon/react';
import { Link, useParams, useSearchParams } from 'react-router-dom';
import PaginationForTable from '../components/PaginationForTable';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import {
convertSecondsToFormattedDateString,
convertSecondsToFormattedDateTime,
getPageInfoFromSearchParams,
modifyProcessIdentifierForPathParam,
} from '../helpers';
import HttpService from '../services/HttpService';
import { FormatProcessModelDisplayName } from '../components/MiniComponents';
import { MessageInstance } from '../interfaces';
export default function MessageInstanceList() {
const params = useParams();
@ -17,6 +21,9 @@ export default function MessageInstanceList() {
const [messageIntances, setMessageInstances] = useState([]);
const [pagination, setPagination] = useState(null);
const [messageInstanceForModal, setMessageInstanceForModal] =
useState<MessageInstance | null>(null);
useEffect(() => {
const setMessageInstanceListFromResult = (result: any) => {
setMessageInstances(result.results);
@ -35,41 +42,89 @@ export default function MessageInstanceList() {
});
}, [searchParams, params]);
const buildTable = () => {
// return null;
const rows = messageIntances.map((row) => {
const rowToUse = row as any;
const handleCorrelationDisplayClose = () => {
setMessageInstanceForModal(null);
};
const correlationsDisplayModal = () => {
if (messageInstanceForModal) {
let failureCausePre = null;
if (messageInstanceForModal.failure_cause) {
failureCausePre = (
<>
<p className="failure-string">
{messageInstanceForModal.failure_cause}
</p>
<br />
</>
);
}
return (
<tr key={rowToUse.id}>
<td>{rowToUse.id}</td>
<td>
<Link
data-qa="process-model-show-link"
to={`/admin/process-models/${modifyProcessIdentifierForPathParam(
rowToUse.process_model_identifier
)}`}
>
{rowToUse.process_model_identifier}
</Link>
</td>
<Modal
open={!!messageInstanceForModal}
passiveModal
onRequestClose={handleCorrelationDisplayClose}
modalHeading={`Message ${messageInstanceForModal.id} (${messageInstanceForModal.message_identifier}) ${messageInstanceForModal.message_type} data:`}
modalLabel="Details"
>
{failureCausePre}
<p>Correlations:</p>
<pre>
{JSON.stringify(
messageInstanceForModal.message_correlations,
null,
2
)}
</pre>
</Modal>
);
}
return null;
};
const buildTable = () => {
const rows = messageIntances.map((row: MessageInstance) => {
let errorIcon = null;
let errorTitle = null;
if (row.failure_cause) {
errorTitle = 'Instance has an error';
errorIcon = (
<>
&nbsp;
<ErrorOutline className="red-icon" />
</>
);
}
return (
<tr key={row.id}>
<td>{row.id}</td>
<td>{FormatProcessModelDisplayName(row)}</td>
<td>
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifyProcessIdentifierForPathParam(
rowToUse.process_model_identifier
)}/process-instances/${rowToUse.process_instance_id}`}
to={`/admin/process-instances/${modifyProcessIdentifierForPathParam(
row.process_model_identifier
)}/${row.process_instance_id}`}
>
{rowToUse.process_instance_id}
{row.process_instance_id}
</Link>
</td>
<td>{rowToUse.message_identifier}</td>
<td>{rowToUse.message_type}</td>
<td>{rowToUse.failure_cause || '-'}</td>
<td>{rowToUse.status}</td>
<td>{row.message_identifier}</td>
<td>{row.message_type}</td>
<td>
{convertSecondsToFormattedDateString(
rowToUse.created_at_in_seconds
)}
<Button
kind="ghost"
className="button-link"
onClick={() => setMessageInstanceForModal(row)}
title={errorTitle}
>
View
{errorIcon}
</Button>
</td>
<td>{row.status}</td>
<td>
{convertSecondsToFormattedDateTime(row.created_at_in_seconds)}
</td>
</tr>
);
@ -78,12 +133,12 @@ export default function MessageInstanceList() {
<Table striped bordered>
<thead>
<tr>
<th>Instance Id</th>
<th>Process Model</th>
<th>Id</th>
<th>Process</th>
<th>Process Instance</th>
<th>Message Model</th>
<th>Name</th>
<th>Type</th>
<th>Failure Cause</th>
<th>Details</th>
<th>Status</th>
<th>Created At</th>
</tr>
@ -108,9 +163,9 @@ export default function MessageInstanceList() {
},
[
`Process Instance: ${searchParams.get('process_instance_id')}`,
`/admin/process-models/${searchParams.get(
`/admin/process-instances/${searchParams.get(
'process_model_id'
)}/process-instances/${searchParams.get('process_instance_id')}`,
)}/${searchParams.get('process_instance_id')}`,
],
['Messages'],
]}
@ -121,6 +176,7 @@ export default function MessageInstanceList() {
<>
{breadcrumbElement}
<h1>Messages</h1>
{correlationsDisplayModal()}
<PaginationForTable
page={page}
perPage={perPage}

View File

@ -55,9 +55,9 @@ export default function MyTasks() {
<p>
Process Instance {processInstance.id} kicked off (
<Link
to={`/admin/process-models/${modifyProcessIdentifierForPathParam(
to={`/admin/process-instances/${modifyProcessIdentifierForPathParam(
processInstance.process_model_identifier
)}/process-instances/${processInstance.id}`}
)}/${processInstance.id}`}
data-qa="process-instance-show-link"
>
view
@ -95,7 +95,7 @@ export default function MyTasks() {
<td>
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}/process-instances/${rowToUse.process_instance_id}`}
to={`/admin/process-instances/${modifiedProcessModelIdentifier}/${rowToUse.process_instance_id}`}
>
{rowToUse.process_instance_id}
</Link>

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
// @ts-ignore
import { Table } from '@carbon/react';
import { Table, Tabs, TabList, Tab } from '@carbon/react';
import { useParams, useSearchParams, Link } from 'react-router-dom';
import PaginationForTable from '../components/PaginationForTable';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
@ -10,15 +10,18 @@ import {
convertSecondsToFormattedDateTime,
} from '../helpers';
import HttpService from '../services/HttpService';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
export default function ProcessInstanceLogList() {
const params = useParams();
const [searchParams] = useSearchParams();
const [searchParams, setSearchParams] = useSearchParams();
const [processInstanceLogs, setProcessInstanceLogs] = useState([]);
const [pagination, setPagination] = useState(null);
const modifiedProcessModelId = modifyProcessIdentifierForPathParam(
`${params.process_model_id}`
);
const { targetUris } = useUriListForPermissions();
const isDetailedView = searchParams.get('detailed') === 'true';
useEffect(() => {
const setProcessInstanceLogListFromResult = (result: any) => {
@ -27,26 +30,36 @@ export default function ProcessInstanceLogList() {
};
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
HttpService.makeCallToBackend({
path: `/process-instances/${params.process_instance_id}/logs?per_page=${perPage}&page=${page}`,
path: `${targetUris.processInstanceLogListPath}?per_page=${perPage}&page=${page}&detailed=${isDetailedView}`,
successCallback: setProcessInstanceLogListFromResult,
});
}, [searchParams, params]);
}, [
searchParams,
params,
targetUris.processInstanceLogListPath,
isDetailedView,
]);
const buildTable = () => {
const rows = processInstanceLogs.map((row) => {
const rowToUse = row as any;
return (
<tr key={rowToUse.id}>
<td>{rowToUse.bpmn_process_identifier}</td>
<td>{rowToUse.id}</td>
<td>{rowToUse.message}</td>
<td>{rowToUse.bpmn_task_identifier}</td>
<td>{rowToUse.bpmn_task_name}</td>
<td>{rowToUse.bpmn_task_type}</td>
{isDetailedView && (
<>
<td>{rowToUse.bpmn_task_identifier}</td>
<td>{rowToUse.bpmn_task_type}</td>
<td>{rowToUse.bpmn_process_identifier}</td>
</>
)}
<td>{rowToUse.username}</td>
<td>
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${rowToUse.process_instance_id}/${rowToUse.spiff_step}`}
to={`/admin/process-instances/${modifiedProcessModelId}/${rowToUse.process_instance_id}/${rowToUse.spiff_step}`}
>
{convertSecondsToFormattedDateTime(rowToUse.timestamp)}
</Link>
@ -58,11 +71,16 @@ export default function ProcessInstanceLogList() {
<Table size="lg">
<thead>
<tr>
<th>Bpmn Process Identifier</th>
<th>Id</th>
<th>Message</th>
<th>Task Identifier</th>
<th>Task Name</th>
<th>Task Type</th>
{isDetailedView && (
<>
<th>Task Identifier</th>
<th>Task Type</th>
<th>Bpmn Process Identifier</th>
</>
)}
<th>User</th>
<th>Timestamp</th>
</tr>
@ -71,11 +89,12 @@ export default function ProcessInstanceLogList() {
</Table>
);
};
const selectedTabIndex = isDetailedView ? 1 : 0;
if (pagination) {
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
return (
<main>
<>
<ProcessBreadcrumb
hotCrumbs={[
['Process Groups', '/admin'],
@ -86,18 +105,41 @@ export default function ProcessInstanceLogList() {
},
[
`Process Instance: ${params.process_instance_id}`,
`/admin/process-models/${params.process_model_id}/process-instances/${params.process_instance_id}`,
`/admin/process-instances/${params.process_model_id}/${params.process_instance_id}`,
],
['Logs'],
]}
/>
<Tabs selectedIndex={selectedTabIndex}>
<TabList aria-label="List of tabs">
<Tab
title="Only show a subset of the logs, and show fewer columns"
onClick={() => {
searchParams.set('detailed', 'false');
setSearchParams(searchParams);
}}
>
Simple
</Tab>
<Tab
title="Show all logs for this process instance, and show extra columns that may be useful for debugging"
onClick={() => {
searchParams.set('detailed', 'true');
setSearchParams(searchParams);
}}
>
Detailed
</Tab>
</TabList>
</Tabs>
<br />
<PaginationForTable
page={page}
perPage={perPage}
pagination={pagination}
tableToDisplay={buildTable()}
/>
</main>
</>
);
}
return null;

View File

@ -59,6 +59,11 @@ export default function ProcessInstanceShow() {
const permissionRequestData: PermissionsToCheck = {
[targetUris.messageInstanceListPath]: ['GET'],
[targetUris.processInstanceTaskListPath]: ['GET'],
[targetUris.processInstanceActionPath]: ['DELETE'],
[targetUris.processInstanceLogListPath]: ['GET'],
[`${targetUris.processInstanceActionPath}/suspend`]: ['PUT'],
[`${targetUris.processInstanceActionPath}/terminate`]: ['PUT'],
[`${targetUris.processInstanceActionPath}/resume`]: ['PUT'],
};
const { ability, permissionsLoaded } = usePermissionFetcher(
permissionRequestData
@ -76,7 +81,7 @@ export default function ProcessInstanceShow() {
setTasksCallHadError(true);
};
HttpService.makeCallToBackend({
path: `/process-models/${modifiedProcessModelId}/process-instances/${params.process_instance_id}`,
path: `/process-instances/${modifiedProcessModelId}/${params.process_instance_id}`,
successCallback: setProcessInstance,
});
let taskParams = '?all_tasks=true';
@ -85,7 +90,7 @@ export default function ProcessInstanceShow() {
}
if (ability.can('GET', targetUris.processInstanceTaskListPath)) {
HttpService.makeCallToBackend({
path: `/process-instances/${modifiedProcessModelId}/${params.process_instance_id}/tasks${taskParams}`,
path: `${targetUris.processInstanceTaskListPath}${taskParams}`,
successCallback: setTasks,
failureCallback: processTaskFailure,
});
@ -97,7 +102,7 @@ export default function ProcessInstanceShow() {
const deleteProcessInstance = () => {
HttpService.makeCallToBackend({
path: `/process-instances/${params.process_instance_id}`,
path: targetUris.processInstanceActionPath,
successCallback: navigateToProcessInstances,
httpMethod: 'DELETE',
});
@ -110,7 +115,7 @@ export default function ProcessInstanceShow() {
const terminateProcessInstance = () => {
HttpService.makeCallToBackend({
path: `/process-instances/${params.process_instance_id}/terminate`,
path: `${targetUris.processInstanceActionPath}/terminate`,
successCallback: refreshPage,
httpMethod: 'POST',
});
@ -118,7 +123,7 @@ export default function ProcessInstanceShow() {
const suspendProcessInstance = () => {
HttpService.makeCallToBackend({
path: `/process-instances/${params.process_instance_id}/suspend`,
path: `${targetUris.processInstanceActionPath}/suspend`,
successCallback: refreshPage,
httpMethod: 'POST',
});
@ -126,7 +131,7 @@ export default function ProcessInstanceShow() {
const resumeProcessInstance = () => {
HttpService.makeCallToBackend({
path: `/process-instances/${params.process_instance_id}/resume`,
path: `${targetUris.processInstanceActionPath}/resume`,
successCallback: refreshPage,
httpMethod: 'POST',
});
@ -174,7 +179,7 @@ export default function ProcessInstanceShow() {
<Link
reloadDocument
data-qa="process-instance-step-link"
to={`/admin/process-models/${
to={`/admin/process-instances/${
params.process_model_id
}/process-instances/${params.process_instance_id}/${
currentSpiffStep(processInstanceToUse) + distance
@ -209,7 +214,7 @@ export default function ProcessInstanceShow() {
if (currentEndDate) {
currentEndDateTag = (
<Grid condensed fullWidth>
<Column sm={1} md={1} lg={1} className="grid-list-title">
<Column sm={1} md={1} lg={2} className="grid-list-title">
Completed:{' '}
</Column>
<Column sm={3} md={3} lg={3} className="grid-date">
@ -235,7 +240,7 @@ export default function ProcessInstanceShow() {
return (
<>
<Grid condensed fullWidth>
<Column sm={1} md={1} lg={1} className="grid-list-title">
<Column sm={1} md={1} lg={2} className="grid-list-title">
Started:{' '}
</Column>
<Column sm={3} md={3} lg={3} className="grid-date">
@ -246,7 +251,7 @@ export default function ProcessInstanceShow() {
</Grid>
{currentEndDateTag}
<Grid condensed fullWidth>
<Column sm={1} md={1} lg={1} className="grid-list-title">
<Column sm={1} md={1} lg={2} className="grid-list-title">
Status:{' '}
</Column>
<Column sm={3} md={3} lg={3}>
@ -259,14 +264,20 @@ export default function ProcessInstanceShow() {
<Grid condensed fullWidth>
<Column sm={2} md={2} lg={2}>
<ButtonSet>
<Button
size="sm"
className="button-white-background"
data-qa="process-instance-log-list-link"
href={`/admin/process-models/${modifiedProcessModelId}/process-instances/${params.process_instance_id}/logs`}
<Can
I="GET"
a={targetUris.processInstanceLogListPath}
ability={ability}
>
Logs
</Button>
<Button
size="sm"
className="button-white-background"
data-qa="process-instance-log-list-link"
href={`/admin/logs/${modifiedProcessModelId}/${params.process_instance_id}`}
>
Logs
</Button>
</Can>
<Can
I="GET"
a={targetUris.messageInstanceListPath}
@ -436,8 +447,8 @@ export default function ProcessInstanceShow() {
// taskToUse is copy of taskToDisplay, with taskDataToDisplay in data attribute
const taskToUse: any = { ...taskToDisplay, data: taskDataToDisplay };
HttpService.makeCallToBackend({
path: `/process-instances/${params.process_instance_id}/task/${taskToUse.id}/update`,
httpMethod: 'POST',
path: `/task-data/${modifiedProcessModelId}/${params.process_instance_id}/${taskToUse.id}`,
httpMethod: 'PUT',
successCallback: saveTaskDataResult,
failureCallback: saveTaskDataFailure,
postBody: {
@ -544,21 +555,33 @@ export default function ProcessInstanceShow() {
const buttonIcons = (processInstanceToUse: any) => {
const elements = [];
elements.push(terminateButton(processInstanceToUse));
elements.push(suspendButton(processInstanceToUse));
elements.push(resumeButton(processInstanceToUse));
elements.push(
<ButtonWithConfirmation
data-qa="process-instance-delete"
kind="ghost"
renderIcon={TrashCan}
iconDescription="Delete"
hasIconOnly
description={`Delete Process Instance: ${processInstanceToUse.id}`}
onConfirmation={deleteProcessInstance}
confirmButtonLabel="Delete"
/>
);
if (
ability.can('POST', `${targetUris.processInstanceActionPath}/terminate`)
) {
elements.push(terminateButton(processInstanceToUse));
}
if (
ability.can('POST', `${targetUris.processInstanceActionPath}/suspend`)
) {
elements.push(suspendButton(processInstanceToUse));
}
if (ability.can('POST', `${targetUris.processInstanceActionPath}/resume`)) {
elements.push(resumeButton(processInstanceToUse));
}
if (ability.can('DELETE', targetUris.processInstanceActionPath)) {
elements.push(
<ButtonWithConfirmation
data-qa="process-instance-delete"
kind="ghost"
renderIcon={TrashCan}
iconDescription="Delete"
hasIconOnly
description={`Delete Process Instance: ${processInstanceToUse.id}`}
onConfirmation={deleteProcessInstance}
confirmButtonLabel="Delete"
/>
);
}
return elements;
};

View File

@ -283,7 +283,7 @@ export default function ProcessModelEditDiagram() {
const onServiceTasksRequested = (event: any) => {
HttpService.makeCallToBackend({
path: `/service_tasks`,
path: `/service-tasks`,
successCallback: makeApiHandler(event),
});
};
@ -735,7 +735,7 @@ export default function ProcessModelEditDiagram() {
if (processModel) {
const files = processModel.files.filter((f) => f.type === type);
files.some((file) => {
if (file.references.some((ref) => ref.id === id)) {
if (file.references.some((ref) => ref.identifier === id)) {
matchFile = file;
return true;
}

View File

@ -66,7 +66,7 @@ export default function ProcessModelShow() {
const permissionRequestData: PermissionsToCheck = {
[targetUris.processModelShowPath]: ['PUT', 'DELETE'],
[targetUris.processInstanceListPath]: ['GET'],
[targetUris.processInstanceActionPath]: ['POST'],
[targetUris.processInstanceCreatePath]: ['POST'],
[targetUris.processModelFileCreatePath]: ['POST', 'PUT', 'GET', 'DELETE'],
};
const { ability, permissionsLoaded } = usePermissionFetcher(
@ -95,7 +95,7 @@ export default function ProcessModelShow() {
<p>
Process Instance {processInstance.id} kicked off (
<Link
to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${processInstance.id}`}
to={`/admin/process-instances/${modifiedProcessModelId}/${processInstance.id}`}
data-qa="process-instance-show-link"
>
view
@ -556,7 +556,7 @@ export default function ProcessModelShow() {
<Stack orientation="horizontal" gap={3}>
<Can
I="POST"
a={targetUris.processInstanceActionPath}
a={targetUris.processInstanceCreatePath}
ability={ability}
>
<>

View File

@ -26,6 +26,9 @@ import Form from '../themes/carbon';
import HttpService from '../services/HttpService';
import ErrorContext from '../contexts/ErrorContext';
import { modifyProcessIdentifierForPathParam } from '../helpers';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { PermissionsToCheck } from '../interfaces';
import { usePermissionFetcher } from '../hooks/PermissionService';
export default function TaskShow() {
const [task, setTask] = useState(null);
@ -35,24 +38,36 @@ export default function TaskShow() {
const setErrorMessage = (useContext as any)(ErrorContext)[1];
useEffect(() => {
const processResult = (result: any) => {
setTask(result);
HttpService.makeCallToBackend({
path: `/process-instances/${modifyProcessIdentifierForPathParam(
result.process_model_identifier
)}/${params.process_instance_id}/tasks`,
successCallback: setUserTasks,
});
};
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {
[targetUris.processInstanceTaskListPath]: ['GET'],
};
const { ability, permissionsLoaded } = usePermissionFetcher(
permissionRequestData
);
HttpService.makeCallToBackend({
path: `/tasks/${params.process_instance_id}/${params.task_id}`,
successCallback: processResult,
// This causes the page to continuously reload
// failureCallback: setErrorMessage,
});
}, [params]);
useEffect(() => {
if (permissionsLoaded) {
const processResult = (result: any) => {
setTask(result);
if (ability.can('GET', targetUris.processInstanceTaskListPath)) {
HttpService.makeCallToBackend({
path: `/task-data/${modifyProcessIdentifierForPathParam(
result.process_model_identifier
)}/${params.process_instance_id}`,
successCallback: setUserTasks,
});
}
};
HttpService.makeCallToBackend({
path: `/tasks/${params.process_instance_id}/${params.task_id}`,
successCallback: processResult,
// This causes the page to continuously reload
// failureCallback: setErrorMessage,
});
}
}, [params, permissionsLoaded, ability, targetUris]);
const processSubmitResult = (result: any) => {
setErrorMessage(null);
@ -116,17 +131,18 @@ export default function TaskShow() {
}
return null;
});
return (
<Tabs
title="Steps in this process instance involving people"
selectedIndex={selectedTabIndex}
>
<TabList aria-label="List of tabs" contained>
{userTasksElement}
</TabList>
</Tabs>
);
}
return (
<Tabs
title="Steps in this process instance involving people"
selectedIndex={selectedTabIndex}
>
<TabList aria-label="List of tabs" contained>
{userTasksElement}
</TabList>
</Tabs>
);
return null;
};
const formElement = (taskToUse: any) => {
@ -207,7 +223,7 @@ export default function TaskShow() {
);
};
if (task && userTasks) {
if (task) {
const taskToUse = task as any;
let statusString = '';
if (taskToUse.state !== 'READY') {