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

This commit is contained in:
jasquat 2022-12-02 10:32:40 -05:00
parent d1a69073a2
commit a4edb5d766
9 changed files with 261 additions and 133 deletions

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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},

View File

@ -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<string>(
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 = (
<TextInput
id="identifier"
name="identifier"
labelText="Identifier"
className="no-wrap"
inline
value={identifier}
onChange={(e: any) => setIdentifier(e.target.value)}
/>
);
}
return (
<Form onSubmit={addProcessInstanceReport}>
<Stack gap={5} orientation="horizontal">
<TextInput
id="identifier"
name="identifier"
labelText="Identifier"
className="no-wrap"
inline
value={identifier}
onChange={(e: any) => setIdentifier(e.target.value)}
/>
<Button disabled={!hasIdentifier()} size="sm" type="submit">
{textInputComponent}
<Button
disabled={!hasIdentifier()}
size="sm"
type="submit"
className={buttonClassName}
>
{buttonText}
</Button>
</Stack>

View File

@ -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 (
<ProcessInstanceListSaveAsReport
onSuccess={callback}
onSuccess={onSaveReportSuccess}
columnArray={reportColumns()}
orderBy=""
buttonText="Save As New Perspective"
processModelSelection={processModelSelection}
processStatusSelection={processStatusSelection}
startFromSeconds={startFromSeconds}
@ -710,6 +704,16 @@ export default function ProcessInstanceListTable({
);
};
const setReportColumnConditionValue = (event: any) => {
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(
<TextInput
id="report-column-condition-value"
name="report-column-condition-value"
labelText="Condition Value"
value={
reportColumnToOperateOn
? reportColumnToOperateOn.condition_value
: ''
}
onChange={setReportColumnConditionValue}
/>
);
}
if (reportColumnFormMode === 'new') {
formElements.push(
<ComboBox
@ -783,20 +803,24 @@ export default function ProcessInstanceListTable({
if (reportColumn.filterable) {
tagType = 'green';
}
let reportColumnLabel = reportColumn.Header;
if (reportColumn.condition_value) {
reportColumnLabel = `${reportColumnLabel}=${reportColumn.condition_value}`;
}
tags.push(
<Tag type={tagType} size="sm" title={reportColumn.accessor}>
<Tag type={tagType} size="sm">
<Button
kind="ghost"
size="sm"
className="button-tag-icon"
title="Edit Header"
title={`Edit ${reportColumn.accessor}`}
onClick={() => {
setReportColumnToOperateOn(reportColumn);
setShowReportColumnForm(true);
setReportColumnFormMode('edit');
}}
>
{reportColumn.Header}
{reportColumnLabel}
</Button>
<Button
data-qa="remove-report-column"
@ -841,6 +865,8 @@ export default function ProcessInstanceListTable({
<>
<Grid fullWidth className="with-bottom-margin">
<Column md={8} lg={16} sm={4}>
<FormLabel>Columns</FormLabel>
<br />
{columnSelections()}
</Column>
</Grid>
@ -1043,11 +1069,40 @@ export default function ProcessInstanceListTable({
const reportSearchComponent = () => {
if (showReports) {
const { startFromSeconds, startToSeconds, endFromSeconds, endToSeconds } =
calculateStartAndEndSeconds();
const columns = [
<Column sm={2} md={4} lg={7}>
<ProcessInstanceReportSearch
onChange={processInstanceReportDidChange}
selectedItem={processInstanceReportSelection}
/>
</Column>,
];
if (processInstanceReportSelection && showFilterOptions) {
columns.push(
<Column sm={2} md={4} lg={2}>
<ProcessInstanceListSaveAsReport
buttonClassName="with-tiny-top-margin"
onSuccess={onSaveReportSuccess}
columnArray={reportColumns()}
orderBy=""
buttonText="Save"
processModelSelection={processModelSelection}
processStatusSelection={processStatusSelection}
startFromSeconds={startFromSeconds}
startToSeconds={startToSeconds}
endFromSeconds={endFromSeconds}
endToSeconds={endToSeconds}
processInstanceReportSelection={processInstanceReportSelection}
/>
</Column>
);
}
return (
<ProcessInstanceReportSearch
onChange={processInstanceReportDidChange}
selectedItem={processInstanceReportSelection}
/>
<Grid className="with-tiny-bottom-margin" fullWidth>
{columns}
</Grid>
);
}
return null;

View File

@ -1,6 +1,8 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import {
ComboBox,
Stack,
FormLabel,
// @ts-ignore
} from '@carbon/react';
import { truncateString } from '../helpers';
@ -22,52 +24,59 @@ export default function ProcessInstanceReportSearch({
ProcessInstanceReport[] | null
>(null);
function setProcessInstanceReportsFromResult(result: any) {
const processInstanceReportsFromApi = result.map((item: any) => {
return { id: item.identifier, display_name: item.identifier };
});
setProcessInstanceReports(processInstanceReportsFromApi);
}
useEffect(() => {
function setProcessInstanceReportsFromResult(
result: ProcessInstanceReport[]
) {
setProcessInstanceReports(result);
}
if (processInstanceReports === null) {
setProcessInstanceReports([]);
HttpService.makeCallToBackend({
path: `/process-instances/reports`,
successCallback: setProcessInstanceReportsFromResult,
});
}
}, []);
const reportSelectionString = (
processInstanceReport: ProcessInstanceReport
) => {
return `${truncateString(processInstanceReport.identifier, 20)} (${
processInstanceReport.id
})`;
};
const shouldFilterProcessInstanceReport = (options: any) => {
const processInstanceReport: ProcessInstanceReport = options.item;
const { inputValue } = options;
return `${processInstanceReport.id} (${processInstanceReport.display_name})`.includes(
inputValue
);
return reportSelectionString(processInstanceReport).includes(inputValue);
};
const reportsAvailable = () => {
return processInstanceReports && processInstanceReports.length > 0;
};
return reportsAvailable() ? (
<ComboBox
onChange={onChange}
id="process-instance-report-select"
data-qa="process-instance-report-selection"
items={processInstanceReports}
itemToString={(processInstanceReport: ProcessInstanceReport) => {
if (processInstanceReport) {
return `${processInstanceReport.id} (${truncateString(
processInstanceReport.display_name,
20
)})`;
}
return null;
}}
shouldFilterItem={shouldFilterProcessInstanceReport}
placeholder="Choose a process instance perspective"
titleText={titleText}
selectedItem={selectedItem}
/>
) : null;
if (reportsAvailable()) {
return (
<Stack orientation="horizontal" gap={2}>
<FormLabel className="with-top-margin">{titleText}</FormLabel>
<ComboBox
onChange={onChange}
id="process-instance-report-select"
data-qa="process-instance-report-selection"
items={processInstanceReports}
itemToString={(processInstanceReport: ProcessInstanceReport) => {
if (processInstanceReport) {
return reportSelectionString(processInstanceReport);
}
return null;
}}
shouldFilterItem={shouldFilterProcessInstanceReport}
placeholder="Choose a process instance perspective"
selectedItem={selectedItem}
/>
</Stack>
);
}
return null;
}

View File

@ -177,6 +177,10 @@ h1.with-icons {
margin-bottom: 3em;
}
.with-tiny-bottom-margin {
margin-bottom: 4px;
}
.diagram-viewer-canvas {
border:1px solid #000000;
height:70vh;

View File

@ -63,9 +63,22 @@ export interface MessageInstance {
message_correlations?: MessageCorrelations;
}
export interface ReportColumn {
Header: string;
accessor: string;
filterable: boolean;
condition_value?: string;
}
export interface ReportMetadata {
columns: ReportColumn[];
}
export interface ProcessInstanceReport {
id: string;
display_name: string;
id: number;
identifier: string;
name: string;
report_metadata: ReportMetadata;
}
export interface ProcessGroupLite {
@ -152,9 +165,3 @@ export interface FormField {
type: string;
enum: string[];
}
export interface ReportColumn {
Header: string;
accessor: string;
filterable: boolean;
}