Skip to content

Commit

Permalink
Merge pull request #3573 from lukasolson/import-export
Browse files Browse the repository at this point in the history
Import/export saved objects
  • Loading branch information
spalger committed Apr 20, 2015
2 parents 5331800 + f866122 commit 9d79891
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 67 deletions.
35 changes: 19 additions & 16 deletions src/kibana/components/courier/saved_object/saved_object.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand All @@ -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.
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -264,7 +268,6 @@ define(function (require) {
});
});
};

}

return SavedObject;
Expand Down
32 changes: 32 additions & 0 deletions src/kibana/directives/file_upload.js
Original file line number Diff line number Diff line change
@@ -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 = $('<input type="file" style="opacity: 0" id="testfile" />');
$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');
});
}
};
});
});
21 changes: 14 additions & 7 deletions src/kibana/plugins/settings/sections/objects/_objects.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
<kbn-settings-app section="objects">
<kbn-settings-objects class="container">
<h2>Edit Saved Objects</h2>
<div class="header">
<h2 class="title">Edit Saved Objects</h2>
<button class="btn btn-default controls" ng-click="exportAll()"><i aria-hidden="true" class="fa fa-download"></i> Export</button>
<button file-upload="importAll(fileContents)" class="btn btn-default controls" ng-click><i aria-hidden="true" class="fa fa-upload"></i> Import</button>
</div>
<p>
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.
</p>
Expand All @@ -20,13 +24,16 @@ <h2>Edit Saved Objects</h2>
<div class="tab-content">
<div class="action-bar">
<label>
<input type="checkbox" ng-model="deleteAll">
<input type="checkbox" ng-checked="currentTab.data.length > 0 && selectedItems.length == currentTab.data.length" ng-click="toggleAll()" />
Select All
</label>
<a ng-disabled="!deleteAllBtn"
<a ng-disabled="selectedItems.length == 0"
confirm-click="bulkDelete()"
confirmation="Are you sure want to delete the selected {{service.title}}? This action is irreversible!"
class="delete-all btn btn-danger btn-xs" aria-label="Delete Selected"><i aria-hidden="true" class="fa fa-trash"></i> Delete Selected</a>
confirmation="Are you sure want to delete the selected {{currentTab.title}}? This action is irreversible!"
class="btn btn-xs btn-danger" aria-label="Delete"><i aria-hidden="true" class="fa fa-trash"></i> Delete</a>
<a ng-disabled="selectedItems.length == 0"
ng-click="bulkExport()"
class="btn btn-xs btn-default" aria-label="Export"><i aria-hidden="true" class="fa fa-download"></i> Export</a>
</div>
<div ng-repeat="service in services" ng-class="{ active: state.tab === service.title }" class="tab-pane">
<ul class="list-unstyled">
Expand All @@ -51,8 +58,8 @@ <h2>Edit Saved Objects</h2>

<div class="pull-left">
<input
ng-click="item.checked = !item.checked; toggleDeleteBtn(service)"
ng-checked="item.checked"
ng-click="toggleItem(item)"
ng-checked="selectedItems.indexOf(item) >= 0"
type="checkbox" >
</div>

Expand Down
158 changes: 116 additions & 42 deletions src/kibana/plugins/settings/sections/objects/_objects.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -12,86 +16,156 @@ 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));
};

$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);
});

}
};
});
Expand Down
2 changes: 1 addition & 1 deletion src/kibana/plugins/settings/sections/objects/_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
8 changes: 7 additions & 1 deletion src/kibana/plugins/settings/styles/main.less
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 9d79891

Please sign in to comment.