updated template list to using new components

This commit is contained in:
John Mitchell 2018-01-11 11:20:12 -05:00
parent 815cd829e0
commit aea37654e2
No known key found for this signature in database
GPG Key ID: FE6A9B5BD4EB5C94
42 changed files with 1077 additions and 802 deletions

View File

@ -3,6 +3,7 @@ import atLibComponents from '~components';
import atLibModels from '~models';
import atFeaturesCredentials from '~features/credentials';
import atFeaturesTemplates from '~features/templates';
const MODULE_NAME = 'at.features';
@ -10,7 +11,8 @@ angular.module(MODULE_NAME, [
atLibServices,
atLibComponents,
atLibModels,
atFeaturesCredentials
atFeaturesCredentials,
atFeaturesTemplates
]);
export default MODULE_NAME;

View File

@ -0,0 +1,11 @@
import TemplatesStrings from './templates.strings';
import ListController from './list-templates.controller';
const MODULE_NAME = 'at.features.templates';
angular
.module(MODULE_NAME, [])
.controller('ListController', ListController)
.service('TemplatesStrings', TemplatesStrings);
export default MODULE_NAME;

View File

@ -0,0 +1,178 @@
function ListTemplatesController (model, strings, $state, $scope, rbacUiControlService, Dataset, $filter, Alert, InitiatePlaybookRun, Prompt, Wait, ProcessErrors) {
const vm = this || {}
const unifiedJobTemplate = model;
init();
function init() {
vm.strings = strings;
// TODO: add the permission based functionality to the base model
$scope.canAdd = false;
rbacUiControlService.canAdd("job_templates")
.then(function(params) {
$scope.canAddJobTemplate = params.canAdd;
});
rbacUiControlService.canAdd("workflow_job_templates")
.then(function(params) {
$scope.canAddWorkflowJobTemplate = params.canAdd;
});
$scope.$watchGroup(["canAddJobTemplate", "canAddWorkflowJobTemplate"], function() {
if ($scope.canAddJobTemplate || $scope.canAddWorkflowJobTemplate) {
$scope.canAdd = true;
} else {
$scope.canAdd = false;
}
});
$scope.list = {
iterator: 'template',
name: 'templates'
};
$scope.collection = {
basePath: 'unified_job_templates',
iterator: 'template'
};
$scope[`${$scope.list.iterator}_dataset`] = Dataset.data;
$scope[$scope.list.name] = $scope[`${$scope.list.iterator}_dataset`].results;
$scope.$on('updateDataset', function(e, dataset) {
$scope[`${$scope.list.iterator}_dataset`] = dataset;
$scope[$scope.list.name] = dataset.results;
});
}
// get modified date and user who modified it
vm.getModified = function(template) {
let val = "";
if (template.modified) {
val += $filter('longDate')(template.modified);
}
if (_.has(template, 'summary_fields.modified_by.username')) {
val += ` by <a href="/#/users/${template.summary_fields.modified_by.id}">${template.summary_fields.modified_by.username}</a>`;
}
if (val === "") {
val = undefined;
}
return val;
};
// get last ran date and user who ran it
vm.getRan = function(template) {
let val = "";
if (template.last_job_run) {
val += $filter('longDate')(template.last_job_run);
}
// TODO: when API gives back a user who last ran the job in summary fields, uncomment and
// update this code
// if (template && template.summary_fields && template.summary_fields.modified_by &&
// template.summary_fields.modified_by.username) {
// val += ` by <a href="/#/users/${template.summary_fields.modified_by.id}">${template.summary_fields.modified_by.username}</a>`;
// }
if (val === "") {
val = undefined;
}
return val;
};
// get pretified template type names from options
vm.templateTypes = unifiedJobTemplate.options('actions.GET.type.choices')
.reduce((acc, i) => {
acc[i[0]] = i[1];
return acc;
}, {});
// get if you should show the active indicator for the row or not
// TODO: edit indicator doesn't update when you enter edit route after initial load right now
vm.activeId = parseInt($state.params.job_template_id || $state.params.workflow_template_id);
// TODO: update to new way of launching job after mike opens his pr
vm.submitJob = function(template) {
if(template) {
if(template.type && (template.type === 'Job Template' || template.type === 'job_template')) {
InitiatePlaybookRun({ scope: $scope, id: template.id, job_type: 'job_template' });
}
else if(template.type && (template.type === 'Workflow Job Template' || template.type === 'workflow_job_template')) {
InitiatePlaybookRun({ scope: $scope, id: template.id, job_type: 'workflow_job_template' });
}
else {
var alertStrings = {
header: 'Error: Unable to determine template type',
body: 'We were unable to determine this template\'s type while launching.'
}
Alert(strings.get('ALERT', alertStrings));
}
} else {
var alertStrings = {
header: 'Error: Unable to launch template',
body: 'Template parameter is missing'
}
Alert(strings.get('ALERT', alertStrings));
}
};
// TODO: implement copy function
vm.copyTemplate = function(template) {
};
vm.deleteTemplate = function(template) {
var action = function() {
$('#prompt-modal').modal('hide');
Wait('start');
// TODO: The request url doesn't work here
unifiedJobTemplate.request('delete', template.id)
.then(() => {
let reloadListStateParams = null;
if($scope.templates.length === 1 && $state.params.template_search && !_.isEmpty($state.params.template_search.page) && $state.params.template_search.page !== '1') {
reloadListStateParams = _.cloneDeep($state.params);
reloadListStateParams.template_search.page = (parseInt(reloadListStateParams.template_search.page)-1).toString();
}
if (parseInt($state.params.template_id) === template.id) {
$state.go("^", reloadListStateParams, { reload: true });
} else {
$state.go('.', reloadListStateParams, {reload: true});
}
})
.catch(({data, status}) => {
ProcessErrors($scope, data, status, null, {
hdr: string.get('error.HEADER'),
msg: strings.get('error.CALL', {path: "" + unifiedJobTemplate.path + template.id, status})
});
})
.finally(function() {
Wait('stop');
});
};
let deleteModalBody = `<div class="Prompt-bodyQuery">${strings.get('deleteResource.CONFIRM', 'template')}</div>`;
Prompt({
hdr: strings.get('deleteResource.HEADER'),
resourceName: $filter('sanitize')(template.name),
body: deleteModalBody,
action: action,
actionText: 'DELETE'
});
};
}
ListTemplatesController.$inject = [
'resolvedModels',
'TemplatesStrings',
'$state',
'$scope',
'rbacUiControlService',
'Dataset',
'$filter',
'Alert',
'InitiatePlaybookRun',
'Prompt',
'Wait',
'ProcessErrors'
];
export default ListTemplatesController;

View File

