Merge pull request #8260 from elastic/jasper/backport/8172/5.x

[backport] PR #8172 to 5.x - Redesign Share UI to emphasize difference between Original URLs and Snapshot URLs.
This commit is contained in:
Court Ewing 2016-09-13 15:05:41 -04:00 committed by GitHub
commit e9dfd52a2f
8 changed files with 415 additions and 152 deletions

View file

@ -95,7 +95,6 @@
"boom": "2.8.0",
"brace": "0.5.1",
"bunyan": "1.7.1",
"clipboard": "1.5.5",
"commander": "2.8.1",
"css-loader": "0.17.0",
"csv-parse": "1.1.0",

View file

@ -386,9 +386,3 @@ vis-editor-vis-options > * {
background-color: darken(@vis-editor-navbar-error-state-bg, 12%) !important; /* 1 */
}
}
form.vis-share {
div.form-control {
height: inherit;
}
}

View file

@ -1,18 +1,169 @@
import {
parse as parseUrl,
format as formatUrl,
} from 'url';
import {
getUnhashableStatesProvider,
unhashUrl,
} from 'ui/state_management/state_hashing';
import Notifier from 'ui/notify/notifier';
import urlShortenerProvider from '../lib/url_shortener';
import uiModules from 'ui/modules';
import shareTemplate from 'ui/share/views/share.html';
const app = uiModules.get('kibana');
app.directive('share', function () {
app.directive('share', function (Private) {
const getUnhashableStates = Private(getUnhashableStatesProvider);
const urlShortener = Private(urlShortenerProvider);
return {
restrict: 'E',
scope: {
objectType: '@',
objectId: '@',
setAllowEmbed: '&?allowEmbed'
allowEmbed: '@',
},
template: shareTemplate,
controller: function ($scope) {
$scope.allowEmbed = $scope.setAllowEmbed ? $scope.setAllowEmbed() : true;
controllerAs: 'share',
controller: function ($scope, $document, $location, globalState) {
if ($scope.allowEmbed !== 'false' && $scope.allowEmbed !== undefined) {
throw new Error('allowEmbed must be "false" or undefined');
}
// Default to allowing an embedded IFRAME, unless it's explicitly set to false.
this.allowEmbed = $scope.allowEmbed === 'false' ? false : true;
this.objectType = $scope.objectType;
function getOriginalUrl() {
// If there is no objectId, then it isn't saved, so it has no original URL.
if ($scope.objectId === undefined || $scope.objectId === '') {
return;
}
const url = $location.absUrl();
// Replace hashes with original RISON values.
const unhashedUrl = unhashUrl(url, getUnhashableStates());
const parsedUrl = parseUrl(unhashedUrl);
// Get the Angular route, after the hash, and remove the #.
const parsedAppUrl = parseUrl(parsedUrl.hash.slice(1), true);
return formatUrl({
protocol: parsedUrl.protocol,
auth: parsedUrl.auth,
host: parsedUrl.host,
pathname: parsedUrl.pathname,
hash: formatUrl({
pathname: parsedAppUrl.pathname,
query: {
// Add global state to the URL so that the iframe doesn't just show the time range
// default.
_g: parsedAppUrl.query._g,
},
}),
});
};
function getSnapshotUrl() {
const url = $location.absUrl();
// Replace hashes with original RISON values.
return unhashUrl(url, getUnhashableStates());
}
this.makeUrlEmbeddable = url => {
const embedQueryParam = '?embed=true';
const urlHasQueryString = url.indexOf('?') !== -1;
if (urlHasQueryString) {
return url.replace('?', `${embedQueryParam}&`);
}
return `${url}${embedQueryParam}`;
};
this.makeIframeTag = url => {
if (!url) return;
const embeddableUrl = this.makeUrlEmbeddable(url);
return `<iframe src="${embeddableUrl}" height="600" width="800"></iframe>`;
};
this.urls = {
original: undefined,
snapshot: undefined,
shortSnapshot: undefined,
shortSnapshotIframe: undefined,
};
this.urlFlags = {
shortSnapshot: false,
shortSnapshotIframe: false,
};
const updateUrls = () => {
this.urls = {
original: getOriginalUrl(),
snapshot: getSnapshotUrl(),
shortSnapshot: undefined,
shortSnapshotIframe: undefined,
};
// Whenever the URL changes, reset the Short URLs to regular URLs.
this.urlFlags = {
shortSnapshot: false,
shortSnapshotIframe: false,
};
};
// When the URL changes, update the links in the UI.
$scope.$watch(() => $location.absUrl(), () => {
updateUrls();
});
this.toggleShortSnapshotUrl = () => {
this.urlFlags.shortSnapshot = !this.urlFlags.shortSnapshot;
if (this.urlFlags.shortSnapshot) {
urlShortener.shortenUrl(this.urls.snapshot)
.then(shortUrl => {
this.urls.shortSnapshot = shortUrl;
});
}
};
this.toggleShortSnapshotIframeUrl = () => {
this.urlFlags.shortSnapshotIframe = !this.urlFlags.shortSnapshotIframe;
if (this.urlFlags.shortSnapshotIframe) {
const snapshotIframe = this.makeUrlEmbeddable(this.urls.snapshot);
urlShortener.shortenUrl(snapshotIframe)
.then(shortUrl => {
this.urls.shortSnapshotIframe = shortUrl;
});
}
};
this.copyToClipboard = selector => {
const notify = new Notifier({
location: `Share ${$scope.objectType}`,
});
// Select the text to be copied. If the copy fails, the user can easily copy it manually.
const copyTextarea = $document.find(selector)[0];
copyTextarea.select();
try {
const isCopied = document.execCommand('copy');
if (isCopied) {
notify.info('URL copied to clipboard.');
} else {
notify.info('URL selected. Press Ctrl+C to copy.');
}
} catch (err) {
notify.info('URL selected. Press Ctrl+C to copy.');
}
};
}
};
});

View file

@ -1,91 +0,0 @@
const app = uiModules.get('kibana');
import Clipboard from 'clipboard';
import '../styles/index.less';
import LibUrlShortenerProvider from '../lib/url_shortener';
import uiModules from 'ui/modules';
import shareObjectUrlTemplate from 'ui/share/views/share_object_url.html';
import {
getUnhashableStatesProvider,
unhashUrl,
} from 'ui/state_management/state_hashing';
import { memoize } from 'lodash';
app.directive('shareObjectUrl', function (Private, Notifier) {
const urlShortener = Private(LibUrlShortenerProvider);
const getUnhashableStates = Private(getUnhashableStatesProvider);
return {
restrict: 'E',
scope: {
getShareAsEmbed: '&shareAsEmbed'
},
template: shareObjectUrlTemplate,
link: function ($scope, $el) {
const notify = new Notifier({
location: `Share ${$scope.$parent.objectType}`
});
$scope.textbox = $el.find('input.url')[0];
$scope.clipboardButton = $el.find('button.clipboard-button')[0];
const clipboard = new Clipboard($scope.clipboardButton, {
target(trigger) {
return $scope.textbox;
}
});
clipboard.on('success', e => {
notify.info('URL copied to clipboard.');
e.clearSelection();
});
clipboard.on('error', () => {
notify.info('URL selected. Press Ctrl+C to copy.');
});
$scope.$on('$destroy', () => {
clipboard.destroy();
});
$scope.clipboard = clipboard;
},
controller: function ($scope, $location) {
function updateUrl(url) {
$scope.url = url;
if ($scope.shareAsEmbed) {
$scope.formattedUrl = `<iframe src="${$scope.url}" height="600" width="800"></iframe>`;
} else {
$scope.formattedUrl = $scope.url;
}
$scope.shortGenerated = false;
}
$scope.shareAsEmbed = $scope.getShareAsEmbed();
$scope.generateShortUrl = function () {
if ($scope.shortGenerated) return;
urlShortener.shortenUrl($scope.url)
.then(shortUrl => {
updateUrl(shortUrl);
$scope.shortGenerated = true;
});
};
$scope.getUrl = function () {
const urlWithHashes = $location.absUrl();
const urlWithStates = unhashUrl(urlWithHashes, getUnhashableStates());
if ($scope.shareAsEmbed) {
return urlWithStates.replace('?', '?embed=true&');
}
return urlWithStates;
};
$scope.$watch('getUrl()', updateUrl);
}
};
});

View file

@ -1,2 +1,2 @@
import './styles/index.less';
import './directives/share';
import './directives/share_object_url';

View file

@ -1,21 +1,79 @@
share-object-url {
.input-group {
.share-dropdown {
display: flex;
padding: 5px 15px;
}
.share-panel {
flex: 1 1 0%;
}
.share-panel--left {
margin-right: 30px;
}
.share-panel--right {
margin-left: 30px;
}
.share-panel__title {
margin-bottom: 12px;
padding-bottom: 4px;
font-size: 18px;
color: #000000;
}
.share-panel-section {
margin-bottom: 16px;
}
.share-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.share-panel-header__label {
font-size: 14px;
font-weight: 700;
color: #000000;
}
.share-panel-header__actions {
display: flex;
}
.clipboard-button {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.share-panel-header__action {
font-size: 12px;
.shorten-button {
border-top-right-radius: 0;
border-top-left-radius: 0;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-control.url {
cursor: text;
& + & {
margin-left: 10px;
}
}
.share-panel-input {
width: 100%;
padding: 4px 10px;
margin-bottom: 6px;
border: 0;
border-radius: 4px;
}
.share-panel-form-note {
font-size: 14px;
color: #5A5A5A;
}
.share-panel-help-text {
margin-bottom: 16px;
font-size: 14px;
color: #2D2D2D;
}
.share-panel-warning {
margin-bottom: 16px;
padding: 6px 10px;
font-size: 14px;
color: #2D2D2D;
background-color: #e4e4e4;
}

View file

@ -1,15 +1,188 @@
<form role="form" class="vis-share">
<div class="form-group" ng-if="allowEmbed">
<label>
Embed this {{objectType}}
<small>Add to your html source. Note all clients must still be able to access kibana</small>
</label>
<share-object-url share-as-embed="true"></share-object-url>
<div class="share-dropdown">
<!-- Left panel -->
<div class="share-panel share-panel--left">
<!-- Title -->
<div class="share-panel__title">
Share saved {{share.objectType}}
</div>
<!-- Help text -->
<div ng-if="share.urls.original" class="share-panel-help-text">
You can share this URL with people to let them load the most recent saved version of this {{share.objectType}}.
</div>
<div ng-if="!share.urls.original" class="share-panel-warning">
Please save this {{share.objectType}} to enable this sharing option.
</div>
<div ng-if="share.urls.original">
<!-- iframe -->
<div class="share-panel-section" ng-if="share.allowEmbed">
<!-- Header -->
<div class="share-panel-header">
<div class="share-panel-header__label">
Embedded iframe
</div>
<div class="share-panel-header__actions">
<a
class="share-panel-header__action"
ng-click="share.copyToClipboard('#originalIframeUrl')"
>
Copy
</a>
</div>
</div>
<!-- Input -->
<input
id="originalIframeUrl"
class="share-panel-input"
type="text"
readonly
value="{{share.makeIframeTag(share.urls.original)}}"
/>
<!-- Notes -->
<div class="share-panel-form-note">
Add to your HTML source. Note that all clients must be able to access Kibana.
</div>
</div>
<!-- Link -->
<div class="share-panel-section">
<!-- Header -->
<div class="share-panel-header">
<div class="share-panel-header__label">
Link
</div>
<div class="share-panel-header__actions">
<a
class="share-panel-header__action"
ng-click="share.copyToClipboard('#originalUrl')"
>
Copy
</a>
</div>
</div>
<!-- Input -->
<input
id="originalUrl"
class="share-panel-input"
type="text"
readonly
value="{{share.urls.original}}"
/>
</div>
</div>
</div>
<div class="form-group">
<label>
Share a link
</label>
<share-object-url share-as-embed="false"></share-object-url>
<!-- Right panel -->
<div class="share-panel share-panel--right">
<!-- Title -->
<div class="share-panel__title">
Share Snapshot
</div>
<!-- Help text -->
<div class="share-panel-help-text">
Snapshot URLs encode the current state of the {{share.objectType}} in the URL itself. Edits to the saved {{share.objectType}} won't be visible via this URL.
</div>
<!-- iframe -->
<div class="share-panel-section" ng-if="share.allowEmbed">
<!-- Header -->
<div class="share-panel-header">
<div class="share-panel-header__label">
Embedded iframe
</div>
<div class="share-panel-header__actions">
<a
class="share-panel-header__action"
ng-if="!share.urlFlags.shortSnapshotIframe"
ng-click="share.toggleShortSnapshotIframeUrl()"
>
Short URL
</a>
<a
class="share-panel-header__action"
ng-if="share.urlFlags.shortSnapshotIframe"
ng-click="share.toggleShortSnapshotIframeUrl()"
>
Long URL
</a>
<a
class="share-panel-header__action"
ng-click="share.copyToClipboard('#snapshotIframeUrl')"
>
Copy
</a>
</div>
</div>
<!-- Input -->
<input
id="snapshotIframeUrl"
class="share-panel-input"
type="text"
readonly
value="{{share.urlFlags.shortSnapshotIframe ? share.makeIframeTag(share.urls.shortSnapshotIframe) : share.makeIframeTag(share.urls.snapshot)}}"
/>
<!-- Notes -->
<div class="share-panel-form-note">
Add to your HTML source. Note that all clients must be able to access Kibana.
</div>
</div>
<!-- Link -->
<div class="share-panel-section">
<!-- Header -->
<div class="share-panel-header">
<div class="share-panel-header__label">
Link
</div>
<div class="share-panel-header__actions">
<a
class="share-panel-header__action"
ng-if="!share.urlFlags.shortSnapshot"
ng-click="share.toggleShortSnapshotUrl()"
>
Short URL
</a>
<a
class="share-panel-header__action"
ng-if="share.urlFlags.shortSnapshot"
ng-click="share.toggleShortSnapshotUrl()"
>
Long URL
</a>
<a
class="share-panel-header__action"
ng-click="share.copyToClipboard('#snapshotUrl')"
>
Copy
</a>
</div>
</div>
<!-- Input -->
<input
id="snapshotUrl"
class="share-panel-input"
type="text"
readonly
value="{{share.urlFlags.shortSnapshot ? share.urls.shortSnapshot : share.urls.snapshot}}"
/>
<!-- Notes -->
<div class="share-panel-form-note">
We recommend sharing shortened snapshot URLs for maximum compatibility. Internet Explorer has URL length restrictions, and some wiki and markup parsers don't do well with the full-length version of the snapshot URL, but the short URL should work great.
</div>
</div>
</div>
</form>
</div>

View file

@ -1,21 +0,0 @@
<div class="input-group">
<input
ng-model="formattedUrl"
type="text"
readonly=""
class="form-control url">
</input>
<button
class="shorten-button btn btn-default"
tooltip="Generate Short URL"
ng-click="generateShortUrl()"
ng-disabled="shortGenerated">
<span aria-hidden="true" class="fa fa-compress"></span>
</button>
<button
class="clipboard-button btn btn-default"
tooltip="Copy to Clipboard"
ng-click="copyToClipboard()">
<span aria-hidden="true" class="fa fa-clipboard"></span>
</button>
</div>