chg: [workflow] Continued deleting unused code and improved UI

pull/8530/head
Sami Mokaddem 2022-06-07 15:19:48 +02:00
parent d180c2cc17
commit 52fc9c08c5
No known key found for this signature in database
GPG Key ID: 164C473F627A06FA
9 changed files with 30 additions and 398 deletions

View File

@ -253,37 +253,6 @@ class WorkflowsController extends AppController
return $this->response;
}
public function rearrangeExecutionOrder($trigger_id)
{
$trigger = $this->Workflow->getModuleByID($trigger_id);
if (empty($trigger)) {
throw new NotFoundException(__('Invalid trigger ID'));
}
$trigger = $this->Workflow->attachWorkflowToTriggers([$trigger])[0];
$workflow_order = [];
if (!empty($trigger['Workflows']['blocking'])) {
$workflow_order = Hash::extract($trigger['Workflows']['blocking'], '{n}.Workflow.id');
}
if ($this->request->is('post') || $this->request->is('put')) {
$workflow_order = array_unique(JsonTool::decode($this->request->data['Workflow']['workflow_order']));
$saved = $this->Workflow->saveBlockingWorkflowExecutionOrder($trigger['id'], $workflow_order);
$redirectTarget = ['action' => 'moduleView', $trigger_id];
if (empty($saved)) {
return $this->__getFailResponseBasedOnContext([__('Could not save workflow execution order.')], null, 'rearrangeExecutionOrder', $trigger_id, $redirectTarget);
} else {
$successMessage = __('Workflow execution order saved.');
return $this->__getSuccessResponseBasedOnContext($successMessage, $workflow_order, 'rearrangeExecutionOrder', false, $redirectTarget);
}
} else {
$this->request->data = [
'Workflow' => [
'workflow_order' => JsonTool::encode($workflow_order),
]
];
}
$this->set('trigger', $trigger);
}
private function __getSuccessResponseBasedOnContext($message, $data = null, $action = '', $id = false, $redirect = array())
{
if ($this->_isRest()) {

View File

@ -39,9 +39,9 @@ class Workflow extends AppModel
'rule' => ['hasAcyclicGraph'],
'message' => 'Cannot save a workflow containing a cycle',
],
'MoreThanOneTriggerInstance' => [
'rule' => ['MoreThanOneTriggerInstance'],
'message' => 'Cannot save a workflow containing more than one instance of the same trigger',
'hasOneTrigger' => [
'rule' => ['hasOneTrigger'],
'message' => 'Cannot save a workflow containing more than one trigger',
]
]
];
@ -59,7 +59,6 @@ class Workflow extends AppModel
const MODULE_ROOT_PATH = APP . 'Model/WorkflowModules/';
const REDIS_KEY_WORKFLOW_NAMESPACE = 'workflow';
const REDIS_KEY_WORKFLOW_PER_TRIGGER = 'workflow:workflow_list:%s';
const REDIS_KEY_WORKFLOW_ORDER_PER_BLOCKING_TRIGGER = 'workflow:workflow_blocking_order_list:%s';
const REDIS_KEY_TRIGGER_PER_WORKFLOW = 'workflow:trigger_list:%s';
public function __construct($id = false, $table = null, $ds = null)
@ -183,7 +182,6 @@ class Workflow extends AppModel
foreach ($trigger_to_remove as $trigger_id) {
$pipeline->sRem(sprintf(Workflow::REDIS_KEY_WORKFLOW_PER_TRIGGER, $trigger_id), $workflow['Workflow']['id']);
$pipeline->sRem(sprintf(Workflow::REDIS_KEY_TRIGGER_PER_WORKFLOW, $workflow['Workflow']['id']), $trigger_id);
$pipeline->lRem(sprintf(Workflow::REDIS_KEY_WORKFLOW_ORDER_PER_BLOCKING_TRIGGER, $trigger_id), $workflow['Workflow']['id'], 0);
}
$pipeline->exec();
}
@ -191,14 +189,11 @@ class Workflow extends AppModel
$pipeline = $redis->multi();
foreach ($trigger_to_add as $trigger_id) {
if (
$this->workflowGraphTool->triggerHasNonBlockingPath($new_node_trigger_list_per_id[$trigger_id])
|| $this->workflowGraphTool->triggerHasBlockingPath($new_node_trigger_list_per_id[$trigger_id])
$this->workflowGraphTool->triggerHasNonBlockingPath($new_node_trigger_list_per_id[$trigger_id]) ||
$this->workflowGraphTool->triggerHasBlockingPath($new_node_trigger_list_per_id[$trigger_id])
) {
$pipeline->sAdd(sprintf(Workflow::REDIS_KEY_WORKFLOW_PER_TRIGGER, $trigger_id), $workflow['Workflow']['id']);
$pipeline->sAdd(sprintf(Workflow::REDIS_KEY_TRIGGER_PER_WORKFLOW, $workflow['Workflow']['id']), $trigger_id);
if ($this->workflowGraphTool->triggerHasBlockingPath($new_node_trigger_list_per_id[$trigger_id])) {
$pipeline->rPush(sprintf(Workflow::REDIS_KEY_WORKFLOW_ORDER_PER_BLOCKING_TRIGGER, $trigger_id), $workflow['Workflow']['id']);
}
}
}
$pipeline->exec();
@ -222,22 +217,6 @@ class Workflow extends AppModel
return !empty($list) ? $list : [];
}
/**
* __getOrderedWorkflowsPerTrigger Get list of workflow IDs in the execution order for the specified trigger
*
* @param string $trigger_id
* @return bool|array
*/
private function __getOrderedWorkflowsPerTrigger($trigger_id)
{
try {
$redis = $this->setupRedisWithException();
} catch (Exception $e) {
return false;
}
return $redis->lRange(sprintf(Workflow::REDIS_KEY_WORKFLOW_ORDER_PER_BLOCKING_TRIGGER, $trigger_id), 0, -1);
}
/**
* __getTriggersIDPerWorkflow Get list of trigger name running to the specified workflow
*
@ -254,31 +233,6 @@ class Workflow extends AppModel
return $redis->sMembers(sprintf(Workflow::REDIS_KEY_TRIGGER_PER_WORKFLOW, $workflow_id));
}
/**
* saveBlockingWorkflowExecutionOrder Save the workflow execution order for the provided trigger
*
* @param string $trigger_id
* @param array $workflows List of workflow IDs in priority order
* @return bool
*/
public function saveBlockingWorkflowExecutionOrder($trigger_id, array $workflow_order): bool
{
try {
$redis = $this->setupRedisWithException();
} catch (Exception $e) {
$this->logException('Failed to setup redis ', $e);
return false;
}
$key = sprintf(Workflow::REDIS_KEY_WORKFLOW_ORDER_PER_BLOCKING_TRIGGER, $trigger_id);
$pipeline = $redis->multi();
$pipeline->del($key);
foreach ($workflow_order as $workflow_id) {
$pipeline->rpush($key, (string)$workflow_id);
}
$pipeline->exec();
return true;
}
/**
* attachWorkflowToTriggers Collect the workflows listening to this trigger
*
@ -287,37 +241,6 @@ class Workflow extends AppModel
*/
public function attachWorkflowToTriggers(array $triggers): array
{
// $all_workflow_ids = [];
// $workflows_per_trigger = [];
// $ordered_workflows_per_trigger = [];
// foreach ($triggers as $trigger) {
// $workflow_ids_for_trigger = $this->__getWorkflowsIDPerTrigger($trigger['id']);
// $workflows_per_trigger[$trigger['id']] = $workflow_ids_for_trigger;
// $ordered_workflows_per_trigger[$trigger['id']] = $this->__getOrderedWorkflowsPerTrigger($trigger['id']);
// foreach ($workflow_ids_for_trigger as $id) {
// $all_workflow_ids[$id] = true;
// }
// }
// $all_workflow_ids = array_keys($all_workflow_ids);
// $workflows = $this->fetchWorkflows([
// 'conditions' => [
// 'Workflow.id' => $all_workflow_ids,
// ],
// 'fields' => ['*'],
// ]);
// $workflows = Hash::combine($workflows, '{n}.Workflow.id', '{n}');
// foreach ($triggers as $i => $trigger) {
// $workflow_ids = $workflows_per_trigger[$trigger['id']];
// $ordered_workflow_ids = $ordered_workflows_per_trigger[$trigger['id']];
// $triggers[$i]['Workflows'] = [];
// foreach ($workflow_ids as $workflow_id) {
// $triggers[$i]['Workflows'][] = $workflows[$workflow_id];
// }
// if (!empty($group_per_blocking)) {
// $triggers[$i]['GroupedWorkflows'] = $this->groupWorkflowsPerBlockingType($triggers[$i]['Workflows'], $trigger['id'], $ordered_workflow_ids);
// }
// }
// return $triggers;
$workflows = $this->fetchWorkflows([
'conditions' => [
'Workflow.trigger_id' => Hash::extract($triggers, '{n}.id'),
@ -333,69 +256,6 @@ class Workflow extends AppModel
return $triggers;
}
public function fetchWorkflowsForTrigger($trigger_id, $filterDisabled=false): array
{
$workflow_ids_for_trigger = $this->__getWorkflowsIDPerTrigger($trigger_id);
$conditions = [
'Workflow.id' => $workflow_ids_for_trigger,
];
if (!empty($filterDisabled)) {
$conditions['Workflow.enabled'] = true;
}
$workflows = $this->fetchWorkflows([
'conditions' => $conditions,
'fields' => ['*'],
]);
return $workflows;
}
/**
* getExecutionOrderForTrigger Generate the execution order for the provided trigger
*
* @param array $trigger
* @return array
*/
public function getExecutionOrderForTrigger(array $trigger, $filterDisabled=false): array
{
if (empty($trigger)) {
return ['blocking' => [], 'non-blocking' => [] ];
}
$workflows = $this->fetchWorkflowsForTrigger($trigger['id'], $filterDisabled);
$ordered_workflow_ids = $this->__getOrderedWorkflowsPerTrigger($trigger['id']);
return $this->groupWorkflowsPerBlockingType($workflows, $trigger['id'], $ordered_workflow_ids);
}
/**
* groupWorkflowsPerBlockingType Group workflows together if they have a blocking path set or not. Also, sort the blocking list based on execution order
*
* @param array $workflows
* @param string $trigger_id The trigger for which we should decide if it's blocking or not
* @param array $ordered_workflow_ids If provided, will sort the blocking workflows based on the workflow_id order in of the provided list
* @return array
*/
public function groupWorkflowsPerBlockingType(array $workflows, $trigger_id, $ordered_workflow_ids=false): array
{
$groupedWorkflows = [
'blocking' => [],
'non-blocking' => [],
];
foreach ($workflows as $workflow) {
foreach ($workflow['Workflow']['data'] as $block) {
if ($block['data']['id'] == $trigger_id) {
if ($this->workflowGraphTool->triggerHasBlockingPath($block)) {
$order_index = array_search($workflow['Workflow']['id'], $ordered_workflow_ids);
$groupedWorkflows['blocking'][$order_index] = $workflow;
}
if ($this->workflowGraphTool->triggerHasNonBlockingPath($block)) {
$groupedWorkflows['non-blocking'][] = $workflow;
}
}
}
}
ksort($groupedWorkflows['blocking']);
return $groupedWorkflows;
}
/**
* isGraphAcyclic Return if the graph is acyclic or not
*
@ -411,23 +271,16 @@ class Workflow extends AppModel
}
/**
* MoreThanOneTriggerInstance Return if the graph contain more than one instance of the same trigger
* hasOneTrigger Return if the graph contain more than one instance of the same trigger
*
* @param array $graphData
* @return boolean
*/
public function MoreThanOneTriggerInstance(array $workflow): bool
public function hasOneTrigger(array $workflow): bool
{
$graphData = !empty($workflow['Workflow']) ? $workflow['Workflow']['data'] : $workflow['data'];
$triggers = $this->workflowGraphTool->extractTriggersFromWorkflow($graphData, true);
$visitedTriggerIDs = [];
foreach ($triggers as $trigger) {
if (!empty($visitedTriggerIDs[$trigger['data']['id']])) {
return false;
}
$visitedTriggerIDs[$trigger['data']['id']] = true;
}
return true;
return count($triggers) == 1;
}
/**
@ -452,24 +305,9 @@ class Workflow extends AppModel
}
$blockingPathExecutionSuccess = true;
$workflowExecutionOrder = $this->getExecutionOrderForTrigger($trigger, true);
$orderedBlockingWorkflows = $workflowExecutionOrder['blocking'];
$orderedDeferredWorkflows = $workflowExecutionOrder['non-blocking'];
foreach ($orderedBlockingWorkflows as $workflow) {
$walkResult = [];
$continueExecution = $this->walkGraph($workflow, $trigger_id, 'blocking', $data, $blockingErrors, $walkResult);
// $this->loadLog()->createLogEntry($this->User->getAuthUser($workflow['User']['id']), 'walkGraph', 'Workflow', $workflow['Workflow']['id'], __('Executed blocking path for trigger `%s`', $trigger_id), $this->digestExecutionResult($walkResult));
if (!$continueExecution) {
$blockingPathExecutionSuccess = false;
break;
}
}
foreach ($orderedDeferredWorkflows as $workflow) {
$deferredErrors = [];
$walkResult = [];
$this->walkGraph($workflow, $trigger_id, 'non-blocking', $data, $deferredErrors, $walkResult);
// $this->loadLog()->createLogEntry($this->User->getAuthUser($workflow['User']['id']), 'walkGraph', 'Workflow', $workflow['Workflow']['id'], __('Executed non-blocking path for trigger `%s`', $trigger_id), $this->digestExecutionResult($walkResult));
}
$workflow = $this->fetchWorkflowByTrigger($trigger, true);
$walkResult = [];
$this->walkGraph($workflow, $trigger_id, 'all', $data, $blockingErrors, $walkResult);
return $blockingPathExecutionSuccess;
}

View File

@ -8,7 +8,7 @@ class Module_parallel_task extends WorkflowBaseModule
public $description = 'Allow breaking the execution process and running parallel tasks. You can connect multiple blocks the `parallel` output.';
public $icon = 'random';
public $inputs = 1;
public $outputs = 2;
public $outputs = 1;
public $html_template = 'parallel';
public $params = [];

View File

@ -1,157 +0,0 @@
<div>
<h3 class="bold">
<i class="bold fa-fw <?= $this->FontAwesome->getClass('hourglass-start') ?>" style="font-size: larger;" title="<?= __('Blocking execution path') ?>"></i>
<?= __('Blocking Execution order') ?>
</h3>
<?php if (empty($trigger['GroupedWorkflows']['blocking'])) : ?>
<div class="alert alert-info">
<strong><?= __('No blocking workflows!') ?></strong>
<div><?= __('The trigger <strong>%s</strong> has no blocking workflows listening to it.', h($trigger['name'])) ?></div>
</div>
<?php else : ?>
<div id="container-unsaved-change" class="alert alert-info hidden">
<strong><?= __('Unsaved changes!') ?></strong>
<div><?= __('The execution order has changed and hasn\'t been saved.') ?></div>
<div style="margin-top: 1em;">
<button id="btn-save" class="btn btn-success">
<i class="fa-fw <?= $this->FontAwesome->getClass('save') ?>"></i> <?= __('Save') ?>
<span class="fa-fw fas fa-spin fa-spinner loading-icon hidden"></span>
</button>
<button id="btn-reset" class="btn btn-default"><?= __('Reset order') ?></button>
</div>
</div>
<div style="margin: 2em 1em;">
<ul id="workflows-sortable" class="unstyled">
<?php foreach ($trigger['GroupedWorkflows']['blocking'] as $i => $workflow) : ?>
<?= $this->element('Workflows/executionOrderWidgetLI', ['workflow' => $workflow]) ?>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
</div>
<?php
echo $this->element('genericElements/assetLoader', [
'js' => ['jquery-ui'],
]);
?>
<script>
$(function() {
$(document).ready(function() {
initialHtmlState = $('#workflows-sortable').html()
initialState = collectWorklowOrder()
initSortable()
$('#btn-reset').click(function() {
resetOrder(this)
})
$('#btn-save').click(function() {
saveOrder(this)
})
})
var initialHtmlState = false
var initialState = false
function initSortable() {
$('#workflows-sortable').sortable({
cursor: 'grabbing',
scroll: false,
update: function(event, ui) {
toggleUnsaveWarning()
}
})
}
function resetOrder(clicked) {
$('#workflows-sortable').html(initialHtmlState)
initSortable()
toggleUnsaveWarning()
}
function saveOrder(clicked) {
var workflow_order = collectWorklowOrder()
var url = "<?= $baseurl . '/workflows/rearrangeExecutionOrder/' . h($trigger['id']) ?>"
fetchFormDataAjax(url, function(formHTML) {
var $tmpForm = $(formHTML).find('form')
var formUrl = $tmpForm.attr('action')
$tmpForm.find('[name="data[Workflow][workflow_order]"]').val(JSON.stringify(workflow_order))
$.ajax({
data: $tmpForm.serialize(),
beforeSend: function() {
toggleLoadingInSaveButton(true)
},
success: function(result, textStatus) {
if (result) {
showMessage('success', result.message);
if (result.data !== undefined) {
initialState = result.data
}
}
},
error: function(jqXHR, textStatus, errorThrown) {
showMessage('fail', textStatus + ': ' + errorThrown);
},
complete: function() {
toggleLoadingInSaveButton(false)
window.location.reload()
},
type: "post",
url: formUrl
})
})
}
function collectWorklowOrder() {
var order = []
$('#workflows-sortable > li').each(function() {
var $li = $(this)
order.push($li.data('workflowid'))
})
return order;
}
function toggleUnsaveWarning() {
if (JSON.stringify(initialState) !== JSON.stringify(collectWorklowOrder())) {
$('#container-unsaved-change').show()
} else {
$('#container-unsaved-change').hide()
}
}
function toggleLoadingInSaveButton(saving) {
var $saveButton = $('#btn-save')
$saveButton.prop('disabled', saving)
if (saving) {
$saveButton.find('.loading-icon').show();
} else {
$saveButton.find('.loading-icon').hide();
}
}
})
</script>
<style>
#workflows-sortable > li {
display: flex;
width: 30%;
min-width: 350px;
border: 1px solid #ddd;
border-radius: 3px;
padding: 0.25em 0.5em;
margin-bottom: 0.5em;
cursor: grab;
font-size: larger;
}
#workflows-sortable > li > i.fas {
margin-right: 10px;
align-self: center;
}
#workflows-sortable > li > div {
flex-grow: 1;
overflow: hidden;
}
</style>

View File

@ -424,7 +424,7 @@
),
array(
'text' => __('Workflows'),
'url' => $baseurl . '/workflows/index',
'url' => $baseurl . '/workflows/triggers',
'requirement' => $isSiteAdmin
),
array(

View File

@ -9,9 +9,9 @@ $triggerModules = $modules['blocks_trigger'];
<div class="root-container">
<div class="main-container">
<div class="side-panel">
<a href="<?= $baseurl . '/workflows/index' ?>">
<a href="<?= $baseurl . '/workflows/triggers' ?>">
<i class="fa-fw <?= $this->FontAwesome->getClass('caret-left') ?>"></i>
<?= __('Workflow index') ?>
<?= __('Trigger index') ?>
</a>
<h3>
<span style="font-weight:normal;"><?= __('Workflows:') ?></span>

View File

@ -1,24 +0,0 @@
<?php
$modelForForm = 'Workflow';
echo $this->element('genericElements/Form/genericForm', [
'form' => $this->Form,
'data' => [
'title' => __('Rearrange Execution Order'),
'model' => $modelForForm,
'fields' => [
[
'field' => 'workflow_order',
'class' => 'span6',
'type' => 'textarea'
],
],
'submit' => [
'action' => $this->request->params['action'],
'ajaxSubmit' => sprintf('submitPopoverForm(\'%s\', \'rearrangeExecutionOrder\', 0, 1)', h($trigger['id']))
],
]
]);
if (empty($ajax)) {
echo $this->element('/genericElements/SideMenu/side_menu', array('menuList' => 'workflows', 'menuItem' => $this->request->params['action']));
}

View File

@ -15,7 +15,7 @@
'data_path' => 'description',
],
[
'name' => __('Module Enabled'),
'name' => __('Trigger Enabled'),
'sort' => 'disabled',
'class' => 'short',
'data_path' => 'disabled',

View File

@ -244,17 +244,21 @@
display: block;
}
.drawflow .drawflow-node.block-type-trigger > .outputs > .output::after {
color: white;
color: black;
font-family: 'Font Awesome 5 Free';
font-weight: 900;
position: absolute;
top: -4px;
width: 12px;
top: -3px;
width: 14px;
font-size: x-small;
text-align: center;
transition-property: width,top,font-size;
transition-duration: .1s
}
.drawflow .drawflow-node.block-type-trigger > .outputs > .output:hover {
width: 18px;
height: 18px;
}
.drawflow .drawflow-node.block-type-trigger > .outputs > .output:hover::after {
font-size: larger;
width: 18px;
@ -262,18 +266,20 @@
}
.drawflow .drawflow-node.block-type-trigger > .outputs > .output_1 {
background-color: #73a2c9;
background-color: #fff;
height: 14px;
width: 14px;
}
.drawflow .drawflow-node.block-type-trigger > .outputs > .output_1::before {
content: 'Listen';
content: 'Listen to this trigger';
top: -26px;
}
.drawflow .drawflow-node.block-type-trigger > .outputs > .output_1::after {
content: "\f025";
font-size: 9px;
content: "\f024";
font-size: 11px;
}
.drawflow .drawflow-node.block-type-trigger > .outputs > .output_1:hover::after {
font-size: 14px;
font-size: 15px;
}
.drawflow .drawflow-node.block-type-IF > .outputs > .output::before {