@ -1,15 +1,24 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import ListController from './list-templates.controller';
const listTemplate = require('~features/templates/list.view.html');
import { N_ } from '../../src/i18n';
import { N_ } from '../../i18n';
function TemplatesResolve (UnifiedJobTemplate) {
return new UnifiedJobTemplate(['get', 'options']);
}
TemplatesResolve.$inject = [
'UnifiedJobTemplateModel'
];
export default {
name: 'templates',
route: '/templates',
ncyBreadcrumb: {
// TODO: this would be best done with our
// strings file pattern, but it's not possible to
// get a handle on this route within a DI based
// on the state tree generation as present in
// src/templates currently
label: N_("TEMPLATES")
},
data: {
@ -32,18 +41,13 @@ export default {
searchPrefix: 'template',
views: {
'@': {
controller: 'TemplatesListController',
templateProvider: function(TemplateList, generateList) {
let html = generateList.build({
list: TemplateList,
mode: 'edit'
});
html = generateList.wrapPanel(html);
return generateList.insertFormView() + html;
}
controller: ListController,
templateUrl: listTemplate,
controllerAs: 'vm'
}
},
resolve: {
resolvedModels: TemplatesResolve,
Dataset: ['TemplateList', 'QuerySet', '$stateParams', 'GetBasePath',
function(list, qs, $stateParams, GetBasePath) {
let path = GetBasePath(list.basePath) || GetBasePath(list.name);

View File

@ -0,0 +1,118 @@
<div ui-view="form"></div>
<at-panel>
<at-panel-heading hide-dismiss="true">
{{:: vm.strings.get('list.PANEL_TITLE') }}
<div class="at-Panel-headingTitleBadge" ng-show="template_dataset.count">
{{ template_dataset.count }}
</div>
</at-panel-heading>
<at-panel-body>
<div class="at-List-toolbar">
<smart-search
class="at-List-search"
django-model="templates"
base-path="unified_job_templates"
iterator="template"
list="list"
dataset="template_dataset"
collection="collection"
search-tags="searchTags"
query-set="querySet">
</smart-search>
<div class="at-List-toolbarAction" ng-show="canAdd">
<button
type="button"
class="at-List-toolbarActionButton at-Button--success"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false">
&#43; {{:: vm.strings.get('list.ADD_BUTTON_LABEL') }}
<span class="at-List-toolbarDropdownCarat"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu at-List-toolbarActionDropdownMenu">
<li>
<a ui-sref="templates.addJobTemplate">
{{:: vm.strings.get('list.ADD_DD_JT_LABEL') }}
</a>
</li>
<li>
<a ui-sref="templates.addWorkflowJobTemplate">
{{:: vm.strings.get('list.ADD_DD_WF_LABEL') }}
</a>
</li>
</ul>
</div>
</div>
<at-list results="templates">
<!-- TODO: implement resources are missing red indicator as present in mockup -->
<at-row ng-repeat="template in templates"
ng-class="{'at-Row--active': (template.id === vm.activeId)}"
template-id="{{ template.id }}">
<div class="at-Row-items">
<at-row-item
header-value="{{ template.name }}"
header-link="/#/templates/job_template/{{ template.id }}"
header-tag="{{ vm.templateTypes[template.type] }}"
ng-if="template.type === 'job_template'">
</at-row-item>
<at-row-item
header-value="{{ template.name }}"
header-link="/#/templates/workflow_job_template/{{ template.id }}"
header-tag="{{ vm.templateTypes[template.type] }}"
ng-if="template.type === 'workflow_job_template'">
</at-row-item>
<at-row-item
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_ACTIVITY') }}"
smart-status="template">
</at-row-item>
<at-row-item
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_INVENTORY') }}"
value="{{ template.summary_fields.inventory.name }}"
value-link="/#/inventories/inventory/{{ template.summary_fields.inventory.id }}">
</at-row-item>
<at-row-item
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_PROJECT') }}"
value="{{ template.summary_fields.project.name }}"
value-link="/#/projects/{{ template.summary_fields.project.id }}">
</at-row-item>
<!-- TODO: add see more for creds -->
<at-row-item
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_CREDENTIALS') }}"
tag-values="template.summary_fields.credentials"
tags-are-creds="true">
</at-row-item>
<at-row-item
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_MODIFIED') }}"
value="{{ vm.getModified(template) }}">
</at-row-item>
<at-row-item
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_RAN') }}"
value="{{ vm.getRan(template) }}">
</at-row-item>
<labels-list class="LabelList" show-delete="false" is-row-item="true">
</labels-list>
</div>
<div class="at-Row-actions">
<at-row-action icon="icon-launch" ng-click="vm.submitJob(template)"
ng-show="template.summary_fields.user_capabilities.start">
</at-row-action>
<at-row-action icon="fa-copy" ng-click="vm.copyTemplate(template)"
ng-show="template.summary_fields.user_capabilities.copy">
</at-row-action>
<at-row-action icon="fa-trash" ng-click="vm.deleteTemplate(template)"
ng-show="template.summary_fields.user_capabilities.delete">
</at-row-action>
</div>
</at-row>
</at-list>
<paginate
collection="collection"
dataset="template_dataset"
iterator="template"
base-path="unified_job_templates"
query-set="querySet">
</paginate>
</at-panel-body>
</at-panel>

View File

@ -0,0 +1,27 @@
function TemplatesStrings (BaseString) {
BaseString.call(this, 'templates');
const { t } = this;
const ns = this.templates;
ns.state = {
LIST_BREADCRUMB_LABEL: t.s('TEMPLATES')
}
ns.list = {
PANEL_TITLE: t.s('TEMPLATES'),
ADD_BUTTON_LABEL: t.s('ADD'),
ADD_DD_JT_LABEL: t.s('Job Template'),
ADD_DD_WF_LABEL: t.s('Workflow Template'),
ROW_ITEM_LABEL_ACTIVITY: t.s('Activity'),
ROW_ITEM_LABEL_INVENTORY: t.s('Inventory'),
ROW_ITEM_LABEL_PROJECT: t.s('Project'),
ROW_ITEM_LABEL_CREDENTIALS: t.s('Credentials'),
ROW_ITEM_LABEL_MODIFIED: t.s('Last Modified'),
ROW_ITEM_LABEL_RAN: t.s('Last Ran'),
}
}
TemplatesStrings.$inject = ['BaseStringService'];
export default TemplatesStrings;

View File

@ -1,6 +1,7 @@
@import 'action/_index';
@import 'input/_index';
@import 'layout/_index';
@import 'list/_index';
@import 'modal/_index';
@import 'panel/_index';
@import 'popover/_index';

View File

@ -88,6 +88,10 @@ function ComponentsStrings (BaseString) {
ALL: t.s('All'),
FAILED: t.s('Failed')
};
ns.list = {
DEFAULT_EMPTY_LIST: t.s('List is empty.')
};
}
ComponentsStrings.$inject = ['BaseStringService'];

View File

@ -16,6 +16,10 @@ import inputText from '~components/input/text.directive';
import inputTextarea from '~components/input/textarea.directive';
import inputTextareaSecret from '~components/input/textarea-secret.directive';
import layout from '~components/layout/layout.directive';
import list from '~components/list/list.directive';
import row from '~components/list/row.directive';
import rowItem from '~components/list/row-item.directive';
import rowAction from '~components/list/row-action.directive';
import modal from '~components/modal/modal.directive';
import panel from '~components/panel/panel.directive';
import panelBody from '~components/panel/body.directive';
@ -54,6 +58,10 @@ angular
.directive('atInputTextarea', inputTextarea)
.directive('atInputTextareaSecret', inputTextareaSecret)
.directive('atLayout', layout)
.directive('atList', list)
.directive('atRow', row)
.directive('atRowItem', rowItem)
.directive('atRowAction', rowAction)
.directive('atModal', modal)
.directive('atPanel', panel)
.directive('atPanelBody', panelBody)

View File

@ -175,9 +175,7 @@
}
}
@breakpoint-sm: 700px;
@media screen and (max-width: @breakpoint-sm) {
@media screen and (max-width: @at-breakpoint-mobile-layout) {
.at-Layout {
&-side {
top: 60px;
@ -215,4 +213,4 @@
}
}
}
}
}

View File

@ -0,0 +1,191 @@
.at-List {
margin-top: @at-margin-top-list;
}
.at-List--empty {
margin-top: @at-margin-top-list;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: @at-height-list-empty;
border-radius: @at-border-radius;
border: @at-border-default-width solid @at-color-list-empty-border;
background-color: @at-color-list-empty-background;
color: @at-color-list-empty;
text-transform: uppercase;
text-align: center;
padding: @at-padding-list-empty;
}
.at-List-toolbar {
width: 100%;
display: flex;
align-items: center;
margin-bottom: @at-margin-bottom-list-toolbar;
}
.at-List-search {
flex: auto;
}
.at-List-toolbarAction {
margin-left: @at-margin-left-toolbar-action;
display: flex;
align-items: center;
justify-content: center;
height: @at-height-toolbar-action;
border-radius: @at-border-radius;
position: relative;
}
.at-List-toolbarActionButton {
border-radius: @at-border-radius;
min-width: 80px;
}
.at-List-toolbarDropdownCarat {
margin-left: @at-margin-left-toolbar-carat;
display: inline-block;
width: 0;
height: 0;
vertical-align: middle;
border-top: 4px dashed;
border-right: 4px solid transparent;
border-left: 4px solid transparent;
}
.at-List-toolbarActionDropdownMenu {
float: right;
right: 0;
left: auto;
}
.at-List-container {
border: @at-border-default-width solid @at-color-list-border;
border-radius: @at-border-radius;
}
.at-Row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: @at-padding-list-row;
}
.at-Row--active {
border-left: @at-border-style-list-active-indicator;
border-top-left-radius: @at-border-radius;
border-top-right-radius: @at-border-radius;
}
.at-Row ~ .at-Row {
border-top-left-radius: 0px;
border-top-right-radius: 0px;
border-top: @at-border-default-width solid @at-color-list-border;
}
.at-Row-actions {
display: flex;
}
.at-Row-items {
align-self: flex-start;
}
.at-RowItem {
display: flex;
align-items: center;
line-height: @at-height-list-row-item;
}
.at-RowItem--isHeader {
margin-bottom: @at-margin-bottom-list-header;
line-height: @at-line-height-list-row-item-header;
}
.at-RowItem--labels {
line-height: @at-line-height-list-row-item-labels;
}
.at-RowItem-header {
font-weight: bold;
}
.at-RowItem-tagContainer {
margin-left: @at-margin-left-list-row-item-tag-container;
}
.at-RowItem-tag {
text-transform: uppercase;
font-weight: 100;
background-color: @at-color-list-row-item-tag-background;
border-radius: @at-border-radius;
color: @at-color-list-row-item-tag;
font-size: @at-font-size-list-row-item-tag;
margin-left: @at-margin-left-list-row-item-tag;
margin-top: @at-margin-top-list-row-item-tag;
padding: @at-padding-list-row-item-tag;
line-height: @at-line-height-list-row-item-tag;
}
.at-RowItem-tag--primary {
background: @at-color-list-row-item-tag-primary-background;
color: @at-color-list-row-item-tag-primary;
}
.at-RowItem-tag--header {
height: @at-height-list-row-item-tag;
line-height: inherit;
}
.at-RowItem-tagIcon {
margin-right: @at-margin-right-list-row-item-tag-icon;
}
.at-RowItem-label {
text-transform: uppercase;
width: @at-width-list-row-item-label;
color: @at-color-list-row-item-label;
}
.at-RowAction {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
margin-left: @at-margin-left-list-row-action;
padding: @at-padding-list-row-action;
background: @at-color-list-row-action-background;
border-radius: @at-border-radius;
height: @at-height-list-row-action;
width: @at-width-list-row-action;
i {
font-size: @at-font-size-list-row-action-icon;
color: @at-color-list-row-action-icon;
}
}
.at-RowAction:hover {
background-color: @at-color-list-row-action-hover;
i {
color: @at-color-list-row-action-icon-hover;
}
}
.at-RowAction--danger:hover {
background-color: @at-color-list-row-action-hover-danger;
}
@media screen and (max-width: @at-breakpoint-compact-list) {
.at-Row-actions {
flex-direction: column;
}
.at-RowAction {
margin: @at-margin-list-row-action-mobile;
}
}

View File

@ -0,0 +1,25 @@
const templateUrl = require('~components/list/list.partial.html');
// TODO: figure out emptyListReason scope property
function AtListController (strings) {
this.strings = strings;
}
AtListController.$inject = ['ComponentsStrings'];
function atList () {
return {
restrict: 'E',
replace: true,
transclude: true,
templateUrl,
scope: {
results: '=',
},
controller: AtListController,
controllerAs: 'vm',
};
}
export default atList;

View File

@ -0,0 +1,7 @@
<div class="at-List">
<div class="at-List-container" ng-hide="results.length === 0" ng-transclude>
</div>
<div class="at-List--empty" ng-show="results.length === 0">
{{ emptyListReason || vm.strings.get("list.DEFAULT_EMPTY_LIST") }}
</div>
</div>

View File

@ -0,0 +1,15 @@
const templateUrl = require('~components/list/row-action.partial.html');
function atRowAction () {
return {
restrict: 'E',
replace: true,
transclude: true,
templateUrl,
scope: {
icon: '@'
}
};
}
export default atRowAction;

View File

@ -0,0 +1,4 @@
<div class="at-RowAction"
ng-class="{'at-RowAction--danger': (icon === 'fa-trash' || icon === 'fa-times')}">
<i class="fa" ng-class="icon"></i>
</div>

View File

@ -0,0 +1,24 @@
const templateUrl = require('~components/list/row-item.partial.html');
function atRowItem () {
return {
restrict: 'E',
replace: true,
transclude: true,
templateUrl,
scope: {
headerValue: '@',
headerLink: '@',
headerTag: '@',
labelValue: '@',
value: '@',
valueLink: '@',
smartStatus: '=?',
tagValues: '=?',
// TODO: add see more for tags if applicable
tagsAreCreds: '@'
}
};
}
export default atRowItem;

View File

@ -0,0 +1,38 @@
<div class="at-RowItem" ng-class="{'at-RowItem--isHeader': headerValue}"
ng-show="headerValue || value || (smartStatus && smartStatus.summary_fields.recent_jobs.length) || (tagValues && tagValues.length)">
<div class="at-RowItem-header" ng-if="headerValue && headerLink">
<a ng-href="{{ headerLink }}">{{ headerValue }}</a>
</div>
<div class="at-RowItem-header" ng-if="headerValue && !headerLink">
{{ headerValue }}
</div>
<div class="at-RowItem-tag at-RowItem-tag--header" ng-if="headerTag">
{{ headerTag }}
</div>
<div class="at-RowItem-label" ng-if="labelValue">
{{ labelValue }}
</div>
<div class="at-RowItem-value" ng-if="value && valueLink">
<a ng-href="{{ valueLink }}">{{ value }}</a>
</div>
<div class="at-RowItem-value" ng-if="value && !valueLink"
ng-bind-html="value">
</div>
<aw-smart-status jobs="smartStatus.summary_fields.recent_jobs"
template-type="smartStatus.type" ng-if="smartStatus">
</aw-smart-status>
<div class="at-RowItem-tagContainer" ng-if="tagValues && tagValues.length">
<div ng-repeat="tag in tagValues" class="at-RowItem-tag at-RowItem-tag--primary">
<span ng-switch="tag.kind" class="at-RowItem-tagIcon"
ng-if="tagsAreCreds">
<span class="fa fa-cloud" ng-switch-when="cloud"></span>
<span class="fa fa-info" ng-switch-when="insights"></span>
<span class="fa fa-sitemap" ng-switch-when="net"></span>
<span class="fa fa-code-fork" ng-switch-when="scm"></span>
<span class="fa fa-key" ng-switch-when="ssh"></span>
<span class="fa fa-archive" ng-switch-when="vault"></span>
</span>
{{ tag.name }}
</div>
</div>
</div>

View File

@ -0,0 +1,15 @@
const templateUrl = require('~components/list/row.partial.html');
function atRow () {
return {
restrict: 'E',
replace: true,
transclude: true,
templateUrl,
scope: {
templateId: '@'
}
};
}
export default atRow;

View File

@ -0,0 +1,2 @@
<div class="at-Row" id="row-{{ templateId }}" ng-transclude>
</div>

View File

@ -9,6 +9,10 @@
padding: 0;
}
.at-Panel-headingRow {
margin-bottom: 20px;
}
.at-Panel-dismiss {
.at-mixin-ButtonIcon();
color: @at-color-icon-dismiss;
@ -24,3 +28,19 @@
.at-mixin-Heading(@at-font-size-panel-heading);
text-transform: none;
}
.at-Panel-headingTitleBadge {
font-size: 11px;
font-weight: normal;
padding: 2px 10px;
line-height: 10px;
background-color: #848992;
border-radius: 5px;
display: inline-block;
min-width: 10px;
color: #fff;
vertical-align: middle;
white-space: nowrap;
text-align: center;
margin-left: 5px;
}

