mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
666 lines
18 KiB
TypeScript
666 lines
18 KiB
TypeScript
/*
|
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
* or more contributor license agreements. Licensed under the Elastic License;
|
|
* you may not use this file except in compliance with the Elastic License.
|
|
*/
|
|
|
|
import { SavedObjectsClientContract } from 'kibana/public';
|
|
import _ from 'lodash';
|
|
import {
|
|
loadInitialState,
|
|
loadIndexPatterns,
|
|
changeIndexPattern,
|
|
changeLayerIndexPattern,
|
|
syncExistingFields,
|
|
} from './loader';
|
|
import { IndexPatternPersistedState, IndexPatternPrivateState } from './types';
|
|
import { documentField } from './document_field';
|
|
|
|
jest.mock('./operations');
|
|
|
|
const createMockStorage = (lastData?: Record<string, string>) => {
|
|
return {
|
|
get: jest.fn().mockImplementation(() => lastData),
|
|
set: jest.fn(),
|
|
remove: jest.fn(),
|
|
clear: jest.fn(),
|
|
};
|
|
};
|
|
|
|
const sampleIndexPatterns = {
|
|
a: {
|
|
id: 'a',
|
|
title: 'my-fake-index-pattern',
|
|
timeFieldName: 'timestamp',
|
|
fields: [
|
|
{
|
|
name: 'timestamp',
|
|
type: 'date',
|
|
aggregatable: true,
|
|
searchable: true,
|
|
},
|
|
{
|
|
name: 'start_date',
|
|
type: 'date',
|
|
aggregatable: true,
|
|
searchable: true,
|
|
},
|
|
{
|
|
name: 'bytes',
|
|
type: 'number',
|
|
aggregatable: true,
|
|
searchable: true,
|
|
},
|
|
{
|
|
name: 'memory',
|
|
type: 'number',
|
|
aggregatable: true,
|
|
searchable: true,
|
|
},
|
|
{
|
|
name: 'source',
|
|
type: 'string',
|
|
aggregatable: true,
|
|
searchable: true,
|
|
esTypes: ['keyword'],
|
|
},
|
|
{
|
|
name: 'dest',
|
|
type: 'string',
|
|
aggregatable: true,
|
|
searchable: true,
|
|
esTypes: ['keyword'],
|
|
},
|
|
documentField,
|
|
],
|
|
},
|
|
b: {
|
|
id: 'b',
|
|
title: 'my-fake-restricted-pattern',
|
|
timeFieldName: 'timestamp',
|
|
fields: [
|
|
{
|
|
name: 'timestamp',
|
|
type: 'date',
|
|
aggregatable: true,
|
|
searchable: true,
|
|
aggregationRestrictions: {
|
|
date_histogram: {
|
|
agg: 'date_histogram',
|
|
fixed_interval: '1d',
|
|
delay: '7d',
|
|
time_zone: 'UTC',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: 'bytes',
|
|
type: 'number',
|
|
aggregatable: true,
|
|
searchable: true,
|
|
aggregationRestrictions: {
|
|
// Ignored in the UI
|
|
histogram: {
|
|
agg: 'histogram',
|
|
interval: 1000,
|
|
},
|
|
avg: {
|
|
agg: 'avg',
|
|
},
|
|
max: {
|
|
agg: 'max',
|
|
},
|
|
min: {
|
|
agg: 'min',
|
|
},
|
|
sum: {
|
|
agg: 'sum',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: 'source',
|
|
type: 'string',
|
|
aggregatable: false,
|
|
searchable: false,
|
|
scripted: true,
|
|
aggregationRestrictions: {
|
|
terms: {
|
|
agg: 'terms',
|
|
},
|
|
},
|
|
esTypes: ['keyword'],
|
|
},
|
|
documentField,
|
|
],
|
|
},
|
|
};
|
|
|
|
function indexPatternSavedObject({ id }: { id: keyof typeof sampleIndexPatterns }) {
|
|
const pattern = {
|
|
...sampleIndexPatterns[id],
|
|
fields: [
|
|
...sampleIndexPatterns[id].fields,
|
|
{
|
|
name: 'description',
|
|
type: 'string',
|
|
aggregatable: false,
|
|
searchable: true,
|
|
esTypes: ['text'],
|
|
},
|
|
],
|
|
};
|
|
return {
|
|
id,
|
|
type: 'index-pattern',
|
|
attributes: {
|
|
title: pattern.title,
|
|
timeFieldName: pattern.timeFieldName,
|
|
fields: JSON.stringify(pattern.fields.filter((f) => f.type !== 'document')),
|
|
},
|
|
};
|
|
}
|
|
|
|
function mockClient() {
|
|
return ({
|
|
find: jest.fn(async () => ({
|
|
savedObjects: [
|
|
{ id: 'a', attributes: { title: sampleIndexPatterns.a.title } },
|
|
{ id: 'b', attributes: { title: sampleIndexPatterns.b.title } },
|
|
],
|
|
})),
|
|
async bulkGet(indexPatterns: Array<{ id: keyof typeof sampleIndexPatterns }>) {
|
|
return {
|
|
savedObjects: indexPatterns.map(({ id }) => indexPatternSavedObject({ id })),
|
|
};
|
|
},
|
|
} as unknown) as Pick<SavedObjectsClientContract, 'find' | 'bulkGet'>;
|
|
}
|
|
|
|
describe('loader', () => {
|
|
describe('loadIndexPatterns', () => {
|
|
it('should not load index patterns that are already loaded', async () => {
|
|
const cache = await loadIndexPatterns({
|
|
cache: sampleIndexPatterns,
|
|
patterns: ['a', 'b'],
|
|
savedObjectsClient: {
|
|
bulkGet: jest.fn(() => Promise.reject('bulkGet should not have been called')),
|
|
find: jest.fn(() => Promise.reject('find should not have been called')),
|
|
},
|
|
});
|
|
|
|
expect(cache).toEqual(sampleIndexPatterns);
|
|
});
|
|
|
|
it('should load index patterns that are not loaded', async () => {
|
|
const cache = await loadIndexPatterns({
|
|
cache: {
|
|
b: sampleIndexPatterns.b,
|
|
},
|
|
patterns: ['a', 'b'],
|
|
savedObjectsClient: mockClient(),
|
|
});
|
|
|
|
expect(cache).toMatchObject(sampleIndexPatterns);
|
|
});
|
|
|
|
it('should allow scripted, but not full text fields', async () => {
|
|
const cache = await loadIndexPatterns({
|
|
cache: {},
|
|
patterns: ['a', 'b'],
|
|
savedObjectsClient: mockClient(),
|
|
});
|
|
|
|
expect(cache).toMatchObject(sampleIndexPatterns);
|
|
});
|
|
|
|
it('should apply field restrictions from typeMeta', async () => {
|
|
const cache = await loadIndexPatterns({
|
|
cache: {},
|
|
patterns: ['foo'],
|
|
savedObjectsClient: ({
|
|
...mockClient(),
|
|
async bulkGet() {
|
|
return {
|
|
savedObjects: [
|
|
{
|
|
id: 'foo',
|
|
type: 'index-pattern',
|
|
attributes: {
|
|
title: 'Foo index',
|
|
typeMeta: JSON.stringify({
|
|
aggs: {
|
|
date_histogram: {
|
|
timestamp: {
|
|
agg: 'date_histogram',
|
|
fixed_interval: 'm',
|
|
},
|
|
},
|
|
sum: {
|
|
bytes: {
|
|
agg: 'sum',
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
fields: JSON.stringify([
|
|
{
|
|
name: 'timestamp',
|
|
type: 'date',
|
|
aggregatable: true,
|
|
searchable: true,
|
|
},
|
|
{
|
|
name: 'bytes',
|
|
type: 'number',
|
|
aggregatable: true,
|
|
searchable: true,
|
|
},
|
|
]),
|
|
},
|
|
},
|
|
],
|
|
};
|
|
},
|
|
} as unknown) as Pick<SavedObjectsClientContract, 'find' | 'bulkGet'>,
|
|
});
|
|
|
|
expect(cache.foo.fields.find((f) => f.name === 'bytes')!.aggregationRestrictions).toEqual({
|
|
sum: { agg: 'sum' },
|
|
});
|
|
expect(cache.foo.fields.find((f) => f.name === 'timestamp')!.aggregationRestrictions).toEqual(
|
|
{
|
|
date_histogram: { agg: 'date_histogram', fixed_interval: 'm' },
|
|
}
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('loadInitialState', () => {
|
|
it('should load a default state', async () => {
|
|
const storage = createMockStorage();
|
|
const state = await loadInitialState({
|
|
savedObjectsClient: mockClient(),
|
|
storage,
|
|
});
|
|
|
|
expect(state).toMatchObject({
|
|
currentIndexPatternId: 'a',
|
|
indexPatternRefs: [
|
|
{ id: 'a', title: sampleIndexPatterns.a.title },
|
|
{ id: 'b', title: sampleIndexPatterns.b.title },
|
|
],
|
|
indexPatterns: {
|
|
a: sampleIndexPatterns.a,
|
|
},
|
|
layers: {},
|
|
});
|
|
expect(storage.set).toHaveBeenCalledWith('lens-settings', {
|
|
indexPatternId: 'a',
|
|
});
|
|
});
|
|
|
|
it('should load a default state when lastUsedIndexPatternId is not found in indexPatternRefs', async () => {
|
|
const storage = createMockStorage({ indexPatternId: 'c' });
|
|
const state = await loadInitialState({
|
|
savedObjectsClient: mockClient(),
|
|
storage,
|
|
});
|
|
|
|
expect(state).toMatchObject({
|
|
currentIndexPatternId: 'a',
|
|
indexPatternRefs: [
|
|
{ id: 'a', title: sampleIndexPatterns.a.title },
|
|
{ id: 'b', title: sampleIndexPatterns.b.title },
|
|
],
|
|
indexPatterns: {
|
|
a: sampleIndexPatterns.a,
|
|
},
|
|
layers: {},
|
|
});
|
|
expect(storage.set).toHaveBeenCalledWith('lens-settings', {
|
|
indexPatternId: 'a',
|
|
});
|
|
});
|
|
|
|
it('should load lastUsedIndexPatternId if in localStorage', async () => {
|
|
const state = await loadInitialState({
|
|
savedObjectsClient: mockClient(),
|
|
storage: createMockStorage({ indexPatternId: 'b' }),
|
|
});
|
|
|
|
expect(state).toMatchObject({
|
|
currentIndexPatternId: 'b',
|
|
indexPatternRefs: [
|
|
{ id: 'a', title: sampleIndexPatterns.a.title },
|
|
{ id: 'b', title: sampleIndexPatterns.b.title },
|
|
],
|
|
indexPatterns: {
|
|
b: sampleIndexPatterns.b,
|
|
},
|
|
layers: {},
|
|
});
|
|
});
|
|
|
|
it('should use the default index pattern id, if provided', async () => {
|
|
const storage = createMockStorage();
|
|
const state = await loadInitialState({
|
|
defaultIndexPatternId: 'b',
|
|
savedObjectsClient: mockClient(),
|
|
storage,
|
|
});
|
|
|
|
expect(state).toMatchObject({
|
|
currentIndexPatternId: 'b',
|
|
indexPatternRefs: [
|
|
{ id: 'a', title: sampleIndexPatterns.a.title },
|
|
{ id: 'b', title: sampleIndexPatterns.b.title },
|
|
],
|
|
indexPatterns: {
|
|
b: sampleIndexPatterns.b,
|
|
},
|
|
layers: {},
|
|
});
|
|
expect(storage.set).toHaveBeenCalledWith('lens-settings', {
|
|
indexPatternId: 'b',
|
|
});
|
|
});
|
|
|
|
it('should initialize from saved state', async () => {
|
|
const savedState: IndexPatternPersistedState = {
|
|
currentIndexPatternId: 'b',
|
|
layers: {
|
|
layerb: {
|
|
indexPatternId: 'b',
|
|
columnOrder: ['col1', 'col2'],
|
|
columns: {
|
|
col1: {
|
|
dataType: 'date',
|
|
isBucketed: true,
|
|
label: 'My date',
|
|
operationType: 'date_histogram',
|
|
params: {
|
|
interval: 'm',
|
|
},
|
|
sourceField: 'timestamp',
|
|
},
|
|
col2: {
|
|
dataType: 'number',
|
|
isBucketed: false,
|
|
label: 'Sum of bytes',
|
|
operationType: 'sum',
|
|
sourceField: 'bytes',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
const storage = createMockStorage({ indexPatternId: 'a' });
|
|
const state = await loadInitialState({
|
|
state: savedState,
|
|
savedObjectsClient: mockClient(),
|
|
storage,
|
|
});
|
|
|
|
expect(state).toMatchObject({
|
|
currentIndexPatternId: 'b',
|
|
indexPatternRefs: [
|
|
{ id: 'a', title: sampleIndexPatterns.a.title },
|
|
{ id: 'b', title: sampleIndexPatterns.b.title },
|
|
],
|
|
indexPatterns: {
|
|
b: sampleIndexPatterns.b,
|
|
},
|
|
layers: savedState.layers,
|
|
});
|
|
|
|
expect(storage.set).toHaveBeenCalledWith('lens-settings', {
|
|
indexPatternId: 'b',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('changeIndexPattern', () => {
|
|
it('loads the index pattern and then sets it as current', async () => {
|
|
const setState = jest.fn();
|
|
const state: IndexPatternPrivateState = {
|
|
currentIndexPatternId: 'b',
|
|
indexPatternRefs: [],
|
|
indexPatterns: {},
|
|
existingFields: {},
|
|
layers: {},
|
|
};
|
|
const storage = createMockStorage({ indexPatternId: 'b' });
|
|
|
|
await changeIndexPattern({
|
|
state,
|
|
setState,
|
|
id: 'a',
|
|
savedObjectsClient: mockClient(),
|
|
onError: jest.fn(),
|
|
storage,
|
|
});
|
|
|
|
expect(setState).toHaveBeenCalledTimes(1);
|
|
expect(setState.mock.calls[0][0](state)).toMatchObject({
|
|
currentIndexPatternId: 'a',
|
|
indexPatterns: {
|
|
a: sampleIndexPatterns.a,
|
|
},
|
|
});
|
|
expect(storage.set).toHaveBeenCalledWith('lens-settings', {
|
|
indexPatternId: 'a',
|
|
});
|
|
});
|
|
|
|
it('handles errors', async () => {
|
|
const setState = jest.fn();
|
|
const onError = jest.fn();
|
|
const err = new Error('NOPE!');
|
|
const state: IndexPatternPrivateState = {
|
|
currentIndexPatternId: 'b',
|
|
indexPatternRefs: [],
|
|
existingFields: {},
|
|
indexPatterns: {},
|
|
layers: {},
|
|
};
|
|
|
|
const storage = createMockStorage({ indexPatternId: 'b' });
|
|
|
|
await changeIndexPattern({
|
|
state,
|
|
setState,
|
|
id: 'a',
|
|
savedObjectsClient: {
|
|
...mockClient(),
|
|
bulkGet: jest.fn(async () => {
|
|
throw err;
|
|
}),
|
|
},
|
|
onError,
|
|
storage,
|
|
});
|
|
|
|
expect(setState).not.toHaveBeenCalled();
|
|
expect(storage.set).not.toHaveBeenCalled();
|
|
expect(onError).toHaveBeenCalledWith(err);
|
|
});
|
|
});
|
|
|
|
describe('changeLayerIndexPattern', () => {
|
|
it('loads the index pattern and then changes the specified layer', async () => {
|
|
const setState = jest.fn();
|
|
const state: IndexPatternPrivateState = {
|
|
currentIndexPatternId: 'b',
|
|
indexPatternRefs: [],
|
|
existingFields: {},
|
|
indexPatterns: {
|
|
a: sampleIndexPatterns.a,
|
|
},
|
|
layers: {
|
|
l0: {
|
|
columnOrder: ['col1'],
|
|
columns: {},
|
|
indexPatternId: 'a',
|
|
},
|
|
l1: {
|
|
columnOrder: ['col2'],
|
|
columns: {
|
|
col2: {
|
|
dataType: 'date',
|
|
isBucketed: true,
|
|
label: 'My hist',
|
|
operationType: 'date_histogram',
|
|
params: {
|
|
interval: 'm',
|
|
},
|
|
sourceField: 'timestamp',
|
|
},
|
|
},
|
|
indexPatternId: 'a',
|
|
},
|
|
},
|
|
};
|
|
|
|
const storage = createMockStorage({ indexPatternId: 'a' });
|
|
|
|
await changeLayerIndexPattern({
|
|
state,
|
|
setState,
|
|
indexPatternId: 'b',
|
|
layerId: 'l1',
|
|
savedObjectsClient: mockClient(),
|
|
onError: jest.fn(),
|
|
storage,
|
|
});
|
|
|
|
expect(setState).toHaveBeenCalledTimes(1);
|
|
expect(setState.mock.calls[0][0](state)).toMatchObject({
|
|
currentIndexPatternId: 'b',
|
|
indexPatterns: {
|
|
a: sampleIndexPatterns.a,
|
|
b: sampleIndexPatterns.b,
|
|
},
|
|
layers: {
|
|
l0: {
|
|
columnOrder: ['col1'],
|
|
columns: {},
|
|
indexPatternId: 'a',
|
|
},
|
|
l1: {
|
|
columnOrder: ['col2'],
|
|
columns: {
|
|
col2: {
|
|
dataType: 'date',
|
|
isBucketed: true,
|
|
label: 'My hist',
|
|
operationType: 'date_histogram',
|
|
params: {
|
|
interval: '1d',
|
|
},
|
|
sourceField: 'timestamp',
|
|
},
|
|
},
|
|
indexPatternId: 'b',
|
|
},
|
|
},
|
|
});
|
|
expect(storage.set).toHaveBeenCalledWith('lens-settings', {
|
|
indexPatternId: 'b',
|
|
});
|
|
});
|
|
|
|
it('handles errors', async () => {
|
|
const setState = jest.fn();
|
|
const onError = jest.fn();
|
|
const err = new Error('NOPE!');
|
|
const state: IndexPatternPrivateState = {
|
|
currentIndexPatternId: 'b',
|
|
indexPatternRefs: [],
|
|
existingFields: {},
|
|
indexPatterns: {
|
|
a: sampleIndexPatterns.a,
|
|
},
|
|
layers: {
|
|
l0: {
|
|
columnOrder: ['col1'],
|
|
columns: {},
|
|
indexPatternId: 'a',
|
|
},
|
|
},
|
|
};
|
|
|
|
const storage = createMockStorage({ indexPatternId: 'b' });
|
|
|
|
await changeLayerIndexPattern({
|
|
state,
|
|
setState,
|
|
indexPatternId: 'b',
|
|
layerId: 'l0',
|
|
savedObjectsClient: {
|
|
...mockClient(),
|
|
bulkGet: jest.fn(async () => {
|
|
throw err;
|
|
}),
|
|
},
|
|
onError,
|
|
storage,
|
|
});
|
|
|
|
expect(setState).not.toHaveBeenCalled();
|
|
expect(storage.set).not.toHaveBeenCalled();
|
|
expect(onError).toHaveBeenCalledWith(err);
|
|
});
|
|
});
|
|
|
|
describe('syncExistingFields', () => {
|
|
const dslQuery = {
|
|
bool: {
|
|
must: [],
|
|
filter: [{ match_all: {} }],
|
|
should: [],
|
|
must_not: [],
|
|
},
|
|
};
|
|
|
|
it('should call once for each index pattern', async () => {
|
|
const setState = jest.fn();
|
|
const fetchJson = jest.fn((path: string) => {
|
|
const indexPatternTitle = _.last(path.split('/'));
|
|
return {
|
|
indexPatternTitle,
|
|
existingFieldNames: ['field_1', 'field_2'].map(
|
|
(fieldName) => `${indexPatternTitle}_${fieldName}`
|
|
),
|
|
};
|
|
});
|
|
|
|
await syncExistingFields({
|
|
dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' },
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
fetchJson: fetchJson as any,
|
|
indexPatterns: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
setState,
|
|
dslQuery,
|
|
});
|
|
|
|
expect(fetchJson).toHaveBeenCalledTimes(3);
|
|
expect(setState).toHaveBeenCalledTimes(1);
|
|
|
|
const [fn] = setState.mock.calls[0];
|
|
const newState = fn({
|
|
foo: 'bar',
|
|
existingFields: {},
|
|
});
|
|
|
|
expect(newState).toEqual({
|
|
foo: 'bar',
|
|
existingFields: {
|
|
a: { a_field_1: true, a_field_2: true },
|
|
b: { b_field_1: true, b_field_2: true },
|
|
c: { c_field_1: true, c_field_2: true },
|
|
},
|
|
});
|
|
});
|
|
});
|
|
});
|