multiinstance ui (#469)

* multiinstance ui

* fix black

* added migration merge file

---------

Co-authored-by: burnettk <burnettk@users.noreply.github.com>
Co-authored-by: jasquat <jasquat@users.noreply.github.com>
This commit is contained in:
Elizabeth Esswein 2023-09-07 14:00:09 -04:00 committed by GitHub
parent ef297b1eeb
commit 75e6007ef3
7 changed files with 182 additions and 3 deletions

View File

@ -0,0 +1,32 @@
"""empty message
Revision ID: 5579975401dd
Revises: 1073364bc015
Create Date: 2023-09-01 11:07:39.816184
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '5579975401dd'
down_revision = '1073364bc015'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('task', schema=None) as batch_op:
batch_op.add_column(sa.Column('runtime_info', sa.JSON(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('task', schema=None) as batch_op:
batch_op.drop_column('runtime_info')
# ### end Alembic commands ###

View File

@ -0,0 +1,24 @@
"""merging two heads
Revision ID: 57df21dc569d
Revises: 5579975401dd, f04cbd9f43ec
Create Date: 2023-09-07 13:54:23.061873
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '57df21dc569d'
down_revision = ('5579975401dd', 'f04cbd9f43ec')
branch_labels = None
depends_on = None
def upgrade():
pass
def downgrade():
pass

View File

@ -65,6 +65,7 @@ class TaskModel(SpiffworkflowBaseDBModel):
json_data_hash: str = db.Column(db.String(255), nullable=False, index=True)
python_env_data_hash: str = db.Column(db.String(255), nullable=False, index=True)
runtime_info: dict = db.Column(db.JSON)
start_in_seconds: float | None = db.Column(db.DECIMAL(17, 6))
end_in_seconds: float | None = db.Column(db.DECIMAL(17, 6))
@ -140,6 +141,7 @@ class Task:
multi_instance_index: str = "",
process_identifier: str = "",
properties: dict | None = None,
runtime_info: dict | None = None,
process_instance_id: int | None = None,
process_instance_status: str | None = None,
process_model_display_name: str | None = None,
@ -297,6 +299,7 @@ class TaskSchema(Schema):
"form_schema",
"form_ui_schema",
"event_definition",
"runtime_info",
]
multi_instance_type = EnumField(MultiInstanceType)

View File