View File

@ -1,6 +1,7 @@
const templateUrl = require('~components/panel/heading.partial.html');
function link (scope, el, attrs, panel) {
scope.hideDismiss = Boolean(attrs.hideDismiss);
panel.use(scope);
}

View File

@ -1,12 +1,20 @@
<div class="row">
<div class="col-xs-10">
<div class="row at-Panel-headingRow">
<div class="col-xs-10"
ng-if="!hideDismiss">
<h3 class="at-Panel-headingTitle">
<ng-transclude></ng-transclude>
</h3>
</div>
<div class="col-xs-2">
<div class="col-xs-2"
ng-if="!hideDismiss">
<div class="at-Panel-dismiss">
<i class="fa fa-times-circle fa-lg" ng-click="dismiss()"></i>
</div>
</div>
<div class="col-xs-12"
ng-if="hideDismiss">
<h3 class="at-Panel-headingTitle">
<ng-transclude></ng-transclude>
</h3>
</div>
</div>

View File

@ -7,7 +7,7 @@
}
.at-Truncate-copy {
color: @at-gray-dark-2x;
color: @at-gray-b7;
cursor: pointer;
margin-left: 10px;

View File

@ -0,0 +1,110 @@
let BaseModel;
let WorkflowJobTemplateNode;
let $http;
function optionsLaunch (id) {
const req = {
method: 'OPTIONS',
url: `${this.path}${id}/launch/`
};
return $http(req);
}
function getLaunch (id) {
const req = {
method: 'GET',
url: `${this.path}${id}/launch/`
};
return $http(req)
.then(res => {
this.model.launch.GET = res.data;
return res;
});
}
function postLaunch (params) {
const req = {
method: 'POST',
url: `${this.path}${params.id}/launch/`
};
if (params.launchData) {
req.data = params.launchData;
}
return $http(req);
}
function getSurveyQuestions (id) {
const req = {
method: 'GET',
url: `${this.path}${id}/survey_spec/`
};
return $http(req);
}
function canLaunchWithoutPrompt () {
const launchData = this.model.launch.GET;
return (
launchData.can_start_without_user_input &&
!launchData.ask_inventory_on_launch &&
!launchData.ask_credential_on_launch &&
!launchData.ask_verbosity_on_launch &&
!launchData.ask_job_type_on_launch &&
!launchData.ask_limit_on_launch &&
!launchData.ask_tags_on_launch &&
!launchData.ask_skip_tags_on_launch &&
!launchData.ask_variables_on_launch &&
!launchData.ask_diff_mode_on_launch &&
!launchData.survey_enabled
);
}
function setDependentResources (id) {
this.dependentResources = [
{
model: new WorkflowJobTemplateNode(),
params: {
unified_job_template: id
}
}
];
}
function UnifiedJobTemplateModel (method, resource, graft) {
BaseModel.call(this, 'unified_job_templates');
this.Constructor = UnifiedJobTemplateModel;
this.setDependentResources = setDependentResources.bind(this);
this.optionsLaunch = optionsLaunch.bind(this);
this.getLaunch = getLaunch.bind(this);
this.postLaunch = postLaunch.bind(this);
this.getSurveyQuestions = getSurveyQuestions.bind(this);
this.canLaunchWithoutPrompt = canLaunchWithoutPrompt.bind(this);
this.model.launch = {};
return this.create(method, resource, graft);
}
function UnifiedJobTemplateModelLoader (_BaseModel_, WorkflowJobTemplateNodeModel, _$http_) {
BaseModel = _BaseModel_;
WorkflowJobTemplateNode = WorkflowJobTemplateNodeModel;
$http = _$http_;
return UnifiedJobTemplateModel;
}
UnifiedJobTemplateModelLoader.$inject = [
'BaseModel',
'WorkflowJobTemplateNodeModel',
'$http',
'$state'
];
export default UnifiedJobTemplateModelLoader;

View File

@ -14,6 +14,7 @@ import Inventory from '~models/Inventory';
import InventoryScript from '~models/InventoryScript';
import ModelsStrings from '~models/models.strings';
import UnifiedJobTemplate from '~models/UnifiedJobTemplate';
const MODULE_NAME = 'at.lib.models';
@ -33,6 +34,7 @@ angular
.service('InventorySourceModel', InventorySource)
.service('InventoryModel', Inventory)
.service('InventoryScriptModel', InventoryScript)
.service('ModelsStrings', ModelsStrings);
.service('ModelsStrings', ModelsStrings)
.service('UnifiedJobTemplateModel', UnifiedJobTemplate);
export default MODULE_NAME;

View File

@ -61,9 +61,16 @@ function BaseStringService (namespace) {
this.SAVE = t.s('SAVE');
this.OK = t.s('OK');
this.deleteResource = {
HEADER: t.s('Delete'),
USED_BY: resourceType => t.s('The {{ resourceType }} is currently being used by other resources.', { resourceType }),
CONFIRM: resourceType => t.s('Are you sure you want to delete this {{ resourceType }}?', { resourceType })
};
this.error = {
HEADER: t.s('Error!'),
CALL: ({ path, status }) => t.s('Call to {{ path }} failed. DELETE returned status: {{ status }}.', { path, status })
};
this.ALERT = ({ header, body }) => t.s('{{ header }} {{ body }}', { header, body });
/**
* This getter searches the extending class' namespace first for a match then falls back to

View File

@ -45,6 +45,7 @@
.at-mixin-ButtonColor (@background, @color, @hover: '@{background}-hover') {
background-color: @@background;
border-color: @@background;
&, &:hover, &:focus {
color: @@color;
@ -52,6 +53,7 @@
&:hover, &:focus {
background-color: @@hover;
border-color: @@hover;
}
&[disabled] {

View File

@ -14,22 +14,24 @@
* 1. Colors
* 2. Typography
* 3. Layout
* 4. Breakpoints
*
*/
// 1. Colors --------------------------------------------------------------------------------------
@at-gray-light-3x: #fcfcfc;
@at-gray-light-2-5x: #fafafa;
@at-gray-light-2x: #f2f2f2;
@at-gray-light: #ebebeb;
@at-gray: #e1e1e1;
@at-gray-dark: #d7d7d7;
@at-gray-dark-2x: #b7b7b7;
@at-gray-dark-3x: #A9A9A9;
@at-gray-dark-4x: #848992;
@at-gray-dark-5x: #707070;
@at-gray-dark-6x: #161b1f;
@at-gray-fc: #fcfcfc;
@at-gray-fa: #fafafa;
@at-gray-f2: #f2f2f2;
@at-gray-f6: #f6f6f6;
@at-gray-eb: #ebebeb;
@at-gray-e1: #e1e1e1;
@at-gray-d7: #d7d7d7;
@at-gray-b7: #b7b7b7;
@at-gray-a9: #a9a9a9;
@at-gray-848992: #848992;
@at-gray-70: #707070;
@at-gray-161b1f: #161b1f;
@at-white: #ffffff;
@at-white-hover: #f2f2f2;
@ -38,7 +40,7 @@
@at-blue-hover: #286090;
@at-green: #5cb85c;
@at-green-hover: #449D44;
@at-green-hover: #449d44;
@at-orange: #f0ad4e;
@at-orange-hover: #ec971f;
@ -66,6 +68,11 @@
@at-space-2x: 10px;
@at-space-3x: 15px;
@at-space-4x: 20px;
@at-space-5x: 25px;
// 4. Breakpoints ---------------------------------------------------------------------------------
@at-breakpoint-sm: 700px;
/**
* All variables used in the UI. Use these variables directly during the development of components
@ -83,6 +90,7 @@
* 3. Layout
* 4. Buttons
* 5. Misc
* 6. Breakpoints
*
*/
@ -106,64 +114,79 @@
@at-color-success: @at-green;
@at-color-success-hover: @at-green-hover;
@at-color-disabled: @at-gray-dark;
@at-color-disabled: @at-gray-d7;
@at-color-body-background-dark: @at-gray-dark-5x;
@at-color-body-background-dark: @at-gray-70;
@at-color-body-text-dark: @at-white;
@at-color-body-background: @at-gray-light-3x;
@at-color-body-text: @at-gray-dark-5x;
@at-color-body-background: @at-gray-fc;
@at-color-body-text: @at-gray-70;
@at-color-button-border-default: @at-gray-dark-2x;
@at-color-button-text-default: @at-gray-dark-5x;
@at-color-button-border-default: @at-gray-b7;
@at-color-button-text-default: @at-gray-70;
@at-color-tab-default-active: @at-gray-dark-4x;
@at-color-tab-border-default-active: @at-gray-dark-4x;
@at-color-tab-default-active: @at-gray-848992;
@at-color-tab-border-default-active: @at-gray-848992;
@at-color-tab-text-default-active: @at-white;
@at-color-tab-default-disabled: @at-white;
@at-color-tab-border-default-disabled: @at-gray-dark-2x;
@at-color-tab-text-default-disabled: @at-gray-dark-5x;
@at-color-tab-border-default-disabled: @at-gray-b7;
@at-color-tab-text-default-disabled: @at-gray-70;
@at-color-form-label: @at-gray-dark-5x;
@at-color-form-label: @at-gray-70;
@at-color-input-background: @at-gray-light-3x;
@at-color-input-border: @at-gray-dark-2x;
@at-color-input-button: @at-gray-light-3x;
@at-color-input-button-hover: @at-gray-light-2x;
@at-color-input-disabled: @at-gray-light;
@at-color-input-background: @at-gray-fc;
@at-color-input-border: @at-gray-b7;
@at-color-input-button: @at-gray-fc;
@at-color-input-button-hover: @at-gray-f2;
@at-color-input-disabled: @at-gray-eb;
@at-color-input-readonly: @at-color-input-background;
@at-color-input-error: @at-color-error;
@at-color-input-focus: @at-color-info;
@at-color-input-hint: @at-gray-dark-4x;
@at-color-input-icon: @at-gray-dark-2x;
@at-color-input-placeholder: @at-gray-dark-4x;
@at-color-input-text: @at-gray-dark-6x;
@at-color-input-hint: @at-gray-848992;
@at-color-input-icon: @at-gray-b7;
@at-color-input-placeholder: @at-gray-848992;
@at-color-input-text: @at-gray-161b1f;
@at-color-icon-dismiss: @at-gray-dark;
@at-color-icon-popover: @at-gray-dark-4x;
@at-color-icon-hover: @at-gray-dark-4x;
@at-color-icon-dismiss: @at-gray-d7;
@at-color-icon-popover: @at-gray-848992;
@at-color-icon-hover: @at-gray-848992;
@at-color-panel-heading: @at-gray-dark-5x;
@at-color-panel-border: @at-gray-dark-2x;
@at-color-panel-heading: @at-gray-70;
@at-color-panel-border: @at-gray-b7;
@at-color-search-key-active: @at-blue;
@at-color-table-header-background: @at-gray-light;
@at-color-line-separator: @at-gray;
@at-color-table-header-background: @at-gray-eb;
@at-color-line-separator: @at-gray-e1;
@at-color-top-nav-background: @at-white;
@at-color-top-nav-border-bottom: @at-gray-dark-2x;
@at-color-top-nav-item-text: @at-gray-dark-5x;
@at-color-top-nav-item-icon: @at-gray-dark-4x;
@at-color-top-nav-border-bottom: @at-gray-b7;
@at-color-top-nav-item-text: @at-gray-70;
@at-color-top-nav-item-icon: @at-gray-848992;
@at-color-top-nav-item-icon-socket-outline: @at-white;
@at-color-top-nav-item-background-hover: @at-gray-light-2-5x;
@at-color-side-nav-background: @at-gray-dark-4x;
@at-color-top-nav-item-background-hover: @at-gray-fa;
@at-color-side-nav-background: @at-gray-848992;
@at-color-side-nav-content: @at-white;
@at-color-side-nav-item-background-hover: @at-gray-dark-2x;
@at-color-side-nav-item-background-hover: @at-gray-b7;
@at-color-side-nav-item-border-hover: @at-white;
@at-color-footer-background: @at-gray-light-3x;
@at-color-footer: @at-gray-dark-5x;
@at-color-footer-background: @at-gray-fc;
@at-color-footer: @at-gray-70;
@at-color-list-empty-border: @at-gray-d7;
@at-color-list-empty-background: @at-gray-f6;
@at-color-list-empty: @at-gray-848992;
@at-color-list-border: @at-gray-b7;
@at-color-list-row-item-tag-background: @at-gray-eb;
@at-color-list-row-item-tag: @at-gray-70;
@at-color-list-row-item-label: @at-gray-848992;
@at-color-list-row-action-background: @at-white;
@at-color-list-row-action-icon: @at-gray-848992;
@at-color-list-row-action-hover: @at-blue;
@at-color-list-row-action-hover-danger: @at-red;
@at-color-list-row-action-icon-hover: @at-white;
@at-color-list-row-item-tag-primary-background: @at-blue;
@at-color-list-row-item-tag-primary: @at-white;
// 2. Typography ----------------------------------------------------------------------------------
@ -181,6 +204,9 @@
@at-font-size-navigation: @at-font-size-3x;
@at-font-size-table-heading: @at-font-size-3x;
@at-font-size-menu-icon: @at-font-size-5x;
@at-font-size-list-row-item-tag: 10px;
@at-font-size-list-row-action: 19px;
@at-font-size-list-row-action-icon: 19px;
@at-font-weight-body: @at-font-weight;
@at-font-weight-heading: @at-font-weight-2x;
@ -199,6 +225,10 @@
@at-padding-between-side-nav-icon-text: @at-space-3x;
@at-padding-footer-right: @at-space-4x;
@at-padding-footer-bottom: @at-space-4x;
@at-padding-list-empty: @at-space-2x;
@at-padding-list-row-item-tag: 0 @at-space-2x;
@at-padding-list-row-action: 7px;
@at-padding-list-row: 10px 20px;
@at-margin-input-message: @at-space;
@at-margin-item-column: @at-space-3x;
@ -215,6 +245,18 @@
@at-margin-top-search-key: @at-space-2x;
@at-margin-top-list: @at-space-5x;
@at-margin-bottom-list-toolbar: @at-space-4x;
@at-margin-left-toolbar-action: @at-space-4x;
@at-margin-left-toolbar-carat: @at-space;
@at-margin-bottom-list-header: @at-space;
@at-margin-left-list-row-item-tag: @at-space-2x;
@at-margin-top-list-row-item-tag: 2.25px;
@at-margin-left-list-row-action: @at-space-4x;
@at-margin-right-list-row-item-tag-icon: 8px;
@at-margin-left-list-row-item-tag-container: -10px;
@at-margin-list-row-action-mobile: 10px;
@at-height-divider: @at-margin-panel;
@at-height-input: 30px;
@at-height-textarea: 144px;
@ -226,11 +268,21 @@
@at-height-side-nav-item-icon: 20px;
@at-height-side-nav-spacer: 20px;
@at-height-top-side-nav-makeup: 55px;
@at-height-list-empty: 200px;
@at-height-toolbar-action: 30px;
@at-height-list-row-item: 27px;
@at-height-list-row-item-tag: 15px;
@at-height-list-row-action: 30px;
@at-width-input-button-sm: 72px;
@at-width-input-button-md: 84px;
@at-width-collapsed-side-nav: 50px;
@at-width-expanded-side-nav: 200px;
@at-width-list-row-item-label: 120px;
@at-width-list-row-action: 30px;
@at-line-height-list-row-item-header: @at-space-3x;
@at-line-height-list-row-item-labels: 17px;
// 4. Transitions ---------------------------------------------------------------------------------
@ -249,3 +301,10 @@
@at-z-index-side-nav: 1030;
@at-z-index-footer: 1020;
@at-border-default-width: 1px;
@at-border-style-list-active-indicator: 5px solid @at-color-info;
@at-line-height-list-row-item-tag: 22px;
// 6. Breakpoints ---------------------------------------------------------------------------------
@at-breakpoint-mobile-layout: @at-breakpoint-sm;
@at-breakpoint-compact-list: @at-breakpoint-sm;

View File

@ -16,6 +16,10 @@ export default {
label: N_("JOB TEMPLATES")
},
views: {
// TODO: this controller was removed and replaced
// with the new features/templates controller
// this view should be updated with the new
// expanded list
'related': {
templateProvider: function(FormDefinition, GenerateForm) {
let html = GenerateForm.buildCollection({

View File

@ -44,7 +44,9 @@ export default ['$stateParams', '$scope', '$state', 'GetBasePath', 'QuerySet', '
qs.initFieldset(path, $scope.djangoModel).then((data) => {
$scope.models = data.models;
$scope.options = data.options.data;
$scope.$emit(`${$scope.list.iterator}_options`, data.options);
if ($scope.list) {
$scope.$emit(`${$scope.list.iterator}_options`, data.options);
}
});
$scope.searchPlaceholder = $scope.disableSearch ? i18n._('Cannot search running job') : i18n._('Search');
@ -76,6 +78,7 @@ export default ['$stateParams', '$scope', '$state', 'GetBasePath', 'QuerySet', '
qs.search(path, queryset).then((res) => {
$scope.dataset = res.data;
$scope.collection = res.data.results;
$scope.$emit('updateDataset', res.data);
});
$scope.searchTerm = null;

View File

@ -10,44 +10,48 @@
flex: 0 1 auto;
}
.SmartStatus--success{
color: @default-succ;
margin-top: 10px;
margin-bottom: 10px;
padding: 0px;
.SmartStatus-icon {
width: 16px;
height: 16px;
}
.SmartStatus--failed{
color: @default-err;
margin-top: 10px;
margin-bottom: 10px;
padding: 0px;
.SmartStatus-iconDirectionPlaceholder {
width: 16px;
height: 8px;
border: 1px solid #d7d7d7;
background: #f2f2f2;
}
.SmartStatus--failed:before {
content: "\f06a";
.SmartStatus-iconDirectionPlaceholder--bottom {
border-bottom: 0;
}
.SmartStatus--running{
color: @default-icon;
margin-top: 10px;
padding: 0px;
.pulsate();
.SmartStatus-iconDirectionPlaceholder--top {
border-top: 0;
}
.SmartStatus-vertCenter{
margin-top: 10px;
margin-bottom: 10px;
padding: 0px;
.SmartStatus-iconIndicator {
width: 16px;
height: 8px;
}
.SmartStatus-tooltip{
text-align: left;
max-width: 250px;
padding: 10px;
line-height: 22px;
.SmartStatus-iconIndicator--success {
background: #5cb85c;
}
.SmartStatus-iconIndicator--failed {
background: #d9534f;
}
.SmartStatus-iconPlaceholder {
height: 15px;
width: 15px;
border: 1px solid #d7d7d7;
background: #f2f2f2;
}
.SmartStatus-tooltip--successful,
.SmartStatus-tooltip--success{
color: @default-succ;

View File

@ -16,7 +16,7 @@ export default ['$scope', '$filter',
var firstJobStatus;
var recentJobs = $scope.jobs;
var detailsBaseUrl;
if(!recentJobs){
return;
}
@ -74,6 +74,7 @@ export default ['$scope', '$filter',
$scope.singleJobStatus = singleJobStatus;
$scope.sparkArray = sparkData;
$scope.placeholders = new Array(10 - sparkData.length);
}
$scope.$watchCollection('jobs', function(){
init();

View File

@ -9,13 +9,21 @@
data-container="body"
tooltipInnerClass="SmartStatus-tooltip"
title="">
<i class="fa
DashboardList-status"
ng-class="{'SmartStatus--success icon-job-successful': job.value === 1,
'SmartStatus--failed icon-job-failed': job.value === -1,
'SmartStatus--running icon-job-successful': job.value === 0,
'SmartStatus-vertCenter': singleJobStatus}">
</i>
<div class="SmartStatus-icon">
<div ng-show="job.value === -1"
class="SmartStatus-iconDirectionPlaceholder SmartStatus-iconDirectionPlaceholder--bottom">
</div>
<div class="SmartStatus-iconIndicator"
ng-class="{'SmartStatus-iconIndicator--success': job.value === 1,
'SmartStatus-iconIndicator--failed': job.value === -1}">
</div>
<div ng-show="job.value === 1"
class="SmartStatus-iconDirectionPlaceholder SmartStatus-iconDirectionPlaceholder--top">
</div>
</div>
</a>
</div>
<div ng-repeat="n in placeholders track by $index" class='SmartStatus-iconContainer'>
<div class="SmartStatus-iconPlaceholder"></div>
</div>
</div>

View File

@ -16,6 +16,7 @@ export default
templateUrl: templateUrl('templates/labels/labelsList'),
link: function(scope, element, attrs) {
scope.showDelete = attrs.showDelete === 'true';
scope.isRowItem = attrs.isRowItem === 'true';
scope.seeMoreInactive = true;
var getNext = function(data, arr, resolve) {
@ -91,18 +92,23 @@ export default
});
};
scope.$watchCollection(scope.$parent.list.iterator, function() {
// To keep the array of labels fresh, we need to set up a watcher - otherwise, the
// array will get set initially and then never be updated as labels are removed
if (scope[scope.$parent.list.iterator].summary_fields.labels){
scope.labels = scope[scope.$parent.list.iterator].summary_fields.labels.results.slice(0, 5);
scope.count = scope[scope.$parent.list.iterator].summary_fields.labels.count;
}
else{
scope.labels = null;
scope.count = null;
}
});
if (scope.$parent.$parent.template) {
scope.labels = scope.$parent.$parent.template.summary_fields.labels.results.slice(0, 5);
scope.count = scope.$parent.$parent.template.summary_fields.labels.count;
} else {
scope.$watchCollection(scope.$parent.list.iterator, function() {
// To keep the array of labels fresh, we need to set up a watcher - otherwise, the
// array will get set initially and then never be updated as labels are removed
if (scope[scope.$parent.list.iterator].summary_fields.labels){
scope.labels = scope[scope.$parent.list.iterator].summary_fields.labels.results.slice(0, 5);
scope.count = scope[scope.$parent.list.iterator].summary_fields.labels.count;
}
else{
scope.labels = null;
scope.count = null;
}
});
}
}
};

View File

@ -1,4 +1,4 @@
<div class="LabelList-tagContainer" ng-repeat="label in labels">
<div class="LabelList-tagContainer" ng-repeat="label in labels" ng-if="!isRowItem">
<div class="LabelList-deleteContainer"
ng-click="deleteLabel(template, label)"
ng-show="showDelete && template.summary_fields.user_capabilities.edit">
@ -9,6 +9,25 @@
</div>
</div>
<div class="LabelList-seeMoreLess" ng-show="count > 5 && seeMoreInactive"
ng-click="seeMore()">View More</div>
ng-click="seeMore()" ng-if="!isRowItem">View More</div>
<div class="LabelList-seeMoreLess" ng-show="count > 5 && !seeMoreInactive"
ng-click="seeLess()">View Less</div>
ng-click="seeLess()" ng-if="!isRowItem">View Less</div>
<div class="at-RowItem at-RowItem--labels" ng-show="count > 0" ng-if="isRowItem">
<div class="at-RowItem-label">
Labels
</div>
<div class="LabelList-tagContainer" ng-repeat="label in labels">
<div class="LabelList-deleteContainer"
ng-click="deleteLabel(template, label)"
ng-show="showDelete && template.summary_fields.user_capabilities.edit">
<i class="fa fa-times LabelList-tagDelete"></i>
</div>
<div class="LabelList-tag" ng-class="{'LabelList-tag--deletable' : (showDelete && template.summary_fields.user_capabilities.edit)}">
<span class="LabelList-name">{{ label.name }}</span>
</div>
</div>
<div class="LabelList-seeMoreLess" ng-show="count > 5 && seeMoreInactive"
ng-click="seeMore()">View More</div>
<div class="LabelList-seeMoreLess" ng-show="count > 5 && !seeMoreInactive"
ng-click="seeLess()">View Less</div>
</div>

View File

@ -1,11 +0,0 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import controller from './templates-list.controller';
export default
angular.module('templatesList', [])
.controller('TemplatesListController', controller);

View File

@ -1,360 +0,0 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default ['$scope', '$rootScope',
'Alert','TemplateList', 'Prompt', 'ProcessErrors',
'GetBasePath', 'InitiatePlaybookRun', 'Wait', '$state', '$filter',
'Dataset', 'rbacUiControlService', 'TemplatesService','QuerySet',
'TemplateCopyService', 'i18n', 'JobTemplateModel', 'TemplatesStrings',
function(
$scope, $rootScope, Alert,
TemplateList, Prompt, ProcessErrors, GetBasePath,
InitiatePlaybookRun, Wait, $state, $filter, Dataset, rbacUiControlService, TemplatesService,
qs, TemplateCopyService, i18n, JobTemplate, TemplatesStrings
) {
let jobTemplate = new JobTemplate();
var list = TemplateList;
init();
function init() {
$scope.canAdd = false;
rbacUiControlService.canAdd("job_templates")
.then(function(params) {
$scope.canAddJobTemplate = params.canAdd;
});
rbacUiControlService.canAdd("workflow_job_templates")
.then(function(params) {
$scope.canAddWorkflowJobTemplate = params.canAdd;
});
// search init
$scope.list = list;
$scope[`${list.iterator}_dataset`] = Dataset.data;
$scope[list.name] = $scope[`${list.iterator}_dataset`].results;
$scope.options = {};
$rootScope.flashMessage = null;
}
$scope.$on(`${list.iterator}_options`, function(event, data){
$scope.options = data.data.actions.GET;
optionsRequestDataProcessing();
});
$scope.$watchCollection('templates', function() {
optionsRequestDataProcessing();
}
);
// iterate over the list and add fields like type label, after the
// OPTIONS request returns, or the list is sorted/paginated/searched
function optionsRequestDataProcessing(){
$scope[list.name].forEach(function(item, item_idx) {
var itm = $scope[list.name][item_idx];
// Set the item type label
if (list.fields.type && $scope.options.hasOwnProperty('type')) {
$scope.options.type.choices.forEach(function(choice) {
if (choice[0] === item.type) {
itm.type_label = choice[1];
}
});
}
});
}
$scope.$on(`ws-jobs`, function () {
let path = GetBasePath(list.basePath) || GetBasePath(list.name);
qs.search(path, $state.params[`${list.iterator}_search`])
.then(function(searchResponse) {
$scope[`${list.iterator}_dataset`] = searchResponse.data;
$scope[list.name] = $scope[`${list.iterator}_dataset`].results;
});
});
$scope.editJobTemplate = function(template) {
if(template) {
if(template.type && (template.type === 'Job Template' || template.type === 'job_template')) {
$state.transitionTo('templates.editJobTemplate', {job_template_id: template.id});
}
else if(template.type && (template.type === 'Workflow Job Template' || template.type === 'workflow_job_template')) {
$state.transitionTo('templates.editWorkflowJobTemplate', {workflow_job_template_id: template.id});
}
else {
// Something went wrong - Let the user know that we're unable to launch because we don't know
// what type of job template this is
Alert('Error: Unable to determine template type', 'We were unable to determine this template\'s type while routing to edit.');
}
}
else {
Alert('Error: Unable to edit template', 'Template parameter is missing');
}
};
$scope.deleteJobTemplate = function(template) {
if(template) {
var action = function() {
function handleSuccessfulDelete(isWorkflow) {
let stateParamId = isWorkflow ? $state.params.workflow_job_template_id : $state.params.job_template_id;
let reloadListStateParams = null;
if($scope.templates.length === 1 && $state.params.template_search && !_.isEmpty($state.params.template_search.page) && $state.params.template_search.page !== '1') {
reloadListStateParams = _.cloneDeep($state.params);
reloadListStateParams.template_search.page = (parseInt(reloadListStateParams.template_search.page)-1).toString();
}
if (parseInt(stateParamId) === template.id) {
// Move the user back to the templates list
$state.go("templates", reloadListStateParams, {reload: true});
} else {
$state.go(".", reloadListStateParams, {reload: true});
}
Wait('stop');
}
$('#prompt-modal').modal('hide');
Wait('start');
if(template.type && (template.type === 'Workflow Job Template' || template.type === 'workflow_job_template')) {
TemplatesService.deleteWorkflowJobTemplate(template.id)
.then(function () {
handleSuccessfulDelete(true);
})
.catch(function (response) {
Wait('stop');
ProcessErrors($scope, response.data, response.status, null, { hdr: 'Error!',
msg: 'Call to delete workflow job template failed. DELETE returned status: ' + response.status + '.'});
});
}
else if(template.type && (template.type === 'Job Template' || template.type === 'job_template')) {
TemplatesService.deleteJobTemplate(template.id)
.then(function () {
handleSuccessfulDelete();
})
.catch(function (response) {
Wait('stop');
ProcessErrors($scope, response.data, response.status, null, { hdr: 'Error!',
msg: 'Call to delete job template failed. DELETE returned status: ' + response.status + '.'});
});
}
else {
Wait('stop');
Alert('Error: Unable to determine template type', 'We were unable to determine this template\'s type while deleting.');
}
};
if(template.type && (template.type === 'Workflow Job Template' || template.type === 'workflow_job_template')) {
Prompt({
hdr: i18n._('Delete'),
resourceName: $filter('sanitize')(template.name),
body: TemplatesStrings.get('deleteResource.CONFIRM', 'workflow job template'),
action: action,
actionText: 'DELETE'
});
}
else if(template.type && (template.type === 'Job Template' || template.type === 'job_template')) {
jobTemplate.getDependentResourceCounts(template.id)
.then((counts) => {
const invalidateRelatedLines = [];
let deleteModalBody = `<div class="Prompt-bodyQuery">${TemplatesStrings.get('deleteResource.CONFIRM', 'job template')}</div>`;
counts.forEach(countObj => {
if(countObj.count && countObj.count > 0) {
invalidateRelatedLines.push(`<div><span class="Prompt-warningResourceTitle">${countObj.label}</span><span class="badge List-titleBadge">${countObj.count}</span></div>`);
}
});
if (invalidateRelatedLines && invalidateRelatedLines.length > 0) {
deleteModalBody = `<div class="Prompt-bodyQuery">${TemplatesStrings.get('deleteResource.USED_BY', 'job template')} ${TemplatesStrings.get('deleteResource.CONFIRM', 'job template')}</div>`;
invalidateRelatedLines.forEach(invalidateRelatedLine => {
deleteModalBody += invalidateRelatedLine;
});
}
Prompt({
hdr: i18n._('Delete'),
resourceName: $filter('sanitize')(template.name),
body: deleteModalBody,
action: action,
actionText: 'DELETE'
});
});
}
}
else {
Alert('Error: Unable to delete template', 'Template parameter is missing');
}
};
$scope.submitJob = function(template) {
if(template) {
if(template.type && (template.type === 'Job Template' || template.type === 'job_template')) {
InitiatePlaybookRun({ scope: $scope, id: template.id, job_type: 'job_template' });
}
else if(template.type && (template.type === 'Workflow Job Template' || template.type === 'workflow_job_template')) {
InitiatePlaybookRun({ scope: $scope, id: template.id, job_type: 'workflow_job_template' });
}
else {
// Something went wrong - Let the user know that we're unable to launch because we don't know
// what type of job template this is
Alert('Error: Unable to determine template type', 'We were unable to determine this template\'s type while launching.');
}
}
else {
Alert('Error: Unable to launch template', 'Template parameter is missing');
}
};
$scope.scheduleJob = function(template) {
if(template) {
if(template.type && (template.type === 'Job Template' || template.type === 'job_template')) {
$state.go('jobTemplateSchedules', {id: template.id});
}
else if(template.type && (template.type === 'Workflow Job Template' || template.type === 'workflow_job_template')) {
$state.go('workflowJobTemplateSchedules', {id: template.id});
}
else {
// Something went wrong - Let the user know that we're unable to redirect to schedule because we don't know
// what type of job template this is
Alert('Error: Unable to determine template type', 'We were unable to determine this template\'s type while routing to schedule.');
}
}
else {
Alert('Error: Unable to schedule job', 'Template parameter is missing');
}
};
$scope.copyTemplate = function(template) {
if(template) {
if(template.type && template.type === 'job_template') {
Wait('start');
TemplateCopyService.get(template.id)
.then(function(response){
TemplateCopyService.set(response.data.results)
.then(function(results){
Wait('stop');
if(results.type && results.type === 'job_template') {
$state.go('templates.editJobTemplate', {job_template_id: results.id}, {reload: true});
}
})
.catch(({data, status}) => {
ProcessErrors($scope, data, status, null, {
hdr: 'Error!',
msg: 'Call failed. Return status: ' + status
});
});
})
.catch(({data, status}) => {
ProcessErrors($rootScope, data, status, null, {hdr: 'Error!',
msg: 'Call failed. Return status: '+ status});
});
}
else if(template.type && template.type === 'workflow_job_template') {
TemplateCopyService.getWorkflowCopy(template.id)
.then(function(result) {
if(result.data.can_copy) {
if(result.data.can_copy_without_user_input) {
// Go ahead and copy the workflow - the user has full priveleges on all the resources
TemplateCopyService.copyWorkflow(template.id)
.then(function(result) {
$state.go('templates.editWorkflowJobTemplate', {workflow_job_template_id: result.data.id}, {reload: true});
})
.catch(function (response) {
Wait('stop');
ProcessErrors($scope, response.data, response.status, null, { hdr: 'Error!',
msg: 'Call to copy workflow job template failed. Return status: ' + response.status + '.'});
});
}
else {
let bodyHtml = `
<div class="Prompt-bodyQuery">
You do not have access to all resources used by this workflow. Resources that you don\'t have access to will not be copied and will result in an incomplete workflow.
</div>
<div class="Prompt-bodyTarget">`;
// List the unified job templates user can not access
if (result.data.templates_unable_to_copy.length > 0) {
bodyHtml += '<div>Unified Job Templates that can not be copied<ul>';
_.forOwn(result.data.templates_unable_to_copy, function(ujt) {
if(ujt) {
bodyHtml += '<li>' + ujt + '</li>';
}
});
bodyHtml += '</ul></div>';
}
// List the prompted inventories user can not access
if (result.data.inventories_unable_to_copy.length > 0) {
bodyHtml += '<div>Node prompted inventories that can not be copied<ul>';
_.forOwn(result.data.inventories_unable_to_copy, function(inv) {
if(inv) {
bodyHtml += '<li>' + inv + '</li>';
}
});
bodyHtml += '</ul></div>';
}
// List the prompted credentials user can not access
if (result.data.credentials_unable_to_copy.length > 0) {
bodyHtml += '<div>Node prompted credentials that can not be copied<ul>';
_.forOwn(result.data.credentials_unable_to_copy, function(cred) {
if(cred) {
bodyHtml += '<li>' + cred + '</li>';
}
});
bodyHtml += '</ul></div>';
}
bodyHtml += '</div>';
Prompt({
hdr: 'Copy Workflow',
body: bodyHtml,
action: function() {
$('#prompt-modal').modal('hide');
Wait('start');
TemplateCopyService.copyWorkflow(template.id)
.then(function(result) {
Wait('stop');
$state.go('templates.editWorkflowJobTemplate', {workflow_job_template_id: result.data.id}, {reload: true});
}, function (data) {
Wait('stop');
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Call to copy template failed. POST returned status: ' + status });
});
},
actionText: 'COPY',
class: 'Modal-primaryButton'
});
}
}
else {
Alert('Error: Unable to copy workflow job template', 'You do not have permission to perform this action.');
}
}, function (data) {
Wait('stop');
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Call to copy template failed. GET returned status: ' + status });
});
}
else {
// Something went wrong - Let the user know that we're unable to copy because we don't know
// what type of job template this is
Alert('Error: Unable to determine template type', 'We were unable to determine this template\'s type while copying.');
}
}
else {
Alert('Error: Unable to copy job', 'Template parameter is missing');
}
};
}
];

View File

@ -6,7 +6,6 @@
import templatesService from './templates.service';
import surveyMaker from './survey-maker/main';
import templatesList from './list/main';
import jobTemplates from './job_templates/main';
import workflowAdd from './workflows/add-workflow/main';
import workflowEdit from './workflows/edit-workflow/main';
@ -14,7 +13,6 @@ import labels from './labels/main';
import workflowChart from './workflows/workflow-chart/main';
import workflowMaker from './workflows/workflow-maker/main';
import workflowControls from './workflows/workflow-controls/main';
import templatesListRoute from './list/templates-list.route';
import workflowService from './workflows/workflow.service';
import templateCopyService from './copy-template/template-copy.service';
import WorkflowForm from './workflows.form';
@ -22,9 +20,10 @@ import CompletedJobsList from './completed-jobs.list';
import InventorySourcesList from './inventory-sources.list';
import TemplateList from './templates.list';
import TemplatesStrings from './templates.strings';
import listRoute from '~features/templates/list.route.js';
export default
angular.module('templates', [surveyMaker.name, templatesList.name, jobTemplates.name, labels.name, workflowAdd.name, workflowEdit.name,
angular.module('templates', [surveyMaker.name, jobTemplates.name, labels.name, workflowAdd.name, workflowEdit.name,
workflowChart.name, workflowMaker.name, workflowControls.name
])
.service('TemplatesService', templatesService)
@ -32,6 +31,7 @@ angular.module('templates', [surveyMaker.name, templatesList.name, jobTemplates.
.service('TemplateCopyService', templateCopyService)
.factory('WorkflowForm', WorkflowForm)
.factory('CompletedJobsList', CompletedJobsList)
// TODO: currently being kept arround for rbac selection, templates within projects and orgs, etc.
.factory('TemplateList', TemplateList)
.value('InventorySourcesList', InventorySourcesList)
.service('TemplatesStrings', TemplatesStrings)
@ -897,7 +897,7 @@ angular.module('templates', [surveyMaker.name, templatesList.name, jobTemplates.
states: _.reduce(generated, (result, definition) => {
return result.concat(definition.states);
}, [
stateExtender.buildDefinition(templatesListRoute),
stateExtender.buildDefinition(listRoute),
stateExtender.buildDefinition(workflowMaker),
stateExtender.buildDefinition(inventoryLookup),
stateExtender.buildDefinition(credentialLookup)

View File

@ -1,5 +1,5 @@
@breakpoint-md: 1200px;
@breakpoint-sm: 623px;
@breakpoint-sm: 700px;
.WorkflowResults {
.OnePlusTwo-container(100%, @breakpoint-md);

View File

@ -247,7 +247,7 @@ module.exports = {
templates.waitForElementPresent('i[class$="launch"]');
templates.waitForElementNotPresent('i[class$="launch"]:nth-of-type(2)');
templates.expect.element('.List-titleBadge').text.equal('1');
templates.expect.element('.at-Panel-headingTitleBadge').text.equal('1');
templates.waitForElementVisible('i[class$="launch"]');
templates.click('i[class$="launch"]');

View File

@ -167,35 +167,36 @@ module.exports = {
client.expect.element('#job_template_form').visible;
},
'check template list for unsanitized content': client => {
const itemRow = `#templates_table tr[id="${data.jobTemplate.id}"]`;
const itemName = `${itemRow} td[class*="name-"] a`;
const itemRow = `#row-${data.jobTemplate.id}`;
const itemName = `${itemRow} .at-RowItem-header`;
client.expect.element('div[class^="Panel"] smart-search').visible;
client.expect.element('div[class^="Panel"] smart-search input').enabled;
client.expect.element('.at-Panel smart-search').visible;
client.expect.element('.at-Panel smart-search input').enabled;
client.sendKeys('div[class^="Panel"] smart-search input', `id:${data.jobTemplate.id}`);
client.sendKeys('div[class^="Panel"] smart-search input', client.Keys.ENTER);
client.sendKeys('.at-Panel smart-search input', `id:${data.jobTemplate.id}`);
client.sendKeys('.at-Panel smart-search input', client.Keys.ENTER);
client.expect.element('div.spinny').not.visible;
client.expect.element('.List-titleBadge').text.equal('1');
client.expect.element('.at-Panel-headingTitleBadge').text.equal('1');
client.expect.element(itemName).visible;
client.moveToElement(itemName, 0, 0, () => {
client.expect.element(itemName).attribute('aria-describedby');
client.getAttribute(itemName, 'aria-describedby', ({ value }) => {
const tooltip = `#${value}`;
client.expect.element(tooltip).present;
client.expect.element(tooltip).visible;
client.expect.element('#xss').not.present;
client.expect.element('[class=xss]').not.present;
client.expect.element(tooltip).attribute('innerHTML')
.contains('&lt;div id="xss" class="xss"&gt;test&lt;/div&gt;');
});
});
// TODO: uncomment when tooltips are added
// client.moveToElement(itemName, 0, 0, () => {
// client.expect.element(itemName).attribute('aria-describedby');
//
// client.getAttribute(itemName, 'aria-describedby', ({ value }) => {
// const tooltip = `#${value}`;
//
// client.expect.element(tooltip).present;
// client.expect.element(tooltip).visible;
//
// client.expect.element('#xss').not.present;
// client.expect.element('[class=xss]').not.present;
// client.expect.element(tooltip).attribute('innerHTML')
// .contains('&lt;div id="xss" class="xss"&gt;test&lt;/div&gt;');
// });
// });
client.click(`${itemRow} i[class*="trash"]`);

View File

@ -1,281 +0,0 @@
'use strict';
describe('Controller: TemplatesList', () => {
// Setup
let scope,
rootScope,
state,
TemplatesListController,
GetChoices,
Alert,
Prompt,
InitiatePlaybookRun,
rbacUiControlService,
canAddDeferred,
q,
TemplatesService,
JobTemplateModel,
deleteWorkflowJobTemplateDeferred,
deleteJobTemplateDeferred,
jobTemplateGetDepDeferred,
Dataset;
beforeEach(angular.mock.module('awApp'));
beforeEach(angular.mock.module('templates', ($provide) => {
state = jasmine.createSpyObj('state', [
'$get',
'transitionTo',
'go'
]);
state.params = {
id: 1
};
rbacUiControlService = {
canAdd: function(){
return angular.noop;
}
};
TemplatesService = {
deleteWorkflowJobTemplate: function(){
return angular.noop;
},
deleteJobTemplate: function(){
return angular.noop;
}
};
Dataset = {
data: {
results: []
}
};
GetChoices = jasmine.createSpy('GetChoices');
Alert = jasmine.createSpy('Alert');
Prompt = jasmine.createSpy('Prompt').and.callFake(function(args) {
args.action();
});
InitiatePlaybookRun = jasmine.createSpy('InitiatePlaybookRun');
$provide.value('GetChoices', GetChoices);
$provide.value('Alert', Alert);
$provide.value('Prompt', Prompt);
$provide.value('state', state);
$provide.value('InitiatePlaybookRun', InitiatePlaybookRun);
}));
beforeEach(angular.mock.inject( ($rootScope, $controller, $q, _state_, _ConfigService_, _GetChoices_, _Alert_, _Prompt_, _InitiatePlaybookRun_) => {
scope = $rootScope.$new();
rootScope = $rootScope;
q = $q;
state = _state_;
GetChoices = _GetChoices_;
Alert = _Alert_;
Prompt = _Prompt_;
InitiatePlaybookRun = _InitiatePlaybookRun_;
canAddDeferred = q.defer();
deleteWorkflowJobTemplateDeferred = q.defer();
deleteJobTemplateDeferred = q.defer();
jobTemplateGetDepDeferred = q.defer();
rbacUiControlService.canAdd = jasmine.createSpy('canAdd').and.returnValue(canAddDeferred.promise);
TemplatesService.deleteWorkflowJobTemplate = jasmine.createSpy('deleteWorkflowJobTemplate').and.returnValue(deleteWorkflowJobTemplateDeferred.promise);
TemplatesService.deleteJobTemplate = jasmine.createSpy('deleteJobTemplate').and.returnValue(deleteJobTemplateDeferred.promise);
JobTemplateModel = function () {
this.getDependentResourceCounts = function() {
return jobTemplateGetDepDeferred.promise;
};
};
TemplatesListController = $controller('TemplatesListController', {
$scope: scope,
$rootScope: rootScope,
$state: state,
GetChoices: GetChoices,
Alert: Alert,
Prompt: Prompt,
InitiatePlaybookRun: InitiatePlaybookRun,
rbacUiControlService: rbacUiControlService,
TemplatesService: TemplatesService,
JobTemplateModel: JobTemplateModel,
Dataset: Dataset
});
}));
describe('scope.editJobTemplate()', () => {
it('should call Alert when template param is not present', ()=>{
scope.editJobTemplate();
expect(Alert).toHaveBeenCalledWith('Error: Unable to edit template', 'Template parameter is missing');
});
it('should transition to templates.editJobTemplate when type is "Job Template"', ()=>{
var testTemplate = {
type: "Job Template",
id: 1
};
scope.editJobTemplate(testTemplate);
expect(state.transitionTo).toHaveBeenCalledWith('templates.editJobTemplate', {job_template_id: 1});
});
it('should transition to templates.templates.editWorkflowJobTemplate when type is "Workflow Job Template"', ()=>{
var testTemplate = {
type: "Workflow Job Template",
id: 1
};
scope.editJobTemplate(testTemplate);
expect(state.transitionTo).toHaveBeenCalledWith('templates.editWorkflowJobTemplate', {workflow_job_template_id: 1});
});
it('should call Alert when type is not "Job Template" or "Workflow Job Template"', ()=>{
var testTemplate = {
type: "Some Other Type",
id: 1
};
scope.editJobTemplate(testTemplate);
expect(Alert).toHaveBeenCalledWith('Error: Unable to determine template type', 'We were unable to determine this template\'s type while routing to edit.');
});
});
xdescribe('scope.deleteJobTemplate()', () => {
it('should call Alert when template param is not present', ()=>{
scope.deleteJobTemplate();
expect(Alert).toHaveBeenCalledWith('Error: Unable to delete template', 'Template parameter is missing');
});
it('should call Prompt if template param is present', ()=>{
var testTemplate = {
id: 1,
name: "Test Template",
type: "Job Template"
};
scope.deleteJobTemplate(testTemplate);
jobTemplateGetDepDeferred.resolve([]);
rootScope.$apply();
expect(Prompt).toHaveBeenCalled();
});
it('should call TemplatesService.deleteWorkflowJobTemplate when the user takes affirmative action on the delete modal and type = "Workflow Job Template"', ()=>{
// Note that Prompt has been mocked up above to immediately call the callback function that gets passed in
// which is how we access the private function in the controller
var testTemplate = {
id: 1,
name: "Test Template",
type: "Workflow Job Template"
};
scope.deleteJobTemplate(testTemplate);
jobTemplateGetDepDeferred.resolve([]);
rootScope.$apply();
expect(TemplatesService.deleteWorkflowJobTemplate).toHaveBeenCalled();
});
it('should call TemplatesService.deleteJobTemplate when the user takes affirmative action on the delete modal and type = "Workflow Job Template"', ()=>{
// Note that Prompt has been mocked up above to immediately call the callback function that gets passed in
// which is how we access the private function in the controller
var testTemplate = {
id: 1,
name: "Test Template",
type: "Job Template"
};
scope.deleteJobTemplate(testTemplate);
jobTemplateGetDepDeferred.resolve([]);
rootScope.$apply();
expect(TemplatesService.deleteJobTemplate).toHaveBeenCalled();
});
});
describe('scope.submitJob()', () => {
it('should call Alert when template param is not present', ()=>{
scope.submitJob();
expect(Alert).toHaveBeenCalledWith('Error: Unable to launch template', 'Template parameter is missing');
});
it('should call InitiatePlaybookRun when type is "Job Template"', ()=>{
var testTemplate = {
type: "Job Template",
id: 1
};
scope.submitJob(testTemplate);
expect(InitiatePlaybookRun).toHaveBeenCalled();
});
xit('should call [something] when type is "Workflow Job Template"', ()=>{
var testTemplate = {
type: "Workflow Job Template",
id: 1
};
scope.submitJob(testTemplate);
expect([something]).toHaveBeenCalled();
});
it('should call Alert when type is not "Job Template" or "Workflow Job Template"', ()=>{
var testTemplate = {
type: "Some Other Type",
id: 1
};
scope.submitJob(testTemplate);
expect(Alert).toHaveBeenCalledWith('Error: Unable to determine template type', 'We were unable to determine this template\'s type while launching.');
});
});
describe('scope.scheduleJob()', () => {
it('should transition to jobTemplateSchedules when type is "Job Template"', ()=>{
var testTemplate = {
type: "Job Template",
id: 1
};
scope.scheduleJob(testTemplate);
expect(state.go).toHaveBeenCalledWith('jobTemplateSchedules', {id: 1});
});
it('should transition to workflowJobTemplateSchedules when type is "Workflow Job Template"', ()=>{
var testTemplate = {
type: "Workflow Job Template",
id: 1
};
scope.scheduleJob(testTemplate);
expect(state.go).toHaveBeenCalledWith('workflowJobTemplateSchedules', {id: 1});
});
it('should call Alert when template param is not present', ()=>{
scope.scheduleJob();
expect(Alert).toHaveBeenCalledWith('Error: Unable to schedule job', 'Template parameter is missing');
});
});
});