From 04864692b6cc003c3c5574d34f0ec99a3a9e9e8b Mon Sep 17 00:00:00 2001 From: jasquat Date: Tue, 25 Apr 2023 15:30:26 -0400 Subject: [PATCH] added typeahead search for bpmn name and identifier in process instance event list w/ burnettk --- .../src/spiffworkflow_backend/api.yml | 35 +++-- .../routes/connector_proxy_controller.py | 6 +- .../process_instance_events_controller.py | 45 ++++++- .../services/authorization_service.py | 3 +- .../integration/test_logging_service.py | 2 +- .../scripts/test_get_all_permissions.py | 5 + .../unit/test_authorization_service.py | 8 +- .../components/ProcessInstanceListTable.tsx | 11 +- .../src/routes/ProcessInstanceLogList.tsx | 121 +++++++++--------- .../src/routes/TaskShow.tsx | 2 +- 10 files changed, 156 insertions(+), 82 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index 807b0c14a..c37bd3801 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -1999,10 +1999,10 @@ paths: description: The number of items to show per page. Defaults to page 10. schema: type: integer - - name: detailed + - name: events in: query required: false - description: Show the detailed view, which includes all log entries + description: Show the events view, which includes all log entries schema: type: boolean - name: bpmn_name @@ -2042,11 +2042,30 @@ paths: schema: $ref: "#/components/schemas/ProcessInstanceLog" - /logs/types: + /logs/typeahead-filter-values/{modified_process_model_identifier}/{process_instance_id}: + parameters: + - name: process_instance_id + in: path + required: true + description: the id of the process instance + schema: + type: integer + - name: modified_process_model_identifier + in: path + required: true + description: The process_model_id, modified to replace slashes (/) + schema: + type: string + - name: task_type + in: query + required: false + description: The task type of the typehead filter values to get. + schema: + type: string get: tags: - Process Instance Events - operationId: spiffworkflow_backend.routes.process_instance_events_controller.types + operationId: spiffworkflow_backend.routes.process_instance_events_controller.typeahead_filter_values summary: returns a list of task types and event typs. useful for building log queries. responses: "200": @@ -2079,7 +2098,7 @@ paths: get: tags: - Process Instance Events - operationId: spiffworkflow_backend.routes.process_instance_events_controller.error_details + operationId: spiffworkflow_backend.routes.process_instance_events_controller.error_detail_show summary: returns the error details for a given process instance event. responses: "200": @@ -2206,12 +2225,12 @@ paths: schema: $ref: "#/components/schemas/Secret" - /connector-proxy/type-ahead/{category}: + /connector-proxy/typeahead/{category}: parameters: - name: category in: path required: true - description: The category for the type-ahead search + description: The category for the typeahead search schema: type: string - name: prefix @@ -2227,7 +2246,7 @@ paths: schema: type: integer get: - operationId: spiffworkflow_backend.routes.connector_proxy_controller.type_ahead + operationId: spiffworkflow_backend.routes.connector_proxy_controller.typeahead summary: Return type ahead search results tags: - Type Ahead diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/connector_proxy_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/connector_proxy_controller.py index 45c0bd28e..dca3cbe75 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/connector_proxy_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/connector_proxy_controller.py @@ -6,13 +6,13 @@ from flask import current_app from flask.wrappers import Response -def connector_proxy_type_ahead_url() -> Any: +def connector_proxy_typeahead_url() -> Any: """Returns the connector proxy type ahead url.""" return current_app.config["SPIFFWORKFLOW_BACKEND_CONNECTOR_PROXY_TYPE_AHEAD_URL"] -def type_ahead(category: str, prefix: str, limit: int) -> flask.wrappers.Response: - url = f"{connector_proxy_type_ahead_url()}/v1/type-ahead/{category}?prefix={prefix}&limit={limit}" +def typeahead(category: str, prefix: str, limit: int) -> flask.wrappers.Response: + url = f"{connector_proxy_typeahead_url()}/v1/type-ahead/{category}?prefix={prefix}&limit={limit}" proxy_response = requests.get(url) status = proxy_response.status_code diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instance_events_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instance_events_controller.py index 5831632b6..7df2d6a8f 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instance_events_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_instance_events_controller.py @@ -1,4 +1,5 @@ from typing import Optional +from typing import Set import flask.wrappers from flask import jsonify @@ -23,7 +24,7 @@ def log_list( process_instance_id: int, page: int = 1, per_page: int = 100, - detailed: bool = False, + events: bool = False, bpmn_name: Optional[str] = None, bpmn_identifier: Optional[str] = None, task_type: Optional[str] = None, @@ -41,7 +42,7 @@ def log_list( BpmnProcessDefinitionModel.id == TaskDefinitionModel.bpmn_process_definition_id, ) ) - if not detailed: + if not events: log_query = log_query.filter( and_( TaskModel.state.in_(["COMPLETED"]), # type: ignore @@ -87,14 +88,48 @@ def log_list( return make_response(jsonify(response_json), 200) -def types() -> flask.wrappers.Response: +def typeahead_filter_values( + modified_process_model_identifier: str, + process_instance_id: int, + task_type: Optional[str] = None, +) -> flask.wrappers.Response: + process_instance = _find_process_instance_by_id_or_raise(process_instance_id) 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) + task_definition_query = ( + db.session.query(TaskDefinitionModel.bpmn_identifier, TaskDefinitionModel.bpmn_name) + .distinct(TaskDefinitionModel.bpmn_identifier, TaskDefinitionModel.bpmn_name) # type: ignore + .join(TaskModel, TaskModel.task_definition_id == TaskDefinitionModel.id) + .join(ProcessInstanceEventModel, ProcessInstanceEventModel.task_guid == TaskModel.guid) + .filter(TaskModel.process_instance_id == process_instance.id) + ) + if task_type is not None: + task_definition_query = task_definition_query.filter(TaskDefinitionModel.typename == task_type) + task_definitions = task_definition_query.all() + + task_bpmn_names: Set[str] = set() + task_bpmn_identifiers: Set[str] = set() + for task_definition in task_definitions: + # not checking for None so we also exclude empty strings + if task_definition.bpmn_name: + task_bpmn_names.add(task_definition.bpmn_name) + task_bpmn_identifiers.add(task_definition.bpmn_identifier) + + return make_response( + jsonify( + { + "task_types": task_types, + "event_types": event_types, + "task_bpmn_names": list(task_bpmn_names), + "task_bpmn_identifiers": list(task_bpmn_identifiers), + } + ), + 200, + ) -def error_details( +def error_detail_show( modified_process_model_identifier: str, process_instance_id: int, process_instance_event_id: int, diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py index 0b6561ac3..fbbf367ea 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py @@ -75,6 +75,7 @@ class PermissionToAssign: PATH_SEGMENTS_FOR_PERMISSION_ALL = [ {"path": "/event-error-details", "relevant_permissions": ["read"]}, {"path": "/logs", "relevant_permissions": ["read"]}, + {"path": "/logs/typeahead-filter-values", "relevant_permissions": ["read"]}, { "path": "/process-instances", "relevant_permissions": ["create", "read", "delete"], @@ -546,6 +547,7 @@ class AuthorizationService: for target_uri in [ f"/process-instances/for-me/{process_related_path_segment}", f"/logs/{process_related_path_segment}", + f"/logs/typeahead-filter-values/{process_related_path_segment}", f"/process-data-file-download/{process_related_path_segment}", f"/event-error-details/{process_related_path_segment}", ]: @@ -572,7 +574,6 @@ 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/*") diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_logging_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_logging_service.py index 7890e156a..6e45386b4 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_logging_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_logging_service.py @@ -53,7 +53,7 @@ class TestLoggingService(BaseTest): headers = self.logged_in_headers(with_super_admin_user) log_response = client.get( - f"/v1.0/logs/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance.id}?detailed=true", + f"/v1.0/logs/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance.id}?events=true", headers=headers, ) assert log_response.status_code == 200 diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_all_permissions.py b/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_all_permissions.py index c96122e57..d12d50723 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_all_permissions.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_get_all_permissions.py @@ -46,6 +46,11 @@ class TestGetAllPermissions(BaseTest): "uri": "/logs/hey:group:*", "permissions": ["read"], }, + { + "group_identifier": "my_test_group", + "uri": "/logs/typeahead-filter-values/hey:group:*", + "permissions": ["read"], + }, { "group_identifier": "my_test_group", "uri": "/process-instances/hey:group:*", diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py index 2b1f7051f..2dd4de8d3 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py @@ -126,6 +126,7 @@ class TestAuthorizationService(BaseTest): [ ("/event-error-details/some-process-group:some-process-model:*", "read"), ("/logs/some-process-group:some-process-model:*", "read"), + ("/logs/typeahead-filter-values/some-process-group:some-process-model:*", "read"), ("/process-data/some-process-group:some-process-model:*", "read"), ( "/process-data-file-download/some-process-group:some-process-model:*", @@ -179,6 +180,10 @@ class TestAuthorizationService(BaseTest): "/logs/some-process-group:some-process-model:*", "read", ), + ( + "/logs/typeahead-filter-values/some-process-group:some-process-model:*", + "read", + ), ( "/process-data-file-download/some-process-group:some-process-model:*", "read", @@ -210,6 +215,7 @@ class TestAuthorizationService(BaseTest): "/process-data-file-download/some-process-group:some-process-model/*", "read", ), + ("/logs/typeahead-filter-values/some-process-group:some-process-model/*", "read"), ("/process-data/some-process-group:some-process-model/*", "read"), ( "/process-instance-suspend/some-process-group:some-process-model/*", @@ -258,6 +264,7 @@ class TestAuthorizationService(BaseTest): "/logs/some-process-group:some-process-model/*", "read", ), + ("/logs/typeahead-filter-values/some-process-group:some-process-model/*", "read"), ( "/process-data-file-download/some-process-group:some-process-model/*", "read", @@ -282,7 +289,6 @@ 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"), diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index 2aaf2039d..ccd620756 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -24,6 +24,7 @@ import { FormLabel, // @ts-ignore } from '@carbon/react'; +import { useDebouncedCallback } from 'use-debounce'; import { PROCESS_STATUSES, DATE_FORMAT_CARBON, @@ -247,6 +248,14 @@ export default function ProcessInstanceListTable({ } }; + const addDebouncedSearchProcessInitiator = useDebouncedCallback( + (value: string) => { + searchForProcessInitiator(value); + }, + // delay in ms + 250 + ); + const parametersToGetFromSearchParams = useMemo(() => { const figureOutProcessInitiator = (processInitiatorSearchText: string) => { searchForProcessInitiator(processInitiatorSearchText); @@ -1179,7 +1188,7 @@ export default function ProcessInstanceListTable({ if (hasAccess) { return ( { setProcessInitiatorSelection(event.selectedItem); setRequiresRefilter(true); diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceLogList.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceLogList.tsx index 64910fd47..efb5bd9fe 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInstanceLogList.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInstanceLogList.tsx @@ -9,7 +9,6 @@ import { Column, ButtonSet, Button, - TextInput, ComboBox, Modal, Loading, @@ -21,7 +20,6 @@ import { useParams, useSearchParams, } from 'react-router-dom'; -import { useDebouncedCallback } from 'use-debounce'; import PaginationForTable from '../components/PaginationForTable'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; import { @@ -54,11 +52,10 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) { const [processInstanceLogs, setProcessInstanceLogs] = useState([]); const [pagination, setPagination] = useState(null); - const [taskName, setTaskName] = useState(''); - const [taskIdentifier, setTaskIdentifier] = useState(''); - const [taskTypes, setTaskTypes] = useState([]); const [eventTypes, setEventTypes] = useState([]); + const [taskBpmnNames, setTaskBpmnNames] = useState([]); + const [taskBpmnIdentifiers, setTaskBpmnIdentifiers] = useState([]); const [eventForModal, setEventForModal] = useState(null); @@ -84,8 +81,8 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) { if (variant === 'all') { processInstanceShowPageBaseUrl = `/admin/process-instances/${params.process_model_id}`; } - const isDetailedView = searchParams.get('detailed') === 'true'; - const taskNameHeader = isDetailedView ? 'Task Name' : 'Milestone'; + const isEventsView = searchParams.get('events') === 'true'; + const taskNameHeader = isEventsView ? 'Task Name' : 'Milestone'; const updateSearchParams = (value: string, key: string) => { if (value) { @@ -96,14 +93,6 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) { 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. @@ -116,7 +105,7 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) { }; const searchParamsToInclude = [ - 'detailed', + 'events', 'page', 'per_page', 'bpmn_name', @@ -129,31 +118,31 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) { searchParamsToInclude ); - if ('bpmn_name' in pickedSearchParams) { - setTaskName(pickedSearchParams.bpmn_name); - } - if ('bpmn_identifier' in pickedSearchParams) { - setTaskIdentifier(pickedSearchParams.bpmn_identifier); - } - HttpService.makeCallToBackend({ path: `${targetUris.processInstanceLogListPath}?${createSearchParams( pickedSearchParams )}`, successCallback: setProcessInstanceLogListFromResult, }); + + let typeaheadQueryParamString = ''; + if (!isEventsView) { + typeaheadQueryParamString = '?task_type=IntermediateThrowEvent'; + } HttpService.makeCallToBackend({ - path: `/v1.0/logs/types`, + path: `/v1.0/logs/typeahead-filter-values/${params.process_model_id}/${params.process_instance_id}${typeaheadQueryParamString}`, successCallback: (result: any) => { setTaskTypes(result.task_types); setEventTypes(result.event_types); + setTaskBpmnNames(result.task_bpmn_names); + setTaskBpmnIdentifiers(result.task_bpmn_identifiers); }, }); }, [ searchParams, params, targetUris.processInstanceLogListPath, - isDetailedView, + isEventsView, ]); const handleErrorEventModalClose = () => { @@ -251,20 +240,14 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) { const getTableRow = (logEntry: ProcessInstanceLogEntry) => { const tableRow = []; - const taskNameCell = ( - - {logEntry.task_definition_name || - (logEntry.bpmn_task_type === 'StartEvent' ? 'Process Started' : '') || - (logEntry.bpmn_task_type === 'EndEvent' ? 'Process Ended' : '')} - - ); + const taskNameCell = {logEntry.task_definition_name}; const bpmnProcessCell = ( {logEntry.bpmn_process_definition_name || logEntry.bpmn_process_definition_identifier} ); - if (isDetailedView) { + if (isEventsView) { tableRow.push( <> {logEntry.id} @@ -280,7 +263,7 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) { ); } - if (isDetailedView) { + if (isEventsView) { tableRow.push( <> {logEntry.task_definition_identifier} @@ -324,7 +307,7 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) { ); const tableHeaders = []; - if (isDetailedView) { + if (isEventsView) { tableHeaders.push( <> Id @@ -340,7 +323,7 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) { ); } - if (isDetailedView) { + if (isEventsView) { tableHeaders.push( <> Task Identifier @@ -362,13 +345,13 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) { }; const resetFilters = () => { - setTaskIdentifier(''); - setTaskName(''); - ['bpmn_name', 'bpmn_identifier', 'task_type', 'event_type'].forEach( (value: string) => searchParams.delete(value) ); + }; + const resetFiltersAndRun = () => { + resetFilters(); setSearchParams(searchParams); }; @@ -391,34 +374,48 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) { } const filterElements = []; + let taskNameFilterPlaceholder = 'Choose a milestone'; + if (isEventsView) { + taskNameFilterPlaceholder = 'Choose a task bpmn name'; + } filterElements.push( - { - const newValue = event.target.value; - setTaskName(newValue); - addDebouncedSearchParams(newValue, 'bpmn_name'); + { + updateSearchParams(value.selectedItem, 'bpmn_name'); }} + id="task-name-filter" + data-qa="task-type-select" + items={taskBpmnNames} + itemToString={(value: string) => { + return value; + }} + shouldFilterItem={shouldFilterStringItem} + placeholder={taskNameFilterPlaceholder} + titleText={taskNameHeader} + selectedItem={searchParams.get('bpmn_name')} /> ); - if (isDetailedView) { + if (isEventsView) { filterElements.push( <> - { - const newValue = event.target.value; - setTaskIdentifier(newValue); - addDebouncedSearchParams(newValue, 'bpmn_identifier'); + { + updateSearchParams(value.selectedItem, 'bpmn_identifier'); }} + id="task-identifier-filter" + data-qa="task-type-select" + items={taskBpmnIdentifiers} + itemToString={(value: string) => { + return value; + }} + shouldFilterItem={shouldFilterStringItem} + placeholder="Choose a task bpmn identifier" + titleText="Task Identifier" + selectedItem={searchParams.get('bpmn_identifier')} /> @@ -470,7 +467,7 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) { @@ -491,7 +488,7 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) { }; const tabs = () => { - const selectedTabIndex = isDetailedView ? 1 : 0; + const selectedTabIndex = isEventsView ? 1 : 0; return ( @@ -499,7 +496,8 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) { title="Only show a subset of the logs, and show fewer columns" data-qa="process-instance-log-simple" onClick={() => { - searchParams.set('detailed', 'false'); + resetFilters(); + searchParams.set('events', 'false'); setSearchParams(searchParams); }} > @@ -507,9 +505,10 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) { { - searchParams.set('detailed', 'true'); + resetFilters(); + searchParams.set('events', 'true'); setSearchParams(searchParams); }} > diff --git a/spiffworkflow-frontend/src/routes/TaskShow.tsx b/spiffworkflow-frontend/src/routes/TaskShow.tsx index 7d6bbe7f4..6468f6554 100644 --- a/spiffworkflow-frontend/src/routes/TaskShow.tsx +++ b/spiffworkflow-frontend/src/routes/TaskShow.tsx @@ -33,7 +33,7 @@ function TypeAheadWidget({ options: any; }) { const pathForCategory = (inputText: string) => { - return `/connector-proxy/type-ahead/${category}?prefix=${inputText}&limit=100`; + return `/connector-proxy/typeahead/${category}?prefix=${inputText}&limit=100`; }; const lastSearchTerm = useRef('');