diff --git a/src/kibana/components/courier/saved_object/saved_object.js b/src/kibana/components/courier/saved_object/saved_object.js
index ce984078f3819..cc934b6c390f9 100644
--- a/src/kibana/components/courier/saved_object/saved_object.js
+++ b/src/kibana/components/courier/saved_object/saved_object.js
@@ -119,20 +119,7 @@ define(function (require) {
_.assign(self, self._source);
return Promise.try(function () {
- // if we have a searchSource, set it's state based on the searchSourceJSON field
- if (self.searchSource) {
- var state = {};
- try {
- state = JSON.parse(meta.searchSourceJSON);
- } catch (e) {}
-
- var oldState = self.searchSource.toJSON();
- var fnProps = _.transform(oldState, function (dynamic, val, name) {
- if (_.isFunction(val)) dynamic[name] = val;
- }, {});
-
- self.searchSource.set(_.defaults(state, fnProps));
- }
+ parseSearchSource(meta.searchSourceJSON);
})
.then(hydrateIndexPattern)
.then(function () {
@@ -153,6 +140,23 @@ define(function (require) {
});
});
+ function parseSearchSource(searchSourceJson) {
+ if (!self.searchSource) return;
+
+ // if we have a searchSource, set its state based on the searchSourceJSON field
+ var state = {};
+ try {
+ state = JSON.parse(searchSourceJson);
+ } catch (e) {}
+
+ var oldState = self.searchSource.toJSON();
+ var fnProps = _.transform(oldState, function (dynamic, val, name) {
+ if (_.isFunction(val)) dynamic[name] = val;
+ }, {});
+
+ self.searchSource.set(_.defaults(state, fnProps));
+ }
+
/**
* After creation or fetching from ES, ensure that the searchSources index indexPattern
* is an bonafide IndexPattern object.
@@ -229,7 +233,7 @@ define(function (require) {
return docSource.doCreate(source)
.then(finish)
.catch(function (err) {
- var confirmMessage = 'Are you sure you want to overwrite this?';
+ var confirmMessage = 'Are you sure you want to overwrite ' + self.title + '?';
if (_.deepGet(err, 'origError.status') === 409 && window.confirm(confirmMessage)) {
return docSource.doIndex(source).then(finish);
}
@@ -264,7 +268,6 @@ define(function (require) {
});
});
};
-
}
return SavedObject;
diff --git a/src/kibana/directives/file_upload.js b/src/kibana/directives/file_upload.js
new file mode 100644
index 0000000000000..bccdd50e2a4f7
--- /dev/null
+++ b/src/kibana/directives/file_upload.js
@@ -0,0 +1,32 @@
+define(function (require) {
+ var module = require('modules').get('kibana');
+ var $ = require('jquery');
+
+ module.directive('fileUpload', function ($parse) {
+ return {
+ restrict: 'A',
+ link: function ($scope, $elem, attrs) {
+ var onUpload = $parse(attrs.fileUpload);
+
+ var $fileInput = $('');
+ $elem.after($fileInput);
+
+ $fileInput.on('change', function (e) {
+ var reader = new FileReader();
+ reader.onload = function (e) {
+ $scope.$apply(function () {
+ onUpload($scope, {fileContents: e.target.result});
+ });
+ };
+
+ var target = e.srcElement || e.target;
+ if (target && target.files && target.files.length) reader.readAsText(target.files[0]);
+ });
+
+ $elem.on('click', function (e) {
+ $fileInput.trigger('click');
+ });
+ }
+ };
+ });
+});
diff --git a/src/kibana/plugins/settings/sections/objects/_objects.html b/src/kibana/plugins/settings/sections/objects/_objects.html
index bce27431a4252..241520acb65f7 100644
--- a/src/kibana/plugins/settings/sections/objects/_objects.html
+++ b/src/kibana/plugins/settings/sections/objects/_objects.html
@@ -1,6 +1,10 @@
-
Edit Saved Objects
+
+
Edit Saved Objects
+
+
+
From here you can delete saved objects, such as saved searches. You can also edit the raw data of saved objects. Typically objects are only modified via their associated application, which is probably what you should use instead of this screen. Each tab is limited to 100 results. You can use the filter to find objects not in the default list.
@@ -20,13 +24,16 @@
Edit Saved Objects
- Delete Selected
+ confirmation="Are you sure want to delete the selected {{currentTab.title}}? This action is irreversible!"
+ class="btn btn-xs btn-danger" aria-label="Delete"> Delete
+ Export
@@ -51,8 +58,8 @@
Edit Saved Objects
diff --git a/src/kibana/plugins/settings/sections/objects/_objects.js b/src/kibana/plugins/settings/sections/objects/_objects.js
index 00e8c2c48bf78..425825c072d44 100644
--- a/src/kibana/plugins/settings/sections/objects/_objects.js
+++ b/src/kibana/plugins/settings/sections/objects/_objects.js
@@ -1,8 +1,12 @@
define(function (require) {
var _ = require('lodash');
+ var angular = require('angular');
+ var saveAs = require('file_saver');
var registry = require('plugins/settings/saved_object_registry');
var objectIndexHTML = require('text!plugins/settings/sections/objects/_objects.html');
+ require('directives/file_upload');
+
require('routes')
.when('/settings/objects', {
template: objectIndexHTML
@@ -12,42 +16,52 @@ define(function (require) {
.directive('kbnSettingsObjects', function (config, Notifier, Private, kbnUrl) {
return {
restrict: 'E',
- controller: function ($scope, $injector, $q, AppState) {
+ controller: function ($scope, $injector, $q, AppState, es) {
+ var notify = new Notifier({ location: 'Saved Objects' });
var $state = $scope.state = new AppState();
-
- var resetCheckBoxes = function () {
- $scope.deleteAll = false;
- _.each($scope.services, function (service) {
- _.each(service.data, function (item) {
- item.checked = false;
- });
- });
- };
+ $scope.currentTab = null;
+ $scope.selectedItems = [];
var getData = function (filter) {
var services = registry.all().map(function (obj) {
var service = $injector.get(obj.service);
return service.find(filter).then(function (data) {
- return { service: obj.service, title: obj.title, data: data.hits, total: data.total };
+ return {
+ service: service,
+ serviceName: obj.service,
+ title: obj.title,
+ type: service.type,
+ data: data.hits,
+ total: data.total
+ };
});
});
+
$q.all(services).then(function (data) {
$scope.services = _.sortBy(data, 'title');
- if (!$state.tab) {
- $scope.changeTab($scope.services[0]);
- }
+ var tab = $scope.services[0];
+ if ($state.tab) tab = _.find($scope.services, {title: $state.tab});
+ $scope.changeTab(tab);
});
};
- $scope.$watch('deleteAll', function (checked) {
- var service = _.find($scope.services, { title: $state.tab });
- if (!service) return;
- _.each(service.data, function (item) {
- item.checked = checked;
- });
- $scope.toggleDeleteBtn(service);
- });
+ $scope.toggleAll = function () {
+ if ($scope.selectedItems.length === $scope.currentTab.data.length) {
+ $scope.selectedItems.length = 0;
+ } else {
+ $scope.selectedItems = [].concat($scope.currentTab.data);
+ }
+ };
+
+ $scope.toggleItem = function (item) {
+ var i = $scope.selectedItems.indexOf(item);
+ if (i >= 0) {
+ $scope.selectedItems.splice(i, 1);
+ } else {
+ $scope.selectedItems.push(item);
+ }
+ };
$scope.open = function (item) {
kbnUrl.change(item.url.substr(1));
@@ -55,43 +69,103 @@ define(function (require) {
$scope.edit = function (service, item) {
var params = {
- service: service.service,
+ service: service.serviceName,
id: item.id
};
kbnUrl.change('/settings/objects/{{ service }}/{{ id }}', params);
};
- $scope.toggleDeleteBtn = function (service) {
- $scope.deleteAllBtn = _.some(service.data, { checked: true});
+ $scope.bulkDelete = function () {
+ $scope.currentTab.service.delete(_.pluck($scope.selectedItems, 'id')).then(refreshData);
};
- $scope.bulkDelete = function () {
- var serviceObj = _.find($scope.services, { title: $state.tab });
- if (!serviceObj) return;
- var service = $injector.get(serviceObj.service);
- var ids = _(serviceObj.data)
- .filter({ checked: true})
- .pluck('id')
- .value();
- service.delete(ids).then(function (resp) {
- serviceObj.data = _.filter(serviceObj.data, function (obj) {
- return !obj.checked;
- });
- resetCheckBoxes();
+ $scope.bulkExport = function () {
+ var objs = $scope.selectedItems.map(_.partialRight(_.extend, {type: $scope.currentTab.type}));
+ retrieveAndExportDocs(objs);
+ };
+
+ $scope.exportAll = function () {
+ var objs = $scope.services.map(function (service) {
+ return service.data.map(_.partialRight(_.extend, {type: service.type}));
});
+ retrieveAndExportDocs(_.flatten(objs));
};
- $scope.changeTab = function (obj) {
- $state.tab = obj.title;
+ function retrieveAndExportDocs(objs) {
+ es.mget({
+ index: config.file.kibana_index,
+ body: {docs: objs.map(transformToMget)}
+ })
+ .then(function (response) {
+ saveToFile(response.docs.map(_.partialRight(_.pick, '_id', '_type', '_source')));
+ });
+ }
+
+ // Takes an object and returns the associated data needed for an mget API request
+ function transformToMget(obj) {
+ return {_id: obj.id, _type: obj.type};
+ }
+
+ function saveToFile(results) {
+ var blob = new Blob([angular.toJson(results, true)], {type: 'application/json'});
+ saveAs(blob, 'export.json');
+ }
+
+ $scope.importAll = function (fileContents) {
+ var docs;
+ try {
+ docs = JSON.parse(fileContents);
+ } catch (e) {
+ notify.error('The file could not be processed.');
+ }
+
+ return es.mget({
+ index: config.file.kibana_index,
+ body: {docs: docs.map(_.partialRight(_.pick, '_id', '_type'))}
+ })
+ .then(function (response) {
+ var existingDocs = _.where(response.docs, {found: true});
+ var confirmMessage = 'The following objects will be overwritten:\n\n';
+ if (existingDocs.length === 0 || window.confirm(confirmMessage + _.pluck(existingDocs, '_id').join('\n'))) {
+ return es.bulk({
+ index: config.file.kibana_index,
+ body: _.flatten(docs.map(transformToBulk))
+ })
+ .then(refreshIndex)
+ .then(refreshData, notify.error);
+ }
+ });
+ };
+
+ // Takes a doc and returns the associated two entries for an index bulk API request
+ function transformToBulk(doc) {
+ return [
+ {index: _.pick(doc, '_id', '_type')},
+ doc._source
+ ];
+ }
+
+ function refreshIndex() {
+ return es.indices.refresh({
+ index: config.file.kibana_index
+ });
+ }
+
+ function refreshData() {
+ return getData($scope.advancedFilter);
+ }
+
+ $scope.changeTab = function (tab) {
+ $scope.currentTab = tab;
+ $scope.selectedItems.length = 0;
+ $state.tab = tab.title;
$state.save();
- resetCheckBoxes();
};
$scope.$watch('advancedFilter', function (filter) {
getData(filter);
});
-
}
};
});
diff --git a/src/kibana/plugins/settings/sections/objects/_view.js b/src/kibana/plugins/settings/sections/objects/_view.js
index 51b7b67f78ece..476096f5e472e 100644
--- a/src/kibana/plugins/settings/sections/objects/_view.js
+++ b/src/kibana/plugins/settings/sections/objects/_view.js
@@ -29,7 +29,7 @@ define(function (require) {
*
* @param {array} memo The stack of fields
* @param {mixed} value The value of the field
- * @param {stirng} key The key of the field
+ * @param {string} key The key of the field
* @param {object} collection This is a reference the collection being reduced
* @param {array} parents The parent keys to the field
* @returns {array}
diff --git a/src/kibana/plugins/settings/styles/main.less b/src/kibana/plugins/settings/styles/main.less
index 731248bdc549a..338adc1fb34ca 100644
--- a/src/kibana/plugins/settings/styles/main.less
+++ b/src/kibana/plugins/settings/styles/main.less
@@ -48,12 +48,18 @@ kbn-settings-objects {
font-weight: normal;
}
- .delete-all {
+ .btn {
font-size: 10px;
margin-left: 20px;
}
}
+ .header {
+ .title, .controls {
+ padding-right: 1em;
+ display: inline-block;
+ }
+ }
}
kbn-settings-advanced {