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) 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) 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)) start_in_seconds: float | None = db.Column(db.DECIMAL(17, 6))
end_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 = "", multi_instance_index: str = "",
process_identifier: str = "", process_identifier: str = "",
properties: dict | None = None, properties: dict | None = None,
runtime_info: dict | None = None,
process_instance_id: int | None = None, process_instance_id: int | None = None,
process_instance_status: str | None = None, process_instance_status: str | None = None,
process_model_display_name: str | None = None, process_model_display_name: str | None = None,
@ -297,6 +299,7 @@ class TaskSchema(Schema):
"form_schema", "form_schema",
"form_ui_schema", "form_ui_schema",
"event_definition", "event_definition",
"runtime_info",
] ]
multi_instance_type = EnumField(MultiInstanceType) multi_instance_type = EnumField(MultiInstanceType)

View File

@ -500,10 +500,14 @@ def process_instance_task_list(
task_models_of_parent_bpmn_processes, task_models_of_parent_bpmn_processes,
) = TaskService.task_models_of_parent_bpmn_processes(to_task_model) ) = 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] 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( task_model_query = task_model_query.filter(
or_( or_(
TaskModel.end_in_seconds <= to_task_model.end_in_seconds, # type: ignore 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.state,
TaskModel.end_in_seconds, TaskModel.end_in_seconds,
TaskModel.start_in_seconds, TaskModel.start_in_seconds,
TaskModel.runtime_info,
) )
) )
@ -554,6 +559,7 @@ def process_instance_task_list(
task_models = task_model_query.all() task_models = task_model_query.all()
if most_recent_tasks_only: if most_recent_tasks_only:
most_recent_tasks = {} most_recent_tasks = {}
additional_tasks = []
# if you have a loop and there is a subprocess, and you are going around for the second time, # 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 # ignore the tasks in the "first loop" subprocess
@ -573,9 +579,13 @@ def process_instance_task_list(
most_recent_tasks[row_key] = task_model most_recent_tasks[row_key] = task_model
if task_model.typename in ["SubWorkflowTask", "CallActivity"]: if task_model.typename in ["SubWorkflowTask", "CallActivity"]:
relevant_subprocess_guids.add(task_model.guid) 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_models = [
task_model 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 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 self.json_data_dicts[json_data_dict["hash"]] = json_data_dict
if python_env_dict is not None: if python_env_dict is not None:
self.json_data_dicts[python_env_dict["hash"]] = python_env_dict 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( def find_or_create_task_model_from_spiff_task(
self, self,

View File

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

View File

@ -23,9 +23,12 @@ import {
Warning, Warning,
} from '@carbon/icons-react'; } from '@carbon/icons-react';
import { import {
Accordion,
AccordionItem,
Grid, Grid,
Column, Column,
Button, Button,
ButtonSet,
Tag, Tag,
Modal, Modal,
Dropdown, Dropdown,
@ -1008,7 +1011,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
height={`${heightInEm}rem`} height={`${heightInEm}rem`}
width="auto" width="auto"
defaultLanguage="json" defaultLanguage="json"
defaultValue={taskDataToDisplay} value={taskDataToDisplay}
onChange={(value) => { onChange={(value) => {
setTaskDataToDisplay(value || ''); setTaskDataToDisplay(value || '');
}} }}
@ -1102,6 +1105,98 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
return dataArea; 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 = () => { const taskUpdateDisplayArea = () => {
if (!taskToDisplay) { if (!taskToDisplay) {
return null; return null;
@ -1133,6 +1228,18 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
dangerous = true; 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 ( return (
<Modal <Modal
open={!!taskToUse} open={!!taskToUse}
@ -1174,6 +1281,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
</div> </div>
) : null} ) : null}
{taskActionDetails()} {taskActionDetails()}
{multiInstanceSelector()}
</Modal> </Modal>
); );
}; };