added ability to filter process instances by process initiator

This commit is contained in:
jasquat 2023-01-04 16:11:52 -05:00
parent ee9e307d21
commit ee650e6039
8 changed files with 168 additions and 19 deletions

View File

@ -0,0 +1,10 @@
#!/usr/bin/env bash
function error_handler() {
>&2 echo "Exited with BAD EXIT CODE '${2}' in ${0} script at line: ${1}."
exit "$2"
}
trap 'error_handler ${LINENO} $?' ERR
set -o errtrace -o errexit -o nounset -o pipefail
grep -E '^ +\/' src/spiffworkflow_backend/api.yml | sort

View File

@ -1414,11 +1414,34 @@ paths:
items: items:
$ref: "#/components/schemas/Task" $ref: "#/components/schemas/Task"
/users/search:
parameters:
- name: username_prefix
in: query
required: true
description: The prefix of the user
schema:
type: string
get:
tags:
- Users
operationId: spiffworkflow_backend.routes.users_controller.user_search
summary: Returns a list of users that the search param
responses:
"200":
description: list of users
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/User"
/user-groups/for-current-user: /user-groups/for-current-user:
get: get:
tags: tags:
- Process Instances - User Groups
operationId: spiffworkflow_backend.routes.process_api_blueprint.user_group_list_for_current_user operationId: spiffworkflow_backend.routes.users_controller.user_group_list_for_current_user
summary: Group identifiers for current logged in user summary: Group identifiers for current logged in user
responses: responses:
"200": "200":

View File

