From a4edb5d76672cd6c4a6bbd03f3de051bff3aa1c5 Mon Sep 17 00:00:00 2001 From: jasquat Date: Fri, 2 Dec 2022 10:32:40 -0500 Subject: [PATCH] favor report id over identifier but support both and ui updates to allow setting a condition value on a metadata field, changing the display name, and fixes for saving and updating a report --- .../src/spiffworkflow_backend/api.yml | 12 +- .../models/process_instance_report.py | 28 ++-- .../routes/process_api_blueprint.py | 35 +++-- .../process_instance_report_service.py | 23 ++- .../ProcessInstanceListSaveAsReport.tsx | 61 +++++--- .../components/ProcessInstanceListTable.tsx | 133 +++++++++++++----- .../ProcessInstanceReportSearch.tsx | 75 +++++----- spiffworkflow-frontend/src/index.css | 4 + spiffworkflow-frontend/src/interfaces.ts | 23 +-- 9 files changed, 261 insertions(+), 133 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index 81fa92bd..5be50b8d 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -544,6 +544,12 @@ paths: description: Specifies the identifier of a report to use, if any schema: type: string + - name: report_id + in: query + required: false + description: Specifies the identifier of a report to use, if any + schema: + type: integer get: operationId: spiffworkflow_backend.routes.process_api_blueprint.process_instance_list summary: Returns a list of process instances for a given process model @@ -857,14 +863,14 @@ paths: items: $ref: "#/components/schemas/Workflow" - /process-instances/reports/{report_identifier}: + /process-instances/reports/{report_id}: parameters: - - name: report_identifier + - name: report_id in: path required: true description: The unique id of an existing report schema: - type: string + type: integer - name: page in: query required: false diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py index 4f0b0f46..3c0e8646 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py @@ -26,6 +26,10 @@ from spiffworkflow_backend.services.process_instance_processor import ( ReportMetadata = dict[str, Any] +class ProcessInstanceReportAlreadyExistsError(Exception): + """ProcessInstanceReportAlreadyExistsError.""" + + class ProcessInstanceReportResult(TypedDict): """ProcessInstanceReportResult.""" @@ -63,7 +67,7 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel): ), ) - id = db.Column(db.Integer, primary_key=True) + id: int = db.Column(db.Integer, primary_key=True) identifier: str = db.Column(db.String(50), nullable=False, index=True) report_metadata: dict = deferred(db.Column(db.JSON)) # type: ignore created_by_id = db.Column(ForeignKey(UserModel.id), nullable=False, index=True) @@ -120,21 +124,27 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel): identifier: str, user: UserModel, report_metadata: ReportMetadata, - ) -> None: + ) -> ProcessInstanceReportModel: """Make_fixture_report.""" process_instance_report = ProcessInstanceReportModel.query.filter_by( identifier=identifier, created_by_id=user.id, ).first() - if process_instance_report is None: - process_instance_report = cls( - identifier=identifier, - created_by_id=user.id, - report_metadata=report_metadata, + if process_instance_report is not None: + raise ProcessInstanceReportAlreadyExistsError( + f"Process instance report with identifier already exists: {identifier}" ) - db.session.add(process_instance_report) - db.session.commit() + + process_instance_report = cls( + identifier=identifier, + created_by_id=user.id, + report_metadata=report_metadata, + ) + db.session.add(process_instance_report) + db.session.commit() + + return process_instance_report # type: ignore @classmethod def ticket_for_month_report(cls) -> dict: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py index 25dc7eea..ce77996d 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -782,10 +782,12 @@ def process_instance_list( with_tasks_completed_by_my_group: Optional[bool] = None, user_filter: Optional[bool] = False, report_identifier: Optional[str] = None, + report_id: Optional[int] = None, ) -> flask.wrappers.Response: """Process_instance_list.""" + process_instance_report = ProcessInstanceReportService.report_with_identifier( - g.user, report_identifier + g.user, report_id, report_identifier ) if user_filter: @@ -960,11 +962,9 @@ def process_instance_list( results = ProcessInstanceReportService.add_metadata_columns_to_process_instance( process_instances.items, process_instance_report.report_metadata["columns"] ) - report_metadata = process_instance_report.report_metadata response_json = { - "report_identifier": process_instance_report.identifier, - "report_metadata": report_metadata, + "report": process_instance_report, "results": results, "filters": report_filter.to_dict(), "pagination": { @@ -982,7 +982,8 @@ def process_instance_report_column_list() -> flask.wrappers.Response: table_columns = ProcessInstanceReportService.builtin_column_options() columns_for_metadata = db.session.query(ProcessInstanceMetadataModel.key).distinct().all() # type: ignore columns_for_metadata_strings = [ - {"Header": i[0], "accessor": i[0], "filterable": True} for i in columns_for_metadata + {"Header": i[0], "accessor": i[0], "filterable": True} + for i in columns_for_metadata ] return make_response(jsonify(table_columns + columns_for_metadata_strings), 200) @@ -1043,22 +1044,22 @@ def process_instance_report_list( def process_instance_report_create(body: Dict[str, Any]) -> flask.wrappers.Response: """Process_instance_report_create.""" - ProcessInstanceReportModel.create_report( + process_instance_report = ProcessInstanceReportModel.create_report( identifier=body["identifier"], user=g.user, report_metadata=body["report_metadata"], ) - return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") + return make_response(jsonify(process_instance_report), 201) def process_instance_report_update( - report_identifier: str, + report_id: int, body: Dict[str, Any], ) -> flask.wrappers.Response: """Process_instance_report_create.""" process_instance_report = ProcessInstanceReportModel.query.filter_by( - identifier=report_identifier, + id=report_id, created_by_id=g.user.id, ).first() if process_instance_report is None: @@ -1071,15 +1072,15 @@ def process_instance_report_update( process_instance_report.report_metadata = body["report_metadata"] db.session.commit() - return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") + return make_response(jsonify(process_instance_report), 201) def process_instance_report_delete( - report_identifier: str, + report_id: int, ) -> flask.wrappers.Response: """Process_instance_report_create.""" process_instance_report = ProcessInstanceReportModel.query.filter_by( - identifier=report_identifier, + id=report_id, created_by_id=g.user.id, ).first() if process_instance_report is None: @@ -1098,8 +1099,6 @@ def process_instance_report_delete( def service_tasks_show() -> flask.wrappers.Response: """Service_tasks_show.""" available_connectors = ServiceTaskService.available_connectors() - print(available_connectors) - return Response( json.dumps(available_connectors), status=200, mimetype="application/json" ) @@ -1133,10 +1132,11 @@ def authentication_callback( def process_instance_report_show( - report_identifier: str, + report_id: int, page: int = 1, per_page: int = 100, ) -> flask.wrappers.Response: + """Process_instance_report_show.""" process_instances = ProcessInstanceModel.query.order_by( # .filter_by(process_model_identifier=process_model.id) ProcessInstanceModel.start_in_seconds.desc(), ProcessInstanceModel.id.desc() # type: ignore ).paginate( @@ -1144,7 +1144,7 @@ def process_instance_report_show( ) process_instance_report = ProcessInstanceReportModel.query.filter_by( - identifier=report_identifier, + id=report_id, created_by_id=g.user.id, ).first() if process_instance_report is None: @@ -1421,9 +1421,6 @@ def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response task.form_ui_schema = ui_form_contents if task.properties and task.data and "instructionsForEndUser" in task.properties: - print( - f"task.properties['instructionsForEndUser']: {task.properties['instructionsForEndUser']}" - ) if task.properties["instructionsForEndUser"]: task.properties["instructionsForEndUser"] = render_jinja_template( task.properties["instructionsForEndUser"], task.data diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py index 3c579a24..3486316f 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_report_service.py @@ -29,6 +29,8 @@ class ProcessInstanceReportFilter: """To_dict.""" d = {} + print(f"dir(self): {dir(self)}") + if self.process_model_identifier is not None: d["process_model_identifier"] = self.process_model_identifier if self.start_from is not None: @@ -60,12 +62,21 @@ class ProcessInstanceReportService: @classmethod def report_with_identifier( - cls, user: UserModel, report_identifier: Optional[str] = None + cls, + user: UserModel, + report_id: Optional[int] = None, + report_identifier: Optional[str] = None, ) -> ProcessInstanceReportModel: """Report_with_filter.""" + if report_id is not None: + process_instance_report = ProcessInstanceReportModel.query.filter_by( + id=report_id, created_by_id=user.id + ).first() + if process_instance_report is not None: + return process_instance_report # type: ignore + if report_identifier is None: report_identifier = "default" - process_instance_report = ProcessInstanceReportModel.query.filter_by( identifier=report_identifier, created_by_id=user.id ).first() @@ -75,9 +86,7 @@ class ProcessInstanceReportService: # TODO replace with system reports that are loaded on launch (or similar) temp_system_metadata_map = { - "default": { - "columns": cls.builtin_column_options() - }, + "default": {"columns": cls.builtin_column_options()}, "system_report_instances_initiated_by_me": { "columns": [ {"Header": "id", "accessor": "id"}, @@ -113,6 +122,7 @@ class ProcessInstanceReportService: created_by_id=user.id, report_metadata=temp_system_metadata_map[report_identifier], # type: ignore ) + # db.session.add(pro return process_instance_report # type: ignore @@ -246,7 +256,8 @@ class ProcessInstanceReportService: {"Header": "Id", "accessor": "id", "filterable": False}, { "Header": "Process", - "accessor": "process_model_display_name", "filterable": False, + "accessor": "process_model_display_name", + "filterable": False, }, {"Header": "Start", "accessor": "start_in_seconds", "filterable": False}, {"Header": "End", "accessor": "end_in_seconds", "filterable": False}, diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx index 5d93b66e..2942ba4d 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListSaveAsReport.tsx @@ -6,7 +6,7 @@ import { Stack, // @ts-ignore } from '@carbon/react'; -import { ProcessModel } from '../interfaces'; +import { ProcessInstanceReport, ProcessModel } from '../interfaces'; import HttpService from '../services/HttpService'; type OwnProps = { @@ -20,6 +20,8 @@ type OwnProps = { endFromSeconds: string | null; endToSeconds: string | null; buttonText?: string; + buttonClassName?: string; + processInstanceReportSelection?: ProcessInstanceReport | null; }; export default function ProcessInstanceListSaveAsReport({ @@ -27,25 +29,33 @@ export default function ProcessInstanceListSaveAsReport({ columnArray, orderBy, processModelSelection, + processInstanceReportSelection, processStatusSelection, startFromSeconds, startToSeconds, endFromSeconds, endToSeconds, buttonText = 'Save as Perspective', + buttonClassName, }: OwnProps) { - const [identifier, setIdentifier] = useState(''); + const [identifier, setIdentifier] = useState( + processInstanceReportSelection?.identifier || '' + ); const hasIdentifier = () => { return identifier?.length > 0; }; const responseHandler = (result: any) => { - if (result.ok === true) { - onSuccess(identifier); + if (result) { + onSuccess(result); } }; + const isEditMode = () => { + return !!processInstanceReportSelection; + }; + const addProcessInstanceReport = (event: any) => { event.preventDefault(); @@ -94,10 +104,17 @@ export default function ProcessInstanceListSaveAsReport({ }); } + let path = `/process-instances/reports`; + let httpMethod = 'POST'; + if (isEditMode() && processInstanceReportSelection) { + httpMethod = 'PUT'; + path = `${path}/${processInstanceReportSelection.id}`; + } + HttpService.makeCallToBackend({ - path: `/process-instances/reports`, + path, successCallback: responseHandler, - httpMethod: 'POST', + httpMethod, postBody: { identifier, report_metadata: { @@ -109,19 +126,31 @@ export default function ProcessInstanceListSaveAsReport({ }); }; + let textInputComponent = null; + if (!isEditMode()) { + textInputComponent = ( + setIdentifier(e.target.value)} + /> + ); + } + return (
- setIdentifier(e.target.value)} - /> - diff --git a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx index 250eb6a0..cb782c03 100644 --- a/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInstanceListTable.tsx @@ -7,7 +7,7 @@ import { } from 'react-router-dom'; // @ts-ignore -import { Filter, Close, AddAlt, AddFilled } from '@carbon/icons-react'; +import { Filter, Close, AddAlt } from '@carbon/icons-react'; import { Button, ButtonSet, @@ -27,6 +27,7 @@ import { Modal, ComboBox, TextInput, + FormLabel, // @ts-ignore } from '@carbon/react'; import { PROCESS_STATUSES, DATE_FORMAT, DATE_FORMAT_CARBON } from '../config'; @@ -93,7 +94,7 @@ export default function ProcessInstanceListTable({ autoReload = false, }: OwnProps) { const params = useParams(); - const [searchParams, setSearchParams] = useSearchParams(); + const [searchParams] = useSearchParams(); const navigate = useNavigate(); const [processInstances, setProcessInstances] = useState([]); @@ -175,16 +176,12 @@ export default function ProcessInstanceListTable({ function setProcessInstancesFromResult(result: any) { const processInstancesFromApi = result.results; setProcessInstances(processInstancesFromApi); - setReportMetadata(result.report_metadata); setPagination(result.pagination); setProcessInstanceFilters(result.filters); - // TODO: need to iron out this interaction some more - if (result.report_identifier !== 'default') { - setProcessInstanceReportSelection({ - id: result.report_identifier, - display_name: result.report_identifier, - }); + setReportMetadata(result.report.report_metadata); + if (result.report.id) { + setProcessInstanceReportSelection(result.report); } } function getProcessInstances() { @@ -206,14 +203,10 @@ export default function ProcessInstanceListTable({ queryParamString += `&user_filter=${userAppliedFilter}`; } - let reportIdentifierToUse: any = reportIdentifier; - - if (!reportIdentifierToUse) { - reportIdentifierToUse = searchParams.get('report_identifier'); - } - - if (reportIdentifierToUse) { - queryParamString += `&report_identifier=${reportIdentifierToUse}`; + if (searchParams.get('report_id')) { + queryParamString += `&report_id=${searchParams.get('report_id')}`; + } else if (reportIdentifier) { + queryParamString += `&report_identifier=${reportIdentifier}`; } Object.keys(dateParametersToAlwaysFilterBy).forEach( @@ -376,7 +369,7 @@ export default function ProcessInstanceListTable({ title="Perspective Saved" subtitle={`as '${ processInstanceReportSelection - ? processInstanceReportSelection.display_name + ? processInstanceReportSelection.identifier : '' }'`} kind="success" @@ -498,7 +491,7 @@ export default function ProcessInstanceListTable({ } if (processInstanceReportSelection) { - queryParamString += `&report_identifier=${processInstanceReportSelection.id}`; + queryParamString += `&report_id=${processInstanceReportSelection.id}`; } setErrorMessage(null); @@ -595,18 +588,17 @@ export default function ProcessInstanceListTable({ savedReport: boolean = false ) => { clearFilters(); - const selectedReport = selection.selectedItem; setProcessInstanceReportSelection(selectedReport); let queryParamString = ''; if (selectedReport) { - queryParamString = `&report_identifier=${selectedReport.id}`; + queryParamString = `?report_id=${selectedReport.id}`; } setErrorMessage(null); setProcessInstanceReportJustSaved(savedReport); - navigate(`/admin/process-instances?${queryParamString}`); + navigate(`/admin/process-instances${queryParamString}`); }; const reportColumns = () => { @@ -619,16 +611,17 @@ export default function ProcessInstanceListTable({ }); }; + // TODO onSuccess reload/select the new report in the report search + const onSaveReportSuccess = (result: any) => { + processInstanceReportDidChange( + { + selectedItem: result, + }, + true + ); + }; + const saveAsReportComponent = () => { - // TODO onSuccess reload/select the new report in the report search - const callback = (identifier: string) => { - processInstanceReportDidChange( - { - selectedItem: { id: identifier, display_name: identifier }, - }, - true - ); - }; const { valid, startFromSeconds, @@ -642,9 +635,10 @@ export default function ProcessInstanceListTable({ } return ( { + if (reportColumnToOperateOn) { + const reportColumnToOperateOnCopy = { + ...reportColumnToOperateOn, + }; + reportColumnToOperateOnCopy.condition_value = event.target.value; + setReportColumnToOperateOn(reportColumnToOperateOnCopy); + } + }; + const reportColumnForm = () => { if (reportColumnFormMode === '') { return null; @@ -732,6 +736,22 @@ export default function ProcessInstanceListTable({ }} />, ]; + if (reportColumnToOperateOn && reportColumnToOperateOn.filterable) { + console.log('reportColumnToOperateOn', reportColumnToOperateOn); + formElements.push( + + ); + } if (reportColumnFormMode === 'new') { formElements.push( +