mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Backport PR #8172
---------
**Commit 1:**
Redesign Share UI to emphasize difference between Saved URLs and Snapshot URLs.
- Remove share_object_url.
- Remove clipboard npm dependency.
- Add toggle for switching between Short and Long URLs.
- Add ability to share embedded iframe to saved visualizations, including current global state.
* Original sha: a985840692
* Authored by CJ Cenizal <cj@cenizal.com> on 2016-09-02T20:15:27Z
This commit is contained in:
parent
de691d7710
commit
2670a0e24e
8 changed files with 415 additions and 152 deletions
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.');
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,2 +1,2 @@
|
|||
import './styles/index.less';
|
||||
import './directives/share';
|
||||
import './directives/share_object_url';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
Loading…
Add table
Add a link
Reference in a new issue