diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel.html b/src/core_plugins/kibana/public/dashboard/panel/panel.html index 2f94871efead3..38d56f67777ea 100644 --- a/src/core_plugins/kibana/public/dashboard/panel/panel.html +++ b/src/core_plugins/kibana/public/dashboard/panel/panel.html @@ -73,7 +73,7 @@
- +
- {{$select.selected}} + {{$select.selected.title}} - -
+ +
@@ -20,8 +20,9 @@ class="index-pattern-label" id="index_pattern_id" tabindex="0" - css-truncate - >{{ indexPattern.id }} + css-truncate> + + {{ indexPattern.title }} diff --git a/src/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.js b/src/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.js index a22fe04717585..c95c7cda63925 100644 --- a/src/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.js +++ b/src/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.js @@ -12,8 +12,6 @@ import { uiModules } from 'ui/modules'; import fieldChooserTemplate from 'plugins/kibana/discover/components/field_chooser/field_chooser.html'; const app = uiModules.get('apps/discover'); - - app.directive('discFieldChooser', function ($location, globalState, config, $route, Private) { const FieldList = Private(IndexPatternsFieldListProvider); @@ -32,8 +30,9 @@ app.directive('discFieldChooser', function ($location, globalState, config, $rou }, template: fieldChooserTemplate, link: function ($scope) { - $scope.setIndexPattern = function (id) { - $scope.state.index = id; + $scope.indexPatternList = _.sortBy($scope.indexPatternList, o => o.get('title')); + $scope.setIndexPattern = function (pattern) { + $scope.state.index = pattern.id; $scope.state.save(); }; diff --git a/src/core_plugins/kibana/public/discover/controllers/discover.js b/src/core_plugins/kibana/public/discover/controllers/discover.js index 4f6f2f1fed57f..70872d902b0d3 100644 --- a/src/core_plugins/kibana/public/discover/controllers/discover.js +++ b/src/core_plugins/kibana/public/discover/controllers/discover.js @@ -29,6 +29,7 @@ import { uiModules } from 'ui/modules'; import indexTemplate from 'plugins/kibana/discover/index.html'; import { StateProvider } from 'ui/state_management/state'; import { documentationLinks } from 'ui/documentation_links/documentation_links'; +import { SavedObjectsClientProvider } from 'ui/saved_objects'; const app = uiModules.get('apps/discover', [ 'kibana/notify', @@ -46,8 +47,14 @@ uiRoutes resolve: { ip: function (Promise, courier, config, $location, Private) { const State = Private(StateProvider); - return courier.indexPatterns.getIds() - .then(function (list) { + const savedObjectsClient = Private(SavedObjectsClientProvider); + + return savedObjectsClient.find({ + type: 'index-pattern', + fields: ['title'], + perPage: 10000 + }) + .then(({ savedObjects }) => { /** * In making the indexPattern modifiable it was placed in appState. Unfortunately, * the load order of AppState conflicts with the load order of many other things @@ -60,12 +67,12 @@ uiRoutes const state = new State('_a', {}); const specified = !!state.index; - const exists = _.contains(list, state.index); + const exists = _.findIndex(savedObjects, o => o.id === state.index) > -1; const id = exists ? state.index : config.get('defaultIndex'); state.destroy(); return Promise.props({ - list: list, + list: savedObjects, loaded: courier.indexPatterns.get(id), stateVal: state.index, stateValFound: specified && exists @@ -211,7 +218,7 @@ function discoverController($scope, config, courier, $route, $window, Notifier, const body = await searchSource.getSearchRequestBody(); return { searchRequest: { - index: searchSource.get('index').id, + index: searchSource.get('index').title, body }, fields: selectFields, @@ -251,8 +258,6 @@ function discoverController($scope, config, courier, $route, $window, Notifier, $scope.opts = { // number of records to fetch, then paginate through sampleSize: config.get('discover:sampleSize'), - // Index to match - index: $scope.indexPattern.id, timefield: $scope.indexPattern.timeFieldName, savedSearch: savedSearch, indexPatternList: $route.current.locals.ip.list, @@ -661,7 +666,7 @@ function discoverController($scope, config, courier, $route, $window, Notifier, if (own && !stateVal) return own; if (stateVal && !stateValFound) { - const err = '"' + stateVal + '" is not a configured pattern. '; + const err = '"' + stateVal + '" is not a configured pattern ID. '; if (own) { notify.warning(err + ' Using the saved index pattern: "' + own.id + '"'); return own; diff --git a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern/__tests__/create_index_pattern.js b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern/__tests__/create_index_pattern.js index f29a6be44cd98..05d8ae759071e 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern/__tests__/create_index_pattern.js +++ b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern/__tests__/create_index_pattern.js @@ -19,7 +19,7 @@ describe('createIndexPattern UI', () => { current: { params: {}, locals: { - indexPatternIds: [] + indexPatterns: [] } } }); diff --git a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern/create_index_pattern.html b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern/create_index_pattern.html index 5bc4f9ec40338..27160a9a0471d 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern/create_index_pattern.html +++ b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern/create_index_pattern.html @@ -23,12 +23,20 @@ class="kuiVerticalRhythm" ng-submit="controller.createIndexPattern()" > +
- +
+ +
+ + +
+ +
+ + +
+

+
+
+
diff --git a/src/core_plugins/kibana/public/management/sections/indices/index.html b/src/core_plugins/kibana/public/management/sections/indices/index.html index f12b3b5cc3c95..03f2b974ad84a 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/index.html +++ b/src/core_plugins/kibana/public/management/sections/indices/index.html @@ -25,13 +25,13 @@
diff --git a/src/core_plugins/kibana/public/management/sections/indices/index.js b/src/core_plugins/kibana/public/management/sections/indices/index.js index f53024a762411..5fedf3ca08be2 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/index.js +++ b/src/core_plugins/kibana/public/management/sections/indices/index.js @@ -4,10 +4,17 @@ import './edit_index_pattern'; import uiRoutes from 'ui/routes'; import { uiModules } from 'ui/modules'; import indexTemplate from 'plugins/kibana/management/sections/indices/index.html'; +import { SavedObjectsClientProvider } from 'ui/saved_objects'; const indexPatternsResolutions = { - indexPatternIds: function (courier) { - return courier.indexPatterns.getIds(); + indexPatterns: function (Private) { + const savedObjectsClient = Private(SavedObjectsClientProvider); + + return savedObjectsClient.find({ + type: 'index-pattern', + fields: ['title'], + perPage: 10000 + }).then(response => response.savedObjects); } }; @@ -34,10 +41,12 @@ uiModules.get('apps/management') config.bindToScope($scope, 'defaultIndex'); $scope.$watch('defaultIndex', function () { - const ids = $route.current.locals.indexPatternIds; - $scope.indexPatternList = ids.map(function (id) { + $scope.indexPatternList = $route.current.locals.indexPatterns.map(pattern => { + const id = pattern.id; + return { id: id, + title: pattern.get('title'), url: kbnUrl.eval('#/management/kibana/indices/{{id}}', { id: id }), class: 'sidebar-item-title ' + ($scope.editingId === id ? 'active' : ''), default: $scope.defaultIndex === id diff --git a/src/core_plugins/kibana/public/management/sections/indices/refresh_kibana_index.js b/src/core_plugins/kibana/public/management/sections/indices/refresh_kibana_index.js deleted file mode 100644 index d185d9a049a5b..0000000000000 --- a/src/core_plugins/kibana/public/management/sections/indices/refresh_kibana_index.js +++ /dev/null @@ -1,7 +0,0 @@ -export function RefreshKibanaIndex(esAdmin, kbnIndex) { - return function () { - return esAdmin.indices.refresh({ - index: kbnIndex - }); - }; -} diff --git a/src/core_plugins/kibana/public/visualize/listing/visualize_listing.html b/src/core_plugins/kibana/public/visualize/listing/visualize_listing.html index 6155b54c49f72..ad829c04380ca 100644 --- a/src/core_plugins/kibana/public/visualize/listing/visualize_listing.html +++ b/src/core_plugins/kibana/public/visualize/listing/visualize_listing.html @@ -192,7 +192,6 @@
- diff --git a/src/core_plugins/kibana/public/visualize/wizard/step_2.html b/src/core_plugins/kibana/public/visualize/wizard/step_2.html index 0d6b224e440ad..492e5d66e0174 100644 --- a/src/core_plugins/kibana/public/visualize/wizard/step_2.html +++ b/src/core_plugins/kibana/public/visualize/wizard/step_2.html @@ -22,6 +22,7 @@

diff --git a/src/core_plugins/kibana/public/visualize/wizard/wizard.js b/src/core_plugins/kibana/public/visualize/wizard/wizard.js index dc90f63f9b006..ace5f77567f9f 100644 --- a/src/core_plugins/kibana/public/visualize/wizard/wizard.js +++ b/src/core_plugins/kibana/public/visualize/wizard/wizard.js @@ -13,6 +13,7 @@ import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; import { uiModules } from 'ui/modules'; import visualizeWizardStep1Template from './step_1.html'; import visualizeWizardStep2Template from './step_2.html'; +import { SavedObjectsClientProvider } from 'ui/saved_objects'; const module = uiModules.get('app/visualize', ['kibana/courier']); @@ -166,8 +167,14 @@ routes.when(VisualizeConstants.WIZARD_STEP_2_PAGE_PATH, { template: visualizeWizardStep2Template, controller: 'VisualizeWizardStep2', resolve: { - indexPatternIds: function (courier) { - return courier.indexPatterns.getIds(); + indexPatterns: function (Private) { + const savedObjectsClient = Private(SavedObjectsClientProvider); + + return savedObjectsClient.find({ + type: 'index-pattern', + fields: ['title'], + perPage: 10000 + }).then(response => response.savedObjects); } } }); @@ -197,7 +204,7 @@ module.controller('VisualizeWizardStep2', function ($route, $scope, timefilter, $scope.indexPattern = { selection: null, - list: $route.current.locals.indexPatternIds + list: $route.current.locals.indexPatterns }; $scope.makeUrl = function (pattern) { @@ -206,9 +213,9 @@ module.controller('VisualizeWizardStep2', function ($route, $scope, timefilter, if (addToDashMode) { return `#${VisualizeConstants.CREATE_PATH}` + `?${DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM}` + - `&type=${type}&indexPattern=${pattern}`; + `&type=${type}&indexPattern=${pattern.id}`; } - return `#${VisualizeConstants.CREATE_PATH}?type=${type}&indexPattern=${pattern}`; + return `#${VisualizeConstants.CREATE_PATH}?type=${type}&indexPattern=${pattern.id}`; }; }); diff --git a/src/core_plugins/kibana/translations/en.json b/src/core_plugins/kibana/translations/en.json index 54af102ae8f5c..1fe78b8a9e88f 100644 --- a/src/core_plugins/kibana/translations/en.json +++ b/src/core_plugins/kibana/translations/en.json @@ -11,7 +11,9 @@ "KIBANA-WILD_CARD_PATTERN": " using wildcard pattern names instead of time-interval based index patterns.", "KIBANA-RECOMMEND_WILD_CARD_PATTERN_DETAILS": "Kibana is now smart enough to automatically determine which indices to search against within the current time range for wildcard index patterns. This means that wildcard index patterns now get the same performance optimizations when searching within a time range as time-interval patterns.", "KIBANA-INDEX_PATTERN_INTERVAL": "Index pattern interval", - "KIBANA-INDEX_NAME_OR_PATTERN": "Index name or pattern", + "KIBANA-INDEX_PATTERN": "Index pattern", + "KIBANA-INDEX_PATTERN_ID": "Index pattern ID", + "KIBANA-INDEX_PATTERN_SPECIFY_ID": "Creates the index pattern with the specified ID.", "KIBANA-WILDCARD_DYNAMIC_INDEX_PATTERNS": "Patterns allow you to define dynamic index names using * as a wildcard. Example: logstash-*", "KIBANA-STATIC_TEXT_IN_DYNAMIC_INDEX_PATTERNS": "Patterns allow you to define dynamic index names. Static text in an index name is denoted using brackets. Example: [logstash-]YYYY.MM.DD. Please note that weeks are setup to use ISO weeks which start on Monday.", "KIBANA-NOTE_COLON": "Note:", @@ -29,6 +31,7 @@ "KIBANA-EXISTING_MATCH_PERCENT": "Pattern matches {{indexExistingMatchPercent}} of existing indices and aliases", "KIBANA-NON_MATCHING_INDICES_AND_ALIASES": "Indices and aliases that were found, but did not match the pattern:", "KIBANA-MORE": "more", + "KIBANA-ADVANCED_OPTIONS": "advanced options", "KIBANA-TIME_FILTER_FIELD_NAME": "Time Filter field name", "KIBANA-NO_DATE_FIELD_DESIRED": "I don't want to use the Time Filter", "KIBANA-REFRESH_FIELDS": "refresh fields", diff --git a/src/fixtures/stubbed_doc_source_response.js b/src/fixtures/stubbed_doc_source_response.js deleted file mode 100644 index 99055142e5bb1..0000000000000 --- a/src/fixtures/stubbed_doc_source_response.js +++ /dev/null @@ -1,22 +0,0 @@ -import FixturesLogstashFieldsProvider from 'fixtures/logstash_fields'; - -function stubbedDocSourceResponse(Private) { - const mockLogstashFields = Private(FixturesLogstashFieldsProvider); - - return function (id, index) { - index = index || '.kibana'; - return { - _id: id, - _index: index, - _type: 'index-pattern', - _version: 2, - found: true, - _source: { - customFormats: '{}', - fields: JSON.stringify(mockLogstashFields) - } - }; - }; -} - -export default stubbedDocSourceResponse; \ No newline at end of file diff --git a/src/fixtures/stubbed_saved_object_index_pattern.js b/src/fixtures/stubbed_saved_object_index_pattern.js new file mode 100644 index 0000000000000..5d30587a3b6f2 --- /dev/null +++ b/src/fixtures/stubbed_saved_object_index_pattern.js @@ -0,0 +1,18 @@ +import FixturesLogstashFieldsProvider from 'fixtures/logstash_fields'; +import { SavedObject } from 'ui/saved_objects'; + +export function FixturesStubbedSavedObjectIndexPatternProvider(Private) { + const mockLogstashFields = Private(FixturesLogstashFieldsProvider); + + return function (id) { + return new SavedObject(undefined, { + id, + type: 'index-pattern', + attributes: { + customFormats: '{}', + fields: JSON.stringify(mockLogstashFields) + }, + version: 2 + }); + }; +} diff --git a/src/ui/public/courier/__tests__/saved_object.js b/src/ui/public/courier/__tests__/saved_object.js index 82d0e2a38e37f..66db8176e0815 100644 --- a/src/ui/public/courier/__tests__/saved_object.js +++ b/src/ui/public/courier/__tests__/saved_object.js @@ -36,7 +36,6 @@ describe('Saved Object', function () { // Necessary to avoid a timeout condition. sinon.stub(esAdminStub.indices, 'putMapping').returns(BluebirdPromise.resolve()); - sinon.stub(esAdminStub.indices, 'refresh').returns(BluebirdPromise.resolve()); } /** @@ -62,11 +61,9 @@ describe('Saved Object', function () { * @param {Object} mockDocResponse */ function stubESResponse(mockDocResponse) { - sinon.stub(esAdminStub, 'mget').returns(BluebirdPromise.resolve({ docs: [mockDocResponse] })); - sinon.stub(esAdminStub, 'index').returns(BluebirdPromise.resolve(mockDocResponse)); - // Stub out search for duplicate title: sinon.stub(savedObjectsClientStub, 'get').returns(BluebirdPromise.resolve(mockDocResponse)); + sinon.stub(savedObjectsClientStub, 'update').returns(BluebirdPromise.resolve(mockDocResponse)); sinon.stub(savedObjectsClientStub, 'find').returns(BluebirdPromise.resolve({ savedObjects: [], total: 0 })); sinon.stub(savedObjectsClientStub, 'bulkGet').returns(BluebirdPromise.resolve({ savedObjects: [mockDocResponse] })); @@ -112,8 +109,6 @@ describe('Saved Object', function () { describe('with confirmOverwrite', function () { function stubConfirmOverwrite() { window.confirm = sinon.stub().returns(true); - - sinon.stub(esAdminStub, 'create').returns(BluebirdPromise.reject({ status : 409 })); sinon.stub(esDataStub, 'create').returns(BluebirdPromise.reject({ status : 409 })); } @@ -421,7 +416,6 @@ describe('Saved Object', function () { }); describe('searchSource', function () { - it('when true, creates index', function () { const indexPatternId = 'testIndexPattern'; const afterESRespCallback = sinon.spy(); @@ -434,10 +428,12 @@ describe('Saved Object', function () { }; stubESResponse({ - _id: indexPatternId, - _type: 'dashboard', - _source: {}, - found: true + id: indexPatternId, + type: 'dashboard', + attributes: { + title: 'testIndexPattern' + }, + _version: 2 }); const savedObject = new SavedObject(config); diff --git a/src/ui/public/courier/courier.js b/src/ui/public/courier/courier.js index 390c567b4a216..c3f7ba57d3902 100644 --- a/src/ui/public/courier/courier.js +++ b/src/ui/public/courier/courier.js @@ -12,7 +12,6 @@ import { SearchStrategyProvider } from './fetch/strategy/search'; import { RequestQueueProvider } from './_request_queue'; import { FetchProvider } from './fetch'; import { DocDataLooperProvider } from './looper/doc_data'; -import { DocAdminLooperProvider } from './looper/doc_admin'; import { SearchLooperProvider } from './looper/search'; import { RootSearchSourceProvider } from './data_source/_root_search_source'; import { SavedObjectProvider } from './saved_object'; @@ -32,7 +31,6 @@ uiModules.get('kibana/courier') const fetch = Private(FetchProvider); const docDataLooper = self.docLooper = Private(DocDataLooperProvider); - const docAdminLooper = self.docLooper = Private(DocAdminLooperProvider); const searchLooper = self.searchLooper = Private(SearchLooperProvider); // expose some internal modules @@ -62,7 +60,6 @@ uiModules.get('kibana/courier') self.start = function () { searchLooper.start(); docDataLooper.start(); - docAdminLooper.start(); return this; }; @@ -121,7 +118,6 @@ uiModules.get('kibana/courier') */ self.close = function () { searchLooper.stop(); - docAdminLooper.stop(); docDataLooper.stop(); _.invoke(requestQueue, 'abort'); diff --git a/src/ui/public/courier/data_source/admin_doc_source.js b/src/ui/public/courier/data_source/admin_doc_source.js deleted file mode 100644 index 087f06821f044..0000000000000 --- a/src/ui/public/courier/data_source/admin_doc_source.js +++ /dev/null @@ -1,21 +0,0 @@ -import { AbstractDocSourceProvider } from './_abstract_doc_source'; -import { DocAdminStrategyProvider } from '../fetch/strategy/doc_admin'; -import { AdminDocRequestProvider } from '../fetch/request/doc_admin'; - -export function AdminDocSourceProvider(Private) { - const AbstractDocSource = Private(AbstractDocSourceProvider); - const docStrategy = Private(DocAdminStrategyProvider); - const AdminDocRequest = Private(AdminDocRequestProvider); - - class AdminDocSource extends AbstractDocSource { - constructor(initialState) { - super(initialState, docStrategy); - } - - _createRequest(defer) { - return new AdminDocRequest(this, defer); - } - } - - return AdminDocSource; -} diff --git a/src/ui/public/courier/fetch/request/doc_admin.js b/src/ui/public/courier/fetch/request/doc_admin.js deleted file mode 100644 index bb679f9e76d52..0000000000000 --- a/src/ui/public/courier/fetch/request/doc_admin.js +++ /dev/null @@ -1,14 +0,0 @@ -import { DocAdminStrategyProvider } from '../strategy/doc_admin'; -import { AbstractDocRequestProvider } from './_abstract_doc'; - -export function AdminDocRequestProvider(Private) { - - const docStrategy = Private(DocAdminStrategyProvider); - const AbstractDocRequest = Private(AbstractDocRequestProvider); - - class AdminDocRequest extends AbstractDocRequest { - strategy = docStrategy; - } - - return AdminDocRequest; -} diff --git a/src/ui/public/courier/fetch/strategy/doc_admin.js b/src/ui/public/courier/fetch/strategy/doc_admin.js deleted file mode 100644 index 7253474c40390..0000000000000 --- a/src/ui/public/courier/fetch/strategy/doc_admin.js +++ /dev/null @@ -1,26 +0,0 @@ -export function DocAdminStrategyProvider(Promise) { - return { - id: 'doc_admin', - clientMethod: 'mget', - - /** - * Flatten a series of requests into as ES request body - * @param {array} requests - an array of flattened requests - * @return {Promise} - a promise that is fulfilled by the request body - */ - reqsFetchParamsToBody: function (reqsFetchParams) { - return Promise.resolve({ - docs: reqsFetchParams - }); - }, - - /** - * Fetch the multiple responses from the ES Response - * @param {object} resp - The response sent from Elasticsearch - * @return {array} - the list of responses - */ - getResponses: function (resp) { - return resp.docs; - } - }; -} diff --git a/src/ui/public/courier/looper/doc_admin.js b/src/ui/public/courier/looper/doc_admin.js deleted file mode 100644 index 9201594c0621b..0000000000000 --- a/src/ui/public/courier/looper/doc_admin.js +++ /dev/null @@ -1,19 +0,0 @@ -import { FetchProvider } from '../fetch'; -import { LooperProvider } from './_looper'; -import { DocAdminStrategyProvider } from '../fetch/strategy/doc_admin'; - -export function DocAdminLooperProvider(Private) { - const fetch = Private(FetchProvider); - const Looper = Private(LooperProvider); - const DocStrategy = Private(DocAdminStrategyProvider); - - /** - * The Looper which will manage the doc fetch interval - * @type {Looper} - */ - const docLooper = new Looper(1500, function () { - fetch.fetchQueued(DocStrategy); - }); - - return docLooper; -} diff --git a/src/ui/public/courier/saved_object/get_title_already_exists.js b/src/ui/public/courier/saved_object/get_title_already_exists.js deleted file mode 100644 index 17a8d8afd6105..0000000000000 --- a/src/ui/public/courier/saved_object/get_title_already_exists.js +++ /dev/null @@ -1,33 +0,0 @@ -import { find } from 'lodash'; -/** - * Returns true if the given saved object has a title that already exists, false otherwise. Search is case - * insensitive. - * @param savedObject {SavedObject} The object with the title to check. - * @param esAdmin {Object} Used to query es - * @returns {Promise} Returns the title that matches. Because this search is not case - * sensitive, it may not exactly match the title of the object. - */ -export function getTitleAlreadyExists(savedObject, savedObjectsClient) { - const { title, id } = savedObject; - const type = savedObject.getEsType(); - if (!title) { - throw new Error('Title must be supplied'); - } - - // Elastic search will return the most relevant results first, which means exact matches should come - // first, and so we shouldn't need to request everything. Using 10 just to be on the safe side. - const perPage = 10; - return savedObjectsClient.find({ - type, - perPage, - search: title, - searchFields: 'title', - fields: ['title'] - }).then(response => { - const match = find(response.savedObjects, (obj) => { - return obj.id !== id && obj.get('title').toLowerCase() === title.toLowerCase(); - }); - - return match ? match.get('title') : undefined; - }); -} diff --git a/src/ui/public/courier/saved_object/saved_object.js b/src/ui/public/courier/saved_object/saved_object.js index c46074bce16be..209e2d908721c 100644 --- a/src/ui/public/courier/saved_object/saved_object.js +++ b/src/ui/public/courier/saved_object/saved_object.js @@ -16,8 +16,7 @@ import { SavedObjectNotFound } from 'ui/errors'; import MappingSetupProvider from 'ui/utils/mapping_setup'; import { SearchSourceProvider } from '../data_source/search_source'; -import { getTitleAlreadyExists } from './get_title_already_exists'; -import { SavedObjectsClientProvider } from 'ui/saved_objects'; +import { SavedObjectsClientProvider, findObjectByTitle } from 'ui/saved_objects'; /** * An error message to be used when the user rejects a confirm overwrite. @@ -312,12 +311,13 @@ export function SavedObjectProvider(esAdmin, kbnIndex, Promise, Private, Notifie return Promise.resolve(); } - return getTitleAlreadyExists(this, savedObjectsClient) - .then(duplicateTitle => { - if (!duplicateTitle) return true; + return findObjectByTitle(savedObjectsClient, this.getEsType(), this.title) + .then(duplicate => { + if (!duplicate) return true; + if (duplicate.id === this.id) return true; const confirmMessage = - `A ${this.getDisplayName()} with the title '${duplicateTitle}' already exists. Would you like to save anyway?`; + `A ${this.getDisplayName()} with the title '${this.title}' already exists. Would you like to save anyway?`; return confirmModalPromise(confirmMessage, { confirmButtonText: `Save ${this.getDisplayName()}` }) .catch(() => Promise.reject(new Error(SAVE_DUPLICATE_REJECTED))); diff --git a/src/ui/public/directives/__tests__/validate_index_name.js b/src/ui/public/directives/__tests__/validate_index_name.js index 72667f91a89f6..26c43f3be3148 100644 --- a/src/ui/public/directives/__tests__/validate_index_name.js +++ b/src/ui/public/directives/__tests__/validate_index_name.js @@ -8,6 +8,7 @@ describe('Validate index name directive', function () { let $compile; let $rootScope; const noWildcardHtml = ''; + const requiredHtml = ''; const allowWildcardHtml = ''; beforeEach(ngMock.module('kibana')); @@ -24,10 +25,13 @@ describe('Validate index name directive', function () { return element; } - const badPatterns = [ - null, + const emptyPatterns = [ undefined, - '', + null, + '' + ]; + + const badPatterns = [ '.', '..', 'foo\\bar', @@ -71,6 +75,14 @@ describe('Validate index name directive', function () { }); }); + emptyPatterns.forEach(function (pattern) { + it('should not accept index pattern: ' + pattern, function () { + const element = checkPattern(pattern, requiredHtml); + expect(element.hasClass('ng-invalid')).to.be(true); + expect(element.hasClass('ng-valid')).to.not.be(true); + }); + }); + it('should disallow wildcards by default', function () { wildcardPatterns.forEach(function (pattern) { const element = checkPattern(pattern, noWildcardHtml); diff --git a/src/ui/public/directives/paginated_selectable_list.js b/src/ui/public/directives/paginated_selectable_list.js index 5382284024870..2676fe9c23ef5 100644 --- a/src/ui/public/directives/paginated_selectable_list.js +++ b/src/ui/public/directives/paginated_selectable_list.js @@ -15,7 +15,7 @@ module.directive('paginatedSelectableList', function () { scope: { perPage: '=?', list: '=', - listProperty: '=', + listProperty: '@', userMakeUrl: '=?', userOnSelect: '=?' }, @@ -32,7 +32,7 @@ module.directive('paginatedSelectableList', function () { } $scope.perPage = $scope.perPage || 10; - $scope.hits = $scope.list = _.sortBy($scope.list, accessor); + $scope.hits = $scope.list = _.sortBy($scope.list, $scope.accessor); $scope.hitCount = $scope.hits.length; /** @@ -48,7 +48,7 @@ module.directive('paginatedSelectableList', function () { * @return {Array} Array sorted either ascending or descending */ $scope.sortHits = function (hits) { - const sortedList = _.sortBy(hits, accessor); + const sortedList = _.sortBy(hits, $scope.accessor); $scope.isAscending = !$scope.isAscending; $scope.hits = $scope.isAscending ? sortedList : sortedList.reverse(); @@ -62,10 +62,10 @@ module.directive('paginatedSelectableList', function () { return $scope.userOnSelect(hit, $event); }; - function accessor(val) { + $scope.accessor = function (val) { const prop = $scope.listProperty; - return prop ? val[prop] : val; - } + return prop ? _.get(val, prop) : val; + }; } }; }); diff --git a/src/ui/public/directives/validate_index_name.js b/src/ui/public/directives/validate_index_name.js index 9d727885f5272..6c20e34560a09 100644 --- a/src/ui/public/directives/validate_index_name.js +++ b/src/ui/public/directives/validate_index_name.js @@ -16,11 +16,13 @@ uiModules } const isValid = function (input) { - if (input == null || input === '' || input === '.' || input === '..') return false; + if (input == null || input === '') return !attr.required === true; + if (input === '.' || input === '..') return false; const match = _.find(illegalCharacters, function (character) { return input.indexOf(character) >= 0; }); + return !match; }; diff --git a/src/ui/public/errors.js b/src/ui/public/errors.js index 89958fd5830fa..c0686550ebe46 100644 --- a/src/ui/public/errors.js +++ b/src/ui/public/errors.js @@ -161,15 +161,31 @@ export class DuplicateField extends KbnError { } } +/** + * when a mapping already exists for a field the user is attempting to add + * @param {String} name - the field name + */ +export class IndexPatternAlreadyExists extends KbnError { + constructor(name) { + super( + `An index pattern of "${name}" already exists`, + IndexPatternAlreadyExists); + } +} + /** * A saved object was not found */ export class SavedObjectNotFound extends KbnError { - constructor(type, id) { + constructor(type, id, link) { const idMsg = id ? ` (id: ${id})` : ''; - super( - `Could not locate that ${type}${idMsg}`, - SavedObjectNotFound); + let message = `Could not locate that ${type}${idMsg}`; + + if (link) { + message += `, [click here to re-create it](${link})`; + } + + super(message, SavedObjectNotFound); this.savedObjectType = type; this.savedObjectId = id; diff --git a/src/ui/public/index_patterns/__tests__/_index_pattern.js b/src/ui/public/index_patterns/__tests__/_index_pattern.js index 35c07a5cad673..bdd0af06f11ab 100644 --- a/src/ui/public/index_patterns/__tests__/_index_pattern.js +++ b/src/ui/public/index_patterns/__tests__/_index_pattern.js @@ -6,8 +6,7 @@ import Promise from 'bluebird'; import { DuplicateField } from 'ui/errors'; import { IndexedArray } from 'ui/indexed_array'; import FixturesLogstashFieldsProvider from 'fixtures/logstash_fields'; -import FixturesStubbedDocSourceResponseProvider from 'fixtures/stubbed_doc_source_response'; -import { AdminDocSourceProvider } from 'ui/courier/data_source/admin_doc_source'; +import { FixturesStubbedSavedObjectIndexPatternProvider } from 'fixtures/stubbed_saved_object_index_pattern'; import UtilsMappingSetupProvider from 'ui/utils/mapping_setup'; import { IndexPatternsIntervalsProvider } from 'ui/index_patterns/_intervals'; import { IndexPatternProvider } from 'ui/index_patterns/_index_pattern'; @@ -19,6 +18,7 @@ import { StubIndexPatternsApiClientModule } from './stub_index_patterns_api_clie import { IndexPatternsApiClientProvider } from '../index_patterns_api_client_provider'; import { IndexPatternsCalculateIndicesProvider } from '../_calculate_indices'; import { IsUserAwareOfUnsupportedTimePatternProvider } from '../unsupported_time_patterns'; +import { SavedObjectsClientProvider } from 'ui/saved_objects'; const MARKDOWN_LINK_RE = /\[(.+?)\]\((.+?)\)/; @@ -29,8 +29,8 @@ describe('index pattern', function () { let fieldsFetcher; let mappingSetup; let mockLogstashFields; - let DocSource; - let docSourceResponse; + let savedObjectsClient; + let savedObjectsResponse; const indexPatternId = 'test-pattern'; let indexPattern; let calculateIndices; @@ -61,11 +61,12 @@ describe('index pattern', function () { beforeEach(ngMock.inject(function (Private) { mockLogstashFields = Private(FixturesLogstashFieldsProvider); defaultTimeField = mockLogstashFields.find(f => f.type === 'date'); - docSourceResponse = Private(FixturesStubbedDocSourceResponseProvider); + savedObjectsResponse = Private(FixturesStubbedSavedObjectIndexPatternProvider); - DocSource = Private(AdminDocSourceProvider); - sinon.stub(DocSource.prototype, 'doIndex'); - sinon.stub(DocSource.prototype, 'fetch'); + savedObjectsClient = Private(SavedObjectsClientProvider); + sinon.stub(savedObjectsClient, 'create'); + sinon.stub(savedObjectsClient, 'get'); + sinon.stub(savedObjectsClient, 'update'); // stub mappingSetup mappingSetup = Private(UtilsMappingSetupProvider); @@ -95,14 +96,17 @@ describe('index pattern', function () { // helper function to create index patterns function create(id, payload) { const indexPattern = new IndexPattern(id); - DocSource.prototype.doIndex.returns(Promise.resolve(id)); - payload = _.defaults(payload || {}, docSourceResponse(id)); + payload = _.defaults(payload || {}, savedObjectsResponse(id)); + + savedObjectsClient.create.returns(Promise.resolve(payload)); setDocsourcePayload(payload); + return indexPattern.init(); } function setDocsourcePayload(payload) { - DocSource.prototype.fetch.returns(Promise.resolve(payload)); + savedObjectsClient.get.returns(Promise.resolve(payload)); + savedObjectsClient.update.returns(Promise.resolve(payload)); } describe('api', function () { @@ -128,7 +132,7 @@ describe('index pattern', function () { describe('init', function () { it('should append the found fields', function () { - expect(DocSource.prototype.fetch.callCount).to.be(1); + expect(savedObjectsClient.get.callCount).to.be(1); expect(indexPattern.fields).to.have.length(mockLogstashFields.length); expect(indexPattern.fields).to.be.an(IndexedArray); }); @@ -295,8 +299,7 @@ describe('index pattern', function () { it('invokes interval toDetailedIndexList with given start/stop times', async function () { await indexPattern.toDetailedIndexList(1, 2); - const id = indexPattern.id; - sinon.assert.calledWith(intervals.toIndexList, id, interval, 1, 2); + sinon.assert.calledWith(intervals.toIndexList, indexPattern.title, interval, 1, 2); }); it('is fulfilled by the result of interval toDetailedIndexList', async function () { @@ -315,7 +318,8 @@ describe('index pattern', function () { describe('when index pattern is a time-base wildcard', function () { beforeEach(function () { - indexPattern.id = 'logstash-*'; + indexPattern.id = 'randomID'; + indexPattern.title = 'logstash-*'; indexPattern.timeFieldName = defaultTimeField.name; indexPattern.intervalName = null; indexPattern.notExpandable = false; @@ -323,9 +327,13 @@ describe('index pattern', function () { it('invokes calculateIndices with given start/stop times and sortOrder', async function () { await indexPattern.toDetailedIndexList(1, 2, 'sortOrder'); - const id = indexPattern.id; - const field = indexPattern.timeFieldName; - expect(calculateIndices.calledWith(id, field, 1, 2, 'sortOrder')).to.be(true); + + const { title, timeFieldName } = indexPattern; + + sinon.assert.calledOnce(calculateIndices); + expect(calculateIndices.getCall(0).args).to.eql([ + title, timeFieldName, 1, 2, 'sortOrder' + ]); }); it('is fulfilled by the result of calculateIndices', async function () { @@ -337,29 +345,31 @@ describe('index pattern', function () { describe('when index pattern is a time-base wildcard that is configured not to expand', function () { beforeEach(function () { - indexPattern.id = 'logstash-*'; + indexPattern.id = 'randomID'; + indexPattern.title = 'logstash-*'; indexPattern.timeFieldName = defaultTimeField.name; indexPattern.intervalName = null; indexPattern.notExpandable = true; }); - it('is fulfilled by id', async function () { + it('is fulfilled by title', async function () { const indexList = await indexPattern.toDetailedIndexList(); - expect(indexList.map(i => i.index)).to.eql([indexPattern.id]); + expect(indexList.map(i => i.index)).to.eql([indexPattern.title]); }); }); describe('when index pattern is neither an interval nor a time-based wildcard', function () { beforeEach(function () { - indexPattern.id = 'logstash-0'; + indexPattern.id = 'randomID'; + indexPattern.title = 'logstash-0'; indexPattern.timeFieldName = null; indexPattern.intervalName = null; indexPattern.notExpandable = true; }); - it('is fulfilled by id', async function () { + it('is fulfilled by title', async function () { const indexList = await indexPattern.toDetailedIndexList(); - expect(indexList.map(i => i.index)).to.eql([indexPattern.id]); + expect(indexList.map(i => i.index)).to.eql([indexPattern.title]); }); }); }); @@ -369,7 +379,8 @@ describe('index pattern', function () { let interval; beforeEach(function () { - indexPattern.id = '[logstash-]YYYY'; + indexPattern.id = 'randomID'; + indexPattern.title = '[logstash-]YYYY'; indexPattern.timeFieldName = defaultTimeField.name; interval = intervals.byName.years; indexPattern.intervalName = interval.name; @@ -378,8 +389,8 @@ describe('index pattern', function () { it('invokes interval toIndexList with given start/stop times', async function () { await indexPattern.toIndexList(1, 2); - const id = indexPattern.id; - sinon.assert.calledWith(intervals.toIndexList, id, interval, 1, 2); + const { title } = indexPattern; + sinon.assert.calledWith(intervals.toIndexList, title, interval, 1, 2); }); it('is fulfilled by the result of interval toIndexList', async function () { @@ -401,7 +412,8 @@ describe('index pattern', function () { describe('when index pattern is a time-base wildcard', function () { beforeEach(function () { - indexPattern.id = 'logstash-*'; + indexPattern.id = 'randomID'; + indexPattern.title = 'logstash-*'; indexPattern.timeFieldName = defaultTimeField.name; indexPattern.intervalName = null; indexPattern.notExpandable = false; @@ -409,9 +421,8 @@ describe('index pattern', function () { it('invokes calculateIndices with given start/stop times and sortOrder', async function () { await indexPattern.toIndexList(1, 2, 'sortOrder'); - const id = indexPattern.id; - const field = indexPattern.timeFieldName; - expect(calculateIndices.calledWith(id, field, 1, 2, 'sortOrder')).to.be(true); + const { title, timeFieldName } = indexPattern; + expect(calculateIndices.calledWith(title, timeFieldName, 1, 2, 'sortOrder')).to.be(true); }); it('is fulfilled by the result of calculateIndices', async function () { @@ -423,7 +434,8 @@ describe('index pattern', function () { describe('when index pattern is a time-base wildcard that is configured not to expand', function () { beforeEach(function () { - indexPattern.id = 'logstash-*'; + indexPattern.id = 'randomID'; + indexPattern.title = 'logstash-*'; indexPattern.timeFieldName = defaultTimeField.name; indexPattern.intervalName = null; indexPattern.notExpandable = true; @@ -431,13 +443,14 @@ describe('index pattern', function () { it('is fulfilled using the id', async function () { const indexList = await indexPattern.toIndexList(); - expect(indexList).to.eql([indexPattern.id]); + expect(indexList).to.eql([indexPattern.title]); }); }); describe('when index pattern is neither an interval nor a time-based wildcard', function () { beforeEach(function () { - indexPattern.id = 'logstash-0'; + indexPattern.id = 'randomID'; + indexPattern.title = 'logstash-0'; indexPattern.timeFieldName = null; indexPattern.intervalName = null; indexPattern.notExpandable = true; @@ -445,7 +458,7 @@ describe('index pattern', function () { it('is fulfilled by id', async function () { const indexList = await indexPattern.toIndexList(); - expect(indexList).to.eql([indexPattern.id]); + expect(indexList).to.eql([indexPattern.title]); }); }); }); @@ -490,11 +503,11 @@ describe('index pattern', function () { describe('#isWildcard()', function () { it('returns true if id has an *', function () { - indexPattern.id = 'foo*'; + indexPattern.title = 'foo*'; expect(indexPattern.isWildcard()).to.be(true); }); it('returns false if id has no *', function () { - indexPattern.id = 'foo'; + indexPattern.title = 'foo'; expect(indexPattern.isWildcard()).to.be(false); }); }); @@ -512,8 +525,9 @@ describe('index pattern', function () { describe('unsupported time pattern warning', () => { async function createUnsupportedTimePattern() { - return await create('pattern-id', { - _source: { + return await create('randomID', { + attributes: { + title: 'pattern-id', timeFieldName: '@timestamp', intervalName: 'days', fields: '[]' @@ -524,10 +538,11 @@ describe('index pattern', function () { it('logs a warning when the index pattern source includes `intervalName`', async () => { const indexPattern = await createUnsupportedTimePattern(); expect(Notifier.prototype._notifs).to.have.length(1); - const notif = Notifier.prototype._notifs.shift(); + const notif = Notifier.prototype._notifs.shift(); expect(notif).to.have.property('type', 'warning'); expect(notif.content).to.match(MARKDOWN_LINK_RE); + const [,text,url] = notif.content.match(MARKDOWN_LINK_RE); expect(text).to.contain(indexPattern.id); expect(url).to.contain(indexPattern.id); diff --git a/src/ui/public/index_patterns/_get_ids.js b/src/ui/public/index_patterns/_get_ids.js index f5dd526a8ba26..1d149baf93020 100644 --- a/src/ui/public/index_patterns/_get_ids.js +++ b/src/ui/public/index_patterns/_get_ids.js @@ -1,6 +1,8 @@ import _ from 'lodash'; +import { SavedObjectsClientProvider } from 'ui/saved_objects'; -export function IndexPatternsGetIdsProvider(esAdmin, kbnIndex) { +export function IndexPatternsGetIdsProvider(Private) { + const savedObjectsClient = Private(SavedObjectsClientProvider); // many places may require the id list, so we will cache it separately // didn't incorporate with the indexPattern cache to prevent id collisions. @@ -14,17 +16,12 @@ export function IndexPatternsGetIdsProvider(esAdmin, kbnIndex) { }); } - cachedPromise = esAdmin.search({ - index: kbnIndex, + cachedPromise = savedObjectsClient.find({ type: 'index-pattern', - storedFields: [], - body: { - query: { match_all: {} }, - size: 10000 - } - }) - .then(function (resp) { - return _.pluck(resp.hits.hits, '_id'); + fields: [], + perPage: 10000 + }).then(resp => { + return resp.savedObjects.map(obj => obj.id); }); // ensure that the response stays pristine by cloning it here too diff --git a/src/ui/public/index_patterns/_index_pattern.js b/src/ui/public/index_patterns/_index_pattern.js index 21bb4eb9c6436..1b69da45cf972 100644 --- a/src/ui/public/index_patterns/_index_pattern.js +++ b/src/ui/public/index_patterns/_index_pattern.js @@ -1,8 +1,7 @@ import _ from 'lodash'; -import { SavedObjectNotFound, DuplicateField, IndexPatternMissingIndices } from 'ui/errors'; +import { SavedObjectNotFound, DuplicateField, IndexPatternAlreadyExists, IndexPatternMissingIndices } from 'ui/errors'; import angular from 'angular'; import { RegistryFieldFormatsProvider } from 'ui/registry/field_formats'; -import { AdminDocSourceProvider } from 'ui/courier/data_source/admin_doc_source'; import UtilsMappingSetupProvider from 'ui/utils/mapping_setup'; import { Notifier } from 'ui/notify'; @@ -16,6 +15,7 @@ import { IndexPatternsCalculateIndicesProvider } from './_calculate_indices'; import { IndexPatternsPatternCacheProvider } from './_pattern_cache'; import { FieldsFetcherProvider } from './fields_fetcher_provider'; import { IsUserAwareOfUnsupportedTimePatternProvider } from './unsupported_time_patterns'; +import { SavedObjectsClientProvider, findObjectByTitle } from 'ui/saved_objects'; export function getRoutes() { return { @@ -33,18 +33,17 @@ export function IndexPatternProvider(Private, $http, config, kbnIndex, Promise, const getIds = Private(IndexPatternsGetIdsProvider); const fieldsFetcher = Private(FieldsFetcherProvider); const intervals = Private(IndexPatternsIntervalsProvider); - const DocSource = Private(AdminDocSourceProvider); const mappingSetup = Private(UtilsMappingSetupProvider); const FieldList = Private(IndexPatternsFieldListProvider); const flattenHit = Private(IndexPatternsFlattenHitProvider); const calculateIndices = Private(IndexPatternsCalculateIndicesProvider); const patternCache = Private(IndexPatternsPatternCacheProvider); const isUserAwareOfUnsupportedTimePattern = Private(IsUserAwareOfUnsupportedTimePatternProvider); + const savedObjectsClient = Private(SavedObjectsClientProvider); const type = 'index-pattern'; const notify = new Notifier(); const configWatchers = new WeakMap(); - const docSources = new WeakMap(); const mapping = mappingSetup.expandShorthand({ title: 'text', @@ -78,7 +77,13 @@ export function IndexPatternProvider(Private, $http, config, kbnIndex, Promise, function updateFromElasticSearch(indexPattern, response) { if (!response.found) { - throw new SavedObjectNotFound(type, indexPattern.id); + const markdownSaveId = indexPattern.id.replace('*', '%2A'); + + throw new SavedObjectNotFound( + type, + indexPattern.id, + kbnUrl.eval('#/management/kibana/index?id={{id}}&name=', { id: markdownSaveId }) + ); } _.forOwn(mapping, (fieldMapping, name) => { @@ -103,15 +108,7 @@ export function IndexPatternProvider(Private, $http, config, kbnIndex, Promise, } } - const promise = indexFields(indexPattern); - - // any time index pattern in ES is updated, update index pattern object - docSources - .get(indexPattern) - .onUpdate() - .then(response => updateFromElasticSearch(indexPattern, response), notify.fatal); - - return promise; + return indexFields(indexPattern); } function isFieldRefreshRequired(indexPattern) { @@ -190,8 +187,6 @@ export function IndexPatternProvider(Private, $http, config, kbnIndex, Promise, class IndexPattern { constructor(id) { setId(this, id); - docSources.set(this, new DocSource()); - this.metaFields = config.get('metaFields'); this.getComputedFields = getComputedFields.bind(this); @@ -205,12 +200,6 @@ export function IndexPatternProvider(Private, $http, config, kbnIndex, Promise, } init() { - docSources - .get(this) - .index(kbnIndex) - .type(type) - .id(this.id); - watch(this); return mappingSetup @@ -225,8 +214,19 @@ export function IndexPatternProvider(Private, $http, config, kbnIndex, Promise, if (!this.id) { return; // no id === no elasticsearch document } - return docSources.get(this).fetch() - .then(response => updateFromElasticSearch(this, response)); + + return savedObjectsClient.get(type, this.id) + .then(resp => { + // temporary compatability for savedObjectsClient + + return { + _id: resp.id, + _type: resp.type, + _source: _.cloneDeep(resp.attributes), + found: resp._version ? true : false + }; + }) + .then(response => updateFromElasticSearch(this, response)); }) .then(() => this); } @@ -306,19 +306,19 @@ export function IndexPatternProvider(Private, $http, config, kbnIndex, Promise, return Promise.resolve().then(() => { if (this.isTimeBasedInterval()) { return intervals.toIndexList( - this.id, this.getInterval(), start, stop, sortDirection + this.title, this.getInterval(), start, stop, sortDirection ); } if (this.isTimeBasedWildcard() && this.isIndexExpansionEnabled()) { return calculateIndices( - this.id, this.timeFieldName, start, stop, sortDirection + this.title, this.timeFieldName, start, stop, sortDirection ); } return [ { - index: this.id, + index: this.title, min: -Infinity, max: Infinity } @@ -352,7 +352,7 @@ export function IndexPatternProvider(Private, $http, config, kbnIndex, Promise, } isWildcard() { - return _.includes(this.id, '*'); + return _.includes(this.title, '*'); } prepBody() { @@ -367,45 +367,68 @@ export function IndexPatternProvider(Private, $http, config, kbnIndex, Promise, } }); - // ensure that the docSource has the current this.id - docSources.get(this).id(this.id); - // clear the indexPattern list cache getIds.clearCache(); return body; } + /** + * Returns a promise that resolves to true if either the title is unique, or if the user confirmed they + * wished to save the duplicate title. Promise is rejected if the user rejects the confirmation. + */ + warnIfDuplicateTitle() { + return findObjectByTitle(savedObjectsClient, type, this.title) + .then(duplicate => { + if (!duplicate) return false; + if (duplicate.id === this.id) return false; + + const confirmMessage = + `An index pattern with the title '${this.title}' already exists.`; + + return confirmModalPromise(confirmMessage, { confirmButtonText: 'Edit existing pattern' }) + .then(() => { + kbnUrl.change('/management/kibana/indices/{{id}}', { id: duplicate.id }); + return true; + }) + .catch(() => { + throw new IndexPatternAlreadyExists(this.title); + }); + }); + } + create() { - const body = this.prepBody(); - return docSources.get(this) - .doCreate(body) - .then(id => setId(this, id)) - .catch(err => { - if (_.get(err, 'origError.status') !== 409) { - return Promise.resolve(false); - } - const confirmMessage = 'Are you sure you want to overwrite this?'; - - return confirmModalPromise(confirmMessage, { confirmButtonText: 'Overwrite' }) - .then(() => Promise - .try(() => { - const cached = patternCache.get(this.id); - if (cached) { - return cached.then(pattern => pattern.destroy()); + return this.warnIfDuplicateTitle().then((duplicate) => { + if (duplicate) return; + + const body = this.prepBody(); + + return savedObjectsClient.create(type, body, { id: this.id }) + .then(response => setId(this, response.id)) + .catch(err => { + if (err.statusCode !== 409) { + return Promise.resolve(false); } - }) - .then(() => docSources.get(this).doIndex(body)) - .then(id => setId(this, id)), - _.constant(false) // if the user doesn't overwrite, resolve with false - ); + const confirmMessage = 'Are you sure you want to overwrite this?'; + + return confirmModalPromise(confirmMessage, { confirmButtonText: 'Overwrite' }) + .then(() => Promise + .try(() => { + const cached = patternCache.get(this.id); + if (cached) { + return cached.then(pattern => pattern.destroy()); + } + }) + .then(() => savedObjectsClient.create(type, body, { id: this.id, overwrite: true })) + .then(response => setId(this, response.id)), + _.constant(false) // if the user doesn't overwrite, resolve with false + ); + }); }); } - save() { - const body = this.prepBody(); - return docSources.get(this) - .doIndex(body) - .then(id => setId(this, id)); + async save() { + return savedObjectsClient.update(type, this.id, this.prepBody()) + .then(({ id }) => setId(this, id)); } refreshFields() { @@ -438,8 +461,7 @@ export function IndexPatternProvider(Private, $http, config, kbnIndex, Promise, destroy() { unwatch(this); patternCache.clear(this.id); - docSources.get(this).destroy(); - docSources.delete(this); + return savedObjectsClient.delete(type, this.id); } } diff --git a/src/ui/public/index_patterns/fields_fetcher.js b/src/ui/public/index_patterns/fields_fetcher.js index a23b414be49c5..c69d9607dbf5e 100644 --- a/src/ui/public/index_patterns/fields_fetcher.js +++ b/src/ui/public/index_patterns/fields_fetcher.js @@ -3,10 +3,10 @@ export function createFieldsFetcher(apiClient, config) { fetch(indexPattern) { if (indexPattern.isTimeBasedInterval()) { const interval = indexPattern.getInterval().name; - return this.fetchForTimePattern(indexPattern.id, interval); + return this.fetchForTimePattern(indexPattern.title, interval); } - return this.fetchForWildcard(indexPattern.id); + return this.fetchForWildcard(indexPattern.title); } fetchForTimePattern(indexPatternId) { diff --git a/src/ui/public/index_patterns/index_patterns.js b/src/ui/public/index_patterns/index_patterns.js index f9dd9c61684da..f63afa668b62c 100644 --- a/src/ui/public/index_patterns/index_patterns.js +++ b/src/ui/public/index_patterns/index_patterns.js @@ -10,7 +10,8 @@ import { uiModules } from 'ui/modules'; const module = uiModules.get('kibana/index_patterns'); export { IndexPatternsApiClientProvider } from './index_patterns_api_client_provider'; -export function IndexPatternsProvider(esAdmin, Notifier, Private, Promise, kbnIndex) { + +export function IndexPatternsProvider(Notifier, Private) { const self = this; const IndexPattern = Private(IndexPatternProvider); @@ -29,13 +30,7 @@ export function IndexPatternsProvider(esAdmin, Notifier, Private, Promise, kbnIn self.delete = function (pattern) { self.getIds.clearCache(); - pattern.destroy(); - - return esAdmin.delete({ - index: kbnIndex, - type: 'index-pattern', - id: pattern.id - }); + return pattern.destroy(); }; self.errors = { diff --git a/src/ui/public/partials/paginated_selectable_list.html b/src/ui/public/partials/paginated_selectable_list.html index d0287d53065bf..85aa72e799279 100644 --- a/src/ui/public/partials/paginated_selectable_list.html +++ b/src/ui/public/partials/paginated_selectable_list.html @@ -42,11 +42,11 @@
  • - {{ hit }} + {{ accessor(hit) }}
    - {{ hit }} + {{ accessor(hit) }}
  • diff --git a/src/ui/public/saved_objects/__tests__/find_object_by_title.js b/src/ui/public/saved_objects/__tests__/find_object_by_title.js new file mode 100644 index 0000000000000..36024c20e3249 --- /dev/null +++ b/src/ui/public/saved_objects/__tests__/find_object_by_title.js @@ -0,0 +1,30 @@ +import sinon from 'sinon'; +import expect from 'expect.js'; +import { findObjectByTitle } from '../find_object_by_title'; +import { SavedObject } from '../saved_object'; + +describe('findObjectByTitle', () => { + const sandbox = sinon.sandbox.create(); + const savedObjectsClient = {}; + + beforeEach(() => { + savedObjectsClient.find = sandbox.stub(); + }); + + afterEach(() => sandbox.restore()); + + it('returns undefined if title is not provided', async () => { + const match = await findObjectByTitle(savedObjectsClient, 'index-pattern'); + expect(match).to.be(undefined); + }); + + it('matches any case', async () => { + const indexPattern = new SavedObject(savedObjectsClient, { attributes: { title: 'foo' } }); + savedObjectsClient.find.returns(Promise.resolve({ + savedObjects: [indexPattern] + })); + + const match = await findObjectByTitle(savedObjectsClient, 'index-pattern', 'FOO'); + expect(match).to.eql(indexPattern); + }); +}); diff --git a/src/ui/public/saved_objects/find_object_by_title.js b/src/ui/public/saved_objects/find_object_by_title.js new file mode 100644 index 0000000000000..0059c40d7f435 --- /dev/null +++ b/src/ui/public/saved_objects/find_object_by_title.js @@ -0,0 +1,29 @@ +import { find } from 'lodash'; + +/** + * Returns an object matching a given title + * + * @param savedObjectsClient {SavedObjectsClient} + * @param type {string} + * @param title {string} + * @returns {Promise} + */ +export function findObjectByTitle(savedObjectsClient, type, title) { + if (!title) return Promise.resolve(); + + // Elastic search will return the most relevant results first, which means exact matches should come + // first, and so we shouldn't need to request everything. Using 10 just to be on the safe side. + return savedObjectsClient.find({ + type, + perPage: 10, + search: `"${title}"`, + searchFields: 'title', + fields: ['title'] + }).then(response => { + const match = find(response.savedObjects, (obj) => { + return obj.get('title').toLowerCase() === title.toLowerCase(); + }); + + return match; + }); +} diff --git a/src/ui/public/saved_objects/index.js b/src/ui/public/saved_objects/index.js index 7b1d90d8452b0..44ed8e0d58cf6 100644 --- a/src/ui/public/saved_objects/index.js +++ b/src/ui/public/saved_objects/index.js @@ -2,3 +2,4 @@ export { SavedObjectsClient } from './saved_objects_client'; export { SavedObjectRegistryProvider } from './saved_object_registry'; export { SavedObjectsClientProvider } from './saved_objects_client_provider'; export { SavedObject } from './saved_object'; +export { findObjectByTitle } from './find_object_by_title'; diff --git a/src/ui/public/vis/editors/default/sidebar.html b/src/ui/public/vis/editors/default/sidebar.html index b951bf5d7be48..48a648d5e60a0 100644 --- a/src/ui/public/vis/editors/default/sidebar.html +++ b/src/ui/public/vis/editors/default/sidebar.html @@ -8,11 +8,11 @@
    - {{ vis.indexPattern.id }} + {{ vis.indexPattern.title }}