@ -500,10 +500,14 @@ def process_instance_task_list(
task_models_of_parent_bpmn_processes,
) = TaskService.task_models_of_parent_bpmn_processes(to_task_model)
task_models_of_parent_bpmn_processes_guids = [p.guid for p in task_models_of_parent_bpmn_processes if p.guid]
if "instance" in to_task_model.runtime_info or "iteration" in to_task_model.runtime_info:
to_task_model_parent = [to_task_model.properties_json["parent"]]
else:
to_task_model_parent = []
task_model_query = task_model_query.filter(
or_(
TaskModel.end_in_seconds <= to_task_model.end_in_seconds, # type: ignore
TaskModel.guid.in_(task_models_of_parent_bpmn_processes_guids), # type: ignore
TaskModel.guid.in_(task_models_of_parent_bpmn_processes_guids + to_task_model_parent), # type: ignore
)
)
@ -545,6 +549,7 @@ def process_instance_task_list(
TaskModel.state,
TaskModel.end_in_seconds,
TaskModel.start_in_seconds,
TaskModel.runtime_info,
)
)
@ -554,6 +559,7 @@ def process_instance_task_list(
task_models = task_model_query.all()
if most_recent_tasks_only:
most_recent_tasks = {}
additional_tasks = []
# if you have a loop and there is a subprocess, and you are going around for the second time,
# ignore the tasks in the "first loop" subprocess
@ -573,9 +579,13 @@ def process_instance_task_list(
most_recent_tasks[row_key] = task_model
if task_model.typename in ["SubWorkflowTask", "CallActivity"]:
relevant_subprocess_guids.add(task_model.guid)
elif "instance" in task_model.runtime_info or "iteration" in task_model.runtime_info:
# This handles adding all instances of a MI and iterations of loop tasks
additional_tasks.append(task_model)
task_models = [
task_model
for task_model in most_recent_tasks.values()
for task_model in list(most_recent_tasks.values()) + additional_tasks
if task_model.bpmn_process_guid in relevant_subprocess_guids
]

View File

@ -276,6 +276,7 @@ class TaskService:
self.json_data_dicts[json_data_dict["hash"]] = json_data_dict
if python_env_dict is not None:
self.json_data_dicts[python_env_dict["hash"]] = python_env_dict
task_model.runtime_info = spiff_task.task_spec.task_info(spiff_task)
def find_or_create_task_model_from_spiff_task(
self,

View File

@ -86,6 +86,7 @@ export interface Task extends BasicTask {
event_definition?: EventDefinition;
saved_form_data?: any;
runtime_info?: any;
}
// Currently used like ApiTask in backend

View File

@ -23,9 +23,12 @@ import {
Warning,
} from '@carbon/icons-react';
import {
Accordion,
AccordionItem,
Grid,
Column,
Button,
ButtonSet,
Tag,
Modal,
Dropdown,
@ -1008,7 +1011,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
height={`${heightInEm}rem`}
width="auto"
defaultLanguage="json"
defaultValue={taskDataToDisplay}
value={taskDataToDisplay}
onChange={(value) => {
setTaskDataToDisplay(value || '');
}}
@ -1102,6 +1105,98 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
return dataArea;
};
const switchToTask = (taskId: string) => {
if (tasks) {
const task = tasks.find((t: Task) => t.guid === taskId);
if (task) {
setTaskToDisplay(task);
initializeTaskDataToDisplay(task);
}
}
};
const multiInstanceSelector = () => {
if (!taskToDisplay) {
return null;
}
const clickAction = (item: any) => {
return () => {
switchToTask(taskToDisplay.runtime_info.instance_map[item]);
};
};
const createButtonSet = (instances: string[]) => {
return (
<ButtonSet stacked>
{instances.map((v: any) => (
<Button kind="ghost" onClick={clickAction(v)}>
{v}
</Button>
))}
</ButtonSet>
);
};
if (
taskToDisplay.typename === 'ParallelMultiInstanceTask' ||
taskToDisplay.typename === 'SequentialMultiInstanceTask'
) {
let completedInstances = null;
if (taskToDisplay.runtime_info.completed.length > 0) {
completedInstances = createButtonSet(
taskToDisplay.runtime_info.completed
);
}
let runningInstances = null;
if (taskToDisplay.runtime_info.running.length > 0) {
runningInstances = createButtonSet(taskToDisplay.runtime_info.running);
}
let futureInstances = null;
if (taskToDisplay.runtime_info.future.length > 0) {
futureInstances = createButtonSet(taskToDisplay.runtime_info.future);
}
return (
<Accordion>
<AccordionItem title="Completed instances">
{completedInstances}
</AccordionItem>
<AccordionItem title="Running instances">
{runningInstances}
</AccordionItem>
<AccordionItem title="Future instances">
{futureInstances}
</AccordionItem>
</Accordion>
);
}
if (taskToDisplay.typename === 'StandardLoopTask') {
const buttons = [];
for (
let i = 0;
i < taskToDisplay.runtime_info.iterations_completed;
i += 1
)
buttons.push(
<Button kind="ghost" onClick={clickAction(i)}>
{i}
</Button>
);
let text = 'Loop iterations';
if (
typeof taskToDisplay.runtime_info.iterations_remaining !== 'undefined'
)
text += ` (${taskToDisplay.runtime_info.iterations_remaining} remaining)`;
return (
<div>
<div>{text}</div>
<div>{buttons}</div>
</div>
);
}
return null;
};
const taskUpdateDisplayArea = () => {
if (!taskToDisplay) {
return null;
@ -1133,6 +1228,18 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
dangerous = true;
}
if (typeof taskToUse.runtime_info.instance !== 'undefined') {
secondaryButtonText = 'Return to MultiInstance Task';
onSecondarySubmit = () => {
switchToTask(taskToUse.properties_json.parent);
};
} else if (typeof taskToUse.runtime_info.iteration !== 'undefined') {
secondaryButtonText = 'Return to Loop Task';
onSecondarySubmit = () => {
switchToTask(taskToUse.properties_json.parent);
};
}
return (
<Modal
open={!!taskToUse}
@ -1174,6 +1281,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
</div>
) : null}
{taskActionDetails()}
{multiInstanceSelector()}
</Modal>
);
};