Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Url shortener #5497

Merged
merged 8 commits into from
Dec 18, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,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",
Expand Down
11 changes: 2 additions & 9 deletions src/plugins/kibana/public/dashboard/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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: '<iframe src="' + $location.absUrl().replace('?', '?embed&') +
'" height="600" width="800"></iframe>'
};
}
addSearch: $scope.addSearch
};

init();
Expand Down
25 changes: 4 additions & 21 deletions src/plugins/kibana/public/dashboard/partials/share.html
Original file line number Diff line number Diff line change
@@ -1,21 +1,4 @@
<form role="form" class="vis-share">

<p>
<div class="input-group">
<label>
Embed this dashboard
<small>Add to your html source. Note all clients must still be able to access kibana</small>
</label>
<div class="form-control" disabled>{{opts.shareData().embed}}</div>
</div>
</p>

<p>
<div class="input-group">
<label>
Share a link
</label>
<div class="form-control" disabled>{{opts.shareData().link}}</div>
</div>
</p>
</form>
<share
object-type="dashboard"
object-id="{{opts.dashboard.id}}">
</share>
4 changes: 3 additions & 1 deletion src/plugins/kibana/public/discover/controllers/discover.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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;
Expand Down
10 changes: 10 additions & 0 deletions src/plugins/kibana/public/discover/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@
<i aria-hidden="true" class="fa fa-folder-open-o"></i>
</button>
</kbn-tooltip>
<kbn-tooltip text="Share" placement="bottom" append-to-body="1">
<button
aria-label="Share Search"
aria-haspopup="true"
aria-expanded="{{ configTemplate.is('share') }}"
ng-class="{active: configTemplate.is('share')}"
ng-click="configTemplate.toggle('share');">
<i aria-hidden="true" class="fa fa-external-link"></i>
</button>
</kbn-tooltip>
</div>
</navbar>

Expand Down
5 changes: 5 additions & 0 deletions src/plugins/kibana/public/discover/partials/share_search.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<share
object-type="search"
object-id="{{opts.savedSearch.id}}"
allow-embed="false">
</share>
10 changes: 1 addition & 9 deletions src/plugins/kibana/public/visualize/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ define(function (require) {

require('ui/visualize');
require('ui/collapsible_sidebar');
require('ui/share');

require('ui/routes')
.when('/visualize/create', {
Expand Down Expand Up @@ -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: '<iframe src="' + $location.absUrl().replace('?', '?embed&') +
'" height="600" width="800"></iframe>'
};
};

$scope.unlink = function () {
if (!$state.linked) return;

Expand Down
26 changes: 4 additions & 22 deletions src/plugins/kibana/public/visualize/editor/panels/share.html
Original file line number Diff line number Diff line change
@@ -1,22 +1,4 @@
<form role="form" class="vis-share">

<p>
<div class="form-group">
<label>
Embed this visualization.
<small>Add to your html source. Note all clients must still be able to access kibana</small>
</label>
<div class="form-control" disabled>{{conf.shareData().embed}}</div>
</div>
</p>

<p>
<div class="form-group">
<label>
Share a link
</label>
<div class="form-control" disabled>{{conf.shareData().link}}</div>
</div>
</p>

</form>
<share
object-type="visualization"
object-id="{{conf.savedVis.id}}">
</share>
20 changes: 20 additions & 0 deletions src/server/http/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -122,5 +124,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'));
};
101 changes: 101 additions & 0 deletions src/server/http/short_url_lookup.js
Original file line number Diff line number Diff line change
@@ -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 '/';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thoughts on letting the server throw instead of redirecting to /? similar to how url/app/asdf will throw an invalid app error, or other pages give 404s

}
}
};
};
16 changes: 16 additions & 0 deletions src/ui/public/share/directives/share.js
Original file line number Diff line number Diff line change
@@ -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;
}
};
});
76 changes: 76 additions & 0 deletions src/ui/public/share/directives/share_object_url.js
Original file line number Diff line number Diff line change
@@ -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, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this need to be cleaned up on destroy?

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 () {
return $location.absUrl();
};

$scope.$watch('getUrl()', updateUrl);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we use $on('$locationChangeSuccess' instead?

}
};
});
2 changes: 2 additions & 0 deletions src/ui/public/share/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
require('./directives/share');
require('./directives/share_object_url');
Loading