mirror of
https://github.com/ansible/awx.git
synced 2026-01-13 19:10:07 -03:30
Merge pull request #6903 from marshmalien/copyProjectRevisionIcon
Add copy icon and update Tooltip content to project revision sha
This commit is contained in:
commit
35e28e9347
@ -5,3 +5,4 @@
|
||||
@import 'popover/_index';
|
||||
@import 'tabs/_index';
|
||||
@import 'utility/_index';
|
||||
@import 'truncate/_index';
|
||||
|
||||
@ -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'];
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
@ -1,16 +1,16 @@
|
||||
<div ng-show="state.help_text"
|
||||
class="at-Popover"
|
||||
ng-class="{ 'at-Popover--inline': inline }">
|
||||
<span class="at-Popover-icon">
|
||||
<i class="fa fa-question-circle"></i>
|
||||
<div ng-show="popover.text" class="at-Popover" ng-class="{ 'at-Popover--inline': popover.inline }">
|
||||
<span class="at-Popover-icon"
|
||||
ng-class="{ 'at-Popover-icon--defaultCursor': popover.on === 'mouseenter' && !popover.click }">
|
||||
<i class="fa {{ popover.icon }}"></i>
|
||||
</span>
|
||||
<div class="at-Popover-container">
|
||||
<div class="at-Popover-arrow">
|
||||
<i class="fa fa-caret-left fa-2x"></i>
|
||||
<i ng-if="popover.position === 'right'" class="fa fa-caret-left fa-2x"></i>
|
||||
<i ng-if="popover.position === 'top'" class="fa fa-caret-down fa-2x"></i>
|
||||
</div>
|
||||
<div class="at-Popover-content">
|
||||
<h4 class="at-Popover-title">{{::state.label}}</h4>
|
||||
<p class="at-Popover-text">{{::state.help_text}}</p>
|
||||
<h4 ng-if="popover.title" class="at-Popover-title">{{ popover.title }}</h4>
|
||||
<p ng-if="popover.text" class="at-Popover-text">{{ popover.text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
34
awx/ui/client/lib/components/truncate/_index.less
Normal file
34
awx/ui/client/lib/components/truncate/_index.less
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
68
awx/ui/client/lib/components/truncate/truncate.directive.js
Normal file
68
awx/ui/client/lib/components/truncate/truncate.directive.js
Normal file
@ -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;
|
||||
@ -0,0 +1,9 @@
|
||||
<div class="at-Truncate">
|
||||
<div class="at-Truncate-tag">
|
||||
<span class="at-Truncate-text">{{vm.truncatedString}}</span>
|
||||
</div>
|
||||
<div class="at-Truncate-copy" data-placement="top">
|
||||
<at-popover position="top" on="mouseover" state="vm.tooltip"></at-popover>
|
||||
</div>
|
||||
<textarea class="at-Truncate-textarea"></textarea>
|
||||
</div>
|
||||
@ -108,10 +108,6 @@
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.JobResults-resultRowText--revision{
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.JobResults-resultRowText--instanceGroup {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@ -247,9 +247,8 @@
|
||||
<label class="JobResults-resultRowLabel">
|
||||
Revision
|
||||
</label>
|
||||
<div class="JobResults-resultRowText JobResults-resultRowText--revision">
|
||||
{{ job.scm_revision }}
|
||||
</div>
|
||||
<at-truncate string="{{job.scm_revision}}" maxLength="7" class="JobResults-resultRowText">
|
||||
</at-truncate>
|
||||
</div>
|
||||
|
||||
<!-- PLAYBOOK DETAIL -->
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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);
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
];
|
||||
@ -1,6 +0,0 @@
|
||||
<div class="RevisionHash-tag">
|
||||
<span class="RevisionHash-name">{{revisionHash}}</span>
|
||||
</div>
|
||||
<div class="RevisionHash-copy" ng-if="count > 7" aw-tool-tip="Copy full revision to clipboard" data-placement="top" ng-click="copyRevisionHash()">
|
||||
Copy
|
||||
</div>
|
||||
@ -525,7 +525,7 @@ angular.module('GeneratorHelpers', [systemStatus.name])
|
||||
Attr(field, 'columnClass') : "";
|
||||
html += `
|
||||
<td ${classList}>
|
||||
<revisions class=\"RevisionHash\"></revisions>
|
||||
<at-truncate string="{{project.scm_revision}}" maxLength="7"></at-truncate>
|
||||
</td>`;
|
||||
} else if (field.type === 'badgeCount') {
|
||||
html = BadgeCount(params);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user