First pass at custom report/perspective for Process Instance List (#23)

This commit is contained in:
jbirddog 2022-11-08 09:26:42 -05:00 committed by GitHub
parent 96c26af365
commit fc7bf31670
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 152 additions and 161 deletions

View File

@ -1,8 +1,8 @@
"""empty message
Revision ID: b1647eff45c9
Revision ID: 7c12964efde1
Revises:
Create Date: 2022-11-02 14:25:09.992800
Create Date: 2022-11-08 07:48:44.265652
"""
from alembic import op
@ -10,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'b1647eff45c9'
revision = '7c12964efde1'
down_revision = None
branch_labels = None
depends_on = None
@ -115,19 +115,16 @@ def upgrade():
op.create_table('process_instance_report',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('identifier', sa.String(length=50), nullable=False),
sa.Column('process_model_identifier', sa.String(length=50), nullable=False),
sa.Column('process_group_identifier', sa.String(length=50), nullable=False),
sa.Column('report_metadata', sa.JSON(), nullable=True),
sa.Column('created_by_id', sa.Integer(), nullable=False),
sa.Column('created_at_in_seconds', sa.Integer(), nullable=True),
sa.Column('updated_at_in_seconds', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['created_by_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('process_group_identifier', 'process_model_identifier', 'identifier', name='process_instance_report_unique')
sa.UniqueConstraint('created_by_id', 'identifier', name='process_instance_report_unique')
)
op.create_index(op.f('ix_process_instance_report_created_by_id'), 'process_instance_report', ['created_by_id'], unique=False)
op.create_index(op.f('ix_process_instance_report_identifier'), 'process_instance_report', ['identifier'], unique=False)
op.create_index(op.f('ix_process_instance_report_process_group_identifier'), 'process_instance_report', ['process_group_identifier'], unique=False)
op.create_index(op.f('ix_process_instance_report_process_model_identifier'), 'process_instance_report', ['process_model_identifier'], unique=False)
op.create_table('refresh_token',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
@ -292,9 +289,8 @@ def downgrade():
op.drop_table('user_group_assignment')
op.drop_table('secret')
op.drop_table('refresh_token')
op.drop_index(op.f('ix_process_instance_report_process_model_identifier'), table_name='process_instance_report')
op.drop_index(op.f('ix_process_instance_report_process_group_identifier'), table_name='process_instance_report')
op.drop_index(op.f('ix_process_instance_report_identifier'), table_name='process_instance_report')
op.drop_index(op.f('ix_process_instance_report_created_by_id'), table_name='process_instance_report')
op.drop_table('process_instance_report')
op.drop_index(op.f('ix_process_instance_process_model_identifier'), table_name='process_instance')
op.drop_index(op.f('ix_process_instance_process_group_identifier'), table_name='process_instance')

View File

@ -761,20 +761,8 @@ paths:
schema:
$ref: "#/components/schemas/OkTrue"
/process-models/{process_group_id}/{process_model_id}/process-instances/reports:
/process-instances/reports:
parameters:
- name: process_group_id
in: path
required: true
description: The unique id of an existing process group
schema:
type: string
- name: process_model_id
in: path
required: true
description: The unique id of an existing process model.
schema:
type: string
- name: page
in: query
required: false
@ -814,20 +802,8 @@ paths:
schema:
$ref: "#/components/schemas/OkTrue"
/process-models/{process_group_id}/{process_model_id}/process-instances/reports/{report_identifier}:
/process-instances/reports/{report_identifier}:
parameters:
- name: process_group_id
in: path
required: true
description: The unique id of an existing process group
schema:
type: string
- name: process_model_id
in: path
required: true
description: The unique id of an existing process model.
schema:
type: string
- name: report_identifier
in: path
required: true

View File

@ -21,7 +21,6 @@ from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.services.process_instance_processor import (
ProcessInstanceProcessor,
)
from spiffworkflow_backend.services.process_model_service import ProcessModelService
ReportMetadata = dict[str, Any]
@ -58,8 +57,7 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel):
__tablename__ = "process_instance_report"
__table_args__ = (
db.UniqueConstraint(
"process_group_identifier",
"process_model_identifier",
"created_by_id",
"identifier",
name="process_instance_report_unique",
),
@ -67,21 +65,50 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel):
id = db.Column(db.Integer, primary_key=True)
identifier: str = db.Column(db.String(50), nullable=False, index=True)
process_model_identifier: str = db.Column(db.String(50), nullable=False, index=True)
process_group_identifier = 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)
created_by_id = db.Column(ForeignKey(UserModel.id), nullable=False, index=True)
created_by = relationship("UserModel")
created_at_in_seconds = db.Column(db.Integer)
updated_at_in_seconds = db.Column(db.Integer)
@classmethod
def default_report(cls, user: UserModel) -> ProcessInstanceReportModel:
"""Default_report."""
identifier = "default"
process_instance_report = ProcessInstanceReportModel.query.filter_by(
identifier=identifier, created_by_id=user.id
).first()
if process_instance_report is None:
report_metadata = {
"columns": [
{"Header": "id", "accessor": "id"},
{
"Header": "process_group_identifier",
"accessor": "process_group_identifier",
},
{
"Header": "process_model_identifier",
"accessor": "process_model_identifier",
},
{"Header": "start_in_seconds", "accessor": "start_in_seconds"},
{"Header": "end_in_seconds", "accessor": "end_in_seconds"},
{"Header": "status", "accessor": "status"},
],
}
process_instance_report = cls(
identifier=identifier,
created_by_id=user.id,
report_metadata=report_metadata,
)
return process_instance_report # type: ignore
@classmethod
def add_fixtures(cls) -> None:
"""Add_fixtures."""
try:
process_model = ProcessModelService().get_process_model(
group_id="sartography-admin", process_model_id="ticket"
)
user = UserModel.query.first()
columns = [
{"Header": "id", "accessor": "id"},
@ -96,29 +123,21 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel):
cls.create_report(
identifier="standard",
process_group_identifier=process_model.process_group_id,
process_model_identifier=process_model.id,
user=user,
report_metadata=json,
)
cls.create_report(
identifier="for-month",
process_group_identifier="sartography-admin",
process_model_identifier="ticket",
user=user,
report_metadata=cls.ticket_for_month_report(),
)
cls.create_report(
identifier="for-month-3",
process_group_identifier="sartography-admin",
process_model_identifier="ticket",
user=user,
report_metadata=cls.ticket_for_month_3_report(),
)
cls.create_report(
identifier="hot-report",
process_group_identifier="category_number_one",
process_model_identifier="process-model-with-form",
user=user,
report_metadata=cls.process_model_with_form_report_fixture(),
)
@ -130,23 +149,18 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel):
def create_report(
cls,
identifier: str,
process_group_identifier: str,
process_model_identifier: str,
user: UserModel,
report_metadata: ReportMetadata,
) -> None:
"""Make_fixture_report."""
process_instance_report = ProcessInstanceReportModel.query.filter_by(
identifier=identifier,
process_group_identifier=process_group_identifier,
process_model_identifier=process_model_identifier,
created_by_id=user.id,
).first()
if process_instance_report is None:
process_instance_report = cls(
identifier=identifier,
process_group_identifier=process_group_identifier,
process_model_identifier=process_model_identifier,
created_by_id=user.id,
report_metadata=report_metadata,
)
@ -217,19 +231,12 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel):
def create_with_attributes(
cls,
identifier: str,
process_group_identifier: str,
process_model_identifier: str,
report_metadata: dict,
user: UserModel,
) -> ProcessInstanceReportModel:
"""Create_with_attributes."""
process_model = ProcessModelService().get_process_model(
group_id=process_group_identifier, process_model_id=process_model_identifier
)
process_instance_report = cls(
identifier=identifier,
process_group_identifier=process_model.process_group_id,
process_model_identifier=process_model.id,
created_by_id=user.id,
report_metadata=report_metadata,
)

