Feature/metadata filtering (#418)

* added ability to filter metadata by is and contains w/ burnettk

* added the empty options for metadata filtering

* remove the filter when removing the corresponding column on the frontend

---------

Co-authored-by: jasquat <jasquat@users.noreply.github.com>
Co-authored-by: burnettk <burnettk@users.noreply.github.com>
This commit is contained in:
jasquat 2023-08-02 04:31:38 -04:00 committed by GitHub
parent 5cd7cd32f9
commit 5626e15d35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 152 additions and 35 deletions

View File

@ -549,7 +549,22 @@ class ProcessInstanceReportService:
]
if filter_for_column:
isouter = False
conditions.append(instance_metadata_alias.value == filter_for_column["field_value"])
if "operator" not in filter_for_column or filter_for_column["operator"] == "equals":
conditions.append(instance_metadata_alias.value == filter_for_column["field_value"])
elif filter_for_column["operator"] == "not equals":
conditions.append(instance_metadata_alias.value != filter_for_column["field_value"])
elif filter_for_column["operator"] == "contains":
conditions.append(instance_metadata_alias.value.like(f"%{filter_for_column['field_value']}%"))
elif filter_for_column["operator"] == "is_empty":
# we still need to return results if the metadata value is null so make sure it's outer join
isouter = True
conditions.append(
or_(instance_metadata_alias.value.is_(None), instance_metadata_alias.value == "")
)
elif filter_for_column["operator"] == "is_not_empty":
conditions.append(
or_(instance_metadata_alias.value.is_not(None), instance_metadata_alias.value != "")
)
process_instance_query = process_instance_query.join(
instance_metadata_alias, and_(*conditions), isouter=isouter
).add_columns(func.max(instance_metadata_alias.value).label(column["accessor"]))

View File

