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
42 changed files with 1077 additions and 802 deletions

View File

@@ -3,6 +3,7 @@ import atLibComponents from '~components';
import atLibModels from '~models'; import atLibModels from '~models';
import atFeaturesCredentials from '~features/credentials'; import atFeaturesCredentials from '~features/credentials';
import atFeaturesTemplates from '~features/templates';
const MODULE_NAME = 'at.features'; const MODULE_NAME = 'at.features';
@@ -10,7 +11,8 @@ angular.module(MODULE_NAME, [
atLibServices, atLibServices,
atLibComponents, atLibComponents,
atLibModels, atLibModels,
atFeaturesCredentials atFeaturesCredentials,
atFeaturesTemplates
]); ]);
export default MODULE_NAME; 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 @@
/************************************************* import ListController from './list-templates.controller';
* Copyright (c) 2016 Ansible, Inc. const listTemplate = require('~features/templates/list.view.html');
* import { N_ } from '../../src/i18n';
* All Rights Reserved
*************************************************/
import { N_ } from '../../i18n'; function TemplatesResolve (UnifiedJobTemplate) {
return new UnifiedJobTemplate(['get', 'options']);
}
TemplatesResolve.$inject = [
'UnifiedJobTemplateModel'
];
export default { export default {
name: 'templates', name: 'templates',
route: '/templates', route: '/templates',
ncyBreadcrumb: { 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") label: N_("TEMPLATES")
}, },
data: { data: {
@@ -32,18 +41,13 @@ export default {
searchPrefix: 'template', searchPrefix: 'template',
views: { views: {
'@': { '@': {
controller: 'TemplatesListController', controller: ListController,
templateProvider: function(TemplateList, generateList) { templateUrl: listTemplate,
let html = generateList.build({ controllerAs: 'vm'
list: TemplateList,
mode: 'edit'
});
html = generateList.wrapPanel(html);
return generateList.insertFormView() + html;
}
} }
}, },
resolve: { resolve: {
resolvedModels: TemplatesResolve,
Dataset: ['TemplateList', 'QuerySet', '$stateParams', 'GetBasePath', Dataset: ['TemplateList', 'QuerySet', '$stateParams', 'GetBasePath',
function(list, qs, $stateParams, GetBasePath) { function(list, qs, $stateParams, GetBasePath) {
let path = GetBasePath(list.basePath) || GetBasePath(list.name); 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 'action/_index';
@import 'input/_index'; @import 'input/_index';
@import 'layout/_index'; @import 'layout/_index';
@import 'list/_index';
@import 'modal/_index'; @import 'modal/_index';
@import 'panel/_index'; @import 'panel/_index';
@import 'popover/_index'; @import 'popover/_index';

View File

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

View File

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

View File

@@ -175,9 +175,7 @@
} }
} }
@breakpoint-sm: 700px; @media screen and (max-width: @at-breakpoint-mobile-layout) {
@media screen and (max-width: @breakpoint-sm) {
.at-Layout { .at-Layout {
&-side { &-side {
top: 60px; 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; padding: 0;
} }
.at-Panel-headingRow {
margin-bottom: 20px;
}
.at-Panel-dismiss { .at-Panel-dismiss {
.at-mixin-ButtonIcon(); .at-mixin-ButtonIcon();
color: @at-color-icon-dismiss; color: @at-color-icon-dismiss;
@@ -24,3 +28,19 @@
.at-mixin-Heading(@at-font-size-panel-heading); .at-mixin-Heading(@at-font-size-panel-heading);
text-transform: none; 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'); const templateUrl = require('~components/panel/heading.partial.html');
function link (scope, el, attrs, panel) { function link (scope, el, attrs, panel) {
scope.hideDismiss = Boolean(attrs.hideDismiss);
panel.use(scope); panel.use(scope);
} }

View File

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

View File

@@ -7,7 +7,7 @@
} }
.at-Truncate-copy { .at-Truncate-copy {
color: @at-gray-dark-2x; color: @at-gray-b7;
cursor: pointer; cursor: pointer;
margin-left: 10px; 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 InventoryScript from '~models/InventoryScript';
import ModelsStrings from '~models/models.strings'; import ModelsStrings from '~models/models.strings';
import UnifiedJobTemplate from '~models/UnifiedJobTemplate';
const MODULE_NAME = 'at.lib.models'; const MODULE_NAME = 'at.lib.models';
@@ -33,6 +34,7 @@ angular
.service('InventorySourceModel', InventorySource) .service('InventorySourceModel', InventorySource)
.service('InventoryModel', Inventory) .service('InventoryModel', Inventory)
.service('InventoryScriptModel', InventoryScript) .service('InventoryScriptModel', InventoryScript)
.service('ModelsStrings', ModelsStrings); .service('ModelsStrings', ModelsStrings)
.service('UnifiedJobTemplateModel', UnifiedJobTemplate);
export default MODULE_NAME; export default MODULE_NAME;

View File

@@ -61,9 +61,16 @@ function BaseStringService (namespace) {
this.SAVE = t.s('SAVE'); this.SAVE = t.s('SAVE');
this.OK = t.s('OK'); this.OK = t.s('OK');
this.deleteResource = { this.deleteResource = {
HEADER: t.s('Delete'),
USED_BY: resourceType => t.s('The {{ resourceType }} is currently being used by other resources.', { resourceType }), 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 }) 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 * 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') { .at-mixin-ButtonColor (@background, @color, @hover: '@{background}-hover') {
background-color: @@background; background-color: @@background;
border-color: @@background;
&, &:hover, &:focus { &, &:hover, &:focus {
color: @@color; color: @@color;
@@ -52,6 +53,7 @@
&:hover, &:focus { &:hover, &:focus {
background-color: @@hover; background-color: @@hover;
border-color: @@hover;
} }
&[disabled] { &[disabled] {

View File

@@ -14,22 +14,24 @@
* 1. Colors * 1. Colors
* 2. Typography * 2. Typography
* 3. Layout * 3. Layout
* 4. Breakpoints
* *
*/ */
// 1. Colors -------------------------------------------------------------------------------------- // 1. Colors --------------------------------------------------------------------------------------
@at-gray-light-3x: #fcfcfc; @at-gray-fc: #fcfcfc;
@at-gray-light-2-5x: #fafafa; @at-gray-fa: #fafafa;
@at-gray-light-2x: #f2f2f2; @at-gray-f2: #f2f2f2;
@at-gray-light: #ebebeb; @at-gray-f6: #f6f6f6;
@at-gray: #e1e1e1; @at-gray-eb: #ebebeb;
@at-gray-dark: #d7d7d7; @at-gray-e1: #e1e1e1;
@at-gray-dark-2x: #b7b7b7; @at-gray-d7: #d7d7d7;
@at-gray-dark-3x: #A9A9A9; @at-gray-b7: #b7b7b7;
@at-gray-dark-4x: #848992; @at-gray-a9: #a9a9a9;
@at-gray-dark-5x: #707070; @at-gray-848992: #848992;
@at-gray-dark-6x: #161b1f; @at-gray-70: #707070;
@at-gray-161b1f: #161b1f;
@at-white: #ffffff; @at-white: #ffffff;
@at-white-hover: #f2f2f2; @at-white-hover: #f2f2f2;
@@ -38,7 +40,7 @@
@at-blue-hover: #286090; @at-blue-hover: #286090;
@at-green: #5cb85c; @at-green: #5cb85c;
@at-green-hover: #449D44; @at-green-hover: #449d44;
@at-orange: #f0ad4e; @at-orange: #f0ad4e;
@at-orange-hover: #ec971f; @at-orange-hover: #ec971f;
@@ -66,6 +68,11 @@
@at-space-2x: 10px; @at-space-2x: 10px;
@at-space-3x: 15px; @at-space-3x: 15px;
@at-space-4x: 20px; @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 * All variables used in the UI. Use these variables directly during the development of components
@@ -83,6 +90,7 @@
* 3. Layout * 3. Layout
* 4. Buttons * 4. Buttons
* 5. Misc * 5. Misc
* 6. Breakpoints
* *
*/ */
@@ -106,64 +114,79 @@
@at-color-success: @at-green; @at-color-success: @at-green;
@at-color-success-hover: @at-green-hover; @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-text-dark: @at-white;
@at-color-body-background: @at-gray-light-3x; @at-color-body-background: @at-gray-fc;
@at-color-body-text: @at-gray-dark-5x; @at-color-body-text: @at-gray-70;
@at-color-button-border-default: @at-gray-dark-2x; @at-color-button-border-default: @at-gray-b7;
@at-color-button-text-default: @at-gray-dark-5x; @at-color-button-text-default: @at-gray-70;
@at-color-tab-default-active: @at-gray-dark-4x; @at-color-tab-default-active: @at-gray-848992;
@at-color-tab-border-default-active: @at-gray-dark-4x; @at-color-tab-border-default-active: @at-gray-848992;
@at-color-tab-text-default-active: @at-white; @at-color-tab-text-default-active: @at-white;
@at-color-tab-default-disabled: @at-white; @at-color-tab-default-disabled: @at-white;
@at-color-tab-border-default-disabled: @at-gray-dark-2x; @at-color-tab-border-default-disabled: @at-gray-b7;
@at-color-tab-text-default-disabled: @at-gray-dark-5x; @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-background: @at-gray-fc;
@at-color-input-border: @at-gray-dark-2x; @at-color-input-border: @at-gray-b7;
@at-color-input-button: @at-gray-light-3x; @at-color-input-button: @at-gray-fc;
@at-color-input-button-hover: @at-gray-light-2x; @at-color-input-button-hover: @at-gray-f2;
@at-color-input-disabled: @at-gray-light; @at-color-input-disabled: @at-gray-eb;
@at-color-input-readonly: @at-color-input-background; @at-color-input-readonly: @at-color-input-background;
@at-color-input-error: @at-color-error; @at-color-input-error: @at-color-error;
@at-color-input-focus: @at-color-info; @at-color-input-focus: @at-color-info;
@at-color-input-hint: @at-gray-dark-4x; @at-color-input-hint: @at-gray-848992;
@at-color-input-icon: @at-gray-dark-2x; @at-color-input-icon: @at-gray-b7;
@at-color-input-placeholder: @at-gray-dark-4x; @at-color-input-placeholder: @at-gray-848992;
@at-color-input-text: @at-gray-dark-6x; @at-color-input-text: @at-gray-161b1f;
@at-color-icon-dismiss: @at-gray-dark; @at-color-icon-dismiss: @at-gray-d7;
@at-color-icon-popover: @at-gray-dark-4x; @at-color-icon-popover: @at-gray-848992;
@at-color-icon-hover: @at-gray-dark-4x; @at-color-icon-hover: @at-gray-848992;
@at-color-panel-heading: @at-gray-dark-5x; @at-color-panel-heading: @at-gray-70;
@at-color-panel-border: @at-gray-dark-2x; @at-color-panel-border: @at-gray-b7;
@at-color-search-key-active: @at-blue; @at-color-search-key-active: @at-blue;
@at-color-table-header-background: @at-gray-light; @at-color-table-header-background: @at-gray-eb;
@at-color-line-separator: @at-gray; @at-color-line-separator: @at-gray-e1;
@at-color-top-nav-background: @at-white; @at-color-top-nav-background: @at-white;
@at-color-top-nav-border-bottom: @at-gray-dark-2x; @at-color-top-nav-border-bottom: @at-gray-b7;
@at-color-top-nav-item-text: @at-gray-dark-5x; @at-color-top-nav-item-text: @at-gray-70;
@at-color-top-nav-item-icon: @at-gray-dark-4x; @at-color-top-nav-item-icon: @at-gray-848992;
@at-color-top-nav-item-icon-socket-outline: @at-white; @at-color-top-nav-item-icon-socket-outline: @at-white;
@at-color-top-nav-item-background-hover: @at-gray-light-2-5x; @at-color-top-nav-item-background-hover: @at-gray-fa;
@at-color-side-nav-background: @at-gray-dark-4x; @at-color-side-nav-background: @at-gray-848992;
@at-color-side-nav-content: @at-white; @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-side-nav-item-border-hover: @at-white;
@at-color-footer-background: @at-gray-light-3x; @at-color-footer-background: @at-gray-fc;
@at-color-footer: @at-gray-dark-5x; @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 ---------------------------------------------------------------------------------- // 2. Typography ----------------------------------------------------------------------------------
@@ -181,6 +204,9 @@
@at-font-size-navigation: @at-font-size-3x; @at-font-size-navigation: @at-font-size-3x;
@at-font-size-table-heading: @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-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-body: @at-font-weight;
@at-font-weight-heading: @at-font-weight-2x; @at-font-weight-heading: @at-font-weight-2x;
@@ -199,6 +225,10 @@
@at-padding-between-side-nav-icon-text: @at-space-3x; @at-padding-between-side-nav-icon-text: @at-space-3x;
@at-padding-footer-right: @at-space-4x; @at-padding-footer-right: @at-space-4x;
@at-padding-footer-bottom: @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-input-message: @at-space;
@at-margin-item-column: @at-space-3x; @at-margin-item-column: @at-space-3x;
@@ -215,6 +245,18 @@
@at-margin-top-search-key: @at-space-2x; @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-divider: @at-margin-panel;
@at-height-input: 30px; @at-height-input: 30px;
@at-height-textarea: 144px; @at-height-textarea: 144px;
@@ -226,11 +268,21 @@
@at-height-side-nav-item-icon: 20px; @at-height-side-nav-item-icon: 20px;
@at-height-side-nav-spacer: 20px; @at-height-side-nav-spacer: 20px;
@at-height-top-side-nav-makeup: 55px; @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-sm: 72px;
@at-width-input-button-md: 84px; @at-width-input-button-md: 84px;
@at-width-collapsed-side-nav: 50px; @at-width-collapsed-side-nav: 50px;
@at-width-expanded-side-nav: 200px; @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 --------------------------------------------------------------------------------- // 4. Transitions ---------------------------------------------------------------------------------
@@ -249,3 +301,10 @@
@at-z-index-side-nav: 1030; @at-z-index-side-nav: 1030;
@at-z-index-footer: 1020; @at-z-index-footer: 1020;
@at-border-default-width: 1px; @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") label: N_("JOB TEMPLATES")
}, },
views: { 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': { 'related': {
templateProvider: function(FormDefinition, GenerateForm) { templateProvider: function(FormDefinition, GenerateForm) {
let html = GenerateForm.buildCollection({ let html = GenerateForm.buildCollection({

View File

@@ -44,7 +44,9 @@ export default ['$stateParams', '$scope', '$state', 'GetBasePath', 'QuerySet', '
qs.initFieldset(path, $scope.djangoModel).then((data) => { qs.initFieldset(path, $scope.djangoModel).then((data) => {
$scope.models = data.models; $scope.models = data.models;
$scope.options = data.options.data; $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'); $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) => { qs.search(path, queryset).then((res) => {
$scope.dataset = res.data; $scope.dataset = res.data;
$scope.collection = res.data.results; $scope.collection = res.data.results;
$scope.$emit('updateDataset', res.data);
}); });
$scope.searchTerm = null; $scope.searchTerm = null;

View File

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

View File

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

View File

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

View File

@@ -16,6 +16,7 @@ export default
templateUrl: templateUrl('templates/labels/labelsList'), templateUrl: templateUrl('templates/labels/labelsList'),
link: function(scope, element, attrs) { link: function(scope, element, attrs) {
scope.showDelete = attrs.showDelete === 'true'; scope.showDelete = attrs.showDelete === 'true';
scope.isRowItem = attrs.isRowItem === 'true';
scope.seeMoreInactive = true; scope.seeMoreInactive = true;
var getNext = function(data, arr, resolve) { var getNext = function(data, arr, resolve) {
@@ -91,18 +92,23 @@ export default
}); });
}; };
scope.$watchCollection(scope.$parent.list.iterator, function() { if (scope.$parent.$parent.template) {
// To keep the array of labels fresh, we need to set up a watcher - otherwise, the scope.labels = scope.$parent.$parent.template.summary_fields.labels.results.slice(0, 5);
// array will get set initially and then never be updated as labels are removed scope.count = scope.$parent.$parent.template.summary_fields.labels.count;
if (scope[scope.$parent.list.iterator].summary_fields.labels){ } else {
scope.labels = scope[scope.$parent.list.iterator].summary_fields.labels.results.slice(0, 5); scope.$watchCollection(scope.$parent.list.iterator, function() {
scope.count = scope[scope.$parent.list.iterator].summary_fields.labels.count; // 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
else{ if (scope[scope.$parent.list.iterator].summary_fields.labels){
scope.labels = null; scope.labels = scope[scope.$parent.list.iterator].summary_fields.labels.results.slice(0, 5);
scope.count = null; 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" <div class="LabelList-deleteContainer"
ng-click="deleteLabel(template, label)" ng-click="deleteLabel(template, label)"
ng-show="showDelete && template.summary_fields.user_capabilities.edit"> ng-show="showDelete && template.summary_fields.user_capabilities.edit">
@@ -9,6 +9,25 @@
</div> </div>
</div> </div>
<div class="LabelList-seeMoreLess" ng-show="count > 5 && seeMoreInactive" <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" <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 templatesService from './templates.service';
import surveyMaker from './survey-maker/main'; import surveyMaker from './survey-maker/main';
import templatesList from './list/main';
import jobTemplates from './job_templates/main'; import jobTemplates from './job_templates/main';
import workflowAdd from './workflows/add-workflow/main'; import workflowAdd from './workflows/add-workflow/main';
import workflowEdit from './workflows/edit-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 workflowChart from './workflows/workflow-chart/main';
import workflowMaker from './workflows/workflow-maker/main'; import workflowMaker from './workflows/workflow-maker/main';
import workflowControls from './workflows/workflow-controls/main'; import workflowControls from './workflows/workflow-controls/main';
import templatesListRoute from './list/templates-list.route';
import workflowService from './workflows/workflow.service'; import workflowService from './workflows/workflow.service';
import templateCopyService from './copy-template/template-copy.service'; import templateCopyService from './copy-template/template-copy.service';
import WorkflowForm from './workflows.form'; import WorkflowForm from './workflows.form';
@@ -22,9 +20,10 @@ import CompletedJobsList from './completed-jobs.list';
import InventorySourcesList from './inventory-sources.list'; import InventorySourcesList from './inventory-sources.list';
import TemplateList from './templates.list'; import TemplateList from './templates.list';
import TemplatesStrings from './templates.strings'; import TemplatesStrings from './templates.strings';
import listRoute from '~features/templates/list.route.js';
export default 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 workflowChart.name, workflowMaker.name, workflowControls.name
]) ])
.service('TemplatesService', templatesService) .service('TemplatesService', templatesService)
@@ -32,6 +31,7 @@ angular.module('templates', [surveyMaker.name, templatesList.name, jobTemplates.
.service('TemplateCopyService', templateCopyService) .service('TemplateCopyService', templateCopyService)
.factory('WorkflowForm', WorkflowForm) .factory('WorkflowForm', WorkflowForm)
.factory('CompletedJobsList', CompletedJobsList) .factory('CompletedJobsList', CompletedJobsList)
// TODO: currently being kept arround for rbac selection, templates within projects and orgs, etc.
.factory('TemplateList', TemplateList) .factory('TemplateList', TemplateList)
.value('InventorySourcesList', InventorySourcesList) .value('InventorySourcesList', InventorySourcesList)
.service('TemplatesStrings', TemplatesStrings) .service('TemplatesStrings', TemplatesStrings)
@@ -897,7 +897,7 @@ angular.module('templates', [surveyMaker.name, templatesList.name, jobTemplates.
states: _.reduce(generated, (result, definition) => { states: _.reduce(generated, (result, definition) => {
return result.concat(definition.states); return result.concat(definition.states);
}, [ }, [
stateExtender.buildDefinition(templatesListRoute), stateExtender.buildDefinition(listRoute),
stateExtender.buildDefinition(workflowMaker), stateExtender.buildDefinition(workflowMaker),
stateExtender.buildDefinition(inventoryLookup), stateExtender.buildDefinition(inventoryLookup),
stateExtender.buildDefinition(credentialLookup) stateExtender.buildDefinition(credentialLookup)

View File

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

View File

@@ -247,7 +247,7 @@ module.exports = {
templates.waitForElementPresent('i[class$="launch"]'); templates.waitForElementPresent('i[class$="launch"]');
templates.waitForElementNotPresent('i[class$="launch"]:nth-of-type(2)'); 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.waitForElementVisible('i[class$="launch"]');
templates.click('i[class$="launch"]'); templates.click('i[class$="launch"]');

View File

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