View File

@ -711,10 +711,29 @@ def process_instance_list(
ProcessInstanceModel.start_in_seconds.desc(), ProcessInstanceModel.id.desc() # type: ignore
).paginate(page=page, per_page=per_page, error_out=False)
process_instance_report = ProcessInstanceReportModel.default_report(g.user)
# TODO need to look into this more - how the filter here interacts with the
# one defined in the report.
# TODO need to look into test failures when the results from result_dict is
# used instead of the process instances
# substitution_variables = request.args.to_dict()
# result_dict = process_instance_report.generate_report(
# process_instances.items, substitution_variables
# )
# results = result_dict["results"]
# report_metadata = result_dict["report_metadata"]
results = process_instances.items
report_metadata = process_instance_report.report_metadata
response_json = {
"results": process_instances.items,
"report_metadata": report_metadata,
"results": results,
"pagination": {
"count": len(process_instances.items),
"count": len(results),
"total": process_instances.total,
"pages": process_instances.pages,
},
@ -762,27 +781,20 @@ def process_instance_delete(
def process_instance_report_list(
process_group_id: str, process_model_id: str, page: int = 1, per_page: int = 100
page: int = 1, per_page: int = 100
) -> flask.wrappers.Response:
"""Process_instance_report_list."""
process_model = get_process_model(process_model_id, process_group_id)
process_instance_reports = ProcessInstanceReportModel.query.filter_by(
process_group_identifier=process_group_id,
process_model_identifier=process_model.id,
created_by_id=g.user.id,
).all()
return make_response(jsonify(process_instance_reports), 200)
def process_instance_report_create(
process_group_id: str, process_model_id: str, body: Dict[str, Any]
) -> flask.wrappers.Response:
def process_instance_report_create(body: Dict[str, Any]) -> flask.wrappers.Response:
"""Process_instance_report_create."""
ProcessInstanceReportModel.create_report(
identifier=body["identifier"],
process_group_identifier=process_group_id,
process_model_identifier=process_model_id,
user=g.user,
report_metadata=body["report_metadata"],
)
@ -791,16 +803,13 @@ def process_instance_report_create(
def process_instance_report_update(
process_group_id: str,
process_model_id: str,
report_identifier: str,
body: Dict[str, Any],
) -> flask.wrappers.Response:
"""Process_instance_report_create."""
process_instance_report = ProcessInstanceReportModel.query.filter_by(
identifier=report_identifier,
process_group_identifier=process_group_id,
process_model_identifier=process_model_id,
created_by_id=g.user.id,
).first()
if process_instance_report is None:
raise ApiError(
@ -816,15 +825,12 @@ def process_instance_report_update(
def process_instance_report_delete(
process_group_id: str,
process_model_id: str,
report_identifier: str,
) -> flask.wrappers.Response:
"""Process_instance_report_create."""
process_instance_report = ProcessInstanceReportModel.query.filter_by(
identifier=report_identifier,
process_group_identifier=process_group_id,
process_model_identifier=process_model_id,
created_by_id=g.user.id,
).first()
if process_instance_report is None:
raise ApiError(
@ -877,25 +883,20 @@ def authentication_callback(
def process_instance_report_show(
process_group_id: str,
process_model_id: str,
report_identifier: str,
page: int = 1,
per_page: int = 100,
) -> flask.wrappers.Response:
"""Process_instance_list."""
process_model = get_process_model(process_model_id, process_group_id)
process_instances = (
ProcessInstanceModel.query.filter_by(process_model_identifier=process_model.id)
.order_by(
ProcessInstanceModel.start_in_seconds.desc(), ProcessInstanceModel.id.desc() # type: ignore
)
.paginate(page=page, per_page=per_page, error_out=False)
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(
page=page, per_page=per_page, error_out=False
)
process_instance_report = ProcessInstanceReportModel.query.filter_by(
identifier=report_identifier
identifier=report_identifier,
created_by_id=g.user.id,
).first()
if process_instance_report is None:
raise ApiError(

View File

@ -1352,13 +1352,11 @@ class TestProcessApi(BaseTest):
report_metadata = {"order_by": ["month"]}
ProcessInstanceReportModel.create_with_attributes(
identifier=report_identifier,
process_group_identifier=process_group_identifier,
process_model_identifier=process_model_identifier,
report_metadata=report_metadata,
user=with_super_admin_user,
)
response = client.get(
f"/v1.0/process-models/{process_group_identifier}/{process_model_identifier}/process-instances/reports",
"/v1.0/process-instances/reports",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
@ -1400,14 +1398,12 @@ class TestProcessApi(BaseTest):
ProcessInstanceReportModel.create_with_attributes(
identifier="sure",
process_group_identifier=test_process_group_id,
process_model_identifier=process_model_dir_name,
report_metadata=report_metadata,
user=with_super_admin_user,
)
response = client.get(
f"/v1.0/process-models/{test_process_group_id}/{process_model_dir_name}/process-instances/reports/sure",
"/v1.0/process-instances/reports/sure",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
@ -1438,9 +1434,6 @@ class TestProcessApi(BaseTest):
setup_process_instances_for_reports: list[ProcessInstanceModel],
) -> None:
"""Test_process_instance_report_show_with_default_list."""
test_process_group_id = "runs_without_input"
process_model_dir_name = "sample"
report_metadata = {
"filter_by": [
{
@ -1453,14 +1446,12 @@ class TestProcessApi(BaseTest):
ProcessInstanceReportModel.create_with_attributes(
identifier="sure",
process_group_identifier=test_process_group_id,
process_model_identifier=process_model_dir_name,
report_metadata=report_metadata,
user=with_super_admin_user,
)
response = client.get(
f"/v1.0/process-models/{test_process_group_id}/{process_model_dir_name}/process-instances/reports/sure?grade_level=1",
"/v1.0/process-instances/reports/sure?grade_level=1",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
@ -1476,11 +1467,8 @@ class TestProcessApi(BaseTest):
setup_process_instances_for_reports: list[ProcessInstanceModel],
) -> None:
"""Test_process_instance_report_show_with_default_list."""
test_process_group_id = "runs_without_input"
process_model_dir_name = "sample"
response = client.get(
f"/v1.0/process-models/{test_process_group_id}/{process_model_dir_name}/process-instances/reports/sure?grade_level=1",
"/v1.0/process-instances/reports/sure?grade_level=1",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 404

View File

@ -128,8 +128,6 @@ def do_report_with_metadata_and_instances(
"""Do_report_with_metadata_and_instances."""
process_instance_report = ProcessInstanceReportModel.create_with_attributes(
identifier="sure",
process_group_identifier=process_instances[0].process_group_identifier,
process_model_identifier=process_instances[0].process_model_identifier,
report_metadata=report_metadata,
user=BaseTest.find_or_create_user(),
)

View File

@ -49,6 +49,7 @@ export default function ProcessInstanceList() {
const navigate = useNavigate();
const [processInstances, setProcessInstances] = useState([]);
const [reportMetadata, setReportMetadata] = useState({});
const [pagination, setPagination] = useState<PaginationObject | null>(null);
const oneHourInSeconds = 3600;
@ -95,6 +96,7 @@ export default function ProcessInstanceList() {
function setProcessInstancesFromResult(result: any) {
const processInstancesFromApi = result.results;
setProcessInstances(processInstancesFromApi);
setReportMetadata(result.report_metadata);
setPagination(result.pagination);
}
function getProcessInstances() {
@ -378,53 +380,76 @@ export default function ProcessInstanceList() {
};
const buildTable = () => {
const rows = processInstances.map((row: any) => {
const formattedStartDate =
convertSecondsToFormattedDate(row.start_in_seconds) || '-';
const formattedEndDate =
convertSecondsToFormattedDate(row.end_in_seconds) || '-';
const headerLabels: Record<string, string> = {
id: 'Process Instance Id',
process_group_identifier: 'Process Group',
process_model_identifier: 'Process Model',
start_in_seconds: 'Start Time',
end_in_seconds: 'End Time',
status: 'Status',
spiff_step: 'SpiffWorkflow Step',
};
const getHeaderLabel = (header: string) => {
return headerLabels[header] ?? header;
};
const headers = (reportMetadata as any).columns.map((column: any) => {
return <th>{getHeaderLabel((column as any).Header)}</th>;
});
const formatProcessInstanceId = (row: any, id: any) => {
return (
<tr key={row.id}>
<td>
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${row.process_group_identifier}/${row.process_model_identifier}/process-instances/${row.id}`}
>
{row.id}
</Link>
</td>
<td>
<Link to={`/admin/process-groups/${row.process_group_identifier}`}>
{row.process_group_identifier}
</Link>
</td>
<td>
<Link
to={`/admin/process-models/${row.process_group_identifier}/${row.process_model_identifier}`}
>
{row.process_model_identifier}
</Link>
</td>
<td>{formattedStartDate}</td>
<td>{formattedEndDate}</td>
<td data-qa={`process-instance-status-${row.status}`}>
{row.status}
</td>
</tr>
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${row.process_group_identifier}/${row.process_model_identifier}/process-instances/${row.id}`}
>
{id}
</Link>
);
};
const formatProcessGroupIdentifier = (row: any, identifier: any) => {
return (
<Link to={`/admin/process-groups/${identifier}`}>{identifier}</Link>
);
};
const formatProcessModelIdentifier = (row: any, identifier: any) => {
return (
<Link
to={`/admin/process-models/${row.process_group_identifier}/${identifier}`}
>
{identifier}
</Link>
);
};
const formatSecondsForDisplay = (row: any, seconds: any) => {
return convertSecondsToFormattedDate(seconds) || '-';
};
const defaultFormatter = (row: any, value: any) => {
return value;
};
const columnFormatters: Record<string, any> = {
id: formatProcessInstanceId,
process_group_identifier: formatProcessGroupIdentifier,
process_model_identifier: formatProcessModelIdentifier,
start_in_seconds: formatSecondsForDisplay,
end_in_seconds: formatSecondsForDisplay,
};
const formattedColumn = (row: any, column: any) => {
const formatter = columnFormatters[column.accessor] ?? defaultFormatter;
const value = row[column.accessor];
return <td>{formatter(row, value)}</td>;
};
const rows = processInstances.map((row) => {
const currentRow = (reportMetadata as any).columns.map((column: any) => {
return formattedColumn(row, column);
});
return <tr key={(row as any).id}>{currentRow}</tr>;
});
return (
<Table size="lg">
<thead>
<tr>
<th>Process Instance Id</th>
<th>Process Group</th>
<th>Process Model</th>
<th>Start Time</th>
<th>End Time</th>
<th>Status</th>
</tr>
<tr>{headers}</tr>
</thead>
<tbody>{rows}</tbody>
</Table>

View File

@ -56,7 +56,7 @@ export default function ProcessInstanceReportEdit() {
};
function getProcessInstanceReport() {
HttpService.makeCallToBackend({
path: `/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/reports/${params.report_identifier}?per_page=1`,
path: `/process-instances/reports/${params.report_identifier}?per_page=1`,
successCallback: processResult,
});
}
@ -88,7 +88,7 @@ export default function ProcessInstanceReportEdit() {
.filter((n) => n);
HttpService.makeCallToBackend({
path: `/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/reports/${params.report_identifier}`,
path: `/process-instances/reports/${params.report_identifier}`,
successCallback: navigateToProcessInstanceReport,
httpMethod: 'PUT',
postBody: {
@ -103,7 +103,7 @@ export default function ProcessInstanceReportEdit() {
const deleteProcessInstanceReport = () => {
HttpService.makeCallToBackend({
path: `/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/reports/${params.report_identifier}`,
path: `/process-instances/reports/${params.report_identifier}`,
successCallback: navigateToProcessInstanceReports,
httpMethod: 'DELETE',
});

View File

@ -11,7 +11,7 @@ export default function ProcessInstanceReportList() {
useEffect(() => {
HttpService.makeCallToBackend({
path: `/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/reports`,
path: `/process-instances/reports`,
successCallback: setProcessInstanceReports,
});
}, [params]);

View File

@ -42,7 +42,7 @@ export default function ProcessInstanceReportNew() {
.filter((n) => n);
HttpService.makeCallToBackend({
path: `/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/reports`,
path: `/process-instances/reports`,
successCallback: navigateToNewProcessInstance,
httpMethod: 'POST',
postBody: {

View File

@ -39,7 +39,7 @@ export default function ProcessInstanceReport() {
}
});
HttpService.makeCallToBackend({
path: `/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/reports/${params.report_identifier}?${query}`,
path: `/process-instances/reports/${params.report_identifier}${query}`,
successCallback: processResult,
});
}