[7.x] removing angular from indexPatterns (#34418) (#38967)

* removing angular from indexPatterns (#34418)

# Conflicts:
#	src/legacy/ui/public/index_patterns/__tests__/_index_pattern.js
#	src/legacy/ui/public/index_patterns/_index_pattern.js
#	src/legacy/ui/public/index_patterns/fields_fetcher.js
#	src/legacy/ui/public/saved_objects/__tests__/saved_object.js
#	x-pack/plugins/translations/translations/ja-JP.json
#	x-pack/plugins/translations/translations/zh-CN.json

* i18n fix

* removing basePath from apiClient
This commit is contained in:
Peter Pisljar 2019-06-18 09:53:41 +02:00 committed by GitHub
parent 623783b731
commit 0063a9279a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 1177 additions and 1381 deletions

View file

@ -17,11 +17,12 @@
* under the License.
*/
import stubbedLogstashFields from 'fixtures/logstash_fields';
import { SimpleSavedObject } from 'ui/saved_objects';
import fixturesLogstashFieldsProvider from './logstash_fields';
import { SimpleSavedObject } from '../legacy/ui/public/saved_objects/simple_saved_object';
export function FixturesStubbedSavedObjectIndexPatternProvider() {
const mockLogstashFields = stubbedLogstashFields();
export function fixturesStubbedSavedObjectIndexPatternProvider() {
const mockLogstashFields = fixturesLogstashFieldsProvider();
return function (id) {
return new SimpleSavedObject(undefined, {
@ -29,7 +30,7 @@ export function FixturesStubbedSavedObjectIndexPatternProvider() {
type: 'index-pattern',
attributes: {
customFormats: '{}',
fields: JSON.stringify(mockLogstashFields)
fields: mockLogstashFields
},
version: 2
});

View file

@ -17,42 +17,25 @@
* under the License.
*/
// @ts-ignore
import { Field } from 'ui/index_patterns/_field.js';
// @ts-ignore
import { FieldList } from 'ui/index_patterns/_field_list';
// @ts-ignore
import { IndexPatternsFlattenHitProvider } from 'ui/index_patterns/_flatten_hit';
// @ts-ignore
import { getComputedFields } from 'ui/index_patterns/_get_computed_fields';
// @ts-ignore
import { getRoutes, IndexPatternProvider } from 'ui/index_patterns/_index_pattern';
import chrome from 'ui/chrome';
// @ts-ignore
import { mockFields, mockIndexPattern } from 'ui/index_patterns/fixtures';
// @ts-ignore
import { CONTAINS_SPACES } from 'ui/index_patterns/index';
// @ts-ignore
import { ILLEGAL_CHARACTERS } from 'ui/index_patterns/index';
// @ts-ignore
import { INDEX_PATTERN_ILLEGAL_CHARACTERS } from 'ui/index_patterns/index';
// @ts-ignore
import { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE } from 'ui/index_patterns/index';
// @ts-ignore
import { IndexPatternsApiClientProvider } from 'ui/index_patterns/index';
// @ts-ignore
import { IndexPatternSelect } from 'ui/index_patterns/index';
// @ts-ignore
import { IndexPatternsProvider } from 'ui/index_patterns/index';
import { IndexPatterns } from 'ui/index_patterns/index';
// @ts-ignore
import { validateIndexPattern } from 'ui/index_patterns/index';
// @ts-ignore
import setupRouteWithDefaultPattern from 'ui/index_patterns/route_setup/load_default';
// @ts-ignore
import { getFromSavedObject, isFilterable } from 'ui/index_patterns/static_utils';
// IndexPattern, StaticIndexPattern, StaticIndexPatternField, Field
import * as types from 'ui/index_patterns';
const config = chrome.getUiSettingsClient();
const savedObjectsClient = chrome.getSavedObjectsClient();
/**
* Index Patterns Service
*
@ -67,33 +50,7 @@ import * as types from 'ui/index_patterns';
export class IndexPatternsService {
public setup() {
return {
getRoutes,
IndexPatternProvider,
IndexPatternsApiClientProvider,
IndexPatternsFlattenHitProvider,
IndexPatternsProvider,
setupRouteWithDefaultPattern, // only used in kibana/management
validateIndexPattern,
constants: {
ILLEGAL_CHARACTERS,
CONTAINS_SPACES,
INDEX_PATTERN_ILLEGAL_CHARACTERS,
INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE,
},
fields: {
Field,
FieldList,
getComputedFields,
getFromSavedObject,
isFilterable,
},
fixtures: {
mockFields,
mockIndexPattern,
},
ui: {
IndexPatternSelect,
},
indexPatterns: new IndexPatterns(config, savedObjectsClient),
};
}
@ -102,6 +59,24 @@ export class IndexPatternsService {
}
}
// static exports
const constants = {
INDEX_PATTERN_ILLEGAL_CHARACTERS,
INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE,
};
const fixtures = {
mockFields,
mockIndexPattern,
};
const ui = {
IndexPatternSelect,
};
export { validateIndexPattern, constants, fixtures, ui };
/** @public */
export type IndexPatternsSetup = ReturnType<IndexPatternsService['setup']>;

View file

@ -49,7 +49,7 @@ uiRoutes
redirectTo: '/management'
});
require('ui/index_patterns/route_setup/load_default')({
require('./route_setup/load_default')({
whenMissingRedirectTo: '/management/kibana/index_pattern'
});

View file

@ -19,10 +19,9 @@
import _ from 'lodash';
import React from 'react';
import { banners } from '../../notify';
import { NoDefaultIndexPattern } from '../../errors';
import { IndexPatternsGetProvider } from '../_get';
import uiRoutes from '../../routes';
import { banners } from 'ui/notify';
import { NoDefaultIndexPattern } from 'ui/index_patterns/errors';
import uiRoutes from 'ui/routes';
import {
EuiCallOut,
} from '@elastic/eui';
@ -44,7 +43,7 @@ function displayBanner() {
color="warning"
iconType="iInCircle"
title={
i18n.translate('common.ui.indexPattern.bannerLabel',
i18n.translate('kbn.management.indexPattern.bannerLabel',
//eslint-disable-next-line max-len
{ defaultMessage: 'In order to visualize and explore data in Kibana, you\'ll need to create an index pattern to retrieve data from Elasticsearch.' })
}
@ -65,15 +64,14 @@ export default function (opts) {
const whenMissingRedirectTo = opts.whenMissingRedirectTo || null;
uiRoutes
.addSetupWork(function loadDefaultIndexPattern(Private, $route, config) {
const getIds = Private(IndexPatternsGetProvider)('id');
.addSetupWork(function loadDefaultIndexPattern(Promise, $route, config, indexPatterns) {
const route = _.get($route, 'current.$$route');
if (!route.requireDefaultIndex) {
return;
}
return getIds()
return indexPatterns.getIds()
.then(function (patterns) {
let defaultId = config.get('defaultIndex');
let defined = !!defaultId;

View file

@ -96,8 +96,8 @@ uiRoutes
});
},
resolve: {
indexPattern: function ($route, redirectWhenMissing, indexPatterns) {
return indexPatterns.get($route.current.params.indexPatternId)
indexPattern: function ($route, Promise, redirectWhenMissing, indexPatterns) {
return Promise.resolve(indexPatterns.get($route.current.params.indexPatternId))
.catch(redirectWhenMissing('/management/kibana/index_patterns'));
}
},

View file

@ -161,9 +161,8 @@ uiRoutes
template,
k7Breadcrumbs: getEditBreadcrumbs,
resolve: {
indexPattern: function ($route, redirectWhenMissing, indexPatterns) {
return indexPatterns
.get($route.current.params.indexPatternId)
indexPattern: function ($route, Promise, redirectWhenMissing, indexPatterns) {
return Promise.resolve(indexPatterns.get($route.current.params.indexPatternId))
.catch(redirectWhenMissing('/management/kibana/index_patterns'));
}
},
@ -171,7 +170,7 @@ uiRoutes
uiModules.get('apps/management')
.controller('managementIndexPatternsEdit', function (
$scope, $location, $route, config, indexPatterns, Private, AppState, confirmModal) {
$scope, $location, $route, Promise, config, indexPatterns, Private, AppState, confirmModal) {
const $state = $scope.state = new AppState();
const { fieldWildcardMatcher } = Private(FieldWildcardProvider);
const indexPatternListProvider = Private(IndexPatternListFactory)();
@ -272,7 +271,7 @@ uiModules.get('apps/management')
}
}
indexPatterns.delete($scope.indexPattern)
Promise.resolve(indexPatterns.delete($scope.indexPattern))
.then(function () {
$location.url('/management/kibana/index_patterns');
})

View file

@ -28,9 +28,6 @@ import {
FieldNotFoundInCache,
DuplicateField,
SavedObjectNotFound,
IndexPatternMissingIndices,
NoDefinedIndexPatterns,
NoDefaultIndexPattern,
PersistedStateError,
VislibError,
ContainerTooSmall,
@ -53,9 +50,6 @@ describe('ui/errors', () => {
new FieldNotFoundInCache('aname'),
new DuplicateField('dupfield'),
new SavedObjectNotFound('dashboard', '123'),
new IndexPatternMissingIndices(),
new NoDefinedIndexPatterns(),
new NoDefaultIndexPattern(),
new PersistedStateError(),
new VislibError('err'),
new ContainerTooSmall(),

View file

@ -156,18 +156,6 @@ 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
*/
@ -187,41 +175,6 @@ export class SavedObjectNotFound extends KbnError {
}
}
/**
* Tried to call a method that relies on SearchSource having an indexPattern assigned
*/
export class IndexPatternMissingIndices extends KbnError {
constructor(message) {
const defaultMessage = 'IndexPattern\'s configured pattern does not match any indices';
super(
(message && message.length) ? `No matching indices found: ${message}` : defaultMessage,
IndexPatternMissingIndices);
}
}
/**
* Tried to call a method that relies on SearchSource having an indexPattern assigned
*/
export class NoDefinedIndexPatterns extends KbnError {
constructor() {
super(
'Define at least one index pattern to continue',
NoDefinedIndexPatterns);
}
}
/**
* Tried to load a route besides management/kibana/index but you don't have a default index pattern!
*/
export class NoDefaultIndexPattern extends KbnError {
constructor() {
super(
'Please specify a default index pattern',
NoDefaultIndexPattern);
}
}
export class PersistedStateError extends KbnError {
constructor() {
super(

View file

@ -1,300 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import _ from 'lodash';
import sinon from 'sinon';
import ngMock from 'ng_mock';
import expect from '@kbn/expect';
import Promise from 'bluebird';
import { DuplicateField } from '../../errors';
import { IndexedArray } from '../../indexed_array';
import stubbedLogstashFields from 'fixtures/logstash_fields';
import { FixturesStubbedSavedObjectIndexPatternProvider } from 'fixtures/stubbed_saved_object_index_pattern';
import { IndexPatternProvider } from '../_index_pattern';
import NoDigestPromises from 'test_utils/no_digest_promises';
import { FieldsFetcherProvider } from '../fields_fetcher_provider';
import { StubIndexPatternsApiClientModule } from './stub_index_patterns_api_client';
import { IndexPatternsApiClientProvider } from '../index_patterns_api_client_provider';
import { SavedObjectsClientProvider } from '../../saved_objects';
describe('index pattern', function () {
NoDigestPromises.activateForSuite();
let IndexPattern;
let fieldsFetcher;
let mockLogstashFields;
let savedObjectsClient;
let savedObjectsResponse;
const indexPatternId = 'test-pattern';
let indexPattern;
let indexPatternsApiClient;
beforeEach(ngMock.module('kibana', StubIndexPatternsApiClientModule));
beforeEach(ngMock.inject(function (Private) {
mockLogstashFields = stubbedLogstashFields();
savedObjectsResponse = Private(FixturesStubbedSavedObjectIndexPatternProvider);
savedObjectsClient = Private(SavedObjectsClientProvider);
sinon.stub(savedObjectsClient, 'create');
sinon.stub(savedObjectsClient, 'get');
sinon.stub(savedObjectsClient, 'update');
IndexPattern = Private(IndexPatternProvider);
fieldsFetcher = Private(FieldsFetcherProvider);
indexPatternsApiClient = Private(IndexPatternsApiClientProvider);
}));
// create an indexPattern instance for each test
beforeEach(function () {
return create(indexPatternId).then(function (pattern) {
indexPattern = pattern;
});
});
// helper function to create index patterns
function create(id, payload) {
const indexPattern = new IndexPattern(id);
payload = _.defaults(payload || {}, savedObjectsResponse(id));
savedObjectsClient.create.returns(Promise.resolve(payload));
setDocsourcePayload(payload);
return indexPattern.init();
}
function setDocsourcePayload(payload) {
savedObjectsClient.get.returns(Promise.resolve(payload));
savedObjectsClient.update.returns(Promise.resolve(payload));
}
describe('api', function () {
it('should have expected properties', function () {
return create('test-pattern').then(function (indexPattern) {
// methods
expect(indexPattern).to.have.property('refreshFields');
expect(indexPattern).to.have.property('popularizeField');
expect(indexPattern).to.have.property('getScriptedFields');
expect(indexPattern).to.have.property('getNonScriptedFields');
expect(indexPattern).to.have.property('addScriptedField');
expect(indexPattern).to.have.property('removeScriptedField');
expect(indexPattern).to.have.property('toString');
expect(indexPattern).to.have.property('toJSON');
expect(indexPattern).to.have.property('save');
// properties
expect(indexPattern).to.have.property('fields');
});
});
});
describe('init', function () {
it('should append the found fields', function () {
expect(savedObjectsClient.get.callCount).to.be(1);
expect(indexPattern.fields).to.have.length(mockLogstashFields.length);
expect(indexPattern.fields).to.be.an(IndexedArray);
});
});
describe('fields', function () {
it('should have expected properties on fields', function () {
expect(indexPattern.fields[0]).to.have.property('displayName');
expect(indexPattern.fields[0]).to.have.property('filterable');
expect(indexPattern.fields[0]).to.have.property('format');
expect(indexPattern.fields[0]).to.have.property('sortable');
expect(indexPattern.fields[0]).to.have.property('scripted');
});
});
describe('getScriptedFields', function () {
it('should return all scripted fields', function () {
const scriptedNames = _(mockLogstashFields).where({ scripted: true }).pluck('name').value();
const respNames = _.pluck(indexPattern.getScriptedFields(), 'name');
expect(respNames).to.eql(scriptedNames);
});
});
describe('getNonScriptedFields', function () {
it('should return all non-scripted fields', function () {
const notScriptedNames = _(mockLogstashFields).where({ scripted: false }).pluck('name').value();
const respNames = _.pluck(indexPattern.getNonScriptedFields(), 'name');
expect(respNames).to.eql(notScriptedNames);
});
});
describe('refresh fields', function () {
it('should fetch fields from the fieldsFetcher', async function () {
expect(indexPattern.fields.length).to.be.greaterThan(2);
sinon.spy(fieldsFetcher, 'fetch');
indexPatternsApiClient.swapStubNonScriptedFields([
{ name: 'foo' },
{ name: 'bar' }
]);
await indexPattern.refreshFields();
sinon.assert.calledOnce(fieldsFetcher.fetch);
const newFields = indexPattern.getNonScriptedFields();
expect(newFields).to.have.length(2);
expect(newFields.map(f => f.name)).to.eql(['foo', 'bar']);
});
it('should preserve the scripted fields', async function () {
// add spy to indexPattern.getScriptedFields
sinon.spy(indexPattern, 'getScriptedFields');
// refresh fields, which will fetch
await indexPattern.refreshFields();
// called to append scripted fields to the response from mapper.getFieldsForIndexPattern
sinon.assert.calledOnce(indexPattern.getScriptedFields);
expect(indexPattern.getScriptedFields().map(f => f.name))
.to.eql(mockLogstashFields.filter(f => f.scripted).map(f => f.name));
});
});
describe('add and remove scripted fields', function () {
it('should append the scripted field', function () {
// keep a copy of the current scripted field count
const saveSpy = sinon.spy(indexPattern, 'save');
const oldCount = indexPattern.getScriptedFields().length;
// add a new scripted field
const scriptedField = {
name: 'new scripted field',
script: 'false',
type: 'boolean'
};
indexPattern.addScriptedField(scriptedField.name, scriptedField.script, scriptedField.type);
const scriptedFields = indexPattern.getScriptedFields();
expect(saveSpy.callCount).to.equal(1);
expect(scriptedFields).to.have.length(oldCount + 1);
expect(indexPattern.fields.byName[scriptedField.name].name).to.equal(scriptedField.name);
});
it('should remove scripted field, by name', function () {
const saveSpy = sinon.spy(indexPattern, 'save');
const scriptedFields = indexPattern.getScriptedFields();
const oldCount = scriptedFields.length;
const scriptedField = _.last(scriptedFields);
indexPattern.removeScriptedField(scriptedField.name);
expect(saveSpy.callCount).to.equal(1);
expect(indexPattern.getScriptedFields().length).to.equal(oldCount - 1);
expect(indexPattern.fields.byName[scriptedField.name]).to.equal(undefined);
});
it('should not allow duplicate names', function () {
const scriptedFields = indexPattern.getScriptedFields();
const scriptedField = _.last(scriptedFields);
expect(function () {
indexPattern.addScriptedField(scriptedField.name, '\'new script\'', 'string');
}).to.throwError(function (e) {
expect(e).to.be.a(DuplicateField);
});
});
});
describe('popularizeField', function () {
it('should increment the popularity count by default', function () {
const saveSpy = sinon.stub(indexPattern, 'save');
indexPattern.fields.forEach(function (field, i) {
const oldCount = field.count;
indexPattern.popularizeField(field.name);
expect(saveSpy.callCount).to.equal(i + 1);
expect(field.count).to.equal(oldCount + 1);
});
});
it('should increment the popularity count', function () {
const saveSpy = sinon.stub(indexPattern, 'save');
indexPattern.fields.forEach(function (field, i) {
const oldCount = field.count;
const incrementAmount = 4;
indexPattern.popularizeField(field.name, incrementAmount);
expect(saveSpy.callCount).to.equal(i + 1);
expect(field.count).to.equal(oldCount + incrementAmount);
});
});
it('should decrement the popularity count', function () {
indexPattern.fields.forEach(function (field) {
const oldCount = field.count;
const incrementAmount = 4;
const decrementAmount = -2;
indexPattern.popularizeField(field.name, incrementAmount);
indexPattern.popularizeField(field.name, decrementAmount);
expect(field.count).to.equal(oldCount + incrementAmount + decrementAmount);
});
});
it('should not go below 0', function () {
indexPattern.fields.forEach(function (field) {
const decrementAmount = -Number.MAX_VALUE;
indexPattern.popularizeField(field.name, decrementAmount);
expect(field.count).to.equal(0);
});
});
});
describe('getIndex', () => {
it('should return the title when there is no intervalName', () => {
expect(indexPattern.getIndex()).to.be(indexPattern.title);
});
it('should convert time-based intervals to wildcards', () => {
const oldTitle = indexPattern.title;
indexPattern.intervalName = 'daily';
indexPattern.title = '[logstash-]YYYY.MM.DD';
expect(indexPattern.getIndex()).to.be('logstash-*');
indexPattern.title = 'YYYY.MM.DD[-logstash]';
expect(indexPattern.getIndex()).to.be('*-logstash');
indexPattern.title = 'YYYY[-logstash-]YYYY.MM.DD';
expect(indexPattern.getIndex()).to.be('*-logstash-*');
indexPattern.title = 'YYYY[-logstash-]YYYY[-foo-]MM.DD';
expect(indexPattern.getIndex()).to.be('*-logstash-*-foo-*');
indexPattern.title = 'YYYY[-logstash-]YYYY[-foo-]MM.DD[-bar]';
expect(indexPattern.getIndex()).to.be('*-logstash-*-foo-*-bar');
indexPattern.title = '[logstash-]YYYY[-foo-]MM.DD[-bar]';
expect(indexPattern.getIndex()).to.be('logstash-*-foo-*-bar');
indexPattern.title = '[logstash]';
expect(indexPattern.getIndex()).to.be('logstash');
indexPattern.title = oldTitle;
delete indexPattern.intervalName;
});
});
});

View file

@ -17,7 +17,11 @@
* under the License.
*/
import { IndexPatternProvider } from '../_index_pattern';
import _ from 'lodash';
import { IndexedArray } from '../../indexed_array';
import { IndexPattern } from '../_index_pattern';
import mockLogstashFields from '../../../../../fixtures/logstash_fields';
import { fixturesStubbedSavedObjectIndexPatternProvider } from '../../../../../fixtures/stubbed_saved_object_index_pattern';
jest.mock('../../errors', () => ({
SavedObjectNotFound: jest.fn(),
@ -25,6 +29,11 @@ jest.mock('../../errors', () => ({
IndexPatternMissingIndices: jest.fn(),
}));
jest.mock('../errors', () => ({
IndexPatternMissingIndices: jest.fn(),
}));
jest.mock('../../registry/field_formats', () => ({
fieldFormats: {
getDefaultInstance: jest.fn(),
@ -35,6 +44,9 @@ jest.mock('../../utils/mapping_setup', () => ({
expandShorthand: jest.fn().mockImplementation(() => ({
id: true,
title: true,
fieldFormatMap: {
_deserialize: jest.fn().mockImplementation(() => ([])),
},
}))
}));
@ -46,7 +58,7 @@ jest.mock('../../notify', () => ({
}));
jest.mock('../_format_hit', () => ({
formatHit: jest.fn().mockImplementation(() => ({
formatHitProvider: jest.fn().mockImplementation(() => ({
formatField: jest.fn(),
}))
}));
@ -58,80 +70,271 @@ jest.mock('../_get', () => ({
}));
jest.mock('../_flatten_hit', () => ({
IndexPatternsFlattenHitProvider: jest.fn(),
flattenHitWrapper: jest.fn(),
}));
jest.mock('../_pattern_cache', () => ({
IndexPatternsPatternCacheProvider: {
clear: jest.fn(),
}
}));
jest.mock('../fields_fetcher_provider', () => ({
FieldsFetcherProvider: {
fetch: jest.fn().mockImplementation(() => ([]))
}
}));
jest.mock('../../saved_objects', () => {
const object = {
_version: 'foo',
_id: 'foo',
attributes: {
title: 'something'
}
};
return {
SavedObjectsClientProvider: {
get: async () => object,
update: async (type, id, body, { version }) => {
if (object._version !== version) {
throw {
res: {
status: 409
}
};
}
object.attributes.title = body.title;
object._version += 'a';
return {
id: object._id,
_version: object._version,
};
}
},
findObjectByTitle: jest.fn(),
};
});
const Private = arg => arg;
let object;
const savedObjectsClient = {
create: jest.fn(),
get: jest.fn().mockImplementation(() => object),
update: jest.fn().mockImplementation(async (type, id, body, { version }) => {
if (object._version !== version) {
throw {
res: {
status: 409
}
};
}
object.attributes.title = body.title;
object._version += 'a';
return {
id: object._id,
_version: object._version,
};
}),
};
const patternCache = {
clear: jest.fn(),
};
let fields = [];
const fieldsFetcher = {
fetch: jest.fn().mockImplementation(() => fields),
every: jest.fn(),
};
const getIds = {
clearCache: jest.fn(),
};
const config = {
get: jest.fn(),
watchAll: jest.fn(),
};
const Promise = window.Promise;
const confirmModalPromise = jest.fn();
const kbnUrl = {
eval: jest.fn(),
};
const i18n = arg => arg;
const savedObjectsResponse = fixturesStubbedSavedObjectIndexPatternProvider();
// helper function to create index patterns
function create(id, payload) {
const indexPattern = new IndexPattern(id, config, savedObjectsClient, patternCache, fieldsFetcher, getIds);
setDocsourcePayload(id, payload);
return indexPattern.init();
}
function setDocsourcePayload(id, providedPayload) {
object = _.defaults(providedPayload || {}, savedObjectsResponse(id));
}
describe('IndexPattern', () => {
it('should handle version conflicts', async () => {
const IndexPattern = IndexPatternProvider(Private, config, Promise, confirmModalPromise, kbnUrl, i18n); // eslint-disable-line new-cap
const indexPatternId = 'test-pattern';
let indexPattern;
// create an indexPattern instance for each test
beforeEach(function () {
return create(indexPatternId).then(function (pattern) {
indexPattern = pattern;
});
});
describe('api', function () {
it('should have expected properties', function () {
expect(indexPattern).toHaveProperty('refreshFields');
expect(indexPattern).toHaveProperty('popularizeField');
expect(indexPattern).toHaveProperty('getScriptedFields');
expect(indexPattern).toHaveProperty('getNonScriptedFields');
expect(indexPattern).toHaveProperty('addScriptedField');
expect(indexPattern).toHaveProperty('removeScriptedField');
expect(indexPattern).toHaveProperty('toString');
expect(indexPattern).toHaveProperty('toJSON');
expect(indexPattern).toHaveProperty('save');
// properties
expect(indexPattern).toHaveProperty('fields');
});
});
describe('init', function () {
it('should append the found fields', function () {
expect(savedObjectsClient.get).toHaveBeenCalled();
expect(indexPattern.fields).toHaveLength(mockLogstashFields().length);
expect(indexPattern.fields).toBeInstanceOf(IndexedArray);
});
});
describe('fields', function () {
it('should have expected properties on fields', function () {
expect(indexPattern.fields[0]).toHaveProperty('displayName');
expect(indexPattern.fields[0]).toHaveProperty('filterable');
expect(indexPattern.fields[0]).toHaveProperty('format');
expect(indexPattern.fields[0]).toHaveProperty('sortable');
expect(indexPattern.fields[0]).toHaveProperty('scripted');
});
});
describe('getScriptedFields', function () {
it('should return all scripted fields', function () {
const scriptedNames = _(mockLogstashFields()).where({ scripted: true }).pluck('name').value();
const respNames = _.pluck(indexPattern.getScriptedFields(), 'name');
expect(respNames).toEqual(scriptedNames);
});
});
describe('getNonScriptedFields', function () {
it('should return all non-scripted fields', function () {
const notScriptedNames = _(mockLogstashFields()).where({ scripted: false }).pluck('name').value();
const respNames = _.pluck(indexPattern.getNonScriptedFields(), 'name');
expect(respNames).toEqual(notScriptedNames);
});
});
describe('refresh fields', function () {
it('should fetch fields from the fieldsFetcher', async function () {
expect(indexPattern.fields.length).toBeGreaterThan(2);
fields = [
{ name: 'foo' },
{ name: 'bar' }
];
await indexPattern.refreshFields();
expect(fieldsFetcher.fetch).toHaveBeenCalledTimes(1);
fields = [];
const newFields = indexPattern.getNonScriptedFields();
expect(newFields).toHaveLength(2);
expect(newFields.map(f => f.name)).toEqual(['foo', 'bar']);
});
it('should preserve the scripted fields', async function () {
// add spy to indexPattern.getScriptedFields
// sinon.spy(indexPattern, 'getScriptedFields');
// refresh fields, which will fetch
await indexPattern.refreshFields();
// called to append scripted fields to the response from mapper.getFieldsForIndexPattern
// sinon.assert.calledOnce(indexPattern.getScriptedFields);
expect(indexPattern.getScriptedFields().map(f => f.name))
.toEqual(mockLogstashFields().filter(f => f.scripted).map(f => f.name));
});
});
describe('add and remove scripted fields', function () {
it('should append the scripted field', function () {
// keep a copy of the current scripted field count
// const saveSpy = sinon.spy(indexPattern, 'save');
const oldCount = indexPattern.getScriptedFields().length;
// add a new scripted field
const scriptedField = {
name: 'new scripted field',
script: 'false',
type: 'boolean'
};
indexPattern.addScriptedField(scriptedField.name, scriptedField.script, scriptedField.type);
const scriptedFields = indexPattern.getScriptedFields();
// expect(saveSpy.callCount).to.equal(1);
expect(scriptedFields).toHaveLength(oldCount + 1);
expect(indexPattern.fields.byName[scriptedField.name].name).toEqual(scriptedField.name);
});
it('should remove scripted field, by name', function () {
// const saveSpy = sinon.spy(indexPattern, 'save');
const scriptedFields = indexPattern.getScriptedFields();
const oldCount = scriptedFields.length;
const scriptedField = _.last(scriptedFields);
indexPattern.removeScriptedField(scriptedField.name);
// expect(saveSpy.callCount).to.equal(1);
expect(indexPattern.getScriptedFields().length).toEqual(oldCount - 1);
expect(indexPattern.fields.byName[scriptedField.name]).toEqual(undefined);
});
it('should not allow duplicate names', function () {
const scriptedFields = indexPattern.getScriptedFields();
const scriptedField = _.last(scriptedFields);
expect(function () {
indexPattern.addScriptedField(scriptedField.name, '\'new script\'', 'string');
}).toThrow();
});
});
describe('popularizeField', function () {
it('should increment the popularity count by default', function () {
// const saveSpy = sinon.stub(indexPattern, 'save');
indexPattern.fields.forEach(function (field) {
const oldCount = field.count;
indexPattern.popularizeField(field.name);
// expect(saveSpy.callCount).to.equal(i + 1);
expect(field.count).toEqual(oldCount + 1);
});
});
it('should increment the popularity count', function () {
// const saveSpy = sinon.stub(indexPattern, 'save');
indexPattern.fields.forEach(function (field) {
const oldCount = field.count;
const incrementAmount = 4;
indexPattern.popularizeField(field.name, incrementAmount);
// expect(saveSpy.callCount).to.equal(i + 1);
expect(field.count).toEqual(oldCount + incrementAmount);
});
});
it('should decrement the popularity count', function () {
indexPattern.fields.forEach(function (field) {
const oldCount = field.count;
const incrementAmount = 4;
const decrementAmount = -2;
indexPattern.popularizeField(field.name, incrementAmount);
indexPattern.popularizeField(field.name, decrementAmount);
expect(field.count).toEqual(oldCount + incrementAmount + decrementAmount);
});
});
it('should not go below 0', function () {
indexPattern.fields.forEach(function (field) {
const decrementAmount = -Number.MAX_VALUE;
indexPattern.popularizeField(field.name, decrementAmount);
expect(field.count).toEqual(0);
});
});
});
it('should handle version conflicts', async () => {
setDocsourcePayload(null, {
_version: 'foo',
_id: 'foo',
attributes: {
title: 'something'
}
});
// Create a normal index pattern
const pattern = new IndexPattern('foo');
const pattern = new IndexPattern('foo', config, savedObjectsClient, patternCache, fieldsFetcher, getIds);
await pattern.init();
expect(pattern.version).toBe('fooa');
// Create the same one - we're going to handle concurrency
const samePattern = new IndexPattern('foo');
const samePattern = new IndexPattern('foo', config, savedObjectsClient, patternCache, fieldsFetcher, getIds);
await samePattern.init();
expect(samePattern.version).toBe('fooaa');

View file

@ -0,0 +1,92 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { IndexPatterns } from '../index_patterns';
jest.mock('../errors', () => ({
IndexPatternMissingIndices: jest.fn(),
}));
jest.mock('../../registry/field_formats', () => ({
fieldFormats: {
getDefaultInstance: jest.fn(),
}
}));
jest.mock('../../notify', () => ({
Notifier: jest.fn().mockImplementation(() => ({
error: jest.fn(),
})),
toastNotifications: {
addDanger: jest.fn(),
}
}));
jest.mock('../_get', () => ({
indexPatternsGetProvider: jest.fn().mockImplementation(() => {
return () => {};
})
}));
jest.mock('../_index_pattern', () => {
class IndexPattern {
init = async () => {
return this;
}
}
return {
IndexPattern
};
});
jest.mock('../index_patterns_api_client', () => {
class IndexPatternsApiClient {
getFieldsForWildcard = async () => ({})
}
return {
IndexPatternsApiClient
};
});
const savedObjectsClient = {
create: jest.fn(),
get: jest.fn(),
update: jest.fn()
};
const config = {
get: jest.fn(),
};
describe('IndexPatterns', () => {
const indexPatterns = new IndexPatterns(config, savedObjectsClient);
it('does not cache gets without an id', function () {
expect(indexPatterns.get()).not.toBe(indexPatterns.get());
});
it('does cache gets for the same id', function () {
expect(indexPatterns.get(1)).toBe(indexPatterns.get(1));
});
});

View file

@ -19,40 +19,39 @@
import expect from '@kbn/expect';
import ngMock from 'ng_mock';
import { IndexPatternsFlattenHitProvider } from '../_flatten_hit';
import { flattenHitWrapper } from '../_flatten_hit';
const indexPattern = {
fields: {
byName: {
'tags.text': { type: 'string' },
'tags.label': { type: 'string' },
'message': { type: 'string' },
'geo.coordinates': { type: 'geo_point' },
'geo.dest': { type: 'string' },
'geo.src': { type: 'string' },
'bytes': { type: 'number' },
'@timestamp': { type: 'date' },
'team': { type: 'nested' },
'team.name': { type: 'string' },
'team.role': { type: 'string' },
'user': { type: 'conflict' },
'user.name': { type: 'string' },
'user.id': { type: 'conflict' },
'delta': { type: 'number', scripted: true }
}
}
};
describe('IndexPattern#flattenHit()', function () {
let flattenHit;
let config;
let hit;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private, $injector) {
const indexPattern = {
fields: {
byName: {
'tags.text': { type: 'string' },
'tags.label': { type: 'string' },
'message': { type: 'string' },
'geo.coordinates': { type: 'geo_point' },
'geo.dest': { type: 'string' },
'geo.src': { type: 'string' },
'bytes': { type: 'number' },
'@timestamp': { type: 'date' },
'team': { type: 'nested' },
'team.name': { type: 'string' },
'team.role': { type: 'string' },
'user': { type: 'conflict' },
'user.name': { type: 'string' },
'user.id': { type: 'conflict' },
'delta': { type: 'number', scripted: true }
}
}
};
beforeEach(ngMock.inject(function () {
flattenHit = Private(IndexPatternsFlattenHitProvider)(indexPattern);
config = $injector.get('config');
flattenHit = flattenHitWrapper(indexPattern, []);
hit = {
_source: {
@ -154,14 +153,14 @@ describe('IndexPattern#flattenHit()', function () {
});
it('ignores fields that start with an _ and are not in the metaFields', function () {
config.set('metaFields', ['_metaKey']);
flattenHit = flattenHitWrapper(indexPattern, ['_metaKey']);
hit.fields._notMetaKey = [100];
const flat = flattenHit(hit);
expect(flat).to.not.have.property('_notMetaKey');
});
it('includes underscore-prefixed keys that are in the metaFields', function () {
config.set('metaFields', ['_metaKey']);
flattenHit = flattenHitWrapper(indexPattern, ['_metaKey']);
hit.fields._metaKey = [100];
const flat = flattenHit(hit);
expect(flat).to.have.property('_metaKey', 100);
@ -170,18 +169,18 @@ describe('IndexPattern#flattenHit()', function () {
it('adapts to changes in the metaFields', function () {
hit.fields._metaKey = [100];
config.set('metaFields', ['_metaKey']);
flattenHit = flattenHitWrapper(indexPattern, ['_metaKey']);
let flat = flattenHit(hit);
expect(flat).to.have.property('_metaKey', 100);
config.set('metaFields', []);
flattenHit = flattenHitWrapper(indexPattern, []);
flat = flattenHit(hit);
expect(flat).to.not.have.property('_metaKey');
});
it('handles fields that are not arrays, like _timestamp', function () {
hit.fields._metaKey = 20000;
config.set('metaFields', ['_metaKey']);
flattenHit = flattenHitWrapper(indexPattern, ['_metaKey']);
const flat = flattenHit(hit);
expect(flat).to.have.property('_metaKey', 20000);
});

View file

@ -17,7 +17,6 @@
* under the License.
*/
import './_index_pattern';
import './_get_computed_fields';
describe('Index Patterns', function () {
});

View file

@ -1,54 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import ngMock from 'ng_mock';
import expect from '@kbn/expect';
import sinon from 'sinon';
import { IndexPatternProvider } from '../_index_pattern';
import { IndexPatternsProvider } from '../index_patterns';
describe('IndexPatterns service', function () {
let indexPatterns;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
const IndexPattern = Private(IndexPatternProvider);
indexPatterns = Private(IndexPatternsProvider);
// prevent IndexPattern initialization from doing anything
Private.stub(
IndexPatternProvider,
function (...args) {
const indexPattern = new IndexPattern(...args);
sinon.stub(indexPattern, 'init', function () {
return new Promise();
});
return indexPattern;
}
);
}));
it('does not cache gets without an id', function () {
expect(indexPatterns.get()).to.not.be(indexPatterns.get());
});
it('does cache gets for the same id', function () {
expect(indexPatterns.get(1)).to.be(indexPatterns.get(1));
});
});

View file

@ -1,46 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import MockLogstashFieldsProvider from 'fixtures/logstash_fields';
import sinon from 'sinon';
import { IndexPatternsApiClientProvider } from '../index_patterns_api_client_provider';
// place in a ngMock.module() call to swap out the IndexPatternsApiClient
export function StubIndexPatternsApiClientModule(PrivateProvider) {
PrivateProvider.swap(
IndexPatternsApiClientProvider,
(Private, Promise) => {
let nonScriptedFields = Private(MockLogstashFieldsProvider).filter(field => (
field.scripted !== true
));
class StubIndexPatternsApiClient {
getFieldsForTimePattern = sinon.spy(() => Promise.resolve(nonScriptedFields));
getFieldsForWildcard = sinon.spy(() => Promise.resolve(nonScriptedFields));
swapStubNonScriptedFields = (newNonScriptedFields) => {
nonScriptedFields = newNonScriptedFields;
}
}
return new StubIndexPatternsApiClient();
}
);
}

View file

@ -17,13 +17,12 @@
* under the License.
*/
import { ObjDefine } from '../utils/obj_define';
import { ObjDefine } from 'ui/utils/obj_define';
import { FieldFormat } from '../../field_formats/field_format';
import { fieldFormats } from '../registry/field_formats';
import { fieldFormats } from 'ui/registry/field_formats';
import { getKbnFieldType } from '../../../utils';
import { shortenDottedString } from '../../../core_plugins/kibana/common/utils/shorten_dotted_string';
import { toastNotifications } from 'ui/notify';
import chrome from 'ui/chrome';
import { i18n } from '@kbn/i18n';
export function Field(indexPattern, spec) {
@ -39,6 +38,8 @@ export function Field(indexPattern, spec) {
spec.type = '_source';
}
const shortDotsEnable = indexPattern.shortDotsEnable;
// find the type for this field, fallback to unknown type
let type = getKbnFieldType(spec.type);
if (spec.type && !type) {
@ -93,7 +94,7 @@ export function Field(indexPattern, spec) {
// computed values
obj.comp('indexPattern', indexPattern);
obj.comp('displayName', chrome.getUiSettingsClient().get('shortDots:enable') ? shortenDottedString(spec.name) : spec.name);
obj.comp('displayName', shortDotsEnable ? shortenDottedString(spec.name) : spec.name);
obj.comp('$$spec', spec);
// conflict info

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { IndexedArray } from '../indexed_array';
import { IndexedArray } from 'ui/indexed_array';
import { Field } from './_field';
export class FieldList extends IndexedArray {

View file

@ -23,82 +23,76 @@ const flattenedCache = new WeakMap();
// Takes a hit, merges it with any stored/scripted fields, and with the metaFields
// returns a flattened version
export function IndexPatternsFlattenHitProvider(config) {
let metaFields = config.get('metaFields');
config.watch('metaFields', value => {
metaFields = value;
});
function flattenHit(indexPattern, hit, deep) {
const flat = {};
function flattenHit(indexPattern, hit, deep) {
const flat = {};
// recursively merge _source
const fields = indexPattern.fields.byName;
(function flatten(obj, keyPrefix) {
keyPrefix = keyPrefix ? keyPrefix + '.' : '';
_.forOwn(obj, function (val, key) {
key = keyPrefix + key;
// recursively merge _source
const fields = indexPattern.fields.byName;
(function flatten(obj, keyPrefix) {
keyPrefix = keyPrefix ? keyPrefix + '.' : '';
_.forOwn(obj, function (val, key) {
key = keyPrefix + key;
if (deep) {
const isNestedField = fields[key] && fields[key].type === 'nested';
const isArrayOfObjects = Array.isArray(val) && _.isPlainObject(_.first(val));
if (isArrayOfObjects && !isNestedField) {
_.each(val, v => flatten(v, key));
return;
}
} else if (flat[key] !== void 0) {
if (deep) {
const isNestedField = fields[key] && fields[key].type === 'nested';
const isArrayOfObjects = Array.isArray(val) && _.isPlainObject(_.first(val));
if (isArrayOfObjects && !isNestedField) {
_.each(val, v => flatten(v, key));
return;
}
const hasValidMapping = fields[key] && fields[key].type !== 'conflict';
const isValue = !_.isPlainObject(val);
if (hasValidMapping || isValue) {
if (!flat[key]) {
flat[key] = val;
} else if (Array.isArray(flat[key])) {
flat[key].push(val);
} else {
flat[key] = [ flat[key], val ];
}
return;
}
flatten(val, key);
});
}(hit._source));
return flat;
}
function decorateFlattenedWrapper(hit) {
return function (flattened) {
// assign the meta fields
_.each(metaFields, function (meta) {
if (meta === '_source') return;
flattened[meta] = hit[meta];
});
// unwrap computed fields
_.forOwn(hit.fields, function (val, key) {
if (key[0] === '_' && !_.contains(metaFields, key)) return;
flattened[key] = Array.isArray(val) && val.length === 1 ? val[0] : val;
});
return flattened;
};
}
return function flattenHitWrapper(indexPattern) {
return function cachedFlatten(hit, deep = false) {
const decorateFlattened = decorateFlattenedWrapper(hit);
const cached = flattenedCache.get(hit);
const flattened = cached || flattenHit(indexPattern, hit, deep);
if (!cached) {
flattenedCache.set(hit, { ...flattened });
} else if (flat[key] !== void 0) {
return;
}
return decorateFlattened(flattened);
};
const hasValidMapping = fields[key] && fields[key].type !== 'conflict';
const isValue = !_.isPlainObject(val);
if (hasValidMapping || isValue) {
if (!flat[key]) {
flat[key] = val;
} else if (Array.isArray(flat[key])) {
flat[key].push(val);
} else {
flat[key] = [ flat[key], val ];
}
return;
}
flatten(val, key);
});
}(hit._source));
return flat;
}
function decorateFlattenedWrapper(hit, metaFields) {
return function (flattened) {
// assign the meta fields
_.each(metaFields, function (meta) {
if (meta === '_source') return;
flattened[meta] = hit[meta];
});
// unwrap computed fields
_.forOwn(hit.fields, function (val, key) {
if (key[0] === '_' && !_.contains(metaFields, key)) return;
flattened[key] = Array.isArray(val) && val.length === 1 ? val[0] : val;
});
return flattened;
};
}
export function flattenHitWrapper(indexPattern, metaFields = {}) {
return function cachedFlatten(hit, deep = false) {
const decorateFlattened = decorateFlattenedWrapper(hit, metaFields);
const cached = flattenedCache.get(hit);
const flattened = cached || flattenHit(indexPattern, hit, deep);
if (!cached) {
flattenedCache.set(hit, { ...flattened });
}
return decorateFlattened(flattened);
};
}

View file

@ -18,14 +18,13 @@
*/
import _ from 'lodash';
import chrome from '../chrome';
const formattedCache = new WeakMap();
const partialFormattedCache = new WeakMap();
// Takes a hit, merges it with any stored/scripted fields, and with the metaFields
// returns a formatted version
export function formatHit(indexPattern, defaultFormat) {
export function formatHitProvider(indexPattern, defaultFormat) {
function convert(hit, val, fieldName) {
const field = indexPattern.fields.byName[fieldName];
@ -33,7 +32,6 @@ export function formatHit(indexPattern, defaultFormat) {
const parsedUrl = {
origin: window.location.origin,
pathname: window.location.pathname,
basePath: chrome.getBasePath(),
};
return field.format.getConverterFor('html')(val, field, hit, parsedUrl);
}

View file

@ -18,10 +18,8 @@
*/
import _ from 'lodash';
import { SavedObjectsClientProvider } from '../saved_objects';
export function IndexPatternsGetProvider(Private) {
const savedObjectsClient = Private(SavedObjectsClientProvider);
export function indexPatternsGetProvider(savedObjectsClient) {
// many places may require the id list, so we will cache it separately
// didn't incorporate with the indexPattern cache to prevent id collisions.

View file

@ -17,23 +17,24 @@
* under the License.
*/
import _ from 'lodash';
import { each, reject } from 'lodash';
export function getComputedFields() {
const self = this;
const scriptFields = {};
let docvalueFields = [];
// Date value returned in "_source" could be in any number of formats
// Use a docvalue for each date field to ensure standardized formats when working with date fields
// indexPattern.flattenHit will override "_source" values when the same field is also defined in "fields"
docvalueFields = _.reject(self.fields.byType.date, 'scripted')
.map((dateField) => ({
field: dateField.name,
format: dateField.esTypes && dateField.esTypes.indexOf('date_nanos') !== -1 ? 'strict_date_time' : 'date_time',
}));
const docvalueFields = reject(self.fields.byType.date, 'scripted')
.map((dateField) => {
return {
field: dateField.name,
format: dateField.esTypes && dateField.esTypes.indexOf('date_nanos') !== -1 ? 'strict_date_time' : 'date_time',
};
});
_.each(self.getScriptedFields(), function (field) {
each(self.getScriptedFields(), function (field) {
scriptFields[field.name] = {
script: {
source: field.script,

View file

@ -18,91 +18,132 @@
*/
import _ from 'lodash';
import { SavedObjectNotFound, DuplicateField, IndexPatternMissingIndices } from '../errors';
import angular from 'angular';
import { fieldFormats } from '../registry/field_formats';
import UtilsMappingSetupProvider from '../utils/mapping_setup';
import { toastNotifications } from '../notify';
import { i18n } from '@kbn/i18n';
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React from 'react';
import { getComputedFields } from './_get_computed_fields';
import { formatHit } from './_format_hit';
import { IndexPatternsGetProvider } from './_get';
import { FieldList } from './_field_list';
import { IndexPatternsFlattenHitProvider } from './_flatten_hit';
import { IndexPatternsPatternCacheProvider } from './_pattern_cache';
import { FieldsFetcherProvider } from './fields_fetcher_provider';
import { SavedObjectsClientProvider, findObjectByTitle } from '../saved_objects';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
export function getRoutes() {
return {
edit: '/management/kibana/index_patterns/{{id}}',
addField: '/management/kibana/index_patterns/{{id}}/create-field',
indexedFields: '/management/kibana/index_patterns/{{id}}?_a=(tab:indexedFields)',
scriptedFields: '/management/kibana/index_patterns/{{id}}?_a=(tab:scriptedFields)',
sourceFilters: '/management/kibana/index_patterns/{{id}}?_a=(tab:sourceFilters)'
};
}
import { SavedObjectNotFound, DuplicateField } from 'ui/errors';
import { fieldFormats } from 'ui/registry/field_formats';
import { expandShorthand } from 'ui/utils/mapping_setup';
import { toastNotifications } from 'ui/notify';
import { findObjectByTitle } from 'ui/saved_objects';
import { IndexPatternMissingIndices } from './errors';
import { getComputedFields } from './_get_computed_fields';
import { getRoutes } from './get_routes';
import { formatHitProvider } from './_format_hit';
import { FieldList } from './_field_list';
import { flattenHitWrapper } from './_flatten_hit';
const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3;
const type = 'index-pattern';
export function IndexPatternProvider(Private, config, Promise, kbnUrl) {
const getConfig = (...args) => config.get(...args);
const getIds = Private(IndexPatternsGetProvider)('id');
const fieldsFetcher = Private(FieldsFetcherProvider);
const mappingSetup = Private(UtilsMappingSetupProvider);
const flattenHit = Private(IndexPatternsFlattenHitProvider);
const patternCache = Private(IndexPatternsPatternCacheProvider);
const savedObjectsClient = Private(SavedObjectsClientProvider);
const fieldformats = fieldFormats;
export class IndexPattern {
constructor(id, config, savedObjectsClient, patternCache, fieldsFetcher, getIds) {
this._setId(id);
this.config = config;
this.savedObjectsClient = savedObjectsClient;
this.patternCache = patternCache;
this.fieldsFetcher = fieldsFetcher;
this.getIds = getIds;
const type = 'index-pattern';
const configWatchers = new WeakMap();
this.metaFields = config.get('metaFields');
this.shortDotsEnable = config.get('shortDots:enable');
this.getComputedFields = getComputedFields.bind(this);
const mapping = mappingSetup.expandShorthand({
title: 'text',
timeFieldName: 'keyword',
intervalName: 'keyword',
fields: 'json',
sourceFilters: 'json',
fieldFormatMap: {
type: 'text',
_serialize(map = {}) {
const serialized = _.transform(map, serializeFieldFormatMap);
return _.isEmpty(serialized) ? undefined : angular.toJson(serialized);
},
_deserialize(map = '{}') {
return _.mapValues(angular.fromJson(map), deserializeFieldFormatMap);
this.flattenHit = flattenHitWrapper(this, this.metaFields);
this.formatHit = formatHitProvider(this, fieldFormats.getDefaultInstance('string'));
this.formatField = this.formatHit.formatField;
const getConfig = cfg => config.get(cfg);
function serializeFieldFormatMap(flat, format, field) {
if (format) {
flat[field] = format;
}
},
type: 'keyword',
typeMeta: 'json',
});
function serializeFieldFormatMap(flat, format, field) {
if (format) {
flat[field] = format;
}
function deserializeFieldFormatMap(mapping) {
const FieldFormat = fieldFormats.byId[mapping.id];
return FieldFormat && new FieldFormat(mapping.params, getConfig);
}
this.mapping = expandShorthand({
title: 'text',
timeFieldName: 'keyword',
intervalName: 'keyword',
fields: 'json',
sourceFilters: 'json',
fieldFormatMap: {
type: 'text',
_serialize(map = {}) {
const serialized = _.transform(map, serializeFieldFormatMap);
return _.isEmpty(serialized) ? undefined : JSON.stringify(serialized);
},
_deserialize(map = '{}') {
return _.mapValues(JSON.parse(map), mapping => { return deserializeFieldFormatMap(mapping); });
}
},
type: 'keyword',
typeMeta: 'json',
});
}
function deserializeFieldFormatMap(mapping) {
const FieldFormat = fieldformats.byId[mapping.id];
return FieldFormat && new FieldFormat(mapping.params, getConfig);
_setId(id) {
this.id = id;
return this;
}
function updateFromElasticSearch(indexPattern, response, forceFieldRefresh = false) {
_setVersion(version) {
this.version = version;
return this;
}
_initFields(input) {
const oldValue = this.fields;
const newValue = input || oldValue || [];
this.fields = new FieldList(this, newValue);
}
async _indexFields(forceFieldRefresh = false) {
if (!this.id) {
return;
}
function isFieldRefreshRequired(indexPattern) {
if (!indexPattern.fields) {
return true;
}
return indexPattern.fields.every(field => {
// See https://github.com/elastic/kibana/pull/8421
const hasFieldCaps = ('aggregatable' in field) && ('searchable' in field);
// See https://github.com/elastic/kibana/pull/11969
const hasDocValuesFlag = ('readFromDocValues' in field);
return !hasFieldCaps || !hasDocValuesFlag;
});
}
if (forceFieldRefresh || isFieldRefreshRequired(this)) {
await this.refreshFields();
}
this._initFields();
}
_updateFromElasticSearch(response, forceFieldRefresh = false) {
if (!response.found) {
throw new SavedObjectNotFound(
type,
indexPattern.id,
this.id,
'#/management/kibana/index_pattern',
);
}
_.forOwn(mapping, (fieldMapping, name) => {
_.forOwn(this.mapping, (fieldMapping, name) => {
if (!fieldMapping._deserialize) {
return;
}
@ -110,13 +151,13 @@ export function IndexPatternProvider(Private, config, Promise, kbnUrl) {
});
// give index pattern all of the values in _source
_.assign(indexPattern, response._source);
_.assign(this, response._source);
if (!indexPattern.title) {
indexPattern.title = indexPattern.id;
if (!this.title) {
this.title = this.id;
}
if (indexPattern.isUnsupportedTimePattern()) {
if (this.isUnsupportedTimePattern()) {
const warningTitle = i18n.translate('common.ui.indexPattern.warningTitle', {
defaultMessage: 'Support for time interval index patterns removed',
});
@ -124,11 +165,13 @@ export function IndexPatternProvider(Private, config, Promise, kbnUrl) {
const warningText = i18n.translate('common.ui.indexPattern.warningText', {
defaultMessage: 'Currently querying all indices matching {index}. {title} should be migrated to a wildcard-based index pattern.',
values: {
title: indexPattern.title,
index: indexPattern.getIndex()
title: this.title,
index: this.getIndex()
}
});
const url = `#${this.routes.edit.replace('{{id}}', this.id)}`;
toastNotifications.addWarning({
title: warningTitle,
text: (
@ -136,7 +179,7 @@ export function IndexPatternProvider(Private, config, Promise, kbnUrl) {
<p>{warningText}</p>
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiButton size="s" href={kbnUrl.getRouteHref(indexPattern, 'edit')}>
<EuiButton size="s" href={url}>
<FormattedMessage id="common.ui.indexPattern.editIndexPattern" defaultMessage="Edit index pattern" />
</EuiButton>
</EuiFlexItem>
@ -146,392 +189,308 @@ export function IndexPatternProvider(Private, config, Promise, kbnUrl) {
});
}
return indexFields(indexPattern, forceFieldRefresh);
return this._indexFields(forceFieldRefresh);
}
function isFieldRefreshRequired(indexPattern) {
if (!indexPattern.fields) {
return true;
get routes() {
return getRoutes();
}
async init(forceFieldRefresh = false) {
if (!this.id) {
return this; // no id === no elasticsearch document
}
return indexPattern.fields.every(field => {
// See https://github.com/elastic/kibana/pull/8421
const hasFieldCaps = ('aggregatable' in field) && ('searchable' in field);
const savedObject = await this.savedObjectsClient.get(type, this.id);
this._setVersion(savedObject._version);
// See https://github.com/elastic/kibana/pull/11969
const hasDocValuesFlag = ('readFromDocValues' in field);
const response = {
_id: savedObject.id,
_type: savedObject.type,
_source: _.cloneDeep(savedObject.attributes),
found: savedObject._version ? true : false
};
// Do this before we attempt to update from ES since that call can potentially perform a save
this.originalBody = this.prepBody();
await this._updateFromElasticSearch(response, forceFieldRefresh);
// Do it after to ensure we have the most up to date information
this.originalBody = this.prepBody();
return !hasFieldCaps || !hasDocValuesFlag;
return this;
}
migrate(newTitle) {
return this.savedObjectsClient.update(type, this.id, {
title: newTitle,
intervalName: null
}).then(({ attributes: { title, intervalName } }) => {
this.title = title;
this.intervalName = intervalName;
}).then(() => this);
}
// Get the source filtering configuration for that index.
getSourceFiltering() {
return {
excludes: this.sourceFilters && this.sourceFilters.map(filter => filter.value) || []
};
}
addScriptedField(name, script, type = 'string', lang) {
const scriptedFields = this.getScriptedFields();
const names = _.pluck(scriptedFields, 'name');
if (_.contains(names, name)) {
throw new DuplicateField(name);
}
this.fields.push({
name: name,
script: script,
type: type,
scripted: true,
lang: lang
});
this.save();
}
function indexFields(indexPattern, forceFieldRefresh = false) {
let promise = Promise.resolve();
if (!indexPattern.id) {
return promise;
}
if (forceFieldRefresh || isFieldRefreshRequired(indexPattern)) {
promise = indexPattern.refreshFields();
}
return promise.then(() => {
initFields(indexPattern);
removeScriptedField(name) {
const fieldIndex = _.findIndex(this.fields, {
name: name,
scripted: true
});
if(fieldIndex > -1) {
this.fields.splice(fieldIndex, 1);
delete this.fieldFormatMap[name];
return this.save();
}
}
function setId(indexPattern, id) {
indexPattern.id = id;
return id;
}
function setVersion(indexPattern, version) {
indexPattern.version = version;
return version;
}
function watch(indexPattern) {
if (configWatchers.has(indexPattern)) {
popularizeField(fieldName, unit = 1) {
const field = _.get(this, ['fields', 'byName', fieldName]);
if (!field) {
return;
}
const unwatch = config.watchAll(() => {
if (indexPattern.fields) {
initFields(indexPattern); // re-init fields when config changes, but only if we already had fields
}
});
configWatchers.set(indexPattern, { unwatch });
}
function unwatch(indexPattern) {
if (!configWatchers.has(indexPattern)) {
const count = Math.max((field.count || 0) + unit, 0);
if (field.count === count) {
return;
}
configWatchers.get(indexPattern).unwatch();
configWatchers.delete(indexPattern);
field.count = count;
this.save();
}
function initFields(indexPattern, input) {
const oldValue = indexPattern.fields;
const newValue = input || oldValue || [];
indexPattern.fields = new FieldList(indexPattern, newValue);
getNonScriptedFields() {
return _.where(this.fields, { scripted: false });
}
function fetchFields(indexPattern) {
return Promise.resolve()
.then(() => fieldsFetcher.fetch(indexPattern))
.then(fields => {
const scripted = indexPattern.getScriptedFields();
const all = fields.concat(scripted);
initFields(indexPattern, all);
});
getScriptedFields() {
return _.where(this.fields, { scripted: true });
}
class IndexPattern {
constructor(id) {
setId(this, id);
this.metaFields = config.get('metaFields');
this.getComputedFields = getComputedFields.bind(this);
this.flattenHit = flattenHit(this);
this.formatHit = formatHit(this, fieldformats.getDefaultInstance('string'));
this.formatField = this.formatHit.formatField;
getIndex() {
if (!this.isUnsupportedTimePattern()) {
return this.title;
}
get routes() {
return getRoutes();
}
// Take a time-based interval index pattern title (like [foo-]YYYY.MM.DD[-bar]) and turn it
// into the actual index (like foo-*-bar) by replacing anything not inside square brackets
// with a *.
const regex = /\[[^\]]*]/g; // Matches text inside brackets
const splits = this.title.split(regex); // e.g. ['', 'YYYY.MM.DD', ''] from the above example
const matches = this.title.match(regex); // e.g. ['[foo-]', '[-bar]'] from the above example
return splits.map((split, i) => {
const match = i >= matches.length ? '' : matches[i].replace(/[\[\]]/g, '');
return `${split.length ? '*' : ''}${match}`;
}).join('');
}
init(forceFieldRefresh = false) {
watch(this);
isTimeBased() {
return !!this.timeFieldName && (!this.fields || !!this.getTimeField());
}
if (!this.id) {
return Promise.resolve(this); // no id === no elasticsearch document
isTimeNanosBased() {
const timeField = this.getTimeField();
return timeField && timeField.esTypes && timeField.esTypes.indexOf('date_nanos') !== -1;
}
isUnsupportedTimePattern() {
return !!this.intervalName;
}
isTimeBasedWildcard() {
return this.isTimeBased() && this.isWildcard();
}
getTimeField() {
if (!this.timeFieldName || !this.fields || !this.fields.byName) return;
return this.fields.byName[this.timeFieldName];
}
isWildcard() {
return _.includes(this.title, '*');
}
prepBody() {
const body = {};
// serialize json fields
_.forOwn(this.mapping, (fieldMapping, fieldName) => {
if (this[fieldName] != null) {
body[fieldName] = (fieldMapping._serialize)
? fieldMapping._serialize(this[fieldName])
: this[fieldName];
}
});
// clear the indexPattern list cache
this.getIds.clearCache();
return body;
}
async create(allowOverride = false) {
const _create = async (duplicateId) => {
if (duplicateId) {
const duplicatePattern = new IndexPattern(duplicateId,
this.config,
this.savedObjectsClient,
this.patternCache,
this.fieldsFetcher,
this.getIds);
await duplicatePattern.destroy();
}
return savedObjectsClient.get(type, this.id)
.then(resp => {
// temporary compatability for savedObjectsClient
setVersion(this, resp._version);
return {
_id: resp.id,
_type: resp.type,
_source: _.cloneDeep(resp.attributes),
found: resp._version ? true : false
};
})
// Do this before we attempt to update from ES
// since that call can potentially perform a save
.then(response => {
this.originalBody = this.prepBody();
return response;
})
.then(response => updateFromElasticSearch(this, response, forceFieldRefresh))
// Do it after to ensure we have the most up to date information
.then(() => {
this.originalBody = this.prepBody();
})
.then(() => this);
}
migrate(newTitle) {
return savedObjectsClient.update(type, this.id, {
title: newTitle,
intervalName: null
}).then(({ attributes: { title, intervalName } }) => {
this.title = title;
this.intervalName = intervalName;
}).then(() => this);
}
// Get the source filtering configuration for that index.
getSourceFiltering() {
return {
excludes: this.sourceFilters && this.sourceFilters.map(filter => filter.value) || []
};
}
addScriptedField(name, script, type = 'string', lang) {
const scriptedFields = this.getScriptedFields();
const names = _.pluck(scriptedFields, 'name');
if (_.contains(names, name)) {
throw new DuplicateField(name);
}
this.fields.push({
name: name,
script: script,
type: type,
scripted: true,
lang: lang
});
this.save();
}
removeScriptedField(name) {
const fieldIndex = _.findIndex(this.fields, {
name: name,
scripted: true
});
if(fieldIndex > -1) {
this.fields.splice(fieldIndex, 1);
delete this.fieldFormatMap[name];
return this.save();
}
}
popularizeField(fieldName, unit = 1) {
const field = _.get(this, ['fields', 'byName', fieldName]);
if (!field) {
return;
}
const count = Math.max((field.count || 0) + unit, 0);
if (field.count === count) {
return;
}
field.count = count;
this.save();
}
getNonScriptedFields() {
return _.where(this.fields, { scripted: false });
}
getScriptedFields() {
return _.where(this.fields, { scripted: true });
}
getIndex() {
if (!this.isUnsupportedTimePattern()) {
return this.title;
}
// Take a time-based interval index pattern title (like [foo-]YYYY.MM.DD[-bar]) and turn it
// into the actual index (like foo-*-bar) by replacing anything not inside square brackets
// with a *.
const regex = /\[[^\]]*]/g; // Matches text inside brackets
const splits = this.title.split(regex); // e.g. ['', 'YYYY.MM.DD', ''] from the above example
const matches = this.title.match(regex); // e.g. ['[foo-]', '[-bar]'] from the above example
return splits.map((split, i) => {
const match = i >= matches.length ? '' : matches[i].replace(/[\[\]]/g, '');
return `${split.length ? '*' : ''}${match}`;
}).join('');
}
isTimeBased() {
return !!this.timeFieldName && (!this.fields || !!this.getTimeField());
}
isTimeNanosBased() {
const timeField = this.getTimeField();
return timeField && timeField.esTypes && timeField.esTypes.indexOf('date_nanos') !== -1;
}
isUnsupportedTimePattern() {
return !!this.intervalName;
}
isTimeBasedWildcard() {
return this.isTimeBased() && this.isWildcard();
}
getTimeField() {
if (!this.timeFieldName || !this.fields || !this.fields.byName) return;
return this.fields.byName[this.timeFieldName];
}
isWildcard() {
return _.includes(this.title, '*');
}
prepBody() {
const body = {};
// serialize json fields
_.forOwn(mapping, (fieldMapping, fieldName) => {
if (this[fieldName] != null) {
body[fieldName] = (fieldMapping._serialize)
? fieldMapping._serialize(this[fieldName])
: this[fieldName];
}
});
// clear the indexPattern list cache
getIds.clearCache();
return body;
}
async create(allowOverride = false) {
const _create = async (duplicateId) => {
if (duplicateId) {
const duplicatePattern = new IndexPattern(duplicateId);
await duplicatePattern.destroy();
}
const body = this.prepBody();
const response = await savedObjectsClient.create(type, body, { id: this.id });
return setId(this, response.id);
};
const potentialDuplicateByTitle = await findObjectByTitle(savedObjectsClient, type, this.title);
// If there is potentially duplicate title, just create it
if (!potentialDuplicateByTitle) {
return await _create();
}
// We found a duplicate but we aren't allowing override, show the warn modal
if (!allowOverride) {
return false;
}
return await _create(potentialDuplicateByTitle.id);
}
save(saveAttempts = 0) {
const body = this.prepBody();
// What keys changed since they last pulled the index pattern
const originalChangedKeys = Object.keys(body).filter(key => body[key] !== this.originalBody[key]);
return savedObjectsClient.update(type, this.id, body, { version: this.version })
.then(({ id, _version }) => {
setId(this, id);
setVersion(this, _version);
})
.catch(err => {
if (_.get(err, 'res.status') === 409 && saveAttempts++ < MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS) {
const samePattern = new IndexPattern(this.id);
return samePattern.init()
.then(() => {
// What keys changed from now and what the server returned
const updatedBody = samePattern.prepBody();
const response = await this.savedObjectsClient.create(type, body, { id: this.id });
// Build a list of changed keys from the server response
// and ensure we ignore the key if the server response
// is the same as the original response (since that is expected
// if we made a change in that key)
const serverChangedKeys = Object.keys(updatedBody).filter(key => {
return updatedBody[key] !== body[key] && this.originalBody[key] !== updatedBody[key];
});
this._setId(response.id);
return response.id;
};
let unresolvedCollision = false;
for (const originalKey of originalChangedKeys) {
for (const serverKey of serverChangedKeys) {
if (originalKey === serverKey) {
unresolvedCollision = true;
break;
}
const potentialDuplicateByTitle = await findObjectByTitle(this.savedObjectsClient, type, this.title);
// If there is potentially duplicate title, just create it
if (!potentialDuplicateByTitle) {
return await _create();
}
// We found a duplicate but we aren't allowing override, show the warn modal
if (!allowOverride) {
return false;
}
return await _create(potentialDuplicateByTitle.id);
}
save(saveAttempts = 0) {
const body = this.prepBody();
// What keys changed since they last pulled the index pattern
const originalChangedKeys = Object.keys(body).filter(key => body[key] !== this.originalBody[key]);
return this.savedObjectsClient.update(type, this.id, body, { version: this.version })
.then(({ id, _version }) => {
this._setId(id);
this._setVersion(_version);
})
.catch(err => {
if (_.get(err, 'res.status') === 409 && saveAttempts++ < MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS) {
const samePattern = new IndexPattern(this.id,
this.config,
this.savedObjectsClient,
this.patternCache,
this.fieldsFetcher,
this.getIds);
return samePattern.init()
.then(() => {
// What keys changed from now and what the server returned
const updatedBody = samePattern.prepBody();
// Build a list of changed keys from the server response
// and ensure we ignore the key if the server response
// is the same as the original response (since that is expected
// if we made a change in that key)
const serverChangedKeys = Object.keys(updatedBody).filter(key => {
return updatedBody[key] !== body[key] && this.originalBody[key] !== updatedBody[key];
});
let unresolvedCollision = false;
for (const originalKey of originalChangedKeys) {
for (const serverKey of serverChangedKeys) {
if (originalKey === serverKey) {
unresolvedCollision = true;
break;
}
}
}
if (unresolvedCollision) {
const message = i18n.translate(
'common.ui.indexPattern.unableWriteLabel',
{ defaultMessage: 'Unable to write index pattern! Refresh the page to get the most up to date changes for this index pattern.' } // eslint-disable-line max-len
);
toastNotifications.addDanger(message);
throw err;
}
if (unresolvedCollision) {
const message = i18n.translate(
'common.ui.indexPattern.unableWriteLabel',
{ defaultMessage: 'Unable to write index pattern! Refresh the page to get the most up to date changes for this index pattern.' } // eslint-disable-line max-len
);
toastNotifications.addDanger(message);
throw err;
}
// Set the updated response on this object
serverChangedKeys.forEach(key => {
this[key] = samePattern[key];
});
setVersion(this, samePattern.version);
// Clear cache
patternCache.clear(this.id);
// Try the save again
return this.save(saveAttempts);
// Set the updated response on this object
serverChangedKeys.forEach(key => {
this[key] = samePattern[key];
});
}
throw err;
});
}
refreshFields() {
return fetchFields(this)
.then(() => this.save())
.catch((err) => {
// https://github.com/elastic/kibana/issues/9224
// This call will attempt to remap fields from the matching
// ES index which may not actually exist. In that scenario,
// we still want to notify the user that there is a problem
// but we do not want to potentially make any pages unusable
// so do not rethrow the error here
if (err instanceof IndexPatternMissingIndices) {
toastNotifications.addDanger(err.message);
return [];
}
this._setVersion(samePattern.version);
toastNotifications.addError(err, {
title: i18n.translate('common.ui.indexPattern.fetchFieldErrorTitle', {
defaultMessage: 'Error fetching fields',
}),
});
throw err;
});
}
// Clear cache
this.patternCache.clear(this.id);
toJSON() {
return this.id;
}
toString() {
return '' + this.toJSON();
}
destroy() {
unwatch(this);
patternCache.clear(this.id);
return savedObjectsClient.delete(type, this.id);
}
// Try the save again
return this.save(saveAttempts);
});
}
throw err;
});
}
return IndexPattern;
async _fetchFields() {
const fields = await this.fieldsFetcher.fetch(this);
const scripted = this.getScriptedFields();
const all = fields.concat(scripted);
await this._initFields(all);
}
refreshFields() {
return this._fetchFields()
.then(() => this.save())
.catch((err) => {
// https://github.com/elastic/kibana/issues/9224
// This call will attempt to remap fields from the matching
// ES index which may not actually exist. In that scenario,
// we still want to notify the user that there is a problem
// but we do not want to potentially make any pages unusable
// so do not rethrow the error here
if (err instanceof IndexPatternMissingIndices) {
toastNotifications.addDanger(err.message);
return [];
}
toastNotifications.addError(err, {
title: i18n.translate('common.ui.indexPattern.fetchFieldErrorTitle', {
defaultMessage: 'Error fetching fields',
}),
});
throw err;
});
}
toJSON() {
return this.id;
}
toString() {
return '' + this.toJSON();
}
destroy() {
this.patternCache.clear(this.id);
return this.savedObjectsClient.delete(type, this.id);
}
}

View file

@ -17,32 +17,35 @@
* under the License.
*/
export function IndexPatternsPatternCacheProvider() {
export function createIndexPatternCache() {
const vals = {};
const cache = {};
const validId = function (id) {
return typeof id !== 'object';
};
this.get = function (id) {
cache.get = function (id) {
if (validId(id)) return vals[id];
};
this.set = function (id, prom) {
cache.set = function (id, prom) {
if (validId(id)) vals[id] = prom;
return prom;
};
this.clear = this.delete = function (id) {
cache.clear = cache.delete = function (id) {
if (validId(id)) delete vals[id];
};
this.clearAll = function () {
cache.clearAll = function () {
for (const id in vals) {
if (vals.hasOwnProperty(id)) {
delete vals[id];
}
}
};
return cache;
}

View file

@ -0,0 +1,67 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { KbnError } from 'ui/errors';
/**
* 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);
}
}
/**
* Tried to call a method that relies on SearchSource having an indexPattern assigned
*/
export class IndexPatternMissingIndices extends KbnError {
constructor(message) {
const defaultMessage = 'IndexPattern\'s configured pattern does not match any indices';
super(
(message && message.length) ? `No matching indices found: ${message}` : defaultMessage,
IndexPatternMissingIndices);
}
}
/**
* Tried to call a method that relies on SearchSource having an indexPattern assigned
*/
export class NoDefinedIndexPatterns extends KbnError {
constructor() {
super(
'Define at least one index pattern to continue',
NoDefinedIndexPatterns);
}
}
/**
* Tried to load a route besides management/kibana/index but you don't have a default index pattern!
*/
export class NoDefaultIndexPattern extends KbnError {
constructor() {
super(
'Please specify a default index pattern',
NoDefaultIndexPattern);
}
}

View file

@ -17,24 +17,25 @@
* under the License.
*/
export function createFieldsFetcher(apiClient, config) {
class FieldsFetcher {
fetch(indexPattern) {
return this.fetchForWildcard(indexPattern.getIndex(), {
type: indexPattern.type,
params: indexPattern.typeMeta && indexPattern.typeMeta.params,
});
}
fetchForWildcard(indexPatternId, options = {}) {
return apiClient.getFieldsForWildcard({
pattern: indexPatternId,
metaFields: config.get('metaFields'),
type: options.type,
params: options.params || {},
});
}
export class FieldsFetcher {
constructor(apiClient, metaFields) {
this.apiClient = apiClient;
this.metaFields = metaFields;
}
fetch(indexPattern, options) {
return this.fetchForWildcard(indexPattern.title, {
...options,
type: indexPattern.type,
params: indexPattern.typeMeta && indexPattern.typeMeta.params,
});
}
return new FieldsFetcher();
fetchForWildcard(indexPatternId, options = {}) {
return this.apiClient.getFieldsForWildcard({
pattern: indexPatternId,
metaFields: this.metaFields,
type: options.type,
params: options.params || {},
});
}
}

View file

@ -1,26 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { createFieldsFetcher } from './fields_fetcher';
import { IndexPatternsApiClientProvider } from './index_patterns_api_client_provider';
export function FieldsFetcherProvider(Private, config) {
const apiClient = Private(IndexPatternsApiClientProvider);
return createFieldsFetcher(apiClient, config);
}

View file

@ -17,9 +17,12 @@
* under the License.
*/
import chrome from '../chrome';
import { createIndexPatternsApiClient } from './index_patterns_api_client';
export function IndexPatternsApiClientProvider($http) {
return createIndexPatternsApiClient($http, chrome.getBasePath());
export function getRoutes() {
return {
edit: '/management/kibana/index_patterns/{{id}}',
addField: '/management/kibana/index_patterns/{{id}}/create-field',
indexedFields: '/management/kibana/index_patterns/{{id}}?_a=(tab:indexedFields)',
scriptedFields: '/management/kibana/index_patterns/{{id}}?_a=(tab:scriptedFields)',
sourceFilters: '/management/kibana/index_patterns/{{id}}?_a=(tab:sourceFilters)'
};
}

View file

@ -19,19 +19,11 @@
export { IndexPatternSelect } from './components/index_pattern_select';
export { IndexPatternsProvider } from './index_patterns';
export {
IndexPatternsApiClientProvider,
} from './index_patterns_api_client_provider';
export { IndexPatterns, IndexPatternsProvider } from './index_patterns';
export {
INDEX_PATTERN_ILLEGAL_CHARACTERS,
INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE,
} from './constants';
export {
ILLEGAL_CHARACTERS,
CONTAINS_SPACES,
validateIndexPattern,
} from './validate';
export { validateIndexPattern, CONTAINS_SPACES, ILLEGAL_CHARACTERS } from './validate';

View file

@ -17,58 +17,78 @@
* under the License.
*/
import { IndexPatternMissingIndices } from '../errors';
import { IndexPatternProvider } from './_index_pattern';
import { IndexPatternsPatternCacheProvider } from './_pattern_cache';
import { IndexPatternsGetProvider } from './_get';
import { FieldsFetcherProvider } from './fields_fetcher_provider';
import { fieldFormats } from '../registry/field_formats';
import { uiModules } from '../modules';
const module = uiModules.get('kibana/index_patterns');
export function IndexPatternsProvider(Private, config) {
const self = this;
import { IndexPatternMissingIndices } from './errors';
import { IndexPattern } from './_index_pattern';
import { createIndexPatternCache } from './_pattern_cache';
import { indexPatternsGetProvider } from './_get';
import { FieldsFetcher } from './fields_fetcher';
import { IndexPatternsApiClient } from './index_patterns_api_client';
const IndexPattern = Private(IndexPatternProvider);
const patternCache = Private(IndexPatternsPatternCacheProvider);
const getProvider = Private(IndexPatternsGetProvider);
export class IndexPatterns {
constructor(config, savedObjectsClient) {
const getProvider = indexPatternsGetProvider(savedObjectsClient);
const apiClient = new IndexPatternsApiClient();
this.config = config;
this.savedObjectsClient = savedObjectsClient;
this.errors = {
MissingIndices: IndexPatternMissingIndices
};
this.fieldsFetcher = new FieldsFetcher(apiClient, config.get('metaFields'));
this.cache = createIndexPatternCache();
this.getIds = getProvider('id');
this.getTitles = getProvider('attributes.title');
this.getFields = getProvider.multiple;
this.fieldFormats = fieldFormats;
}
self.get = function (id) {
if (!id) return self.make();
get = (id) => {
if (!id) return this.make();
const cache = patternCache.get(id);
return cache || patternCache.set(id, self.make(id));
const cache = this.cache.get(id);
return cache || this.cache.set(id, this.make(id));
};
self.getDefault = async () => {
const defaultIndexPatternId = config.get('defaultIndex');
getDefault = async () => {
const defaultIndexPatternId = this.config.get('defaultIndex');
if (defaultIndexPatternId) {
return await self.get(defaultIndexPatternId);
return await this.get(defaultIndexPatternId);
}
return null;
};
self.make = function (id) {
return (new IndexPattern(id)).init();
make = (id) => {
return (new IndexPattern(id,
this.config,
this.savedObjectsClient,
this.cache,
this.fieldsFetcher,
this.getIds,
)).init();
};
self.delete = function (pattern) {
self.getIds.clearCache();
delete = (pattern) => {
this.getIds.clearCache();
return pattern.destroy();
};
self.errors = {
MissingIndices: IndexPatternMissingIndices
};
self.cache = patternCache;
self.getIds = getProvider('id');
self.getTitles = getProvider('attributes.title');
self.getFields = getProvider.multiple;
self.fieldsFetcher = Private(FieldsFetcherProvider);
self.fieldFormats = fieldFormats;
}
module.service('indexPatterns', Private => Private(IndexPatternsProvider));
// add angular service for backward compatibility
import { uiModules } from '../modules';
const module = uiModules.get('kibana/index_patterns');
let _service;
module.service('indexPatterns', function (chrome) {
if (!_service) _service = new IndexPatterns(chrome.getUiSettingsClient(), chrome.getSavedObjectsClient());
return _service;
});
export const IndexPatternsProvider = (chrome) => {
if (!_service) _service = new IndexPatterns(chrome.getUiSettingsClient(), chrome.getSavedObjectsClient());
return _service;
};

View file

@ -17,96 +17,85 @@
* under the License.
*/
import { resolve as resolveUrl, format as formatUrl } from 'url';
import { kfetch } from '../kfetch';
import { pick, mapValues } from 'lodash';
import { IndexPatternMissingIndices } from './errors';
import { IndexPatternMissingIndices } from '../errors';
function join(...uriComponents) {
return uriComponents.filter(Boolean).map(encodeURIComponent).join('/');
}
export function createIndexPatternsApiClient($http, basePath) {
const apiBaseUrl = `${basePath}/api/index_patterns/`;
function join(...uriComponents) {
return uriComponents.filter(Boolean).map(encodeURIComponent).join('/');
}
function getUrl(path, query) {
const noNullsQuery = pick(query, value => value != null);
const noArraysQuery = mapValues(noNullsQuery, value => (
Array.isArray(value) ? JSON.stringify(value) : value
));
return resolveUrl(apiBaseUrl, formatUrl({
pathname: join(...path),
query: noArraysQuery,
}));
}
function request(method, url, body) {
return $http({
method,
url,
data: body,
})
.then(resp => resp.data)
.catch((resp) => {
// convert $http errors into actual error objects
const respBody = resp.data;
if (resp.status === 404 && respBody.code === 'no_matching_indices') {
throw new IndexPatternMissingIndices(respBody.message);
}
const err = new Error(respBody.message || respBody.error || `${resp.status} Response`);
err.status = resp.status;
err.body = respBody;
throw err;
});
}
class IndexPatternsApiClient {
getFieldsForTimePattern(options = {}) {
const {
pattern,
lookBack,
metaFields,
} = options;
const url = getUrl(['_fields_for_time_pattern'], {
pattern,
look_back: lookBack,
meta_fields: metaFields,
});
return request('GET', url).then(resp => resp.fields);
}
getFieldsForWildcard(options = {}) {
const {
pattern,
metaFields,
type,
params,
} = options;
let url;
if(type) {
url = getUrl([type, '_fields_for_wildcard'], {
pattern,
meta_fields: metaFields,
params: JSON.stringify(params),
});
} else {
url = getUrl(['_fields_for_wildcard'], {
pattern,
meta_fields: metaFields,
});
function request(method, url, query, body) {
return kfetch({
method,
pathname: url,
query,
body,
})
.catch((resp) => {
if (resp.body.statusCode === 404 && resp.body.statuscode === 'no_matching_indices') {
throw new IndexPatternMissingIndices(resp.body.message);
}
return request('GET', url).then(resp => resp.fields);
}
const err = new Error(resp.body.message || resp.body.error || `${resp.body.statusCode} Response`);
err.status = resp.body.statusCode;
err.body = resp.body.message;
throw err;
});
}
export class IndexPatternsApiClient {
constructor() {
this.apiBaseUrl = `/api/index_patterns/`;
}
return new IndexPatternsApiClient();
_getUrl(path) {
return this.apiBaseUrl + join(path);
}
getFieldsForTimePattern(options = {}) {
const {
pattern,
lookBack,
metaFields,
} = options;
const url = this._getUrl(['_fields_for_time_pattern']);
return request('GET', url, {
pattern,
look_back: lookBack,
meta_fields: metaFields,
}).then(resp => resp.fields);
}
getFieldsForWildcard(options = {}) {
const {
pattern,
metaFields,
type,
params,
} = options;
let url;
let query;
if(type) {
url = this._getUrl([type, '_fields_for_wildcard']);
query = {
pattern,
meta_fields: metaFields,
params: JSON.stringify(params),
};
} else {
url = this._getUrl(['_fields_for_wildcard']);
query = {
pattern,
meta_fields: metaFields,
};
}
return request('GET', url, query).then(resp => resp.fields);
}
}

View file

@ -26,11 +26,11 @@ import {
} from './validate_index';
jest.mock('ui/index_patterns/index_patterns.js', () => ({
IndexPatternsProvider: jest.fn(),
IndexPatterns: jest.fn(),
}));
jest.mock('ui/index_patterns/index_patterns_api_client_provider.js', () => ({
IndexPatternsApiClientProvider: jest.fn(),
jest.mock('ui/index_patterns/index_patterns_api_client.js', () => ({
IndexPatternsApiClient: jest.fn(),
}));
describe('Index name validation', () => {

View file

@ -23,16 +23,18 @@ import sinon from 'sinon';
import BluebirdPromise from 'bluebird';
import { SavedObjectProvider } from '../saved_object';
import { IndexPatternProvider } from '../../index_patterns/_index_pattern';
import { IndexPattern } from '../../index_patterns/_index_pattern';
import { SavedObjectsClientProvider } from '../saved_objects_client_provider';
import { StubIndexPatternsApiClientModule } from '../../index_patterns/__tests__/stub_index_patterns_api_client';
import { InvalidJSONProperty } from '../../errors';
const configMock = {
get: cfg => cfg
};
describe('Saved Object', function () {
require('test_utils/no_digest_promises').activateForSuite();
let SavedObject;
let IndexPattern;
let esDataStub;
let savedObjectsClientStub;
let window;
@ -87,7 +89,6 @@ describe('Saved Object', function () {
beforeEach(ngMock.module(
'kibana',
StubIndexPatternsApiClientModule,
// Use the native window.confirm instead of our specialized version to make testing
// this easier.
function ($provide) {
@ -98,7 +99,6 @@ describe('Saved Object', function () {
beforeEach(ngMock.inject(function (es, Private, $window) {
SavedObject = Private(SavedObjectProvider);
IndexPattern = Private(IndexPatternProvider);
esDataStub = es;
savedObjectsClientStub = Private(SavedObjectsClientProvider);
window = $window;
@ -339,7 +339,9 @@ describe('Saved Object', function () {
type: 'dashboard',
});
});
savedObject.searchSource.setField('index', new IndexPattern('my-index', null, []));
const indexPattern = new IndexPattern('my-index', configMock, null, []);
indexPattern.title = indexPattern.id;
savedObject.searchSource.setField('index', indexPattern);
return savedObject
.save()
.then(() => {
@ -690,6 +692,12 @@ describe('Saved Object', function () {
});
const savedObject = new SavedObject(config);
sinon.stub(savedObject, 'hydrateIndexPattern').callsFake(() => {
const indexPattern = new IndexPattern(indexPatternId, configMock, null, []);
indexPattern.title = indexPattern.id;
savedObject.searchSource.setField('index', indexPattern);
return Promise.resolve(indexPattern);
});
expect(!!savedObject.searchSource.getField('index')).to.be(false);
return savedObject.init().then(() => {

View file

@ -32,7 +32,7 @@ import angular from 'angular';
import _ from 'lodash';
import { InvalidJSONProperty, SavedObjectNotFound } from '../errors';
import MappingSetupProvider from '../utils/mapping_setup';
import { expandShorthand } from '../utils/mapping_setup';
import { SearchSourceProvider } from '../courier/search_source';
import { findObjectByTitle } from './find_object_by_title';
@ -69,7 +69,6 @@ function isErrorNonFatal(error) {
export function SavedObjectProvider(Promise, Private, confirmModalPromise, indexPatterns) {
const savedObjectsClient = Private(SavedObjectsClientProvider);
const SearchSource = Private(SearchSourceProvider);
const mappingSetup = Private(MappingSetupProvider);
/**
* The SavedObject class is a base class for saved objects loaded from the server and
@ -109,7 +108,7 @@ export function SavedObjectProvider(Promise, Private, confirmModalPromise, index
this.defaults = config.defaults || {};
// mapping definition for the fields that this object will expose
const mapping = mappingSetup.expandShorthand(config.mapping);
const mapping = expandShorthand(config.mapping);
const afterESResp = config.afterESResp || _.noop;
const customInit = config.init || _.noop;

View file

@ -20,40 +20,35 @@
import _ from 'lodash';
import ngMock from 'ng_mock';
import expect from '@kbn/expect';
import UtilsMappingSetupProvider from '../mapping_setup';
let mappingSetup;
import { expandShorthand } from '../mapping_setup';
describe('ui/utils/mapping_setup', function () {
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
mappingSetup = Private(UtilsMappingSetupProvider);
}));
describe('#expandShorthand()', function () {
it('allows shortcuts for field types by just setting the value to the type name', function () {
const mapping = mappingSetup.expandShorthand({ foo: 'boolean' });
const mapping = expandShorthand({ foo: 'boolean' });
expect(mapping.foo.type).to.be('boolean');
});
it('can set type as an option', function () {
const mapping = mappingSetup.expandShorthand({ foo: { type: 'integer' } });
const mapping = expandShorthand({ foo: { type: 'integer' } });
expect(mapping.foo.type).to.be('integer');
});
describe('when type is json', function () {
it('returned object is type text', function () {
const mapping = mappingSetup.expandShorthand({ foo: 'json' });
const mapping = expandShorthand({ foo: 'json' });
expect(mapping.foo.type).to.be('text');
});
it('returned object has _serialize function', function () {
const mapping = mappingSetup.expandShorthand({ foo: 'json' });
const mapping = expandShorthand({ foo: 'json' });
expect(_.isFunction(mapping.foo._serialize)).to.be(true);
});
it('returned object has _deserialize function', function () {
const mapping = mappingSetup.expandShorthand({ foo: 'json' });
const mapping = expandShorthand({ foo: 'json' });
expect(_.isFunction(mapping.foo._serialize)).to.be(true);
});
});

View file

@ -17,35 +17,29 @@
* under the License.
*/
import angular from 'angular';
import _ from 'lodash';
import { mapValues } from 'lodash';
// eslint-disable-next-line import/no-default-export
export default function MappingSetupService() {
const mappingSetup = this;
const json = {
_serialize: function (val) {
if (val != null) return JSON.stringify(val);
},
_deserialize: function (val) {
if (val != null) return JSON.parse(val);
}
};
const json = {
_serialize: function (val) {
if (val != null) return angular.toJson(val);
},
_deserialize: function (val) {
if (val != null) return JSON.parse(val);
export const expandShorthand = function (sh) {
return mapValues(sh || {}, function (val) {
// allow shortcuts for the field types, by just setting the value
// to the type name
if (typeof val === 'string') val = { type: val };
if (val.type === 'json') {
val.type = 'text';
val._serialize = json._serialize;
val._deserialize = json._deserialize;
}
};
mappingSetup.expandShorthand = function (sh) {
return _.mapValues(sh || {}, function (val) {
// allow shortcuts for the field types, by just setting the value
// to the type name
if (typeof val === 'string') val = { type: val };
if (val.type === 'json') {
val.type = 'text';
val._serialize = json._serialize;
val._deserialize = json._deserialize;
}
return val;
});
};
}
return val;
});
};

View file

@ -18,17 +18,15 @@
*/
import sinon from 'sinon';
import { IndexPatternProvider, getRoutes } from 'ui/index_patterns/_index_pattern';
import { formatHit } from 'ui/index_patterns/_format_hit';
import { IndexPattern } from 'ui/index_patterns/_index_pattern';
import { getRoutes } from 'ui/index_patterns/get_routes';
import { formatHitProvider } from 'ui/index_patterns/_format_hit';
import { getComputedFields } from 'ui/index_patterns/_get_computed_fields';
import { fieldFormats } from 'ui/registry/field_formats';
import { IndexPatternsFlattenHitProvider } from 'ui/index_patterns/_flatten_hit';
import { flattenHitWrapper } from 'ui/index_patterns/_flatten_hit';
import { FieldList } from 'ui/index_patterns/_field_list';
export default function (Private) {
const flattenHit = Private(IndexPatternsFlattenHitProvider);
const IndexPattern = Private(IndexPatternProvider);
export default function () {
function StubIndexPattern(pattern, timeField, fields) {
this.id = pattern;
@ -45,8 +43,9 @@ export default function (Private) {
this.getIndex = () => pattern;
this.getComputedFields = getComputedFields.bind(this);
this.flattenHit = flattenHit(this);
this.formatHit = formatHit(this, fieldFormats.getDefaultInstance('string'));
this.flattenHit = flattenHitWrapper(this, this.metaFields);
this.formatHit = formatHitProvider(this, fieldFormats.getDefaultInstance('string'));
this.fieldsFetcher = { apiClient: { baseUrl: '' } };
this.formatField = this.formatHit.formatField;
this._reindexFields = function () {

View file

@ -12,11 +12,11 @@ jest.mock('../services/auto_follow_pattern_validators', () => ({
}));
jest.mock('ui/index_patterns/index_patterns.js', () => ({
IndexPatternsProvider: jest.fn(),
IndexPatterns: jest.fn(),
}));
jest.mock('ui/index_patterns/index_patterns_api_client_provider.js', () => ({
IndexPatternsApiClientProvider: jest.fn(),
jest.mock('ui/index_patterns/index_patterns_api_client.js', () => ({
IndexPatternsApiClient: jest.fn(),
}));
describe('<AutoFollowPatternForm state update', () => {

View file

@ -8,11 +8,11 @@
import { validateAutoFollowPattern } from './auto_follow_pattern_validators';
jest.mock('ui/index_patterns/index_patterns.js', () => ({
IndexPatternsProvider: jest.fn(),
IndexPatterns: jest.fn(),
}));
jest.mock('ui/index_patterns/index_patterns_api_client_provider.js', () => ({
IndexPatternsApiClientProvider: jest.fn(),
jest.mock('ui/index_patterns/index_patterns_api_client.js', () => ({
IndexPatternsApiClient: jest.fn(),
}));
describe('Auto-follow pattern validators', () => {

View file

@ -12,7 +12,7 @@ import {
convertMapExtentToPolygon,
} from './elasticsearch_geo_utils';
import { IndexPatternsFlattenHitProvider } from 'ui/index_patterns/_flatten_hit';
import { flattenHitWrapper } from 'ui/index_patterns/_flatten_hit';
const geoFieldName = 'location';
const mapExtent = {
@ -132,16 +132,6 @@ describe('hitsToGeoJson', () => {
});
describe('dot in geoFieldName', () => {
const configMock = {
get: (key) => {
if (key === 'metaFields') {
return [];
}
throw new Error(`Unexpected config key: ${key}`);
},
watch: () => {}
};
const flattenHitWrapper = IndexPatternsFlattenHitProvider(configMock); // eslint-disable-line new-cap
const indexPatternMock = {
fields: {
byName: {

View file

@ -467,7 +467,6 @@
"common.ui.flotCharts.thuLabel": "木",
"common.ui.flotCharts.tueLabel": "火",
"common.ui.flotCharts.wedLabel": "水",
"common.ui.indexPattern.bannerLabel": "Kibana でデータの可視化と閲覧を行うには、Elasticsearch からデータを取得するためのインデックスパターンの作成が必要です。",
"common.ui.indexPattern.editIndexPattern": "インデックスパターンを編集",
"common.ui.indexPattern.unableWriteLabel": "インデックスパターンを書き込めません!このインデックスパターンへの最新の変更を取得するにな、ページを更新してください。",
"common.ui.indexPattern.unknownFieldErrorMessage": "インデックスパターン「{title}」のフィールド「{name}」が不明なフィールドタイプを使用しています。",

View file

@ -466,7 +466,6 @@
"common.ui.flotCharts.thuLabel": "周四",
"common.ui.flotCharts.tueLabel": "周二",
"common.ui.flotCharts.wedLabel": "周三",
"common.ui.indexPattern.bannerLabel": "若要在 Kibana 中可视化和浏览数据,您需要创建索引模式,以从 Elasticsearch 检索数据。",
"common.ui.indexPattern.editIndexPattern": "编辑索引模式",
"common.ui.indexPattern.unableWriteLabel": "无法写入索引模式!请刷新页面以获取此索引模式的最新更改。",
"common.ui.indexPattern.unknownFieldErrorMessage": "indexPattern “{title}” 中的字段 “{name}” 使用未知字段类型。",
@ -9980,4 +9979,4 @@
"xpack.watcher.watchActionsTitle": "满足后将执行 {watchActionsCount, plural, one{# 个操作} other {# 个操作}}",
"xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。"
}
}
}