@ -1,6 +1,8 @@
"""User.""" """User."""
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
import jwt import jwt
import marshmallow import marshmallow
from flask import current_app from flask import current_app
@ -16,15 +18,18 @@ class UserNotFoundError(Exception):
"""UserNotFoundError.""" """UserNotFoundError."""
@dataclass
class UserModel(SpiffworkflowBaseDBModel): class UserModel(SpiffworkflowBaseDBModel):
"""UserModel.""" """UserModel."""
__tablename__ = "user" __tablename__ = "user"
__table_args__ = (db.UniqueConstraint("service", "service_id", name="service_key"),) __table_args__ = (db.UniqueConstraint("service", "service_id", name="service_key"),)
id = db.Column(db.Integer, primary_key=True)
username = db.Column( id: int = db.Column(db.Integer, primary_key=True)
username: str = db.Column(
db.String(255), nullable=False, unique=True db.String(255), nullable=False, unique=True
) # should always be a unique value )
service = db.Column( service = db.Column(
db.String(255), nullable=False, unique=False db.String(255), nullable=False, unique=False
) # not 'openid' -- google, aws ) # not 'openid' -- google, aws

View File

@ -71,14 +71,6 @@ def permissions_check(body: Dict[str, Dict[str, list[str]]]) -> flask.wrappers.R
return make_response(jsonify({"results": response_dict}), 200) return make_response(jsonify({"results": response_dict}), 200)
def user_group_list_for_current_user() -> flask.wrappers.Response:
"""User_group_list_for_current_user."""
groups = g.user.groups
# TODO: filter out the default group and have a way to know what is the default group
group_identifiers = [i.identifier for i in groups if i.identifier != "everybody"]
return make_response(jsonify(sorted(group_identifiers)), 200)
def process_list() -> Any: def process_list() -> Any:
"""Returns a list of all known processes. """Returns a list of all known processes.

View File

@ -0,0 +1,26 @@
"""users_controller."""
import flask
from spiffworkflow_backend.models.user import UserModel
from flask import make_response
from flask import jsonify
from flask import g
def user_search(username_prefix: str) -> flask.wrappers.Response:
"""User_search."""
found_users = UserModel.query.filter(UserModel.username.like(f"{username_prefix}%")).all() # type: ignore
response_json = {
"users": found_users,
"username_prefix": username_prefix,
}
return make_response(jsonify(response_json), 200)
def user_group_list_for_current_user() -> flask.wrappers.Response:
"""User_group_list_for_current_user."""
groups = g.user.groups
# TODO: filter out the default group and have a way to know what is the default group
group_identifiers = [i.identifier for i in groups if i.identifier != "everybody"]
return make_response(jsonify(sorted(group_identifiers)), 200)

View File

@ -0,0 +1,41 @@
"""test_users_controller."""
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from spiffworkflow_backend.models.user import UserModel
from flask.testing import FlaskClient
from flask.app import Flask
class TestUsersController(BaseTest):
"""TestUsersController."""
def test_user_search_returns_a_user(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
"""Test_user_search_returns_a_user."""
self.find_or_create_user(username="aa")
self.find_or_create_user(username="ab")
self.find_or_create_user(username="abc")
self.find_or_create_user(username="ac")
self._assert_search_has_count(client, with_super_admin_user, 'aa', 1)
self._assert_search_has_count(client, with_super_admin_user, 'ab', 2)
self._assert_search_has_count(client, with_super_admin_user, 'ac', 1)
self._assert_search_has_count(client, with_super_admin_user, 'ad', 0)
self._assert_search_has_count(client, with_super_admin_user, 'a', 4)
def _assert_search_has_count(self, client: FlaskClient, with_super_admin_user: UserModel, username_prefix: str, expected_count: int) -> None:
"""_assert_search_has_count."""
response = client.get(
f"/v1.0/users/search?username_prefix={username_prefix}",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
assert response.json
assert response.json['users'] is not None
assert response.json['username_prefix'] == username_prefix
assert len(response.json['users']) == expected_count

View File

@ -1,4 +1,4 @@
import { useContext, useEffect, useMemo, useState } from 'react'; import { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { import {
Link, Link,
useNavigate, useNavigate,
@ -60,6 +60,7 @@ import {
ReportColumnForEditing, ReportColumnForEditing,
ReportMetadata, ReportMetadata,
ReportFilter, ReportFilter,
User,
} from '../interfaces'; } from '../interfaces';
import ProcessModelSearch from './ProcessModelSearch'; import ProcessModelSearch from './ProcessModelSearch';
import ProcessInstanceReportSearch from './ProcessInstanceReportSearch'; import ProcessInstanceReportSearch from './ProcessInstanceReportSearch';
@ -134,10 +135,14 @@ export default function ProcessInstanceListTable({
const [errorObject, setErrorObject] = (useContext as any)(ErrorContext); const [errorObject, setErrorObject] = (useContext as any)(ErrorContext);
const processInstancePathPrefix = const processInstanceListPathPrefix =
variant === 'all' variant === 'all'
? '/admin/process-instances/all' ? '/admin/process-instances/all'
: '/admin/process-instances/for-me'; : '/admin/process-instances/for-me';
const processInstanceShowPathPrefix =
variant === 'all'
? '/admin/process-instances'
: '/admin/process-instances/for-me';
const [processStatusAllOptions, setProcessStatusAllOptions] = useState<any[]>( const [processStatusAllOptions, setProcessStatusAllOptions] = useState<any[]>(
[] []
@ -164,6 +169,12 @@ export default function ProcessInstanceListTable({
useState<ReportColumnForEditing | null>(null); useState<ReportColumnForEditing | null>(null);
const [reportColumnFormMode, setReportColumnFormMode] = useState<string>(''); const [reportColumnFormMode, setReportColumnFormMode] = useState<string>('');
const [processInstanceInitiatorOptions, setProcessInstanceInitiatorOptions] =
useState<string[]>([]);
const [processInitiatorSelection, setProcessInitiatorSelection] =
useState<User | null>(null);
const lastRequestedInitatorSearchTerm = useRef<string>();
const dateParametersToAlwaysFilterBy: dateParameters = useMemo(() => { const dateParametersToAlwaysFilterBy: dateParameters = useMemo(() => {
return { return {
start_from: [setStartFromDate, setStartFromTime], start_from: [setStartFromDate, setStartFromTime],
@ -529,7 +540,7 @@ export default function ProcessInstanceListTable({
setErrorObject(null); setErrorObject(null);
setProcessInstanceReportJustSaved(null); setProcessInstanceReportJustSaved(null);
navigate(`${processInstancePathPrefix}?${queryParamString}`); navigate(`${processInstanceListPathPrefix}?${queryParamString}`);
}; };
const dateComponent = ( const dateComponent = (
@ -628,7 +639,7 @@ export default function ProcessInstanceListTable({
setErrorObject(null); setErrorObject(null);
setProcessInstanceReportJustSaved(mode || null); setProcessInstanceReportJustSaved(mode || null);
navigate(`${processInstancePathPrefix}${queryParamString}`); navigate(`${processInstanceListPathPrefix}${queryParamString}`);
}; };
const reportColumns = () => { const reportColumns = () => {
@ -976,6 +987,22 @@ export default function ProcessInstanceListTable({
return null; return null;
}; };
const handleProcessInstanceInitiatorSearchResult = (result: any) => {
if (lastRequestedInitatorSearchTerm.current === result.username_prefix) {
setProcessInstanceInitiatorOptions(result.users);
}
};
const searchForProcessInitiator = (inputText: string) => {
if (inputText) {
lastRequestedInitatorSearchTerm.current = inputText;
HttpService.makeCallToBackend({
path: `/users/search?username_prefix=${inputText}`,
successCallback: handleProcessInstanceInitiatorSearchResult,
});
}
};
const filterOptions = () => { const filterOptions = () => {
if (!showFilterOptions) { if (!showFilterOptions) {
return null; return null;
@ -1008,7 +1035,27 @@ export default function ProcessInstanceListTable({
selectedItem={processModelSelection} selectedItem={processModelSelection}
/> />
</Column> </Column>
<Column md={8}>{processStatusSearch()}</Column> <Column md={4}>
<ComboBox
onInputChange={searchForProcessInitiator}
onChange={(event: any) => {
setProcessInitiatorSelection(event.selectedItem);
}}
id="process-instance-initiator-search"
data-qa="process-instance-initiator-search"
items={processInstanceInitiatorOptions}
itemToString={(processInstanceInitatorOption: User) => {
if (processInstanceInitatorOption) {
return processInstanceInitatorOption.username;
}
return null;
}}
placeholder="Process Initiator"
titleText="PROC"
selectedItem={processInitiatorSelection}
/>
</Column>
<Column md={4}>{processStatusSearch()}</Column>
</Grid> </Grid>
<Grid fullWidth className="with-bottom-margin"> <Grid fullWidth className="with-bottom-margin">
<Column md={4}> <Column md={4}>
@ -1114,7 +1161,7 @@ export default function ProcessInstanceListTable({
return ( return (
<Link <Link
data-qa="process-instance-show-link" data-qa="process-instance-show-link"
to={`${processInstancePathPrefix}/${modifiedProcessModelId}/${id}`} to={`${processInstanceShowPathPrefix}/${modifiedProcessModelId}/${id}`}
title={`View process instance ${id}`} title={`View process instance ${id}`}
> >
<span data-qa="paginated-entity-id">{id}</span> <span data-qa="paginated-entity-id">{id}</span>

View File

@ -1,3 +1,8 @@
export interface User {
id: number;
username: string;
}
export interface Secret { export interface Secret {
id: number; id: number;
key: string; key: string;