blob: a4f3f221f36105f7692de7c5bfb136c85a7036f6 [file] [log] [blame]
/*
* 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 {HIDE_INTERSTITIAL_SPINNER_EVENT} from 'brooklyn-ui-utils/interstitial-spinner/interstitial-spinner';
import template from "./detail.template.html";
import modalTemplate from './kilt.modal.template.html';
import {makeTaskStubFromWorkflowRecord} from "../activities.controller";
import jsyaml from 'js-yaml';
import runWorkflowModalTemplate from "../../run-workflow-modal.template.html";
import {runWorkflowController} from "../../inspect.controller";
export const detailState = {
name: 'main.inspect.activities.detail',
url: '/:activityId?workflowId?workflowLatestRun',
template: template,
controller: ['$scope', '$state', '$stateParams', '$location', '$log', '$uibModal', '$timeout', '$sanitize', '$sce', 'activityApi', 'entityApi', 'brUtilsGeneral', DetailController],
controllerAs: 'vm',
}
function DetailController($scope, $state, $stateParams, $location, $log, $uibModal, $timeout, $sanitize, $sce, activityApi, entityApi, Utils) {
$scope.$emit(HIDE_INTERSTITIAL_SPINNER_EVENT);
const {
applicationId,
entityId,
activityId,
} = $stateParams;
$scope.workflowId = $stateParams.workflowId;
let vm = this;
vm.redirectToWorkflowLatestRun = $stateParams.workflowLatestRun;
$stateParams.workflowLatestRun = null;
$location.search('workflowLatestRun', null)
vm.model = {
appId: applicationId,
entityId: entityId,
activityId: activityId,
childFilter: {'EFFECTOR': true, 'SUB-TASK': false},
accordion: {summaryOpen: true, subTaskOpen: true, streamsOpen: true, workflowOpen: true},
activity: {},
workflow: {},
};
vm.modalTemplate = modalTemplate;
vm.wideKilt = false;
vm.toggleOldWorkflowRunStepDetails = () => { $scope.showOldWorkflowRunStepDetails = !$scope.showOldWorkflowRunStepDetails; }
$scope.actions = {};
let observers = [];
if ($state.current.name === detailState.name) {
function onActivityOrWorkflowUpdate() {
delete $scope.actions['cancel'];
delete $scope.actions['delete'];
if (!vm.model.activity.endTimeUtc || vm.model.activity.endTimeUtc<=0) {
$scope.actions.cancel = { label: 'Cancel task', doAction: () => { activityApi.cancelActivity(activityId); } };
} else if (vm.model.workflow.data && vm.model.workflow.data.taskId && vm.model.workflow.data.status === 'RUNNING') {
$scope.actions.cancel = { label: 'Cancel workflow', doAction: () => { activityApi.cancelActivity(vm.model.workflow.taskId); } };
} else if (vm.model.workflow.data && vm.model.workflow.tag) {
$scope.actions.delete = { label: 'Delete workflow', doAction: () => {
entityApi.deleteWorkflow(vm.model.workflow.tag.applicationId || applicationId, vm.model.workflow.tag.entityId || entityId, $scope.workflowId)
.then(result => $state.go($state.current, {}, {reload: true}) );
} };
}
if (vm.model.activity.result!=undefined) {
vm.model.activity.resultYaml = vm.yaml(vm.model.activity.result);
const lines = vm.model.activity.resultYaml.split('\n');
vm.model.activity.resultLineCount = lines.length;
vm.model.activity.resultLineMaxLen = Math.max(...lines.map(x => x.length));
}
}
function loadWorkflow(workflowTag, opts) {
if (!opts) opts = {};
if (!workflowTag) {
workflowTag = {}
opts.optimistic = true;
}
const optimistic = opts.optimistic;
vm.model.workflow.loading = 'loading';
$scope.workflowId = workflowTag.workflowId || $scope.workflowId || activityId;
return entityApi.getWorkflow(workflowTag.applicationId || applicationId, workflowTag.entityId || entityId, $scope.workflowId).then(wResponse => {
$scope.workflowId = wResponse.data.workflowId;
workflowTag = {applicationId, entityId, workflowId: $scope.workflowId, ...workflowTag};
if (optimistic) {
vm.model.workflow.tag = workflowTag;
}
vm.model.workflow.data = wResponse.data;
vm.model.workflow.loading = 'loaded';
vm.model.workflow.applicationId = workflowTag.applicationId;
vm.model.workflow.entityId = workflowTag.entityId;
if (opts.nonTask) {
const wft = (vm.model.workflow.data.replays || []).find(t => t.taskId === activityId);
if (wft) {
vm.model.activity = makeTaskStubFromWorkflowRecord(vm.model.workflow.data, wft);
vm.model.workflow.tag = getTaskWorkflowTag(vm.model.activity);
} else {
throw "Workflow task " + activityId + " not stored on workflow";
}
// give a better error
vm.error = $sce.trustAsHtml('Limited information on workflow task <b>' + _.escape(activityId) + '</b>.<br/><br/>' +
(!vm.model.activity.endTimeUtc || vm.model.activity.endTimeUtc == -1
? "The run appears to have been interrupted, either by a server restart or a failure or cancellation and removal from memory."
: 'The workflow is known but this task is no longer stored in memory.'));
}
function processWorkflowData(wResponse2) {
// change the workflow object so widgets get refreshed
vm.model.workflow = { ...vm.model.workflow, data: wResponse2.data };
vm.model.workflow.isError = !!(vm.model.workflow.data.status && vm.model.workflow.data.status.startsWith("ERROR"));
const replays = (vm.model.workflow.data.replays || []);
vm.model.workflow.runMultipleTimes = replays.length > 1;
let workflowReplayId = activityId;
if (!replays.find(r => r.taskId === workflowReplayId)) {
let submittedById = ((vm.model.activity.submittedByTask || {}).metadata || {}).id;
if (replays.find(r => r.taskId === submittedById)) workflowReplayId = submittedById;
else workflowReplayId = null;
}
if (workflowReplayId) {
vm.model.workflow.runReplayId = workflowReplayId;
vm.model.workflow.runIsLatest = workflowReplayId == (replays[replays.length - 1] || {}).taskId;
vm.model.workflow.runIsOld = !vm.model.workflow.runIsLatest;
}
if (vm.model.workflow.runIsOld && vm.redirectToWorkflowLatestRun) {
vm.redirectToWorkflowLatestRun = false;
$state.go('main.inspect.activities.detail', {
applicationId: applicationId,
entityId: entityId,
activityId: (replays[replays.length - 1] || {}).taskId,
});
}
let osi = vm.model.workflow.data.oldStepInfo;
vm.model.workflow.finishedWithNoSteps = ((osi["-2"] || {}).previous || [])[0] == -1;
$scope.actions.workflowReplays = [];
if (vm.model.workflow.data.status !== 'RUNNING') {
$scope.actions.workflowReplays = [];
const stepIndex = (vm.model.workflow.tag || {}).stepIndex; // selected by user
const currentStepIndex = vm.model.workflow.data.currentStepIndex; // of workflow
let replayableDisabled = vm.model.workflow.data.replayableDisabled;
let replayableFromStart = vm.model.workflow.data.replayableFromStart && !replayableDisabled,
replayableFromLastStep = vm.model.workflow.data.replayableLastStep>=0 && !replayableDisabled;
if (replayableFromLastStep) {
let msg = 'Replay from last replay point (step '+(vm.model.workflow.data.replayableLastStep+1)+')';
$scope.actions.workflowReplays.push({ targetId: 'last', reason: msg+' from UI', label: msg });
}
// get current step, replay from that step
if (stepIndex>=0) {
const osi = vm.model.workflow.data.oldStepInfo[stepIndex] || {};
if (osi.replayableFromHere && !replayableDisabled) {
$scope.actions.workflowReplays.push({ targetId: ''+stepIndex, reason: 'Replay from step '+(stepIndex+1)+' from UI',
label: 'Replay from here (step '+(stepIndex+1)+')' });
} else {
$scope.actions.workflowReplays.push({ targetId: ''+stepIndex, reason: 'Force replay from step '+(stepIndex+1)+' from UI',
label: 'Force replay from here (step '+(stepIndex+1)+')', force: true });
}
}
if (replayableFromStart) {
let w1 = 'Replay from start', w2 = '(no other replay points)';
if (stepIndex<0 || (_.isNil(stepIndex) && vm.model.workflow.data.replayableLastStep==-2)) { w1 = 'Run'; w2 = 'again'; }
else if (replayableFromLastStep) w2 = '';
else if (_.isNil(stepIndex)) { w2 = '(did not start)'; }
$scope.actions.workflowReplays.push({targetId: 'start', reason: 'Replay from start from UI',
label: w1+' '+w2});
}
if (currentStepIndex>=0 && currentStepIndex < vm.model.workflow.data.stepsDefinition.length) {
let msg = 'eplay resuming (at step ' + (currentStepIndex + 1);
if (!replayableDisabled) {
$scope.actions.workflowReplays.push({ targetId: 'last', label: 'R'+msg+' if possible)', reason: 'R'+msg+') from UI' });
}
$scope.actions.workflowReplays.push({ targetId: 'last', label: 'Force r'+msg+')', reason: 'Force r'+msg+') from UI', force: true });
}
if (!replayableFromStart) {
$scope.actions.workflowReplays.push({targetId: 'start', reason: 'Force replay from start from UI',
label: 'Force replay from start', force: true});
}
// force replays
$scope.actions.workflowReplays.forEach(r => {
// could prompt for a reason
r.action = () => {
const opts = {};
opts.reason = r.reason;
if (r.force) opts.force = true;
entityApi.replayWorkflow(applicationId, entityId, $scope.workflowId, r.targetId, opts)
.then(response => {
console.log("Replay requested", response);
$state.go('main.inspect.activities.detail', {
applicationId: applicationId,
entityId: entityId,
activityId: response.data.id,
});
}).catch(error => {
console.log("Replay failed", error);
});
};
});
}
if (!$scope.actions.workflowReplays.length) delete $scope.actions['workflowReplays'];
onActivityOrWorkflowUpdate();
}
processWorkflowData(wResponse);
if (vm.model.workflow.data.status === 'RUNNING') wResponse.interval(1000);
observers.push(wResponse.subscribe(processWorkflowData, error => {
console.debug("Workflow no longer available, likely completed with retention 0. Removing from view.", error);
vm.model.workflow = {};
}));
function initFromWorkflowFirstReplayTask(task) {
if (task) {
const workflowYaml = (task.tags || []).find(t => t.workflow_yaml);
if (workflowYaml) {
$scope.workflow_yaml = workflowYaml.workflow_yaml;
}
}
}
if ($scope.workflowId === activityId) initFromWorkflowFirstReplayTask(vm.model.activity);
else activityApi.activity($scope.workflowId).then((response)=> {
initFromWorkflowFirstReplayTask(response.data);
});
}).catch(error => {
if (optimistic) {
vm.model.workflow.loading = null;
throw error;
}
console.log("Unable to load workflow", $scope.workflowId, error);
vm.model.workflow.loading = 'error';
});
};
activityApi.activity(activityId).then((response)=> {
vm.model.activity = response.data;
initializeBreadcrumbs(response.data);
delete $scope.actions['effector'];
delete $scope.actions['invokeAgain'];
if ((vm.model.activity.tags || []).find(t => t=="EFFECTOR")) {
const effectorName = (vm.model.activity.tags.find(t => t.effectorName) || {}).effectorName;
const effectorParams = (vm.model.activity.tags.find(t => t.effectorParams) || {}).effectorParams;
if (effectorName) {
$scope.actions.effector = {effectorName};
if (effectorParams) {
$scope.actions.invokeAgain = {effectorName, effectorParams, doAction: () => vm.invokeEffector(effectorName, effectorParams) };
}
}
}
$scope.workflowId = null; // if the task loads, force the workflow id to be found on it, otherwise ignore it
if ((vm.model.activity.tags || []).find(t => t=="WORKFLOW")) {
const workflowTag = getTaskWorkflowTag(vm.model.activity);
if (workflowTag) {
vm.model.workflow.tag = workflowTag;
loadWorkflow(workflowTag);
}
const workflowYaml = vm.model.activity.tags.find(t => t.workflow_yaml);
if (workflowYaml) {
$scope.workflow_yaml = workflowYaml.workflow_yaml;
}
}
function saveActivity(response) {
vm.model.activity = response.data;
onActivityOrWorkflowUpdate();
vm.error = undefined;
vm.errorBasic = false;
}
saveActivity(response);
if (!vm.model.activity.endTimeUtc || vm.model.activity.endTimeUtc<0) response.interval(1000);
observers.push(response.subscribe(saveActivity));
}).catch((error)=> {
$log.warn('Error loading activity for '+activityId, error);
// prefer this simpler error message over the specific ones below
vm.errorBasic = true;
vm.error = $sce.trustAsHtml('Cannot load task with ID: <b>' + _.escape(activityId) + '</b> <br/><br/>' +
'The task is no longer stored in memory. Details may be available in logs.');
// in case it corresponds to a workflow and not a task, try loading as a workflow
loadWorkflow({workflowId: activityId}, { nonTask: true })
.catch(error => {
loadWorkflow(null, { nonTask: true })
.catch(error => {
$log.debug("ID "+activityId+"/"+$scope.workflowId+" does not correspond to workflow either", error);
});
});
});
function onActivityLoadUpdate() {
vm.model.activityChildrenAndDeep = [];
let seen = {};
if (vm.model.activityChildren) {
vm.model.activityChildren.forEach(t => {
vm.model.activityChildrenAndDeep.push(t);
seen[t.id] = true;
});
}
if (vm.model.activitiesDeep) {
Object.values(vm.model.activitiesDeep).forEach(t => {
if (!seen[t.id]) {
vm.model.activityChildrenAndDeep.push(t);
seen[t.id] = true;
}
});
}
}
$scope.breadcrumbsLoading = true;
$scope.breadcrumbs = [];
$scope.breadcrumbsExpanded = false;
function initializeBreadcrumbs(activity) {
$scope.breadcrumbs.unshift(activity);
if (activity.submittedByTask) {
activityApi.activity(activity.submittedByTask.metadata.id).then(response => {
initializeBreadcrumbs(response.data);
}).catch(e => {
console.warn("Error loading breadcrumbs", e);
$scope.breadcrumbsLoading = false;
});
} else {
$scope.breadcrumbsLoading = false;
}
}
vm.expandBreadcrumbs = () => {
$scope.breadcrumbsExpanded = true;
}
activityApi.activityChildren(activityId).then((response)=> {
vm.model.activityChildren = processActivityChildren(response.data);
vm.error = undefined;
onActivityLoadUpdate();
// could improve by making just one call for children+deep, or combining the results;
// but for now just read them both frequently
if (!vm.model.activity.endTimeUtc || vm.model.activity.endTimeUtc<0) response.interval(1000);
observers.push(response.subscribe((response)=> {
vm.model.activityChildren = processActivityChildren(response.data);
if (!vm.errorBasic) {
vm.error = undefined;
}
onActivityLoadUpdate();
}));
}).catch((error)=> {
$log.warn('Error loading activity children for '+activityId, error);
if (!vm.errorBasic) {
vm.error = 'Cannot load activity children for activity ID: ' + activityId;
}
});
activityApi.activityDescendants(activityId, 8, true).then((response)=> {
vm.model.activitiesDeep = response.data;
vm.error = undefined;
onActivityLoadUpdate();
if (!vm.model.activity.endTimeUtc || vm.model.activity.endTimeUtc<0) response.interval(1000);
observers.push(response.subscribe((response)=> {
vm.model.activitiesDeep = response.data;
if (!vm.errorBasic) {
vm.error = undefined;
}
onActivityLoadUpdate();
}));
}).catch((error)=> {
$log.warn('Error loading activity children deep for '+activityId, error);
if (!vm.errorBasic) {
vm.error = 'Cannot load activities children deep for activity ID: ' + activityId;
}
});
}
vm.isNonEmpty = Utils.isNonEmpty;
vm.yaml = (o) => typeof o === 'string' ? o : jsyaml.dump(o);
vm.openInRunModel = function (workflowYaml) {
$uibModal.open({
animation: true,
template: runWorkflowModalTemplate,
controller: ['$scope', '$http', '$uibModalInstance', 'serverApi', 'applicationId', 'entityId', 'workflowYaml', runWorkflowController],
size: 'lg',
resolve: {
applicationId: ()=>(applicationId),
entityId: ()=>(entityId),
workflowYaml: ()=>(workflowYaml),
}
}).result.then((closeData)=> {
$state.go('main.inspect.activities.detail', {
applicationId: applicationId,
entityId: closeData.entityId,
activityId: closeData.id
});
});
}
vm.getStreamIdsInOrder = function () {
// sort with known streams first, in preferred order
// also return just the IDs, setting a map
var knownMap = {env: null, stdin: null, stdout: null, stderr: null};
var otherMap = {};
for (let [name, s] of Object.entries(vm.model.activity.streams)) {
if (name in knownMap) {
knownMap[name] = s;
} else {
otherMap[name] = s;
}
}
// Do not display streams that are not initialized
for (let name in knownMap) {
if (knownMap[name] === null || knownMap[name] === undefined) {
delete knownMap[name];
}
}
$scope.streamsById = Object.assign({}, knownMap, otherMap);
return Object.keys($scope.streamsById);
};
vm.setWideKilt = function (newValue) {
vm.wideKilt = newValue;
// empirically delay of 100ms means it runs after the resize;
// seems there is no way to hook in to resize events so it is
// either this or a $scope.$watch with very low interval
$timeout(function() { $scope.$broadcast('resize') }, 100);
};
vm.stringify = (data) => JSON.stringify(data, null, 2);
vm.stringifiedSize = (data) => JSON.stringify(data).length;
vm.invokeEffector = (effectorName, effectorParams) => {
entityApi.invokeEntityEffector(applicationId, entityId, effectorName, effectorParams).then((response) => {
$state.go('main.inspect.activities.detail', {
applicationId: applicationId,
entityId: entityId,
activityId: response.data.id,
});
});
}
$scope.$on('$destroy', ()=> {
observers.forEach((observer)=> {
observer.unsubscribe();
});
});
function processActivityChildren(data) {
return data.map((child)=> {
if (child.submittedByTask && child.submittedByTask.metadata.id === activityId && child.tags.indexOf('SUB-TASK') === -1) {
child.tags.push("BACKGROUND-TASK");
}
return child;
});
}
vm.onFilteredActivitiesChange = function (newActivities, globalFilters) {
// this uses activity descendants api method which only uses TaskChildren,
// so transient tasks etc less relevant
}
vm.showReplayHelp = () => {
$scope.showReplayHelp = !$scope.showReplayHelp;
}
vm.isNullish = _.isNil;
vm.isEmpty = x => vm.isNullish(x) || (x.length==0) || (typeof x === 'object' && !Object.keys(x).length);
vm.isNonEmpty = x => !vm.isEmpty(x);
}
export function getTaskWorkflowTag(task) {
if (!task) return null;
if (!task.tags) return null;
return task.tags.find(t => t.workflowId);
}