Use SavedObjectsClient for Courier Index Pattern (#12719) (#12779)

* Index pattern is created, by default, with a random ID by Elasticsearch
* Updated all references requiring the pattern itself to use indexPattern.title
* Advanced options toggle added to index pattern creation page to provide a specified ID
* If an index pattern does not exist, the user is given a link to create a pattern with the referenced ID.
This commit is contained in:
Tyler Smalley 2017-07-11 21:43:23 -07:00 committed by GitHub
parent f0b6e7d85f
commit 3c64283dd1
46 changed files with 445 additions and 364 deletions

View file

@ -73,7 +73,7 @@
<div ng-if="error" class="load-error">
<span aria-hidden="true" class="kuiIcon fa-exclamation-triangle"></span>
<span ng-bind="error"></span>
<span ng-bind-html="error | markdown"></span>
</div>
<visualize

View file

@ -8,6 +8,7 @@ import 'ui/private';
import 'plugins/kibana/discover/components/field_chooser/field_chooser';
import FixturesHitsProvider from 'fixtures/hits';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import { SavedObject } from 'ui/saved_objects';
// Load the kibana app dependencies.
@ -70,7 +71,11 @@ describe('discover field chooser directives', function () {
beforeEach(ngMock.inject(function (Private) {
hits = Private(FixturesHitsProvider);
indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
indexPatternList = [ 'b', 'a', 'c' ];
indexPatternList = [
new SavedObject(undefined, { id: '0', attributes: { title: 'b' } }),
new SavedObject(undefined, { id: '1', attributes: { title: 'a' } }),
new SavedObject(undefined, { id: '2', attributes: { title: 'c' } })
];
const fieldCounts = _.transform(hits, function (counts, hit) {
_.keys(indexPattern.flattenHit(hit)).forEach(function (key) {

View file

@ -4,13 +4,13 @@
class="index-pattern-selection"
ng-model="selectedIndexPattern"
on-select="setIndexPattern($item)"
ng-init="selectedIndexPattern = indexPattern.id"
ng-init="selectedIndexPattern = indexPattern"
>
<ui-select-match>
{{$select.selected}}
{{$select.selected.title}}
</ui-select-match>
<ui-select-choices repeat="id in indexPatternList | filter:$select.search | orderBy">
<div ng-bind-html="id | highlight: $select.search"></div>
<ui-select-choices repeat="pattern in indexPatternList | filter:$select.search">
<div ng-bind-html="pattern.get('title') | highlight: $select.search"></div>
</ui-select-choices>
</ui-select>
</div>
@ -20,8 +20,9 @@
class="index-pattern-label"
id="index_pattern_id"
tabindex="0"
css-truncate
>{{ indexPattern.id }}</h2>
css-truncate>
{{ indexPattern.title }}</h2>
</div>
</div>

View file

@ -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();
};

View file

@ -28,6 +28,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',
@ -45,8 +46,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
@ -59,12 +66,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
@ -156,6 +163,7 @@ function discoverController($scope, config, courier, $route, $window, Notifier,
dirty: !savedSearch.id
};
const $state = $scope.state = new AppState(getStateDefaults());
$scope.uiState = $state.makeStateful('uiState');
function getStateDefaults() {
@ -179,8 +187,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,
@ -586,7 +592,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;

View file

@ -19,7 +19,7 @@ describe('createIndexPattern UI', () => {
current: {
params: {},
locals: {
indexPatternIds: []
indexPatterns: []
}
}
});

View file

@ -23,12 +23,20 @@
class="kuiVerticalRhythm"
ng-submit="controller.createIndexPattern()"
>
<!-- Index pattern input -->
<div class="kuiVerticalRhythm">
<label
class="kuiLabel kuiVerticalRhythmSmall"
translate="KIBANA-INDEX_NAME_OR_PATTERN"
></label>
<label class="kuiLabel kuiVerticalRhythmSmall">
<span translate="KIBANA-INDEX_PATTERN"></span>
<small>
<a
class="kuiLink"
ng-click="controller.toggleAdvancedIndexOptions();"
translate="KIBANA-ADVANCED_OPTIONS"
></a>
</small>
</label>
<div class="kuiVerticalRhythm kuiVerticalRhythmSmall">
<input
@ -95,6 +103,35 @@
</div>
</div>
<!-- Index pattern id input -->
<div class="kuiVerticalRhythm" ng-if="controller.showAdvancedOptions">
<label
class="kuiLabel kuiVerticalRhythmSmall"
translate="KIBANA-INDEX_PATTERN_ID">
</label>
<div class="kuiVerticalRhythm kuiVerticalRhythmSmall">
<input
class="kuiTextInput kuiTextInput--large"
data-test-subj="createIndexPatternIdInput"
ng-model="controller.formValues.id"
ng-model-options="{ updateOn: 'default blur', debounce: {'default': 500, 'blur': 0} }"
validate-index-name
allow-wildcard
name="id"
type="text"
>
</div>
<!-- ID help text -->
<div class="kuiVerticalRhythm">
<p
class="kuiSubText kuiVerticalRhythmSmall"
translate="KIBANA-INDEX_PATTERN_SPECIFY_ID"
></p>
</div>
</div>
<!-- Time field select -->
<div class="kuiVerticalRhythm">
<label class="kuiLabel kuiVerticalRhythmSmall">

View file

@ -2,7 +2,6 @@ import _ from 'lodash';
import { IndexPatternMissingIndices } from 'ui/errors';
import 'ui/directives/validate_index_name';
import 'ui/directives/auto_select_if_only_one';
import { RefreshKibanaIndex } from '../refresh_kibana_index';
import uiRoutes from 'ui/routes';
import { uiModules } from 'ui/modules';
import template from './create_index_pattern.html';
@ -16,14 +15,25 @@ uiRoutes
});
uiModules.get('apps/management')
.controller('managementIndicesCreate', function ($scope, kbnUrl, Private, Notifier, indexPatterns, es, config, Promise, $translate) {
.controller('managementIndicesCreate', function (
$scope,
$routeParams,
kbnUrl,
Private,
Notifier,
indexPatterns,
es,
config,
Promise,
$translate
) {
const notify = new Notifier();
const refreshKibanaIndex = Private(RefreshKibanaIndex);
const intervals = indexPatterns.intervals;
let loadingCount = 0;
// Configure the new index pattern we're going to create.
this.formValues = {
id: $routeParams.id ? decodeURIComponent($routeParams.id) : undefined,
name: config.get('indexPattern:placeholder'),
nameIsPattern: false,
expandWildcard: false,
@ -39,6 +49,7 @@ uiModules.get('apps/management')
this.existing = null;
this.nameIntervalOptions = intervals;
this.patternErrors = [];
this.showAdvancedOptions = $routeParams.id || false;
const getTimeFieldOptions = () => {
loadingCount += 1;
@ -260,16 +271,19 @@ uiModules.get('apps/management')
});
};
this.toggleAdvancedIndexOptions = () => {
this.showAdvancedOptions = !!!this.showAdvancedOptions;
};
this.createIndexPattern = () => {
const {
id,
name,
timeFieldOption,
nameIsPattern,
nameInterval,
} = this.formValues;
const id = name;
const timeFieldName = timeFieldOption
? timeFieldOption.fieldName
: undefined;
@ -286,6 +300,7 @@ uiModules.get('apps/management')
loadingCount += 1;
sendCreateIndexPatternRequest(indexPatterns, {
id,
name,
timeFieldName,
intervalName,
notExpandable,
@ -294,17 +309,15 @@ uiModules.get('apps/management')
return;
}
refreshKibanaIndex().then(() => {
if (!config.get('defaultIndex')) {
config.set('defaultIndex', id);
}
if (!config.get('defaultIndex')) {
config.set('defaultIndex', createdId);
}
indexPatterns.cache.clear(id);
kbnUrl.change(`/management/kibana/indices/${id}`);
indexPatterns.cache.clear(createdId);
kbnUrl.change(`/management/kibana/indices/${createdId}`);
// force loading while kbnUrl.change takes effect
loadingCount = Infinity;
});
// force loading while kbnUrl.change takes effect
loadingCount = Infinity;
}).catch(err => {
if (err instanceof IndexPatternMissingIndices) {
return notify.error($translate.instant('KIBANA-NO_INDICES_MATCHING_PATTERN'));

View file

@ -1,5 +1,6 @@
export function sendCreateIndexPatternRequest(indexPatterns, {
id,
name,
timeFieldName,
intervalName,
notExpandable,
@ -7,10 +8,9 @@ export function sendCreateIndexPatternRequest(indexPatterns, {
// get an empty indexPattern to start
return indexPatterns.get()
.then(indexPattern => {
// set both the id and title to the same value
indexPattern.id = indexPattern.title = id;
Object.assign(indexPattern, {
id,
title: name,
timeFieldName,
intervalName,
notExpandable,

View file

@ -20,7 +20,7 @@
</p>
<p class="kuiText kuiVerticalRhythm">
This page lists every field in the <strong>{{::indexPattern.id}}</strong>
This page lists every field in the <strong>{{::indexPattern.title}}</strong>
index and the field's associated core type as recorded by Elasticsearch.
While this list allows you to view the core type of each field, changing
field types must be done using Elasticsearch's

View file

@ -4,7 +4,6 @@ import './indexed_fields_table';
import './scripted_fields_table';
import './scripted_field_editor';
import './source_filters_table';
import { RefreshKibanaIndex } from '../refresh_kibana_index';
import UrlProvider from 'ui/url';
import { IndicesEditSectionsProvider } from './edit_sections';
import uiRoutes from 'ui/routes';
@ -44,12 +43,14 @@ uiModules.get('apps/management')
$scope, $location, $route, config, courier, Notifier, Private, AppState, docTitle, confirmModal) {
const notify = new Notifier();
const $state = $scope.state = new AppState();
const refreshKibanaIndex = Private(RefreshKibanaIndex);
$scope.kbnUrl = Private(UrlProvider);
$scope.indexPattern = $route.current.locals.indexPattern;
docTitle.change($scope.indexPattern.id);
const otherIds = _.without($route.current.locals.indexPatternIds, $scope.indexPattern.id);
const otherPatterns = _.filter($route.current.locals.indexPatterns, pattern => {
return pattern.id !== $scope.indexPattern.id;
});
$scope.$watch('indexPattern.fields', function () {
$scope.editSections = Private(IndicesEditSectionsProvider)($scope.indexPattern);
@ -104,13 +105,13 @@ uiModules.get('apps/management')
function doRemove() {
if ($scope.indexPattern.id === config.get('defaultIndex')) {
config.remove('defaultIndex');
if (otherIds.length) {
config.set('defaultIndex', otherIds[0]);
if (otherPatterns.length) {
config.set('defaultIndex', otherPatterns[0].id);
}
}
courier.indexPatterns.delete($scope.indexPattern)
.then(refreshKibanaIndex)
.then(function () {
$location.url('/management/kibana/index');
})

View file

@ -11,7 +11,7 @@
ng-if="defaultIndex === indexPattern.id"
class="kuiIcon fa-star"
></span>
{{indexPattern.id}}
{{indexPattern.title}}
</h1>
</div>

View file

@ -25,13 +25,13 @@
</li>
<li
ng-repeat="pattern in indexPatternList | orderBy:['-default','id'] track by pattern.id "
ng-repeat="pattern in indexPatternList | orderBy:['-default','title'] track by pattern.id"
class="sidebar-item"
>
<a href="{{::pattern.url}}">
<div class="{{::pattern.class}}">
<i aria-hidden="true" ng-if="pattern.default" class="fa fa-star"></i>
<span ng-bind="::pattern.id"></span>
<span ng-bind="::pattern.title"></span>
</div>
</a>
</li>

View file

@ -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

View file

@ -1,7 +0,0 @@
export function RefreshKibanaIndex(esAdmin, kbnIndex) {
return function () {
return esAdmin.indices.refresh({
index: kbnIndex
});
};
}

View file

@ -8,11 +8,11 @@
<div
css-truncate
aria-label="{{:: 'Index pattern: ' + indexPattern.id}}"
aria-label="{{:: 'Index pattern: ' + indexPattern.title}}"
ng-if="vis.type.requiresSearch"
class="index-pattern"
>
{{ indexPattern.id }}
{{ indexPattern.title }}
</div>
<nav class="navbar navbar-default subnav">

View file

@ -192,7 +192,6 @@
</div>
</td>
</tr>
</tbody>
</table>

View file

@ -22,6 +22,7 @@
<paginated-selectable-list
per-page="20"
list="indexPattern.list"
list-property="attributes.title"
user-make-url="makeUrl"
class="wizard-row visualizeWizardPaginatedSelectableList kuiVerticalRhythm"
></paginated-selectable-list>

View file

@ -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}`;
};
});

View file

@ -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",

View file

@ -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;

View file

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

View file

@ -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);

View file

@ -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');

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}
};
}

View file

@ -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;
}

View file

@ -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<string|undefined>} 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;
});
}

View file

@ -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)));

View file

@ -8,6 +8,7 @@ describe('Validate index name directive', function () {
let $compile;
let $rootScope;
const noWildcardHtml = '<input type="text" ng-model="indexName" validate-index-name />';
const requiredHtml = '<input type="text" ng-model="indexName" validate-index-name required />';
const allowWildcardHtml = '<input type="text" ng-model="indexName" allow-wildcard validate-index-name />';
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);

View file

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

View file

@ -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;
};

View file

@ -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;

View file

@ -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';
@ -17,6 +16,7 @@ import { FieldsFetcherProvider } from '../fields_fetcher_provider';
import { StubIndexPatternsApiClientModule } from './stub_index_patterns_api_client';
import { IndexPatternsApiClientProvider } from '../index_patterns_api_client_provider';
import { IndexPatternsCalculateIndicesProvider } from '../_calculate_indices';
import { SavedObjectsClientProvider } from 'ui/saved_objects';
describe('index pattern', function () {
NoDigestPromises.activateForSuite();
@ -25,8 +25,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;
@ -51,11 +51,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);
@ -85,14 +86,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 () {
@ -118,7 +122,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);
});
@ -285,8 +289,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 () {
@ -305,7 +308,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;
@ -313,9 +317,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 () {
@ -327,29 +335,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]);
});
});
});
@ -359,7 +369,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;
@ -368,8 +379,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 () {
@ -391,7 +402,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;
@ -399,9 +411,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 () {
@ -413,7 +424,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;
@ -421,13 +433,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;
@ -435,7 +448,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]);
});
});
});
@ -480,11 +493,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);
});
});

View file

@ -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

View file

@ -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';
@ -15,13 +14,13 @@ import { IndexPatternsFlattenHitProvider } from './_flatten_hit';
import { IndexPatternsCalculateIndicesProvider } from './_calculate_indices';
import { IndexPatternsPatternCacheProvider } from './_pattern_cache';
import { FieldsFetcherProvider } from './fields_fetcher_provider';
import { SavedObjectsClientProvider, findObjectByTitle } from 'ui/saved_objects';
export function IndexPatternProvider(Private, $http, config, kbnIndex, Promise, confirmModalPromise) {
export function IndexPatternProvider(Private, $http, config, kbnIndex, Promise, confirmModalPromise, kbnUrl) {
const fieldformats = Private(RegistryFieldFormatsProvider);
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);
@ -30,7 +29,6 @@ export function IndexPatternProvider(Private, $http, config, kbnIndex, Promise,
const type = 'index-pattern';
const notify = new Notifier();
const configWatchers = new WeakMap();
const docSources = new WeakMap();
const getRoutes = () => ({
edit: '/management/kibana/indices/{{id}}',
addField: '/management/kibana/indices/{{id}}/create-field',
@ -38,6 +36,7 @@ export function IndexPatternProvider(Private, $http, config, kbnIndex, Promise,
scriptedFields: '/management/kibana/indices/{{id}}?_a=(tab:scriptedFields)',
sourceFilters: '/management/kibana/indices/{{id}}?_a=(tab:sourceFilters)'
});
const savedObjectsClient = Private(SavedObjectsClientProvider);
const mapping = mappingSetup.expandShorthand({
title: 'string',
@ -71,7 +70,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) => {
@ -84,15 +89,7 @@ export function IndexPatternProvider(Private, $http, config, kbnIndex, Promise,
// give index pattern all of the values in _source
_.assign(indexPattern, response._source);
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) {
@ -171,8 +168,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);
@ -186,12 +181,6 @@ export function IndexPatternProvider(Private, $http, config, kbnIndex, Promise,
}
init() {
docSources
.get(this)
.index(kbnIndex)
.type(type)
.id(this.id);
watch(this);
return mappingSetup
@ -206,8 +195,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);
}
@ -287,19 +287,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
}
@ -329,7 +329,7 @@ export function IndexPatternProvider(Private, $http, config, kbnIndex, Promise,
}
isWildcard() {
return _.includes(this.id, '*');
return _.includes(this.title, '*');
}
prepBody() {
@ -344,45 +344,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;
}
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?';
/**
* 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;
return confirmModalPromise(confirmMessage, { confirmButtonText: 'Overwrite' })
.then(() => Promise
.try(() => {
const cached = patternCache.get(this.id);
if (cached) {
return cached.then(pattern => pattern.destroy());
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() {
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() {
@ -415,8 +438,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);
}
}

View file

@ -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);
}
testTimePattern(indexPatternId) {

View file

@ -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 = {

View file

@ -42,11 +42,11 @@
<li class="list-group-item list-group-menu-item" ng-repeat="hit in page">
<a ng-show="userMakeUrl" kbn-href="{{ makeUrl(hit) }}">
<span>{{ hit }}</span>
<span>{{ accessor(hit) }}</span>
</a>
<div ng-show="userOnSelect" ng-click="onSelect(hit, $event)">
<span>{{ hit }}</span>
<span>{{ accessor(hit) }}</span>
</div>
</li>

View file

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

View file

@ -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<SavedObject|undefined>}
*/
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;
});
}

View file

@ -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';

View file

@ -1,6 +1,7 @@
import d3 from 'd3';
import _ from 'lodash';
import $ from 'jquery';
import marked from 'marked';
import { NoResults } from 'ui/errors';
import { Binder } from 'ui/binder';
import { VislibLibLayoutLayoutProvider } from './layout/layout';
@ -203,7 +204,7 @@ export function VisHandlerProvider(Private) {
div.append('div').attr('class', 'item bottom');
} else {
div.append('h4').text(message);
div.append('h4').text(marked.inlineLexer(message, []));
}
$(this.el).trigger('renderComplete');

View file

@ -21,8 +21,11 @@ export default function ({ getService, getPageObjects }) {
});
describe('index pattern creation', function indexPatternCreation() {
let indexPatternId;
before(function () {
return PageObjects.settings.createIndexPattern();
return PageObjects.settings.createIndexPattern()
.then(id => indexPatternId = id);
});
it('should have index pattern in page header', function () {
@ -37,7 +40,7 @@ export default function ({ getService, getPageObjects }) {
return retry.try(function tryingForTime() {
return remote.getCurrentUrl()
.then(function (currentUrl) {
expect(currentUrl).to.contain('logstash-*');
expect(currentUrl).to.contain(indexPatternId);
});
});
});

View file

@ -295,6 +295,17 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
log.debug('Index pattern created: ' + currentUrl);
}
});
return await this.getIndexPatternIdFromUrl();
}
async getIndexPatternIdFromUrl() {
const currentUrl = await remote.getCurrentUrl();
const indexPatternId = currentUrl.match(/.*\/(.*)/)[1];
log.debug('index pattern ID: ', indexPatternId);
return indexPatternId;
}
async setIndexPatternField(pattern) {