added typeahead search for bpmn name and identifier in process instance event list w/ burnettk

This commit is contained in:
jasquat 2023-04-25 15:30:26 -04:00
parent 3a3d5a86fc
commit 9aa9fe913b
No known key found for this signature in database
10 changed files with 156 additions and 82 deletions

View File

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

View File

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

View File

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

View File

@ -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/*")

View File

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

View File

@ -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:*",

View File

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

View File

@ -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 (
<ComboBox
onInputChange={searchForProcessInitiator}
onInputChange={addDebouncedSearchProcessInitiator}
onChange={(event: any) => {
setProcessInitiatorSelection(event.selectedItem);
setRequiresRefilter(true);

View File

@ -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<string>('');
const [taskIdentifier, setTaskIdentifier] = useState<string>('');
const [taskTypes, setTaskTypes] = useState<string[]>([]);
const [eventTypes, setEventTypes] = useState<string[]>([]);
const [taskBpmnNames, setTaskBpmnNames] = useState<string[]>([]);
const [taskBpmnIdentifiers, setTaskBpmnIdentifiers] = useState<string[]>([]);
const [eventForModal, setEventForModal] =
useState<ProcessInstanceLogEntry | null>(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 = (
<td>
{logEntry.task_definition_name ||
(logEntry.bpmn_task_type === 'StartEvent' ? 'Process Started' : '') ||
(logEntry.bpmn_task_type === 'EndEvent' ? 'Process Ended' : '')}
</td>
);
const taskNameCell = <td>{logEntry.task_definition_name}</td>;
const bpmnProcessCell = (
<td>
{logEntry.bpmn_process_definition_name ||
logEntry.bpmn_process_definition_identifier}
</td>
);
if (isDetailedView) {
if (isEventsView) {
tableRow.push(
<>
<td data-qa="paginated-entity-id">{logEntry.id}</td>
@ -280,7 +263,7 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
</>
);
}
if (isDetailedView) {
if (isEventsView) {
tableRow.push(
<>
<td>{logEntry.task_definition_identifier}</td>
@ -324,7 +307,7 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
);
const tableHeaders = [];
if (isDetailedView) {
if (isEventsView) {
tableHeaders.push(
<>
<th>Id</th>
@ -340,7 +323,7 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
</>
);
}
if (isDetailedView) {
if (isEventsView) {
tableHeaders.push(
<>
<th>Task Identifier</th>
@ -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(
<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');
<ComboBox
onChange={(value: any) => {
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')}
/>
</Column>
);
if (isDetailedView) {
if (isEventsView) {
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');
<ComboBox
onChange={(value: any) => {
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')}
/>
</Column>
<Column md={4}>
@ -470,7 +467,7 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
<Button
kind=""
className="button-white-background narrow-button"
onClick={resetFilters}
onClick={resetFiltersAndRun}
>
Reset
</Button>
@ -491,7 +488,7 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
};
const tabs = () => {
const selectedTabIndex = isDetailedView ? 1 : 0;
const selectedTabIndex = isEventsView ? 1 : 0;
return (
<Tabs selectedIndex={selectedTabIndex}>
<TabList aria-label="List of tabs">
@ -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) {
</Tab>
<Tab
title="Show all logs for this process instance, and show extra columns that may be useful for debugging"
data-qa="process-instance-log-detailed"
data-qa="process-instance-log-events"
onClick={() => {
searchParams.set('detailed', 'true');
resetFilters();
searchParams.set('events', 'true');
setSearchParams(searchParams);
}}
>

View File

@ -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('');