Bug 1397436 - Improve actions.json actions editor

This commit is contained in:
Brian Stack 2017-09-06 13:22:07 -07:00 коммит произвёл Cameron Dawson
Родитель f0e92d27d7
Коммит 5c0c7eb5e2
12 изменённых файлов: 144 добавлений и 44 удалений

Просмотреть файл

@ -10,9 +10,11 @@
"node": "8.5.0"
},
"dependencies": {
"ajv": "^5.2.2",
"angular": "1.5.11",
"angular-cookies": "1.5.11",
"angular-local-storage": "0.5.2",
"angular-marked": "^1.2.2",
"angular-resource": "1.5.11",
"angular-route": "1.5.11",
"angular-sanitize": "1.5.11",
@ -29,6 +31,7 @@
"json-e": "2.2.1",
"json-schema-defaults": "0.3.0",
"lodash": "4.17.4",
"marked": "^0.3.6",
"metrics-graphics": "2.11.0",
"mousetrap": "1.6.1",
"neutrino": "4.3.1",

Просмотреть файл

@ -22,6 +22,7 @@ require('mousetrap');
require('js-yaml');
require('ngreact');
require('angular-ui-bootstrap');
require('angular-marked');
require('../../../ui/vendor/resizer.js');
const jsContext = require.context('../../../ui/js', true, /^\.\/.*\.jsx?$/);

Просмотреть файл

@ -352,3 +352,13 @@ kbd {
height: 1px;
padding: 0px;
}
/*
* Fonts
*
* Styles for modifying basic font styles
*/
.pre {
font-family: Menlo,Monaco,Consolas,"Courier New",monospace;
}

Просмотреть файл

@ -30,6 +30,7 @@ require('bootstrap/dist/js/bootstrap');
require('angular-ui-bootstrap');
require('mousetrap');
require('js-yaml');
require('angular-marked');
require('react-dom');
require('ngreact');
require('jquery.scrollto');

Просмотреть файл

@ -90,13 +90,13 @@ treeherderApp.controller('ResultSetCtrl', [
'thUrl', 'thServiceDomain', 'thResultStatusInfo', 'thDateFormat',
'ThResultSetStore', 'thEvents', 'thJobFilters', 'thNotify',
'thBuildApi', 'thPinboard', 'ThResultSetModel', 'dateFilter',
'ThModelErrors', 'ThJobModel', 'ThTaskclusterErrors',
'ThModelErrors', 'ThJobModel', 'ThTaskclusterErrors', '$uibModal',
function ResultSetCtrl(
$scope, $rootScope, $http, ThLog, $location,
thUrl, thServiceDomain, thResultStatusInfo, thDateFormat,
ThResultSetStore, thEvents, thJobFilters, thNotify,
thBuildApi, thPinboard, ThResultSetModel, dateFilter, ThModelErrors,
ThJobModel, ThTaskclusterErrors) {
ThJobModel, ThTaskclusterErrors, $uibModal) {
$scope.getCountClass = function (resultStatus) {
return thResultStatusInfo(resultStatus).btnClass;
@ -190,6 +190,23 @@ treeherderApp.controller('ResultSetCtrl', [
});
};
$scope.customPushAction = function () {
$uibModal.open({
templateUrl: 'partials/main/tcjobactions.html',
controller: 'TCJobActionsCtrl',
size: 'lg',
resolve: {
job: () => null,
repoName: function () {
return $scope.repoName;
},
resultsetId: function () {
return $scope.resultset.id;
}
}
});
};
$scope.triggerMissingJobs = function (revision) {
if (!window.confirm('This will trigger all missing jobs for revision ' + revision + '!\n\nClick "OK" if you want to proceed.')) {
return;

Просмотреть файл

@ -4,13 +4,15 @@ treeherder.controller('TCJobActionsCtrl', [
'$scope', '$http', '$uibModalInstance', 'ThResultSetStore',
'ThJobDetailModel', 'thTaskcluster', 'ThTaskclusterErrors',
'thNotify', 'job', 'repoName', 'resultsetId', 'tcactions',
'jsyaml', 'Ajv', 'jsonSchemaDefaults',
function ($scope, $http, $uibModalInstance, ThResultSetStore,
ThJobDetailModel, thTaskcluster, ThTaskclusterErrors, thNotify,
job, repoName, resultsetId, tcactions) {
let jsonSchemaDefaults = require('json-schema-defaults');
job, repoName, resultsetId, tcactions, jsyaml, Ajv, jsonSchemaDefaults) {
const ajv = new Ajv({ format: 'full', verbose: true, allErrors: true });
let decisionTaskId;
let originalTaskId;
let originalTask;
let validate;
$scope.input = {};
$scope.cancel = function () {
@ -19,9 +21,13 @@ treeherder.controller('TCJobActionsCtrl', [
$scope.updateSelectedAction = function () {
if ($scope.input.selectedAction.schema) {
$scope.input.jsonPayload = JSON.stringify(jsonSchemaDefaults($scope.input.selectedAction.schema), null, 4);
$scope.schema = jsyaml.safeDump($scope.input.selectedAction.schema);
$scope.input.payload = jsyaml.safeDump(jsonSchemaDefaults($scope.input.selectedAction.schema));
validate = ajv.compile($scope.input.selectedAction.schema);
} else {
$scope.input.jsonPayload = undefined;
$scope.input.payload = undefined;
$scope.schema = undefined;
validate = undefined;
}
};
@ -30,6 +36,23 @@ treeherder.controller('TCJobActionsCtrl', [
let tc = thTaskcluster.client();
let input = null;
if (validate && $scope.input.payload) {
try {
input = jsyaml.safeLoad($scope.input.payload);
} catch (e) {
$scope.triggering = false;
thNotify.send(`YAML Error: ${e.message}`, 'danger');
return;
}
const valid = validate(input);
if (!valid) {
$scope.triggering = false;
thNotify.send(ajv.errorsText(validate.errors), 'danger');
return;
}
}
let actionTaskId = tc.slugid();
tcactions.submit({
action: $scope.input.selectedAction,
@ -37,11 +60,23 @@ treeherder.controller('TCJobActionsCtrl', [
decisionTaskId,
taskId: originalTaskId,
task: originalTask,
input: $scope.input.jsonPayload ? JSON.parse($scope.input.jsonPayload) : undefined,
input,
staticActionVariables: $scope.staticActionVariables,
}).then(function () {
$scope.$apply(thNotify.send("Custom action request sent successfully", 'success'));
$scope.triggering = false;
let message = 'Custom action request sent successfully:';
let url = `https://tools.taskcluster.net/tasks/${actionTaskId}`;
// For the time being, we are redirecting specific actions to
// specific urls that are different than usual. At this time, we are
// only directing loaner tasks to the loaner UI in the tools site.
// It is possible that we may make this a part of the spec later.
const loaners = ['docker-worker-linux-loaner', 'generic-worker-windows-loaner'];
if (_.includes(loaners, $scope.input.selectedAction.name)) {
message = 'Visit Taskcluster Tools site to access loaner:';
url = `${url}/connect`;
}
$scope.$apply(thNotify.send(message, 'success', true, 'Open in Taskcluster', url));
$uibModalInstance.close('request sent');
}, function (e) {
$scope.$apply(thNotify.send(ThTaskclusterErrors.format(e), 'danger', true));

Просмотреть файл

@ -203,6 +203,16 @@ treeherder.factory('jsyaml', [
return require('js-yaml');
}]);
treeherder.factory('Ajv', [
function () {
return require('ajv');
}]);
treeherder.factory('jsonSchemaDefaults', [
function () {
return require('json-schema-defaults');
}]);
treeherder.factory('thExtendProperties', [
/* Version of _.extend that works with property descriptors */
function () {

Просмотреть файл

@ -17,6 +17,7 @@ treeherder.factory('tcactions', [
taskId,
task,
input,
ownTaskId: actionTaskId,
}, staticActionVariables));
return queue.task(decisionTaskId).then((decisionTask) => {
@ -39,48 +40,50 @@ treeherder.factory('tcactions', [
'public/actions.json'
);
return $http.get(actionsUrl).then((response) => {
let originalTaskId;
let originalTaskPromise = $q.resolve(null);
if (job) {
if (job.taskcluster_metadata) {
originalTaskId = job.taskcluster_metadata.task_id;
} else {
// This is a bbb job in this case. We'll try our best.
const match = job.reason.match(/Created by BBB for task (.{22})/);
if (match) {
originalTaskId = match[1];
}
}
originalTaskPromise = $http.get('https://queue.taskcluster.net/v1/task/' + originalTaskId).then(response => response.data);
}
return $q.all([
$http.get(actionsUrl),
originalTaskPromise,
]).then(([response, originalTask]) => {
if (!response.data) {
// This is a push with no actions.json so we should
// allow an implementer to fall back to actions.yaml
return null;
}
if (response.data.version !== 1) {
thNotify.send("Wrong version of actions.json, can't continue", "danger", true);
return;
}
let originalTaskPromise = $q.resolve(null);
let originalTaskId;
if (job) {
if (job.taskcluster_metadata) {
originalTaskId = job.taskcluster_metadata.task_id;
} else {
// This is a bbb job in this case. We'll try our best.
const match = job.reason.match(/Created by BBB for task (.{22})/);
if (match) {
originalTaskId = match[1];
}
}
originalTaskPromise = $http.get('https://queue.taskcluster.net/v1/task/' + originalTaskId).then(
function (response) {
return response.data;
});
}
return originalTaskPromise.then(originalTask => ({
// The filter in the value of the actions key is an implementation
// of the specification for action context in
// https://docs.taskcluster.net/manual/using/actions/spec#action-context
// It decides if the specific action is applicable for this task.
return {
originalTask,
originalTaskId,
staticActionVariables: response.data.variables,
actions: response.data.actions.filter(function (action) {
return action.kind === 'task' && (!originalTask || (
!action.context.length || _.some((action.context).map(function (actionContext) {
return !Object.keys(actionContext).length || _.every(_.map(actionContext, function (v, k) {
return (originalTask.tags[k] === v);
}));
}))));
}),
}));
actions: response.data.actions.filter(action => action.kind === 'task' &&
(!action.context.length && !originalTask) ||
originalTask && action.context.some(ctx => Object.keys(ctx).every(tag => (
originalTask.tags[tag] && originalTask.tags[tag] === ctx[tag]
)))
),
};
});
},
};

Просмотреть файл

@ -2,7 +2,8 @@
var treeherderApp = angular.module('treeherder.app',
['treeherder', 'ui.bootstrap', 'ngRoute',
'mc.resizer', 'angular-toArrayFilter', 'react']);
'mc.resizer', 'angular-toArrayFilter', 'react',
'hc.marked']);
treeherderApp.config(['$compileProvider', '$routeProvider', '$httpProvider',
'$logProvider', '$resourceProvider', 'localStorageServiceProvider',

Просмотреть файл

@ -10,13 +10,18 @@
<label>Action</label>
<select aria-describedby="selectedActionHelp" class="form-control" ng-model="input.selectedAction" ng-options="action.title for action in actions" ng-change="updateSelectedAction()">
</select>
<p id="selectedActionHelp" class="help-block">{{input.selectedAction.description}}</p>
<p id="selectedActionHelp" class="help-block" marked="input.selectedAction.description"></p>
</div>
<div class="form-group" ng-if="input.selectedAction.schema">
<label>JSON Payload</label>
<textarea ng-model="input.jsonPayload" class="form-control" rows="5" spellcheck=false/>
<div class="row">
<div class="col-s-12 col-md-6 form-group" ng-if="input.selectedAction.schema">
<label>Payload</label>
<textarea ng-model="input.payload" class="form-control pre" rows="10" spellcheck=false/>
</div>
<div class="col-s-12 col-md-6 form-group" ng-if="input.selectedAction.schema">
<label>Schema</label>
<textarea class="form-control pre" rows="10" readonly>{{schema}}</textarea>
</div>
</div>
{{input.selectedAction.jsonPayload}}
</div>
<div class="modal-footer">
<button ng-if="user.loggedin" class="btn btn-primary" ng-click="triggerAction()" ng-attr-title="{{user.loggedin ? 'Trigger this action' : 'Not logged in'}}" ng-disabled="triggering">

Просмотреть файл

@ -35,6 +35,10 @@
<li><a target="_blank" data-ignore-job-clear-on-click
href="https://bugherder.mozilla.org/?cset={{::resultset.revision}}&amp;tree={{::repoName}}"
title="Use Bugherder to mark the bugs in this push">Mark with Bugherder</a></li>
<li><a target="_blank" data-ignore-job-clear-on-click
href=""
ng-click="customPushAction()"
title="View/Edit/Submit Action tasks for this push">Custom Push Action...</a></li>
<li><a target="_blank" title="Prototype of a Treeherder UI centered on tests, rather than jobs"
href="{{testBasedUIHost}}?repo={{repoName}}&revision={{resultset.revision}}"><strong>Experimental:</strong> Test-Centric UI</a></li>
<li><a target="_blank"

Просмотреть файл

@ -56,7 +56,7 @@ ajv@^4.7.0, ajv@^4.9.1:
co "^4.6.0"
json-stable-stringify "^1.0.1"
ajv@^5.0.0:
ajv@^5.0.0, ajv@^5.2.2:
version "5.2.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39"
dependencies:
@ -98,6 +98,12 @@ angular-local-storage@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/angular-local-storage/-/angular-local-storage-0.5.2.tgz#7079beb0aa5ca91386d223125efefd13ca0ecd0c"
angular-marked@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/angular-marked/-/angular-marked-1.2.2.tgz#031f81015dad5fb01adf8d48922c52feb3588de5"
dependencies:
marked "^0.3.3"
angular-mocks@1.5.11:
version "1.5.11"
resolved "https://registry.yarnpkg.com/angular-mocks/-/angular-mocks-1.5.11.tgz#a0e1dd0ea55fd77ee7a757d75536c5e964c86f81"
@ -4316,6 +4322,10 @@ map-obj@^1.0.0, map-obj@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
marked@^0.3.3, marked@^0.3.6:
version "0.3.6"
resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.6.tgz#b2c6c618fccece4ef86c4fc6cb8a7cbf5aeda8d7"
math-expression-evaluator@^1.2.14:
version "1.2.17"
resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac"