You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by he...@apache.org on 2022/10/21 13:58:10 UTC
[brooklyn-ui] 01/24: ui for workflow on activities detail page
This is an automated email from the ASF dual-hosted git repository.
heneveld pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brooklyn-ui.git
commit da317de996accffe0af7e3b49f477b0fded6ec24
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Mon Oct 3 18:39:05 2022 +0100
ui for workflow on activities detail page
shows list of steps with arrows, details, references to tasks
---
.../entity-effector/entity-effector.less | 8 +-
.../components/providers/entity-api.provider.js | 14 +-
.../components/task-list/task-list.directive.js | 1 -
.../components/workflow/workflow-step.directive.js | 179 +++++++++++
.../workflow/workflow-step.template.html | 194 +++++++++++
.../workflow/workflow-steps.directive.js | 354 +++++++++++++++++++++
.../app/components/workflow/workflow-steps.less | 245 ++++++++++++++
.../workflow/workflow-steps.template.html | 35 ++
ui-modules/app-inspector/app/index.js | 3 +
ui-modules/app-inspector/app/index.less | 1 +
.../inspect/activities/detail/detail.controller.js | 32 +-
.../main/inspect/activities/detail/detail.less | 2 -
.../inspect/activities/detail/detail.template.html | 26 +-
ui-modules/shared/style/first.less | 5 +
14 files changed, 1087 insertions(+), 12 deletions(-)
diff --git a/ui-modules/app-inspector/app/components/entity-effector/entity-effector.less b/ui-modules/app-inspector/app/components/entity-effector/entity-effector.less
index 88872833..dd6eaf97 100644
--- a/ui-modules/app-inspector/app/components/entity-effector/entity-effector.less
+++ b/ui-modules/app-inspector/app/components/entity-effector/entity-effector.less
@@ -83,10 +83,10 @@
border-top-right-radius: 12px;
}
}
- .effector-succeeded { color: #363; }
- .effector-failed { color: #820; }
- .effector-cancelled { color: #660; }
- .effector-active { color: #6a2; }
+ .effector-succeeded { color: @color-succeeded; }
+ .effector-failed { color: @color-failed; }
+ .effector-cancelled { color: @color-cancelled; }
+ .effector-active { color: @color-active; }
}
}
}
\ No newline at end of file
diff --git a/ui-modules/app-inspector/app/components/providers/entity-api.provider.js b/ui-modules/app-inspector/app/components/providers/entity-api.provider.js
index f8d86e23..8ec4e2bd 100644
--- a/ui-modules/app-inspector/app/components/providers/entity-api.provider.js
+++ b/ui-modules/app-inspector/app/components/providers/entity-api.provider.js
@@ -70,8 +70,10 @@ function EntityApi($http, $q) {
startEntityAdjunct: startEntityAdjunct,
stopEntityAdjunct: stopEntityAdjunct,
destroyEntityAdjunct: destroyEntityAdjunct,
- updateEntityAdjunctConfig: updateEntityAdjunctConfig
-
+ updateEntityAdjunctConfig: updateEntityAdjunctConfig,
+
+ getWorkflows: getWorkflows,
+ getWorkflow: getWorkflow,
};
function getEntity(applicationId, entityId) {
@@ -187,5 +189,11 @@ function EntityApi($http, $q) {
}
function updateEntityAdjunctConfig(applicationId, entityId, adjunctId, configId, data) {
return $http.post('/v1/applications/'+ applicationId +'/entities/' + entityId + '/adjuncts/' + adjunctId + '/config/' + configId, data);
- }
+ }
+ function getWorkflows(applicationId, entityId) {
+ return $http.get('/v1/applications/'+ applicationId +'/entities/' + entityId + '/workflows/', {observable: true, ignoreLoadingBar: true});
+ }
+ function getWorkflow(applicationId, entityId, workflowId) {
+ return $http.get('/v1/applications/'+ applicationId +'/entities/' + entityId + '/workflow/' + workflowId, {observable: true, ignoreLoadingBar: true});
+ }
}
\ No newline at end of file
diff --git a/ui-modules/app-inspector/app/components/task-list/task-list.directive.js b/ui-modules/app-inspector/app/components/task-list/task-list.directive.js
index 457d0268..8f9ac4e3 100644
--- a/ui-modules/app-inspector/app/components/task-list/task-list.directive.js
+++ b/ui-modules/app-inspector/app/components/task-list/task-list.directive.js
@@ -190,7 +190,6 @@ function topLevelTasks(tasks) {
return tasks.filter(t => isTopLevelTask(t, tasksById));
}
-
export function timeAgoFilter() {
function timeAgo(input) {
return fromNow(input);
diff --git a/ui-modules/app-inspector/app/components/workflow/workflow-step.directive.js b/ui-modules/app-inspector/app/components/workflow/workflow-step.directive.js
new file mode 100644
index 00000000..9a322297
--- /dev/null
+++ b/ui-modules/app-inspector/app/components/workflow/workflow-step.directive.js
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import template from "./workflow-step.template.html";
+import angular from "angular";
+import jsyaml from 'js-yaml';
+
+const MODULE_NAME = 'inspector.workflow-step';
+
+angular.module(MODULE_NAME, [])
+ .directive('workflowStep', workflowStepDirective);
+
+export default MODULE_NAME;
+
+let count = 0;
+
+export function workflowStepDirective() {
+ return {
+ template: template,
+ restrict: 'E',
+ scope: {
+ workflow: '=',
+ task: '=?',
+ step: '<', // definition
+ stepIndex: '<',
+ expanded: '=',
+ onSizeChange: '=',
+ },
+ controller: ['$sce', '$scope', controller],
+ controllerAs: 'vm',
+ };
+
+ function controller($sce, $scope) {
+ try {
+ let vm = this;
+
+ let step = $scope.step;
+ let index = $scope.stepIndex;
+
+ vm.stepDetails = () => stepDetails($sce, $scope.workflow, step, index, $scope.expanded);
+ vm.toggleExpandState = () => {
+ $scope.expanded = !$scope.expanded;
+ if ($scope.onSizeChange) $scope.onSizeChange();
+ }
+ vm.stringify = stringify;
+ vm.yaml = (data) => jsyaml.dump(data);
+ vm.yamlOrPrimitive = (data) => typeof data === "string" ? data : vm.yaml(data);
+ vm.nonEmpty = (data) => data && (data.length || Object.keys(data).length);
+
+ $scope.json = null;
+ $scope.jsonMode = null;
+ vm.showJson = (mode, json) => {
+ $scope.jsonMode = mode;
+ $scope.json = json ? stringify(json) : null;
+ }
+
+ if (typeof step === 'string') {
+ $scope.stepPrefixClass = 'step-index';
+ $scope.stepPrefix = index + 1;
+ $scope.stepTitleDetail = step;
+ } else {
+
+ let shorthand = step.userSuppliedShorthand || step.s || step.shorthand;
+ $scope.stepTitleDetail = shorthand;
+ if (step.name) {
+ $scope.stepPrefixClass = 'step-name';
+ $scope.stepPrefix = step.name;
+ } else {
+ if (step.id) {
+ $scope.stepPrefixClass = 'step-id';
+ $scope.stepPrefix = step.id;
+ } else {
+ $scope.stepPrefixClass = 'step-index';
+ $scope.stepPrefix = index + 1;
+
+ if (!shorthand) {
+ $scope.stepTitleDetail = step.type || '';
+ if (step.input) $scope.stepTitleDetail += ' ...';
+ }
+ }
+ }
+ }
+
+ function updateData() {
+ let workflow = $scope.workflow;
+ workflow.data = workflow.data || {};
+ $scope.workflowStepClasses = [];
+ if (workflow.data.currentStepIndex === index) $scope.workflowStepClasses.push('current-step');
+
+ $scope.isCurrent = (workflow.data.currentStepIndex === index);
+ $scope.isRunning = (workflow.data.status === 'RUNNING');
+ $scope.isWorkflowError = (workflow.data.status && workflow.data.status.startsWith('ERROR'));
+ $scope.osi = workflow.data.oldStepInfo[index] || {};
+ $scope.stepContext = ($scope.isCurrent ? workflow.data.currentStepInstance : $scope.osi.context) || {};
+
+ $scope.isFocusStep = $scope.workflow.tag && ($scope.workflow.tag.stepIndex === index);
+ $scope.isFocusTask = false;
+
+ if ($scope.task) {
+ if ($scope.stepContext.taskId === $scope.task.id) {
+ $scope.isFocusTask = true;
+
+ } else if ($scope.isFocusStep) {
+ // TODO other instance of this tag selected
+ }
+ }
+ }
+ $scope.$watch('workflow', updateData);
+ updateData();
+
+ } catch (error) {
+ console.log("error showing workflow step", error);
+ // the ng-repeat seems to swallow and mask any error in the above - can't understand why! but log it here in case something breaks.
+ throw error;
+ }
+ }
+
+}
+
+function stepDetails($sce, workflow, step, index, expanded) {
+ let v;
+ if (typeof step === 'string') {
+ v = '<span class="step-index">'+_.escape(index+1)+'</span> ';
+ v += ' <span class="step-body">' + _.escape(step) + '</span>';
+ } else {
+ let shorthand = step.userSuppliedShorthand || step.s || step.shorthand;
+ if (step.name) {
+ v = '<span class="step-name">' + _.escape(step.name) + '</span>';
+ if (shorthand) {
+ v += ' <span class="step-body">' + _.escape(shorthand) + '</span>';
+ }
+ } else {
+ if (step.id) {
+ v = '<span class="step-id">' + _.escape(step.id) + '</span>';
+ if (shorthand) {
+ v += ' <span class="step-body">' + _.escape(shorthand) + '</span>';
+ }
+ } else {
+ v = '<span class="step-index">'+_.escape(index+1)+'</span> ';
+ if (shorthand) {
+ v += '<span class="step-body">' + _.escape(shorthand);
+ } else {
+ v += _.escape(step.type);
+ if (step.input) v += ' ...';
+ }
+ v += '</span>';
+ }
+ }
+ }
+ v = '<div class="step-block-title">'+v+'</div>';
+
+ if (expanded) {
+ v += '<br/>';
+ const oldStepInfo = (workflow.data.oldStepInfo || {})[index]
+ if (oldStepInfo) {
+ v += '<pre>' + _.escape(stringify(oldStepInfo)) + '</pre>';
+ } else {
+ v += _.escape("Step has not been run yet.");
+ }
+ }
+ return $sce.trustAsHtml(v);
+}
+
+function stringify(data) { return JSON.stringify(data, null, 2); }
diff --git a/ui-modules/app-inspector/app/components/workflow/workflow-step.template.html b/ui-modules/app-inspector/app/components/workflow/workflow-step.template.html
new file mode 100644
index 00000000..b3ac63c9
--- /dev/null
+++ b/ui-modules/app-inspector/app/components/workflow/workflow-step.template.html
@@ -0,0 +1,194 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+<div class="workflow-step-outer">
+
+ <div class="workflow-step-status-indicators">
+ <span ng-if="isCurrent">
+ <span ng-if="isRunning" class="running-status">
+ <brooklyn-status-icon value="STARTING"></brooklyn-status-icon>
+ </span>
+ </span>
+
+ <span ng-if="osi.countCompleted && osi.countStarted === osi.countStarted">
+ <span class="color-succeeded">
+ <i class="fa fa-check-circle"></i>
+ </span>
+<!-- <span ng-if="osi.countCompleted > 1">{{ osi.countCompleted }}</span>-->
+ </span>
+ <span ng-if="osi.countStarted && osi.countStarted != osi.countCompleted && !(isCurrent && isRunning)">
+ <span class="color-failed" ng-if="isWorkflowError">
+ <i class="fa fa-times-circle"></i>
+ </span>
+ <span class="color-cancelled" ng-if="!isWorkflowError">
+ <i class="fa fa-exclamation-circle"></i>
+ </span>
+ </span>
+ </div>
+
+ <div class="workflow-step" id="workflow-step-{{stepIindex}}" ng-class="vm.getWorkflowStepClasses(stepIndex)">
+ <div class="rhs-icons">
+ <div ng-if="isFocusTask" class="workflow-step-pill focus-step">
+ selected
+ </div>
+ <div ng-click="vm.toggleExpandState()" class="expand-toggle">
+ <i ng-class="expanded ? 'fa fa-chevron-up' : 'fa fa-chevron-down'"></i>
+ </div>
+ </div>
+
+ <div class="step-block-title">
+ <span ng-class="stepPrefixClass">{{ stepPrefix }}</span>
+ <span class="step-title-detail" ng-if="stepTitleDetail">{{ stepTitleDetail }}</span>
+ </div>
+
+ <div ng-if="expanded" class="step-details">
+ <div ng-if="osi.countStarted" class="space-above">
+ <div>
+ <span ng-if="osi.countCompleted == osi.countStarted">
+ <span ng-if="osi.countCompleted > 1">
+ This step has run
+ <span ng-if="osi.countCompleted == 2">
+ twice,
+ </span>
+ <span ng-if="osi.countCompleted > 2">
+ {{ osi.countCompleted }} times,
+ </span>
+ most recently
+ </span>
+ <span ng-if="osi.countCompleted == 1">
+ This step ran
+ </span>
+ </span>
+ <span ng-if="osi.countCompleted != osi.countStarted">
+ <span ng-if="isCurrent">
+ <span ng-if="osi.countCompleted == osi.countStarted - 1">
+ This step is currently running
+ </span>
+ <span ng-if="osi.countCompleted <= osi.countStarted - 2">
+ This step has had errors previously and is currently running
+ </span>
+ </span>
+ <span ng-if="!isCurrent">
+ <span ng-if="osi.countStarted == 1">
+ This step had errors when it ran
+ </span>
+ <span ng-if="osi.countStarted > 2 && osi.countCompleted==0">
+ This step has had errors on all previous runs, including when last run
+ </span>
+ <span ng-if="osi.countStarted > 2 && osi.countCompleted>0">
+ This step has had errors on some previous runs. It most recently ran
+ </span>
+ </span>
+ </span>
+
+ <span ng-if="isFocusTask">
+ in this task ({{ stepContext.taskId }}).
+ </span>
+ <span ng-if="!isFocusTask">
+ in <a ui-sref="main.inspect.activities.detail({applicationId: workflow.applicationId, entityId: workflow.entityId, activityId: stepContext.taskId })">Task {{ stepContext.taskId }}</a>.
+ </span>
+ </div>
+
+ <div ng-if="isFocusStep && !isFocusTask" class="space-above">
+ <b>The currently selected task ({{ task.id }}) is for a previous invocation of this step.</b>
+ </div>
+
+ <div class="more-space-above">
+ <div class="data-row" ng-if="step.name"><div class="A">Name</div> <div class="B">{{ step.name }}</div></div>
+ <div class="data-row" ng-if="step.id"><div class="A">ID</div> <div class="B fixed-width">{{ step.id }}</div></div>
+ <div class="data-row"><div class="A">Step Number</div> <div class="B">{{ stepIndex+1 }}</div></div>
+ <div class="data-row"><div class="A">Definition</div> <div class="B multiline-code">{{ vm.yamlOrPrimitive(step) }}</div></div>
+ </div>
+
+ <div ng-if="osi.countStarted > 1 && osi.countStarted > osi.countCompleted" class="space-above">
+ <div class="data-row"><div class="A">Runs</div> <div class="B"><b>{{ osi.countStarted }}</b></div></div>
+ <div class="data-row"><div class="A">Succeeded</div> <div class="B">{{ osi.countCompleted }}</div></div>
+ <div class="data-row"><div class="A">Failed</div> <div class="B">{{ osi.countCompleted - osi.countStarted - (isCurrent ? 1 : 0) }}</div></div>
+ </div>
+
+ <div class="more-space-above" ng-if="stepContext.taskId">
+ <div class="data-row">
+ <div class="A"><span ng-if="isCurrent">CURRENT</span><span ng-if="!isCurrent">LAST</span> EXECUTION</div>
+ <div class="B">
+ <span ng-if="isFocusTask">
+ Task {{ stepContext.taskId }}
+ </span>
+ <span ng-if="!isFocusTask">
+ <a ui-sref="main.inspect.activities.detail({applicationId: workflow.applicationId, entityId: workflow.entityId, activityId: stepContext.taskId })">Task {{ stepContext.taskId }}</a>
+ </span>
+ </div>
+ </div>
+ <div ng-if="!isFocusStep || isFocusTask">
+ <div class="data-row nested"><div class="A">Preceeded by</div> <div class="B">
+ <span ng-if="osi.previousTaskId">
+ Step {{ osi.previous[0]+1 }}
+ (<a ui-sref="main.inspect.activities.detail({applicationId: workflow.applicationId, entityId: workflow.entityId, activityId: osi.previousTaskId })"
+ >Task {{ osi.previousTaskId }}</a>)
+ </span>
+ <span ng-if="!osi.previousTaskId">(workflow start)</span>
+ </div></div>
+ <div class="data-row nested" ng-if="!isCurrent"><div class="A">Followed by</div> <div class="B">
+ <span ng-if="osi.nextTaskId">
+ Step {{ osi.next[0]+1 }}
+ (<a ui-sref="main.inspect.activities.detail({applicationId: workflow.applicationId, entityId: workflow.entityId, activityId: osi.nextTaskId })"
+ >Task {{ osi.nextTaskId }}</a>)
+ </span>
+ <span ng-if="!osi.nextTaskId">(workflow end)</span>
+ </div></div>
+
+ <div class="data-row nested" ng-if="osi.workflowScratch"><div class="A">Workflow Vars</div> <div class="B multiline-code">{{ vm.yaml(osi.workflowScratch) }}</div></div>
+ <div class="data-row nested" ng-if="stepContext.input"><div class="A">Input</div> <div class="B multiline-code">{{ vm.yaml(stepContext.input) }}</div></div>
+ <div class="data-row nested" ng-if="!isCurrent && stepContext.output"><div class="A">Output</div> <div class="B multiline-code">{{ vm.yaml(stepContext.output) }}</div></div>
+ </div>
+ </div>
+
+ </div>
+ <div ng-if="!osi.countStarted" class="space-above">
+ This step has not been run<span ng-if="isRunning"> yet</span>.
+ </div>
+
+ <div class="more-space-above" ng-if="vm.nonEmpty(stepContext) || vm.nonEmpty(step) || vm.nonEmpty(osi)">
+
+ <div class="btn-group right" uib-dropdown>
+ <button id="single-button" type="button" class="btn btn-select-dropdown pull-right" uib-dropdown-toggle>
+ View data <span class="caret"></span>
+ </button>
+ <ul class="dropdown-menu pull-right" uib-dropdown-menu role="menu" aria-labelledby="single-button">
+ <li role="menuitem" > <a href="" ng-click="vm.showJson('stepContext', stepContext)" ng-class="{'selected' : jsonMode === 'stepContext'}">
+ <i class="fa fa-check check"></i>
+ Last Execution Context</a> </li>
+ <li role="menuitem" > <a href="" ng-click="vm.showJson('osi', osi)" ng-class="{'selected' : jsonMode === 'osi'}">
+ <i class="fa fa-check check"></i>
+ Executions Record</a> </li>
+ <li role="menuitem" > <a href="" ng-click="vm.showJson('step', step)" ng-class="{'selected' : jsonMode === 'step'}">
+ <i class="fa fa-check check"></i>
+ Step Definition</a> </li>
+ <li role="menuitem" > <a href="" ng-click="vm.showJson(null)" ng-class="{'selected' : jsonMode === null}">
+ <i class="fa fa-check check"></i>
+ None</a> </li>
+ </ul>
+ </div>
+
+ <pre ng-if="json" class="space-above">{{ json }}</pre>
+ </div>
+ </div>
+
+ </div>
+
+</div>
+
diff --git a/ui-modules/app-inspector/app/components/workflow/workflow-steps.directive.js b/ui-modules/app-inspector/app/components/workflow/workflow-steps.directive.js
new file mode 100644
index 00000000..1d509ada
--- /dev/null
+++ b/ui-modules/app-inspector/app/components/workflow/workflow-steps.directive.js
@@ -0,0 +1,354 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import template from "./workflow-steps.template.html";
+import angular from "angular";
+
+const MODULE_NAME = 'inspector.workflow-steps';
+
+angular.module(MODULE_NAME, [])
+ .directive('workflowSteps', workflowStepsDirective);
+
+export default MODULE_NAME;
+
+export function workflowStepsDirective() {
+ return {
+ template: template,
+ restrict: 'E',
+ scope: {
+ workflow: '=',
+ task: '=?',
+ },
+ controller: ['$sce', '$timeout', '$scope', '$element', controller],
+ controllerAs: 'vm',
+ };
+
+ function controller($sce, $timeout, $scope, $element) {
+ let vm = this;
+ //console.log("controller for workflow steps", $scope.workflow);
+
+ vm.stringify = stringify;
+
+ vm.getWorkflowStepsClasses = () => {
+ const c = [];
+ c.push('workflow-status-'+$scope.workflow.data.status);
+ if ($scope.workflow.data.status && $scope.workflow.data.status.startsWith('ERROR')) {
+ c.push('workflow-error');
+ }
+ return c;
+ }
+
+ $scope.expandStates = {};
+ if ($scope.workflow.tag && $scope.workflow.tag.stepIndex) {
+ $scope.expandStates[$scope.workflow.tag.stepIndex] = true;
+ }
+
+ vm.onSizeChange = () => $timeout(()=>recompute($scope, $element));
+
+ $scope.$watch('workflow', vm.onSizeChange);
+ vm.onSizeChange();
+ }
+
+ function recompute($scope, $element) {
+ let svg = $element[0].querySelector('#workflow-step-arrows');
+
+ let steps = $element[0].querySelectorAll('div');
+ // let steps = $element[0].querySelectorAll('.workflow-steps-main');
+ steps = $element[0].querySelectorAll('.workflow-step');
+ let arrows = makeArrows($scope.workflow, steps);
+
+ svg.innerHTML = arrows.join('\n');
+ }
+}
+
+function makeArrows(workflow, steps) {
+ workflow = workflow || {};
+ workflow.data = workflow.data || {};
+
+ let [stepsPrev,stepsNext] = getWorkflowStepsPrevNext(workflow);
+
+ const arrows = [];
+ const strokeWidth = 1.5;
+ const arrowheadLength = 6;
+ const arrowheadWidth = arrowheadLength/3/strokeWidth;
+ const defs = [];
+
+ defs.push('<marker id="arrowhead" markerWidth="'+3*arrowheadWidth+'" markerHeight="'+3*arrowheadWidth+'" refX="'+0+'" refY="'+1.5*arrowheadWidth+'" orient="auto"><polygon fill="#000" points="0 0, '+3*arrowheadWidth+' '+1.5*arrowheadWidth+', 0 '+(3*arrowheadWidth)+'" /></marker><');
+ defs.push('<marker id="arrowhead-gray" markerWidth="'+3*arrowheadWidth+'" markerHeight="'+3*arrowheadWidth+'" refX="'+0+'" refY="'+1.5*arrowheadWidth+'" orient="auto"><polygon fill="#C0C0C0" points="0 0, '+3*arrowheadWidth+' '+1.5*arrowheadWidth+', 0 '+(3*arrowheadWidth)+'" /></marker><');
+
+ if (steps) {
+ let gradientCount = 0;
+ function arrowSvg(y1, y2, opts) {
+ var start = y1==='start/end';
+ var end = y2==='start/end';
+
+ if (y1==null || y2==null || (start&&end)) {
+ // ignore if out of bounds
+ return "";
+ }
+
+ if (!opts) opts = {};
+ const color = opts.color || (opts.colorEnd && opts.colorEnd==opts.colorStart ? opts.colorEnd : '#000');
+
+ const rightFarEdge = 56;
+ const rightArrowheadStart = rightFarEdge - arrowheadLength;
+ const leftFarEdge = 10;
+ const leftActive = rightArrowheadStart + (leftFarEdge - rightArrowheadStart) * (opts.width || 1);
+
+ const curveX = opts.curveX || 1;
+ const curveY = opts.curveY || 1;
+
+ // const controlPointRightFarEdge = rightFarEdge + (leftActive - rightFarEdge) * curveX;
+ const controlPointRightArrowheadStart = rightArrowheadStart + (leftActive - rightArrowheadStart) * curveX;
+ // average of above two, to see which works best
+ // const controlPointRightIntermediate = (rightFarEdge+rightArrowheadStart)/2 + (leftActive - (rightFarEdge+rightArrowheadStart)/2) * curveX;
+ // const controlPointRightExaggerated = rightArrowheadStart + (leftActive - rightFarEdge) * curveX;
+ const controlPointStart = controlPointRightArrowheadStart;
+ const controlPointEnd = controlPointRightArrowheadStart;
+
+ const strokeConstant =
+ 'stroke="'+color+'"';
+
+ let standard =
+ 'stroke-width="'+(opts.lineWidth || strokeWidth)+'" '+
+ 'fill="transparent" '+
+ '/>';
+ if (!opts.hideArrowhead) standard = 'marker-end="url(#'+(opts.arrowheadId || 'arrowhead')+'" ' +standard;
+ if (opts.dashLength) standard = 'stroke-dasharray="'+opts.dashLength+'" '+standard;
+
+ if (start) {
+ return '<path d="M ' + leftFarEdge + ' ' + y2 +
+ ' L ' + rightArrowheadStart + ' ' + y2 + '" '+
+ strokeConstant+' '+standard;
+ }
+ if (end) {
+ return '<path d="M ' + rightFarEdge + ' ' + y1 +
+ ' L ' + (leftFarEdge+arrowheadLength) + ' ' + y1 + '" '+
+ strokeConstant+' '+standard;
+ }
+
+ const yMCH = ((y2 - y1) / 2) * curveY;
+ const yM = (y1 + y2) / 2;
+
+ if (!opts.colorEnd || opts.colorEnd==opts.colorStart || y2==y1) {
+ standard = strokeConstant + ' ' + standard;
+ } else {
+ const gradientId = 'gradient'+(gradientCount++);
+ const gradY = y2>=y1 ? 'y2="1"' : 'y1="1"';
+ defs.push('<linearGradient id="'+gradientId+'" x2="0" '+gradY+'><stop offset="0" stop-color="'+opts.colorStart+'"/><stop offset="1" stop-color="'+opts.colorEnd+'"/></linearGradient>');
+ standard = 'stroke="url(#'+gradientId+')" ' + standard;
+ }
+
+ return '<path d="M ' + rightFarEdge + ' ' + y1 +
+ // ' L ' + r0 + ' ' + y1 + ' ' +
+ ' C ' + controlPointStart + ' ' + y1 + ', ' + leftActive + ' ' + (yM - yMCH) + ', ' + leftActive + ' ' + yM + ' ' +
+ ' S ' + controlPointEnd + ' ' + y2 + ', ' + rightArrowheadStart + ' ' + y2 + '" '+standard;
+ }
+
+ function stepY(n) {
+ if (n==-1) return 'start/end';
+ if (!steps || n<0 || n>=steps.length) {
+ console.log("workflow arrow bounds error", steps, n);
+ return null;
+ }
+ return steps[n].offsetTop + steps[n].offsetHeight / 2;
+ }
+
+ function arrowStep(n1, n2, opts) {
+ let s1 = stepY(n1);
+ let s2 = stepY(n2);
+
+ const deltaForArrowMax = 6;
+ const deltaForArrowTarget = 0.125;
+ if (typeof s1 === "number") s1 += Math.min(steps[n1].offsetHeight * deltaForArrowTarget, deltaForArrowMax);
+ if (typeof s2 === "number") s2 -= Math.min(steps[n2].offsetHeight * deltaForArrowTarget, deltaForArrowMax);
+ return arrowSvg(s1, s2, opts);
+ }
+
+ function colorFor(step, references) {
+ if (!references) return 'red';
+ const i = references.indexOf(step);
+ if (i==-1) return 'red';
+ // skew quadratically for lightness
+ const skewTowards1 = x => (1 - (1-x)*(1-x));
+ let gray = Math.round(240 * skewTowards1(i / references.length) );
+ return 'rgb('+gray+','+gray+','+gray+')';
+ }
+
+ let jumpSizes = {1: 0};
+ for (var i = -1; i < steps.length - 1; i++) {
+ const prevsHere = stepsPrev[i];
+ if (prevsHere && prevsHere.length) {
+ prevsHere.forEach(prev => {
+ if (i!=-1 && prev!=-1 && i!=prev) {
+ jumpSizes[Math.abs(prev - i)] = true;
+ }
+ });
+ }
+ }
+ jumpSizes = Object.keys(jumpSizes).sort();
+
+ function arrowStep2(prev, i, opts) {
+ let curveX = 0.5;
+ let curveY = 0.75;
+ let width = 0.5;
+ if (prev==-1 || i==-1) {
+ // curve values don't matter for start/end
+ } else if (prev==i) {
+ width = 0.15;
+ curveX = 0.1;
+ curveY = 0.75;
+ } else {
+ let rank = jumpSizes.indexOf(''+Math.abs(prev-i));
+ if (rank<0) {
+ console.log("Missing workflow link: ", prev, i);
+ rank = 0;
+ }
+ if (prev > i) rank = rank + 0.5;
+ width = 0.2 + 0.6 * (rank + 0.5) / (jumpSizes.length + 0.5);
+ // curveX = 0.8 + 0.2*width;
+ // curveY = 0.8 + 0.2*width;
+ // higher values (above) look nicer, but make disambiguation of complex paths harder
+ curveX = 0.5 + 0.3*width;
+ curveY = 0.4 + 0.4*width;
+ }
+ return arrowStep(prev, i, {hideArrowhead: prev==i, width, curveX, curveY, ...opts});
+ }
+
+ for (var i = -1; i < steps.length; i++) {
+ const prevsHere = stepsPrev[i];
+ if (prevsHere && prevsHere.length) {
+ let insertionPoint = 0;
+ prevsHere.forEach(prev => {
+ const colorStart = colorFor(i, stepsNext[prev]);
+ const colorEnd = colorFor(prev, prevsHere);
+
+ // last in list has higher z-order; this ensures within each prevStep we preserve order,
+ // so inbound arrows are correct. currently we also prefer earlier steps, which isn't quite right for outbound arrows;
+ // ideally we'd reconstruct the flow order, but that's a bit more work than we want to do just now.
+ // so insertion point is always 0. (header items added at end so we don't need to include those here.)
+ arrows.splice(insertionPoint, 0, arrowStep2(prev, i, { colorStart, colorEnd }));
+ });
+ }
+ }
+
+ // now make pale arrows for the default flow
+ var indexOfId = {};
+ for (var i = 0; i < steps.length; i++) {
+ const s = workflow.data.stepsDefinition[i];
+ if (s.id) {
+ indexOfId[s.id] = i;
+ }
+ }
+ if (steps.length>0) {
+ arrows.splice(0, 0, arrowStep2(-1, 0, {color: '#C0C0C0', arrowheadId: 'arrowhead-gray', dashLength: 8 }));
+ }
+ for (var i = 0; i < steps.length; i++) {
+ const s = workflow.data.stepsDefinition[i];
+ var next = null;
+ if (s.next) {
+ if (indexOfId[s.next]) {
+ next = indexOfId[s.next];
+ } else {
+ next = null;
+ }
+ } else {
+ if (s.type === 'return' || (s.userSuppliedShorthand && s.userSuppliedShorthand.startsWith("return"))) {
+ next = -1;
+ } else {
+ next = i + 1;
+ if (next >= steps.length) next = -1; //end
+ }
+ }
+ if (next!=null) arrows.splice(0, 0, arrowStep2(i, next, { color: '#C0C0C0', arrowheadId: 'arrowhead-gray', dashLength: 8 }));
+ }
+
+ // put defs at start
+ arrows.splice(0, 0, '<defs>'+defs.join('')+'</defs>');
+ }
+
+ return arrows;
+}
+
+function getWorkflowStepsPrevNext(workflow) {
+ let stepsPrev = {}
+ let stepsNext = {}
+
+ if (workflow && workflow.data.oldStepInfo) {
+ Object.entries(workflow.data.oldStepInfo).forEach(([k,v]) => {
+ stepsPrev[k] = v.previous || [];
+ stepsNext[k] = v.next || [];
+ });
+ }
+
+ // mock data
+ // // first in list is most recent
+ // stepsPrev = {
+ // '-1': [ 3 ],
+ // 0: [ -1 ],
+ // 1: [ 0 ],
+ // 2: [ 1 ],
+ // 3: [ 2 ],
+ // }
+ // stepsNext = {
+ // '-1': [ 0 ],
+ // 0: [ 1 ],
+ // 1: [ 2 ],
+ // 2: [ 3 ],
+ // 3: [ -1 ],
+ // }
+ //
+ // stepsPrev = {
+ // '-1': [ 2 ],
+ // 0: [ -1 ],
+ // 1: [ 1, 4, 0 ],
+ // 2: [ 3, 1 ],
+ // 3: [ 2 ],
+ // 4: [ 1 ],
+ // }
+ // stepsNext = {
+ // '-1': [ 0 ],
+ // 0: [ 1 ],
+ // 1: [ 2, 1, 4, 0 ],
+ // 2: [ -1, 3 ],
+ // 3: [ 2 ],
+ // 4: [ 1 ],
+ // }
+
+ // // even more complex
+ // stepsPrev = {
+ // '-1': [ 2 ],
+ // 0: [ 3, -1 ],
+ // 1: [ 1, 4, 0 ],
+ // 2: [ 3, 1 ],
+ // 3: [ 2, 0 ],
+ // 4: [ 1 ],
+ // }
+ // stepsNext = {
+ // '-1': [ 0 ],
+ // 0: [ 1, 3 ],
+ // 1: [ 2, 1, 4, 0 ],
+ // 2: [ -1, 3 ],
+ // 3: [ 2, 0 ],
+ // 4: [ 1 ],
+ // }
+
+ return [stepsPrev, stepsNext];
+}
+
+function stringify(data) { return JSON.stringify(data, null, 2); }
diff --git a/ui-modules/app-inspector/app/components/workflow/workflow-steps.less b/ui-modules/app-inspector/app/components/workflow/workflow-steps.less
new file mode 100644
index 00000000..0a9bd803
--- /dev/null
+++ b/ui-modules/app-inspector/app/components/workflow/workflow-steps.less
@@ -0,0 +1,245 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+.workflow-steps {
+
+ position: relative;
+
+ .workflow-steps-main {
+ position: relative;
+ }
+
+ .workflow-step {
+ margin-left: 60px;
+ margin-right: 60px;
+ margin-top: 12px;
+ margin-bottom: 12px;
+
+ padding-left: 10px;
+ padding-right: 10px;
+ padding-top: 5px;
+ padding-bottom: 5px;
+
+ border: solid @gray-light-lighter 1px;
+
+ // could do borders around active/error steps. but instead icons at left.
+ //&.current-step {
+ // border: solid @gray-light-lighter 2px;
+ // padding-left: 3px;
+ // padding-right: 3px;
+ //}
+ }
+ //&.workflow-status-RUNNING {
+ // .workflow-step.current-step {
+ // border: solid #363 2px;
+ // padding-left: 3px;
+ // padding-right: 3px;
+ // }
+ //}
+ //&.workflow-error {
+ // .workflow-step.current-step {
+ // border: solid #820 2px;
+ // padding-left: 3px;
+ // padding-right: 3px;
+ // }
+ //}
+
+ .workflow-step {
+ .rhs-icons {
+ float: right;
+ display: flex;
+ gap: 1ex;
+ .expand-toggle {
+ cursor: pointer;
+ }
+ }
+ .workflow-step-pill {
+ padding: 2px 6px;
+ border-radius: 12px;
+ background: @gray-lighter;
+ font-size: 75%;
+ &.focus-step {
+ background: @primary-100;
+ }
+ }
+ .step-block-title {
+ overflow-x: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ padding-right: 1.5ex;
+
+ .step-name, .step-id, .step-index {
+ padding-right: 1.5ex;
+ }
+
+ .step-name {
+ font-weight: 600;
+ font-size: 100%;
+ }
+
+ .step-id {
+ .monospace();
+ font-size: 85%;
+ font-weight: 600;
+ }
+
+ .step-index {
+ font-size: 90%;
+ //font-style: italic;
+ }
+
+ .step-title-detail {
+ .monospace();
+ font-size: 85%;
+ font-weight: 300;
+ color: @gray-light;
+ }
+ }
+ .step-details {
+ margin-top: 12px;
+ .space-above {
+ margin-top: 6px;
+ }
+ .more-space-above, .data-row.more-space-above {
+ margin-top: 12px;
+ }
+ .data-row {
+ display: flex;
+ margin-top: 3px;
+ margin-bottom: 3px;
+ .A {
+ flex: 0 0 auto;
+ width: 30%;
+ overflow: hidden;
+ white-space: nowrap;
+ color: @gray-light;
+ font-family: @font-family-monospace;
+ font-size: 85%;
+ text-transform: uppercase;
+ }
+ .B {
+ flex: 1 1 auto;
+ }
+
+ &.nested {
+ .A {
+ padding-left: 1em;
+ }
+ }
+ }
+ .btn-group.right {
+ width: 100%;
+
+ > .pull-right {
+ float: none;
+ }
+ > .dropdown-menu {
+ width: auto;
+ li a {
+ padding-left: 2em;
+ }
+ }
+
+ .selected {
+ .check {
+ margin-left: -1.5em;
+ display: block;
+ width: 0;
+ height: 0;
+ overflow: visible;
+ margin-top: 3px;
+ margin-bottom: -3px;
+ }
+ }
+ .check {
+ display: none;
+ }
+ }
+ }
+ }
+
+ .workflow-step-status-indicators {
+ //position: absolute;
+ //width: 60px;
+ //text-align: right;
+ //padding-right: 1ex;
+ //margin-top: 8px;
+ position: absolute;
+ width: 60px;
+ text-align: right;
+ padding-right: 1ex;
+ margin-top: 0;
+ display: flex;
+ gap: 6px;
+ margin-top: 3px;
+ height: 30px;
+ align-items: center;
+ justify-content: end;
+
+ .color-succeeded { color: @color-succeeded; }
+ .color-failed { color: @color-failed; }
+ .color-cancelled { color: @color-cancelled; }
+ .color-active { color: @color-active; }
+
+ .running-status {
+ svg {
+ //margin: 0 auto;
+ //background: none;
+ //display: block;
+ //
+ width: 18px;
+ height: 18px;
+ margin-top: 2px;
+ }
+ }
+
+ i.fa {
+ font-size: 20px;
+ }
+
+ //// same as entity-effector.less
+ //.effector-pill {
+ // background: @gray-lighter;
+ // padding: 2px 6px;
+ // &:first-child {
+ // padding-left: 10px;
+ // border-bottom-left-radius: 12px;
+ // border-top-left-radius: 12px;
+ // }
+ // &:last-child {
+ // padding-right: 10px;
+ // border-bottom-right-radius: 12px;
+ // border-top-right-radius: 12px;
+ // }
+ //}
+ }
+
+ .multiline-code {
+ white-space: pre;
+ overflow: scroll;
+ max-height: 100px;
+ }
+ .multiline-code, .fixed-width {
+ .monospace();
+ font-size: 85%;
+ }
+ .fixed-width {
+ overflow: hidden;
+ }
+
+}
diff --git a/ui-modules/app-inspector/app/components/workflow/workflow-steps.template.html b/ui-modules/app-inspector/app/components/workflow/workflow-steps.template.html
new file mode 100644
index 00000000..309ce4a2
--- /dev/null
+++ b/ui-modules/app-inspector/app/components/workflow/workflow-steps.template.html
@@ -0,0 +1,35 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+<div class="workflow-steps" ng-class="vm.getWorkflowStepsClasses()">
+
+ <svg width="100%" height="100%" style="position: absolute;">
+ <g transform="scale(-1,1)" transform-origin="center" id="workflow-step-arrows">
+ </g>
+ </svg>
+
+ <div style="position: relative;">
+ <div ng-if="workflow.data.stepsDefinition" class="workflow-steps-main">
+ <div ng-repeat="step in workflow.data.stepsDefinition track by $index" id="workflow-step-outer-{{$index}}">
+ <workflow-step workflow="workflow" task="task" step="step" step-index="$index" expanded="expandStates[$index]" on-size-change="vm.onSizeChange"></workflow-step>
+ </div>
+ </div>
+ </div>
+
+</div>
+
diff --git a/ui-modules/app-inspector/app/index.js b/ui-modules/app-inspector/app/index.js
index 219e842f..c82707b3 100755
--- a/ui-modules/app-inspector/app/index.js
+++ b/ui-modules/app-inspector/app/index.js
@@ -48,6 +48,8 @@ import taskList from "components/task-list/task-list.directive";
import taskSunburst from "components/task-sunburst/task-sunburst.directive";
import stream from "components/stream/stream.directive";
import adjunctsList from "components/adjuncts-list/adjuncts-list";
+import workflowSteps from "components/workflow/workflow-steps.directive";
+import workflowStep from "components/workflow/workflow-step.directive";
import {mainState} from "views/main/main.controller";
import {inspectState} from "views/main/inspect/inspect.controller";
import {summaryState, specToLabelFilter} from "views/main/inspect/summary/summary.controller";
@@ -69,6 +71,7 @@ angular.module('brooklynAppInspector', [ngResource, ngCookies, ngSanitize, uiRou
brServerStatus, brIconGenerator, brInterstitialSpinner, brooklynModuleLinks, brSensitiveField, brooklynUserManagement,
brYamlEditor, brWebNotifications, brExpandablePanel, 'xeditable', brLogbook, apiProvider, entityTree, loadingState,
configSensorTable, entityEffector, entityPolicy, breadcrumbNavigation, taskList, taskSunburst, stream, adjunctsList,
+ workflowSteps, workflowStep,
managementDetail, brandAngularJs])
.provider('catalogApi', catalogApiProvider)
.provider('apiObserverInterceptor', apiObserverInterceptorProvider)
diff --git a/ui-modules/app-inspector/app/index.less b/ui-modules/app-inspector/app/index.less
index 9988be2f..8e446770 100644
--- a/ui-modules/app-inspector/app/index.less
+++ b/ui-modules/app-inspector/app/index.less
@@ -44,6 +44,7 @@
@import "components/stream/stream.less";
@import "components/task-list/task-list.less";
@import "components/task-sunburst/task-sunburst.less";
+@import "components/workflow/workflow-steps.less";
@import "components/breadcrumb-navigation/breadcrumb-navigation.less";
@import "components/adjuncts-list/adjuncts-list.less";
diff --git a/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.controller.js b/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.controller.js
index d0e29f35..02388f97 100644
--- a/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.controller.js
+++ b/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.controller.js
@@ -42,7 +42,8 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
entityId: entityId,
activityId: activityId,
childFilter: {'EFFECTOR': true, 'SUB-TASK': false},
- accordion: {summaryOpen: true, subTaskOpen: true, streamsOpen: true}
+ accordion: {summaryOpen: true, subTaskOpen: true, streamsOpen: true, workflowOpen: true},
+ workflow: {},
};
vm.modalTemplate = modalTemplate;
@@ -67,6 +68,28 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
}
}
+ if ((vm.model.activity.tags || []).find(t => t=="WORKFLOW")) {
+ const workflowTag = findWorkflowTag(vm.model.activity);
+ if (workflowTag) {
+ vm.model.workflow.tag = workflowTag;
+ vm.model.workflow.loading = 'loading';
+ entityApi.getWorkflow(applicationId, entityId, workflowTag.workflowId).then(wResponse => {
+ vm.model.workflow.data = wResponse.data;
+ vm.model.workflow.loading = 'loaded';
+ vm.model.workflow.applicationId = applicationId;
+ vm.model.workflow.entityId = entityId;
+
+ observers.push(wResponse.subscribe((wResponse2)=> {
+ // change the workflow object so widgets get refreshed
+ vm.model.workflow = { ...vm.model.workflow, data: wResponse2.data };
+ }));
+ }).catch(error => {
+ console.log("ERROR loading workflow " + workflowTag.workflowId, error);
+ vm.model.workflow.loading = 'error';
+ });
+ }
+ }
+
vm.error = undefined;
observers.push(response.subscribe((response)=> {
vm.model.activity = response.data;
@@ -151,6 +174,7 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
};
vm.stringifyActivity = () => JSON.stringify(vm.model.activity, null, 2);
+ vm.stringify = (data) => JSON.stringify(data, null, 2);
vm.invokeEffector = (effectorName, effectorParams) => {
entityApi.invokeEntityEffector(applicationId, entityId, effectorName, effectorParams).then((response) => {
@@ -183,3 +207,9 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
}
}
+
+function findWorkflowTag(task) {
+ if (!task) return null;
+ if (!task.tags) return null;
+ return task.tags.find(t => t.workflowId);
+}
\ No newline at end of file
diff --git a/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.less b/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.less
index bdad7360..b6eb05e5 100644
--- a/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.less
+++ b/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.less
@@ -17,8 +17,6 @@
* under the License.
*/
-@gray-light-lighter: lighten(@gray-light, 20%);
-
.activity-detail {
.activity-header {
-webkit-font-smoothing: antialiased;
diff --git a/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.template.html b/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.template.html
index 0598078c..25eada16 100644
--- a/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.template.html
+++ b/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.template.html
@@ -157,10 +157,34 @@
</div>
</br-collapsible>
+ <br-collapsible state="vm.model.accordion.workflowOpen"
+ ng-if="vm.model.workflow">
+ <heading> Workflow</heading>
+
+ <div class="workflow-body">
+ <div ng-if="vm.model.workflow.loading == 'loaded'">
+ <p style="margin-top: 12px; margin-bottom: 24px;">
+ This task is for
+ <span ng-if="vm.model.workflow.tag.stepIndex">step <b>{{ vm.model.workflow.tag.stepIndex+1 }}</b>
+ in workflow
+ <a ui-sref="main.inspect.activities.detail({entityId: vm.model.entityId, activityId: vm.model.workflow.data.taskId})">
+ <b>{{vm.model.workflow.data.name}}</b>.
+ </a>
+ </span>
+ <span ng-if="!vm.model.workflow.tag.stepIndex"> workflow <b>{{vm.model.workflow.data.name}}</b>.</span>
+ </p>
+ <workflow-steps workflow="vm.model.workflow" task="vm.model.activity"></workflow-steps>
+ </div>
+ <div ng-if="vm.model.workflow.loading != 'loaded'">
+ <loading-state error="vm.model.workflow.loading !== 'loading' ? 'Details of this workflow are no longer available.' : ''"></loading-state>
+ </div>
+ </div>
+ </br-collapsible>
+
<br-collapsible state="vm.model.accordion.subTaskOpen"
ng-if="vm.model.activityChildren && vm.model.activityChildren.length > 0">
<heading> Sub-tasks</heading>
-
+
<div class="row">
<div ng-class="{ 'col-md-12': true, 'col-lg-8': !vm.wideKilt && vm.isNonEmpty(vm.model.activitiesDeep), 'col-lg-12': vm.wideKilt || !vm.isNonEmpty(vm.model.activitiesDeep)}">
<task-list tasks="vm.model.activityChildren" task-type="activityChildren" filtered-callback="vm.onFilteredActivitiesChange"></task-list>
diff --git a/ui-modules/shared/style/first.less b/ui-modules/shared/style/first.less
index 436e0c4c..faf640f7 100644
--- a/ui-modules/shared/style/first.less
+++ b/ui-modules/shared/style/first.less
@@ -28,6 +28,11 @@
@import '~font-awesome/less/font-awesome';
@navbar-divider-color: rgba(60, 85, 136, .5);
+@gray-light-lighter: lighten(@gray-light, 20%);
+@color-succeeded: #363;
+@color-failed: #820;
+@color-cancelled: #660;
+@color-active: #6a2;
.navbar-text {
float: left;