From 6901399fd0cb61584f205c027938f7bc20911600 Mon Sep 17 00:00:00 2001 From: Bryan Davis Date: Tue, 22 Jan 2013 15:31:47 -0700 Subject: [PATCH] Add file system based saved searches. A quick and dirty hack to add the ability to save searches to files on the file system. There are no permissions checks to see who can save, load or delete a search. The same list of searches is seen by all users of the instance. - Adds "Load" and "Save" buttons to side bar. - "Load" button retrieves a list of currently saved searches from server and displays a dialog that allows the user to choose a search to load into the current session or to delete. - "Save" prompts for a name and sends the current search hash to the server for storage under that name. If the current search started from a saved search, the same name will be pre-populated in the save dialog. - Adds restful GET, PUT and DELETE endpoints at /api/saved/:name for server side handling of storage/retrieval activities. Related to issue #114 at https://github.com/rashidkpc/Kibana --- .gitignore | 2 +- KibanaConfig.rb | 3 ++ lib/kibana-app.rb | 40 ++++++++++++++ public/index.html | 1 + public/lib/js/ajax.js | 119 +++++++++++++++++++++++++++++++++++++++++- 5 files changed, 163 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 4d124c8e5..36af83590 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ - +saved KibanaConfig.rbackup.rb *.DS_Store /tmp/* diff --git a/KibanaConfig.rb b/KibanaConfig.rb index 227935fc9..39eaee6cc 100644 --- a/KibanaConfig.rb +++ b/KibanaConfig.rb @@ -137,4 +137,7 @@ module KibanaConfig # Use this interval as fallback. Fallback_interval = 900 + + # directory for hash storage + Storage_dir = 'saved' end diff --git a/lib/kibana-app.rb b/lib/kibana-app.rb index 7bd6df05c..8a51832ee 100644 --- a/lib/kibana-app.rb +++ b/lib/kibana-app.rb @@ -329,4 +329,44 @@ def link_to url_fragment, mode=:full_url get '/js/timezone.js' do erb :timezone end + + # Saved Search URL Routes + # + # Get a list of saved searches or a particular saved search + get '/api/saved/:name?' do + if params[:name].nil? + # return a list of all known saved files + data = { + "files" => Dir["#{KibanaConfig::Storage_dir}/*"].map {|f| + File.basename(f) + }, + } + JSON.generate(data) + else + # return the contents of a named file + begin + File.read(File.join(KibanaConfig::Storage_dir, params[:name])) + rescue Errno::ENOENT + return 404 + end + end + end + + # Save a search + put '/api/saved/:name' do + File.open("#{KibanaConfig::Storage_dir}/#{params[:name]}", 'w') do |f| + f.write(request.body.read) + end + return [201, "Created /api/saved/#{params[:name]}"] + end + + # Delete a saved search + delete '/api/saved/:name' do + begin + File.delete("#{KibanaConfig::Storage_dir}/#{params[:name]}") + return [200, "Deleted /api/saved/#{params[:name]}"] + rescue Errno::ENOENT + return 404 + end + end end diff --git a/public/index.html b/public/index.html index 2bd911cec..8bc499e30 100644 --- a/public/index.html +++ b/public/index.html @@ -80,6 +80,7 @@ diff --git a/public/lib/js/ajax.js b/public/lib/js/ajax.js index fb5c14a1e..afed290d0 100644 --- a/public/lib/js/ajax.js +++ b/public/lib/js/ajax.js @@ -196,6 +196,11 @@ function getPage() { '

'); getGraph(window.interval); + // buttons to load/save searches + $('#saved').html( + '
' + + '' + + '
'); } } }); @@ -751,7 +756,7 @@ function pageLinks() { // This is very ugly function blank_page() { var selectors = ['#graph','#graphheader','#feedlinks','#logs','.pagelinks', - '#fields','#analyze'] + '#fields','#analyze','#saved'] for (var selector in selectors) { $(selectors[selector]).text(""); @@ -1721,4 +1726,116 @@ function bind_clicks() { unhighlight_all_events(); }); + // save and load + $("body").delegate("#load-search", "click", function () { + window.request = $.ajax({ + url: "api/saved/", + type: "GET", + cache: false, + success: function (json) { + var resp = JSON.parse(json) + , elm = []; + + for (var idx in resp.files) { + var file = resp.files[idx]; + elm.push(''); + } + + // hide any other popovers + if (popover_visible) { + $('.popover').remove(); + } + + // create and show our popover + $("#load-search").popover({ + trigger: 'manual', + classes: 'auto-width', + title: 'Load saved search', + content: '
' + + '
' + + '' + + '
' + + '   ' + + '' + + '
' + + '
' + + '
', + }).popover('show'); + } //end ajax success + }); + }); + + $("body").delegate("#load-btn-load", "click", function () { + var $sel = $("#load-select"); // hack!! + if ($sel[0].selectedIndex >= 0) { + var fname = $sel[0].options[$sel[0].selectedIndex].value; + window.request = $.ajax({ + url: "api/saved/" + encodeURIComponent(fname), + type: "GET", + cache: false, + success: function (data, status, xhr) { + var hash = data.replace(/^\s+|\s+$/g, ''); + $("#load-search").popover('destroy'); + window.location.hash = hash; + }, //end ajax success + error: function (xhr, status, err) { + console.log(err); + alert("Load failed for " + fname); + } //end ajax error + }); + } else { + // tell them to select something + $sel.wrap('
'); + } + }); + $("body").delegate("#load-btn-delete", "click", function () { + var $sel = $("#load-select"); // hack!! + if ($sel[0].selectedIndex >= 0) { + var fname = $sel[0].options[$sel[0].selectedIndex].value; + window.request = $.ajax({ + url: "api/saved/" + encodeURIComponent(fname), + type: "DELETE", + cache: false, + success: function (data, status, xhr) { + alert('Deleted ' + fname); + $("#load-search").popover('destroy'); + }, //end ajax success + error: function (xhr, status, err) { + console.log(err); + alert("Delete failed for " + fname); + } //end ajax error + }); + } + }); + $("body").delegate("#load-btn-cancel", "click", function () { + $("#load-search").popover('destroy'); + }); + + $("body").delegate("#save-search", "click", function () { + var fname = prompt("Save search as:", window.hashjson.fname || ''); + if (fname) { + window.hashjson.fname = fname; + var payload = Base64.encode(JSON.stringify(window.hashjson, null, '')); + // make the ajax put call + window.request = $.ajax({ + url: "api/saved/" + encodeURIComponent(fname), + type: "PUT", + data: payload, + success: function (data, status, xhr) { + alert('Search saved.'); + }, //end ajax success + error: function (xhr, status, err) { + console.log(err); + alert("Save failed for " + fname); + } //end ajax error + }); + } else { + alert('No save file name specified.'); + } + }); }