@ -41,6 +41,7 @@ import {
convertSecondsToFormattedDateString,
convertSecondsToFormattedDateTime,
convertSecondsToFormattedTimeHoursMinutes,
getKeyByValue,
getPageInfoFromSearchParams,
modifyProcessIdentifierForPathParam,
refreshAtInterval,
@ -67,6 +68,7 @@ import {
User,
ErrorForDisplay,
PermissionsToCheck,
FilterOperatorMapping,
} from '../interfaces';
import ProcessModelSearch from './ProcessModelSearch';
import ProcessInstanceReportSearch from './ProcessInstanceReportSearch';
@ -164,6 +166,14 @@ export default function ProcessInstanceListTable({
const preferredUsername = UserService.getPreferredUsername();
const userEmail = UserService.getUserEmail();
const filterOperatorMappings: FilterOperatorMapping = {
Is: { id: 'equals', requires_value: true },
'Is Not': { id: 'not equals', requires_value: true },
Contains: { id: 'contains', requires_value: true },
'Is Empty': { id: 'is_empty', requires_value: false },
'Is Not Empty': { id: 'is_not_empty', requires_value: false },
};
const processInstanceListPathPrefix =
variant === 'all'
? '/admin/process-instances/all'
@ -361,6 +371,27 @@ export default function ProcessInstanceListTable({
setProcessInstanceReportSelection(processInstanceReport);
}
}
if (additionalReportFilters) {
additionalReportFilters.forEach((arf: ReportFilter) => {
if (!reportMetadataBodyToUse.filter_by.includes(arf)) {
reportMetadataBodyToUse.filter_by.push(arf);
}
});
}
// If the showActionColumn is set to true, we need to include the with_oldest_open_task in the query params
if (
showActionsColumn &&
!reportMetadataBodyToUse.filter_by.some(
(rf: ReportFilter) => rf.field_name === 'with_oldest_open_task'
)
) {
const withOldestReportFilter = {
field_name: 'with_oldest_open_task',
field_value: true,
};
reportMetadataBodyToUse.filter_by.push(withOldestReportFilter);
}
// a bit hacky, clear out all filters before setting them from report metadata
// to ensure old filters are cleared out.
@ -417,6 +448,13 @@ export default function ProcessInstanceListTable({
setShowFilterOptions(true);
}
if (filtersEnabled) {
HttpService.makeCallToBackend({
path: `/user-groups/for-current-user`,
successCallback: setUserGroups,
});
}
// eslint-disable-next-line prefer-const
let { page, perPage } = getPageInfoFromSearchParams(
searchParams,
@ -428,30 +466,8 @@ export default function ProcessInstanceListTable({
// eslint-disable-next-line prefer-destructuring
perPage = perPageOptions[1];
}
const queryParamString = `per_page=${perPage}&page=${page}`;
if (additionalReportFilters) {
additionalReportFilters.forEach((arf: ReportFilter) => {
if (!reportMetadataBodyToUse.filter_by.includes(arf)) {
reportMetadataBodyToUse.filter_by.push(arf);
}
});
}
// If the showActionColumn is set to true, we need to include the with_oldest_open_task in the query params
if (showActionsColumn) {
reportMetadataBodyToUse.filter_by.push({
field_name: 'with_oldest_open_task',
field_value: true,
});
}
if (filtersEnabled) {
HttpService.makeCallToBackend({
path: `/user-groups/for-current-user`,
successCallback: setUserGroups,
});
}
HttpService.makeCallToBackend({
path: `${processInstanceApiSearchPath}?${queryParamString}`,
successCallback: setProcessInstancesFromResult,
@ -952,6 +968,13 @@ export default function ProcessInstanceListTable({
(rc: ReportColumn) => rc.accessor !== reportColumn.accessor
);
Object.assign(reportMetadataCopy, { columns: newColumns });
const newFilters = reportMetadataCopy.filter_by.filter(
(rf: ReportFilter) => rf.field_name !== reportColumn.accessor
);
Object.assign(reportMetadataCopy, {
columns: newColumns,
filter_by: newFilters,
});
setReportMetadata(reportMetadataCopy);
setRequiresRefilter(true);
}
@ -963,6 +986,18 @@ export default function ProcessInstanceListTable({
setReportColumnToOperateOn(null);
};
const getFilterOperatorFromReportColumn = (
reportColumnForEditing: ReportColumnForEditing
) => {
if (reportColumnForEditing.filter_operator) {
// eslint-disable-next-line prefer-destructuring
return Object.entries(filterOperatorMappings).filter(([_key, value]) => {
return value.id === reportColumnForEditing.filter_operator;
})[0][1];
}
return null;
};
const getNewFiltersFromReportForEditing = (
reportColumnForEditing: ReportColumnForEditing
) => {
@ -980,14 +1015,23 @@ export default function ProcessInstanceListTable({
const existingReportFilter = getFilterByFromReportMetadata(
reportColumnForEditing.accessor
);
const filterOperator = getFilterOperatorFromReportColumn(
reportColumnForEditing
);
if (existingReportFilter) {
const existingReportFilterIndex =
reportMetadataCopy.filter_by.indexOf(existingReportFilter);
if (reportColumnForEditing.filter_field_value) {
if (filterOperator && !filterOperator.requires_value) {
newReportFilter.field_value = '';
newReportFilters[existingReportFilterIndex] = newReportFilter;
} else if (reportColumnForEditing.filter_field_value) {
newReportFilters[existingReportFilterIndex] = newReportFilter;
} else {
newReportFilters.splice(existingReportFilterIndex, 1);
}
} else if (filterOperator && !filterOperator.requires_value) {
newReportFilter.field_value = '';
newReportFilters = newReportFilters.concat([newReportFilter]);
} else if (reportColumnForEditing.filter_field_value) {
newReportFilters = newReportFilters.concat([newReportFilter]);
}
@ -1073,6 +1117,19 @@ export default function ProcessInstanceListTable({
}
};
const setReportColumnConditionOperator = (selectedItem: string) => {
if (reportColumnToOperateOn) {
const reportColumnToOperateOnCopy = {
...reportColumnToOperateOn,
};
const filterOperator = filterOperatorMappings[selectedItem];
reportColumnToOperateOnCopy.filter_operator = filterOperator.id;
setReportColumnToOperateOn(reportColumnToOperateOnCopy);
setRequiresRefilter(true);
}
};
// eslint-disable-next-line sonarjs/cognitive-complexity
const reportColumnForm = () => {
if (reportColumnFormMode === '') {
return null;
@ -1120,18 +1177,40 @@ export default function ProcessInstanceListTable({
]);
if (reportColumnToOperateOn && reportColumnToOperateOn.filterable) {
formElements.push(
<TextInput
id="report-column-condition-value"
name="report-column-condition-value"
labelText="Condition Value"
value={
reportColumnToOperateOn
? reportColumnToOperateOn.filter_field_value
: ''
}
onChange={setReportColumnConditionValue}
<Dropdown
titleText="Operator"
id="report-column-condition-operator"
items={Object.keys(filterOperatorMappings)}
selectedItem={getKeyByValue(
filterOperatorMappings,
reportColumnToOperateOn.filter_operator,
'id'
)}
onChange={(value: any) => {
setReportColumnConditionOperator(value.selectedItem);
setRequiresRefilter(true);
}}
/>
);
const filterOperator = getFilterOperatorFromReportColumn(
reportColumnToOperateOn
);
if (filterOperator && filterOperator.requires_value) {
formElements.push(
<TextInput
id="report-column-condition-value"
name="report-column-condition-value"
labelText="Condition Value"
value={
reportColumnToOperateOn
? reportColumnToOperateOn.filter_field_value
: ''
}
onChange={setReportColumnConditionValue}
/>
);
}
}
formElements.push(
<div className="vertical-spacer-to-allow-combo-box-to-expand-in-modal" />
@ -1273,6 +1352,7 @@ export default function ProcessInstanceListTable({
labelText="Include oldest open task information"
id="with-oldest-open-task-checkbox"
checked={withOldestOpenTask}
disabled={showActionsColumn}
onChange={(value: any) => {
setWithOldestOpenTask(value.target.checked);
setRequiresRefilter(true);

View File

@ -37,6 +37,19 @@ export const underscorizeString = (inputString: string) => {
return slugifyString(inputString).replace(/-/g, '_');
};
export const getKeyByValue = (
object: any,
value: string,
onAttribute?: string
) => {
return Object.keys(object).find((key) => {
if (onAttribute) {
return object[key][onAttribute] === value;
}
return object[key] === value;
});
};
export const recursivelyChangeNullAndUndefined = (obj: any, newValue: any) => {
if (obj === null || obj === undefined) {
return newValue;

View File

@ -128,6 +128,15 @@ export interface ProcessReference {
export type ObjectWithStringKeysAndValues = { [key: string]: string };
export interface FilterOperator {
id: string;
requires_value: boolean;
}
export interface FilterOperatorMapping {
[key: string]: FilterOperator;
}
export interface ProcessFile {
content_type: string;
last_modified: string;