some more log stuff using debounce w/ burnettk

This commit is contained in:
jasquat 2023-04-17 11:08:00 -04:00
parent 1768618f27
commit 2caa42700c
8 changed files with 1243 additions and 1085 deletions

View File

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

View File

@ -0,0 +1,58 @@
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)

View File

@ -224,63 +224,6 @@ def process_instance_resume(
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(
process_model_identifier: Optional[str] = None,
page: int = 1,

View File

@ -54,6 +54,7 @@
"react-bootstrap": "^2.5.0",
"react-bootstrap-typeahead": "^6.0.0",
"react-datepicker": "^4.8.0",
"react-debounce-input": "^3.3.0",
"react-devtools": "^4.27.1",
"react-dom": "^18.2.0",
"react-icons": "^4.4.0",
@ -64,6 +65,7 @@
"serve": "^14.0.0",
"timepicker": "^1.13.18",
"typescript": "^4.7.4",
"use-debounce": "^9.0.4",
"web-vitals": "^3.0.2"
},
"devDependencies": {
@ -24152,6 +24154,18 @@
"react-dom": "^16.9.0 || ^17 || ^18"
}
},
"node_modules/react-debounce-input": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/react-debounce-input/-/react-debounce-input-3.3.0.tgz",
"integrity": "sha512-VEqkvs8JvY/IIZvh71Z0TC+mdbxERvYF33RcebnodlsUZ8RSgyKe2VWaHXv4+/8aoOgXLxWrdsYs2hDhcwbUgA==",
"dependencies": {
"lodash.debounce": "^4",
"prop-types": "^15.8.1"
},
"peerDependencies": {
"react": "^15.3.0 || 16 || 17 || 18"
}
},
"node_modules/react-dev-utils": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
@ -30689,6 +30703,17 @@
"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-memo-one": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz",
@ -50123,6 +50148,15 @@
"react-popper": "^2.2.5"
}
},
"react-debounce-input": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/react-debounce-input/-/react-debounce-input-3.3.0.tgz",
"integrity": "sha512-VEqkvs8JvY/IIZvh71Z0TC+mdbxERvYF33RcebnodlsUZ8RSgyKe2VWaHXv4+/8aoOgXLxWrdsYs2hDhcwbUgA==",
"requires": {
"lodash.debounce": "^4",
"prop-types": "^15.8.1"
}
},
"react-dev-utils": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
@ -55104,6 +55138,12 @@
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
"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-memo-one": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz",

View File

