some more log stuff using debounce w/ burnettk
This commit is contained in:
parent
1768618f27
commit
2caa42700c
|
@ -1,11 +1,7 @@
|
||||||
"""Spiff_enum."""
|
|
||||||
import enum
|
import enum
|
||||||
|
|
||||||
|
|
||||||
class SpiffEnum(enum.Enum):
|
class SpiffEnum(enum.Enum):
|
||||||
"""SpiffEnum."""
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def list(cls) -> list[str]:
|
def list(cls) -> list[str]:
|
||||||
"""List."""
|
|
||||||
return [el.value for el in cls]
|
return [el.value for el in cls]
|
||||||
|
|
|
@ -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)
|
|
@ -224,63 +224,6 @@ def process_instance_resume(
|
||||||
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
|
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(
|
def process_instance_list_for_me(
|
||||||
process_model_identifier: Optional[str] = None,
|
process_model_identifier: Optional[str] = None,
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
|
|
|
@ -54,6 +54,7 @@
|
||||||
"react-bootstrap": "^2.5.0",
|
"react-bootstrap": "^2.5.0",
|
||||||
"react-bootstrap-typeahead": "^6.0.0",
|
"react-bootstrap-typeahead": "^6.0.0",
|
||||||
"react-datepicker": "^4.8.0",
|
"react-datepicker": "^4.8.0",
|
||||||
|
"react-debounce-input": "^3.3.0",
|
||||||
"react-devtools": "^4.27.1",
|
"react-devtools": "^4.27.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-icons": "^4.4.0",
|
"react-icons": "^4.4.0",
|
||||||
|
@ -64,6 +65,7 @@
|
||||||
"serve": "^14.0.0",
|
"serve": "^14.0.0",
|
||||||
"timepicker": "^1.13.18",
|
"timepicker": "^1.13.18",
|
||||||
"typescript": "^4.7.4",
|
"typescript": "^4.7.4",
|
||||||
|
"use-debounce": "^9.0.4",
|
||||||
"web-vitals": "^3.0.2"
|
"web-vitals": "^3.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -24152,6 +24154,18 @@
|
||||||
"react-dom": "^16.9.0 || ^17 || ^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": {
|
"node_modules/react-dev-utils": {
|
||||||
"version": "12.0.1",
|
"version": "12.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
|
||||||
|
@ -30689,6 +30703,17 @@
|
||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/use-memo-one": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz",
|
"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-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": {
|
"react-dev-utils": {
|
||||||
"version": "12.0.1",
|
"version": "12.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
|
||||||
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ=="
|
"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": {
|
"use-memo-one": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz",
|
||||||
|
|
|
@ -49,6 +49,7 @@
|
||||||
"react-bootstrap": "^2.5.0",
|
"react-bootstrap": "^2.5.0",
|
||||||
"react-bootstrap-typeahead": "^6.0.0",
|
"react-bootstrap-typeahead": "^6.0.0",
|
||||||
"react-datepicker": "^4.8.0",
|
"react-datepicker": "^4.8.0",
|
||||||
|
"react-debounce-input": "^3.3.0",
|
||||||
"react-devtools": "^4.27.1",
|
"react-devtools": "^4.27.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-icons": "^4.4.0",
|
"react-icons": "^4.4.0",
|
||||||
|
@ -59,6 +60,7 @@
|
||||||
"serve": "^14.0.0",
|
"serve": "^14.0.0",
|
||||||
"timepicker": "^1.13.18",
|
"timepicker": "^1.13.18",
|
||||||
"typescript": "^4.7.4",
|
"typescript": "^4.7.4",
|
||||||
|
"use-debounce": "^9.0.4",
|
||||||
"web-vitals": "^3.0.2"
|
"web-vitals": "^3.0.2"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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)) {
|
if (pagination && (!textToShowIfEmpty || pagination.total > 0)) {
|
||||||
// eslint-disable-next-line prefer-const
|
// eslint-disable-next-line prefer-const
|
||||||
let { page, perPage } = getPageInfoFromSearchParams(
|
let { page, perPage } = getPageInfoFromSearchParams(
|
||||||
|
@ -615,7 +1556,7 @@ export default function ProcessInstanceListTable({
|
||||||
{reportColumnForm()}
|
{reportColumnForm()}
|
||||||
{processInstanceReportSaveTag()}
|
{processInstanceReportSaveTag()}
|
||||||
<Filters
|
<Filters
|
||||||
filterOptions={filterOptions()}
|
filterOptions={filterOptions}
|
||||||
showFilterOptions={showFilterOptions}
|
showFilterOptions={showFilterOptions}
|
||||||
setShowFilterOptions={setShowFilterOptions}
|
setShowFilterOptions={setShowFilterOptions}
|
||||||
reportSearchComponent={reportSearchComponent}
|
reportSearchComponent={reportSearchComponent}
|
||||||
|
|
|
@ -1,7 +1,24 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
// @ts-ignore
|
import {
|
||||||
import { Table, Tabs, TabList, Tab } from '@carbon/react';
|
Table,
|
||||||
import { Link, useParams, useSearchParams } from 'react-router-dom';
|
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 PaginationForTable from '../components/PaginationForTable';
|
||||||
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
|
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
|
||||||
import {
|
import {
|
||||||
|
@ -11,6 +28,7 @@ import {
|
||||||
import HttpService from '../services/HttpService';
|
import HttpService from '../services/HttpService';
|
||||||
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
|
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
|
||||||
import { ProcessInstanceLogEntry } from '../interfaces';
|
import { ProcessInstanceLogEntry } from '../interfaces';
|
||||||
|
import Filters from '../components/Filters';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
variant: string;
|
variant: string;
|
||||||
|
@ -18,17 +36,37 @@ type OwnProps = {
|
||||||
|
|
||||||
export default function ProcessInstanceLogList({ variant }: OwnProps) {
|
export default function ProcessInstanceLogList({ variant }: OwnProps) {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [processInstanceLogs, setProcessInstanceLogs] = useState([]);
|
const [processInstanceLogs, setProcessInstanceLogs] = useState([]);
|
||||||
const [pagination, setPagination] = useState(null);
|
const [pagination, setPagination] = useState(null);
|
||||||
|
|
||||||
|
const [taskName, setTaskName] = useState<string>('');
|
||||||
|
const [taskIdentifier, setTaskIdentifier] = useState<string>('');
|
||||||
|
|
||||||
const { targetUris } = useUriListForPermissions();
|
const { targetUris } = useUriListForPermissions();
|
||||||
const isDetailedView = searchParams.get('detailed') === 'true';
|
const isDetailedView = searchParams.get('detailed') === 'true';
|
||||||
|
|
||||||
|
const [showFilterOptions, setShowFilterOptions] = useState<boolean>(false);
|
||||||
|
|
||||||
let processInstanceShowPageBaseUrl = `/admin/process-instances/for-me/${params.process_model_id}`;
|
let processInstanceShowPageBaseUrl = `/admin/process-instances/for-me/${params.process_model_id}`;
|
||||||
if (variant === 'all') {
|
if (variant === 'all') {
|
||||||
processInstanceShowPageBaseUrl = `/admin/process-instances/${params.process_model_id}`;
|
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(() => {
|
useEffect(() => {
|
||||||
// Clear out any previous results to avoid a "flicker" effect where columns
|
// Clear out any previous results to avoid a "flicker" effect where columns
|
||||||
// are updated above the incorrect data.
|
// are updated above the incorrect data.
|
||||||
|
@ -160,7 +198,96 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
|
||||||
</Table>
|
</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) {
|
if (pagination) {
|
||||||
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
|
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
|
||||||
|
@ -181,30 +308,13 @@ export default function ProcessInstanceLogList({ variant }: OwnProps) {
|
||||||
['Logs'],
|
['Logs'],
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Tabs selectedIndex={selectedTabIndex}>
|
{tabs()}
|
||||||
<TabList aria-label="List of tabs">
|
<Filters
|
||||||
<Tab
|
filterOptions={filterOptions}
|
||||||
title="Only show a subset of the logs, and show fewer columns"
|
showFilterOptions={showFilterOptions}
|
||||||
data-qa="process-instance-log-simple"
|
setShowFilterOptions={setShowFilterOptions}
|
||||||
onClick={() => {
|
filtersEnabled
|
||||||
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>
|
|
||||||
<br />
|
<br />
|
||||||
<PaginationForTable
|
<PaginationForTable
|
||||||
page={page}
|
page={page}
|
||||||
|
|
Loading…
Reference in New Issue