Merge pull request #65 from sartography/new_report

Create new report based on current filter/columns
This commit is contained in:
Kevin Burnett 2022-12-06 13:12:43 -08:00 committed by GitHub
commit bad7adb322
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1709 additions and 293 deletions

View File

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

View File

@ -1,8 +1,8 @@
"""empty message """empty message
Revision ID: ff1c1628337c Revision ID: 40a2ed63cc5a
Revises: Revises:
Create Date: 2022-11-28 15:08:52.014254 Create Date: 2022-11-29 16:59:02.980181
""" """
from alembic import op from alembic import op
@ -10,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = 'ff1c1628337c' revision = '40a2ed63cc5a'
down_revision = None down_revision = None
branch_labels = None branch_labels = None
depends_on = None depends_on = None
@ -249,6 +249,7 @@ def upgrade():
sa.PrimaryKeyConstraint('id'), sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('process_instance_id', 'key', name='process_instance_metadata_unique') 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', op.create_table('spiff_step_details',
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),
sa.Column('process_instance_id', sa.Integer(), nullable=False), sa.Column('process_instance_id', sa.Integer(), nullable=False),
@ -295,6 +296,7 @@ def downgrade():
op.drop_index(op.f('ix_active_task_user_active_task_id'), table_name='active_task_user') 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('active_task_user')
op.drop_table('spiff_step_details') 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('process_instance_metadata')
op.drop_table('permission_assignment') op.drop_table('permission_assignment')
op.drop_table('message_instance') op.drop_table('message_instance')

View File

@ -1851,7 +1851,7 @@ lxml = "*"
type = "git" type = "git"
url = "https://github.com/sartography/SpiffWorkflow" url = "https://github.com/sartography/SpiffWorkflow"
reference = "main" reference = "main"
resolved_reference = "062eaf15d28c66f8cf07f68409429560251b12c7" resolved_reference = "ffb1686757f944065580dd2db8def73d6c1f0134"
[[package]] [[package]]
name = "SQLAlchemy" name = "SQLAlchemy"
@ -2989,7 +2989,18 @@ psycopg2 = [
{file = "psycopg2-2.9.4.tar.gz", hash = "sha256:d529926254e093a1b669f692a3aa50069bc71faf5b0ecd91686a78f62767d52f"}, {file = "psycopg2-2.9.4.tar.gz", hash = "sha256:d529926254e093a1b669f692a3aa50069bc71faf5b0ecd91686a78f62767d52f"},
] ]
pyasn1 = [ 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-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"}, {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"},
] ]
pycodestyle = [ pycodestyle = [

View File

@ -544,6 +544,12 @@ paths:
description: Specifies the identifier of a report to use, if any description: Specifies the identifier of a report to use, if any
schema: schema:
type: string type: string
- name: report_id
in: query
required: false
description: Specifies the identifier of a report to use, if any
schema:
type: integer
get: get:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_list operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_list
summary: Returns a list of process instances for a given process model summary: Returns a list of process instances for a given process model
@ -841,14 +847,30 @@ paths:
schema: schema:
$ref: "#/components/schemas/OkTrue" $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: parameters:
- name: report_identifier - name: report_id
in: path in: path
required: true required: true
description: The unique id of an existing report description: The unique id of an existing report
schema: schema:
type: string type: integer
- name: page - name: page
in: query in: query
required: false required: false

View File

@ -23,7 +23,7 @@ class ProcessInstanceMetadataModel(SpiffworkflowBaseDBModel):
process_instance_id: int = db.Column( process_instance_id: int = db.Column(
ForeignKey(ProcessInstanceModel.id), nullable=False # type: ignore 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) value: str = db.Column(db.String(255), nullable=False)
updated_at_in_seconds: int = db.Column(db.Integer, 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] ReportMetadata = dict[str, Any]
class ProcessInstanceReportAlreadyExistsError(Exception):
"""ProcessInstanceReportAlreadyExistsError."""
class ProcessInstanceReportResult(TypedDict): class ProcessInstanceReportResult(TypedDict):
"""ProcessInstanceReportResult.""" """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) identifier: str = db.Column(db.String(50), nullable=False, index=True)
report_metadata: dict = deferred(db.Column(db.JSON)) # type: ignore report_metadata: dict = deferred(db.Column(db.JSON)) # type: ignore
created_by_id = db.Column(ForeignKey(UserModel.id), nullable=False, index=True) 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) created_at_in_seconds = db.Column(db.Integer)
updated_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 @classmethod
def add_fixtures(cls) -> None: def add_fixtures(cls) -> None:
"""Add_fixtures.""" """Add_fixtures."""
@ -120,14 +129,18 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel):
identifier: str, identifier: str,
user: UserModel, user: UserModel,
report_metadata: ReportMetadata, report_metadata: ReportMetadata,
) -> None: ) -> ProcessInstanceReportModel:
"""Make_fixture_report.""" """Make_fixture_report."""
process_instance_report = ProcessInstanceReportModel.query.filter_by( process_instance_report = ProcessInstanceReportModel.query.filter_by(
identifier=identifier, identifier=identifier,
created_by_id=user.id, created_by_id=user.id,
).first() ).first()
if process_instance_report is None: if process_instance_report is not None:
raise ProcessInstanceReportAlreadyExistsError(
f"Process instance report with identifier already exists: {identifier}"
)
process_instance_report = cls( process_instance_report = cls(
identifier=identifier, identifier=identifier,
created_by_id=user.id, created_by_id=user.id,
@ -136,6 +149,8 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel):
db.session.add(process_instance_report) db.session.add(process_instance_report)
db.session.commit() db.session.commit()
return process_instance_report # type: ignore
@classmethod @classmethod
def ticket_for_month_report(cls) -> dict: def ticket_for_month_report(cls) -> dict:
"""Ticket_for_month_report.""" """Ticket_for_month_report."""
@ -204,18 +219,8 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel):
user: UserModel, user: UserModel,
) -> ProcessInstanceReportModel: ) -> ProcessInstanceReportModel:
"""Create_with_attributes.""" """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( process_instance_report = cls(
identifier=identifier, identifier=identifier,
# >>>>>>> main
created_by_id=user.id, created_by_id=user.id,
report_metadata=report_metadata, report_metadata=report_metadata,
) )

View File

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

View File

@ -1,6 +1,7 @@
"""APIs for dealing with process groups, process models, and process instances.""" """APIs for dealing with process groups, process models, and process instances."""
import json import json
import random import random
import re
import string import string
import uuid import uuid
from typing import Any from typing import Any
@ -30,6 +31,8 @@ from SpiffWorkflow.task import TaskState
from sqlalchemy import and_ from sqlalchemy import and_
from sqlalchemy import asc from sqlalchemy import asc
from sqlalchemy import desc from sqlalchemy import desc
from sqlalchemy import func
from sqlalchemy.orm import aliased
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from spiffworkflow_backend.exceptions.process_entity_not_found_error import ( from spiffworkflow_backend.exceptions.process_entity_not_found_error import (
@ -52,6 +55,9 @@ from spiffworkflow_backend.models.process_instance import ProcessInstanceApiSche
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModelSchema from spiffworkflow_backend.models.process_instance import ProcessInstanceModelSchema
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus 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 ( from spiffworkflow_backend.models.process_instance_report import (
ProcessInstanceReportModel, ProcessInstanceReportModel,
) )
@ -256,19 +262,26 @@ def process_model_create(
modified_process_group_id: str, body: Dict[str, Union[str, bool, int]] modified_process_group_id: str, body: Dict[str, Union[str, bool, int]]
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
"""Process_model_create.""" """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: if modified_process_group_id is None:
raise ApiError( raise ApiError(
error_code="process_group_id_not_specified", error_code="process_group_id_not_specified",
message="Process Model could not be created when process_group_id path param is unspecified", message="Process Model could not be created when process_group_id path param is unspecified",
status_code=400, 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( unmodified_process_group_id = un_modify_modified_process_model_id(
modified_process_group_id modified_process_group_id
@ -281,6 +294,14 @@ def process_model_create(
status_code=400, 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) ProcessModelService.add_process_model(process_model_info)
return Response( return Response(
json.dumps(ProcessModelInfoSchema().dump(process_model_info)), json.dumps(ProcessModelInfoSchema().dump(process_model_info)),
@ -294,7 +315,6 @@ def process_model_delete(
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
"""Process_model_delete.""" """Process_model_delete."""
process_model_identifier = modified_process_model_identifier.replace(":", "/") 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) ProcessModelService().process_model_delete(process_model_identifier)
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
@ -309,6 +329,7 @@ def process_model_update(
"primary_file_name", "primary_file_name",
"primary_process_id", "primary_process_id",
"description", "description",
"metadata_extraction_paths",
] ]
body_filtered = { body_filtered = {
include_item: body[include_item] include_item: body[include_item]
@ -316,7 +337,6 @@ def process_model_update(
if include_item in body if include_item in body
} }
# process_model_identifier = f"{process_group_id}/{process_model_id}"
process_model = get_process_model(process_model_identifier) process_model = get_process_model(process_model_identifier)
ProcessModelService.update_process_model(process_model, body_filtered) ProcessModelService.update_process_model(process_model, body_filtered)
return ProcessModelInfoSchema().dump(process_model) return ProcessModelInfoSchema().dump(process_model)
@ -325,10 +345,7 @@ def process_model_update(
def process_model_show(modified_process_model_identifier: str) -> Any: def process_model_show(modified_process_model_identifier: str) -> Any:
"""Process_model_show.""" """Process_model_show."""
process_model_identifier = modified_process_model_identifier.replace(":", "/") 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) 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)) files = sorted(SpecFileService.get_files(process_model))
process_model.files = files process_model.files = files
for file in process_model.files: for file in process_model.files:
@ -420,7 +437,6 @@ def process_model_file_update(
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
"""Process_model_file_update.""" """Process_model_file_update."""
process_model_identifier = modified_process_model_id.replace(":", "/") process_model_identifier = modified_process_model_id.replace(":", "/")
# process_model_identifier = f"{process_group_id}/{process_model_id}"
process_model = get_process_model(process_model_identifier) process_model = get_process_model(process_model_identifier)
request_file = get_file_from_request() request_file = get_file_from_request()
@ -642,6 +658,7 @@ def message_instance_list(
.add_columns( .add_columns(
MessageModel.identifier.label("message_identifier"), MessageModel.identifier.label("message_identifier"),
ProcessInstanceModel.process_model_identifier, ProcessInstanceModel.process_model_identifier,
ProcessInstanceModel.process_model_display_name,
) )
.paginate(page=page, per_page=per_page, error_out=False) .paginate(page=page, per_page=per_page, error_out=False)
) )
@ -776,10 +793,11 @@ def process_instance_list(
with_tasks_completed_by_my_group: Optional[bool] = None, with_tasks_completed_by_my_group: Optional[bool] = None,
user_filter: Optional[bool] = False, user_filter: Optional[bool] = False,
report_identifier: Optional[str] = None, report_identifier: Optional[str] = None,
report_id: Optional[int] = None,
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
"""Process_instance_list.""" """Process_instance_list."""
process_instance_report = ProcessInstanceReportService.report_with_identifier( process_instance_report = ProcessInstanceReportService.report_with_identifier(
g.user, report_identifier g.user, report_id, report_identifier
) )
if user_filter: if user_filter:
@ -810,7 +828,6 @@ def process_instance_list(
) )
) )
# process_model_identifier = un_modify_modified_process_model_id(modified_process_model_identifier)
process_instance_query = ProcessInstanceModel.query process_instance_query = ProcessInstanceModel.query
# Always join that hot user table for good performance at serialization time. # Always join that hot user table for good performance at serialization time.
process_instance_query = process_instance_query.options( process_instance_query = process_instance_query.options(
@ -928,25 +945,78 @@ def process_instance_list(
UserGroupAssignmentModel.user_id == g.user.id UserGroupAssignmentModel.user_id == g.user.id
) )
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_instances = (
process_instance_query.group_by(ProcessInstanceModel.id) process_instance_query.group_by(ProcessInstanceModel.id)
.order_by( .add_columns(ProcessInstanceModel.id)
ProcessInstanceModel.start_in_seconds.desc(), ProcessInstanceModel.id.desc() # type: ignore .order_by(*order_by_query_array)
)
.paginate(page=page, per_page=per_page, error_out=False) .paginate(page=page, per_page=per_page, error_out=False)
) )
results = list( results = ProcessInstanceReportService.add_metadata_columns_to_process_instance(
map( process_instances.items, process_instance_report.report_metadata["columns"]
ProcessInstanceService.serialize_flat_with_task_data,
process_instances.items,
) )
)
report_metadata = process_instance_report.report_metadata
response_json = { response_json = {
"report_identifier": process_instance_report.identifier, "report": process_instance_report,
"report_metadata": report_metadata,
"results": results, "results": results,
"filters": report_filter.to_dict(), "filters": report_filter.to_dict(),
"pagination": { "pagination": {
@ -959,6 +1029,22 @@ def process_instance_list(
return make_response(jsonify(response_json), 200) 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( def process_instance_show(
modified_process_model_identifier: str, process_instance_id: int modified_process_model_identifier: str, process_instance_id: int
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
@ -1015,22 +1101,22 @@ def process_instance_report_list(
def process_instance_report_create(body: Dict[str, Any]) -> flask.wrappers.Response: def process_instance_report_create(body: Dict[str, Any]) -> flask.wrappers.Response:
"""Process_instance_report_create.""" """Process_instance_report_create."""
ProcessInstanceReportModel.create_report( process_instance_report = ProcessInstanceReportModel.create_report(
identifier=body["identifier"], identifier=body["identifier"],
user=g.user, user=g.user,
report_metadata=body["report_metadata"], 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( def process_instance_report_update(
report_identifier: str, report_id: int,
body: Dict[str, Any], body: Dict[str, Any],
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
"""Process_instance_report_create.""" """Process_instance_report_create."""
process_instance_report = ProcessInstanceReportModel.query.filter_by( process_instance_report = ProcessInstanceReportModel.query.filter_by(
identifier=report_identifier, id=report_id,
created_by_id=g.user.id, created_by_id=g.user.id,
).first() ).first()
if process_instance_report is None: if process_instance_report is None:
@ -1043,15 +1129,15 @@ def process_instance_report_update(
process_instance_report.report_metadata = body["report_metadata"] process_instance_report.report_metadata = body["report_metadata"]
db.session.commit() 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( def process_instance_report_delete(
report_identifier: str, report_id: int,
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
"""Process_instance_report_create.""" """Process_instance_report_create."""
process_instance_report = ProcessInstanceReportModel.query.filter_by( process_instance_report = ProcessInstanceReportModel.query.filter_by(
identifier=report_identifier, id=report_id,
created_by_id=g.user.id, created_by_id=g.user.id,
).first() ).first()
if process_instance_report is None: if process_instance_report is None:
@ -1070,8 +1156,6 @@ def process_instance_report_delete(
def service_tasks_show() -> flask.wrappers.Response: def service_tasks_show() -> flask.wrappers.Response:
"""Service_tasks_show.""" """Service_tasks_show."""
available_connectors = ServiceTaskService.available_connectors() available_connectors = ServiceTaskService.available_connectors()
print(available_connectors)
return Response( return Response(
json.dumps(available_connectors), status=200, mimetype="application/json" json.dumps(available_connectors), status=200, mimetype="application/json"
) )
@ -1105,19 +1189,17 @@ def authentication_callback(
def process_instance_report_show( def process_instance_report_show(
report_identifier: str, report_id: int,
page: int = 1, page: int = 1,
per_page: int = 100, per_page: int = 100,
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
"""Process_instance_list.""" """Process_instance_report_show."""
process_instances = ProcessInstanceModel.query.order_by( # .filter_by(process_model_identifier=process_model.id) process_instances = ProcessInstanceModel.query.order_by(
ProcessInstanceModel.start_in_seconds.desc(), ProcessInstanceModel.id.desc() # type: ignore ProcessInstanceModel.start_in_seconds.desc(), ProcessInstanceModel.id.desc() # type: ignore
).paginate( ).paginate(page=page, per_page=per_page, error_out=False)
page=page, per_page=per_page, error_out=False
)
process_instance_report = ProcessInstanceReportModel.query.filter_by( process_instance_report = ProcessInstanceReportModel.query.filter_by(
identifier=report_identifier, id=report_id,
created_by_id=g.user.id, created_by_id=g.user.id,
).first() ).first()
if process_instance_report is None: if process_instance_report is None:
@ -1394,9 +1476,6 @@ def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response
task.form_ui_schema = ui_form_contents task.form_ui_schema = ui_form_contents
if task.properties and task.data and "instructionsForEndUser" in task.properties: if task.properties and task.data and "instructionsForEndUser" in task.properties:
print(
f"task.properties['instructionsForEndUser']: {task.properties['instructionsForEndUser']}"
)
if task.properties["instructionsForEndUser"]: if task.properties["instructionsForEndUser"]:
task.properties["instructionsForEndUser"] = render_jinja_template( task.properties["instructionsForEndUser"] = render_jinja_template(
task.properties["instructionsForEndUser"], task.data task.properties["instructionsForEndUser"], task.data

View File

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

View File

@ -235,8 +235,9 @@ class AuthenticationService:
refresh_token_object: RefreshTokenModel = RefreshTokenModel.query.filter( refresh_token_object: RefreshTokenModel = RefreshTokenModel.query.filter(
RefreshTokenModel.user_id == user_id RefreshTokenModel.user_id == user_id
).first() ).first()
assert refresh_token_object # noqa: S101 if refresh_token_object:
return refresh_token_object.token return refresh_token_object.token
return None
@classmethod @classmethod
def get_auth_token_from_refresh_token(cls, refresh_token: str) -> dict: 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.message_instance import MessageModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus 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.process_model import ProcessModelInfo
from spiffworkflow_backend.models.script_attributes_context import ( from spiffworkflow_backend.models.script_attributes_context import (
ScriptAttributesContext, ScriptAttributesContext,
@ -178,7 +181,12 @@ class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore
) )
return Script.generate_augmented_list(script_attributes_context) return Script.generate_augmented_list(script_attributes_context)
def evaluate(self, task: SpiffTask, expression: str, external_methods=None) -> Any: def evaluate(
self,
task: SpiffTask,
expression: str,
external_methods: Optional[dict[str, Any]] = None,
) -> Any:
"""Evaluate.""" """Evaluate."""
return self._evaluate(expression, task.data, task, external_methods) return self._evaluate(expression, task.data, task, external_methods)
@ -571,6 +579,36 @@ class ProcessInstanceProcessor:
db.session.add(details_model) db.session.add(details_model)
db.session.commit() 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: def save(self) -> None:
"""Saves the current state of this processor to the database.""" """Saves the current state of this processor to the database."""
self.process_instance_model.bpmn_json = self.serialize() self.process_instance_model.bpmn_json = self.serialize()
@ -597,6 +635,15 @@ class ProcessInstanceProcessor:
process_instance_id=self.process_instance_model.id process_instance_id=self.process_instance_model.id
).all() ).all()
ready_or_waiting_tasks = self.get_all_ready_or_waiting_tasks() 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: for ready_or_waiting_task in ready_or_waiting_tasks:
# filter out non-usertasks # filter out non-usertasks
task_spec = ready_or_waiting_task.task_spec task_spec = ready_or_waiting_task.task_spec
@ -615,13 +662,6 @@ class ProcessInstanceProcessor:
if "formUiSchemaFilename" in properties: if "formUiSchemaFilename" in properties:
ui_form_file_name = properties["formUiSchemaFilename"] 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 active_task = None
for at in active_tasks: for at in active_tasks:
if at.task_id == str(ready_or_waiting_task.id): if at.task_id == str(ready_or_waiting_task.id):
@ -1146,8 +1186,8 @@ class ProcessInstanceProcessor:
def get_current_data(self) -> dict[str, Any]: def get_current_data(self) -> dict[str, Any]:
"""Get the current data for the process. """Get the current data for the process.
Return either most recent task data or the process data Return either the most recent task data or--if the process instance is complete--
if the process instance is complete the process data.
""" """
if self.process_instance_model.status == "complete": if self.process_instance_model.status == "complete":
return self.get_data() return self.get_data()

View File

@ -2,6 +2,9 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional from typing import Optional
import sqlalchemy
from flask_bpmn.models.db import db
from spiffworkflow_backend.models.process_instance_report import ( from spiffworkflow_backend.models.process_instance_report import (
ProcessInstanceReportModel, ProcessInstanceReportModel,
) )
@ -57,12 +60,21 @@ class ProcessInstanceReportService:
@classmethod @classmethod
def report_with_identifier( 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: ) -> ProcessInstanceReportModel:
"""Report_with_filter.""" """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: if report_identifier is None:
report_identifier = "default" report_identifier = "default"
process_instance_report = ProcessInstanceReportModel.query.filter_by( process_instance_report = ProcessInstanceReportModel.query.filter_by(
identifier=report_identifier, created_by_id=user.id identifier=report_identifier, created_by_id=user.id
).first() ).first()
@ -73,17 +85,9 @@ class ProcessInstanceReportService:
# TODO replace with system reports that are loaded on launch (or similar) # TODO replace with system reports that are loaded on launch (or similar)
temp_system_metadata_map = { temp_system_metadata_map = {
"default": { "default": {
"columns": [ "columns": cls.builtin_column_options(),
{"Header": "id", "accessor": "id"}, "filter_by": [],
{ "order_by": ["-start_in_seconds", "-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"},
],
}, },
"system_report_instances_initiated_by_me": { "system_report_instances_initiated_by_me": {
"columns": [ "columns": [
@ -97,48 +101,31 @@ class ProcessInstanceReportService:
{"Header": "status", "accessor": "status"}, {"Header": "status", "accessor": "status"},
], ],
"filter_by": [{"field_name": "initiated_by_me", "field_value": True}], "filter_by": [{"field_name": "initiated_by_me", "field_value": True}],
"order_by": ["-start_in_seconds", "-id"],
}, },
"system_report_instances_with_tasks_completed_by_me": { "system_report_instances_with_tasks_completed_by_me": {
"columns": [ "columns": cls.builtin_column_options(),
{"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"},
],
"filter_by": [ "filter_by": [
{"field_name": "with_tasks_completed_by_me", "field_value": True} {"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": { "system_report_instances_with_tasks_completed_by_my_groups": {
"columns": [ "columns": cls.builtin_column_options(),
{"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"},
],
"filter_by": [ "filter_by": [
{ {
"field_name": "with_tasks_completed_by_my_group", "field_name": "with_tasks_completed_by_my_group",
"field_value": True, "field_value": True,
} }
], ],
"order_by": ["-start_in_seconds", "-id"],
}, },
} }
process_instance_report = ProcessInstanceReportModel( process_instance_report = ProcessInstanceReportModel(
identifier=report_identifier, identifier=report_identifier,
created_by_id=user.id, 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 return process_instance_report # type: ignore
@ -241,3 +228,43 @@ class ProcessInstanceReportService:
) )
return report_filter 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 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:scriptTask id="hot_script_task_OH_YEEEEEEEEEEEEEEEEEEEEAH" name="OHHHHHHHHHHYEEEESSSSSSSSSS">
<bpmn:incoming>Flow_0bazl8x</bpmn:incoming> <bpmn:incoming>Flow_0bazl8x</bpmn:incoming>
<bpmn:outgoing>Flow_1mcaszp</bpmn:outgoing> <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:scriptTask>
<bpmn:endEvent id="Event_1vch1y0"> <bpmn:endEvent id="Event_1vch1y0">
<bpmn:incoming>Flow_1mcaszp</bpmn:incoming> <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

@ -20,6 +20,9 @@ from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.process_group import ProcessGroup from spiffworkflow_backend.models.process_group import ProcessGroup
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus 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 ( from spiffworkflow_backend.models.process_instance_report import (
ProcessInstanceReportModel, ProcessInstanceReportModel,
) )
@ -330,6 +333,9 @@ class TestProcessApi(BaseTest):
process_model.display_name = "Updated Display Name" process_model.display_name = "Updated Display Name"
process_model.primary_file_name = "superduper.bpmn" process_model.primary_file_name = "superduper.bpmn"
process_model.primary_process_id = "superduper" process_model.primary_process_id = "superduper"
process_model.metadata_extraction_paths = [
{"key": "extraction1", "path": "path1"}
]
modified_process_model_identifier = process_model_identifier.replace("/", ":") modified_process_model_identifier = process_model_identifier.replace("/", ":")
response = client.put( response = client.put(
@ -343,6 +349,9 @@ class TestProcessApi(BaseTest):
assert response.json["display_name"] == "Updated Display Name" assert response.json["display_name"] == "Updated Display Name"
assert response.json["primary_file_name"] == "superduper.bpmn" assert response.json["primary_file_name"] == "superduper.bpmn"
assert response.json["primary_process_id"] == "superduper" assert response.json["primary_process_id"] == "superduper"
assert response.json["metadata_extraction_paths"] == [
{"key": "extraction1", "path": "path1"}
]
def test_process_model_list_all( def test_process_model_list_all(
self, self,
@ -1723,14 +1732,14 @@ class TestProcessApi(BaseTest):
], ],
} }
ProcessInstanceReportModel.create_with_attributes( report = ProcessInstanceReportModel.create_with_attributes(
identifier="sure", identifier="sure",
report_metadata=report_metadata, report_metadata=report_metadata,
user=with_super_admin_user, user=with_super_admin_user,
) )
response = client.get( 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), headers=self.logged_in_headers(with_super_admin_user),
) )
assert response.status_code == 200 assert response.status_code == 200
@ -1769,14 +1778,14 @@ class TestProcessApi(BaseTest):
], ],
} }
ProcessInstanceReportModel.create_with_attributes( report = ProcessInstanceReportModel.create_with_attributes(
identifier="sure", identifier="sure",
report_metadata=report_metadata, report_metadata=report_metadata,
user=with_super_admin_user, user=with_super_admin_user,
) )
response = client.get( 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), headers=self.logged_in_headers(with_super_admin_user),
) )
assert response.status_code == 200 assert response.status_code == 200
@ -1791,9 +1800,9 @@ class TestProcessApi(BaseTest):
with_super_admin_user: UserModel, with_super_admin_user: UserModel,
setup_process_instances_for_reports: list[ProcessInstanceModel], setup_process_instances_for_reports: list[ProcessInstanceModel],
) -> None: ) -> None:
"""Test_process_instance_report_show_with_default_list.""" """Test_process_instance_report_show_with_bad_identifier."""
response = client.get( 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), headers=self.logged_in_headers(with_super_admin_user),
) )
assert response.status_code == 404 assert response.status_code == 404
@ -2544,3 +2553,191 @@ class TestProcessApi(BaseTest):
# make sure the new subgroup does exist # make sure the new subgroup does exist
new_process_group = ProcessModelService.get_process_group(new_sub_path) new_process_group = ProcessModelService.get_process_group(new_sub_path)
assert new_process_group.id == new_sub_path assert new_process_group.id == new_sub_path
def test_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, with_db_and_bpmn_file_cleanup: None,
setup_process_instances_for_reports: list[ProcessInstanceModel], setup_process_instances_for_reports: list[ProcessInstanceModel],
) -> None: ) -> 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 process_instances = setup_process_instances_for_reports
report_metadata = { report_metadata = {
"filter_by": [ "filter_by": [
@ -61,7 +61,7 @@ def test_generate_report_with_order_by_and_one_field(
with_db_and_bpmn_file_cleanup: None, with_db_and_bpmn_file_cleanup: None,
setup_process_instances_for_reports: list[ProcessInstanceModel], setup_process_instances_for_reports: list[ProcessInstanceModel],
) -> None: ) -> 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 process_instances = setup_process_instances_for_reports
report_metadata = {"order_by": ["test_score"]} report_metadata = {"order_by": ["test_score"]}
results = do_report_with_metadata_and_instances(report_metadata, process_instances) 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, with_db_and_bpmn_file_cleanup: None,
setup_process_instances_for_reports: list[ProcessInstanceModel], setup_process_instances_for_reports: list[ProcessInstanceModel],
) -> None: ) -> 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 process_instances = setup_process_instances_for_reports
report_metadata = {"order_by": ["grade_level", "test_score"]} report_metadata = {"order_by": ["grade_level", "test_score"]}
results = do_report_with_metadata_and_instances(report_metadata, process_instances) 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, with_db_and_bpmn_file_cleanup: None,
setup_process_instances_for_reports: list[ProcessInstanceModel], setup_process_instances_for_reports: list[ProcessInstanceModel],
) -> None: ) -> 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 process_instances = setup_process_instances_for_reports
report_metadata = {"order_by": ["grade_level", "-test_score"]} report_metadata = {"order_by": ["grade_level", "-test_score"]}
results = do_report_with_metadata_and_instances(report_metadata, process_instances) 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, with_db_and_bpmn_file_cleanup: None,
setup_process_instances_for_reports: list[ProcessInstanceModel], setup_process_instances_for_reports: list[ProcessInstanceModel],
) -> None: ) -> None:
"""Test_user_can_be_given_permission_to_administer_process_group.""" """Test_generate_report_with_columns."""
process_instances = setup_process_instances_for_reports process_instances = setup_process_instances_for_reports
report_metadata = { report_metadata = {
"columns": [ "columns": [

View File

@ -5,12 +5,16 @@ from flask_bpmn.models.db import db
from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec 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.process_model import ProcessModelInfo
from spiffworkflow_backend.models.spec_reference import SpecReferenceCache from spiffworkflow_backend.models.spec_reference import SpecReferenceCache
from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.services.process_instance_processor import ( from spiffworkflow_backend.services.process_instance_processor import (
ProcessInstanceProcessor, ProcessInstanceProcessor,
) )
from spiffworkflow_backend.services.process_model_service import ProcessModelService
class TestProcessModel(BaseTest): class TestProcessModel(BaseTest):
@ -122,6 +126,53 @@ class TestProcessModel(BaseTest):
processor.do_engine_steps(save=True) processor.do_engine_steps(save=True)
assert process_instance.status == "complete" 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: def create_test_process_model(self, id: str, display_name: str) -> ProcessModelInfo:
"""Create_test_process_model.""" """Create_test_process_model."""
return ProcessModelInfo( 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*" labelText="Display Name*"
value={processGroup.display_name} value={processGroup.display_name}
onChange={(event: any) => onDisplayNameChanged(event.target.value)} 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'; } from 'react-router-dom';
// @ts-ignore // @ts-ignore
import { Filter } from '@carbon/icons-react'; import { Filter, Close, AddAlt } from '@carbon/icons-react';
import { import {
Button, Button,
ButtonSet, ButtonSet,
@ -21,6 +21,13 @@ import {
TableHead, TableHead,
TableRow, TableRow,
TimePicker, TimePicker,
Tag,
InlineNotification,
Stack,
Modal,
ComboBox,
TextInput,
FormLabel,
// @ts-ignore // @ts-ignore
} from '@carbon/react'; } from '@carbon/react';
import { PROCESS_STATUSES, DATE_FORMAT, DATE_FORMAT_CARBON } from '../config'; import { PROCESS_STATUSES, DATE_FORMAT, DATE_FORMAT_CARBON } from '../config';
@ -49,9 +56,15 @@ import {
ProcessModel, ProcessModel,
ProcessInstanceReport, ProcessInstanceReport,
ProcessInstance, ProcessInstance,
ReportColumn,
ReportColumnForEditing,
ReportMetadata,
ReportFilter,
} from '../interfaces'; } from '../interfaces';
import ProcessModelSearch from './ProcessModelSearch'; import ProcessModelSearch from './ProcessModelSearch';
import ProcessInstanceReportSearch from './ProcessInstanceReportSearch'; import ProcessInstanceReportSearch from './ProcessInstanceReportSearch';
import ProcessInstanceListSaveAsReport from './ProcessInstanceListSaveAsReport';
import { FormatProcessModelDisplayName } from './MiniComponents';
const REFRESH_INTERVAL = 5; const REFRESH_INTERVAL = 5;
const REFRESH_TIMEOUT = 600; const REFRESH_TIMEOUT = 600;
@ -88,7 +101,7 @@ export default function ProcessInstanceListTable({
const navigate = useNavigate(); const navigate = useNavigate();
const [processInstances, setProcessInstances] = useState([]); const [processInstances, setProcessInstances] = useState([]);
const [reportMetadata, setReportMetadata] = useState({}); const [reportMetadata, setReportMetadata] = useState<ReportMetadata | null>();
const [pagination, setPagination] = useState<PaginationObject | null>(null); const [pagination, setPagination] = useState<PaginationObject | null>(null);
const [processInstanceFilters, setProcessInstanceFilters] = useState({}); const [processInstanceFilters, setProcessInstanceFilters] = useState({});
@ -125,6 +138,17 @@ export default function ProcessInstanceListTable({
const [processInstanceReportSelection, setProcessInstanceReportSelection] = const [processInstanceReportSelection, setProcessInstanceReportSelection] =
useState<ProcessInstanceReport | null>(null); 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(() => { const dateParametersToAlwaysFilterBy: dateParameters = useMemo(() => {
return { return {
start_from: [setStartFromDate, setStartFromTime], start_from: [setStartFromDate, setStartFromTime],
@ -155,16 +179,12 @@ export default function ProcessInstanceListTable({
function setProcessInstancesFromResult(result: any) { function setProcessInstancesFromResult(result: any) {
const processInstancesFromApi = result.results; const processInstancesFromApi = result.results;
setProcessInstances(processInstancesFromApi); setProcessInstances(processInstancesFromApi);
setReportMetadata(result.report_metadata);
setPagination(result.pagination); setPagination(result.pagination);
setProcessInstanceFilters(result.filters); setProcessInstanceFilters(result.filters);
// TODO: need to iron out this interaction some more setReportMetadata(result.report.report_metadata);
if (result.report_identifier !== 'default') { if (result.report.id) {
setProcessInstanceReportSelection({ setProcessInstanceReportSelection(result.report);
id: result.report_identifier,
display_name: result.report_identifier,
});
} }
} }
function getProcessInstances() { function getProcessInstances() {
@ -186,14 +206,10 @@ export default function ProcessInstanceListTable({
queryParamString += `&user_filter=${userAppliedFilter}`; queryParamString += `&user_filter=${userAppliedFilter}`;
} }
let reportIdentifierToUse: any = reportIdentifier; if (searchParams.get('report_id')) {
queryParamString += `&report_id=${searchParams.get('report_id')}`;
if (!reportIdentifierToUse) { } else if (reportIdentifier) {
reportIdentifierToUse = searchParams.get('report_identifier'); queryParamString += `&report_identifier=${reportIdentifier}`;
}
if (reportIdentifierToUse) {
queryParamString += `&report_identifier=${reportIdentifierToUse}`;
} }
Object.keys(dateParametersToAlwaysFilterBy).forEach( Object.keys(dateParametersToAlwaysFilterBy).forEach(
@ -349,6 +365,27 @@ export default function ProcessInstanceListTable({
processModelAvailableItems, processModelAvailableItems,
]); ]);
const processInstanceReportSaveTag = () => {
if (processInstanceReportJustSaved) {
let titleOperation = 'Updated';
if (processInstanceReportJustSaved === 'new') {
titleOperation = 'Created';
}
return (
<InlineNotification
title={`Perspective ${titleOperation}:`}
subtitle={`'${
processInstanceReportSelection
? processInstanceReportSelection.identifier
: ''
}'`}
kind="success"
/>
);
}
return null;
};
// does the comparison, but also returns false if either argument // does the comparison, but also returns false if either argument
// is not truthy and therefore not comparable. // is not truthy and therefore not comparable.
const isTrueComparison = (param1: any, operation: any, param2: any) => { const isTrueComparison = (param1: any, operation: any, param2: any) => {
@ -366,16 +403,8 @@ export default function ProcessInstanceListTable({
} }
}; };
const applyFilter = (event: any) => { // TODO: after factoring this out page hangs when invalid date ranges and applying the filter
event.preventDefault(); const calculateStartAndEndSeconds = () => {
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
undefined,
undefined,
paginationQueryParamPrefix
);
let queryParamString = `per_page=${perPage}&page=${page}&user_filter=true`;
const startFromSeconds = convertDateAndTimeStringsToSeconds( const startFromSeconds = convertDateAndTimeStringsToSeconds(
startFromDate, startFromDate,
startFromTime || '00:00:00' startFromTime || '00:00:00'
@ -392,28 +421,59 @@ export default function ProcessInstanceListTable({
endToDate, endToDate,
endToTime || '00:00:00' endToTime || '00:00:00'
); );
let valid = true;
if (isTrueComparison(startFromSeconds, '>', startToSeconds)) { if (isTrueComparison(startFromSeconds, '>', startToSeconds)) {
setErrorMessage({ setErrorMessage({
message: '"Start date from" cannot be after "start date to"', message: '"Start date from" cannot be after "start date to"',
}); });
return; valid = false;
} }
if (isTrueComparison(endFromSeconds, '>', endToSeconds)) { if (isTrueComparison(endFromSeconds, '>', endToSeconds)) {
setErrorMessage({ setErrorMessage({
message: '"End date from" cannot be after "end date to"', message: '"End date from" cannot be after "end date to"',
}); });
return; valid = false;
} }
if (isTrueComparison(startFromSeconds, '>', endFromSeconds)) { if (isTrueComparison(startFromSeconds, '>', endFromSeconds)) {
setErrorMessage({ setErrorMessage({
message: '"Start date from" cannot be after "end date from"', message: '"Start date from" cannot be after "end date from"',
}); });
return; valid = false;
} }
if (isTrueComparison(startToSeconds, '>', endToSeconds)) { if (isTrueComparison(startToSeconds, '>', endToSeconds)) {
setErrorMessage({ setErrorMessage({
message: '"Start date to" cannot be after "end date to"', 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; return;
} }
@ -438,10 +498,11 @@ export default function ProcessInstanceListTable({
} }
if (processInstanceReportSelection) { if (processInstanceReportSelection) {
queryParamString += `&report_identifier=${processInstanceReportSelection.id}`; queryParamString += `&report_id=${processInstanceReportSelection.id}`;
} }
setErrorMessage(null); setErrorMessage(null);
setProcessInstanceReportJustSaved(null);
navigate(`/admin/process-instances?${queryParamString}`); navigate(`/admin/process-instances?${queryParamString}`);
}; };
@ -529,12 +590,360 @@ export default function ProcessInstanceListTable({
setEndToTime(''); 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 = () => { const filterOptions = () => {
if (!showFilterOptions) { if (!showFilterOptions) {
return null; return null;
} }
return ( 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"> <Grid fullWidth className="with-bottom-margin">
<Column md={8}> <Column md={8}>
<ProcessModelSearch <ProcessModelSearch
@ -598,11 +1007,11 @@ export default function ProcessInstanceListTable({
</Column> </Column>
</Grid> </Grid>
<Grid fullWidth className="with-bottom-margin"> <Grid fullWidth className="with-bottom-margin">
<Column md={4}> <Column sm={4} md={4} lg={8}>
<ButtonSet> <ButtonSet>
<Button <Button
kind="" kind=""
className="button-white-background" className="button-white-background narrow-button"
onClick={clearFilters} onClick={clearFilters}
> >
Clear Clear
@ -611,11 +1020,15 @@ export default function ProcessInstanceListTable({
kind="secondary" kind="secondary"
onClick={applyFilter} onClick={applyFilter}
data-qa="filter-button" data-qa="filter-button"
className="narrow-button"
> >
Filter Filter
</Button> </Button>
</ButtonSet> </ButtonSet>
</Column> </Column>
<Column sm={4} md={4} lg={8}>
{saveAsReportComponent()}
</Column>
</Grid> </Grid>
</> </>
); );
@ -635,7 +1048,7 @@ export default function ProcessInstanceListTable({
const getHeaderLabel = (header: string) => { const getHeaderLabel = (header: string) => {
return headerLabels[header] ?? header; 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 <th>{getHeaderLabel((column as any).Header)}</th>;
return getHeaderLabel((column as any).Header); return getHeaderLabel((column as any).Header);
}); });
@ -665,22 +1078,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) => { const formatSecondsForDisplay = (_row: any, seconds: any) => {
return convertSecondsToFormattedDateTime(seconds) || '-'; return convertSecondsToFormattedDateTime(seconds) || '-';
}; };
@ -688,15 +1085,16 @@ export default function ProcessInstanceListTable({
return value; return value;
}; };
const columnFormatters: Record<string, any> = { const reportColumnFormatters: Record<string, any> = {
id: formatProcessInstanceId, id: formatProcessInstanceId,
process_model_identifier: formatProcessModelIdentifier, process_model_identifier: formatProcessModelIdentifier,
process_model_display_name: formatProcessModelDisplayName, process_model_display_name: FormatProcessModelDisplayName,
start_in_seconds: formatSecondsForDisplay, start_in_seconds: formatSecondsForDisplay,
end_in_seconds: formatSecondsForDisplay, end_in_seconds: formatSecondsForDisplay,
}; };
const formattedColumn = (row: any, column: any) => { const formattedColumn = (row: any, column: any) => {
const formatter = columnFormatters[column.accessor] ?? defaultFormatter; const formatter =
reportColumnFormatters[column.accessor] ?? defaultFormatter;
const value = row[column.accessor]; const value = row[column.accessor];
if (column.accessor === 'status') { if (column.accessor === 'status') {
return ( return (
@ -709,7 +1107,7 @@ export default function ProcessInstanceListTable({
}; };
const rows = processInstances.map((row: any) => { 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 formattedColumn(row, column);
}); });
return <tr key={row.id}>{currentRow}</tr>; return <tr key={row.id}>{currentRow}</tr>;
@ -736,29 +1134,26 @@ export default function ProcessInstanceListTable({
const toggleShowFilterOptions = () => { const toggleShowFilterOptions = () => {
setShowFilterOptions(!showFilterOptions); setShowFilterOptions(!showFilterOptions);
}; HttpService.makeCallToBackend({
path: `/process-instances/reports/columns`,
const processInstanceReportDidChange = (selection: any) => { successCallback: setAvailableReportColumns,
clearFilters(); });
const selectedReport = selection.selectedItem;
setProcessInstanceReportSelection(selectedReport);
const queryParamString = selectedReport
? `&report_identifier=${selectedReport.id}`
: '';
setErrorMessage(null);
navigate(`/admin/process-instances?${queryParamString}`);
}; };
const reportSearchComponent = () => { const reportSearchComponent = () => {
if (showReports) { if (showReports) {
return ( const columns = [
<Column sm={2} md={4} lg={7}>
<ProcessInstanceReportSearch <ProcessInstanceReportSearch
onChange={processInstanceReportDidChange} onChange={processInstanceReportDidChange}
selectedItem={processInstanceReportSelection} selectedItem={processInstanceReportSelection}
/> />
</Column>,
];
return (
<Grid className="with-tiny-bottom-margin" fullWidth>
{columns}
</Grid>
); );
} }
return null; return null;
@ -806,6 +1201,8 @@ export default function ProcessInstanceListTable({
} }
return ( return (
<> <>
{reportColumnForm()}
{processInstanceReportSaveTag()}
{filterComponent()} {filterComponent()}
{reportSearchComponent()} {reportSearchComponent()}
<PaginationForTable <PaginationForTable

View File

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

View File

@ -1,10 +1,20 @@
import { useState } from 'react'; import { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import {
Button,
ButtonSet,
Form,
Stack,
TextInput,
Grid,
Column,
// @ts-ignore
} from '@carbon/react';
// @ts-ignore // @ts-ignore
import { Button, ButtonSet, Form, Stack, TextInput } from '@carbon/react'; import { AddAlt, TrashCan } from '@carbon/icons-react';
import { modifyProcessIdentifierForPathParam, slugifyString } from '../helpers'; import { modifyProcessIdentifierForPathParam, slugifyString } from '../helpers';
import HttpService from '../services/HttpService'; import HttpService from '../services/HttpService';
import { ProcessModel } from '../interfaces'; import { MetadataExtractionPath, ProcessModel } from '../interfaces';
type OwnProps = { type OwnProps = {
mode: string; mode: string;
@ -23,6 +33,7 @@ export default function ProcessModelForm({
const [idHasBeenUpdatedByUser, setIdHasBeenUpdatedByUser] = const [idHasBeenUpdatedByUser, setIdHasBeenUpdatedByUser] =
useState<boolean>(false); useState<boolean>(false);
const [displayNameInvalid, setDisplayNameInvalid] = useState<boolean>(false); const [displayNameInvalid, setDisplayNameInvalid] = useState<boolean>(false);
useState<boolean>(false);
const navigate = useNavigate(); const navigate = useNavigate();
const navigateToProcessModel = (result: ProcessModel) => { const navigateToProcessModel = (result: ProcessModel) => {
@ -64,6 +75,7 @@ export default function ProcessModelForm({
const postBody = { const postBody = {
display_name: processModel.display_name, display_name: processModel.display_name,
description: processModel.description, description: processModel.description,
metadata_extraction_paths: processModel.metadata_extraction_paths,
}; };
if (mode === 'new') { if (mode === 'new') {
Object.assign(postBody, { Object.assign(postBody, {
@ -87,6 +99,80 @@ export default function ProcessModelForm({
setProcessModel(processModelToCopy); 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) => { const onDisplayNameChanged = (newDisplayName: any) => {
setDisplayNameInvalid(false); setDisplayNameInvalid(false);
const updateDict = { display_name: newDisplayName }; const updateDict = { display_name: newDisplayName };
@ -145,6 +231,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; return textInputs;
}; };

View File

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

View File

@ -69,6 +69,24 @@ h2 {
color: black; 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 { .cds--header__global .cds--btn--primary {
background-color: #161616 background-color: #161616
} }
@ -151,10 +169,22 @@ h1.with-icons {
margin-top: 1em; margin-top: 1em;
} }
.with-extra-top-margin {
margin-top: 1.3em;
}
.with-tiny-top-margin {
margin-top: 4px;
}
.with-large-bottom-margin { .with-large-bottom-margin {
margin-bottom: 3em; margin-bottom: 3em;
} }
.with-tiny-bottom-margin {
margin-bottom: 4px;
}
.diagram-viewer-canvas { .diagram-viewer-canvas {
border:1px solid #000000; border:1px solid #000000;
height:70vh; height:70vh;
@ -297,3 +327,38 @@ td.actions-cell {
text-align: right; text-align: right;
padding-bottom: 10px; 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

@ -38,11 +38,58 @@ export interface ProcessFile {
export interface ProcessInstance { export interface ProcessInstance {
id: number; id: number;
process_model_identifier: string; 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 { export interface ProcessInstanceReport {
id: string; id: number;
display_name: string; identifier: string;
name: string;
report_metadata: ReportMetadata;
} }
export interface ProcessGroupLite { export interface ProcessGroupLite {
@ -50,6 +97,11 @@ export interface ProcessGroupLite {
display_name: string; display_name: string;
} }
export interface MetadataExtractionPath {
key: string;
path: string;
}
export interface ProcessModel { export interface ProcessModel {
id: string; id: string;
description: string; description: string;
@ -57,6 +109,7 @@ export interface ProcessModel {
primary_file_name: string; primary_file_name: string;
files: ProcessFile[]; files: ProcessFile[];
parent_groups?: ProcessGroupLite[]; parent_groups?: ProcessGroupLite[];
metadata_extraction_paths?: MetadataExtractionPath[];
} }
export interface ProcessGroup { export interface ProcessGroup {

View File

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