@ -49,6 +49,7 @@
"react-bootstrap": "^2.5.0",
"react-bootstrap-typeahead": "^6.0.0",
"react-datepicker": "^4.8.0",
"react-debounce-input": "^3.3.0",
"react-devtools": "^4.27.1",
"react-dom": "^18.2.0",
"react-icons": "^4.4.0",
@ -59,6 +60,7 @@
"serve": "^14.0.0",
"timepicker": "^1.13.18",
"typescript": "^4.7.4",
"use-debounce": "^9.0.4",
"web-vitals": "^3.0.2"
},
"overrides": {

File diff suppressed because it is too large Load Diff

View File

@ -575,6 +575,947 @@ export default function ProcessInstanceListTable({
};
};
const reportColumns = () => {
return (reportMetadata as any).columns;
};
const reportFilterBy = () => {
return (reportMetadata as any).filter_by;
};
const navigateToNewReport = (queryParamString: string) => {
removeError();
setProcessInstanceReportJustSaved(null);
setProcessInstanceFilters({});
navigate(`${processInstanceListPathPrefix}?${queryParamString}`);
};
const applyFilter = (event: any) => {
event.preventDefault();
setProcessInitiatorNotFoundErrorText('');
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
undefined,
undefined,
paginationQueryParamPrefix
);
let queryParamString = `per_page=${perPage}&page=${page}&user_filter=true`;
const {
valid,
startFromSeconds,
startToSeconds,
endFromSeconds,
endToSeconds,
} = calculateStartAndEndSeconds();
if (!valid) {
return;
}
if (startFromSeconds) {
queryParamString += `&start_from=${startFromSeconds}`;
}
if (startToSeconds) {
queryParamString += `&start_to=${startToSeconds}`;
}
if (endFromSeconds) {
queryParamString += `&end_from=${endFromSeconds}`;
}
if (endToSeconds) {
queryParamString += `&end_to=${endToSeconds}`;
}
if (processStatusSelection.length > 0) {
queryParamString += `&process_status=${processStatusSelection}`;
}
if (processModelSelection) {
queryParamString += `&process_model_identifier=${processModelSelection.id}`;
}
if (processInstanceReportSelection) {
queryParamString += `&report_id=${processInstanceReportSelection.id}`;
}
const reportColumnsBase64 = encodeBase64(JSON.stringify(reportColumns()));
queryParamString += `&report_columns=${reportColumnsBase64}`;
const reportFilterByBase64 = encodeBase64(JSON.stringify(reportFilterBy()));
queryParamString += `&report_filter_by=${reportFilterByBase64}`;
if (processInitiatorSelection) {
queryParamString += `&process_initiator_username=${processInitiatorSelection.username}`;
navigateToNewReport(queryParamString);
} else if (processInitiatorText) {
HttpService.makeCallToBackend({
path: targetUris.userExists,
httpMethod: 'POST',
postBody: { username: processInitiatorText },
successCallback: (result: any) => {
if (result.user_found) {
queryParamString += `&process_initiator_username=${processInitiatorText}`;
navigateToNewReport(queryParamString);
} else {
setProcessInitiatorNotFoundErrorText(
`The provided username is invalid. Please type the exact username.`
);
}
},
});
} else {
navigateToNewReport(queryParamString);
}
};
const dateComponent = (
labelString: any,
name: any,
initialDate: any,
initialTime: string,
onChangeDateFunction: any,
onChangeTimeFunction: any,
timeInvalid: boolean,
setTimeInvalid: any
) => {
return (
<>
<DatePicker dateFormat={DATE_FORMAT_CARBON} datePickerType="single">
<DatePickerInput
id={`date-picker-${name}`}
placeholder={DATE_FORMAT}
labelText={labelString}
type="text"
size="md"
autocomplete="off"
allowInput={false}
onChange={(dateChangeEvent: any) => {
if (!initialDate && !initialTime) {
onChangeTimeFunction(
convertDateObjectToFormattedHoursMinutes(new Date())
);
}
onChangeDateFunction(dateChangeEvent.srcElement.value);
}}
value={initialDate}
/>
</DatePicker>
<TimePicker
invalid={timeInvalid}
id="time-picker"
labelText="Select a time"
pattern="^([01]\d|2[0-3]):?([0-5]\d)$"
value={initialTime}
onChange={(event: any) => {
if (event.srcElement.validity.valid) {
setTimeInvalid(false);
} else {
setTimeInvalid(true);
}
onChangeTimeFunction(event.srcElement.value);
}}
/>
</>
);
};
const processStatusSearch = () => {
return (
<MultiSelect
label="Choose Status"
className="our-class"
id="process-instance-status-select"
titleText="Status"
items={processStatusAllOptions}
onChange={(selection: any) => {
setProcessStatusSelection(selection.selectedItems);
setRequiresRefilter(true);
}}
itemToString={(item: any) => {
return item || '';
}}
selectionFeedback="top-after-reopen"
selectedItems={processStatusSelection}
/>
);
};
const clearFilters = () => {
setProcessModelSelection(null);
setProcessStatusSelection([]);
setStartFromDate('');
setStartFromTime('');
setStartToDate('');
setStartToTime('');
setEndFromDate('');
setEndFromTime('');
setEndToDate('');
setEndToTime('');
setProcessInitiatorSelection(null);
setProcessInitiatorText('');
setRequiresRefilter(true);
if (reportMetadata) {
reportMetadata.filter_by = [];
}
};
const processInstanceReportDidChange = (selection: any, mode?: string) => {
clearFilters();
const selectedReport = selection.selectedItem;
setProcessInstanceReportSelection(selectedReport);
let queryParamString = '';
if (selectedReport) {
queryParamString = `?report_id=${selectedReport.id}`;
}
removeError();
setProcessInstanceReportJustSaved(mode || null);
navigate(`${processInstanceListPathPrefix}${queryParamString}`);
};
const reportColumnAccessors = () => {
return reportColumns().map((reportColumn: ReportColumn) => {
return reportColumn.accessor;
});
};
// TODO onSuccess reload/select the new report in the report search
const onSaveReportSuccess = (result: any, mode: string) => {
processInstanceReportDidChange(
{
selectedItem: result,
},
mode
);
};
const saveAsReportComponent = () => {
const {
valid,
startFromSeconds,
startToSeconds,
endFromSeconds,
endToSeconds,
} = calculateStartAndEndSeconds(false);
if (!valid || !reportMetadata) {
return null;
}
return (
<ProcessInstanceListSaveAsReport
onSuccess={onSaveReportSuccess}
buttonClassName="button-white-background narrow-button"
columnArray={reportColumns()}
orderBy=""
buttonText="Save"
processModelSelection={processModelSelection}
processInitiatorSelection={processInitiatorSelection}
processStatusSelection={processStatusSelection}
processInstanceReportSelection={processInstanceReportSelection}
reportMetadata={reportMetadata}
startFromSeconds={startFromSeconds}
startToSeconds={startToSeconds}
endFromSeconds={endFromSeconds}
endToSeconds={endToSeconds}
/>
);
};
const onDeleteReportSuccess = () => {
processInstanceReportDidChange({ selectedItem: null });
};
const deleteReportComponent = () => {
return processInstanceReportSelection ? (
<ProcessInstanceListDeleteReport
onSuccess={onDeleteReportSuccess}
processInstanceReportSelection={processInstanceReportSelection}
/>
) : null;
};
const removeColumn = (reportColumn: ReportColumn) => {
if (reportMetadata) {
const reportMetadataCopy = { ...reportMetadata };
const newColumns = reportColumns().filter(
(rc: ReportColumn) => rc.accessor !== reportColumn.accessor
);
Object.assign(reportMetadataCopy, { columns: newColumns });
setReportMetadata(reportMetadataCopy);
setRequiresRefilter(true);
}
};
const handleColumnFormClose = () => {
setShowReportColumnForm(false);
setReportColumnFormMode('');
setReportColumnToOperateOn(null);
};
const getFilterByFromReportMetadata = (reportColumnAccessor: string) => {
if (reportMetadata) {
return reportMetadata.filter_by.find((reportFilter: ReportFilter) => {
return reportColumnAccessor === reportFilter.field_name;
});
}
return null;
};
const getNewFiltersFromReportForEditing = (
reportColumnForEditing: ReportColumnForEditing
) => {
if (!reportMetadata) {
return null;
}
const reportMetadataCopy = { ...reportMetadata };
let newReportFilters = reportMetadataCopy.filter_by;
if (reportColumnForEditing.filterable) {
const newReportFilter: ReportFilter = {
field_name: reportColumnForEditing.accessor,
field_value: reportColumnForEditing.filter_field_value,
operator: reportColumnForEditing.filter_operator || 'equals',
};
const existingReportFilter = getFilterByFromReportMetadata(
reportColumnForEditing.accessor
);
if (existingReportFilter) {
const existingReportFilterIndex =
reportMetadataCopy.filter_by.indexOf(existingReportFilter);
if (reportColumnForEditing.filter_field_value) {
newReportFilters[existingReportFilterIndex] = newReportFilter;
} else {
newReportFilters.splice(existingReportFilterIndex, 1);
}
} else if (reportColumnForEditing.filter_field_value) {
newReportFilters = newReportFilters.concat([newReportFilter]);
}
}
return newReportFilters;
};
const handleUpdateReportColumn = () => {
if (reportColumnToOperateOn && reportMetadata) {
const reportMetadataCopy = { ...reportMetadata };
let newReportColumns = null;
if (reportColumnFormMode === 'new') {
newReportColumns = reportColumns().concat([reportColumnToOperateOn]);
} else {
newReportColumns = reportColumns().map((rc: ReportColumn) => {
if (rc.accessor === reportColumnToOperateOn.accessor) {
return reportColumnToOperateOn;
}
return rc;
});
}
Object.assign(reportMetadataCopy, {
columns: newReportColumns,
filter_by: getNewFiltersFromReportForEditing(reportColumnToOperateOn),
});
setReportMetadata(reportMetadataCopy);
setReportColumnToOperateOn(null);
setShowReportColumnForm(false);
setRequiresRefilter(true);
}
};
const reportColumnToReportColumnForEditing = (reportColumn: ReportColumn) => {
const reportColumnForEditing: ReportColumnForEditing = Object.assign(
reportColumn,
{ filter_field_value: '', filter_operator: '' }
);
const reportFilter = getFilterByFromReportMetadata(
reportColumnForEditing.accessor
);
if (reportFilter) {
reportColumnForEditing.filter_field_value = reportFilter.field_value;
reportColumnForEditing.filter_operator =
reportFilter.operator || 'equals';
}
return reportColumnForEditing;
};
const updateReportColumn = (event: any) => {
let reportColumnForEditing = null;
if (event.selectedItem) {
reportColumnForEditing = reportColumnToReportColumnForEditing(
event.selectedItem
);
}
setReportColumnToOperateOn(reportColumnForEditing);
setRequiresRefilter(true);
};
// options includes item and inputValue
const shouldFilterReportColumn = (options: any) => {
const reportColumn: ReportColumn = options.item;
const { inputValue } = options;
return (
!reportColumnAccessors().includes(reportColumn.accessor) &&
(reportColumn.accessor || '')
.toLowerCase()
.includes((inputValue || '').toLowerCase())
);
};
const setReportColumnConditionValue = (event: any) => {
if (reportColumnToOperateOn) {
const reportColumnToOperateOnCopy = {
...reportColumnToOperateOn,
};
reportColumnToOperateOnCopy.filter_field_value = event.target.value;
setReportColumnToOperateOn(reportColumnToOperateOnCopy);
setRequiresRefilter(true);
}
};
const reportColumnForm = () => {
if (reportColumnFormMode === '') {
return null;
}
const formElements = [];
if (reportColumnFormMode === 'new') {
formElements.push(
<ComboBox
onChange={updateReportColumn}
id="report-column-selection"
data-qa="report-column-selection"
data-modal-primary-focus
items={availableReportColumns}
itemToString={(reportColumn: ReportColumn) => {
if (reportColumn) {
return reportColumn.accessor;
}
return null;
}}
shouldFilterItem={shouldFilterReportColumn}
placeholder="Choose a column to show"
titleText="Column"
selectedItem={reportColumnToOperateOn}
/>
);
}
formElements.push([
<TextInput
id="report-column-display-name"
name="report-column-display-name"
labelText="Display Name"
disabled={!reportColumnToOperateOn}
value={reportColumnToOperateOn ? reportColumnToOperateOn.Header : ''}
onChange={(event: any) => {
if (reportColumnToOperateOn) {
setRequiresRefilter(true);
const reportColumnToOperateOnCopy = {
...reportColumnToOperateOn,
};
reportColumnToOperateOnCopy.Header = event.target.value;
setReportColumnToOperateOn(reportColumnToOperateOnCopy);
}
}}
/>,
]);
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}
/>
);
}
formElements.push(
<div className="vertical-spacer-to-allow-combo-box-to-expand-in-modal" />
);
const modalHeading =
reportColumnFormMode === 'new'
? 'Add Column'
: `Edit ${
reportColumnToOperateOn ? reportColumnToOperateOn.accessor : ''
} column`;
return (
<Modal
open={showReportColumnForm}
modalHeading={modalHeading}
primaryButtonText="Save"
primaryButtonDisabled={!reportColumnToOperateOn}
onRequestSubmit={handleUpdateReportColumn}
onRequestClose={handleColumnFormClose}
hasScrollingContent
>
{formElements}
</Modal>
);
};
const columnSelections = () => {
if (reportColumns()) {
const tags: any = [];
(reportColumns() as any).forEach((reportColumn: ReportColumn) => {
const reportColumnForEditing =
reportColumnToReportColumnForEditing(reportColumn);
let tagType = 'cool-gray';
let tagTypeClass = '';
if (reportColumnForEditing.filterable) {
tagType = 'green';
tagTypeClass = 'tag-type-green';
}
let reportColumnLabel = reportColumnForEditing.Header;
if (reportColumnForEditing.filter_field_value) {
reportColumnLabel = `${reportColumnLabel}=${reportColumnForEditing.filter_field_value}`;
}
tags.push(
<Tag type={tagType} size="sm">
<Button
kind="ghost"
size="sm"
className={`button-tag-icon ${tagTypeClass}`}
title={`Edit ${reportColumnForEditing.accessor} column`}
onClick={() => {
setReportColumnToOperateOn(reportColumnForEditing);
setShowReportColumnForm(true);
setReportColumnFormMode('edit');
}}
>
{reportColumnLabel}
</Button>
<Button
data-qa="remove-report-column"
renderIcon={Close}
iconDescription="Remove Column"
className={`button-tag-icon ${tagTypeClass}`}
hasIconOnly
size="sm"
kind="ghost"
onClick={() => removeColumn(reportColumnForEditing)}
/>
</Tag>
);
});
return (
<Stack orientation="horizontal">
{tags}
<Button
data-qa="add-column-button"
renderIcon={AddAlt}
iconDescription="Column options"
className="with-tiny-top-margin"
kind="ghost"
hasIconOnly
size="sm"
onClick={() => {
setShowReportColumnForm(true);
setReportColumnFormMode('new');
}}
/>
</Stack>
);
}
return null;
};
const filterOptions = () => {
if (!showFilterOptions) {
return null;
}
let queryParamString = '';
if (processModelSelection) {
queryParamString += `?process_model_identifier=${processModelSelection.id}`;
}
// get the columns anytime we display the filter options if they are empty.
// and if the columns are not empty, check if the columns are stale
// because we selected a different process model in the filter options.
const columnFilterIsStale = lastColumnFilter !== queryParamString;
if (availableReportColumns.length < 1 || columnFilterIsStale) {
setLastColumnFilter(queryParamString);
HttpService.makeCallToBackend({
path: `/process-instances/reports/columns${queryParamString}`,
successCallback: setAvailableReportColumns,
});
}
return (
<>
<Grid fullWidth className="with-bottom-margin">
<Column md={8} lg={16} sm={4}>
<FormLabel>Columns</FormLabel>
<br />
{columnSelections()}
</Column>
</Grid>
<Grid fullWidth className="with-bottom-margin">
<Column md={8}>
<ProcessModelSearch
onChange={(selection: any) => {
setProcessModelSelection(selection.selectedItem);
setRequiresRefilter(true);
}}
processModels={processModelAvailableItems}
selectedItem={processModelSelection}
/>
</Column>
<Column md={4}>
<Can
I="GET"
a={targetUris.userSearch}
ability={ability}
passThrough
>
{(hasAccess: boolean) => {
if (hasAccess) {
return (
<ComboBox
onInputChange={searchForProcessInitiator}
onChange={(event: any) => {
setProcessInitiatorSelection(event.selectedItem);
setRequiresRefilter(true);
}}
id="process-instance-initiator-search"
data-qa="process-instance-initiator-search"
items={processInstanceInitiatorOptions}
itemToString={(processInstanceInitatorOption: User) => {
if (processInstanceInitatorOption) {
return processInstanceInitatorOption.username;
}
return null;
}}
placeholder="Start typing username"
titleText="Process Initiator"
selectedItem={processInitiatorSelection}
/>
);
}
return (
<TextInput
id="process-instance-initiator-search"
placeholder="Enter username"
labelText="Process Initiator"
invalid={processInitiatorNotFoundErrorText !== ''}
invalidText={processInitiatorNotFoundErrorText}
onChange={(event: any) => {
setProcessInitiatorText(event.target.value);
setRequiresRefilter(true);
}}
/>
);
}}
</Can>
</Column>
<Column md={4}>{processStatusSearch()}</Column>
</Grid>
<Grid fullWidth className="with-bottom-margin">
<Column md={4}>
{dateComponent(
'Start date from',
'start-from',
startFromDate,
startFromTime,
(val: string) => {
setStartFromDate(val);
setRequiresRefilter(true);
},
(val: string) => {
setStartFromTime(val);
setRequiresRefilter(true);
},
startFromTimeInvalid,
setStartFromTimeInvalid
)}
</Column>
<Column md={4}>
{dateComponent(
'Start date to',
'start-to',
startToDate,
startToTime,
(val: string) => {
setStartToDate(val);
setRequiresRefilter(true);
},
(val: string) => {
setStartToTime(val);
setRequiresRefilter(true);
},
startToTimeInvalid,
setStartToTimeInvalid
)}
</Column>
<Column md={4}>
{dateComponent(
'End date from',
'end-from',
endFromDate,
endFromTime,
(val: string) => {
setEndFromDate(val);
setRequiresRefilter(true);
},
(val: string) => {
setEndFromTime(val);
setRequiresRefilter(true);
},
endFromTimeInvalid,
setEndFromTimeInvalid
)}
</Column>
<Column md={4}>
{dateComponent(
'End date to',
'end-to',
endToDate,
endToTime,
(val: string) => {
setEndToDate(val);
setRequiresRefilter(true);
},
(val: string) => {
setEndToTime(val);
setRequiresRefilter(true);
},
endToTimeInvalid,
setEndToTimeInvalid
)}
</Column>
</Grid>
<Grid fullWidth className="with-bottom-margin">
<Column sm={4} md={4} lg={8}>
<ButtonSet>
<Button
kind=""
className="button-white-background narrow-button"
onClick={clearFilters}
>
Clear
</Button>
<Button
kind="secondary"
disabled={!requiresRefilter}
onClick={applyFilter}
data-qa="filter-button"
className="narrow-button"
>
Apply
</Button>
</ButtonSet>
</Column>
<Column sm={4} md={4} lg={8}>
{saveAsReportComponent()}
{deleteReportComponent()}
</Column>
</Grid>
</>
);
};
const getWaitingForTableCellComponent = (processInstanceTask: any) => {
let fullUsernameString = '';
let shortUsernameString = '';
if (processInstanceTask.potential_owner_usernames) {
fullUsernameString = processInstanceTask.potential_owner_usernames;
const usernames =
processInstanceTask.potential_owner_usernames.split(',');
const firstTwoUsernames = usernames.slice(0, 2);
if (usernames.length > 2) {
firstTwoUsernames.push('...');
}
shortUsernameString = firstTwoUsernames.join(',');
}
if (processInstanceTask.assigned_user_group_identifier) {
fullUsernameString = processInstanceTask.assigned_user_group_identifier;
shortUsernameString = processInstanceTask.assigned_user_group_identifier;
}
return <span title={fullUsernameString}>{shortUsernameString}</span>;
};
const formatProcessInstanceId = (row: ProcessInstance, id: number) => {
return <span data-qa="paginated-entity-id">{id}</span>;
};
const formatProcessModelIdentifier = (_row: any, identifier: any) => {
return <span>{identifier}</span>;
};
const formatProcessModelDisplayName = (_row: any, identifier: any) => {
return <span>{identifier}</span>;
};
const formatSecondsForDisplay = (_row: any, seconds: any) => {
return convertSecondsToFormattedDateTime(seconds) || '-';
};
const defaultFormatter = (_row: any, value: any) => {
return value;
};
const formattedColumn = (row: any, column: any) => {
const reportColumnFormatters: Record<string, any> = {
id: formatProcessInstanceId,
process_model_identifier: formatProcessModelIdentifier,
process_model_display_name: formatProcessModelDisplayName,
start_in_seconds: formatSecondsForDisplay,
end_in_seconds: formatSecondsForDisplay,
updated_at_in_seconds: formatSecondsForDisplay,
};
const formatter =
reportColumnFormatters[column.accessor] ?? defaultFormatter;
const value = row[column.accessor];
const modifiedModelId = modifyProcessIdentifierForPathParam(
row.process_model_identifier
);
const navigateToProcessInstance = () => {
navigate(`${processInstanceShowPathPrefix}/${modifiedModelId}/${row.id}`);
};
const navigateToProcessModel = () => {
navigate(`/admin/process-models/${modifiedModelId}`);
};
if (column.accessor === 'status') {
return (
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
<td
onClick={navigateToProcessInstance}
onKeyDown={navigateToProcessInstance}
data-qa={`process-instance-status-${value}`}
>
{formatter(row, value)}
</td>
);
}
if (column.accessor === 'process_model_display_name') {
const pmStyle = { background: 'rgba(0, 0, 0, .02)' };
return (
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
<td
style={pmStyle}
onClick={navigateToProcessModel}
onKeyDown={navigateToProcessModel}
>
{formatter(row, value)}
</td>
);
}
if (column.accessor === 'waiting_for') {
return <td>{getWaitingForTableCellComponent(row)}</td>;
}
if (column.accessor === 'updated_at_in_seconds') {
return (
<TableCellWithTimeAgoInWords
timeInSeconds={row.updated_at_in_seconds}
/>
);
}
return (
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
<td
data-qa={`process-instance-show-link-${column.accessor}`}
onKeyDown={navigateToProcessModel}
onClick={navigateToProcessInstance}
>
{formatter(row, value)}
</td>
);
};
const buildTable = () => {
const headerLabels: Record<string, string> = {
id: 'Id',
process_model_identifier: 'Process',
process_model_display_name: 'Process',
start_in_seconds: 'Start Time',
end_in_seconds: 'End Time',
status: 'Status',
process_initiator_username: 'Started By',
};
const getHeaderLabel = (header: string) => {
return headerLabels[header] ?? header;
};
const headers = reportColumns().map((column: any) => {
return getHeaderLabel((column as any).Header);
});
if (showActionsColumn) {
headers.push('Actions');
}
const rows = processInstances.map((row: any) => {
const currentRow = reportColumns().map((column: any) => {
return formattedColumn(row, column);
});
if (showActionsColumn) {
let buttonElement = null;
if (row.task_id) {
const taskUrl = `/tasks/${row.id}/${row.task_id}`;
const regex = new RegExp(`\\b(${preferredUsername}|${userEmail})\\b`);
let hasAccessToCompleteTask = false;
if (
canCompleteAllTasks ||
(row.potential_owner_usernames || '').match(regex)
) {
hasAccessToCompleteTask = true;
}
buttonElement = (
<Button
variant="primary"
href={taskUrl}
hidden={row.status === 'suspended'}
disabled={!hasAccessToCompleteTask}
>
Go
</Button>
);
}
currentRow.push(<td>{buttonElement}</td>);
}
const rowStyle = { cursor: 'pointer' };
return (
<tr style={rowStyle} key={row.id}>
{currentRow}
</tr>
);
});
return (
<Table size="lg">
<TableHead>
<TableRow>
{headers.map((header: any) => (
<TableHeader
key={header}
title={header === 'Id' ? 'Process Instance Id' : null}
>
{header}
</TableHeader>
))}
</TableRow>
</TableHead>
<tbody>{rows}</tbody>
</Table>
);
};
const reportSearchComponent = () => {
if (showReports) {
const columns = [
<Column sm={2} md={4} lg={7}>
<ProcessInstanceReportSearch
onChange={processInstanceReportDidChange}
selectedItem={processInstanceReportSelection}
/>
</Column>,
];
return (
<Grid className="with-tiny-bottom-margin" fullWidth>
{columns}
</Grid>
);
}
return null;
};
if (pagination && (!textToShowIfEmpty || pagination.total > 0)) {
// eslint-disable-next-line prefer-const
let { page, perPage } = getPageInfoFromSearchParams(
@ -615,7 +1556,7 @@ export default function ProcessInstanceListTable({
{reportColumnForm()}
{processInstanceReportSaveTag()}
<Filters
filterOptions={filterOptions()}
filterOptions={filterOptions}
showFilterOptions={showFilterOptions}
setShowFilterOptions={setShowFilterOptions}
reportSearchComponent={reportSearchComponent}

View File

@ -1,7 +1,24 @@
import { useEffect, useState } from 'react';
// @ts-ignore
import { Table, Tabs, TabList, Tab } from '@carbon/react';
import { Link, useParams, useSearchParams } from 'react-router-dom';
import {
Table,
Tabs,
TabList,
Tab,
Grid,
Column,
ButtonSet,
Button,
TextInput,
// @ts-ignore
} from '@carbon/react';
import {
Link,
useNavigate,
useParams,
useSearchParams,
} from 'react-router-dom';
import { DebounceInput } from 'react-debounce-input';
import { useDebounce, useDebouncedCallback } from 'use-debounce';
import PaginationForTable from '../components/PaginationForTable';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import {
@ -11,6 +28,7 @@ import {
import HttpService from '../services/HttpService';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { ProcessInstanceLogEntry } from '../interfaces';
import Filters from '../components/Filters';
type OwnProps = {
variant: string;
@ -18,17 +36,37 @@ type OwnProps = {
export default function ProcessInstanceLogList({ variant }: OwnProps) {
const params = useParams();
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const [processInstanceLogs, setProcessInstanceLogs] = useState([]);
const [pagination, setPagination] = useState(null);
const [taskName, setTaskName] = useState<string>('');
const [taskIdentifier, setTaskIdentifier] = useState<string>('');
const { targetUris } = useUriListForPermissions();
const isDetailedView = searchParams.get('detailed') === 'true';
const [showFilterOptions, setShowFilterOptions] = useState<boolean>(false);
let processInstanceShowPageBaseUrl = `/admin/process-instances/for-me/${params.process_model_id}`;
if (variant === 'all') {
processInstanceShowPageBaseUrl = `/admin/process-instances/${params.process_model_id}`;
}
const addDebouncedSearchParams = useDebouncedCallback(
(value, key) => {
if (value) {
searchParams.set(key, value);
} else {
searchParams.delete(key);
}
setSearchParams(searchParams);
},
// delay in ms
1000
);
useEffect(() => {
// Clear out any previous results to avoid a "flicker" effect where columns
// are updated above the incorrect data.
@ -160,7 +198,96 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
</Table>
);
};
const selectedTabIndex = isDetailedView ? 1 : 0;
const resetFilters = () => {
setTaskIdentifier('');
setTaskName('');
searchParams.delete(taskName);
searchParams.delete(taskIdentifier);
setSearchParams(searchParams);
};
const filterOptions = () => {
if (!showFilterOptions) {
return null;
}
return (
<>
<Grid fullWidth className="with-bottom-margin">
<Column md={4}>
<TextInput
id="task-name-filter"
labelText="Task Name"
value={taskName}
onChange={(event: any) => {
const newValue = event.target.value;
setTaskName(newValue);
addDebouncedSearchParams(newValue, 'bpmn_name');
}}
/>
</Column>
<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>
</Grid>
<Grid fullWidth className="with-bottom-margin">
<Column sm={4} md={4} lg={8}>
<ButtonSet>
<Button
kind=""
className="button-white-background narrow-button"
onClick={resetFilters}
>
Reset
</Button>
</ButtonSet>
</Column>
</Grid>
</>
);
};
const tabs = () => {
const selectedTabIndex = isDetailedView ? 1 : 0;
return (
<Tabs selectedIndex={selectedTabIndex}>
<TabList aria-label="List of tabs">
<Tab
title="Only show a subset of the logs, and show fewer columns"
data-qa="process-instance-log-simple"
onClick={() => {
searchParams.set('detailed', 'false');
setSearchParams(searchParams);
}}
>
Milestones
</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"
onClick={() => {
searchParams.set('detailed', 'true');
setSearchParams(searchParams);
}}
>
Events
</Tab>
</TabList>
</Tabs>
);
};
if (pagination) {
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
@ -181,30 +308,13 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
['Logs'],
]}
/>
<Tabs selectedIndex={selectedTabIndex}>
<TabList aria-label="List of tabs">
<Tab
title="Only show a subset of the logs, and show fewer columns"
data-qa="process-instance-log-simple"
onClick={() => {
searchParams.set('detailed', 'false');
setSearchParams(searchParams);
}}
>
Milestones
</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"
onClick={() => {
searchParams.set('detailed', 'true');
setSearchParams(searchParams);
}}
>
Events
</Tab>
</TabList>
</Tabs>
{tabs()}
<Filters
filterOptions={filterOptions}
showFilterOptions={showFilterOptions}
setShowFilterOptions={setShowFilterOptions}
filtersEnabled
/>
<br />
<PaginationForTable
page={page}