Merge pull request #217 from sartography/feature/log_filters
Feature/log filters
This commit is contained in:
commit
991f177dad
|
@ -1959,10 +1959,34 @@ paths:
|
|||
description: Show the detailed view, which includes all log entries
|
||||
schema:
|
||||
type: boolean
|
||||
- name: bpmn_name
|
||||
in: query
|
||||
required: false
|
||||
description: The bpmn name of the task to search for.
|
||||
schema:
|
||||
type: string
|
||||
- name: bpmn_identifier
|
||||
in: query
|
||||
required: false
|
||||
description: The bpmn identifier of the task to search for.
|
||||
schema:
|
||||
type: string
|
||||
- name: task_type
|
||||
in: query
|
||||
required: false
|
||||
description: The task type of the task to search for.
|
||||
schema:
|
||||
type: string
|
||||
- name: event_type
|
||||
in: query
|
||||
required: false
|
||||
description: The type of the event to search for.
|
||||
schema:
|
||||
type: string
|
||||
get:
|
||||
tags:
|
||||
- Process Instances
|
||||
operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_log_list
|
||||
- Process Instance Events
|
||||
operationId: spiffworkflow_backend.routes.process_instance_events_controller.log_list
|
||||
summary: returns a list of logs associated with the process instance
|
||||
responses:
|
||||
"200":
|
||||
|
@ -1972,6 +1996,20 @@ paths:
|
|||
schema:
|
||||
$ref: "#/components/schemas/ProcessInstanceLog"
|
||||
|
||||
/logs/types:
|
||||
get:
|
||||
tags:
|
||||
- Process Instance Events
|
||||
operationId: spiffworkflow_backend.routes.process_instance_events_controller.types
|
||||
summary: returns a list of task types and event typs. useful for building log queries.
|
||||
responses:
|
||||
"200":
|
||||
description: list of types
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ProcessInstanceLog"
|
||||
|
||||
/secrets:
|
||||
parameters:
|
||||
- name: page
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
"""Spiff_enum."""
|
||||
import enum
|
||||
|
||||
|
||||
class SpiffEnum(enum.Enum):
|
||||
"""SpiffEnum."""
|
||||
|
||||
@classmethod
|
||||
def list(cls) -> list[str]:
|
||||
"""List."""
|
||||
return [el.value for el in cls]
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
from typing import Optional
|
||||
|
||||
import flask.wrappers
|
||||
from flask import jsonify
|
||||
from flask import make_response
|
||||
from sqlalchemy import and_
|
||||
|
||||
from spiffworkflow_backend.models.bpmn_process_definition import BpmnProcessDefinitionModel
|
||||
from spiffworkflow_backend.models.db import db
|
||||
from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventModel
|
||||
from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventType
|
||||
from spiffworkflow_backend.models.task import TaskModel # noqa: F401
|
||||
from spiffworkflow_backend.models.task_definition import TaskDefinitionModel
|
||||
from spiffworkflow_backend.models.user import UserModel
|
||||
from spiffworkflow_backend.routes.process_api_blueprint import (
|
||||
_find_process_instance_by_id_or_raise,
|
||||
)
|
||||
|
||||
|
||||
def log_list(
|
||||
modified_process_model_identifier: str,
|
||||
process_instance_id: int,
|
||||
page: int = 1,
|
||||
per_page: int = 100,
|
||||
detailed: bool = False,
|
||||
bpmn_name: Optional[str] = None,
|
||||
bpmn_identifier: Optional[str] = None,
|
||||
task_type: Optional[str] = None,
|
||||
event_type: Optional[str] = None,
|
||||
) -> flask.wrappers.Response:
|
||||
# to make sure the process instance exists
|
||||
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
|
||||
|
||||
log_query = (
|
||||
ProcessInstanceEventModel.query.filter_by(process_instance_id=process_instance.id)
|
||||
.outerjoin(TaskModel, TaskModel.guid == ProcessInstanceEventModel.task_guid)
|
||||
.outerjoin(TaskDefinitionModel, TaskDefinitionModel.id == TaskModel.task_definition_id)
|
||||
.outerjoin(
|
||||
BpmnProcessDefinitionModel,
|
||||
BpmnProcessDefinitionModel.id == TaskDefinitionModel.bpmn_process_definition_id,
|
||||
)
|
||||
)
|
||||
if not detailed:
|
||||
log_query = log_query.filter(
|
||||
and_(
|
||||
TaskModel.state.in_(["COMPLETED"]), # type: ignore
|
||||
TaskDefinitionModel.typename.in_(["IntermediateThrowEvent"]), # type: ignore
|
||||
)
|
||||
)
|
||||
|
||||
if bpmn_name is not None:
|
||||
log_query = log_query.filter(TaskDefinitionModel.bpmn_name == bpmn_name)
|
||||
if bpmn_identifier is not None:
|
||||
log_query = log_query.filter(TaskDefinitionModel.bpmn_identifier == bpmn_identifier)
|
||||
if task_type is not None:
|
||||
log_query = log_query.filter(TaskDefinitionModel.typename == task_type)
|
||||
if event_type is not None:
|
||||
log_query = log_query.filter(ProcessInstanceEventModel.event_type == event_type)
|
||||
|
||||
logs = (
|
||||
log_query.order_by(
|
||||
ProcessInstanceEventModel.timestamp.desc(), ProcessInstanceEventModel.id.desc() # type: ignore
|
||||
)
|
||||
.outerjoin(UserModel, UserModel.id == ProcessInstanceEventModel.user_id)
|
||||
.add_columns(
|
||||
TaskModel.guid.label("spiff_task_guid"), # type: ignore
|
||||
UserModel.username,
|
||||
BpmnProcessDefinitionModel.bpmn_identifier.label("bpmn_process_definition_identifier"), # type: ignore
|
||||
BpmnProcessDefinitionModel.bpmn_name.label("bpmn_process_definition_name"), # type: ignore
|
||||
TaskDefinitionModel.bpmn_identifier.label("task_definition_identifier"), # type: ignore
|
||||
TaskDefinitionModel.bpmn_name.label("task_definition_name"), # type: ignore
|
||||
TaskDefinitionModel.typename.label("bpmn_task_type"), # type: ignore
|
||||
)
|
||||
.paginate(page=page, per_page=per_page, error_out=False)
|
||||
)
|
||||
|
||||
response_json = {
|
||||
"results": logs.items,
|
||||
"pagination": {
|
||||
"count": len(logs.items),
|
||||
"total": logs.total,
|
||||
"pages": logs.pages,
|
||||
},
|
||||
}
|
||||
|
||||
return make_response(jsonify(response_json), 200)
|
||||
|
||||
|
||||
def types() -> flask.wrappers.Response:
|
||||
query = db.session.query(TaskDefinitionModel.typename).distinct() # type: ignore
|
||||
task_types = [t.typename for t in query]
|
||||
event_types = ProcessInstanceEventType.list()
|
||||
return make_response(jsonify({"task_types": task_types, "event_types": event_types}), 200)
|
|
@ -30,9 +30,6 @@ from spiffworkflow_backend.models.process_instance import (
|
|||
)
|
||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
|
||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceModelSchema
|
||||
from spiffworkflow_backend.models.process_instance_event import (
|
||||
ProcessInstanceEventModel,
|
||||
)
|
||||
from spiffworkflow_backend.models.process_instance_metadata import (
|
||||
ProcessInstanceMetadataModel,
|
||||
)
|
||||
|
@ -47,7 +44,6 @@ from spiffworkflow_backend.models.spec_reference import SpecReferenceCache
|
|||
from spiffworkflow_backend.models.spec_reference import SpecReferenceNotFoundError
|
||||
from spiffworkflow_backend.models.task import TaskModel
|
||||
from spiffworkflow_backend.models.task_definition import TaskDefinitionModel
|
||||
from spiffworkflow_backend.models.user import UserModel
|
||||
from spiffworkflow_backend.routes.process_api_blueprint import (
|
||||
_find_process_instance_by_id_or_raise,
|
||||
)
|
||||
|
@ -224,63 +220,6 @@ def process_instance_resume(
|
|||
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
|
||||
|
||||
|
||||
def process_instance_log_list(
|
||||
modified_process_model_identifier: str,
|
||||
process_instance_id: int,
|
||||
page: int = 1,
|
||||
per_page: int = 100,
|
||||
detailed: bool = False,
|
||||
) -> flask.wrappers.Response:
|
||||
"""Process_instance_log_list."""
|
||||
# to make sure the process instance exists
|
||||
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
|
||||
|
||||
log_query = (
|
||||
ProcessInstanceEventModel.query.filter_by(process_instance_id=process_instance.id)
|
||||
.outerjoin(TaskModel, TaskModel.guid == ProcessInstanceEventModel.task_guid)
|
||||
.outerjoin(TaskDefinitionModel, TaskDefinitionModel.id == TaskModel.task_definition_id)
|
||||
.outerjoin(
|
||||
BpmnProcessDefinitionModel,
|
||||
BpmnProcessDefinitionModel.id == TaskDefinitionModel.bpmn_process_definition_id,
|
||||
)
|
||||
)
|
||||
if not detailed:
|
||||
log_query = log_query.filter(
|
||||
and_(
|
||||
TaskModel.state.in_(["COMPLETED"]), # type: ignore
|
||||
TaskDefinitionModel.typename.in_(["IntermediateThrowEvent"]), # type: ignore
|
||||
)
|
||||
)
|
||||
|
||||
logs = (
|
||||
log_query.order_by(
|
||||
ProcessInstanceEventModel.timestamp.desc(), ProcessInstanceEventModel.id.desc() # type: ignore
|
||||
)
|
||||
.outerjoin(UserModel, UserModel.id == ProcessInstanceEventModel.user_id)
|
||||
.add_columns(
|
||||
TaskModel.guid.label("spiff_task_guid"), # type: ignore
|
||||
UserModel.username,
|
||||
BpmnProcessDefinitionModel.bpmn_identifier.label("bpmn_process_definition_identifier"), # type: ignore
|
||||
BpmnProcessDefinitionModel.bpmn_name.label("bpmn_process_definition_name"), # type: ignore
|
||||
TaskDefinitionModel.bpmn_identifier.label("task_definition_identifier"), # type: ignore
|
||||
TaskDefinitionModel.bpmn_name.label("task_definition_name"), # type: ignore
|
||||
TaskDefinitionModel.typename.label("bpmn_task_type"), # type: ignore
|
||||
)
|
||||
.paginate(page=page, per_page=per_page, error_out=False)
|
||||
)
|
||||
|
||||
response_json = {
|
||||
"results": logs.items,
|
||||
"pagination": {
|
||||
"count": len(logs.items),
|
||||
"total": logs.total,
|
||||
"pages": logs.pages,
|
||||
},
|
||||
}
|
||||
|
||||
return make_response(jsonify(response_json), 200)
|
||||
|
||||
|
||||
def process_instance_list_for_me(
|
||||
process_model_identifier: Optional[str] = None,
|
||||
page: int = 1,
|
||||
|
|
|
@ -567,6 +567,7 @@ class AuthorizationService:
|
|||
permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/processes"))
|
||||
permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/service-tasks"))
|
||||
permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/user-groups/for-current-user"))
|
||||
permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/logs/types"))
|
||||
permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/users/exists/by-username"))
|
||||
permissions_to_assign.append(
|
||||
PermissionToAssign(permission="read", target_uri="/process-instances/find-by-id/*")
|
||||
|
|
|
@ -275,6 +275,7 @@ class TestAuthorizationService(BaseTest):
|
|||
) -> None:
|
||||
"""Test_explode_permissions_basic."""
|
||||
expected_permissions = [
|
||||
("/logs/types", "read"),
|
||||
("/process-instances/find-by-id/*", "read"),
|
||||
("/process-instances/for-me", "read"),
|
||||
("/process-instances/reports/*", "create"),
|
||||
|
|
|
@ -58,11 +58,12 @@
|
|||
"react-icons": "^4.4.0",
|
||||
"react-jsonschema-form": "^1.8.1",
|
||||
"react-router": "^6.3.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-router-dom": "6.3.0",
|
||||
"react-scripts": "^5.0.1",
|
||||
"serve": "^14.0.0",
|
||||
"timepicker": "^1.13.18",
|
||||
"typescript": "^4.7.4",
|
||||
"use-debounce": "^9.0.4",
|
||||
"web-vitals": "^3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -15500,6 +15501,14 @@
|
|||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/history": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz",
|
||||
"integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.7.6"
|
||||
}
|
||||
},
|
||||
"node_modules/hmac-drbg": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
|
||||
|
@ -25507,21 +25516,29 @@
|
|||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.10.0.tgz",
|
||||
"integrity": "sha512-E5dfxRPuXKJqzwSe/qGcqdwa18QiWC6f3H3cWXM24qj4N0/beCIf/CWTipop2xm7mR0RCS99NnaqPNjHtrAzCg==",
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz",
|
||||
"integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.5.0",
|
||||
"react-router": "6.10.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
"history": "^5.2.0",
|
||||
"react-router": "6.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8",
|
||||
"react-dom": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom/node_modules/react-router": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz",
|
||||
"integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==",
|
||||
"dependencies": {
|
||||
"history": "^5.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-scripts": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
|
||||
|
@ -30671,6 +30688,17 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-debounce": {
|
||||
"version": "9.0.4",
|
||||
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-9.0.4.tgz",
|
||||
"integrity": "sha512-6X8H/mikbrt0XE8e+JXRtZ8yYVvKkdYRfmIhWZYsP8rcNs9hk3APV8Ua2mFkKRLcJKVdnX2/Vwrmg2GWKUQEaQ==",
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-resize-observer": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-6.1.0.tgz",
|
||||
|
@ -43754,6 +43782,14 @@
|
|||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
|
||||
},
|
||||
"history": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz",
|
||||
"integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.7.6"
|
||||
}
|
||||
},
|
||||
"hmac-drbg": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
|
||||
|
@ -50970,12 +51006,22 @@
|
|||
}
|
||||
},
|
||||
"react-router-dom": {
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.10.0.tgz",
|
||||
"integrity": "sha512-E5dfxRPuXKJqzwSe/qGcqdwa18QiWC6f3H3cWXM24qj4N0/beCIf/CWTipop2xm7mR0RCS99NnaqPNjHtrAzCg==",
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz",
|
||||
"integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
|
||||
"requires": {
|
||||
"@remix-run/router": "1.5.0",
|
||||
"react-router": "6.10.0"
|
||||
"history": "^5.2.0",
|
||||
"react-router": "6.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"react-router": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz",
|
||||
"integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==",
|
||||
"requires": {
|
||||
"history": "^5.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-scripts": {
|
||||
|
@ -54978,6 +55024,12 @@
|
|||
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
|
||||
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ=="
|
||||
},
|
||||
"use-debounce": {
|
||||
"version": "9.0.4",
|
||||
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-9.0.4.tgz",
|
||||
"integrity": "sha512-6X8H/mikbrt0XE8e+JXRtZ8yYVvKkdYRfmIhWZYsP8rcNs9hk3APV8Ua2mFkKRLcJKVdnX2/Vwrmg2GWKUQEaQ==",
|
||||
"requires": {}
|
||||
},
|
||||
"use-resize-observer": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-6.1.0.tgz",
|
||||
|
|
|
@ -53,11 +53,12 @@
|
|||
"react-icons": "^4.4.0",
|
||||
"react-jsonschema-form": "^1.8.1",
|
||||
"react-router": "^6.3.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-router-dom": "6.3.0",
|
||||
"react-scripts": "^5.0.1",
|
||||
"serve": "^14.0.0",
|
||||
"timepicker": "^1.13.18",
|
||||
"typescript": "^4.7.4",
|
||||
"use-debounce": "^9.0.4",
|
||||
"web-vitals": "^3.0.2"
|
||||
},
|
||||
"overrides": {
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
// @ts-ignore
|
||||
import { Filter } from '@carbon/icons-react';
|
||||
import {
|
||||
Button,
|
||||
Grid,
|
||||
Column,
|
||||
// @ts-ignore
|
||||
} from '@carbon/react';
|
||||
|
||||
type OwnProps = {
|
||||
showFilterOptions: boolean;
|
||||
setShowFilterOptions: Function;
|
||||
filterOptions: Function;
|
||||
filtersEnabled?: boolean;
|
||||
reportSearchComponent?: Function | null;
|
||||
};
|
||||
|
||||
export default function Filters({
|
||||
showFilterOptions,
|
||||
setShowFilterOptions,
|
||||
filterOptions,
|
||||
reportSearchComponent = null,
|
||||
filtersEnabled = true,
|
||||
}: OwnProps) {
|
||||
const toggleShowFilterOptions = () => {
|
||||
setShowFilterOptions(!showFilterOptions);
|
||||
};
|
||||
|
||||
if (filtersEnabled) {
|
||||
let reportSearchSection = null;
|
||||
if (reportSearchComponent) {
|
||||
reportSearchSection = (
|
||||
<Column sm={2} md={4} lg={7}>
|
||||
{reportSearchComponent()}
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Grid fullWidth>
|
||||
{reportSearchSection}
|
||||
<Column
|
||||
className="filterIcon"
|
||||
sm={{ span: 1, offset: 3 }}
|
||||
md={{ span: 1, offset: 7 }}
|
||||
lg={{ span: 1, offset: 15 }}
|
||||
>
|
||||
<Button
|
||||
data-qa="filter-section-expand-toggle"
|
||||
renderIcon={Filter}
|
||||
iconDescription="Filter Options"
|
||||
hasIconOnly
|
||||
size="lg"
|
||||
onClick={toggleShowFilterOptions}
|
||||
/>
|
||||
</Column>
|
||||
</Grid>
|
||||
{filterOptions()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
|
@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
|||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
|
||||
// @ts-ignore
|
||||
import { Filter, Close, AddAlt } from '@carbon/icons-react';
|
||||
import { Close, AddAlt } from '@carbon/icons-react';
|
||||
import {
|
||||
Button,
|
||||
ButtonSet,
|
||||
|
@ -71,6 +71,7 @@ import { usePermissionFetcher } from '../hooks/PermissionService';
|
|||
import { Can } from '../contexts/Can';
|
||||
import TableCellWithTimeAgoInWords from './TableCellWithTimeAgoInWords';
|
||||
import UserService from '../services/UserService';
|
||||
import Filters from './Filters';
|
||||
|
||||
type OwnProps = {
|
||||
filtersEnabled?: boolean;
|
||||
|
@ -1506,10 +1507,6 @@ export default function ProcessInstanceListTable({
|
|||
);
|
||||
};
|
||||
|
||||
const toggleShowFilterOptions = () => {
|
||||
setShowFilterOptions(!showFilterOptions);
|
||||
};
|
||||
|
||||
const reportSearchComponent = () => {
|
||||
if (showReports) {
|
||||
const columns = [
|
||||
|
@ -1529,37 +1526,6 @@ export default function ProcessInstanceListTable({
|
|||
return null;
|
||||
};
|
||||
|
||||
const filterComponent = () => {
|
||||
if (!filtersEnabled) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Grid fullWidth>
|
||||
<Column sm={2} md={4} lg={7}>
|
||||
{reportSearchComponent()}
|
||||
</Column>
|
||||
<Column
|
||||
className="filterIcon"
|
||||
sm={{ span: 1, offset: 3 }}
|
||||
md={{ span: 1, offset: 7 }}
|
||||
lg={{ span: 1, offset: 15 }}
|
||||
>
|
||||
<Button
|
||||
data-qa="filter-section-expand-toggle"
|
||||
renderIcon={Filter}
|
||||
iconDescription="Filter Options"
|
||||
hasIconOnly
|
||||
size="lg"
|
||||
onClick={toggleShowFilterOptions}
|
||||
/>
|
||||
</Column>
|
||||
</Grid>
|
||||
{filterOptions()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
if (pagination && (!textToShowIfEmpty || pagination.total > 0)) {
|
||||
// eslint-disable-next-line prefer-const
|
||||
let { page, perPage } = getPageInfoFromSearchParams(
|
||||
|
@ -1599,7 +1565,13 @@ export default function ProcessInstanceListTable({
|
|||
<>
|
||||
{reportColumnForm()}
|
||||
{processInstanceReportSaveTag()}
|
||||
{filterComponent()}
|
||||
<Filters
|
||||
filterOptions={filterOptions}
|
||||
showFilterOptions={showFilterOptions}
|
||||
setShowFilterOptions={setShowFilterOptions}
|
||||
reportSearchComponent={reportSearchComponent}
|
||||
filtersEnabled={filtersEnabled}
|
||||
/>
|
||||
{resultsTable}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -26,6 +26,17 @@ export const underscorizeString = (inputString: string) => {
|
|||
return slugifyString(inputString).replace(/-/g, '_');
|
||||
};
|
||||
|
||||
export const selectKeysFromSearchParams = (obj: any, keys: string[]) => {
|
||||
const newSearchParams: { [key: string]: string } = {};
|
||||
keys.forEach((key: string) => {
|
||||
const value = obj.get(key);
|
||||
if (value) {
|
||||
newSearchParams[key] = value;
|
||||
}
|
||||
});
|
||||
return newSearchParams;
|
||||
};
|
||||
|
||||
export const capitalizeFirstLetter = (string: any) => {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
};
|
||||
|
|
|
@ -1,16 +1,35 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
// @ts-ignore
|
||||
import { Table, Tabs, TabList, Tab } from '@carbon/react';
|
||||
import { Link, useParams, useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
Table,
|
||||
Tabs,
|
||||
TabList,
|
||||
Tab,
|
||||
Grid,
|
||||
Column,
|
||||
ButtonSet,
|
||||
Button,
|
||||
TextInput,
|
||||
ComboBox,
|
||||
// @ts-ignore
|
||||
} from '@carbon/react';
|
||||
import {
|
||||
createSearchParams,
|
||||
Link,
|
||||
useParams,
|
||||
useSearchParams,
|
||||
} from 'react-router-dom';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import PaginationForTable from '../components/PaginationForTable';
|
||||
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
|
||||
import {
|
||||
getPageInfoFromSearchParams,
|
||||
convertSecondsToFormattedDateTime,
|
||||
selectKeysFromSearchParams,
|
||||
} from '../helpers';
|
||||
import HttpService from '../services/HttpService';
|
||||
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
|
||||
import { ProcessInstanceLogEntry } from '../interfaces';
|
||||
import Filters from '../components/Filters';
|
||||
|
||||
type OwnProps = {
|
||||
variant: string;
|
||||
|
@ -21,14 +40,42 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
|
|||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [processInstanceLogs, setProcessInstanceLogs] = useState([]);
|
||||
const [pagination, setPagination] = useState(null);
|
||||
|
||||
const [taskName, setTaskName] = useState<string>('');
|
||||
const [taskIdentifier, setTaskIdentifier] = useState<string>('');
|
||||
|
||||
const [taskTypes, setTaskTypes] = useState<string[]>([]);
|
||||
const [eventTypes, setEventTypes] = useState<string[]>([]);
|
||||
|
||||
const { targetUris } = useUriListForPermissions();
|
||||
const isDetailedView = searchParams.get('detailed') === 'true';
|
||||
|
||||
const taskNameHeader = isDetailedView ? 'Task Name' : 'Milestone';
|
||||
|
||||
const [showFilterOptions, setShowFilterOptions] = useState<boolean>(false);
|
||||
|
||||
let processInstanceShowPageBaseUrl = `/admin/process-instances/for-me/${params.process_model_id}`;
|
||||
if (variant === 'all') {
|
||||
processInstanceShowPageBaseUrl = `/admin/process-instances/${params.process_model_id}`;
|
||||
}
|
||||
|
||||
const updateSearchParams = (value: string, key: string) => {
|
||||
if (value) {
|
||||
searchParams.set(key, value);
|
||||
} else {
|
||||
searchParams.delete(key);
|
||||
}
|
||||
setSearchParams(searchParams);
|
||||
};
|
||||
|
||||
const addDebouncedSearchParams = useDebouncedCallback(
|
||||
(value: string, key: string) => {
|
||||
updateSearchParams(value, key);
|
||||
},
|
||||
// delay in ms
|
||||
1000
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Clear out any previous results to avoid a "flicker" effect where columns
|
||||
// are updated above the incorrect data.
|
||||
|
@ -39,11 +86,41 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
|
|||
setProcessInstanceLogs(result.results);
|
||||
setPagination(result.pagination);
|
||||
};
|
||||
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
|
||||
|
||||
const searchParamsToInclude = [
|
||||
'detailed',
|
||||
'page',
|
||||
'per_page',
|
||||
'bpmn_name',
|
||||
'bpmn_identifier',
|
||||
'task_type',
|
||||
'event_type',
|
||||
];
|
||||
const pickedSearchParams = selectKeysFromSearchParams(
|
||||
searchParams,
|
||||
searchParamsToInclude
|
||||
);
|
||||
|
||||
if ('bpmn_name' in pickedSearchParams) {
|
||||
setTaskName(pickedSearchParams.bpmn_name);
|
||||
}
|
||||
if ('bpmn_identifier' in pickedSearchParams) {
|
||||
setTaskIdentifier(pickedSearchParams.bpmn_identifier);
|
||||
}
|
||||
|
||||
HttpService.makeCallToBackend({
|
||||
path: `${targetUris.processInstanceLogListPath}?per_page=${perPage}&page=${page}&detailed=${isDetailedView}`,
|
||||
path: `${targetUris.processInstanceLogListPath}?${createSearchParams(
|
||||
pickedSearchParams
|
||||
)}`,
|
||||
successCallback: setProcessInstanceLogListFromResult,
|
||||
});
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/v1.0/logs/types`,
|
||||
successCallback: (result: any) => {
|
||||
setTaskTypes(result.task_types);
|
||||
setEventTypes(result.event_types);
|
||||
},
|
||||
});
|
||||
}, [
|
||||
searchParams,
|
||||
params,
|
||||
|
@ -85,6 +162,7 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
|
|||
if (isDetailedView) {
|
||||
tableRow.push(
|
||||
<>
|
||||
<td>{logEntry.task_definition_identifier}</td>
|
||||
<td>{logEntry.bpmn_task_type}</td>
|
||||
<td>{logEntry.event_type}</td>
|
||||
<td>
|
||||
|
@ -130,13 +208,13 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
|
|||
<>
|
||||
<th>Id</th>
|
||||
<th>Bpmn Process</th>
|
||||
<th>Task Name</th>
|
||||
<th>{taskNameHeader}</th>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
tableHeaders.push(
|
||||
<>
|
||||
<th>Event</th>
|
||||
<th>{taskNameHeader}</th>
|
||||
<th>Bpmn Process</th>
|
||||
</>
|
||||
);
|
||||
|
@ -144,8 +222,9 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
|
|||
if (isDetailedView) {
|
||||
tableHeaders.push(
|
||||
<>
|
||||
<th>Task Identifier</th>
|
||||
<th>Task Type</th>
|
||||
<th>Event</th>
|
||||
<th>Event Type</th>
|
||||
<th>User</th>
|
||||
</>
|
||||
);
|
||||
|
@ -160,27 +239,126 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
|
|||
</Table>
|
||||
);
|
||||
};
|
||||
const selectedTabIndex = isDetailedView ? 1 : 0;
|
||||
|
||||
if (pagination) {
|
||||
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
|
||||
const resetFilters = () => {
|
||||
setTaskIdentifier('');
|
||||
setTaskName('');
|
||||
|
||||
['bpmn_name', 'bpmn_identifier', 'task_type', 'event_type'].forEach(
|
||||
(value: string) => searchParams.delete(value)
|
||||
);
|
||||
|
||||
setSearchParams(searchParams);
|
||||
};
|
||||
|
||||
const shouldFilterStringItem = (options: any) => {
|
||||
const stringItem = options.item;
|
||||
let { inputValue } = options;
|
||||
if (!inputValue) {
|
||||
inputValue = '';
|
||||
}
|
||||
return stringItem.toLowerCase().includes(inputValue.toLowerCase());
|
||||
};
|
||||
|
||||
const filterOptions = () => {
|
||||
if (!showFilterOptions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filterElements = [];
|
||||
filterElements.push(
|
||||
<Column md={4}>
|
||||
<TextInput
|
||||
id="task-name-filter"
|
||||
labelText={taskNameHeader}
|
||||
value={taskName}
|
||||
onChange={(event: any) => {
|
||||
const newValue = event.target.value;
|
||||
setTaskName(newValue);
|
||||
addDebouncedSearchParams(newValue, 'bpmn_name');
|
||||
}}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
|
||||
if (isDetailedView) {
|
||||
filterElements.push(
|
||||
<>
|
||||
<Column md={4}>
|
||||
<TextInput
|
||||
id="task-identifier-filter"
|
||||
labelText="Task Identifier"
|
||||
value={taskIdentifier}
|
||||
onChange={(event: any) => {
|
||||
const newValue = event.target.value;
|
||||
setTaskIdentifier(newValue);
|
||||
addDebouncedSearchParams(newValue, 'bpmn_identifier');
|
||||
}}
|
||||
/>
|
||||
</Column>
|
||||
<Column md={4}>
|
||||
<ComboBox
|
||||
onChange={(value: any) => {
|
||||
updateSearchParams(value.selectedItem, 'task_type');
|
||||
}}
|
||||
id="task-type-select"
|
||||
data-qa="task-type-select"
|
||||
items={taskTypes}
|
||||
itemToString={(value: string) => {
|
||||
return value;
|
||||
}}
|
||||
shouldFilterItem={shouldFilterStringItem}
|
||||
placeholder="Choose a process model"
|
||||
titleText="Task Type"
|
||||
selectedItem={searchParams.get('task_type')}
|
||||
/>
|
||||
</Column>
|
||||
<Column md={4}>
|
||||
<ComboBox
|
||||
onChange={(value: any) => {
|
||||
updateSearchParams(value.selectedItem, 'event_type');
|
||||
}}
|
||||
id="event-type-select"
|
||||
data-qa="event-type-select"
|
||||
items={eventTypes}
|
||||
itemToString={(value: string) => {
|
||||
return value;
|
||||
}}
|
||||
shouldFilterItem={shouldFilterStringItem}
|
||||
placeholder="Choose a process model"
|
||||
titleText="Event Type"
|
||||
selectedItem={searchParams.get('event_type')}
|
||||
/>
|
||||
</Column>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProcessBreadcrumb
|
||||
hotCrumbs={[
|
||||
['Process Groups', '/admin'],
|
||||
{
|
||||
entityToExplode: params.process_model_id || '',
|
||||
entityType: 'process-model-id',
|
||||
linkLastItem: true,
|
||||
},
|
||||
[
|
||||
`Process Instance: ${params.process_instance_id}`,
|
||||
`${processInstanceShowPageBaseUrl}/${params.process_instance_id}`,
|
||||
],
|
||||
['Logs'],
|
||||
]}
|
||||
/>
|
||||
<Grid fullWidth className="with-bottom-margin">
|
||||
{filterElements}
|
||||
</Grid>
|
||||
<Grid fullWidth className="with-bottom-margin">
|
||||
<Column sm={4} md={4} lg={8}>
|
||||
<ButtonSet>
|
||||
<Button
|
||||
kind=""
|
||||
className="button-white-background narrow-button"
|
||||
onClick={resetFilters}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</ButtonSet>
|
||||
</Column>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const tabs = () => {
|
||||
const selectedTabIndex = isDetailedView ? 1 : 0;
|
||||
return (
|
||||
<Tabs selectedIndex={selectedTabIndex}>
|
||||
<TabList aria-label="List of tabs">
|
||||
<Tab
|
||||
|
@ -205,6 +383,34 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
|
|||
</Tab>
|
||||
</TabList>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
|
||||
return (
|
||||
<>
|
||||
<ProcessBreadcrumb
|
||||
hotCrumbs={[
|
||||
['Process Groups', '/admin'],
|
||||
{
|
||||
entityToExplode: params.process_model_id || '',
|
||||
entityType: 'process-model-id',
|
||||
linkLastItem: true,
|
||||
},
|
||||
[
|
||||
`Process Instance: ${params.process_instance_id}`,
|
||||
`${processInstanceShowPageBaseUrl}/${params.process_instance_id}`,
|
||||
],
|
||||
['Logs'],
|
||||
]}
|
||||
/>
|
||||
{tabs()}
|
||||
<Filters
|
||||
filterOptions={filterOptions}
|
||||
showFilterOptions={showFilterOptions}
|
||||
setShowFilterOptions={setShowFilterOptions}
|
||||
filtersEnabled
|
||||
/>
|
||||
<br />
|
||||
<PaginationForTable
|
||||
page={page}
|
||||
|
@ -214,6 +420,4 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
|
|||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -890,7 +890,7 @@ export default function ProcessModelEditDiagram() {
|
|||
const path = generatePath(
|
||||
'/admin/process-models/:process_model_id/form/:file_name',
|
||||
{
|
||||
process_model_id: params.process_model_id || null,
|
||||
process_model_id: params.process_model_id,
|
||||
file_name: fileName,
|
||||
}
|
||||
);
|
||||
|
@ -902,7 +902,7 @@ export default function ProcessModelEditDiagram() {
|
|||
const path = generatePath(
|
||||
'/admin/process-models/:process_model_id/files/:file_name',
|
||||
{
|
||||
process_model_id: params.process_model_id || null,
|
||||
process_model_id: params.process_model_id,
|
||||
file_name: file.name,
|
||||
}
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue