Merge pull request #217 from sartography/feature/log_filters

Feature/log filters
This commit is contained in:
jasquat 2023-04-18 12:44:19 -04:00 committed by GitHub
commit 991f177dad
13 changed files with 551 additions and 180 deletions

View File

@ -1959,10 +1959,34 @@ paths:
description: Show the detailed view, which includes all log entries description: Show the detailed view, which includes all log entries
schema: schema:
type: boolean 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: get:
tags: tags:
- Process Instances - Process Instance Events
operationId: spiffworkflow_backend.routes.process_instances_controller.process_instance_log_list operationId: spiffworkflow_backend.routes.process_instance_events_controller.log_list
summary: returns a list of logs associated with the process instance summary: returns a list of logs associated with the process instance
responses: responses:
"200": "200":
@ -1972,6 +1996,20 @@ paths:
schema: schema:
$ref: "#/components/schemas/ProcessInstanceLog" $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: /secrets:
parameters: parameters:
- name: page - name: page

View File

@ -1,11 +1,7 @@
"""Spiff_enum."""
import enum import enum
class SpiffEnum(enum.Enum): class SpiffEnum(enum.Enum):
"""SpiffEnum."""
@classmethod @classmethod
def list(cls) -> list[str]: def list(cls) -> list[str]:
"""List."""
return [el.value for el in cls] return [el.value for el in cls]

View File

@ -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)

View File

@ -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 ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModelSchema 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 ( from spiffworkflow_backend.models.process_instance_metadata import (
ProcessInstanceMetadataModel, 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.spec_reference import SpecReferenceNotFoundError
from spiffworkflow_backend.models.task import TaskModel from spiffworkflow_backend.models.task import TaskModel
from spiffworkflow_backend.models.task_definition import TaskDefinitionModel from spiffworkflow_backend.models.task_definition import TaskDefinitionModel
from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.routes.process_api_blueprint import ( from spiffworkflow_backend.routes.process_api_blueprint import (
_find_process_instance_by_id_or_raise, _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") 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( def process_instance_list_for_me(
process_model_identifier: Optional[str] = None, process_model_identifier: Optional[str] = None,
page: int = 1, page: int = 1,

View File

@ -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="/processes"))
permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/service-tasks")) 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="/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="create", target_uri="/users/exists/by-username"))
permissions_to_assign.append( permissions_to_assign.append(
PermissionToAssign(permission="read", target_uri="/process-instances/find-by-id/*") PermissionToAssign(permission="read", target_uri="/process-instances/find-by-id/*")

View File

@ -275,6 +275,7 @@ class TestAuthorizationService(BaseTest):
) -> None: ) -> None:
"""Test_explode_permissions_basic.""" """Test_explode_permissions_basic."""
expected_permissions = [ expected_permissions = [
("/logs/types", "read"),
("/process-instances/find-by-id/*", "read"), ("/process-instances/find-by-id/*", "read"),
("/process-instances/for-me", "read"), ("/process-instances/for-me", "read"),
("/process-instances/reports/*", "create"), ("/process-instances/reports/*", "create"),

View File

@ -58,11 +58,12 @@
"react-icons": "^4.4.0", "react-icons": "^4.4.0",
"react-jsonschema-form": "^1.8.1", "react-jsonschema-form": "^1.8.1",
"react-router": "^6.3.0", "react-router": "^6.3.0",
"react-router-dom": "^6.3.0", "react-router-dom": "6.3.0",
"react-scripts": "^5.0.1", "react-scripts": "^5.0.1",
"serve": "^14.0.0", "serve": "^14.0.0",
"timepicker": "^1.13.18", "timepicker": "^1.13.18",
"typescript": "^4.7.4", "typescript": "^4.7.4",
"use-debounce": "^9.0.4",
"web-vitals": "^3.0.2" "web-vitals": "^3.0.2"
}, },
"devDependencies": { "devDependencies": {
@ -15500,6 +15501,14 @@
"he": "bin/he" "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": { "node_modules/hmac-drbg": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
@ -25507,21 +25516,29 @@
} }
}, },
"node_modules/react-router-dom": { "node_modules/react-router-dom": {
"version": "6.10.0", "version": "6.3.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.10.0.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz",
"integrity": "sha512-E5dfxRPuXKJqzwSe/qGcqdwa18QiWC6f3H3cWXM24qj4N0/beCIf/CWTipop2xm7mR0RCS99NnaqPNjHtrAzCg==", "integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
"dependencies": { "dependencies": {
"@remix-run/router": "1.5.0", "history": "^5.2.0",
"react-router": "6.10.0" "react-router": "6.3.0"
},
"engines": {
"node": ">=14"
}, },
"peerDependencies": { "peerDependencies": {
"react": ">=16.8", "react": ">=16.8",
"react-dom": ">=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": { "node_modules/react-scripts": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
@ -30671,6 +30688,17 @@
"node": ">=0.10.0" "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": { "node_modules/use-resize-observer": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-6.1.0.tgz", "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", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" "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": { "hmac-drbg": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
@ -50970,12 +51006,22 @@
} }
}, },
"react-router-dom": { "react-router-dom": {
"version": "6.10.0", "version": "6.3.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.10.0.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz",
"integrity": "sha512-E5dfxRPuXKJqzwSe/qGcqdwa18QiWC6f3H3cWXM24qj4N0/beCIf/CWTipop2xm7mR0RCS99NnaqPNjHtrAzCg==", "integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
"requires": { "requires": {
"@remix-run/router": "1.5.0", "history": "^5.2.0",
"react-router": "6.10.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": { "react-scripts": {
@ -54978,6 +55024,12 @@
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" "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": { "use-resize-observer": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-6.1.0.tgz", "resolved": "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-6.1.0.tgz",

View File

@ -53,11 +53,12 @@
"react-icons": "^4.4.0", "react-icons": "^4.4.0",
"react-jsonschema-form": "^1.8.1", "react-jsonschema-form": "^1.8.1",
"react-router": "^6.3.0", "react-router": "^6.3.0",
"react-router-dom": "^6.3.0", "react-router-dom": "6.3.0",
"react-scripts": "^5.0.1", "react-scripts": "^5.0.1",
"serve": "^14.0.0", "serve": "^14.0.0",
"timepicker": "^1.13.18", "timepicker": "^1.13.18",
"typescript": "^4.7.4", "typescript": "^4.7.4",
"use-debounce": "^9.0.4",
"web-vitals": "^3.0.2" "web-vitals": "^3.0.2"
}, },
"overrides": { "overrides": {

View File

@ -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;
}

View File

@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
// @ts-ignore // @ts-ignore
import { Filter, Close, AddAlt } from '@carbon/icons-react'; import { Close, AddAlt } from '@carbon/icons-react';
import { import {
Button, Button,
ButtonSet, ButtonSet,
@ -71,6 +71,7 @@ import { usePermissionFetcher } from '../hooks/PermissionService';
import { Can } from '../contexts/Can'; import { Can } from '../contexts/Can';
import TableCellWithTimeAgoInWords from './TableCellWithTimeAgoInWords'; import TableCellWithTimeAgoInWords from './TableCellWithTimeAgoInWords';
import UserService from '../services/UserService'; import UserService from '../services/UserService';
import Filters from './Filters';
type OwnProps = { type OwnProps = {
filtersEnabled?: boolean; filtersEnabled?: boolean;
@ -1506,10 +1507,6 @@ export default function ProcessInstanceListTable({
); );
}; };
const toggleShowFilterOptions = () => {
setShowFilterOptions(!showFilterOptions);
};
const reportSearchComponent = () => { const reportSearchComponent = () => {
if (showReports) { if (showReports) {
const columns = [ const columns = [
@ -1529,37 +1526,6 @@ export default function ProcessInstanceListTable({
return null; 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)) { if (pagination && (!textToShowIfEmpty || pagination.total > 0)) {
// eslint-disable-next-line prefer-const // eslint-disable-next-line prefer-const
let { page, perPage } = getPageInfoFromSearchParams( let { page, perPage } = getPageInfoFromSearchParams(
@ -1599,7 +1565,13 @@ export default function ProcessInstanceListTable({
<> <>
{reportColumnForm()} {reportColumnForm()}
{processInstanceReportSaveTag()} {processInstanceReportSaveTag()}
{filterComponent()} <Filters
filterOptions={filterOptions}
showFilterOptions={showFilterOptions}
setShowFilterOptions={setShowFilterOptions}
reportSearchComponent={reportSearchComponent}
filtersEnabled={filtersEnabled}
/>
{resultsTable} {resultsTable}
</> </>
); );

View File

@ -26,6 +26,17 @@ export const underscorizeString = (inputString: string) => {
return slugifyString(inputString).replace(/-/g, '_'); 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) => { export const capitalizeFirstLetter = (string: any) => {
return string.charAt(0).toUpperCase() + string.slice(1); return string.charAt(0).toUpperCase() + string.slice(1);
}; };

View File

@ -1,16 +1,35 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
// @ts-ignore import {
import { Table, Tabs, TabList, Tab } from '@carbon/react'; Table,
import { Link, useParams, useSearchParams } from 'react-router-dom'; 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 PaginationForTable from '../components/PaginationForTable';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import { import {
getPageInfoFromSearchParams, getPageInfoFromSearchParams,
convertSecondsToFormattedDateTime, convertSecondsToFormattedDateTime,
selectKeysFromSearchParams,
} from '../helpers'; } from '../helpers';
import HttpService from '../services/HttpService'; import HttpService from '../services/HttpService';
import { useUriListForPermissions } from '../hooks/UriListForPermissions'; import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { ProcessInstanceLogEntry } from '../interfaces'; import { ProcessInstanceLogEntry } from '../interfaces';
import Filters from '../components/Filters';
type OwnProps = { type OwnProps = {
variant: string; variant: string;
@ -21,14 +40,42 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const [processInstanceLogs, setProcessInstanceLogs] = useState([]); const [processInstanceLogs, setProcessInstanceLogs] = useState([]);
const [pagination, setPagination] = useState(null); 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 { targetUris } = useUriListForPermissions();
const isDetailedView = searchParams.get('detailed') === 'true'; 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}`; let processInstanceShowPageBaseUrl = `/admin/process-instances/for-me/${params.process_model_id}`;
if (variant === 'all') { if (variant === 'all') {
processInstanceShowPageBaseUrl = `/admin/process-instances/${params.process_model_id}`; 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(() => { useEffect(() => {
// Clear out any previous results to avoid a "flicker" effect where columns // Clear out any previous results to avoid a "flicker" effect where columns
// are updated above the incorrect data. // are updated above the incorrect data.
@ -39,11 +86,41 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
setProcessInstanceLogs(result.results); setProcessInstanceLogs(result.results);
setPagination(result.pagination); 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({ HttpService.makeCallToBackend({
path: `${targetUris.processInstanceLogListPath}?per_page=${perPage}&page=${page}&detailed=${isDetailedView}`, path: `${targetUris.processInstanceLogListPath}?${createSearchParams(
pickedSearchParams
)}`,
successCallback: setProcessInstanceLogListFromResult, successCallback: setProcessInstanceLogListFromResult,
}); });
HttpService.makeCallToBackend({
path: `/v1.0/logs/types`,
successCallback: (result: any) => {
setTaskTypes(result.task_types);
setEventTypes(result.event_types);
},
});
}, [ }, [
searchParams, searchParams,
params, params,
@ -85,6 +162,7 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
if (isDetailedView) { if (isDetailedView) {
tableRow.push( tableRow.push(
<> <>
<td>{logEntry.task_definition_identifier}</td>
<td>{logEntry.bpmn_task_type}</td> <td>{logEntry.bpmn_task_type}</td>
<td>{logEntry.event_type}</td> <td>{logEntry.event_type}</td>
<td> <td>
@ -130,13 +208,13 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
<> <>
<th>Id</th> <th>Id</th>
<th>Bpmn Process</th> <th>Bpmn Process</th>
<th>Task Name</th> <th>{taskNameHeader}</th>
</> </>
); );
} else { } else {
tableHeaders.push( tableHeaders.push(
<> <>
<th>Event</th> <th>{taskNameHeader}</th>
<th>Bpmn Process</th> <th>Bpmn Process</th>
</> </>
); );
@ -144,8 +222,9 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
if (isDetailedView) { if (isDetailedView) {
tableHeaders.push( tableHeaders.push(
<> <>
<th>Task Identifier</th>
<th>Task Type</th> <th>Task Type</th>
<th>Event</th> <th>Event Type</th>
<th>User</th> <th>User</th>
</> </>
); );
@ -160,27 +239,126 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
</Table> </Table>
); );
}; };
const selectedTabIndex = isDetailedView ? 1 : 0;
if (pagination) { const resetFilters = () => {
const { page, perPage } = getPageInfoFromSearchParams(searchParams); 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 ( return (
<> <>
<ProcessBreadcrumb <Grid fullWidth className="with-bottom-margin">
hotCrumbs={[ {filterElements}
['Process Groups', '/admin'], </Grid>
{ <Grid fullWidth className="with-bottom-margin">
entityToExplode: params.process_model_id || '', <Column sm={4} md={4} lg={8}>
entityType: 'process-model-id', <ButtonSet>
linkLastItem: true, <Button
}, kind=""
[ className="button-white-background narrow-button"
`Process Instance: ${params.process_instance_id}`, onClick={resetFilters}
`${processInstanceShowPageBaseUrl}/${params.process_instance_id}`, >
], Reset
['Logs'], </Button>
]} </ButtonSet>
/> </Column>
</Grid>
</>
);
};
const tabs = () => {
const selectedTabIndex = isDetailedView ? 1 : 0;
return (
<Tabs selectedIndex={selectedTabIndex}> <Tabs selectedIndex={selectedTabIndex}>
<TabList aria-label="List of tabs"> <TabList aria-label="List of tabs">
<Tab <Tab
@ -205,6 +383,34 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
</Tab> </Tab>
</TabList> </TabList>
</Tabs> </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 /> <br />
<PaginationForTable <PaginationForTable
page={page} page={page}
@ -214,6 +420,4 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
/> />
</> </>
); );
}
return null;
} }

View File

@ -890,7 +890,7 @@ export default function ProcessModelEditDiagram() {
const path = generatePath( const path = generatePath(
'/admin/process-models/:process_model_id/form/:file_name', '/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, file_name: fileName,
} }
); );
@ -902,7 +902,7 @@ export default function ProcessModelEditDiagram() {
const path = generatePath( const path = generatePath(
'/admin/process-models/:process_model_id/files/:file_name', '/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, file_name: file.name,
} }
); );