diff --git a/awx/ui/client/lib/components/_index.less b/awx/ui/client/lib/components/_index.less index 729d577b54..1902014e11 100644 --- a/awx/ui/client/lib/components/_index.less +++ b/awx/ui/client/lib/components/_index.less @@ -5,3 +5,4 @@ @import 'popover/_index'; @import 'tabs/_index'; @import 'utility/_index'; +@import 'truncate/_index'; diff --git a/awx/ui/client/lib/components/components.strings.js b/awx/ui/client/lib/components/components.strings.js index 804dfd81ce..2fba205de5 100644 --- a/awx/ui/client/lib/components/components.strings.js +++ b/awx/ui/client/lib/components/components.strings.js @@ -1,6 +1,6 @@ function ComponentsStrings (BaseString) { BaseString.call(this, 'components'); - + let t = this.t; let ns = this.components; @@ -42,6 +42,11 @@ function ComponentsStrings (BaseString) { ns.lookup = { NOT_FOUND: t('That value was not found. Please enter or select a valid value.') }; + + ns.truncate = { + DEFAULT: t('Copy full revision to clipboard.'), + COPIED: t('Copied to clipboard.') + } } ComponentsStrings.$inject = ['BaseStringService']; diff --git a/awx/ui/client/lib/components/index.js b/awx/ui/client/lib/components/index.js index fa8b5d6d0b..40a0264e28 100644 --- a/awx/ui/client/lib/components/index.js +++ b/awx/ui/client/lib/components/index.js @@ -19,6 +19,7 @@ import panelBody from './panel/body.directive'; import popover from './popover/popover.directive'; import tab from './tabs/tab.directive'; import tabGroup from './tabs/group.directive'; +import truncate from './truncate/truncate.directive'; import BaseInputController from './input/base.controller'; import ComponentsStrings from './components.strings'; @@ -46,7 +47,8 @@ angular .directive('atPopover', popover) .directive('atTab', tab) .directive('atTabGroup', tabGroup) - .service('BaseInputController', BaseInputController) - .service('ComponentsStrings', ComponentsStrings); + .directive('atTruncate', truncate) + .service('ComponentsStrings', ComponentsStrings) + .service('BaseInputController', BaseInputController); diff --git a/awx/ui/client/lib/components/popover/_index.less b/awx/ui/client/lib/components/popover/_index.less index 8959835ba9..9018729eda 100644 --- a/awx/ui/client/lib/components/popover/_index.less +++ b/awx/ui/client/lib/components/popover/_index.less @@ -7,13 +7,19 @@ } .at-Popover-icon { - .at-mixin-ButtonIcon(); + .at-mixin-ButtonIcon(); color: @at-color-icon-popover; font-size: @at-font-size-icon; - padding: 0; + padding: 1px; margin: 0; } +.at-Popover-icon--defaultCursor { + i > { + cursor: default; + } +} + .at-Popover-container { visibility: hidden; opacity: 0; @@ -24,7 +30,7 @@ height: auto; position: fixed; z-index: 2000; - margin: 0 0 0 18px; + margin: 0; border-radius: @at-border-radius; box-shadow: 0 5px 10px rgba(0,0,0, 0.2); transition: opacity .15s linear; @@ -36,7 +42,7 @@ position: fixed; z-index: 1999; padding: 0; - margin: 8px 0 0 3px; + margin: 0; } .at-Popover-title { @@ -48,5 +54,5 @@ .at-Popover-text { margin: 0; padding: 0; - font-size: @at-font-size-body; + font-size: @at-font-size; } diff --git a/awx/ui/client/lib/components/popover/popover.directive.js b/awx/ui/client/lib/components/popover/popover.directive.js index 66857f6e4c..8f8ee5198a 100644 --- a/awx/ui/client/lib/components/popover/popover.directive.js +++ b/awx/ui/client/lib/components/popover/popover.directive.js @@ -1,10 +1,22 @@ +const DEFAULT_POSITION = 'right'; +const DEFAULT_ACTION = 'click'; +const DEFAULT_ICON = 'fa fa-question-circle'; +const DEFAULT_ALIGNMENT = 'inline'; +const DEFAULT_ARROW_HEIGHT = 14; +const DEFAULT_PADDING = 10; +const DEFAULT_REFRESH_DELAY = 50; +const DEFAULT_RESET_ON_EXIT = false; + function atPopoverLink (scope, el, attr, controllers) { let popoverController = controllers[0]; let container = el[0]; let popover = container.getElementsByClassName('at-Popover-container')[0]; let icon = container.getElementsByTagName('i')[0]; - popoverController.init(scope, container, icon, popover); + let done = scope.$watch('state', () => { + popoverController.init(scope, container, icon, popover); + done(); + }); } function AtPopoverController () { @@ -13,13 +25,35 @@ function AtPopoverController () { let container; let icon; let popover; + let scope; - vm.init = (scope, _container_, _icon_, _popover_) => { + vm.init = (_scope_, _container_, _icon_, _popover_) => { + scope = _scope_; icon = _icon_; popover = _popover_; - scope.inline = true; - icon.addEventListener('click', vm.createDisplayListener()); + scope.popover = scope.state.popover || {}; + + scope.popover.text = scope.state.help_text || scope.popover.text; + scope.popover.title = scope.state.label || scope.popover.title; + scope.popover.inline = scope.popover.inline || DEFAULT_ALIGNMENT; + scope.popover.position = scope.popover.position || DEFAULT_POSITION; + scope.popover.icon = scope.popover.icon || DEFAULT_ICON; + scope.popover.on = scope.popover.on || DEFAULT_ACTION; + scope.popover.resetOnExit = scope.popover.resetOnExit || DEFAULT_RESET_ON_EXIT; + + if (scope.popover.resetOnExit) { + scope.originalText = scope.popover.text; + scope.originalTitle = scope.popover.title; + } + + icon.addEventListener(scope.popover.on, vm.createDisplayListener()); + + scope.$watch('popover.text', vm.refresh); + + if (scope.popover.click) { + icon.addEventListener('click', scope.popover.click); + } }; vm.createDismissListener = (createEvent) => { @@ -30,16 +64,30 @@ function AtPopoverController () { return; } - vm.open = false; + vm.dismiss(); - popover.style.visibility = 'hidden'; - popover.style.opacity = 0; + if (scope.popover.on === 'mouseenter') { + icon.removeEventListener('mouseleave', vm.dismissListener); + } else { + window.addEventListener(scope.popover.on, vm.dismissListener); + } - window.removeEventListener('click', vm.dismissListener); window.removeEventListener('resize', vm.dismissListener); }; }; + vm.dismiss = (refresh) => { + if (!refresh && scope.popover.resetOnExit) { + scope.popover.text = scope.originalText; + scope.popover.title = scope.originalTitle; + } + + vm.open = false; + + popover.style.visibility = 'hidden'; + popover.style.opacity = 0; + }; + vm.isClickWithinPopover = (event, popover) => { let box = popover.getBoundingClientRect(); @@ -61,38 +109,96 @@ function AtPopoverController () { event.stopPropagation(); - vm.open = true; - - let arrow = popover.getElementsByClassName('at-Popover-arrow')[0]; - - let iPos = icon.getBoundingClientRect(); - let pPos = popover.getBoundingClientRect(); - - let wHeight = window.clientHeight; - let pHeight = pPos.height; - - let cx = Math.floor(iPos.left + (iPos.width / 2)); - let cy = Math.floor(iPos.top + (iPos.height / 2)); - - arrow.style.top = (iPos.top - iPos.height) + 'px'; - arrow.style.left = iPos.right + 'px'; - - if (cy < (pHeight / 2)) { - popover.style.top = '10px'; - } else { - popover.style.top = (cy - pHeight / 2) + 'px'; - } - - popover.style.left = cx + 'px'; - popover.style.visibility = 'visible'; - popover.style.opacity = 1; + vm.display(); vm.dismissListener = vm.createDismissListener(event); - window.addEventListener('click', vm.dismissListener); + if (scope.popover.on === 'mouseenter') { + icon.addEventListener('mouseleave', vm.dismissListener); + } else { + window.addEventListener(scope.popover.on, vm.dismissListener); + } + window.addEventListener('resize', vm.dismissListener); }; }; + + vm.refresh = () => { + if (!vm.open) { + return; + } + + vm.dismiss(true); + window.setTimeout(vm.display, DEFAULT_REFRESH_DELAY); + }; + + vm.getPositions = () => { + let arrow = popover.getElementsByClassName('at-Popover-arrow')[0]; + + arrow.style.lineHeight = `${DEFAULT_ARROW_HEIGHT}px`; + arrow.children[0].style.lineHeight = `${DEFAULT_ARROW_HEIGHT}px`; + + let data = { + arrow, + icon: icon.getBoundingClientRect(), + popover: popover.getBoundingClientRect(), + windowHeight: window.innerHeight + }; + + data.cx = Math.floor(data.icon.left + (data.icon.width / 2)); + data.cy = Math.floor(data.icon.top + (data.icon.height / 2)); + + return data; + }; + + vm.display = () => { + vm.open = true; + + let positions = vm.getPositions(); + + popover.style.visibility = 'visible'; + popover.style.opacity = 1; + + if (scope.popover.position === 'right') { + vm.displayRight(positions); + } else if (scope.popover.position === 'top') { + vm.displayTop(positions); + } + }; + + vm.displayRight = (pos) => { + let arrowTop = Math.floor((pos.cy - (pos.icon.height / 2))); + let arrowLeft = pos.cx + DEFAULT_PADDING; + + let popoverTop; + let popoverLeft = arrowLeft + DEFAULT_PADDING - 1; + + if (pos.cy < (pos.popover.height / 2)) { + popoverTop = DEFAULT_PADDING; + } else { + popoverTop = Math.floor((pos.cy - pos.popover.height / 2)); + } + + pos.arrow.style.top = `${arrowTop}px`; + pos.arrow.style.left = `${arrowLeft}px`; + + popover.style.top = `${popoverTop}px`; + popover.style.left = `${popoverLeft}px`; + }; + + vm.displayTop = (pos) => { + let arrowTop = pos.icon.top - pos.icon.height - DEFAULT_PADDING; + let arrowLeft = Math.floor(pos.icon.right - pos.icon.width - (pos.arrow.style.width / 2)); + + let popoverTop = pos.icon.top - pos.popover.height - pos.icon.height - 5; + let popoverLeft = Math.floor(pos.cx - (pos.popover.width / 2)); + + pos.arrow.style.top = `${arrowTop}px`; + pos.arrow.style.left = `${arrowLeft}px`; + + popover.style.top = `${popoverTop}px`; + popover.style.left = `${popoverLeft}px`; + }; } function atPopover (pathService) { @@ -115,4 +221,4 @@ atPopover.$inject = [ 'PathService' ]; -export default atPopover; +export default atPopover; \ No newline at end of file diff --git a/awx/ui/client/lib/components/popover/popover.partial.html b/awx/ui/client/lib/components/popover/popover.partial.html index f8acff1c84..0aa71a84ff 100644 --- a/awx/ui/client/lib/components/popover/popover.partial.html +++ b/awx/ui/client/lib/components/popover/popover.partial.html @@ -1,16 +1,16 @@ -
- - +
+ +
- + +
-

{{::state.label}}

-

{{::state.help_text}}

+

{{ popover.title }}

+

{{ popover.text }}

diff --git a/awx/ui/client/lib/components/truncate/_index.less b/awx/ui/client/lib/components/truncate/_index.less new file mode 100644 index 0000000000..2b2cdfede5 --- /dev/null +++ b/awx/ui/client/lib/components/truncate/_index.less @@ -0,0 +1,34 @@ +.at-Truncate { + display: flex; + align-items: center; + + .at-Truncate-text { + font-family: monospace; + } + + .at-Truncate-copy { + color: @at-gray-dark-2x; + cursor: pointer; + margin-left: 10px; + + i:hover { + color: @at-blue; + } + } + + .at-Truncate-textarea { + background: transparent; + border: none; + box-shadow: none; + height: 2em; + left: 0px; + outline: none; + padding: 0px; + position: fixed; + top: 0px; + width: 2em; + } +} + + + diff --git a/awx/ui/client/lib/components/truncate/truncate.directive.js b/awx/ui/client/lib/components/truncate/truncate.directive.js new file mode 100644 index 0000000000..6367d39918 --- /dev/null +++ b/awx/ui/client/lib/components/truncate/truncate.directive.js @@ -0,0 +1,68 @@ +function atTruncateLink (scope, el, attr, ctrl) { + let truncateController = ctrl; + let string = attr.string; + let maxlength = attr.maxlength; + + truncateController.init(el, string, maxlength); +} + +function AtTruncateController (strings) { + let vm = this; + let el; + let string; + let maxlength; + vm.strings = strings; + + vm.init = (_el_, _string_, _maxlength_) => { + el = _el_; + string = _string_; + maxlength = _maxlength_; + vm.truncatedString = string.substring(0, maxlength); + }; + + vm.tooltip = { + popover: { + text: vm.strings.components.truncate.DEFAULT, + on: 'mouseover', + position: 'top', + icon: 'fa fa-clone', + resetOnExit: true, + click: copyToClipboard + } + }; + + function copyToClipboard() { + vm.tooltip.popover.text = vm.strings.components.truncate.COPIED; + + let textarea = el[0].getElementsByClassName('at-Truncate-textarea')[0]; + textarea.value = string; + textarea.select(); + + document.execCommand('copy'); + }; +} + +AtTruncateController.$inject = ['ComponentsStrings']; + +function atTruncate(pathService) { + return { + restrict: 'E', + replace: true, + transclude: true, + templateUrl: pathService.getPartialPath('components/truncate/truncate'), + controller: AtTruncateController, + controllerAs: 'vm', + link: atTruncateLink, + scope: { + state: '=', + maxLength: '@', + string: '@' + } + } +} + +atTruncate.$inject = [ + 'PathService' +]; + +export default atTruncate; \ No newline at end of file diff --git a/awx/ui/client/lib/components/truncate/truncate.partial.html b/awx/ui/client/lib/components/truncate/truncate.partial.html new file mode 100644 index 0000000000..aa329908eb --- /dev/null +++ b/awx/ui/client/lib/components/truncate/truncate.partial.html @@ -0,0 +1,9 @@ +
+
+ {{vm.truncatedString}} +
+
+ +
+ +
diff --git a/awx/ui/client/src/job-results/job-results.block.less b/awx/ui/client/src/job-results/job-results.block.less index 63be335c97..4ee845e811 100644 --- a/awx/ui/client/src/job-results/job-results.block.less +++ b/awx/ui/client/src/job-results/job-results.block.less @@ -108,10 +108,6 @@ margin-left: 10px; } -.JobResults-resultRowText--revision{ - font-family: monospace; -} - .JobResults-resultRowText--instanceGroup { display: flex; } diff --git a/awx/ui/client/src/job-results/job-results.partial.html b/awx/ui/client/src/job-results/job-results.partial.html index 3592935bbd..ac5ad64022 100644 --- a/awx/ui/client/src/job-results/job-results.partial.html +++ b/awx/ui/client/src/job-results/job-results.partial.html @@ -247,9 +247,8 @@ -
- {{ job.scm_revision }} -
+ +
diff --git a/awx/ui/client/src/projects/main.js b/awx/ui/client/src/projects/main.js index 52fa7270b6..778cfab07a 100644 --- a/awx/ui/client/src/projects/main.js +++ b/awx/ui/client/src/projects/main.js @@ -13,10 +13,9 @@ import { N_ } from '../i18n'; import GetProjectPath from './factories/get-project-path.factory'; import GetProjectIcon from './factories/get-project-icon.factory'; import GetProjectToolTip from './factories/get-project-tool-tip.factory'; -import revisions from './revisions/main'; export default -angular.module('Projects', [revisions.name]) +angular.module('Projects', []) .controller('ProjectsList', ProjectsList) .controller('ProjectsAdd', ProjectsAdd) .controller('ProjectsEdit', ProjectsEdit) diff --git a/awx/ui/client/src/projects/revisions/main.js b/awx/ui/client/src/projects/revisions/main.js deleted file mode 100644 index 592cc2c976..0000000000 --- a/awx/ui/client/src/projects/revisions/main.js +++ /dev/null @@ -1,11 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import revisions from './revisions.directive'; - -export default - angular.module('revisions', []) - .directive('revisions', revisions); \ No newline at end of file diff --git a/awx/ui/client/src/projects/revisions/revisions.block.less b/awx/ui/client/src/projects/revisions/revisions.block.less deleted file mode 100644 index 084a8ba553..0000000000 --- a/awx/ui/client/src/projects/revisions/revisions.block.less +++ /dev/null @@ -1,22 +0,0 @@ -@import "./client/src/shared/branding/colors.default.less"; - -.RevisionHash { - display: flex; - align-items: center; -} - -.RevisionHash-name { - font-family: monospace; -} - -.RevisionHash-copy { - color: @default-link; - text-transform: uppercase; - cursor: pointer; - font-size: 11px; - margin-left: 10px; -} - -.RevisionHash-copy:hover { - color: @default-link-hov; -} \ No newline at end of file diff --git a/awx/ui/client/src/projects/revisions/revisions.directive.js b/awx/ui/client/src/projects/revisions/revisions.directive.js deleted file mode 100644 index b5e7ef02c3..0000000000 --- a/awx/ui/client/src/projects/revisions/revisions.directive.js +++ /dev/null @@ -1,51 +0,0 @@ -export default - [ 'templateUrl', - 'Rest', - '$q', - '$filter', - function(templateUrl, Rest, $q, $filter) { - return { - restrict: 'E', - scope: false, - templateUrl: templateUrl('projects/revisions/revisions'), - link: function(scope) { - let full_revision = scope.project.scm_revision; - scope.revisionHash = $filter('limitTo')(full_revision, 7, 0); - scope.count = scope.project.scm_revision.length; - - scope.copyRevisionHash = function() { - let textArea = document.createElement("textarea"); - - // Place in top-left corner of screen regardless of scroll position. - textArea.style.position = 'fixed'; - textArea.style.top = 0; - textArea.style.left = 0; - - // Ensure it has a small width and height. Setting to 1px / 1em - // doesn't work as this gives a negative w/h on some browsers. - textArea.style.width = '2em'; - textArea.style.height = '2em'; - - // We don't need padding, reducing the size if it does flash render. - textArea.style.padding = 0; - - // Clean up any borders. - textArea.style.border = 'none'; - textArea.style.outline = 'none'; - textArea.style.boxShadow = 'none'; - - // Avoid flash of white box if rendered for any reason. - textArea.style.background = 'transparent'; - - textArea.value = full_revision; - document.body.appendChild(textArea); - textArea.select(); - - document.execCommand('copy'); - - document.body.removeChild(textArea); - }; - } - }; - } - ]; \ No newline at end of file diff --git a/awx/ui/client/src/projects/revisions/revisions.partial.html b/awx/ui/client/src/projects/revisions/revisions.partial.html deleted file mode 100644 index 6b72870994..0000000000 --- a/awx/ui/client/src/projects/revisions/revisions.partial.html +++ /dev/null @@ -1,6 +0,0 @@ -
- {{revisionHash}} -
-
- Copy -
\ No newline at end of file diff --git a/awx/ui/client/src/shared/generator-helpers.js b/awx/ui/client/src/shared/generator-helpers.js index f4d6aade11..5a77a7059d 100644 --- a/awx/ui/client/src/shared/generator-helpers.js +++ b/awx/ui/client/src/shared/generator-helpers.js @@ -525,7 +525,7 @@ angular.module('GeneratorHelpers', [systemStatus.name]) Attr(field, 'columnClass') : ""; html += ` - + `; } else if (field.type === 'badgeCount') { html = BadgeCount(params);