diff --git a/package.json b/package.json
index e8c3c1b550533..2fcbbc9aba29c 100644
--- a/package.json
+++ b/package.json
@@ -85,6 +85,7 @@
"bootstrap": "3.3.5",
"brace": "0.5.1",
"bunyan": "1.4.0",
+ "clipboard": "1.5.5",
"commander": "2.8.1",
"css-loader": "0.17.0",
"d3": "3.5.6",
diff --git a/src/plugins/kibana/public/dashboard/index.js b/src/plugins/kibana/public/dashboard/index.js
index 45e247b498755..d3464adca65be 100644
--- a/src/plugins/kibana/public/dashboard/index.js
+++ b/src/plugins/kibana/public/dashboard/index.js
@@ -10,6 +10,7 @@ define(function (require) {
require('ui/config');
require('ui/notify');
require('ui/typeahead');
+ require('ui/share');
require('plugins/kibana/dashboard/directives/grid');
require('plugins/kibana/dashboard/components/panel/panel');
@@ -233,15 +234,7 @@ define(function (require) {
ui: $state.options,
save: $scope.save,
addVis: $scope.addVis,
- addSearch: $scope.addSearch,
- shareData: function () {
- return {
- link: $location.absUrl(),
- // This sucks, but seems like the cleanest way. Uhg.
- embed: ''
- };
- }
+ addSearch: $scope.addSearch
};
init();
diff --git a/src/plugins/kibana/public/dashboard/partials/share.html b/src/plugins/kibana/public/dashboard/partials/share.html
index bf34366604db1..046acbb5c95b8 100644
--- a/src/plugins/kibana/public/dashboard/partials/share.html
+++ b/src/plugins/kibana/public/dashboard/partials/share.html
@@ -1,21 +1,4 @@
-
\ No newline at end of file
+
+
diff --git a/src/plugins/kibana/public/discover/controllers/discover.js b/src/plugins/kibana/public/discover/controllers/discover.js
index 80ee285644abb..1943384297480 100644
--- a/src/plugins/kibana/public/discover/controllers/discover.js
+++ b/src/plugins/kibana/public/discover/controllers/discover.js
@@ -20,6 +20,7 @@ define(function (require) {
require('ui/state_management/app_state');
require('ui/timefilter');
require('ui/highlight/highlight_tags');
+ require('ui/share');
var app = require('ui/modules').get('apps/discover', [
'kibana/notify',
@@ -91,7 +92,8 @@ define(function (require) {
// config panel templates
$scope.configTemplate = new ConfigTemplate({
load: require('plugins/kibana/discover/partials/load_search.html'),
- save: require('plugins/kibana/discover/partials/save_search.html')
+ save: require('plugins/kibana/discover/partials/save_search.html'),
+ share: require('plugins/kibana/discover/partials/share_search.html')
});
$scope.timefilter = timefilter;
diff --git a/src/plugins/kibana/public/discover/index.html b/src/plugins/kibana/public/discover/index.html
index dc05a802d75f4..6fb0d76d19414 100644
--- a/src/plugins/kibana/public/discover/index.html
+++ b/src/plugins/kibana/public/discover/index.html
@@ -50,6 +50,16 @@
+
+
+
diff --git a/src/plugins/kibana/public/discover/partials/share_search.html b/src/plugins/kibana/public/discover/partials/share_search.html
new file mode 100644
index 0000000000000..69fee7ad756d0
--- /dev/null
+++ b/src/plugins/kibana/public/discover/partials/share_search.html
@@ -0,0 +1,5 @@
+
+
diff --git a/src/plugins/kibana/public/visualize/editor/editor.js b/src/plugins/kibana/public/visualize/editor/editor.js
index ab0c6609466ca..cec9316e2b4e9 100644
--- a/src/plugins/kibana/public/visualize/editor/editor.js
+++ b/src/plugins/kibana/public/visualize/editor/editor.js
@@ -6,6 +6,7 @@ define(function (require) {
require('ui/visualize');
require('ui/collapsible_sidebar');
+ require('ui/share');
require('ui/routes')
.when('/visualize/create', {
@@ -234,15 +235,6 @@ define(function (require) {
}, notify.fatal);
};
- $scope.shareData = function () {
- return {
- link: $location.absUrl(),
- // This sucks, but seems like the cleanest way. Uhg.
- embed: ''
- };
- };
-
$scope.unlink = function () {
if (!$state.linked) return;
diff --git a/src/plugins/kibana/public/visualize/editor/panels/share.html b/src/plugins/kibana/public/visualize/editor/panels/share.html
index a356060024e00..016109cfff6f5 100644
--- a/src/plugins/kibana/public/visualize/editor/panels/share.html
+++ b/src/plugins/kibana/public/visualize/editor/panels/share.html
@@ -1,22 +1,4 @@
-
\ No newline at end of file
+
+
diff --git a/src/server/http/index.js b/src/server/http/index.js
index 2528ff2e31990..f0c542c41f08d 100644
--- a/src/server/http/index.js
+++ b/src/server/http/index.js
@@ -10,6 +10,8 @@ module.exports = function (kbnServer, server, config) {
server = kbnServer.server = new Hapi.Server();
+ const shortUrlLookup = require('./short_url_lookup')(server);
+
// Create a new connection
var connectionOptions = {
host: config.get('server.host'),
@@ -154,5 +156,23 @@ module.exports = function (kbnServer, server, config) {
}
});
+ server.route({
+ method: 'GET',
+ path: '/goto/{urlId}',
+ handler: async function (request, reply) {
+ const url = await shortUrlLookup.getUrl(request.params.urlId);
+ reply().redirect(url);
+ }
+ });
+
+ server.route({
+ method: 'POST',
+ path: '/shorten',
+ handler: async function (request, reply) {
+ const urlId = await shortUrlLookup.generateUrlId(request.payload.url);
+ reply(urlId);
+ }
+ });
+
return kbnServer.mixin(require('./xsrf'));
};
diff --git a/src/server/http/short_url_lookup.js b/src/server/http/short_url_lookup.js
new file mode 100644
index 0000000000000..4ced5e5d812a9
--- /dev/null
+++ b/src/server/http/short_url_lookup.js
@@ -0,0 +1,101 @@
+const crypto = require('crypto');
+
+export default function (server) {
+ async function updateMetadata(urlId, urlDoc) {
+ const client = server.plugins.elasticsearch.client;
+
+ try {
+ await client.update({
+ index: '.kibana',
+ type: 'url',
+ id: urlId,
+ body: {
+ doc: {
+ 'accessDate': new Date(),
+ 'accessCount': urlDoc._source.accessCount + 1
+ }
+ }
+ });
+ } catch (err) {
+ server.log('Warning: Error updating url metadata', err);
+ //swallow errors. It isn't critical if there is no update.
+ }
+ }
+
+ async function getUrlDoc(urlId) {
+ const urlDoc = await new Promise((resolve, reject) => {
+ const client = server.plugins.elasticsearch.client;
+
+ client.get({
+ index: '.kibana',
+ type: 'url',
+ id: urlId
+ })
+ .then(response => {
+ resolve(response);
+ })
+ .catch(err => {
+ resolve();
+ });
+ });
+
+ return urlDoc;
+ }
+
+ async function createUrlDoc(url, urlId) {
+ const newUrlId = await new Promise((resolve, reject) => {
+ const client = server.plugins.elasticsearch.client;
+
+ client.index({
+ index: '.kibana',
+ type: 'url',
+ id: urlId,
+ body: {
+ url,
+ 'accessCount': 0,
+ 'createDate': new Date(),
+ 'accessDate': new Date()
+ }
+ })
+ .then(response => {
+ resolve(response._id);
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+
+ return newUrlId;
+ }
+
+ function createUrlId(url) {
+ const urlId = crypto.createHash('md5')
+ .update(url)
+ .digest('hex');
+
+ return urlId;
+ }
+
+ return {
+ async generateUrlId(url) {
+ const urlId = createUrlId(url);
+
+ const urlDoc = await getUrlDoc(urlId);
+ if (urlDoc) return urlId;
+
+ return createUrlDoc(url, urlId);
+ },
+ async getUrl(urlId) {
+ try {
+ const urlDoc = await getUrlDoc(urlId);
+ if (!urlDoc) throw new Error('Requested shortened url does note exist in kibana index');
+
+ updateMetadata(urlId, urlDoc);
+
+ return urlDoc._source.url;
+ } catch (err) {
+ return '/';
+ }
+ }
+ };
+};
diff --git a/src/ui/public/share/directives/share.js b/src/ui/public/share/directives/share.js
new file mode 100644
index 0000000000000..49ebb95afba34
--- /dev/null
+++ b/src/ui/public/share/directives/share.js
@@ -0,0 +1,16 @@
+const app = require('ui/modules').get('kibana');
+
+app.directive('share', function () {
+ return {
+ restrict: 'E',
+ scope: {
+ objectType: '@',
+ objectId: '@',
+ setAllowEmbed: '&?allowEmbed'
+ },
+ template: require('ui/share/views/share.html'),
+ controller: function ($scope) {
+ $scope.allowEmbed = $scope.setAllowEmbed ? $scope.setAllowEmbed() : true;
+ }
+ };
+});
diff --git a/src/ui/public/share/directives/share_object_url.js b/src/ui/public/share/directives/share_object_url.js
new file mode 100644
index 0000000000000..ee65c86345d44
--- /dev/null
+++ b/src/ui/public/share/directives/share_object_url.js
@@ -0,0 +1,76 @@
+const app = require('ui/modules').get('kibana');
+const Clipboard = require('clipboard');
+
+require('../styles/index.less');
+
+app.directive('shareObjectUrl', function (Private, Notifier) {
+ const urlShortener = Private(require('../lib/url_shortener'));
+
+ return {
+ restrict: 'E',
+ scope: {
+ getShareAsEmbed: '&shareAsEmbed'
+ },
+ template: require('ui/share/views/share_object_url.html'),
+ 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 = ``;
+ } 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 () {
+ return $location.absUrl();
+ };
+
+ $scope.$watch('getUrl()', updateUrl);
+ }
+ };
+});
diff --git a/src/ui/public/share/index.js b/src/ui/public/share/index.js
new file mode 100644
index 0000000000000..2dec91dcc725d
--- /dev/null
+++ b/src/ui/public/share/index.js
@@ -0,0 +1,2 @@
+require('./directives/share');
+require('./directives/share_object_url');
diff --git a/src/ui/public/share/lib/url_shortener.js b/src/ui/public/share/lib/url_shortener.js
new file mode 100644
index 0000000000000..3e29500d8d2f0
--- /dev/null
+++ b/src/ui/public/share/lib/url_shortener.js
@@ -0,0 +1,24 @@
+export default function createUrlShortener(Notifier, $http, $location) {
+ const notify = new Notifier({
+ location: 'Url Shortener'
+ });
+ const baseUrl = `${$location.protocol()}://${$location.host()}:${$location.port()}`;
+
+ async function shortenUrl(url) {
+ const relativeUrl = url.replace(baseUrl, '');
+ const formData = { url: relativeUrl };
+
+ try {
+ const result = await $http.post('/shorten', formData);
+
+ return `${baseUrl}/goto/${result.data}`;
+ } catch (err) {
+ notify.error(err);
+ throw err;
+ }
+ }
+
+ return {
+ shortenUrl
+ };
+};
diff --git a/src/ui/public/share/styles/index.less b/src/ui/public/share/styles/index.less
new file mode 100644
index 0000000000000..5acb20bb3449a
--- /dev/null
+++ b/src/ui/public/share/styles/index.less
@@ -0,0 +1,21 @@
+share-object-url {
+ .input-group {
+ display: flex;
+
+ .clipboard-button {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+
+ .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;
+ }
+ }
+}
diff --git a/src/ui/public/share/views/share.html b/src/ui/public/share/views/share.html
new file mode 100644
index 0000000000000..5f7ffdd0a6797
--- /dev/null
+++ b/src/ui/public/share/views/share.html
@@ -0,0 +1,15 @@
+
diff --git a/src/ui/public/share/views/share_object_url.html b/src/ui/public/share/views/share_object_url.html
new file mode 100644
index 0000000000000..6c1b1438e9e90
--- /dev/null
+++ b/src/ui/public/share/views/share_object_url.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+