diff --git a/awx/ui/client/features/index.js b/awx/ui/client/features/index.js index 60598ed192..1cb67b73d4 100644 --- a/awx/ui/client/features/index.js +++ b/awx/ui/client/features/index.js @@ -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; diff --git a/awx/ui/client/features/templates/index.js b/awx/ui/client/features/templates/index.js new file mode 100644 index 0000000000..a2f6ab25a4 --- /dev/null +++ b/awx/ui/client/features/templates/index.js @@ -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; diff --git a/awx/ui/client/features/templates/list-templates.controller.js b/awx/ui/client/features/templates/list-templates.controller.js new file mode 100644 index 0000000000..f9f02a43a0 --- /dev/null +++ b/awx/ui/client/features/templates/list-templates.controller.js @@ -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 ${template.summary_fields.modified_by.username}`; + } + 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 ${template.summary_fields.modified_by.username}`; + // } + + 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 = `
${strings.get('deleteResource.CONFIRM', 'template')}
`; + + 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; diff --git a/awx/ui/client/src/templates/list/templates-list.route.js b/awx/ui/client/features/templates/list.route.js similarity index 55% rename from awx/ui/client/src/templates/list/templates-list.route.js rename to awx/ui/client/features/templates/list.route.js index a0317842e5..384164b421 100644 --- a/awx/ui/client/src/templates/list/templates-list.route.js +++ b/awx/ui/client/features/templates/list.route.js @@ -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); diff --git a/awx/ui/client/features/templates/list.view.html b/awx/ui/client/features/templates/list.view.html new file mode 100644 index 0000000000..d68ace80e5 --- /dev/null +++ b/awx/ui/client/features/templates/list.view.html @@ -0,0 +1,118 @@ +
+ + + {{:: vm.strings.get('list.PANEL_TITLE') }} +
+ {{ template_dataset.count }} +
+
+ + +
+ + +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + +
+
+ + + + + + +
+
+
+ + +
+
diff --git a/awx/ui/client/features/templates/templates.strings.js b/awx/ui/client/features/templates/templates.strings.js new file mode 100644 index 0000000000..0c60c10568 --- /dev/null +++ b/awx/ui/client/features/templates/templates.strings.js @@ -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; diff --git a/awx/ui/client/lib/components/_index.less b/awx/ui/client/lib/components/_index.less index 7bb09843e0..6fdef9ece1 100644 --- a/awx/ui/client/lib/components/_index.less +++ b/awx/ui/client/lib/components/_index.less @@ -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'; diff --git a/awx/ui/client/lib/components/components.strings.js b/awx/ui/client/lib/components/components.strings.js index 1571d9b532..93f5ab1416 100644 --- a/awx/ui/client/lib/components/components.strings.js +++ b/awx/ui/client/lib/components/components.strings.js @@ -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']; diff --git a/awx/ui/client/lib/components/index.js b/awx/ui/client/lib/components/index.js index 33377960bb..6f15cae762 100644 --- a/awx/ui/client/lib/components/index.js +++ b/awx/ui/client/lib/components/index.js @@ -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) diff --git a/awx/ui/client/lib/components/layout/_index.less b/awx/ui/client/lib/components/layout/_index.less index 78e7bf42b6..03e9c4bb2b 100644 --- a/awx/ui/client/lib/components/layout/_index.less +++ b/awx/ui/client/lib/components/layout/_index.less @@ -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 @@ } } } -} \ No newline at end of file +} diff --git a/awx/ui/client/lib/components/list/_index.less b/awx/ui/client/lib/components/list/_index.less new file mode 100644 index 0000000000..cd1fa9a023 --- /dev/null +++ b/awx/ui/client/lib/components/list/_index.less @@ -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; + } +} diff --git a/awx/ui/client/lib/components/list/list.directive.js b/awx/ui/client/lib/components/list/list.directive.js new file mode 100644 index 0000000000..28c3c546ea --- /dev/null +++ b/awx/ui/client/lib/components/list/list.directive.js @@ -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; diff --git a/awx/ui/client/lib/components/list/list.partial.html b/awx/ui/client/lib/components/list/list.partial.html new file mode 100644 index 0000000000..3bd90e6663 --- /dev/null +++ b/awx/ui/client/lib/components/list/list.partial.html @@ -0,0 +1,7 @@ +
+
+
+
+ {{ emptyListReason || vm.strings.get("list.DEFAULT_EMPTY_LIST") }} +
+
diff --git a/awx/ui/client/lib/components/list/row-action.directive.js b/awx/ui/client/lib/components/list/row-action.directive.js new file mode 100644 index 0000000000..18b1835499 --- /dev/null +++ b/awx/ui/client/lib/components/list/row-action.directive.js @@ -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; diff --git a/awx/ui/client/lib/components/list/row-action.partial.html b/awx/ui/client/lib/components/list/row-action.partial.html new file mode 100644 index 0000000000..58a7479769 --- /dev/null +++ b/awx/ui/client/lib/components/list/row-action.partial.html @@ -0,0 +1,4 @@ +
+ +
diff --git a/awx/ui/client/lib/components/list/row-item.directive.js b/awx/ui/client/lib/components/list/row-item.directive.js new file mode 100644 index 0000000000..972008f7a2 --- /dev/null +++ b/awx/ui/client/lib/components/list/row-item.directive.js @@ -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; diff --git a/awx/ui/client/lib/components/list/row-item.partial.html b/awx/ui/client/lib/components/list/row-item.partial.html new file mode 100644 index 0000000000..a9b81ae20c --- /dev/null +++ b/awx/ui/client/lib/components/list/row-item.partial.html @@ -0,0 +1,38 @@ +
+
+ {{ headerValue }} +
+
+ {{ headerValue }} +
+
+ {{ headerTag }} +
+
+ {{ labelValue }} +
+
+ {{ value }} +
+
+
+ + +
+
+ + + + + + + + + {{ tag.name }} +
+
+
diff --git a/awx/ui/client/lib/components/list/row.directive.js b/awx/ui/client/lib/components/list/row.directive.js new file mode 100644 index 0000000000..50adb3d8a4 --- /dev/null +++ b/awx/ui/client/lib/components/list/row.directive.js @@ -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; diff --git a/awx/ui/client/lib/components/list/row.partial.html b/awx/ui/client/lib/components/list/row.partial.html new file mode 100644 index 0000000000..385a825bc6 --- /dev/null +++ b/awx/ui/client/lib/components/list/row.partial.html @@ -0,0 +1,2 @@ +
+
diff --git a/awx/ui/client/lib/components/panel/_index.less b/awx/ui/client/lib/components/panel/_index.less index 55d7fdc0e4..7e50d8593a 100644 --- a/awx/ui/client/lib/components/panel/_index.less +++ b/awx/ui/client/lib/components/panel/_index.less @@ -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; +} diff --git a/awx/ui/client/lib/components/panel/heading.directive.js b/awx/ui/client/lib/components/panel/heading.directive.js index 7850f1c354..8850c10c2f 100644 --- a/awx/ui/client/lib/components/panel/heading.directive.js +++ b/awx/ui/client/lib/components/panel/heading.directive.js @@ -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); } diff --git a/awx/ui/client/lib/components/panel/heading.partial.html b/awx/ui/client/lib/components/panel/heading.partial.html index 7026a3f11a..e57e6a880d 100644 --- a/awx/ui/client/lib/components/panel/heading.partial.html +++ b/awx/ui/client/lib/components/panel/heading.partial.html @@ -1,12 +1,20 @@ -
-
+
+

-
+
+
+

+ +

+
diff --git a/awx/ui/client/lib/components/truncate/_index.less b/awx/ui/client/lib/components/truncate/_index.less index e06a8f6eb9..bc26f1c820 100644 --- a/awx/ui/client/lib/components/truncate/_index.less +++ b/awx/ui/client/lib/components/truncate/_index.less @@ -7,7 +7,7 @@ } .at-Truncate-copy { - color: @at-gray-dark-2x; + color: @at-gray-b7; cursor: pointer; margin-left: 10px; diff --git a/awx/ui/client/lib/models/UnifiedJobTemplate.js b/awx/ui/client/lib/models/UnifiedJobTemplate.js new file mode 100644 index 0000000000..94c066af6a --- /dev/null +++ b/awx/ui/client/lib/models/UnifiedJobTemplate.js @@ -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; diff --git a/awx/ui/client/lib/models/index.js b/awx/ui/client/lib/models/index.js index 08de2e3786..aa2c6dfa68 100644 --- a/awx/ui/client/lib/models/index.js +++ b/awx/ui/client/lib/models/index.js @@ -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; diff --git a/awx/ui/client/lib/services/base-string.service.js b/awx/ui/client/lib/services/base-string.service.js index f086c987a3..a14871ae68 100644 --- a/awx/ui/client/lib/services/base-string.service.js +++ b/awx/ui/client/lib/services/base-string.service.js @@ -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 diff --git a/awx/ui/client/lib/theme/_mixins.less b/awx/ui/client/lib/theme/_mixins.less index e72f8cd391..6dc36a7b24 100644 --- a/awx/ui/client/lib/theme/_mixins.less +++ b/awx/ui/client/lib/theme/_mixins.less @@ -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] { diff --git a/awx/ui/client/lib/theme/_variables.less b/awx/ui/client/lib/theme/_variables.less index 2ff0156021..be5cde41ee 100644 --- a/awx/ui/client/lib/theme/_variables.less +++ b/awx/ui/client/lib/theme/_variables.less @@ -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; diff --git a/awx/ui/client/src/projects/projects-templates.route.js b/awx/ui/client/src/projects/projects-templates.route.js index 0015c9661e..f15c229edd 100644 --- a/awx/ui/client/src/projects/projects-templates.route.js +++ b/awx/ui/client/src/projects/projects-templates.route.js @@ -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({ diff --git a/awx/ui/client/src/shared/smart-search/smart-search.controller.js b/awx/ui/client/src/shared/smart-search/smart-search.controller.js index 8721b7a467..87a3b7ad18 100644 --- a/awx/ui/client/src/shared/smart-search/smart-search.controller.js +++ b/awx/ui/client/src/shared/smart-search/smart-search.controller.js @@ -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; diff --git a/awx/ui/client/src/smart-status/smart-status.block.less b/awx/ui/client/src/smart-status/smart-status.block.less index 1c9138e331..96ce93228f 100644 --- a/awx/ui/client/src/smart-status/smart-status.block.less +++ b/awx/ui/client/src/smart-status/smart-status.block.less @@ -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; diff --git a/awx/ui/client/src/smart-status/smart-status.controller.js b/awx/ui/client/src/smart-status/smart-status.controller.js index 1283552a55..2debca7d7c 100644 --- a/awx/ui/client/src/smart-status/smart-status.controller.js +++ b/awx/ui/client/src/smart-status/smart-status.controller.js @@ -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(); diff --git a/awx/ui/client/src/smart-status/smart-status.partial.html b/awx/ui/client/src/smart-status/smart-status.partial.html index ec1a81fac3..1718632bb2 100644 --- a/awx/ui/client/src/smart-status/smart-status.partial.html +++ b/awx/ui/client/src/smart-status/smart-status.partial.html @@ -9,13 +9,21 @@ data-container="body" tooltipInnerClass="SmartStatus-tooltip" title=""> - - +
+
+
+
+
+
+
+
+
+
+
diff --git a/awx/ui/client/src/templates/labels/labelsList.directive.js b/awx/ui/client/src/templates/labels/labelsList.directive.js index 180371c862..208c111b80 100644 --- a/awx/ui/client/src/templates/labels/labelsList.directive.js +++ b/awx/ui/client/src/templates/labels/labelsList.directive.js @@ -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; + } + }); + } } }; diff --git a/awx/ui/client/src/templates/labels/labelsList.partial.html b/awx/ui/client/src/templates/labels/labelsList.partial.html index 15ca1e905a..71e9da8b7e 100644 --- a/awx/ui/client/src/templates/labels/labelsList.partial.html +++ b/awx/ui/client/src/templates/labels/labelsList.partial.html @@ -1,4 +1,4 @@ -
+
@@ -9,6 +9,25 @@
View More
+ ng-click="seeMore()" ng-if="!isRowItem">View More
View Less
+ ng-click="seeLess()" ng-if="!isRowItem">View Less
+
+
+ Labels +
+
+
+ +
+
+ {{ label.name }} +
+
+
View More
+
View Less
+
diff --git a/awx/ui/client/src/templates/list/main.js b/awx/ui/client/src/templates/list/main.js deleted file mode 100644 index d802c64814..0000000000 --- a/awx/ui/client/src/templates/list/main.js +++ /dev/null @@ -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); diff --git a/awx/ui/client/src/templates/list/templates-list.controller.js b/awx/ui/client/src/templates/list/templates-list.controller.js deleted file mode 100644 index a0575730fd..0000000000 --- a/awx/ui/client/src/templates/list/templates-list.controller.js +++ /dev/null @@ -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 = `
${TemplatesStrings.get('deleteResource.CONFIRM', 'job template')}
`; - - counts.forEach(countObj => { - if(countObj.count && countObj.count > 0) { - invalidateRelatedLines.push(`
${countObj.label}${countObj.count}
`); - } - }); - - if (invalidateRelatedLines && invalidateRelatedLines.length > 0) { - deleteModalBody = `
${TemplatesStrings.get('deleteResource.USED_BY', 'job template')} ${TemplatesStrings.get('deleteResource.CONFIRM', 'job template')}
`; - 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 = ` -
- 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. -
-
`; - - // List the unified job templates user can not access - if (result.data.templates_unable_to_copy.length > 0) { - bodyHtml += '
Unified Job Templates that can not be copied
'; - } - // List the prompted inventories user can not access - if (result.data.inventories_unable_to_copy.length > 0) { - bodyHtml += '
Node prompted inventories that can not be copied
'; - } - // List the prompted credentials user can not access - if (result.data.credentials_unable_to_copy.length > 0) { - bodyHtml += '
Node prompted credentials that can not be copied
'; - } - - bodyHtml += '
'; - - - 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'); - } - }; - } -]; diff --git a/awx/ui/client/src/templates/main.js b/awx/ui/client/src/templates/main.js index b8045c8ac3..53cfe2ce2e 100644 --- a/awx/ui/client/src/templates/main.js +++ b/awx/ui/client/src/templates/main.js @@ -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) diff --git a/awx/ui/client/src/workflow-results/workflow-results.block.less b/awx/ui/client/src/workflow-results/workflow-results.block.less index 48f927eb3a..04e15c0c30 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.block.less +++ b/awx/ui/client/src/workflow-results/workflow-results.block.less @@ -1,5 +1,5 @@ @breakpoint-md: 1200px; -@breakpoint-sm: 623px; +@breakpoint-sm: 700px; .WorkflowResults { .OnePlusTwo-container(100%, @breakpoint-md); diff --git a/awx/ui/test/e2e/tests/smoke.js b/awx/ui/test/e2e/tests/smoke.js index b3193d6d9c..b65e1d5f8a 100644 --- a/awx/ui/test/e2e/tests/smoke.js +++ b/awx/ui/test/e2e/tests/smoke.js @@ -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"]'); diff --git a/awx/ui/test/e2e/tests/test-xss.js b/awx/ui/test/e2e/tests/test-xss.js index d5a1767696..7888824691 100644 --- a/awx/ui/test/e2e/tests/test-xss.js +++ b/awx/ui/test/e2e/tests/test-xss.js @@ -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('<div id="xss" class="xss">test</div>'); - }); - }); + // 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('<div id="xss" class="xss">test</div>'); + // }); + // }); client.click(`${itemRow} i[class*="trash"]`); diff --git a/awx/ui/test/spec/templates/templates-list.controller-test.js b/awx/ui/test/spec/templates/templates-list.controller-test.js deleted file mode 100644 index 149eb1937d..0000000000 --- a/awx/ui/test/spec/templates/templates-list.controller-test.js +++ /dev/null @@ -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'); - }); - - }); - -});