mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
Merge remote-tracking branch 'origin/master' into feature/merge-code
This commit is contained in:
commit
ef0e01e8a9
159 changed files with 5406 additions and 2222 deletions
|
@ -100,7 +100,7 @@
|
|||
"@babel/polyfill": "^7.2.5",
|
||||
"@babel/register": "^7.0.0",
|
||||
"@elastic/datemath": "5.0.2",
|
||||
"@elastic/eui": "9.8.0",
|
||||
"@elastic/eui": "9.9.0",
|
||||
"@elastic/filesaver": "1.1.2",
|
||||
"@elastic/good": "8.1.1-kibana2",
|
||||
"@elastic/numeral": "2.3.2",
|
||||
|
|
|
@ -11,7 +11,8 @@
|
|||
"kbn:watch": "node scripts/build --source-maps --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "npm:@elastic/lodash@3.10.1-kibana1"
|
||||
"lodash": "npm:@elastic/lodash@3.10.1-kibana1",
|
||||
"moment-timezone": "^0.5.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.2.3",
|
||||
|
|
|
@ -103,6 +103,36 @@ describe('build query', function () {
|
|||
expect(result).to.eql(expectedResult);
|
||||
});
|
||||
|
||||
it('should use the default time zone set in the Advanced Settings in queries and filters', function () {
|
||||
const queries = [
|
||||
{ query: '@timestamp:"2019-03-23T13:18:00"', language: 'kuery' },
|
||||
{ query: '@timestamp:"2019-03-23T13:18:00"', language: 'lucene' }
|
||||
];
|
||||
const filters = [
|
||||
{ match_all: {}, meta: { type: 'match_all' } }
|
||||
];
|
||||
const config = {
|
||||
allowLeadingWildcards: true,
|
||||
queryStringOptions: {},
|
||||
ignoreFilterIfFieldNotInIndex: false,
|
||||
dateFormatTZ: 'Africa/Johannesburg',
|
||||
};
|
||||
|
||||
const expectedResult = {
|
||||
bool: {
|
||||
must: [
|
||||
decorateQuery(luceneStringToDsl('@timestamp:"2019-03-23T13:18:00"'), config.queryStringOptions, config.dateFormatTZ),
|
||||
{ match_all: {} }
|
||||
],
|
||||
filter: [toElasticsearchQuery(fromKueryExpression('@timestamp:"2019-03-23T13:18:00"'), indexPattern, config)],
|
||||
should: [],
|
||||
must_not: [],
|
||||
}
|
||||
};
|
||||
const result = buildEsQuery(indexPattern, queries, filters, config);
|
||||
expect(result).to.eql(expectedResult);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -29,4 +29,9 @@ describe('Query decorator', function () {
|
|||
const decoratedQuery = decorateQuery({ query_string: { query: '*' } }, { analyze_wildcard: true });
|
||||
expect(decoratedQuery).to.eql({ query_string: { query: '*', analyze_wildcard: true } });
|
||||
});
|
||||
|
||||
it('should add a default of a time_zone parameter if one is provided', function () {
|
||||
const decoratedQuery = decorateQuery({ query_string: { query: '*' } }, { analyze_wildcard: true }, 'America/Phoenix');
|
||||
expect(decoratedQuery).to.eql({ query_string: { query: '*', analyze_wildcard: true, time_zone: 'America/Phoenix' } });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -60,6 +60,31 @@ describe('build query', function () {
|
|||
);
|
||||
});
|
||||
|
||||
|
||||
it('should accept a specific date format for a kuery query into an ES query in the bool\'s filter clause', function () {
|
||||
const queries = [{ query: '@timestamp:"2018-04-03T19:04:17"', language: 'kuery' }];
|
||||
|
||||
const expectedESQueries = queries.map(query => {
|
||||
return toElasticsearchQuery(fromKueryExpression(query.query), indexPattern, { dateFormatTZ: 'America/Phoenix' });
|
||||
});
|
||||
|
||||
const result = buildQueryFromKuery(indexPattern, queries, true, 'America/Phoenix');
|
||||
|
||||
expect(result.filter).to.eql(expectedESQueries);
|
||||
});
|
||||
|
||||
it('should gracefully handle date queries when no date format is provided', function () {
|
||||
const queries = [{ query: '@timestamp:"2018-04-03T19:04:17Z"', language: 'kuery' }];
|
||||
|
||||
const expectedESQueries = queries.map(query => {
|
||||
return toElasticsearchQuery(fromKueryExpression(query.query), indexPattern);
|
||||
});
|
||||
|
||||
const result = buildQueryFromKuery(indexPattern, queries, true);
|
||||
|
||||
expect(result.filter).to.eql(expectedESQueries);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -66,4 +66,22 @@ describe('build query', function () {
|
|||
|
||||
});
|
||||
|
||||
it('should accept a date format in the decorated queries and combine that into the bool\'s must clause', function () {
|
||||
const queries = [
|
||||
{ query: 'foo:bar', language: 'lucene' },
|
||||
{ query: 'bar:baz', language: 'lucene' },
|
||||
];
|
||||
const dateFormatTZ = 'America/Phoenix';
|
||||
|
||||
const expectedESQueries = queries.map(
|
||||
(query) => {
|
||||
return decorateQuery(luceneStringToDsl(query.query), {}, dateFormatTZ);
|
||||
}
|
||||
);
|
||||
|
||||
const result = buildQueryFromLucene(queries, {}, dateFormatTZ);
|
||||
|
||||
expect(result.must).to.eql(expectedESQueries);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { getEsQueryConfig } from '../get_es_query_config';
|
||||
|
||||
const config = {
|
||||
get(item) {
|
||||
return config[item];
|
||||
},
|
||||
'query:allowLeadingWildcards': {
|
||||
allowLeadingWildcards: true,
|
||||
},
|
||||
'query:queryString:options': {
|
||||
queryStringOptions: {},
|
||||
},
|
||||
'courier:ignoreFilterIfFieldNotInIndex': {
|
||||
ignoreFilterIfFieldNotInIndex: true,
|
||||
},
|
||||
'dateFormat:tz': {
|
||||
dateFormatTZ: 'Browser',
|
||||
},
|
||||
};
|
||||
|
||||
describe('getEsQueryConfig', function () {
|
||||
it('should return the parameters of an Elasticsearch query config requested', function () {
|
||||
const result = getEsQueryConfig(config);
|
||||
const expected = {
|
||||
allowLeadingWildcards: {
|
||||
allowLeadingWildcards: true,
|
||||
},
|
||||
dateFormatTZ: {
|
||||
dateFormatTZ: 'Browser',
|
||||
},
|
||||
ignoreFilterIfFieldNotInIndex: {
|
||||
ignoreFilterIfFieldNotInIndex: true,
|
||||
},
|
||||
queryStringOptions: {
|
||||
queryStringOptions: {},
|
||||
},
|
||||
};
|
||||
expect(result).to.eql(expected);
|
||||
expect(result).to.have.keys(
|
||||
'allowLeadingWildcards',
|
||||
'dateFormatTZ',
|
||||
'ignoreFilterIfFieldNotInIndex',
|
||||
'queryStringOptions'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -28,6 +28,7 @@ import { buildQueryFromLucene } from './from_lucene';
|
|||
* @param filters - a filter object or array of filter objects
|
||||
* @param config - an objects with query:allowLeadingWildcards and query:queryString:options UI
|
||||
* settings in form of { allowLeadingWildcards, queryStringOptions }
|
||||
* config contains dateformat:tz
|
||||
*/
|
||||
export function buildEsQuery(
|
||||
indexPattern,
|
||||
|
@ -37,15 +38,15 @@ export function buildEsQuery(
|
|||
allowLeadingWildcards: false,
|
||||
queryStringOptions: {},
|
||||
ignoreFilterIfFieldNotInIndex: false,
|
||||
dateFormatTZ: null,
|
||||
}) {
|
||||
queries = Array.isArray(queries) ? queries : [queries];
|
||||
filters = Array.isArray(filters) ? filters : [filters];
|
||||
|
||||
const validQueries = queries.filter((query) => has(query, 'query'));
|
||||
const queriesByLanguage = groupBy(validQueries, 'language');
|
||||
|
||||
const kueryQuery = buildQueryFromKuery(indexPattern, queriesByLanguage.kuery, config.allowLeadingWildcards);
|
||||
const luceneQuery = buildQueryFromLucene(queriesByLanguage.lucene, config.queryStringOptions);
|
||||
const kueryQuery = buildQueryFromKuery(indexPattern, queriesByLanguage.kuery, config.allowLeadingWildcards, config.dateFormatTZ);
|
||||
const luceneQuery = buildQueryFromLucene(queriesByLanguage.lucene, config.queryStringOptions, config.dateFormatTZ);
|
||||
const filterQuery = buildQueryFromFilters(filters, indexPattern, config.ignoreFilterIfFieldNotInIndex);
|
||||
|
||||
return {
|
||||
|
|
|
@ -18,16 +18,22 @@
|
|||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { getTimeZoneFromSettings } from '../utils/get_time_zone_from_settings';
|
||||
|
||||
/**
|
||||
* Decorate queries with default parameters
|
||||
* @param query object
|
||||
* @param queryStringOptions query:queryString:options from UI settings
|
||||
* @param dateFormatTZ dateFormat:tz from UI settings
|
||||
* @returns {object}
|
||||
*/
|
||||
export function decorateQuery(query, queryStringOptions) {
|
||||
|
||||
export function decorateQuery(query, queryStringOptions, dateFormatTZ = null) {
|
||||
if (_.has(query, 'query_string.query')) {
|
||||
_.extend(query.query_string, queryStringOptions);
|
||||
if (dateFormatTZ) {
|
||||
_.defaults(query.query_string, { time_zone: getTimeZoneFromSettings(dateFormatTZ) });
|
||||
}
|
||||
}
|
||||
|
||||
return query;
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
import { fromLegacyKueryExpression, fromKueryExpression, toElasticsearchQuery, nodeTypes } from '../kuery';
|
||||
|
||||
export function buildQueryFromKuery(indexPattern, queries = [], allowLeadingWildcards) {
|
||||
export function buildQueryFromKuery(indexPattern, queries = [], allowLeadingWildcards, dateFormatTZ = null) {
|
||||
const queryASTs = queries.map(query => {
|
||||
try {
|
||||
return fromKueryExpression(query.query, { allowLeadingWildcards });
|
||||
|
@ -32,12 +32,12 @@ export function buildQueryFromKuery(indexPattern, queries = [], allowLeadingWild
|
|||
throw Error('OutdatedKuerySyntaxError');
|
||||
}
|
||||
});
|
||||
return buildQuery(indexPattern, queryASTs);
|
||||
return buildQuery(indexPattern, queryASTs, { dateFormatTZ });
|
||||
}
|
||||
|
||||
function buildQuery(indexPattern, queryASTs) {
|
||||
function buildQuery(indexPattern, queryASTs, config = null) {
|
||||
const compoundQueryAST = nodeTypes.function.buildNode('and', queryASTs);
|
||||
const kueryQuery = toElasticsearchQuery(compoundQueryAST, indexPattern);
|
||||
const kueryQuery = toElasticsearchQuery(compoundQueryAST, indexPattern, config);
|
||||
return {
|
||||
must: [],
|
||||
filter: [],
|
||||
|
|
|
@ -21,10 +21,10 @@ import _ from 'lodash';
|
|||
import { decorateQuery } from './decorate_query';
|
||||
import { luceneStringToDsl } from './lucene_string_to_dsl';
|
||||
|
||||
export function buildQueryFromLucene(queries, queryStringOptions) {
|
||||
export function buildQueryFromLucene(queries, queryStringOptions, dateFormatTZ = null) {
|
||||
const combinedQueries = _.map(queries, (query) => {
|
||||
const queryDsl = luceneStringToDsl(query.query);
|
||||
return decorateQuery(queryDsl, queryStringOptions);
|
||||
return decorateQuery(queryDsl, queryStringOptions, dateFormatTZ);
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -21,5 +21,6 @@ export function getEsQueryConfig(config) {
|
|||
const allowLeadingWildcards = config.get('query:allowLeadingWildcards');
|
||||
const queryStringOptions = config.get('query:queryString:options');
|
||||
const ignoreFilterIfFieldNotInIndex = config.get('courier:ignoreFilterIfFieldNotInIndex');
|
||||
return { allowLeadingWildcards, queryStringOptions, ignoreFilterIfFieldNotInIndex };
|
||||
const dateFormatTZ = config.get('dateFormat:tz');
|
||||
return { allowLeadingWildcards, queryStringOptions, ignoreFilterIfFieldNotInIndex, dateFormatTZ };
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import indexPatternResponse from '../../../__fixtures__/index_pattern_response.j
|
|||
|
||||
// Helpful utility allowing us to test the PEG parser by simply checking for deep equality between
|
||||
// the nodes the parser generates and the nodes our constructor functions generate.
|
||||
|
||||
function fromLegacyKueryExpressionNoMeta(text) {
|
||||
return ast.fromLegacyKueryExpression(text, { includeMetadata: false });
|
||||
}
|
||||
|
@ -416,6 +417,14 @@ describe('kuery AST API', function () {
|
|||
expect(ast.toElasticsearchQuery(unknownTypeNode)).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should return the given node type\'s ES query representation including a time zone parameter when one is provided', function () {
|
||||
const config = { dateFormatTZ: 'America/Phoenix' };
|
||||
const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"');
|
||||
const expected = nodeTypes.function.toElasticsearchQuery(node, indexPattern, config);
|
||||
const result = ast.toElasticsearchQuery(node, indexPattern, config);
|
||||
expect(result).to.eql(expected);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('doesKueryExpressionHaveLuceneSyntaxError', function () {
|
||||
|
|
|
@ -51,15 +51,19 @@ function fromExpression(expression, parseOptions = {}, parse = parseKuery) {
|
|||
|
||||
return parse(expression, parseOptions);
|
||||
}
|
||||
|
||||
// indexPattern isn't required, but if you pass one in, we can be more intelligent
|
||||
// about how we craft the queries (e.g. scripted fields)
|
||||
export function toElasticsearchQuery(node, indexPattern) {
|
||||
/**
|
||||
* @params {String} indexPattern
|
||||
* @params {Object} config - contains the dateFormatTZ
|
||||
*
|
||||
* IndexPattern isn't required, but if you pass one in, we can be more intelligent
|
||||
* about how we craft the queries (e.g. scripted fields)
|
||||
*/
|
||||
export function toElasticsearchQuery(node, indexPattern, config = {}) {
|
||||
if (!node || !node.type || !nodeTypes[node.type]) {
|
||||
return toElasticsearchQuery(nodeTypes.function.buildNode('and', []));
|
||||
}
|
||||
|
||||
return nodeTypes[node.type].toElasticsearchQuery(node, indexPattern);
|
||||
return nodeTypes[node.type].toElasticsearchQuery(node, indexPattern, config);
|
||||
}
|
||||
|
||||
export function doesKueryExpressionHaveLuceneSyntaxError(expression) {
|
||||
|
|
|
@ -23,7 +23,6 @@ import { nodeTypes } from '../../node_types';
|
|||
import * as ast from '../../ast';
|
||||
import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json';
|
||||
|
||||
|
||||
let indexPattern;
|
||||
|
||||
const childNode1 = nodeTypes.function.buildNode('is', 'machine.os', 'osx');
|
||||
|
@ -57,8 +56,6 @@ describe('kuery functions', function () {
|
|||
[childNode1, childNode2].map((childNode) => ast.toElasticsearchQuery(childNode, indexPattern))
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
|
@ -199,6 +199,52 @@ describe('kuery functions', function () {
|
|||
expect(result.bool.should[0]).to.have.key('script');
|
||||
});
|
||||
|
||||
it('should support date fields without a dateFormat provided', function () {
|
||||
const expected = {
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: '2018-04-03T19:04:17',
|
||||
lte: '2018-04-03T19:04:17',
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
minimum_should_match: 1
|
||||
}
|
||||
};
|
||||
|
||||
const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"');
|
||||
const result = is.toElasticsearchQuery(node, indexPattern);
|
||||
expect(result).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should support date fields with a dateFormat provided', function () {
|
||||
const config = { dateFormatTZ: 'America/Phoenix' };
|
||||
const expected = {
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: '2018-04-03T19:04:17',
|
||||
lte: '2018-04-03T19:04:17',
|
||||
time_zone: 'America/Phoenix',
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
minimum_should_match: 1
|
||||
}
|
||||
};
|
||||
|
||||
const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"');
|
||||
const result = is.toElasticsearchQuery(node, indexPattern, config);
|
||||
expect(result).to.eql(expected);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -23,7 +23,6 @@ import { nodeTypes } from '../../node_types';
|
|||
import * as ast from '../../ast';
|
||||
import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json';
|
||||
|
||||
|
||||
let indexPattern;
|
||||
|
||||
const childNode = nodeTypes.function.buildNode('is', 'extension', 'jpg');
|
||||
|
@ -32,7 +31,6 @@ describe('kuery functions', function () {
|
|||
|
||||
describe('not', function () {
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
indexPattern = indexPatternResponse;
|
||||
});
|
||||
|
@ -56,6 +54,7 @@ describe('kuery functions', function () {
|
|||
expect(result.bool).to.only.have.keys('must_not');
|
||||
expect(result.bool.must_not).to.eql(ast.toElasticsearchQuery(childNode, indexPattern));
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -23,7 +23,6 @@ import { nodeTypes } from '../../node_types';
|
|||
import * as ast from '../../ast';
|
||||
import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json';
|
||||
|
||||
|
||||
let indexPattern;
|
||||
|
||||
const childNode1 = nodeTypes.function.buildNode('is', 'machine.os', 'osx');
|
||||
|
@ -33,11 +32,11 @@ describe('kuery functions', function () {
|
|||
|
||||
describe('or', function () {
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
indexPattern = indexPatternResponse;
|
||||
});
|
||||
|
||||
|
||||
describe('buildNodeParams', function () {
|
||||
|
||||
it('arguments should contain the unmodified child nodes', function () {
|
||||
|
|
|
@ -22,7 +22,6 @@ import * as range from '../range';
|
|||
import { nodeTypes } from '../../node_types';
|
||||
import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json';
|
||||
|
||||
|
||||
let indexPattern;
|
||||
|
||||
describe('kuery functions', function () {
|
||||
|
@ -136,6 +135,52 @@ describe('kuery functions', function () {
|
|||
expect(result.bool.should[0]).to.have.key('script');
|
||||
});
|
||||
|
||||
it('should support date fields without a dateFormat provided', function () {
|
||||
const expected = {
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gt: '2018-01-03T19:04:17',
|
||||
lt: '2018-04-03T19:04:17',
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
minimum_should_match: 1
|
||||
}
|
||||
};
|
||||
|
||||
const node = nodeTypes.function.buildNode('range', '@timestamp', { gt: '2018-01-03T19:04:17', lt: '2018-04-03T19:04:17' });
|
||||
const result = range.toElasticsearchQuery(node, indexPattern);
|
||||
expect(result).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should support date fields with a dateFormat provided', function () {
|
||||
const config = { dateFormatTZ: 'America/Phoenix' };
|
||||
const expected = {
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gt: '2018-01-03T19:04:17',
|
||||
lt: '2018-04-03T19:04:17',
|
||||
time_zone: 'America/Phoenix',
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
minimum_should_match: 1
|
||||
}
|
||||
};
|
||||
|
||||
const node = nodeTypes.function.buildNode('range', '@timestamp', { gt: '2018-01-03T19:04:17', lt: '2018-04-03T19:04:17' });
|
||||
const result = range.toElasticsearchQuery(node, indexPattern, config);
|
||||
expect(result).to.eql(expected);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -25,13 +25,13 @@ export function buildNodeParams(children) {
|
|||
};
|
||||
}
|
||||
|
||||
export function toElasticsearchQuery(node, indexPattern) {
|
||||
export function toElasticsearchQuery(node, indexPattern, config) {
|
||||
const children = node.arguments || [];
|
||||
|
||||
return {
|
||||
bool: {
|
||||
filter: children.map((child) => {
|
||||
return ast.toElasticsearchQuery(child, indexPattern);
|
||||
return ast.toElasticsearchQuery(child, indexPattern, config);
|
||||
})
|
||||
}
|
||||
};
|
||||
|
|
|
@ -23,6 +23,7 @@ import * as literal from '../node_types/literal';
|
|||
import * as wildcard from '../node_types/wildcard';
|
||||
import { getPhraseScript } from '../../filters';
|
||||
import { getFields } from './utils/get_fields';
|
||||
import { getTimeZoneFromSettings } from '../../utils/get_time_zone_from_settings';
|
||||
|
||||
export function buildNodeParams(fieldName, value, isPhrase = false) {
|
||||
if (_.isUndefined(fieldName)) {
|
||||
|
@ -35,19 +36,16 @@ export function buildNodeParams(fieldName, value, isPhrase = false) {
|
|||
const fieldNode = typeof fieldName === 'string' ? ast.fromLiteralExpression(fieldName) : literal.buildNode(fieldName);
|
||||
const valueNode = typeof value === 'string' ? ast.fromLiteralExpression(value) : literal.buildNode(value);
|
||||
const isPhraseNode = literal.buildNode(isPhrase);
|
||||
|
||||
return {
|
||||
arguments: [fieldNode, valueNode, isPhraseNode],
|
||||
};
|
||||
}
|
||||
|
||||
export function toElasticsearchQuery(node, indexPattern) {
|
||||
export function toElasticsearchQuery(node, indexPattern = null, config = {}) {
|
||||
const { arguments: [ fieldNameArg, valueArg, isPhraseArg ] } = node;
|
||||
|
||||
const fieldName = ast.toElasticsearchQuery(fieldNameArg);
|
||||
const value = !_.isUndefined(valueArg) ? ast.toElasticsearchQuery(valueArg) : valueArg;
|
||||
const type = isPhraseArg.value ? 'phrase' : 'best_fields';
|
||||
|
||||
if (fieldNameArg.value === null) {
|
||||
if (valueArg.type === 'wildcard') {
|
||||
return {
|
||||
|
@ -67,7 +65,6 @@ export function toElasticsearchQuery(node, indexPattern) {
|
|||
}
|
||||
|
||||
const fields = indexPattern ? getFields(fieldNameArg, indexPattern) : [];
|
||||
|
||||
// If no fields are found in the index pattern we send through the given field name as-is. We do this to preserve
|
||||
// the behaviour of lucene on dashboards where there are panels based on different index patterns that have different
|
||||
// fields. If a user queries on a field that exists in one pattern but not the other, the index pattern without the
|
||||
|
@ -116,6 +113,22 @@ export function toElasticsearchQuery(node, indexPattern) {
|
|||
}
|
||||
}];
|
||||
}
|
||||
/*
|
||||
If we detect that it's a date field and the user wants an exact date, we need to convert the query to both >= and <= the value provided to force a range query. This is because match and match_phrase queries do not accept a timezone parameter.
|
||||
dateFormatTZ can have the value of 'Browser', in which case we guess the timezone using moment.tz.guess.
|
||||
*/
|
||||
else if (field.type === 'date') {
|
||||
const timeZoneParam = config.dateFormatTZ ? { time_zone: getTimeZoneFromSettings(config.dateFormatTZ) } : {};
|
||||
return [...accumulator, {
|
||||
range: {
|
||||
[field.name]: {
|
||||
gte: value,
|
||||
lte: value,
|
||||
...timeZoneParam,
|
||||
},
|
||||
}
|
||||
}];
|
||||
}
|
||||
else {
|
||||
const queryType = type === 'phrase' ? 'match_phrase' : 'match';
|
||||
return [...accumulator, {
|
||||
|
@ -134,3 +147,4 @@ export function toElasticsearchQuery(node, indexPattern) {
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -25,12 +25,12 @@ export function buildNodeParams(child) {
|
|||
};
|
||||
}
|
||||
|
||||
export function toElasticsearchQuery(node, indexPattern) {
|
||||
export function toElasticsearchQuery(node, indexPattern, config) {
|
||||
const [ argument ] = node.arguments;
|
||||
|
||||
return {
|
||||
bool: {
|
||||
must_not: ast.toElasticsearchQuery(argument, indexPattern)
|
||||
must_not: ast.toElasticsearchQuery(argument, indexPattern, config)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -25,13 +25,13 @@ export function buildNodeParams(children) {
|
|||
};
|
||||
}
|
||||
|
||||
export function toElasticsearchQuery(node, indexPattern) {
|
||||
export function toElasticsearchQuery(node, indexPattern, config) {
|
||||
const children = node.arguments || [];
|
||||
|
||||
return {
|
||||
bool: {
|
||||
should: children.map((child) => {
|
||||
return ast.toElasticsearchQuery(child, indexPattern);
|
||||
return ast.toElasticsearchQuery(child, indexPattern, config);
|
||||
}),
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
|
|
|
@ -22,6 +22,7 @@ import { nodeTypes } from '../node_types';
|
|||
import * as ast from '../ast';
|
||||
import { getRangeScript } from '../../filters';
|
||||
import { getFields } from './utils/get_fields';
|
||||
import { getTimeZoneFromSettings } from '../../utils/get_time_zone_from_settings';
|
||||
|
||||
export function buildNodeParams(fieldName, params) {
|
||||
params = _.pick(params, 'gt', 'lt', 'gte', 'lte', 'format');
|
||||
|
@ -35,7 +36,7 @@ export function buildNodeParams(fieldName, params) {
|
|||
};
|
||||
}
|
||||
|
||||
export function toElasticsearchQuery(node, indexPattern) {
|
||||
export function toElasticsearchQuery(node, indexPattern = null, config = {}) {
|
||||
const [ fieldNameArg, ...args ] = node.arguments;
|
||||
const fields = indexPattern ? getFields(fieldNameArg, indexPattern) : [];
|
||||
const namedArgs = extractArguments(args);
|
||||
|
@ -60,7 +61,17 @@ export function toElasticsearchQuery(node, indexPattern) {
|
|||
script: getRangeScript(field, queryParams),
|
||||
};
|
||||
}
|
||||
|
||||
else if (field.type === 'date') {
|
||||
const timeZoneParam = config.dateFormatTZ ? { time_zone: getTimeZoneFromSettings(config.dateFormatTZ) } : {};
|
||||
return {
|
||||
range: {
|
||||
[field.name]: {
|
||||
...queryParams,
|
||||
...timeZoneParam,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
range: {
|
||||
[field.name]: queryParams
|
||||
|
|
|
@ -31,7 +31,6 @@ describe('kuery node types', function () {
|
|||
|
||||
let indexPattern;
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
indexPattern = indexPatternResponse;
|
||||
});
|
||||
|
|
|
@ -21,8 +21,8 @@ import _ from 'lodash';
|
|||
import { functions } from '../functions';
|
||||
|
||||
export function buildNode(functionName, ...functionArgs) {
|
||||
const kueryFunction = functions[functionName];
|
||||
|
||||
const kueryFunction = functions[functionName];
|
||||
if (_.isUndefined(kueryFunction)) {
|
||||
throw new Error(`Unknown function "${functionName}"`);
|
||||
}
|
||||
|
@ -47,8 +47,8 @@ export function buildNodeWithArgumentNodes(functionName, argumentNodes) {
|
|||
};
|
||||
}
|
||||
|
||||
export function toElasticsearchQuery(node, indexPattern) {
|
||||
export function toElasticsearchQuery(node, indexPattern, config = {}) {
|
||||
const kueryFunction = functions[node.function];
|
||||
return kueryFunction.toElasticsearchQuery(node, indexPattern);
|
||||
return kueryFunction.toElasticsearchQuery(node, indexPattern, config);
|
||||
}
|
||||
|
||||
|
|
|
@ -17,20 +17,20 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { scanAllTypes } from '../scan_all_types';
|
||||
import expect from '@kbn/expect';
|
||||
import { getTimeZoneFromSettings } from '../get_time_zone_from_settings';
|
||||
|
||||
jest.mock('ui/chrome', () => ({
|
||||
addBasePath: () => 'apiUrl',
|
||||
}));
|
||||
describe('get timezone from settings', function () {
|
||||
|
||||
describe('scanAllTypes', () => {
|
||||
it('should call the api', async () => {
|
||||
const $http = {
|
||||
post: jest.fn().mockImplementation(() => ([]))
|
||||
};
|
||||
const typesToInclude = ['index-pattern', 'dashboard'];
|
||||
|
||||
await scanAllTypes($http, typesToInclude);
|
||||
expect($http.post).toBeCalledWith('apiUrl/export', { typesToInclude });
|
||||
it('should return the config timezone if the time zone is set', function () {
|
||||
const result = getTimeZoneFromSettings('America/Chicago');
|
||||
expect(result).to.eql('America/Chicago');
|
||||
});
|
||||
|
||||
it('should return the system timezone if the time zone is set to "Browser"', function () {
|
||||
const result = getTimeZoneFromSettings('Browser');
|
||||
expect(result).to.not.equal('Browser');
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -17,10 +17,12 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import chrome from 'ui/chrome';
|
||||
import moment from 'moment-timezone';
|
||||
const detectedTimezone = moment.tz.guess();
|
||||
|
||||
const apiBase = chrome.addBasePath('/api/kibana/management/saved_objects/scroll');
|
||||
export async function scanAllTypes($http, typesToInclude) {
|
||||
const results = await $http.post(`${apiBase}/export`, { typesToInclude });
|
||||
return results.data;
|
||||
export function getTimeZoneFromSettings(dateFormatTZ) {
|
||||
if (dateFormatTZ === 'Browser') {
|
||||
return detectedTimezone;
|
||||
}
|
||||
return dateFormatTZ;
|
||||
}
|
20
packages/kbn-es-query/src/utils/index.js
Normal file
20
packages/kbn-es-query/src/utils/index.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './get_time_zone_from_settings';
|
|
@ -77,76 +77,129 @@ exports[`ObjectsTable delete should show a confirm modal 1`] = `
|
|||
`;
|
||||
|
||||
exports[`ObjectsTable export should allow the user to choose when exporting all 1`] = `
|
||||
<EuiConfirmModal
|
||||
buttonColor="primary"
|
||||
cancelButtonText={
|
||||
<FormattedMessage
|
||||
defaultMessage="Cancel"
|
||||
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.cancelButtonLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
confirmButtonText={
|
||||
<FormattedMessage
|
||||
defaultMessage="Export All"
|
||||
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportAllButtonLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
defaultFocusedButton="confirm"
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Export {filteredItemCount, plural, one{# object} other {# objects}}"
|
||||
id="kbn.management.objects.objectsTable.exportObjectsConfirmModalTitle"
|
||||
values={
|
||||
Object {
|
||||
"filteredItemCount": 4,
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
<EuiModal
|
||||
maxWidth={true}
|
||||
onClose={[Function]}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Select which types to export. The number in parentheses indicates how many of this type are available to export."
|
||||
id="kbn.management.objects.objectsTable.exportObjectsConfirmModalDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
<EuiCheckboxGroup
|
||||
idToSelectedMap={
|
||||
Object {
|
||||
"dashboard": true,
|
||||
"index-pattern": true,
|
||||
"search": true,
|
||||
"visualization": true,
|
||||
}
|
||||
}
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"id": "index-pattern",
|
||||
"label": "index-pattern (0)",
|
||||
},
|
||||
Object {
|
||||
"id": "visualization",
|
||||
"label": "visualization (0)",
|
||||
},
|
||||
Object {
|
||||
"id": "dashboard",
|
||||
"label": "dashboard (0)",
|
||||
},
|
||||
Object {
|
||||
"id": "search",
|
||||
"label": "search (0)",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</EuiConfirmModal>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
<FormattedMessage
|
||||
defaultMessage="Export {filteredItemCount, plural, one{# object} other {# objects}}"
|
||||
id="kbn.management.objects.objectsTable.exportObjectsConfirmModalTitle"
|
||||
values={
|
||||
Object {
|
||||
"filteredItemCount": 4,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<EuiText
|
||||
grow={true}
|
||||
size="m"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Select which types to export."
|
||||
id="kbn.management.objects.objectsTable.exportObjectsConfirmModalDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
<EuiCheckboxGroup
|
||||
idToSelectedMap={
|
||||
Object {
|
||||
"dashboard": true,
|
||||
"index-pattern": true,
|
||||
"search": true,
|
||||
"visualization": true,
|
||||
}
|
||||
}
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"id": "index-pattern",
|
||||
"label": "index-pattern (0)",
|
||||
},
|
||||
Object {
|
||||
"id": "visualization",
|
||||
"label": "visualization (0)",
|
||||
},
|
||||
Object {
|
||||
"id": "dashboard",
|
||||
"label": "dashboard (0)",
|
||||
},
|
||||
Object {
|
||||
"id": "search",
|
||||
"label": "search (0)",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiFlexGroup
|
||||
justifyContent="spaceBetween"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiSwitch
|
||||
checked={true}
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Include related objects"
|
||||
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.includeReferencesDeepLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
name="includeReferencesDeep"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiButtonEmpty
|
||||
color="primary"
|
||||
iconSide="left"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Cancel"
|
||||
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.cancelButtonLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
fill={true}
|
||||
iconSide="left"
|
||||
onClick={[Function]}
|
||||
size="m"
|
||||
type="button"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Export all"
|
||||
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportAllButtonLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
`;
|
||||
|
||||
exports[`ObjectsTable import should show the flyout 1`] = `
|
||||
|
|
|
@ -24,6 +24,8 @@ import { ObjectsTable, INCLUDED_TYPES } from '../objects_table';
|
|||
import { Flyout } from '../components/flyout/';
|
||||
import { Relationships } from '../components/relationships/';
|
||||
|
||||
jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() }));
|
||||
|
||||
jest.mock('../components/header', () => ({
|
||||
Header: () => 'Header',
|
||||
}));
|
||||
|
@ -45,12 +47,12 @@ jest.mock('ui/chrome', () => ({
|
|||
addBasePath: () => ''
|
||||
}));
|
||||
|
||||
jest.mock('../../../lib/retrieve_and_export_docs', () => ({
|
||||
retrieveAndExportDocs: jest.fn(),
|
||||
jest.mock('../../../lib/fetch_export_objects', () => ({
|
||||
fetchExportObjects: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../lib/scan_all_types', () => ({
|
||||
scanAllTypes: jest.fn(),
|
||||
jest.mock('../../../lib/fetch_export_by_type', () => ({
|
||||
fetchExportByType: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../lib/get_saved_object_counts', () => ({
|
||||
|
@ -64,8 +66,8 @@ jest.mock('../../../lib/get_saved_object_counts', () => ({
|
|||
})
|
||||
}));
|
||||
|
||||
jest.mock('../../../lib/save_to_file', () => ({
|
||||
saveToFile: jest.fn(),
|
||||
jest.mock('@elastic/filesaver', () => ({
|
||||
saveAs: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../lib/get_relationships', () => ({
|
||||
|
@ -147,6 +149,7 @@ const defaultProps = {
|
|||
};
|
||||
|
||||
let addDangerMock;
|
||||
let addSuccessMock;
|
||||
|
||||
describe('ObjectsTable', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -159,8 +162,10 @@ describe('ObjectsTable', () => {
|
|||
return debounced;
|
||||
};
|
||||
addDangerMock = jest.fn();
|
||||
addSuccessMock = jest.fn();
|
||||
require('ui/notify').toastNotifications = {
|
||||
addDanger: addDangerMock,
|
||||
addSuccess: addSuccessMock,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -222,7 +227,7 @@ describe('ObjectsTable', () => {
|
|||
}))
|
||||
};
|
||||
|
||||
const { retrieveAndExportDocs } = require('../../../lib/retrieve_and_export_docs');
|
||||
const { fetchExportObjects } = require('../../../lib/fetch_export_objects');
|
||||
|
||||
const component = shallowWithIntl(
|
||||
<ObjectsTable.WrappedComponent
|
||||
|
@ -239,10 +244,10 @@ describe('ObjectsTable', () => {
|
|||
// Set some as selected
|
||||
component.instance().onSelectionChanged(mockSelectedSavedObjects);
|
||||
|
||||
await component.instance().onExport();
|
||||
await component.instance().onExport(true);
|
||||
|
||||
expect(mockSavedObjectsClient.bulkGet).toHaveBeenCalledWith(mockSelectedSavedObjects);
|
||||
expect(retrieveAndExportDocs).toHaveBeenCalledWith(mockSavedObjects, mockSavedObjectsClient);
|
||||
expect(fetchExportObjects).toHaveBeenCalledWith(mockSelectedSavedObjects, true);
|
||||
expect(addSuccessMock).toHaveBeenCalledWith({ title: 'Your file is downloading in the background' });
|
||||
});
|
||||
|
||||
it('should allow the user to choose when exporting all', async () => {
|
||||
|
@ -260,12 +265,12 @@ describe('ObjectsTable', () => {
|
|||
component.find('Header').prop('onExportAll')();
|
||||
component.update();
|
||||
|
||||
expect(component.find('EuiConfirmModal')).toMatchSnapshot();
|
||||
expect(component.find('EuiModal')).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should export all', async () => {
|
||||
const { scanAllTypes } = require('../../../lib/scan_all_types');
|
||||
const { saveToFile } = require('../../../lib/save_to_file');
|
||||
const { fetchExportByType } = require('../../../lib/fetch_export_by_type');
|
||||
const { saveAs } = require('@elastic/filesaver');
|
||||
const component = shallowWithIntl(
|
||||
<ObjectsTable.WrappedComponent
|
||||
{...defaultProps}
|
||||
|
@ -278,12 +283,14 @@ describe('ObjectsTable', () => {
|
|||
component.update();
|
||||
|
||||
// Set up mocks
|
||||
scanAllTypes.mockImplementation(() => allSavedObjects);
|
||||
const blob = new Blob([JSON.stringify(allSavedObjects)], { type: 'application/ndjson' });
|
||||
fetchExportByType.mockImplementation(() => blob);
|
||||
|
||||
await component.instance().onExportAll();
|
||||
|
||||
expect(scanAllTypes).toHaveBeenCalledWith(defaultProps.$http, INCLUDED_TYPES);
|
||||
expect(saveToFile).toHaveBeenCalledWith(JSON.stringify(allSavedObjects, null, 2));
|
||||
expect(fetchExportByType).toHaveBeenCalledWith(INCLUDED_TYPES, true);
|
||||
expect(saveAs).toHaveBeenCalledWith(blob, 'export.ndjson');
|
||||
expect(addSuccessMock).toHaveBeenCalledWith({ title: 'Your file is downloading in the background' });
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -22,43 +22,342 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = `
|
|||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
<span>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
iconType="help"
|
||||
size="m"
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Index Pattern Conflicts"
|
||||
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="The following saved objects use index patterns that do not exist. Please select the index patterns you'd like re-associated with them. You can {indexPatternLink} if necessary."
|
||||
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsDescription"
|
||||
values={
|
||||
Object {
|
||||
"indexPatternLink": <EuiLink
|
||||
color="primary"
|
||||
href=""
|
||||
type="button"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="create a new index pattern"
|
||||
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsCalloutLinkText"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiLink>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
</span>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiInMemoryTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"description": "ID of the index pattern",
|
||||
"field": "existingIndexPatternId",
|
||||
"name": "ID",
|
||||
"sortable": true,
|
||||
},
|
||||
Object {
|
||||
"description": "How many affected objects",
|
||||
"field": "list",
|
||||
"name": "Count",
|
||||
"render": [Function],
|
||||
},
|
||||
Object {
|
||||
"description": "Sample of affected objects",
|
||||
"field": "list",
|
||||
"name": "Sample of affected objects",
|
||||
"render": [Function],
|
||||
},
|
||||
Object {
|
||||
"field": "existingIndexPatternId",
|
||||
"name": "New index pattern",
|
||||
"render": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
executeQueryOptions={Object {}}
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"existingIndexPatternId": "MyIndexPattern*",
|
||||
"list": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"title": "My Visualization",
|
||||
"type": "visualization",
|
||||
},
|
||||
],
|
||||
"newIndexPatternId": undefined,
|
||||
},
|
||||
]
|
||||
}
|
||||
pagination={
|
||||
Object {
|
||||
"pageSizeOptions": Array [
|
||||
5,
|
||||
10,
|
||||
25,
|
||||
],
|
||||
}
|
||||
}
|
||||
responsive={true}
|
||||
sorting={false}
|
||||
/>
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
iconType="help"
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup
|
||||
justifyContent="spaceBetween"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiButtonEmpty
|
||||
color="primary"
|
||||
iconSide="left"
|
||||
onClick={[MockFunction]}
|
||||
size="s"
|
||||
type="button"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Cancel"
|
||||
id="kbn.management.objects.objectsTable.flyout.import.cancelButtonLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
data-test-subj="importSavedObjectsConfirmBtn"
|
||||
fill={true}
|
||||
iconSide="left"
|
||||
isLoading={false}
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
type="button"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Confirm all changes"
|
||||
id="kbn.management.objects.objectsTable.flyout.importSuccessful.confirmAllChangesButtonLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
`;
|
||||
|
||||
exports[`Flyout conflicts should allow conflict resolution 2`] = `
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
Object {
|
||||
"getConflictResolutions": [Function],
|
||||
"state": Object {
|
||||
"conflictedIndexPatterns": undefined,
|
||||
"conflictedSavedObjectsLinkedToSavedSearches": undefined,
|
||||
"conflictedSearchDocs": undefined,
|
||||
"conflictingRecord": undefined,
|
||||
"error": undefined,
|
||||
"failedImports": Array [
|
||||
Object {
|
||||
"error": Object {
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "MyIndexPattern*",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "missing_references",
|
||||
},
|
||||
"obj": Object {
|
||||
"id": "1",
|
||||
"title": "My Visualization",
|
||||
"type": "visualization",
|
||||
},
|
||||
},
|
||||
],
|
||||
"file": Object {
|
||||
"name": "foo.ndjson",
|
||||
"path": "/home/foo.ndjson",
|
||||
},
|
||||
"importCount": 0,
|
||||
"indexPatterns": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
},
|
||||
Object {
|
||||
"id": "2",
|
||||
},
|
||||
],
|
||||
"isLegacyFile": false,
|
||||
"isOverwriteAllChecked": true,
|
||||
"loadingMessage": undefined,
|
||||
"status": "loading",
|
||||
"unmatchedReferences": Array [
|
||||
Object {
|
||||
"existingIndexPatternId": "MyIndexPattern*",
|
||||
"list": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"title": "My Visualization",
|
||||
"type": "visualization",
|
||||
},
|
||||
],
|
||||
"newIndexPatternId": "2",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Object {
|
||||
"failedImports": Array [],
|
||||
"importCount": 1,
|
||||
"status": "success",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Flyout conflicts should handle errors 1`] = `
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
iconType="help"
|
||||
size="m"
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Import failed"
|
||||
id="kbn.management.objects.objectsTable.flyout.importFailedTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Failed to import {failedImportCount} of {totalImportCount} objects. Import failed"
|
||||
id="kbn.management.objects.objectsTable.flyout.importFailedDescription"
|
||||
values={
|
||||
Object {
|
||||
"failedImportCount": 1,
|
||||
"totalImportCount": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
<p />
|
||||
</EuiCallOut>
|
||||
`;
|
||||
|
||||
exports[`Flyout legacy conflicts should allow conflict resolution 1`] = `
|
||||
<EuiFlyout
|
||||
closeButtonAriaLabel="Closes this dialog"
|
||||
hideCloseButton={false}
|
||||
maxWidth={false}
|
||||
onClose={[MockFunction]}
|
||||
ownFocus={false}
|
||||
size="s"
|
||||
>
|
||||
<EuiFlyoutHeader>
|
||||
<EuiTitle
|
||||
size="m"
|
||||
title={
|
||||
textTransform="none"
|
||||
>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
defaultMessage="Index Pattern Conflicts"
|
||||
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsTitle"
|
||||
defaultMessage="Import saved objects"
|
||||
id="kbn.management.objects.objectsTable.flyout.importSavedObjectTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="The following saved objects use index patterns that do not exist. Please select the index patterns you'd like re-associated with them. You can {indexPatternLink} if necessary."
|
||||
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsDescription"
|
||||
values={
|
||||
Object {
|
||||
"indexPatternLink": <EuiLink
|
||||
color="primary"
|
||||
href=""
|
||||
type="button"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="create a new index pattern"
|
||||
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsCalloutLinkText"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiLink>,
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<span>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
iconType="help"
|
||||
size="m"
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Support for JSON files is going away"
|
||||
id="kbn.management.objects.objectsTable.flyout.legacyFileUsedTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Use our updated export to generate NDJSON files, and you'll be all set."
|
||||
id="kbn.management.objects.objectsTable.flyout.legacyFileUsedBody"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
</span>
|
||||
<span>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
iconType="help"
|
||||
size="m"
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Index Pattern Conflicts"
|
||||
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="The following saved objects use index patterns that do not exist. Please select the index patterns you'd like re-associated with them. You can {indexPatternLink} if necessary."
|
||||
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsDescription"
|
||||
values={
|
||||
Object {
|
||||
"indexPatternLink": <EuiLink
|
||||
color="primary"
|
||||
href=""
|
||||
type="button"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="create a new index pattern"
|
||||
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsCalloutLinkText"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiLink>,
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
</span>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiInMemoryTable
|
||||
|
@ -97,7 +396,7 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = `
|
|||
"list": Array [
|
||||
Object {
|
||||
"id": "MyIndexPattern*",
|
||||
"name": "MyIndexPattern*",
|
||||
"title": "MyIndexPattern*",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
|
@ -164,7 +463,7 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = `
|
|||
</EuiFlyout>
|
||||
`;
|
||||
|
||||
exports[`Flyout conflicts should handle errors 1`] = `
|
||||
exports[`Flyout legacy conflicts should handle errors 1`] = `
|
||||
<EuiCallOut
|
||||
color="danger"
|
||||
iconType="cross"
|
||||
|
@ -214,7 +513,7 @@ exports[`Flyout should render import step 1`] = `
|
|||
hasEmptyLabelSpace={false}
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Please select a JSON file to import"
|
||||
defaultMessage="Please select a file to import"
|
||||
id="kbn.management.objects.objectsTable.flyout.selectFileToImportFormRowLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
|
|
|
@ -22,6 +22,8 @@ import { shallowWithIntl } from 'test_utils/enzyme_helpers';
|
|||
|
||||
import { Flyout } from '../flyout';
|
||||
|
||||
jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() }));
|
||||
|
||||
jest.mock('ui/errors', () => ({
|
||||
SavedObjectNotFound: class SavedObjectNotFound extends Error {
|
||||
constructor(options) {
|
||||
|
@ -35,12 +37,20 @@ jest.mock('ui/errors', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../../../../lib/import_file', () => ({
|
||||
importFile: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../../lib/resolve_import_errors', () => ({
|
||||
resolveImportErrors: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('ui/chrome', () => ({
|
||||
addBasePath: () => {},
|
||||
}));
|
||||
|
||||
jest.mock('../../../../../lib/import_file', () => ({
|
||||
importFile: jest.fn(),
|
||||
jest.mock('../../../../../lib/import_legacy_file', () => ({
|
||||
importLegacyFile: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../../lib/resolve_saved_objects', () => ({
|
||||
|
@ -57,13 +67,19 @@ const defaultProps = {
|
|||
done: jest.fn(),
|
||||
services: [],
|
||||
newIndexPatternUrl: '',
|
||||
getConflictResolutions: jest.fn(),
|
||||
indexPatterns: {
|
||||
getFields: jest.fn().mockImplementation(() => [{ id: '1' }, { id: '2' }]),
|
||||
},
|
||||
};
|
||||
|
||||
const mockFile = {
|
||||
path: '/home/foo.txt',
|
||||
name: 'foo.ndjson',
|
||||
path: '/home/foo.ndjson',
|
||||
};
|
||||
const legacyMockFile = {
|
||||
name: 'foo.json',
|
||||
path: '/home/foo.json',
|
||||
};
|
||||
|
||||
describe('Flyout', () => {
|
||||
|
@ -104,8 +120,23 @@ describe('Flyout', () => {
|
|||
expect(component.state('file')).toBe(mockFile);
|
||||
});
|
||||
|
||||
it('should allow removing a file', async () => {
|
||||
const component = shallowWithIntl(<Flyout.WrappedComponent {...defaultProps} />);
|
||||
|
||||
// Ensure all promises resolve
|
||||
await Promise.resolve();
|
||||
// Ensure the state changes are reflected
|
||||
component.update();
|
||||
|
||||
expect(component.state('file')).toBe(undefined);
|
||||
component.find('EuiFilePicker').simulate('change', [mockFile]);
|
||||
expect(component.state('file')).toBe(mockFile);
|
||||
component.find('EuiFilePicker').simulate('change', []);
|
||||
expect(component.state('file')).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should handle invalid files', async () => {
|
||||
const { importFile } = require('../../../../../lib/import_file');
|
||||
const { importLegacyFile } = require('../../../../../lib/import_legacy_file');
|
||||
const component = shallowWithIntl(<Flyout.WrappedComponent {...defaultProps} />);
|
||||
|
||||
// Ensure all promises resolve
|
||||
|
@ -113,18 +144,18 @@ describe('Flyout', () => {
|
|||
// Ensure the state changes are reflected
|
||||
component.update();
|
||||
|
||||
importFile.mockImplementation(() => {
|
||||
importLegacyFile.mockImplementation(() => {
|
||||
throw new Error('foobar');
|
||||
});
|
||||
|
||||
await component.instance().import();
|
||||
await component.instance().legacyImport();
|
||||
expect(component.state('error')).toBe('The file could not be processed.');
|
||||
|
||||
importFile.mockImplementation(() => ({
|
||||
importLegacyFile.mockImplementation(() => ({
|
||||
invalid: true,
|
||||
}));
|
||||
|
||||
await component.instance().import();
|
||||
await component.instance().legacyImport();
|
||||
expect(component.state('error')).toBe(
|
||||
'Saved objects file format is invalid and cannot be imported.'
|
||||
);
|
||||
|
@ -132,6 +163,157 @@ describe('Flyout', () => {
|
|||
|
||||
describe('conflicts', () => {
|
||||
const { importFile } = require('../../../../../lib/import_file');
|
||||
const { resolveImportErrors } = require('../../../../../lib/resolve_import_errors');
|
||||
|
||||
beforeEach(() => {
|
||||
importFile.mockImplementation(() => ({
|
||||
success: false,
|
||||
successCount: 0,
|
||||
errors: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'visualization',
|
||||
title: 'My Visualization',
|
||||
error: {
|
||||
type: 'missing_references',
|
||||
references: [
|
||||
{
|
||||
id: 'MyIndexPattern*',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
],
|
||||
}));
|
||||
resolveImportErrors.mockImplementation(() => ({
|
||||
status: 'success',
|
||||
importCount: 1,
|
||||
failedImports: [],
|
||||
}));
|
||||
});
|
||||
|
||||
it('should figure out unmatchedReferences', async () => {
|
||||
const component = shallowWithIntl(<Flyout.WrappedComponent {...defaultProps} />);
|
||||
|
||||
// Ensure all promises resolve
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
// Ensure the state changes are reflected
|
||||
component.update();
|
||||
|
||||
component.setState({ file: mockFile, isLegacyFile: false });
|
||||
await component.instance().import();
|
||||
|
||||
expect(importFile).toHaveBeenCalledWith(mockFile, true);
|
||||
expect(component.state()).toMatchObject({
|
||||
conflictedIndexPatterns: undefined,
|
||||
conflictedSavedObjectsLinkedToSavedSearches: undefined,
|
||||
conflictedSearchDocs: undefined,
|
||||
importCount: 0,
|
||||
status: 'idle',
|
||||
error: undefined,
|
||||
unmatchedReferences: [
|
||||
{
|
||||
existingIndexPatternId: 'MyIndexPattern*',
|
||||
newIndexPatternId: undefined,
|
||||
list: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'visualization',
|
||||
title: 'My Visualization',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow conflict resolution', async () => {
|
||||
const component = shallowWithIntl(<Flyout.WrappedComponent {...defaultProps} />);
|
||||
|
||||
// Ensure all promises resolve
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
// Ensure the state changes are reflected
|
||||
component.update();
|
||||
|
||||
component.setState({ file: mockFile, isLegacyFile: false });
|
||||
await component.instance().import();
|
||||
|
||||
// Ensure it looks right
|
||||
component.update();
|
||||
expect(component).toMatchSnapshot();
|
||||
|
||||
// Ensure we can change the resolution
|
||||
component
|
||||
.instance()
|
||||
.onIndexChanged('MyIndexPattern*', { target: { value: '2' } });
|
||||
expect(component.state('unmatchedReferences')[0].newIndexPatternId).toBe('2');
|
||||
|
||||
// Let's resolve now
|
||||
await component
|
||||
.find('EuiButton[data-test-subj="importSavedObjectsConfirmBtn"]')
|
||||
.simulate('click');
|
||||
// Ensure all promises resolve
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
expect(resolveImportErrors).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle errors', async () => {
|
||||
const component = shallowWithIntl(<Flyout.WrappedComponent {...defaultProps} />);
|
||||
|
||||
// Ensure all promises resolve
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
// Ensure the state changes are reflected
|
||||
component.update();
|
||||
|
||||
resolveImportErrors.mockImplementation(() => ({
|
||||
status: 'success',
|
||||
importCount: 0,
|
||||
failedImports: [
|
||||
{
|
||||
obj: {
|
||||
type: 'visualization',
|
||||
id: '1',
|
||||
},
|
||||
error: {
|
||||
type: 'unknown',
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
component.setState({ file: mockFile, isLegacyFile: false });
|
||||
|
||||
// Go through the import flow
|
||||
await component.instance().import();
|
||||
component.update();
|
||||
// Set a resolution
|
||||
component
|
||||
.instance()
|
||||
.onIndexChanged('MyIndexPattern*', { target: { value: '2' } });
|
||||
await component
|
||||
.find('EuiButton[data-test-subj="importSavedObjectsConfirmBtn"]')
|
||||
.simulate('click');
|
||||
// Ensure all promises resolve
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
|
||||
expect(component.state('failedImports')).toEqual([
|
||||
{
|
||||
error: {
|
||||
type: 'unknown',
|
||||
},
|
||||
obj: {
|
||||
id: '1',
|
||||
type: 'visualization',
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(component.find('EuiFlyoutBody EuiCallOut')).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacy conflicts', () => {
|
||||
const { importLegacyFile } = require('../../../../../lib/import_legacy_file');
|
||||
const {
|
||||
resolveSavedObjects,
|
||||
resolveSavedSearches,
|
||||
|
@ -175,7 +357,7 @@ describe('Flyout', () => {
|
|||
const mockConflictedSearchDocs = [3];
|
||||
|
||||
beforeEach(() => {
|
||||
importFile.mockImplementation(() => mockData);
|
||||
importLegacyFile.mockImplementation(() => mockData);
|
||||
resolveSavedObjects.mockImplementation(() => ({
|
||||
conflictedIndexPatterns: mockConflictedIndexPatterns,
|
||||
conflictedSavedObjectsLinkedToSavedSearches: mockConflictedSavedObjectsLinkedToSavedSearches,
|
||||
|
@ -184,7 +366,7 @@ describe('Flyout', () => {
|
|||
}));
|
||||
});
|
||||
|
||||
it('should figure out conflicts', async () => {
|
||||
it('should figure out unmatchedReferences', async () => {
|
||||
const component = shallowWithIntl(<Flyout.WrappedComponent {...defaultProps} />);
|
||||
|
||||
// Ensure all promises resolve
|
||||
|
@ -192,10 +374,10 @@ describe('Flyout', () => {
|
|||
// Ensure the state changes are reflected
|
||||
component.update();
|
||||
|
||||
component.setState({ file: mockFile });
|
||||
await component.instance().import();
|
||||
component.setState({ file: legacyMockFile, isLegacyFile: true });
|
||||
await component.instance().legacyImport();
|
||||
|
||||
expect(importFile).toHaveBeenCalledWith(mockFile);
|
||||
expect(importLegacyFile).toHaveBeenCalledWith(legacyMockFile);
|
||||
// Remove the last element from data since it should be filtered out
|
||||
expect(resolveSavedObjects).toHaveBeenCalledWith(
|
||||
mockData.slice(0, 2).map((doc) => ({ ...doc, _migrationVersion: {} })),
|
||||
|
@ -209,16 +391,16 @@ describe('Flyout', () => {
|
|||
conflictedSavedObjectsLinkedToSavedSearches: mockConflictedSavedObjectsLinkedToSavedSearches,
|
||||
conflictedSearchDocs: mockConflictedSearchDocs,
|
||||
importCount: 2,
|
||||
isLoading: false,
|
||||
wasImportSuccessful: false,
|
||||
conflicts: [
|
||||
status: 'idle',
|
||||
error: undefined,
|
||||
unmatchedReferences: [
|
||||
{
|
||||
existingIndexPatternId: 'MyIndexPattern*',
|
||||
newIndexPatternId: undefined,
|
||||
list: [
|
||||
{
|
||||
id: 'MyIndexPattern*',
|
||||
name: 'MyIndexPattern*',
|
||||
title: 'MyIndexPattern*',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
|
@ -235,8 +417,8 @@ describe('Flyout', () => {
|
|||
// Ensure the state changes are reflected
|
||||
component.update();
|
||||
|
||||
component.setState({ file: mockFile });
|
||||
await component.instance().import();
|
||||
component.setState({ file: legacyMockFile, isLegacyFile: true });
|
||||
await component.instance().legacyImport();
|
||||
|
||||
// Ensure it looks right
|
||||
component.update();
|
||||
|
@ -246,7 +428,7 @@ describe('Flyout', () => {
|
|||
component
|
||||
.instance()
|
||||
.onIndexChanged('MyIndexPattern*', { target: { value: '2' } });
|
||||
expect(component.state('conflicts')[0].newIndexPatternId).toBe('2');
|
||||
expect(component.state('unmatchedReferences')[0].newIndexPatternId).toBe('2');
|
||||
|
||||
// Let's resolve now
|
||||
await component
|
||||
|
@ -283,10 +465,10 @@ describe('Flyout', () => {
|
|||
throw new Error('foobar');
|
||||
});
|
||||
|
||||
component.setState({ file: mockFile });
|
||||
component.setState({ file: legacyMockFile, isLegacyFile: true });
|
||||
|
||||
// Go through the import flow
|
||||
await component.instance().import();
|
||||
await component.instance().legacyImport();
|
||||
component.update();
|
||||
// Set a resolution
|
||||
component
|
||||
|
|
|
@ -41,8 +41,17 @@ import {
|
|||
EuiCallOut,
|
||||
EuiSpacer,
|
||||
EuiLink,
|
||||
EuiConfirmModal,
|
||||
EuiOverlayMask,
|
||||
EUI_MODAL_CONFIRM_BUTTON,
|
||||
} from '@elastic/eui';
|
||||
import { importFile } from '../../../../lib/import_file';
|
||||
import {
|
||||
importFile,
|
||||
importLegacyFile,
|
||||
resolveImportErrors,
|
||||
logLegacyImport,
|
||||
processImportResponse,
|
||||
} from '../../../../lib';
|
||||
import {
|
||||
resolveSavedObjects,
|
||||
resolveSavedSearches,
|
||||
|
@ -68,15 +77,16 @@ class FlyoutUI extends Component {
|
|||
conflictedIndexPatterns: undefined,
|
||||
conflictedSavedObjectsLinkedToSavedSearches: undefined,
|
||||
conflictedSearchDocs: undefined,
|
||||
conflicts: undefined,
|
||||
unmatchedReferences: undefined,
|
||||
conflictingRecord: undefined,
|
||||
error: undefined,
|
||||
file: undefined,
|
||||
importCount: 0,
|
||||
indexPatterns: undefined,
|
||||
isOverwriteAllChecked: true,
|
||||
isLoading: false,
|
||||
loadingMessage: undefined,
|
||||
wasImportSuccessful: false,
|
||||
isLegacyFile: false,
|
||||
status: 'idle',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -99,22 +109,33 @@ class FlyoutUI extends Component {
|
|||
};
|
||||
|
||||
setImportFile = ([file]) => {
|
||||
this.setState({ file });
|
||||
if (!file) {
|
||||
this.setState({ file: undefined, isLegacyFile: false });
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
file,
|
||||
isLegacyFile: /\.json$/i.test(file.name) || file.type === 'application/json',
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Import
|
||||
*
|
||||
* Does the initial import of a file, resolveImportErrors then handles errors and retries
|
||||
*/
|
||||
import = async () => {
|
||||
const { services, indexPatterns, intl } = this.props;
|
||||
const { intl } = this.props;
|
||||
const { file, isOverwriteAllChecked } = this.state;
|
||||
this.setState({ status: 'loading', error: undefined });
|
||||
|
||||
this.setState({ isLoading: true, error: undefined });
|
||||
|
||||
let contents;
|
||||
|
||||
// Import the file
|
||||
let response;
|
||||
try {
|
||||
contents = await importFile(file);
|
||||
response = await importFile(file, isOverwriteAllChecked);
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
status: 'error',
|
||||
error: intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.flyout.importFileErrorMessage',
|
||||
defaultMessage: 'The file could not be processed.',
|
||||
|
@ -123,9 +144,99 @@ class FlyoutUI extends Component {
|
|||
return;
|
||||
}
|
||||
|
||||
this.setState(processImportResponse(response), () => {
|
||||
// Resolve import errors right away if there's no index patterns to match
|
||||
// This will ask about overwriting each object, etc
|
||||
if (this.state.unmatchedReferences.length === 0) {
|
||||
this.resolveImportErrors();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Conflict Resolutions
|
||||
*
|
||||
* Function iterates through the objects, displays a modal for each asking the user if they wish to overwrite it or not.
|
||||
*
|
||||
* @param {array} objects List of objects to request the user if they wish to overwrite it
|
||||
* @return {Promise<array>} An object with the key being "type:id" and value the resolution chosen by the user
|
||||
*/
|
||||
getConflictResolutions = async (objects) => {
|
||||
const resolutions = {};
|
||||
for (const { type, id, title } of objects) {
|
||||
const overwrite = await new Promise((resolve) => {
|
||||
this.setState({
|
||||
conflictingRecord: {
|
||||
id,
|
||||
type,
|
||||
title,
|
||||
done: resolve,
|
||||
},
|
||||
});
|
||||
});
|
||||
resolutions[`${type}:${id}`] = overwrite;
|
||||
this.setState({ conflictingRecord: undefined });
|
||||
}
|
||||
return resolutions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve Import Errors
|
||||
*
|
||||
* Function goes through the failedImports and tries to resolve the issues.
|
||||
*/
|
||||
resolveImportErrors = async () => {
|
||||
const { intl } = this.props;
|
||||
|
||||
this.setState({
|
||||
error: undefined,
|
||||
status: 'loading',
|
||||
loadingMessage: undefined,
|
||||
});
|
||||
|
||||
try {
|
||||
const updatedState = await resolveImportErrors({
|
||||
state: this.state,
|
||||
getConflictResolutions: this.getConflictResolutions,
|
||||
});
|
||||
this.setState(updatedState);
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
status: 'error',
|
||||
error: intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.flyout.resolveImportErrorsFileErrorMessage',
|
||||
defaultMessage: 'The file could not be processed.',
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
legacyImport = async () => {
|
||||
const { services, indexPatterns, intl } = this.props;
|
||||
const { file, isOverwriteAllChecked } = this.state;
|
||||
|
||||
this.setState({ status: 'loading', error: undefined });
|
||||
|
||||
// Log warning on server, don't wait for response
|
||||
logLegacyImport();
|
||||
|
||||
let contents;
|
||||
try {
|
||||
contents = await importLegacyFile(file);
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
status: 'error',
|
||||
error: intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.flyout.importLegacyFileErrorMessage',
|
||||
defaultMessage: 'The file could not be processed.',
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(contents)) {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
status: 'error',
|
||||
error: intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.flyout.invalidFormatOfImportedFileErrorMessage',
|
||||
defaultMessage: 'Saved objects file format is invalid and cannot be imported.',
|
||||
|
@ -162,7 +273,7 @@ class FlyoutUI extends Component {
|
|||
const byId = groupBy(conflictedIndexPatterns, ({ obj }) =>
|
||||
obj.searchSource.getOwnField('index')
|
||||
);
|
||||
const conflicts = Object.entries(byId).reduce(
|
||||
const unmatchedReferences = Object.entries(byId).reduce(
|
||||
(accum, [existingIndexPatternId, list]) => {
|
||||
accum.push({
|
||||
existingIndexPatternId,
|
||||
|
@ -170,7 +281,7 @@ class FlyoutUI extends Component {
|
|||
list: list.map(({ doc }) => ({
|
||||
id: existingIndexPatternId,
|
||||
type: doc._type,
|
||||
name: doc._source.title,
|
||||
title: doc._source.title,
|
||||
})),
|
||||
});
|
||||
return accum;
|
||||
|
@ -183,19 +294,18 @@ class FlyoutUI extends Component {
|
|||
conflictedSavedObjectsLinkedToSavedSearches,
|
||||
conflictedSearchDocs,
|
||||
failedImports,
|
||||
conflicts,
|
||||
unmatchedReferences,
|
||||
importCount: importedObjectCount,
|
||||
isLoading: false,
|
||||
wasImportSuccessful: conflicts.length === 0,
|
||||
status: unmatchedReferences.length === 0 ? 'success' : 'idle',
|
||||
});
|
||||
};
|
||||
|
||||
get hasConflicts() {
|
||||
return this.state.conflicts && this.state.conflicts.length > 0;
|
||||
get hasUnmatchedReferences() {
|
||||
return this.state.unmatchedReferences && this.state.unmatchedReferences.length > 0;
|
||||
}
|
||||
|
||||
get resolutions() {
|
||||
return this.state.conflicts.reduce(
|
||||
return this.state.unmatchedReferences.reduce(
|
||||
(accum, { existingIndexPatternId, newIndexPatternId }) => {
|
||||
if (newIndexPatternId) {
|
||||
accum.push({
|
||||
|
@ -209,7 +319,7 @@ class FlyoutUI extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
confirmImport = async () => {
|
||||
confirmLegacyImport = async () => {
|
||||
const {
|
||||
conflictedIndexPatterns,
|
||||
isOverwriteAllChecked,
|
||||
|
@ -222,20 +332,20 @@ class FlyoutUI extends Component {
|
|||
|
||||
this.setState({
|
||||
error: undefined,
|
||||
isLoading: true,
|
||||
status: 'loading',
|
||||
loadingMessage: undefined,
|
||||
});
|
||||
|
||||
let importCount = this.state.importCount;
|
||||
|
||||
if (this.hasConflicts) {
|
||||
if (this.hasUnmatchedReferences) {
|
||||
try {
|
||||
const resolutions = this.resolutions;
|
||||
|
||||
// Do not Promise.all these calls as the order matters
|
||||
this.setState({
|
||||
loadingMessage: intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.flyout.confirmImport.resolvingConflictsLoadingMessage',
|
||||
id: 'kbn.management.objects.objectsTable.flyout.confirmLegacyImport.resolvingConflictsLoadingMessage',
|
||||
defaultMessage: 'Resolving conflicts…',
|
||||
}),
|
||||
});
|
||||
|
@ -248,7 +358,7 @@ class FlyoutUI extends Component {
|
|||
}
|
||||
this.setState({
|
||||
loadingMessage: intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.flyout.confirmImport.savingConflictsLoadingMessage',
|
||||
id: 'kbn.management.objects.objectsTable.flyout.confirmLegacyImport.savingConflictsLoadingMessage',
|
||||
defaultMessage: 'Saving conflicts…',
|
||||
}),
|
||||
});
|
||||
|
@ -258,7 +368,7 @@ class FlyoutUI extends Component {
|
|||
);
|
||||
this.setState({
|
||||
loadingMessage: intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.flyout.confirmImport.savedSearchAreLinkedProperlyLoadingMessage',
|
||||
id: 'kbn.management.objects.objectsTable.flyout.confirmLegacyImport.savedSearchAreLinkedProperlyLoadingMessage',
|
||||
defaultMessage: 'Ensure saved searches are linked properly…',
|
||||
}),
|
||||
});
|
||||
|
@ -270,7 +380,7 @@ class FlyoutUI extends Component {
|
|||
);
|
||||
this.setState({
|
||||
loadingMessage: intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.flyout.confirmImport.retryingFailedObjectsLoadingMessage',
|
||||
id: 'kbn.management.objects.objectsTable.flyout.confirmLegacyImport.retryingFailedObjectsLoadingMessage',
|
||||
defaultMessage: 'Retrying failed objects…',
|
||||
}),
|
||||
});
|
||||
|
@ -281,20 +391,20 @@ class FlyoutUI extends Component {
|
|||
} catch (e) {
|
||||
this.setState({
|
||||
error: e.message,
|
||||
isLoading: false,
|
||||
status: 'error',
|
||||
loadingMessage: undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ isLoading: false, wasImportSuccessful: true, importCount });
|
||||
this.setState({ status: 'success', importCount });
|
||||
};
|
||||
|
||||
onIndexChanged = (id, e) => {
|
||||
const value = e.target.value;
|
||||
this.setState(state => {
|
||||
const conflictIndex = state.conflicts.findIndex(
|
||||
const conflictIndex = state.unmatchedReferences.findIndex(
|
||||
conflict => conflict.existingIndexPatternId === id
|
||||
);
|
||||
if (conflictIndex === -1) {
|
||||
|
@ -302,23 +412,23 @@ class FlyoutUI extends Component {
|
|||
}
|
||||
|
||||
return {
|
||||
conflicts: [
|
||||
...state.conflicts.slice(0, conflictIndex),
|
||||
unmatchedReferences: [
|
||||
...state.unmatchedReferences.slice(0, conflictIndex),
|
||||
{
|
||||
...state.conflicts[conflictIndex],
|
||||
...state.unmatchedReferences[conflictIndex],
|
||||
newIndexPatternId: value,
|
||||
},
|
||||
...state.conflicts.slice(conflictIndex + 1),
|
||||
...state.unmatchedReferences.slice(conflictIndex + 1),
|
||||
],
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
renderConflicts() {
|
||||
const { conflicts } = this.state;
|
||||
renderUnmatchedReferences() {
|
||||
const { unmatchedReferences } = this.state;
|
||||
const { intl } = this.props;
|
||||
|
||||
if (!conflicts) {
|
||||
if (!unmatchedReferences) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -362,7 +472,7 @@ class FlyoutUI extends Component {
|
|||
render: list => {
|
||||
return (
|
||||
<ul style={{ listStyle: 'none' }}>
|
||||
{take(list, 3).map((obj, key) => <li key={key}>{obj.name}</li>)}
|
||||
{take(list, 3).map((obj, key) => <li key={key}>{obj.title}</li>)}
|
||||
</ul>
|
||||
);
|
||||
},
|
||||
|
@ -402,7 +512,7 @@ class FlyoutUI extends Component {
|
|||
|
||||
return (
|
||||
<EuiInMemoryTable
|
||||
items={conflicts}
|
||||
items={unmatchedReferences}
|
||||
columns={columns}
|
||||
pagination={pagination}
|
||||
/>
|
||||
|
@ -410,9 +520,9 @@ class FlyoutUI extends Component {
|
|||
}
|
||||
|
||||
renderError() {
|
||||
const { error } = this.state;
|
||||
const { error, status } = this.state;
|
||||
|
||||
if (!error) {
|
||||
if (status !== 'error') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -433,16 +543,17 @@ class FlyoutUI extends Component {
|
|||
}
|
||||
|
||||
renderBody() {
|
||||
const { intl } = this.props;
|
||||
const {
|
||||
isLoading,
|
||||
status,
|
||||
loadingMessage,
|
||||
isOverwriteAllChecked,
|
||||
wasImportSuccessful,
|
||||
importCount,
|
||||
failedImports = [],
|
||||
isLegacyFile,
|
||||
} = this.state;
|
||||
|
||||
if (isLoading) {
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
<EuiFlexGroup justifyContent="spaceAround">
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -456,7 +567,8 @@ class FlyoutUI extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
if (failedImports.length && !this.hasConflicts) {
|
||||
// Kept backwards compatible logic
|
||||
if (failedImports.length && (!this.hasUnmatchedReferences || (isLegacyFile === false && status === 'success'))) {
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={(
|
||||
|
@ -468,18 +580,36 @@ class FlyoutUI extends Component {
|
|||
<p>
|
||||
<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.flyout.importFailedDescription"
|
||||
defaultMessage="Failed to import {failedImportCount} of {totalImportCount} objects.Import failed"
|
||||
defaultMessage="Failed to import {failedImportCount} of {totalImportCount} objects. Import failed"
|
||||
values={{ failedImportCount: failedImports.length, totalImportCount: importCount + failedImports.length, }}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
{failedImports.map(({ error }) => getField(error, 'body.message', error.message || '')).join(' ')}
|
||||
{failedImports.map(({ error, obj }) => {
|
||||
if (error.type === 'missing_references') {
|
||||
return error.references.map((reference) => {
|
||||
return intl.formatMessage(
|
||||
{
|
||||
id: 'kbn.management.objects.objectsTable.flyout.importFailedMissingReference',
|
||||
defaultMessage: '{type} [id={id}] could not locate {refType} [id={refId}]',
|
||||
},
|
||||
{
|
||||
id: obj.id,
|
||||
type: obj.type,
|
||||
refId: reference.id,
|
||||
refType: reference.type,
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
return getField(error, 'body.message', error.message || '');
|
||||
}).join(' ')}
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
);
|
||||
}
|
||||
|
||||
if (wasImportSuccessful) {
|
||||
if (status === 'success') {
|
||||
if (importCount === 0) {
|
||||
return (
|
||||
<EuiCallOut
|
||||
|
@ -518,8 +648,8 @@ class FlyoutUI extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
if (this.hasConflicts) {
|
||||
return this.renderConflicts();
|
||||
if (this.hasUnmatchedReferences) {
|
||||
return this.renderUnmatchedReferences();
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -528,7 +658,7 @@ class FlyoutUI extends Component {
|
|||
label={(
|
||||
<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.flyout.selectFileToImportFormRowLabel"
|
||||
defaultMessage="Please select a JSON file to import"
|
||||
defaultMessage="Please select a file to import"
|
||||
/>
|
||||
)}
|
||||
>
|
||||
|
@ -561,12 +691,12 @@ class FlyoutUI extends Component {
|
|||
}
|
||||
|
||||
renderFooter() {
|
||||
const { isLoading, wasImportSuccessful } = this.state;
|
||||
const { status } = this.state;
|
||||
const { done, close } = this.props;
|
||||
|
||||
let confirmButton;
|
||||
|
||||
if (wasImportSuccessful) {
|
||||
if (status === 'success') {
|
||||
confirmButton = (
|
||||
<EuiButton
|
||||
onClick={done}
|
||||
|
@ -580,13 +710,13 @@ class FlyoutUI extends Component {
|
|||
/>
|
||||
</EuiButton>
|
||||
);
|
||||
} else if (this.hasConflicts) {
|
||||
} else if (this.hasUnmatchedReferences) {
|
||||
confirmButton = (
|
||||
<EuiButton
|
||||
onClick={this.confirmImport}
|
||||
onClick={this.state.isLegacyFile ? this.confirmLegacyImport : this.resolveImportErrors}
|
||||
size="s"
|
||||
fill
|
||||
isLoading={isLoading}
|
||||
isLoading={status === 'loading'}
|
||||
data-test-subj="importSavedObjectsConfirmBtn"
|
||||
>
|
||||
<FormattedMessage
|
||||
|
@ -598,10 +728,10 @@ class FlyoutUI extends Component {
|
|||
} else {
|
||||
confirmButton = (
|
||||
<EuiButton
|
||||
onClick={this.import}
|
||||
onClick={this.state.isLegacyFile ? this.legacyImport : this.import}
|
||||
size="s"
|
||||
fill
|
||||
isLoading={isLoading}
|
||||
isLoading={status === 'loading'}
|
||||
data-test-subj="importSavedObjectsImportBtn"
|
||||
>
|
||||
<FormattedMessage
|
||||
|
@ -629,16 +759,38 @@ class FlyoutUI extends Component {
|
|||
|
||||
renderSubheader() {
|
||||
if (
|
||||
!this.hasConflicts ||
|
||||
this.state.isLoading ||
|
||||
this.state.wasImportSuccessful
|
||||
this.state.status === 'loading' ||
|
||||
this.state.status === 'success'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiSpacer size="s" />
|
||||
let legacyFileWarning;
|
||||
if (this.state.isLegacyFile) {
|
||||
legacyFileWarning = (
|
||||
<EuiCallOut
|
||||
title={(
|
||||
<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.flyout.legacyFileUsedTitle"
|
||||
defaultMessage="Support for JSON files is going away"
|
||||
/>
|
||||
)}
|
||||
color="warning"
|
||||
iconType="help"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.flyout.legacyFileUsedBody"
|
||||
defaultMessage="Use our updated export to generate NDJSON files, and you'll be all set."
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
);
|
||||
}
|
||||
|
||||
let indexPatternConflictsWarning;
|
||||
if (this.hasUnmatchedReferences) {
|
||||
indexPatternConflictsWarning = (
|
||||
<EuiCallOut
|
||||
title={(
|
||||
<FormattedMessage
|
||||
|
@ -667,13 +819,80 @@ class FlyoutUI extends Component {
|
|||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
</EuiCallOut>);
|
||||
}
|
||||
|
||||
if (!legacyFileWarning && !indexPatternConflictsWarning) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{legacyFileWarning &&
|
||||
<span>
|
||||
<EuiSpacer size="s" />
|
||||
{legacyFileWarning}
|
||||
</span>
|
||||
}
|
||||
{indexPatternConflictsWarning &&
|
||||
<span>
|
||||
<EuiSpacer size="s" />
|
||||
{indexPatternConflictsWarning}
|
||||
</span>
|
||||
}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
overwriteConfirmed() {
|
||||
this.state.conflictingRecord.done(true);
|
||||
}
|
||||
|
||||
overwriteSkipped() {
|
||||
this.state.conflictingRecord.done(false);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { close } = this.props;
|
||||
const { close, intl } = this.props;
|
||||
|
||||
let confirmOverwriteModal;
|
||||
if (this.state.conflictingRecord) {
|
||||
confirmOverwriteModal = (
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
title={intl.formatMessage(
|
||||
{
|
||||
id: 'kbn.management.objects.objectsTable.flyout.confirmOverwriteTitle',
|
||||
defaultMessage: 'Overwrite {type}?'
|
||||
},
|
||||
{ type: this.state.conflictingRecord.type }
|
||||
)}
|
||||
cancelButtonText={intl.formatMessage(
|
||||
{
|
||||
id: 'kbn.management.objects.objectsTable.flyout.confirmOverwriteCancelButtonText',
|
||||
defaultMessage: 'Cancel',
|
||||
},
|
||||
)}
|
||||
confirmButtonText={intl.formatMessage(
|
||||
{
|
||||
id: 'kbn.management.objects.objectsTable.flyout.confirmOverwriteOverwriteButtonText',
|
||||
defaultMessage: 'Overwrite',
|
||||
},
|
||||
)}
|
||||
onCancel={this.overwriteSkipped.bind(this)}
|
||||
onConfirm={this.overwriteConfirmed.bind(this)}
|
||||
defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.flyout.confirmOverwriteBody"
|
||||
defaultMessage="Are you sure you want to overwrite {title}?"
|
||||
values={{ title: this.state.conflictingRecord.title }}
|
||||
/>
|
||||
</p>
|
||||
</EuiConfirmModal>
|
||||
</EuiOverlayMask>);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlyout onClose={close} size="s">
|
||||
|
@ -695,6 +914,7 @@ class FlyoutUI extends Component {
|
|||
</EuiFlyoutBody>
|
||||
|
||||
<EuiFlyoutFooter>{this.renderFooter()}</EuiFlyoutFooter>
|
||||
{confirmOverwriteModal}
|
||||
</EuiFlyout>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -20,6 +20,8 @@
|
|||
import React from 'react';
|
||||
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
|
||||
jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() }));
|
||||
|
||||
jest.mock('ui/errors', () => ({
|
||||
SavedObjectNotFound: class SavedObjectNotFound extends Error {
|
||||
constructor(options) {
|
||||
|
@ -37,6 +39,14 @@ jest.mock('ui/chrome', () => ({
|
|||
addBasePath: () => ''
|
||||
}));
|
||||
|
||||
jest.mock('../../../../../lib/fetch_export_by_type', () => ({
|
||||
fetchExportByType: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../../lib/fetch_export_objects', () => ({
|
||||
fetchExportObjects: jest.fn(),
|
||||
}));
|
||||
|
||||
import { Relationships } from '../relationships';
|
||||
|
||||
describe('Relationships', () => {
|
||||
|
|
|
@ -42,22 +42,81 @@ exports[`Table should render normally 1`] = `
|
|||
values={Object {}}
|
||||
/>
|
||||
</EuiButton>,
|
||||
<EuiButton
|
||||
color="primary"
|
||||
fill={false}
|
||||
iconSide="left"
|
||||
iconType="exportAction"
|
||||
isDisabled={false}
|
||||
onClick={[Function]}
|
||||
size="m"
|
||||
type="button"
|
||||
<EuiPopover
|
||||
anchorPosition="downCenter"
|
||||
button={
|
||||
<EuiButton
|
||||
color="primary"
|
||||
fill={false}
|
||||
iconSide="right"
|
||||
iconType="arrowDown"
|
||||
isDisabled={false}
|
||||
onClick={[Function]}
|
||||
size="m"
|
||||
type="button"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Export"
|
||||
id="kbn.management.objects.objectsTable.table.exportPopoverButtonLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiButton>
|
||||
}
|
||||
closePopover={[Function]}
|
||||
hasArrow={true}
|
||||
isOpen={false}
|
||||
ownFocus={false}
|
||||
panelPaddingSize="m"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Export"
|
||||
id="kbn.management.objects.objectsTable.table.exportButtonLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiButton>,
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Options"
|
||||
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportOptionsLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiSwitch
|
||||
checked={true}
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Include related objects"
|
||||
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.includeReferencesDeepLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
name="includeReferencesDeep"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
fill={true}
|
||||
iconSide="left"
|
||||
iconType="exportAction"
|
||||
onClick={[Function]}
|
||||
size="m"
|
||||
type="button"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Export"
|
||||
id="kbn.management.objects.objectsTable.table.exportButtonLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFormRow>
|
||||
</EuiPopover>,
|
||||
]
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -22,6 +22,8 @@ import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers';
|
|||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
import { keyCodes } from '@elastic/eui/lib/services';
|
||||
|
||||
jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() }));
|
||||
|
||||
jest.mock('ui/errors', () => ({
|
||||
SavedObjectNotFound: class SavedObjectNotFound extends Error {
|
||||
constructor(options) {
|
||||
|
|
|
@ -28,7 +28,10 @@ import {
|
|||
EuiLink,
|
||||
EuiSpacer,
|
||||
EuiToolTip,
|
||||
EuiFormErrorText
|
||||
EuiFormErrorText,
|
||||
EuiPopover,
|
||||
EuiSwitch,
|
||||
EuiFormRow
|
||||
} from '@elastic/eui';
|
||||
import { getSavedObjectLabel, getSavedObjectIcon } from '../../../../lib';
|
||||
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
|
||||
|
@ -65,6 +68,8 @@ class TableUI extends PureComponent {
|
|||
state = {
|
||||
isSearchTextValid: true,
|
||||
parseErrorMessage: null,
|
||||
isExportPopoverOpen: false,
|
||||
isIncludeReferencesDeepChecked: true,
|
||||
}
|
||||
|
||||
onChange = ({ query, error }) => {
|
||||
|
@ -83,6 +88,29 @@ class TableUI extends PureComponent {
|
|||
this.props.onQueryChange({ query });
|
||||
}
|
||||
|
||||
closeExportPopover = () => {
|
||||
this.setState({ isExportPopoverOpen: false });
|
||||
}
|
||||
|
||||
toggleExportPopoverVisibility = () => {
|
||||
this.setState(state => ({
|
||||
isExportPopoverOpen: !state.isExportPopoverOpen
|
||||
}));
|
||||
}
|
||||
|
||||
toggleIsIncludeReferencesDeepChecked = () => {
|
||||
this.setState(state => ({
|
||||
isIncludeReferencesDeepChecked: !state.isIncludeReferencesDeepChecked,
|
||||
}));
|
||||
}
|
||||
|
||||
onExportClick = () => {
|
||||
const { onExport } = this.props;
|
||||
const { isIncludeReferencesDeepChecked } = this.state;
|
||||
onExport(isIncludeReferencesDeepChecked);
|
||||
this.setState({ isExportPopoverOpen: false });
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
pageIndex,
|
||||
|
@ -94,7 +122,6 @@ class TableUI extends PureComponent {
|
|||
filterOptions,
|
||||
selectionConfig: selection,
|
||||
onDelete,
|
||||
onExport,
|
||||
selectedSavedObjects,
|
||||
onTableChange,
|
||||
goInApp,
|
||||
|
@ -216,6 +243,20 @@ class TableUI extends PureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
const button = (
|
||||
<EuiButton
|
||||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
onClick={this.toggleExportPopoverVisibility}
|
||||
isDisabled={selectedSavedObjects.length === 0}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.table.exportPopoverButtonLabel"
|
||||
defaultMessage="Export"
|
||||
/>
|
||||
</EuiButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiSearchBar
|
||||
|
@ -235,17 +276,46 @@ class TableUI extends PureComponent {
|
|||
defaultMessage="Delete"
|
||||
/>
|
||||
</EuiButton>,
|
||||
<EuiButton
|
||||
key="exportSO"
|
||||
iconType="exportAction"
|
||||
onClick={onExport}
|
||||
isDisabled={selectedSavedObjects.length === 0}
|
||||
<EuiPopover
|
||||
key="exportSOOptions"
|
||||
button={button}
|
||||
isOpen={this.state.isExportPopoverOpen}
|
||||
closePopover={this.closeExportPopover}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.table.exportButtonLabel"
|
||||
defaultMessage="Export"
|
||||
/>
|
||||
</EuiButton>,
|
||||
<EuiFormRow
|
||||
label={(
|
||||
<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportOptionsLabel"
|
||||
defaultMessage="Options"
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<EuiSwitch
|
||||
name="includeReferencesDeep"
|
||||
label={(
|
||||
<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.includeReferencesDeepLabel"
|
||||
defaultMessage="Include related objects"
|
||||
/>
|
||||
)}
|
||||
checked={this.state.isIncludeReferencesDeepChecked}
|
||||
onChange={this.toggleIsIncludeReferencesDeepChecked}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow>
|
||||
<EuiButton
|
||||
key="exportSO"
|
||||
iconType="exportAction"
|
||||
onClick={this.onExportClick}
|
||||
fill
|
||||
>
|
||||
<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.table.exportButtonLabel"
|
||||
defaultMessage="Export"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFormRow>
|
||||
</EuiPopover>,
|
||||
]}
|
||||
/>
|
||||
{queryParseError}
|
||||
|
|
|
@ -17,9 +17,10 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { saveAs } from '@elastic/filesaver';
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { debounce, flattenDeep } from 'lodash';
|
||||
import { debounce } from 'lodash';
|
||||
import { Header } from './components/header';
|
||||
import { Flyout } from './components/flyout';
|
||||
import { Relationships } from './components/relationships';
|
||||
|
@ -38,16 +39,26 @@ import {
|
|||
EuiCheckboxGroup,
|
||||
EuiToolTip,
|
||||
EuiPageContent,
|
||||
EuiSwitch,
|
||||
EuiModal,
|
||||
EuiModalHeader,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiButtonEmpty,
|
||||
EuiButton,
|
||||
EuiModalHeaderTitle,
|
||||
EuiText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
retrieveAndExportDocs,
|
||||
scanAllTypes,
|
||||
saveToFile,
|
||||
parseQuery,
|
||||
getSavedObjectIcon,
|
||||
getSavedObjectCounts,
|
||||
getRelationships,
|
||||
getSavedObjectLabel,
|
||||
fetchExportObjects,
|
||||
fetchExportByType,
|
||||
} from '../../lib';
|
||||
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
|
||||
|
||||
|
@ -97,6 +108,7 @@ class ObjectsTableUI extends Component {
|
|||
isDeleting: false,
|
||||
exportAllOptions: [],
|
||||
exportAllSelectedOptions: {},
|
||||
isIncludeReferencesDeepChecked: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -278,17 +290,23 @@ class ObjectsTableUI extends Component {
|
|||
});
|
||||
};
|
||||
|
||||
onExport = async () => {
|
||||
const { savedObjectsClient } = this.props;
|
||||
onExport = async (includeReferencesDeep) => {
|
||||
const { intl } = this.props;
|
||||
const { selectedSavedObjects } = this.state;
|
||||
const objects = await savedObjectsClient.bulkGet(selectedSavedObjects);
|
||||
await retrieveAndExportDocs(objects.savedObjects, savedObjectsClient);
|
||||
const objectsToExport = selectedSavedObjects.map(obj => ({ id: obj.id, type: obj.type }));
|
||||
const blob = await fetchExportObjects(objectsToExport, includeReferencesDeep);
|
||||
saveAs(blob, 'export.ndjson');
|
||||
toastNotifications.addSuccess({
|
||||
title: intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.export.successNotification',
|
||||
defaultMessage: 'Your file is downloading in the background',
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
onExportAll = async () => {
|
||||
const { $http } = this.props;
|
||||
const { exportAllSelectedOptions } = this.state;
|
||||
|
||||
const { intl } = this.props;
|
||||
const { exportAllSelectedOptions, isIncludeReferencesDeepChecked } = this.state;
|
||||
const exportTypes = Object.entries(exportAllSelectedOptions).reduce(
|
||||
(accum, [id, selected]) => {
|
||||
if (selected) {
|
||||
|
@ -298,8 +316,15 @@ class ObjectsTableUI extends Component {
|
|||
},
|
||||
[]
|
||||
);
|
||||
const results = await scanAllTypes($http, exportTypes);
|
||||
saveToFile(JSON.stringify(flattenDeep(results), null, 2));
|
||||
const blob = await fetchExportByType(exportTypes, isIncludeReferencesDeepChecked);
|
||||
saveAs(blob, 'export.ndjson');
|
||||
toastNotifications.addSuccess({
|
||||
title: intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.exportAll.successNotification',
|
||||
defaultMessage: 'Your file is downloading in the background',
|
||||
}),
|
||||
});
|
||||
this.setState({ isShowingExportAllOptionsModal: false });
|
||||
};
|
||||
|
||||
finishImport = () => {
|
||||
|
@ -512,12 +537,23 @@ class ObjectsTableUI extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
changeIncludeReferencesDeep = () => {
|
||||
this.setState(state => ({
|
||||
isIncludeReferencesDeepChecked: !state.isIncludeReferencesDeepChecked,
|
||||
}));
|
||||
}
|
||||
|
||||
closeExportAllModal = () => {
|
||||
this.setState({ isShowingExportAllOptionsModal: false });
|
||||
}
|
||||
|
||||
renderExportAllOptionsModal() {
|
||||
const {
|
||||
isShowingExportAllOptionsModal,
|
||||
filteredItemCount,
|
||||
exportAllOptions,
|
||||
exportAllSelectedOptions,
|
||||
isIncludeReferencesDeepChecked,
|
||||
} = this.state;
|
||||
|
||||
if (!isShowingExportAllOptionsModal) {
|
||||
|
@ -526,53 +562,84 @@ class ObjectsTableUI extends Component {
|
|||
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
title={(<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.exportObjectsConfirmModalTitle"
|
||||
defaultMessage="Export {filteredItemCount, plural, one{# object} other {# objects}}"
|
||||
values={{
|
||||
filteredItemCount
|
||||
}}
|
||||
/>)}
|
||||
onCancel={() =>
|
||||
this.setState({ isShowingExportAllOptionsModal: false })
|
||||
}
|
||||
onConfirm={this.onExportAll}
|
||||
cancelButtonText={(
|
||||
<FormattedMessage id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.cancelButtonLabel" defaultMessage="Cancel"/>
|
||||
)}
|
||||
confirmButtonText={(
|
||||
<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportAllButtonLabel"
|
||||
defaultMessage="Export All"
|
||||
/>
|
||||
)}
|
||||
defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON}
|
||||
<EuiModal
|
||||
onClose={this.closeExportAllModal}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.exportObjectsConfirmModalDescription"
|
||||
defaultMessage="Select which types to export. The number in parentheses indicates
|
||||
how many of this type are available to export."
|
||||
/>
|
||||
</p>
|
||||
<EuiCheckboxGroup
|
||||
options={exportAllOptions}
|
||||
idToSelectedMap={exportAllSelectedOptions}
|
||||
onChange={optionId => {
|
||||
const newExportAllSelectedOptions = {
|
||||
...exportAllSelectedOptions,
|
||||
...{
|
||||
[optionId]: !exportAllSelectedOptions[optionId],
|
||||
},
|
||||
};
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.exportObjectsConfirmModalTitle"
|
||||
defaultMessage="Export {filteredItemCount, plural, one{# object} other {# objects}}"
|
||||
values={{
|
||||
filteredItemCount
|
||||
}}
|
||||
/>
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<EuiText>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.exportObjectsConfirmModalDescription"
|
||||
defaultMessage="Select which types to export."
|
||||
/>
|
||||
</p>
|
||||
<EuiCheckboxGroup
|
||||
options={exportAllOptions}
|
||||
idToSelectedMap={exportAllSelectedOptions}
|
||||
onChange={optionId => {
|
||||
const newExportAllSelectedOptions = {
|
||||
...exportAllSelectedOptions,
|
||||
...{
|
||||
[optionId]: !exportAllSelectedOptions[optionId],
|
||||
},
|
||||
};
|
||||
|
||||
this.setState({
|
||||
exportAllSelectedOptions: newExportAllSelectedOptions,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiConfirmModal>
|
||||
this.setState({
|
||||
exportAllSelectedOptions: newExportAllSelectedOptions,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiSwitch
|
||||
name="includeReferencesDeep"
|
||||
label={(
|
||||
<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.includeReferencesDeepLabel"
|
||||
defaultMessage="Include related objects"
|
||||
/>
|
||||
)}
|
||||
checked={isIncludeReferencesDeepChecked}
|
||||
onChange={this.changeIncludeReferencesDeep}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={this.closeExportAllModal}>
|
||||
<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton fill onClick={this.onExportAll}>
|
||||
<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportAllButtonLabel"
|
||||
defaultMessage="Export all"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { importFile } from '../import_file';
|
||||
import { importLegacyFile } from '../import_legacy_file';
|
||||
|
||||
describe('importFile', () => {
|
||||
it('should import a file', async () => {
|
||||
|
@ -33,7 +33,7 @@ describe('importFile', () => {
|
|||
|
||||
const file = 'foo';
|
||||
|
||||
const imported = await importFile(file, FileReader);
|
||||
const imported = await importLegacyFile(file, FileReader);
|
||||
expect(imported).toEqual({ text: file });
|
||||
});
|
||||
|
||||
|
@ -51,7 +51,7 @@ describe('importFile', () => {
|
|||
const file = 'foo';
|
||||
|
||||
try {
|
||||
await importFile(file, FileReader);
|
||||
await importLegacyFile(file, FileReader);
|
||||
} catch (e) {
|
||||
// There isn't a great way to handle throwing exceptions
|
||||
// with async/await but this seems to work :shrug:
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* 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 { processImportResponse } from '../process_import_response';
|
||||
|
||||
describe('processImportResponse()', () => {
|
||||
test('works when no errors exist in the response', () => {
|
||||
const response = {
|
||||
success: true,
|
||||
successCount: 0,
|
||||
};
|
||||
const result = processImportResponse(response);
|
||||
expect(result.status).toBe('success');
|
||||
expect(result.importCount).toBe(0);
|
||||
});
|
||||
|
||||
test('conflict errors get added to failedImports', () => {
|
||||
const response = {
|
||||
success: false,
|
||||
successCount: 0,
|
||||
errors: [
|
||||
{
|
||||
obj: {
|
||||
type: 'a',
|
||||
id: '1',
|
||||
},
|
||||
error: {
|
||||
type: 'conflict',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = processImportResponse(response);
|
||||
expect(result.failedImports).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"error": Object {
|
||||
"type": "conflict",
|
||||
},
|
||||
"obj": Object {
|
||||
"obj": Object {
|
||||
"id": "1",
|
||||
"type": "a",
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('unknown errors get added to failedImports', () => {
|
||||
const response = {
|
||||
success: false,
|
||||
successCount: 0,
|
||||
errors: [
|
||||
{
|
||||
obj: {
|
||||
type: 'a',
|
||||
id: '1',
|
||||
},
|
||||
error: {
|
||||
type: 'unknown',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = processImportResponse(response);
|
||||
expect(result.failedImports).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"error": Object {
|
||||
"type": "unknown",
|
||||
},
|
||||
"obj": Object {
|
||||
"obj": Object {
|
||||
"id": "1",
|
||||
"type": "a",
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('missing references get added to failedImports', () => {
|
||||
const response = {
|
||||
success: false,
|
||||
successCount: 0,
|
||||
errors: [
|
||||
{
|
||||
obj: {
|
||||
type: 'a',
|
||||
id: '1',
|
||||
},
|
||||
error: {
|
||||
type: 'missing_references',
|
||||
references: [
|
||||
{
|
||||
type: 'index-pattern',
|
||||
id: '2',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = processImportResponse(response);
|
||||
expect(result.failedImports).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"error": Object {
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "2",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "missing_references",
|
||||
},
|
||||
"obj": Object {
|
||||
"obj": Object {
|
||||
"id": "1",
|
||||
"type": "a",
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,377 @@
|
|||
/*
|
||||
* 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 { resolveImportErrors } from '../resolve_import_errors';
|
||||
|
||||
jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() }));
|
||||
|
||||
function getFormData(form) {
|
||||
const formData = {};
|
||||
for (const [key, val] of form.entries()) {
|
||||
if (key === 'retries') {
|
||||
formData[key] = JSON.parse(val);
|
||||
continue;
|
||||
}
|
||||
formData[key] = val;
|
||||
}
|
||||
return formData;
|
||||
}
|
||||
|
||||
describe('resolveImportErrors', () => {
|
||||
const getConflictResolutions = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('works with empty import failures', async () => {
|
||||
const result = await resolveImportErrors({
|
||||
getConflictResolutions,
|
||||
state: {
|
||||
importCount: 0,
|
||||
},
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"failedImports": Array [],
|
||||
"importCount": 0,
|
||||
"status": "success",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test(`doesn't retry if only unknown failures are passed in`, async () => {
|
||||
const result = await resolveImportErrors({
|
||||
getConflictResolutions,
|
||||
state: {
|
||||
importCount: 0,
|
||||
failedImports: [
|
||||
{
|
||||
obj: {
|
||||
type: 'a',
|
||||
id: '1',
|
||||
},
|
||||
error: {
|
||||
type: 'unknown',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"failedImports": Array [
|
||||
Object {
|
||||
"error": Object {
|
||||
"type": "unknown",
|
||||
},
|
||||
"obj": Object {
|
||||
"id": "1",
|
||||
"type": "a",
|
||||
},
|
||||
},
|
||||
],
|
||||
"importCount": 0,
|
||||
"status": "success",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('resolves conflicts', async () => {
|
||||
const { kfetch } = require('ui/kfetch');
|
||||
kfetch.mockResolvedValueOnce({
|
||||
success: true,
|
||||
successCount: 1,
|
||||
});
|
||||
getConflictResolutions.mockReturnValueOnce({
|
||||
'a:1': true,
|
||||
'a:2': false,
|
||||
});
|
||||
const result = await resolveImportErrors({
|
||||
getConflictResolutions,
|
||||
state: {
|
||||
importCount: 0,
|
||||
failedImports: [
|
||||
{
|
||||
obj: {
|
||||
type: 'a',
|
||||
id: '1',
|
||||
},
|
||||
error: {
|
||||
type: 'conflict',
|
||||
},
|
||||
},
|
||||
{
|
||||
obj: {
|
||||
type: 'a',
|
||||
id: '2',
|
||||
},
|
||||
error: {
|
||||
type: 'conflict',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"failedImports": Array [],
|
||||
"importCount": 1,
|
||||
"status": "success",
|
||||
}
|
||||
`);
|
||||
const formData = getFormData(kfetch.mock.calls[0][0].body);
|
||||
expect(formData).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"file": "undefined",
|
||||
"retries": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"overwrite": true,
|
||||
"replaceReferences": Array [],
|
||||
"type": "a",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('resolves missing references', async () => {
|
||||
const { kfetch } = require('ui/kfetch');
|
||||
kfetch.mockResolvedValueOnce({
|
||||
success: true,
|
||||
successCount: 2,
|
||||
});
|
||||
getConflictResolutions.mockResolvedValueOnce({});
|
||||
const result = await resolveImportErrors({
|
||||
getConflictResolutions,
|
||||
state: {
|
||||
importCount: 0,
|
||||
unmatchedReferences: [
|
||||
{
|
||||
existingIndexPatternId: '2',
|
||||
newIndexPatternId: '3',
|
||||
},
|
||||
],
|
||||
failedImports: [
|
||||
{
|
||||
obj: {
|
||||
type: 'a',
|
||||
id: '1',
|
||||
},
|
||||
error: {
|
||||
type: 'missing_references',
|
||||
references: [
|
||||
{
|
||||
type: 'index-pattern',
|
||||
id: '2',
|
||||
},
|
||||
],
|
||||
blocking: [
|
||||
{
|
||||
type: 'a',
|
||||
id: '2',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"failedImports": Array [],
|
||||
"importCount": 2,
|
||||
"status": "success",
|
||||
}
|
||||
`);
|
||||
const formData = getFormData(kfetch.mock.calls[0][0].body);
|
||||
expect(formData).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"file": "undefined",
|
||||
"retries": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"overwrite": false,
|
||||
"replaceReferences": Array [
|
||||
Object {
|
||||
"from": "2",
|
||||
"to": "3",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "a",
|
||||
},
|
||||
Object {
|
||||
"id": "2",
|
||||
"type": "a",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test(`doesn't resolve missing references if newIndexPatternId isn't defined`, async () => {
|
||||
getConflictResolutions.mockResolvedValueOnce({});
|
||||
const result = await resolveImportErrors({
|
||||
getConflictResolutions,
|
||||
state: {
|
||||
importCount: 0,
|
||||
unmatchedReferences: [
|
||||
{
|
||||
existingIndexPatternId: '2',
|
||||
newIndexPatternId: undefined,
|
||||
},
|
||||
],
|
||||
failedImports: [
|
||||
{
|
||||
obj: {
|
||||
type: 'a',
|
||||
id: '1',
|
||||
},
|
||||
error: {
|
||||
type: 'missing_references',
|
||||
references: [
|
||||
{
|
||||
type: 'index-pattern',
|
||||
id: '2',
|
||||
},
|
||||
],
|
||||
blocking: [
|
||||
{
|
||||
type: 'a',
|
||||
id: '2',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"failedImports": Array [],
|
||||
"importCount": 0,
|
||||
"status": "success",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('handles missing references then conflicts on the same errored objects', async () => {
|
||||
const { kfetch } = require('ui/kfetch');
|
||||
kfetch.mockResolvedValueOnce({
|
||||
success: false,
|
||||
successCount: 0,
|
||||
errors: [
|
||||
{
|
||||
type: 'a',
|
||||
id: '1',
|
||||
error: {
|
||||
type: 'conflict',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
kfetch.mockResolvedValueOnce({
|
||||
success: true,
|
||||
successCount: 1,
|
||||
});
|
||||
getConflictResolutions.mockResolvedValueOnce({});
|
||||
getConflictResolutions.mockResolvedValueOnce({
|
||||
'a:1': true,
|
||||
});
|
||||
const result = await resolveImportErrors({
|
||||
getConflictResolutions,
|
||||
state: {
|
||||
importCount: 0,
|
||||
unmatchedReferences: [
|
||||
{
|
||||
existingIndexPatternId: '2',
|
||||
newIndexPatternId: '3',
|
||||
},
|
||||
],
|
||||
failedImports: [
|
||||
{
|
||||
obj: {
|
||||
type: 'a',
|
||||
id: '1',
|
||||
},
|
||||
error: {
|
||||
type: 'missing_references',
|
||||
references: [
|
||||
{
|
||||
type: 'index-pattern',
|
||||
id: '2',
|
||||
},
|
||||
],
|
||||
blocking: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"failedImports": Array [],
|
||||
"importCount": 1,
|
||||
"status": "success",
|
||||
}
|
||||
`);
|
||||
const formData1 = getFormData(kfetch.mock.calls[0][0].body);
|
||||
expect(formData1).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"file": "undefined",
|
||||
"retries": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"overwrite": false,
|
||||
"replaceReferences": Array [
|
||||
Object {
|
||||
"from": "2",
|
||||
"to": "3",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "a",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
const formData2 = getFormData(kfetch.mock.calls[1][0].body);
|
||||
expect(formData2).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"file": "undefined",
|
||||
"retries": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"overwrite": true,
|
||||
"replaceReferences": Array [
|
||||
Object {
|
||||
"from": "2",
|
||||
"to": "3",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "a",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -1,108 +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 { retrieveAndExportDocs } from '../retrieve_and_export_docs';
|
||||
|
||||
jest.mock('../save_to_file', () => ({
|
||||
saveToFile: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('ui/errors', () => ({
|
||||
SavedObjectNotFound: class SavedObjectNotFound extends Error {
|
||||
constructor(options) {
|
||||
super();
|
||||
for (const option in options) {
|
||||
if (options.hasOwnProperty(option)) {
|
||||
this[option] = options[option];
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('ui/chrome', () => ({
|
||||
addBasePath: () => {},
|
||||
}));
|
||||
|
||||
describe('retrieveAndExportDocs', () => {
|
||||
let saveToFile;
|
||||
|
||||
beforeEach(() => {
|
||||
saveToFile = require('../save_to_file').saveToFile;
|
||||
saveToFile.mockClear();
|
||||
});
|
||||
|
||||
it('should fetch all', async () => {
|
||||
const savedObjectsClient = {
|
||||
bulkGet: jest.fn().mockImplementation(() => ({
|
||||
savedObjects: [],
|
||||
})),
|
||||
};
|
||||
|
||||
const objs = [1, 2, 3];
|
||||
await retrieveAndExportDocs(objs, savedObjectsClient);
|
||||
expect(savedObjectsClient.bulkGet.mock.calls.length).toBe(1);
|
||||
expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith(objs);
|
||||
});
|
||||
|
||||
it('should use the saveToFile utility', async () => {
|
||||
const savedObjectsClient = {
|
||||
bulkGet: jest.fn().mockImplementation(() => ({
|
||||
savedObjects: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'index-pattern',
|
||||
attributes: {
|
||||
title: 'foobar',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'search',
|
||||
attributes: {
|
||||
title: 'just the foo',
|
||||
},
|
||||
},
|
||||
],
|
||||
})),
|
||||
};
|
||||
|
||||
const objs = [1, 2, 3];
|
||||
await retrieveAndExportDocs(objs, savedObjectsClient);
|
||||
expect(saveToFile.mock.calls.length).toBe(1);
|
||||
expect(saveToFile).toHaveBeenCalledWith(
|
||||
JSON.stringify(
|
||||
[
|
||||
{
|
||||
_id: 1,
|
||||
_type: 'index-pattern',
|
||||
_source: { title: 'foobar' },
|
||||
},
|
||||
{
|
||||
_id: 2,
|
||||
_type: 'search',
|
||||
_source: { title: 'just the foo' },
|
||||
},
|
||||
],
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
|
@ -17,22 +17,15 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { saveToFile } from '../save_to_file';
|
||||
import { kfetch } from 'ui/kfetch';
|
||||
|
||||
jest.mock('@elastic/filesaver', () => ({
|
||||
saveAs: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('saveToFile', () => {
|
||||
let saveAs;
|
||||
|
||||
beforeEach(() => {
|
||||
saveAs = require('@elastic/filesaver').saveAs;
|
||||
saveAs.mockClear();
|
||||
export async function fetchExportByType(types, includeReferencesDeep = false) {
|
||||
return await kfetch({
|
||||
method: 'POST',
|
||||
pathname: '/api/saved_objects/_export',
|
||||
body: JSON.stringify({
|
||||
type: types,
|
||||
includeReferencesDeep,
|
||||
}),
|
||||
});
|
||||
|
||||
it('should use the file saver utility', async () => {
|
||||
saveToFile(JSON.stringify({ foo: 1 }));
|
||||
expect(saveAs.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { kfetch } from 'ui/kfetch';
|
||||
|
||||
export async function fetchExportObjects(objects, includeReferencesDeep = false) {
|
||||
return await kfetch({
|
||||
method: 'POST',
|
||||
pathname: '/api/saved_objects/_export',
|
||||
body: JSON.stringify({
|
||||
objects,
|
||||
includeReferencesDeep,
|
||||
}),
|
||||
});
|
||||
}
|
|
@ -17,16 +17,21 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export async function importFile(file, FileReader = window.FileReader) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fr = new FileReader();
|
||||
fr.onload = ({ target: { result } }) => {
|
||||
try {
|
||||
resolve(JSON.parse(result));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
fr.readAsText(file);
|
||||
import { kfetch } from 'ui/kfetch';
|
||||
|
||||
export async function importFile(file, overwriteAll = false) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
return await kfetch({
|
||||
method: 'POST',
|
||||
pathname: '/api/saved_objects/_import',
|
||||
body: formData,
|
||||
headers: {
|
||||
// Important to be undefined, it forces proper headers to be set for FormData
|
||||
'Content-Type': undefined,
|
||||
},
|
||||
query: {
|
||||
overwrite: overwriteAll
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -17,19 +17,16 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { saveToFile } from './';
|
||||
|
||||
export async function retrieveAndExportDocs(objs, savedObjectsClient) {
|
||||
const response = await savedObjectsClient.bulkGet(objs);
|
||||
const objects = response.savedObjects.map(obj => {
|
||||
return {
|
||||
_id: obj.id,
|
||||
_type: obj.type,
|
||||
_source: obj.attributes,
|
||||
_migrationVersion: obj.migrationVersion,
|
||||
_references: obj.references,
|
||||
export async function importLegacyFile(file, FileReader = window.FileReader) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fr = new FileReader();
|
||||
fr.onload = ({ target: { result } }) => {
|
||||
try {
|
||||
resolve(JSON.parse(result));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
fr.readAsText(file);
|
||||
});
|
||||
|
||||
saveToFile(JSON.stringify(objects, null, 2));
|
||||
}
|
|
@ -17,14 +17,17 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export * from './fetch_export_by_type';
|
||||
export * from './fetch_export_objects';
|
||||
export * from './get_in_app_url';
|
||||
export * from './get_relationships';
|
||||
export * from './get_saved_object_counts';
|
||||
export * from './get_saved_object_icon';
|
||||
export * from './get_saved_object_label';
|
||||
export * from './import_file';
|
||||
export * from './import_legacy_file';
|
||||
export * from './parse_query';
|
||||
export * from './resolve_import_errors';
|
||||
export * from './resolve_saved_objects';
|
||||
export * from './retrieve_and_export_docs';
|
||||
export * from './save_to_file';
|
||||
export * from './scan_all_types';
|
||||
export * from './log_legacy_import';
|
||||
export * from './process_import_response';
|
||||
|
|
|
@ -17,9 +17,11 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { saveAs } from '@elastic/filesaver';
|
||||
import { kfetch } from 'ui/kfetch';
|
||||
|
||||
export function saveToFile(resultsJson) {
|
||||
const blob = new Blob([resultsJson], { type: 'application/json' });
|
||||
saveAs(blob, 'export.json');
|
||||
export async function logLegacyImport() {
|
||||
return await kfetch({
|
||||
method: 'POST',
|
||||
pathname: '/api/saved_objects/_log_legacy_import',
|
||||
});
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export function processImportResponse(response) {
|
||||
// Go through the failures and split between unmatchedReferences and failedImports
|
||||
const failedImports = [];
|
||||
const unmatchedReferences = new Map();
|
||||
for (const { error, ...obj } of response.errors || []) {
|
||||
failedImports.push({ obj, error });
|
||||
if (error.type !== 'missing_references') {
|
||||
continue;
|
||||
}
|
||||
// Currently only supports resolving references on index patterns
|
||||
const indexPatternRefs = error.references.filter(ref => ref.type === 'index-pattern');
|
||||
for (const missingReference of indexPatternRefs) {
|
||||
const conflict = unmatchedReferences.get(`${missingReference.type}:${missingReference.id}`) || {
|
||||
existingIndexPatternId: missingReference.id,
|
||||
list: [],
|
||||
newIndexPatternId: undefined,
|
||||
};
|
||||
conflict.list.push(obj);
|
||||
unmatchedReferences.set(`${missingReference.type}:${missingReference.id}`, conflict);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
failedImports,
|
||||
unmatchedReferences: Array.from(unmatchedReferences.values()),
|
||||
// Import won't be successful in the scenario unmatched references exist, import API returned errors of type unknown or import API
|
||||
// returned errors of type missing_references.
|
||||
status: unmatchedReferences.size === 0 && !failedImports.some(issue => issue.error.type === 'conflict')
|
||||
? 'success'
|
||||
: 'idle',
|
||||
importCount: response.successCount,
|
||||
conflictedSavedObjectsLinkedToSavedSearches: undefined,
|
||||
conflictedSearchDocs: undefined,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* 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 { kfetch } from 'ui/kfetch';
|
||||
|
||||
async function callResolveImportErrorsApi(file, retries) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('retries', JSON.stringify(retries));
|
||||
return await kfetch({
|
||||
method: 'POST',
|
||||
pathname: '/api/saved_objects/_resolve_import_errors',
|
||||
headers: {
|
||||
// Important to be undefined, it forces proper headers to be set for FormData
|
||||
'Content-Type': undefined,
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
}
|
||||
|
||||
function mapImportFailureToRetryObject({ failure, overwriteDecisionCache, replaceReferencesCache, state }) {
|
||||
const { isOverwriteAllChecked, unmatchedReferences } = state;
|
||||
const isOverwriteGranted = isOverwriteAllChecked || overwriteDecisionCache.get(`${failure.obj.type}:${failure.obj.id}`) === true;
|
||||
|
||||
// Conflicts wihtout overwrite granted are skipped
|
||||
if (!isOverwriteGranted && failure.error.type === 'conflict') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace references if user chose a new reference
|
||||
if (failure.error.type === 'missing_references') {
|
||||
const objReplaceReferences = replaceReferencesCache.get(`${failure.obj.type}:${failure.obj.id}`) || [];
|
||||
const indexPatternRefs = failure.error.references.filter(obj => obj.type === 'index-pattern');
|
||||
for (const reference of indexPatternRefs) {
|
||||
for (const unmatchedReference of unmatchedReferences) {
|
||||
const hasNewValue = !!unmatchedReference.newIndexPatternId;
|
||||
const matchesIndexPatternId = unmatchedReference.existingIndexPatternId === reference.id;
|
||||
if (!hasNewValue || !matchesIndexPatternId) {
|
||||
continue;
|
||||
}
|
||||
objReplaceReferences.push({
|
||||
type: 'index-pattern',
|
||||
from: unmatchedReference.existingIndexPatternId,
|
||||
to: unmatchedReference.newIndexPatternId,
|
||||
});
|
||||
}
|
||||
}
|
||||
replaceReferencesCache.set(`${failure.obj.type}:${failure.obj.id}`, objReplaceReferences);
|
||||
// Skip if nothing to replace, the UI option selected would be --Skip Import--
|
||||
if (objReplaceReferences.length === 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: failure.obj.id,
|
||||
type: failure.obj.type,
|
||||
overwrite: isOverwriteAllChecked || overwriteDecisionCache.get(`${failure.obj.type}:${failure.obj.id}`) === true,
|
||||
replaceReferences: replaceReferencesCache.get(`${failure.obj.type}:${failure.obj.id}`) || [],
|
||||
};
|
||||
}
|
||||
|
||||
export async function resolveImportErrors({ getConflictResolutions, state }) {
|
||||
const overwriteDecisionCache = new Map();
|
||||
const replaceReferencesCache = new Map();
|
||||
let { importCount: successImportCount, failedImports: importFailures = [] } = state;
|
||||
const { file, isOverwriteAllChecked } = state;
|
||||
|
||||
const doesntHaveOverwriteDecision = ({ obj }) => {
|
||||
return !overwriteDecisionCache.has(`${obj.type}:${obj.id}`);
|
||||
};
|
||||
const getOverwriteDecision = ({ obj }) => {
|
||||
return overwriteDecisionCache.get(`${obj.type}:${obj.id}`);
|
||||
};
|
||||
const callMapImportFailure = (failure) => {
|
||||
return mapImportFailureToRetryObject({ failure, overwriteDecisionCache, replaceReferencesCache, state });
|
||||
};
|
||||
const isNotSkipped = (failure) => {
|
||||
return (failure.error.type !== 'conflict' && failure.error.type !== 'missing_references') ||
|
||||
getOverwriteDecision(failure);
|
||||
};
|
||||
|
||||
// Loop until all issues are resolved
|
||||
while (importFailures.some(failure => ['conflict', 'missing_references'].includes(failure.error.type))) {
|
||||
// Ask for overwrites
|
||||
if (!isOverwriteAllChecked) {
|
||||
const result = await getConflictResolutions(
|
||||
importFailures
|
||||
.filter(({ error }) => error.type === 'conflict')
|
||||
.filter(doesntHaveOverwriteDecision)
|
||||
.map(({ obj }) => obj)
|
||||
);
|
||||
for (const key of Object.keys(result)) {
|
||||
overwriteDecisionCache.set(key, result[key]);
|
||||
}
|
||||
}
|
||||
|
||||
// Build retries array
|
||||
const retries = importFailures
|
||||
.map(callMapImportFailure)
|
||||
.filter(obj => !!obj);
|
||||
for (const { error, obj } of importFailures) {
|
||||
if (error.type !== 'missing_references') {
|
||||
continue;
|
||||
}
|
||||
if (!retries.some(retryObj => retryObj.type === obj.type && retryObj.id === obj.id)) {
|
||||
continue;
|
||||
}
|
||||
for (const { type, id } of error.blocking || []) {
|
||||
retries.push({ type, id });
|
||||
}
|
||||
}
|
||||
|
||||
// Scenario where everything is skipped and nothing to retry
|
||||
if (retries.length === 0) {
|
||||
// Cancelled overwrites aren't failures anymore
|
||||
importFailures = importFailures.filter(isNotSkipped);
|
||||
break;
|
||||
}
|
||||
|
||||
// Call API
|
||||
const response = await callResolveImportErrorsApi(file, retries);
|
||||
successImportCount += response.successCount;
|
||||
importFailures = [];
|
||||
for (const { error, ...obj } of response.errors || []) {
|
||||
importFailures.push({ error, obj });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
importCount: successImportCount,
|
||||
failedImports: importFailures,
|
||||
};
|
||||
}
|
26
src/legacy/core_plugins/metrics/common/field_types.js
Normal file
26
src/legacy/core_plugins/metrics/common/field_types.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const FIELD_TYPES = {
|
||||
BOOLEAN: 'boolean',
|
||||
DATE: 'date',
|
||||
GEO: 'geo_point',
|
||||
NUMBER: 'number',
|
||||
STRING: 'string',
|
||||
};
|
221
src/legacy/core_plugins/metrics/public/components/splits/__snapshots__/terms.test.js.snap
generated
Normal file
221
src/legacy/core_plugins/metrics/public/components/splits/__snapshots__/terms.test.js.snap
generated
Normal file
|
@ -0,0 +1,221 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`src/legacy/core_plugins/metrics/public/components/splits/terms.test.js <SplitByTermsUI /> should render and match a snapshot 1`] = `
|
||||
<div>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
id="42"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Group by"
|
||||
id="tsvb.splits.terms.groupByLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<InjectIntl(GroupBySelectUi)
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
id="42"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="By"
|
||||
description="This labels a field selector allowing the user to chose 'by' which field to group."
|
||||
id="tsvb.splits.terms.byLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<InjectIntl(FieldSelectUi)
|
||||
fields={
|
||||
Object {
|
||||
"kibana_sample_data_flights": Array [
|
||||
Object {
|
||||
"aggregatable": true,
|
||||
"name": "OriginCityName",
|
||||
"readFromDocValues": true,
|
||||
"searchable": true,
|
||||
"type": "string",
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
indexPattern="kibana_sample_data_flights"
|
||||
onChange={[Function]}
|
||||
value="OriginCityName"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
id="42"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Include"
|
||||
id="tsvb.splits.terms.includeLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldText
|
||||
compressed={false}
|
||||
fullWidth={false}
|
||||
isLoading={false}
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
id="42"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Exclude"
|
||||
id="tsvb.splits.terms.excludeLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldText
|
||||
compressed={false}
|
||||
fullWidth={false}
|
||||
isLoading={false}
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
id="42"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Top"
|
||||
id="tsvb.splits.terms.topLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldNumber
|
||||
compressed={false}
|
||||
fullWidth={false}
|
||||
isLoading={false}
|
||||
onChange={[Function]}
|
||||
value={10}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
id="42"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Order by"
|
||||
id="tsvb.splits.terms.orderByLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<InjectIntl(MetricSelectUi)
|
||||
additionalOptions={
|
||||
Array [
|
||||
Object {
|
||||
"label": undefined,
|
||||
"value": "_count",
|
||||
},
|
||||
Object {
|
||||
"label": undefined,
|
||||
"value": "_key",
|
||||
},
|
||||
]
|
||||
}
|
||||
clearable={false}
|
||||
onChange={[Function]}
|
||||
restrict="basic"
|
||||
value="_count"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
id="42"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Direction"
|
||||
id="tsvb.splits.terms.directionLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiComboBox
|
||||
compressed={false}
|
||||
fullWidth={false}
|
||||
isClearable={false}
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"label": undefined,
|
||||
"value": "desc",
|
||||
},
|
||||
Object {
|
||||
"label": undefined,
|
||||
"value": "asc",
|
||||
},
|
||||
]
|
||||
}
|
||||
selectedOptions={
|
||||
Array [
|
||||
Object {
|
||||
"label": undefined,
|
||||
"value": "desc",
|
||||
},
|
||||
]
|
||||
}
|
||||
singleSelection={
|
||||
Object {
|
||||
"asPlainText": true,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
`;
|
|
@ -19,21 +19,23 @@
|
|||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { get, find } from 'lodash';
|
||||
import GroupBySelect from './group_by_select';
|
||||
import createTextHandler from '../lib/create_text_handler';
|
||||
import createSelectHandler from '../lib/create_select_handler';
|
||||
import FieldSelect from '../aggs/field_select';
|
||||
import MetricSelect from '../aggs/metric_select';
|
||||
import { htmlIdGenerator, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiFieldNumber, EuiComboBox, EuiSpacer } from '@elastic/eui';
|
||||
import { htmlIdGenerator, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiFieldNumber, EuiComboBox, EuiFieldText } from '@elastic/eui';
|
||||
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
|
||||
import { FIELD_TYPES } from '../../../common/field_types';
|
||||
|
||||
const SplitByTermsUi = props => {
|
||||
const DEFAULTS = { terms_direction: 'desc', terms_size: 10, terms_order_by: '_count' };
|
||||
|
||||
export const SplitByTermsUI = ({ onChange, indexPattern, intl, model: seriesModel, fields }) => {
|
||||
const htmlId = htmlIdGenerator();
|
||||
const handleTextChange = createTextHandler(props.onChange);
|
||||
const handleSelectChange = createSelectHandler(props.onChange);
|
||||
const { indexPattern, intl } = props;
|
||||
const defaults = { terms_direction: 'desc', terms_size: 10, terms_order_by: '_count' };
|
||||
const model = { ...defaults, ...props.model };
|
||||
const handleTextChange = createTextHandler(onChange);
|
||||
const handleSelectChange = createSelectHandler(onChange);
|
||||
const model = { ...DEFAULTS, ...seriesModel };
|
||||
const { metrics } = model;
|
||||
const defaultCount = {
|
||||
value: '_count',
|
||||
|
@ -57,10 +59,12 @@ const SplitByTermsUi = props => {
|
|||
const selectedDirectionOption = dirOptions.find(option => {
|
||||
return model.terms_direction === option.value;
|
||||
});
|
||||
const selectedField = find(fields[indexPattern], ({ name }) => name === model.terms_field);
|
||||
const selectedFieldType = get(selectedField, 'type');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id={htmlId('group')}
|
||||
|
@ -88,15 +92,40 @@ const SplitByTermsUi = props => {
|
|||
indexPattern={indexPattern}
|
||||
onChange={handleSelectChange('terms_field')}
|
||||
value={model.terms_field}
|
||||
fields={props.fields}
|
||||
fields={fields}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer />
|
||||
{selectedFieldType === FIELD_TYPES.STRING && (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id={htmlId('include')}
|
||||
label={(<FormattedMessage
|
||||
id="tsvb.splits.terms.includeLabel"
|
||||
defaultMessage="Include"
|
||||
/>)}
|
||||
>
|
||||
<EuiFieldText value={model.terms_include} onChange={handleTextChange('terms_include')} />
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id={htmlId('exclude')}
|
||||
label={(<FormattedMessage
|
||||
id="tsvb.splits.terms.excludeLabel"
|
||||
defaultMessage="Exclude"
|
||||
/>)}
|
||||
>
|
||||
<EuiFieldText value={model.terms_exclude} onChange={handleTextChange('terms_exclude')} />
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id={htmlId('top')}
|
||||
|
@ -152,11 +181,12 @@ const SplitByTermsUi = props => {
|
|||
);
|
||||
};
|
||||
|
||||
SplitByTermsUi.propTypes = {
|
||||
SplitByTermsUI.propTypes = {
|
||||
intl: PropTypes.object,
|
||||
model: PropTypes.object,
|
||||
onChange: PropTypes.func,
|
||||
indexPattern: PropTypes.string,
|
||||
fields: PropTypes.object
|
||||
};
|
||||
|
||||
export const SplitByTerms = injectI18n(SplitByTermsUi);
|
||||
export const SplitByTerms = injectI18n(SplitByTermsUI);
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { SplitByTermsUI } from './terms';
|
||||
|
||||
jest.mock('@elastic/eui', () => ({
|
||||
htmlIdGenerator: jest.fn(() => () => '42'),
|
||||
EuiFlexGroup: require.requireActual('@elastic/eui').EuiFlexGroup,
|
||||
EuiFlexItem: require.requireActual('@elastic/eui').EuiFlexItem,
|
||||
EuiFormRow: require.requireActual('@elastic/eui').EuiFormRow,
|
||||
EuiFieldNumber: require.requireActual('@elastic/eui').EuiFieldNumber,
|
||||
EuiComboBox: require.requireActual('@elastic/eui').EuiComboBox,
|
||||
EuiFieldText: require.requireActual('@elastic/eui').EuiFieldText,
|
||||
}));
|
||||
|
||||
describe('src/legacy/core_plugins/metrics/public/components/splits/terms.test.js', () => {
|
||||
let props;
|
||||
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
intl: {
|
||||
formatMessage: jest.fn(),
|
||||
},
|
||||
model: {
|
||||
terms_field: 'OriginCityName'
|
||||
},
|
||||
onChange: jest.fn(),
|
||||
indexPattern: 'kibana_sample_data_flights',
|
||||
fields: {
|
||||
'kibana_sample_data_flights': [
|
||||
{
|
||||
aggregatable: true,
|
||||
name: 'OriginCityName',
|
||||
readFromDocValues: true,
|
||||
searchable: true,
|
||||
type: 'string'
|
||||
}
|
||||
]
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('<SplitByTermsUI />', () => {
|
||||
test('should render and match a snapshot', () => {
|
||||
const wrapper = shallow(<SplitByTermsUI {...props} />);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -21,12 +21,14 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import * as Rx from 'rxjs';
|
||||
import { share } from 'rxjs/operators';
|
||||
import { isEqual, isEmpty } from 'lodash';
|
||||
import VisEditorVisualization from './vis_editor_visualization';
|
||||
import Visualization from './visualization';
|
||||
import VisPicker from './vis_picker';
|
||||
import PanelConfig from './panel_config';
|
||||
import brushHandler from '../lib/create_brush_handler';
|
||||
import { fetchIndexPatternFields } from '../lib/fetch_fields';
|
||||
import { fetchFields } from '../lib/fetch_fields';
|
||||
import { extractIndexPatterns } from '../lib/extract_index_patterns';
|
||||
|
||||
class VisEditor extends Component {
|
||||
constructor(props) {
|
||||
|
@ -37,7 +39,8 @@ class VisEditor extends Component {
|
|||
model: props.visParams,
|
||||
dirty: false,
|
||||
autoApply: true,
|
||||
visFields: props.visFields
|
||||
visFields: props.visFields,
|
||||
extractedIndexPatterns: [''],
|
||||
};
|
||||
this.onBrush = brushHandler(props.vis.API.timeFilter);
|
||||
this.visDataSubject = new Rx.Subject();
|
||||
|
@ -52,23 +55,45 @@ class VisEditor extends Component {
|
|||
return this.props.config.get(...args);
|
||||
};
|
||||
|
||||
handleUiState = (field, value) => {
|
||||
handleUiState = (field, value) => {
|
||||
this.props.vis.uiStateVal(field, value);
|
||||
};
|
||||
|
||||
handleChange = async (partialModel) => {
|
||||
const nextModel = { ...this.state.model, ...partialModel };
|
||||
this.props.vis.params = nextModel;
|
||||
if (this.state.autoApply) {
|
||||
this.props.vis.updateState();
|
||||
if (isEmpty(partialModel)) {
|
||||
return;
|
||||
}
|
||||
const hasTypeChanged = partialModel.type && this.state.model.type !== partialModel.type;
|
||||
const nextModel = {
|
||||
...this.state.model,
|
||||
...partialModel,
|
||||
};
|
||||
let dirty = true;
|
||||
|
||||
this.props.vis.params = nextModel;
|
||||
|
||||
if (this.state.autoApply || hasTypeChanged) {
|
||||
this.props.vis.updateState();
|
||||
|
||||
dirty = false;
|
||||
}
|
||||
|
||||
if (this.props.isEditorMode) {
|
||||
const { params, fields } = this.props.vis;
|
||||
const extractedIndexPatterns = extractIndexPatterns(params, fields);
|
||||
|
||||
if (!isEqual(this.state.extractedIndexPatterns, extractedIndexPatterns)) {
|
||||
fetchFields(extractedIndexPatterns)
|
||||
.then(visFields => this.setState({
|
||||
visFields,
|
||||
extractedIndexPatterns,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
dirty,
|
||||
model: nextModel,
|
||||
dirty: !this.state.autoApply
|
||||
});
|
||||
const { params, fields } = this.props.vis;
|
||||
fetchIndexPatternFields(params, fields).then(visFields => {
|
||||
this.setState({ visFields });
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -109,7 +134,7 @@ class VisEditor extends Component {
|
|||
return (
|
||||
<div className="tvbEditor">
|
||||
<div className="tvbEditor--hideForReporting">
|
||||
<VisPicker model={model} onChange={this.handleChange} />
|
||||
<VisPicker model={model} onChange={this.handleChange}/>
|
||||
</div>
|
||||
<VisEditorVisualization
|
||||
dirty={this.state.dirty}
|
||||
|
@ -152,7 +177,7 @@ class VisEditor extends Component {
|
|||
}
|
||||
|
||||
VisEditor.defaultProps = {
|
||||
visData: {}
|
||||
visData: {},
|
||||
};
|
||||
|
||||
VisEditor.propTypes = {
|
||||
|
|
|
@ -1,77 +1,89 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { I18nContext } from 'ui/i18n';
|
||||
import chrome from 'ui/chrome';
|
||||
import { fetchIndexPatternFields } from '../lib/fetch_fields';
|
||||
|
||||
function ReactEditorControllerProvider(Private, config) {
|
||||
class ReactEditorController {
|
||||
constructor(el, savedObj) {
|
||||
this.el = el;
|
||||
this.savedObj = savedObj;
|
||||
this.vis = savedObj.vis;
|
||||
this.vis.fields = {};
|
||||
}
|
||||
|
||||
setDefaultIndexPattern = async () => {
|
||||
const savedObjectsClient = chrome.getSavedObjectsClient();
|
||||
const indexPattern = await savedObjectsClient.get('index-pattern', config.get('defaultIndex'));
|
||||
this.vis.params.default_index_pattern = indexPattern.attributes.title;
|
||||
};
|
||||
|
||||
async render(params) {
|
||||
const Component = this.vis.type.editorConfig.component;
|
||||
|
||||
await this.setDefaultIndexPattern();
|
||||
const visFields = await fetchIndexPatternFields(this.vis.params, this.vis.fields);
|
||||
|
||||
render(
|
||||
<I18nContext>
|
||||
<Component
|
||||
config={config}
|
||||
vis={this.vis}
|
||||
visFields={visFields}
|
||||
visParams={this.vis.params}
|
||||
savedObj={this.savedObj}
|
||||
timeRange={params.timeRange}
|
||||
renderComplete={() => {}}
|
||||
isEditorMode={true}
|
||||
appState={params.appState}
|
||||
/>
|
||||
</I18nContext>,
|
||||
this.el);
|
||||
}
|
||||
|
||||
resize() {}
|
||||
|
||||
destroy() {
|
||||
unmountComponentAtNode(this.el);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'react_editor',
|
||||
handler: ReactEditorController
|
||||
};
|
||||
}
|
||||
|
||||
export { ReactEditorControllerProvider };
|
||||
/*
|
||||
* 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 React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { I18nContext } from 'ui/i18n';
|
||||
import chrome from 'ui/chrome';
|
||||
import { fetchIndexPatternFields } from '../lib/fetch_fields';
|
||||
|
||||
function ReactEditorControllerProvider(Private, config) {
|
||||
class ReactEditorController {
|
||||
constructor(el, savedObj) {
|
||||
this.el = el;
|
||||
|
||||
this.state = {
|
||||
savedObj: savedObj,
|
||||
vis: savedObj.vis,
|
||||
isLoaded: false,
|
||||
};
|
||||
}
|
||||
|
||||
fetchDefaultIndexPattern = async () => {
|
||||
const savedObjectsClient = chrome.getSavedObjectsClient();
|
||||
const indexPattern = await savedObjectsClient.get('index-pattern', config.get('defaultIndex'));
|
||||
|
||||
return indexPattern.attributes.title;
|
||||
};
|
||||
|
||||
fetchDefaultParams = async () => {
|
||||
this.state.vis.params.default_index_pattern = await this.fetchDefaultIndexPattern();
|
||||
this.state.vis.fields = await fetchIndexPatternFields(this.state.vis);
|
||||
|
||||
this.state.isLoaded = true;
|
||||
};
|
||||
|
||||
getComponent = () => {
|
||||
return this.state.vis.type.editorConfig.component;
|
||||
};
|
||||
|
||||
async render(params) {
|
||||
const Component = this.getComponent();
|
||||
|
||||
!this.state.isLoaded && await this.fetchDefaultParams();
|
||||
|
||||
render(
|
||||
<I18nContext>
|
||||
<Component
|
||||
config={config}
|
||||
vis={this.state.vis}
|
||||
visFields={this.state.vis.fields}
|
||||
visParams={this.state.vis.params}
|
||||
savedObj={this.state.savedObj}
|
||||
timeRange={params.timeRange}
|
||||
renderComplete={() => {}}
|
||||
isEditorMode={true}
|
||||
appState={params.appState}
|
||||
/>
|
||||
</I18nContext>,
|
||||
this.el);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
unmountComponentAtNode(this.el);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'react_editor',
|
||||
handler: ReactEditorController,
|
||||
};
|
||||
}
|
||||
|
||||
export { ReactEditorControllerProvider };
|
||||
|
|
|
@ -16,8 +16,8 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { uniq } from 'lodash';
|
||||
|
||||
export function extractIndexPatterns(params, fetchedFields) {
|
||||
const patternsToFetch = [];
|
||||
|
||||
|
@ -41,6 +41,9 @@ export function extractIndexPatterns(params, fetchedFields) {
|
|||
});
|
||||
}
|
||||
|
||||
return uniq(patternsToFetch);
|
||||
if (patternsToFetch.length === 0) {
|
||||
patternsToFetch.push('');
|
||||
}
|
||||
|
||||
return uniq(patternsToFetch).sort();
|
||||
}
|
||||
|
|
|
@ -30,27 +30,28 @@ export async function fetchFields(indexPatterns = ['*']) {
|
|||
pathname: '/api/metrics/fields',
|
||||
query: {
|
||||
index: pattern,
|
||||
}
|
||||
},
|
||||
});
|
||||
}));
|
||||
const fields = patterns.reduce((cumulatedFields, currentPattern, index) => {
|
||||
return {
|
||||
...cumulatedFields,
|
||||
[currentPattern]: indexFields[index]
|
||||
[currentPattern]: indexFields[index],
|
||||
};
|
||||
}, {});
|
||||
return fields;
|
||||
} catch(error) {
|
||||
} catch (error) {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate('tsvb.fetchFields.loadIndexPatternFieldsErrorMessage', {
|
||||
defaultMessage: 'Unable to load index_pattern fields'
|
||||
defaultMessage: 'Unable to load index_pattern fields',
|
||||
}),
|
||||
text: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchIndexPatternFields(params, fields) {
|
||||
export async function fetchIndexPatternFields({ params, fields = {} }) {
|
||||
const indexPatterns = extractIndexPatterns(params, fields);
|
||||
|
||||
return await fetchFields(indexPatterns);
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { set } from 'lodash';
|
||||
import basicAggs from '../../../../../common/basic_aggs';
|
||||
import getBucketsPath from '../../helpers/get_buckets_path';
|
||||
import bucketTransform from '../../helpers/bucket_transform';
|
||||
|
@ -26,20 +26,26 @@ export default function splitByTerm(req, panel, series) {
|
|||
return next => doc => {
|
||||
if (series.split_mode === 'terms' && series.terms_field) {
|
||||
const direction = series.terms_direction || 'desc';
|
||||
_.set(doc, `aggs.${series.id}.terms.field`, series.terms_field);
|
||||
_.set(doc, `aggs.${series.id}.terms.size`, series.terms_size);
|
||||
const metric = series.metrics.find(item => item.id === series.terms_order_by);
|
||||
set(doc, `aggs.${series.id}.terms.field`, series.terms_field);
|
||||
set(doc, `aggs.${series.id}.terms.size`, series.terms_size);
|
||||
if (series.terms_include) {
|
||||
set(doc, `aggs.${series.id}.terms.include`, series.terms_include);
|
||||
}
|
||||
if (series.terms_exclude) {
|
||||
set(doc, `aggs.${series.id}.terms.exclude`, series.terms_exclude);
|
||||
}
|
||||
if (metric && metric.type !== 'count' && ~basicAggs.indexOf(metric.type)) {
|
||||
const sortAggKey = `${series.terms_order_by}-SORT`;
|
||||
const fn = bucketTransform[metric.type];
|
||||
const bucketPath = getBucketsPath(series.terms_order_by, series.metrics)
|
||||
.replace(series.terms_order_by, sortAggKey);
|
||||
_.set(doc, `aggs.${series.id}.terms.order`, { [bucketPath]: direction });
|
||||
_.set(doc, `aggs.${series.id}.aggs`, { [sortAggKey]: fn(metric) });
|
||||
set(doc, `aggs.${series.id}.terms.order`, { [bucketPath]: direction });
|
||||
set(doc, `aggs.${series.id}.aggs`, { [sortAggKey]: fn(metric) });
|
||||
} else if (['_key', '_count'].includes(series.terms_order_by)) {
|
||||
_.set(doc, `aggs.${series.id}.terms.order`, { [series.terms_order_by]: direction });
|
||||
set(doc, `aggs.${series.id}.terms.order`, { [series.terms_order_by]: direction });
|
||||
} else {
|
||||
_.set(doc, `aggs.${series.id}.terms.order`, { _count: direction });
|
||||
set(doc, `aggs.${series.id}.terms.order`, { _count: direction });
|
||||
}
|
||||
}
|
||||
return next(doc);
|
||||
|
|
|
@ -23,16 +23,23 @@ export default function timeShift(resp, panel, series) {
|
|||
return next => results => {
|
||||
if (/^([+-]?[\d]+)([shmdwMy]|ms)$/.test(series.offset_time)) {
|
||||
const matches = series.offset_time.match(/^([+-]?[\d]+)([shmdwMy]|ms)$/);
|
||||
|
||||
if (matches) {
|
||||
const offsetValue = matches[1];
|
||||
const offsetValue = Number(matches[1]);
|
||||
const offsetUnit = matches[2];
|
||||
const offset = moment.duration(offsetValue, offsetUnit).valueOf();
|
||||
|
||||
results.forEach(item => {
|
||||
if (_.startsWith(item.id, series.id)) {
|
||||
item.data = item.data.map(row => [moment(row[0]).add(offsetValue, offsetUnit).valueOf(), row[1]]);
|
||||
item.data = item.data.map(([time, value]) => [
|
||||
time + offset,
|
||||
value
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return next(results);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ describe('collectSavedObjects()', () => {
|
|||
Array [
|
||||
Object {
|
||||
"foo": true,
|
||||
"migrationVersion": Object {},
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
@ -60,6 +61,7 @@ Array [
|
|||
Array [
|
||||
Object {
|
||||
"foo": true,
|
||||
"migrationVersion": Object {},
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
|
|
@ -44,6 +44,10 @@ export async function collectSavedObjects(
|
|||
createFilterStream<SavedObject>(obj => !!obj),
|
||||
createLimitStream(objectLimit),
|
||||
createFilterStream<SavedObject>(obj => (filter ? filter(obj) : true)),
|
||||
createMapStream((obj: SavedObject) => {
|
||||
// Ensure migrations execute on every saved object
|
||||
return Object.assign({ migrationVersion: {} }, obj);
|
||||
}),
|
||||
createConcatStream([]),
|
||||
])) as SavedObject[];
|
||||
}
|
||||
|
|
|
@ -124,6 +124,7 @@ Object {
|
|||
"title": "My Index Pattern",
|
||||
},
|
||||
"id": "1",
|
||||
"migrationVersion": Object {},
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
|
@ -132,6 +133,7 @@ Object {
|
|||
"title": "My Search",
|
||||
},
|
||||
"id": "2",
|
||||
"migrationVersion": Object {},
|
||||
"references": Array [],
|
||||
"type": "search",
|
||||
},
|
||||
|
@ -140,6 +142,7 @@ Object {
|
|||
"title": "My Visualization",
|
||||
},
|
||||
"id": "3",
|
||||
"migrationVersion": Object {},
|
||||
"references": Array [],
|
||||
"type": "visualization",
|
||||
},
|
||||
|
@ -148,6 +151,7 @@ Object {
|
|||
"title": "My Dashboard",
|
||||
},
|
||||
"id": "4",
|
||||
"migrationVersion": Object {},
|
||||
"references": Array [],
|
||||
"type": "dashboard",
|
||||
},
|
||||
|
@ -200,6 +204,7 @@ Object {
|
|||
"title": "My Index Pattern",
|
||||
},
|
||||
"id": "1",
|
||||
"migrationVersion": Object {},
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
|
@ -208,6 +213,7 @@ Object {
|
|||
"title": "My Search",
|
||||
},
|
||||
"id": "2",
|
||||
"migrationVersion": Object {},
|
||||
"references": Array [],
|
||||
"type": "search",
|
||||
},
|
||||
|
@ -216,6 +222,7 @@ Object {
|
|||
"title": "My Visualization",
|
||||
},
|
||||
"id": "3",
|
||||
"migrationVersion": Object {},
|
||||
"references": Array [],
|
||||
"type": "visualization",
|
||||
},
|
||||
|
@ -224,6 +231,7 @@ Object {
|
|||
"title": "My Dashboard",
|
||||
},
|
||||
"id": "4",
|
||||
"migrationVersion": Object {},
|
||||
"references": Array [],
|
||||
"type": "dashboard",
|
||||
},
|
||||
|
|
|
@ -141,6 +141,7 @@ Object {
|
|||
"title": "My Visualization",
|
||||
},
|
||||
"id": "3",
|
||||
"migrationVersion": Object {},
|
||||
"references": Array [],
|
||||
"type": "visualization",
|
||||
},
|
||||
|
@ -196,6 +197,7 @@ Object {
|
|||
"title": "My Index Pattern",
|
||||
},
|
||||
"id": "1",
|
||||
"migrationVersion": Object {},
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
|
@ -260,6 +262,7 @@ Object {
|
|||
"title": "My Dashboard",
|
||||
},
|
||||
"id": "4",
|
||||
"migrationVersion": Object {},
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "13",
|
||||
|
|
|
@ -77,6 +77,47 @@ describe('POST /api/saved_objects/_import', () => {
|
|||
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('defaults migrationVersion to empty object', async () => {
|
||||
const request = {
|
||||
method: 'POST',
|
||||
url: '/api/saved_objects/_import',
|
||||
payload: [
|
||||
'--EXAMPLE',
|
||||
'Content-Disposition: form-data; name="file"; filename="export.ndjson"',
|
||||
'Content-Type: application/ndjson',
|
||||
'',
|
||||
'{"type":"index-pattern","id":"my-pattern","attributes":{"title":"my-pattern-*"}}',
|
||||
'--EXAMPLE--',
|
||||
].join('\r\n'),
|
||||
headers: {
|
||||
'content-Type': 'multipart/form-data; boundary=EXAMPLE',
|
||||
},
|
||||
};
|
||||
savedObjectsClient.find.mockResolvedValueOnce({ saved_objects: [] });
|
||||
savedObjectsClient.bulkCreate.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{
|
||||
type: 'index-pattern',
|
||||
id: 'my-pattern',
|
||||
attributes: {
|
||||
title: 'my-pattern-*',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const { payload, statusCode } = await server.inject(request);
|
||||
const response = JSON.parse(payload);
|
||||
expect(statusCode).toBe(200);
|
||||
expect(response).toEqual({
|
||||
success: true,
|
||||
successCount: 1,
|
||||
});
|
||||
expect(savedObjectsClient.bulkCreate.mock.calls).toHaveLength(1);
|
||||
const firstBulkCreateCallArray = savedObjectsClient.bulkCreate.mock.calls[0][0];
|
||||
expect(firstBulkCreateCallArray).toHaveLength(1);
|
||||
expect(firstBulkCreateCallArray[0].migrationVersion).toEqual({});
|
||||
});
|
||||
|
||||
test('imports an index pattern and dashboard', async () => {
|
||||
// NOTE: changes to this scenario should be reflected in the docs
|
||||
const request = {
|
||||
|
|
|
@ -24,6 +24,7 @@ export { createDeleteRoute } from './delete';
|
|||
export { createFindRoute } from './find';
|
||||
export { createGetRoute } from './get';
|
||||
export { createImportRoute } from './import';
|
||||
export { createLogLegacyImportRoute } from './log_legacy_import';
|
||||
export { createResolveImportErrorsRoute } from './resolve_import_errors';
|
||||
export { createUpdateRoute } from './update';
|
||||
export { createExportRoute } from './export';
|
||||
|
|
34
src/legacy/server/saved_objects/routes/log_legacy_import.ts
Normal file
34
src/legacy/server/saved_objects/routes/log_legacy_import.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 Hapi from 'hapi';
|
||||
|
||||
export const createLogLegacyImportRoute = () => ({
|
||||
path: '/api/saved_objects/_log_legacy_import',
|
||||
method: 'POST',
|
||||
options: {
|
||||
handler(request: Hapi.Request) {
|
||||
request.server.log(
|
||||
['warning'],
|
||||
'Importing saved objects from a .json file has been deprecated'
|
||||
);
|
||||
return { success: true };
|
||||
},
|
||||
},
|
||||
});
|
|
@ -77,7 +77,48 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => {
|
|||
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('retries importin a dashboard', async () => {
|
||||
test('defaults migrationVersion to empty object', async () => {
|
||||
const request = {
|
||||
method: 'POST',
|
||||
url: '/api/saved_objects/_resolve_import_errors',
|
||||
payload: [
|
||||
'--EXAMPLE',
|
||||
'Content-Disposition: form-data; name="file"; filename="export.ndjson"',
|
||||
'Content-Type: application/ndjson',
|
||||
'',
|
||||
'{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"}}',
|
||||
'--EXAMPLE',
|
||||
'Content-Disposition: form-data; name="retries"',
|
||||
'',
|
||||
'[{"type":"dashboard","id":"my-dashboard"}]',
|
||||
'--EXAMPLE--',
|
||||
].join('\r\n'),
|
||||
headers: {
|
||||
'content-Type': 'multipart/form-data; boundary=EXAMPLE',
|
||||
},
|
||||
};
|
||||
savedObjectsClient.bulkCreate.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'my-dashboard',
|
||||
attributes: {
|
||||
title: 'Look at my dashboard',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const { payload, statusCode } = await server.inject(request);
|
||||
const response = JSON.parse(payload);
|
||||
expect(statusCode).toBe(200);
|
||||
expect(response).toEqual({ success: true, successCount: 1 });
|
||||
expect(savedObjectsClient.bulkCreate.mock.calls).toHaveLength(1);
|
||||
const firstBulkCreateCallArray = savedObjectsClient.bulkCreate.mock.calls[0][0];
|
||||
expect(firstBulkCreateCallArray).toHaveLength(1);
|
||||
expect(firstBulkCreateCallArray[0].migrationVersion).toEqual({});
|
||||
});
|
||||
|
||||
test('retries importing a dashboard', async () => {
|
||||
// NOTE: changes to this scenario should be reflected in the docs
|
||||
const request = {
|
||||
method: 'POST',
|
||||
|
@ -123,6 +164,7 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => {
|
|||
"title": "Look at my dashboard",
|
||||
},
|
||||
"id": "my-dashboard",
|
||||
"migrationVersion": Object {},
|
||||
"type": "dashboard",
|
||||
},
|
||||
],
|
||||
|
@ -185,6 +227,7 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => {
|
|||
"title": "Look at my dashboard",
|
||||
},
|
||||
"id": "my-dashboard",
|
||||
"migrationVersion": Object {},
|
||||
"type": "dashboard",
|
||||
},
|
||||
],
|
||||
|
@ -266,6 +309,7 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => {
|
|||
"title": "Look at my visualization",
|
||||
},
|
||||
"id": "my-vis",
|
||||
"migrationVersion": Object {},
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "existing",
|
||||
|
|
|
@ -38,6 +38,7 @@ import {
|
|||
createExportRoute,
|
||||
createImportRoute,
|
||||
createResolveImportErrorsRoute,
|
||||
createLogLegacyImportRoute,
|
||||
} from './routes';
|
||||
|
||||
export function savedObjectsMixin(kbnServer, server) {
|
||||
|
@ -71,6 +72,7 @@ export function savedObjectsMixin(kbnServer, server) {
|
|||
server.route(createExportRoute(prereqs, server));
|
||||
server.route(createImportRoute(prereqs, server));
|
||||
server.route(createResolveImportErrorsRoute(prereqs, server));
|
||||
server.route(createLogLegacyImportRoute());
|
||||
|
||||
const schema = new SavedObjectsSchema(kbnServer.uiExports.savedObjectSchemas);
|
||||
const serializer = new SavedObjectsSerializer(schema);
|
||||
|
|
|
@ -110,9 +110,9 @@ describe('Saved Objects Mixin', () => {
|
|||
});
|
||||
|
||||
describe('Routes', () => {
|
||||
it('should create 10 routes', () => {
|
||||
it('should create 11 routes', () => {
|
||||
savedObjectsMixin(mockKbnServer, mockServer);
|
||||
expect(mockServer.route).toHaveBeenCalledTimes(10);
|
||||
expect(mockServer.route).toHaveBeenCalledTimes(11);
|
||||
});
|
||||
it('should add POST /api/saved_objects/_bulk_create', () => {
|
||||
savedObjectsMixin(mockKbnServer, mockServer);
|
||||
|
@ -177,6 +177,12 @@ describe('Saved Objects Mixin', () => {
|
|||
})
|
||||
);
|
||||
});
|
||||
it('should add POST /api/saved_objects/_log_legacy_import', () => {
|
||||
savedObjectsMixin(mockKbnServer, mockServer);
|
||||
expect(mockServer.route).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ path: '/api/saved_objects/_log_legacy_import', method: 'POST' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Saved object service', () => {
|
||||
|
|
|
@ -61,11 +61,14 @@ export async function kfetch(
|
|||
});
|
||||
|
||||
return window.fetch(fullUrl, restOptions).then(async res => {
|
||||
const body = await getBodyAsJson(res);
|
||||
if (res.ok) {
|
||||
return body;
|
||||
if (!res.ok) {
|
||||
throw new KFetchError(res, await getBodyAsJson(res));
|
||||
}
|
||||
throw new KFetchError(res, body);
|
||||
const contentType = res.headers.get('content-type');
|
||||
if (contentType && contentType.split(';')[0] === 'application/ndjson') {
|
||||
return await getBodyAsBlob(res);
|
||||
}
|
||||
return await getBodyAsJson(res);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
@ -96,13 +99,25 @@ async function getBodyAsJson(res: Response) {
|
|||
}
|
||||
}
|
||||
|
||||
async function getBodyAsBlob(res: Response) {
|
||||
try {
|
||||
return await res.blob();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function withDefaultOptions(options?: KFetchOptions): KFetchOptions {
|
||||
return merge(
|
||||
{
|
||||
method: 'GET',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(options && options.headers && options.headers.hasOwnProperty('Content-Type')
|
||||
? {}
|
||||
: {
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
'kbn-version': metadata.version,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -113,6 +113,7 @@ export default function ({ getService }) {
|
|||
type: 'visualization',
|
||||
attributes: {
|
||||
title: 'My favorite vis',
|
||||
visState: '{}',
|
||||
},
|
||||
references: [
|
||||
{
|
||||
|
@ -230,6 +231,7 @@ export default function ({ getService }) {
|
|||
type: 'visualization',
|
||||
attributes: {
|
||||
title: 'My favorite vis',
|
||||
visState: '{}',
|
||||
},
|
||||
references: [
|
||||
{
|
||||
|
|
|
@ -27,157 +27,331 @@ export default function ({ getService, getPageObjects }) {
|
|||
const testSubjects = getService('testSubjects');
|
||||
|
||||
describe('import objects', function describeIndexTests() {
|
||||
beforeEach(async function () {
|
||||
// delete .kibana index and then wait for Kibana to re-create it
|
||||
await kibanaServer.uiSettings.replace({});
|
||||
await PageObjects.settings.navigateTo();
|
||||
await esArchiver.load('management');
|
||||
describe('.ndjson file', () => {
|
||||
beforeEach(async function () {
|
||||
// delete .kibana index and then wait for Kibana to re-create it
|
||||
await kibanaServer.uiSettings.replace({});
|
||||
await PageObjects.settings.navigateTo();
|
||||
await esArchiver.load('management');
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
await esArchiver.unload('management');
|
||||
});
|
||||
|
||||
it('should import saved objects', async function () {
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects.ndjson'));
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.settings.clickImportDone();
|
||||
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
|
||||
const objects = await PageObjects.settings.getSavedObjectsInTable();
|
||||
const isSavedObjectImported = objects.includes('Log Agents');
|
||||
expect(isSavedObjectImported).to.be(true);
|
||||
});
|
||||
|
||||
it('should provide dialog to allow the importing of saved objects with index pattern conflicts', async function () {
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_conflicts.ndjson'));
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.settings.associateIndexPattern('d1e4c910-a2e6-11e7-bb30-233be9be6a15', 'logstash-*');
|
||||
await PageObjects.settings.clickConfirmChanges();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.settings.clickImportDone();
|
||||
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
|
||||
const objects = await PageObjects.settings.getSavedObjectsInTable();
|
||||
const isSavedObjectImported = objects.includes('saved object with index pattern conflict');
|
||||
expect(isSavedObjectImported).to.be(true);
|
||||
});
|
||||
|
||||
it('should allow the user to override duplicate saved objects', async function () {
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
|
||||
// This data has already been loaded by the "visualize" esArchive. We'll load it again
|
||||
// so that we can override the existing visualization.
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_exists.ndjson'), false);
|
||||
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*');
|
||||
await PageObjects.settings.clickConfirmChanges();
|
||||
|
||||
// Override the visualization.
|
||||
await PageObjects.common.clickConfirmOnModal();
|
||||
|
||||
const isSuccessful = await testSubjects.exists('importSavedObjectsSuccess');
|
||||
expect(isSuccessful).to.be(true);
|
||||
});
|
||||
|
||||
it('should allow the user to cancel overriding duplicate saved objects', async function () {
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
|
||||
// This data has already been loaded by the "visualize" esArchive. We'll load it again
|
||||
// so that we can be prompted to override the existing visualization.
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_exists.ndjson'), false);
|
||||
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*');
|
||||
await PageObjects.settings.clickConfirmChanges();
|
||||
|
||||
// *Don't* override the visualization.
|
||||
await PageObjects.common.clickCancelOnModal();
|
||||
|
||||
const isSuccessful = await testSubjects.exists('importSavedObjectsSuccessNoneImported');
|
||||
expect(isSuccessful).to.be(true);
|
||||
});
|
||||
|
||||
it('should import saved objects linked to saved searches', async function () {
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_saved_search.ndjson'));
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.settings.clickImportDone();
|
||||
|
||||
await PageObjects.settings.navigateTo();
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.ndjson'));
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.settings.clickImportDone();
|
||||
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
|
||||
|
||||
const objects = await PageObjects.settings.getSavedObjectsInTable();
|
||||
const isSavedObjectImported = objects.includes('saved object connected to saved search');
|
||||
expect(isSavedObjectImported).to.be(true);
|
||||
});
|
||||
|
||||
it('should not import saved objects linked to saved searches when saved search does not exist', async function () {
|
||||
await PageObjects.settings.navigateTo();
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.ndjson'));
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.settings.clickImportDone();
|
||||
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
|
||||
|
||||
const objects = await PageObjects.settings.getSavedObjectsInTable();
|
||||
const isSavedObjectImported = objects.includes('saved object connected to saved search');
|
||||
expect(isSavedObjectImported).to.be(false);
|
||||
});
|
||||
|
||||
it('should not import saved objects linked to saved searches when saved search index pattern does not exist', async function () {
|
||||
await PageObjects.settings.navigateTo();
|
||||
await PageObjects.settings.clickKibanaIndexPatterns();
|
||||
await PageObjects.settings.clickIndexPatternLogstash();
|
||||
await PageObjects.settings.removeIndexPattern();
|
||||
|
||||
await PageObjects.settings.navigateTo();
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_with_saved_search.ndjson'));
|
||||
// Wait for all the saves to happen
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.settings.clickConfirmChanges();
|
||||
await PageObjects.settings.clickImportDone();
|
||||
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
|
||||
|
||||
const objects = await PageObjects.settings.getSavedObjectsInTable();
|
||||
const isSavedObjectImported = objects.includes('saved object connected to saved search');
|
||||
expect(isSavedObjectImported).to.be(false);
|
||||
});
|
||||
|
||||
it('should import saved objects with index patterns when index patterns already exists', async () => {
|
||||
// First, import the objects
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_with_index_patterns.ndjson'));
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.settings.clickImportDone();
|
||||
// Wait for all the saves to happen
|
||||
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
|
||||
|
||||
const objects = await PageObjects.settings.getSavedObjectsInTable();
|
||||
const isSavedObjectImported = objects.includes('saved object imported with index pattern');
|
||||
expect(isSavedObjectImported).to.be(true);
|
||||
});
|
||||
|
||||
it('should import saved objects with index patterns when index patterns does not exists', async () => {
|
||||
// First, we need to delete the index pattern
|
||||
await PageObjects.settings.navigateTo();
|
||||
await PageObjects.settings.clickKibanaIndexPatterns();
|
||||
await PageObjects.settings.clickIndexPatternLogstash();
|
||||
await PageObjects.settings.removeIndexPattern();
|
||||
|
||||
// Then, import the objects
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_with_index_patterns.ndjson'));
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.settings.clickImportDone();
|
||||
// Wait for all the saves to happen
|
||||
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
|
||||
|
||||
const objects = await PageObjects.settings.getSavedObjectsInTable();
|
||||
const isSavedObjectImported = objects.includes('saved object imported with index pattern');
|
||||
expect(isSavedObjectImported).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
await esArchiver.unload('management');
|
||||
});
|
||||
describe('.json file', () => {
|
||||
beforeEach(async function () {
|
||||
// delete .kibana index and then wait for Kibana to re-create it
|
||||
await kibanaServer.uiSettings.replace({});
|
||||
await PageObjects.settings.navigateTo();
|
||||
await esArchiver.load('management');
|
||||
});
|
||||
|
||||
it('should import saved objects', async function () {
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects.json'));
|
||||
await PageObjects.settings.clickImportDone();
|
||||
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
|
||||
const objects = await PageObjects.settings.getSavedObjectsInTable();
|
||||
const isSavedObjectImported = objects.includes('Log Agents');
|
||||
expect(isSavedObjectImported).to.be(true);
|
||||
});
|
||||
afterEach(async function () {
|
||||
await esArchiver.unload('management');
|
||||
});
|
||||
|
||||
it('should provide dialog to allow the importing of saved objects with index pattern conflicts', async function () {
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects-conflicts.json'));
|
||||
await PageObjects.settings.associateIndexPattern('d1e4c910-a2e6-11e7-bb30-233be9be6a15', 'logstash-*');
|
||||
await PageObjects.settings.clickConfirmChanges();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.settings.clickImportDone();
|
||||
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
|
||||
const objects = await PageObjects.settings.getSavedObjectsInTable();
|
||||
const isSavedObjectImported = objects.includes('saved object with index pattern conflict');
|
||||
expect(isSavedObjectImported).to.be(true);
|
||||
});
|
||||
it('should import saved objects', async function () {
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects.json'));
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.settings.clickImportDone();
|
||||
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
|
||||
const objects = await PageObjects.settings.getSavedObjectsInTable();
|
||||
const isSavedObjectImported = objects.includes('Log Agents');
|
||||
expect(isSavedObjectImported).to.be(true);
|
||||
});
|
||||
|
||||
it('should allow the user to override duplicate saved objects', async function () {
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
it('should provide dialog to allow the importing of saved objects with index pattern conflicts', async function () {
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects-conflicts.json'));
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.settings.associateIndexPattern('d1e4c910-a2e6-11e7-bb30-233be9be6a15', 'logstash-*');
|
||||
await PageObjects.settings.clickConfirmChanges();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.settings.clickImportDone();
|
||||
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
|
||||
const objects = await PageObjects.settings.getSavedObjectsInTable();
|
||||
const isSavedObjectImported = objects.includes('saved object with index pattern conflict');
|
||||
expect(isSavedObjectImported).to.be(true);
|
||||
});
|
||||
|
||||
// This data has already been loaded by the "visualize" esArchive. We'll load it again
|
||||
// so that we can override the existing visualization.
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_exists.json'), false);
|
||||
it('should allow the user to override duplicate saved objects', async function () {
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
|
||||
await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*');
|
||||
await PageObjects.settings.clickConfirmChanges();
|
||||
// This data has already been loaded by the "visualize" esArchive. We'll load it again
|
||||
// so that we can override the existing visualization.
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_exists.json'), false);
|
||||
|
||||
// Override the visualization.
|
||||
await PageObjects.common.clickConfirmOnModal();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*');
|
||||
await PageObjects.settings.clickConfirmChanges();
|
||||
|
||||
const isSuccessful = await testSubjects.exists('importSavedObjectsSuccess');
|
||||
expect(isSuccessful).to.be(true);
|
||||
});
|
||||
// Override the visualization.
|
||||
await PageObjects.common.clickConfirmOnModal();
|
||||
|
||||
it('should allow the user to cancel overriding duplicate saved objects', async function () {
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
const isSuccessful = await testSubjects.exists('importSavedObjectsSuccess');
|
||||
expect(isSuccessful).to.be(true);
|
||||
});
|
||||
|
||||
// This data has already been loaded by the "visualize" esArchive. We'll load it again
|
||||
// so that we can be prompted to override the existing visualization.
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_exists.json'), false);
|
||||
it('should allow the user to cancel overriding duplicate saved objects', async function () {
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
|
||||
await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*');
|
||||
await PageObjects.settings.clickConfirmChanges();
|
||||
// This data has already been loaded by the "visualize" esArchive. We'll load it again
|
||||
// so that we can be prompted to override the existing visualization.
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_exists.json'), false);
|
||||
|
||||
// *Don't* override the visualization.
|
||||
await PageObjects.common.clickCancelOnModal();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*');
|
||||
await PageObjects.settings.clickConfirmChanges();
|
||||
|
||||
const isSuccessful = await testSubjects.exists('importSavedObjectsSuccessNoneImported');
|
||||
expect(isSuccessful).to.be(true);
|
||||
});
|
||||
// *Don't* override the visualization.
|
||||
await PageObjects.common.clickCancelOnModal();
|
||||
|
||||
it('should import saved objects linked to saved searches', async function () {
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_saved_search.json'));
|
||||
await PageObjects.settings.clickImportDone();
|
||||
const isSuccessful = await testSubjects.exists('importSavedObjectsSuccessNoneImported');
|
||||
expect(isSuccessful).to.be(true);
|
||||
});
|
||||
|
||||
await PageObjects.settings.navigateTo();
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json'));
|
||||
await PageObjects.settings.clickImportDone();
|
||||
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
|
||||
it('should import saved objects linked to saved searches', async function () {
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_saved_search.json'));
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.settings.clickImportDone();
|
||||
|
||||
const objects = await PageObjects.settings.getSavedObjectsInTable();
|
||||
const isSavedObjectImported = objects.includes('saved object connected to saved search');
|
||||
expect(isSavedObjectImported).to.be(true);
|
||||
});
|
||||
await PageObjects.settings.navigateTo();
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json'));
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.settings.clickImportDone();
|
||||
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
|
||||
|
||||
it('should not import saved objects linked to saved searches when saved search does not exist', async function () {
|
||||
await PageObjects.settings.navigateTo();
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json'));
|
||||
await PageObjects.settings.clickImportDone();
|
||||
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
|
||||
const objects = await PageObjects.settings.getSavedObjectsInTable();
|
||||
const isSavedObjectImported = objects.includes('saved object connected to saved search');
|
||||
expect(isSavedObjectImported).to.be(true);
|
||||
});
|
||||
|
||||
const objects = await PageObjects.settings.getSavedObjectsInTable();
|
||||
const isSavedObjectImported = objects.includes('saved object connected to saved search');
|
||||
expect(isSavedObjectImported).to.be(false);
|
||||
});
|
||||
it('should not import saved objects linked to saved searches when saved search does not exist', async function () {
|
||||
await PageObjects.settings.navigateTo();
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json'));
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.settings.clickImportDone();
|
||||
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
|
||||
|
||||
it('should not import saved objects linked to saved searches when saved search index pattern does not exist', async function () {
|
||||
// First, import the saved search
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_saved_search.json'));
|
||||
await PageObjects.settings.clickImportDone();
|
||||
const objects = await PageObjects.settings.getSavedObjectsInTable();
|
||||
const isSavedObjectImported = objects.includes('saved object connected to saved search');
|
||||
expect(isSavedObjectImported).to.be(false);
|
||||
});
|
||||
|
||||
// Second, we need to delete the index pattern
|
||||
await PageObjects.settings.navigateTo();
|
||||
await PageObjects.settings.clickKibanaIndexPatterns();
|
||||
await PageObjects.settings.clickIndexPatternLogstash();
|
||||
await PageObjects.settings.removeIndexPattern();
|
||||
it('should not import saved objects linked to saved searches when saved search index pattern does not exist', async function () {
|
||||
// First, import the saved search
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_saved_search.json'));
|
||||
// Wait for all the saves to happen
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.settings.clickImportDone();
|
||||
|
||||
// Last, import a saved object connected to the saved search
|
||||
// This should NOT show the conflicts
|
||||
await PageObjects.settings.navigateTo();
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json'));
|
||||
await PageObjects.settings.clickImportDone();
|
||||
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
|
||||
// Second, we need to delete the index pattern
|
||||
await PageObjects.settings.navigateTo();
|
||||
await PageObjects.settings.clickKibanaIndexPatterns();
|
||||
await PageObjects.settings.clickIndexPatternLogstash();
|
||||
await PageObjects.settings.removeIndexPattern();
|
||||
|
||||
const objects = await PageObjects.settings.getSavedObjectsInTable();
|
||||
const isSavedObjectImported = objects.includes('saved object connected to saved search');
|
||||
expect(isSavedObjectImported).to.be(false);
|
||||
});
|
||||
// Last, import a saved object connected to the saved search
|
||||
// This should NOT show the conflicts
|
||||
await PageObjects.settings.navigateTo();
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json'));
|
||||
// Wait for all the saves to happen
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.settings.clickImportDone();
|
||||
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
|
||||
|
||||
it('should import saved objects with index patterns when index patterns already exists', async () => {
|
||||
// First, import the objects
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_with_index_patterns.json'));
|
||||
await PageObjects.settings.clickImportDone();
|
||||
// Wait for all the saves to happen
|
||||
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
|
||||
const objects = await PageObjects.settings.getSavedObjectsInTable();
|
||||
const isSavedObjectImported = objects.includes('saved object connected to saved search');
|
||||
expect(isSavedObjectImported).to.be(false);
|
||||
});
|
||||
|
||||
const objects = await PageObjects.settings.getSavedObjectsInTable();
|
||||
const isSavedObjectImported = objects.includes('saved object imported with index pattern');
|
||||
expect(isSavedObjectImported).to.be(true);
|
||||
});
|
||||
it('should import saved objects with index patterns when index patterns already exists', async () => {
|
||||
// First, import the objects
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_with_index_patterns.json'));
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.settings.clickImportDone();
|
||||
// Wait for all the saves to happen
|
||||
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
|
||||
|
||||
it('should import saved objects with index patterns when index patterns does not exists', async () => {
|
||||
// First, we need to delete the index pattern
|
||||
await PageObjects.settings.navigateTo();
|
||||
await PageObjects.settings.clickKibanaIndexPatterns();
|
||||
await PageObjects.settings.clickIndexPatternLogstash();
|
||||
await PageObjects.settings.removeIndexPattern();
|
||||
const objects = await PageObjects.settings.getSavedObjectsInTable();
|
||||
const isSavedObjectImported = objects.includes('saved object imported with index pattern');
|
||||
expect(isSavedObjectImported).to.be(true);
|
||||
});
|
||||
|
||||
// Then, import the objects
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_with_index_patterns.json'));
|
||||
await PageObjects.settings.clickImportDone();
|
||||
// Wait for all the saves to happen
|
||||
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
|
||||
it('should import saved objects with index patterns when index patterns does not exists', async () => {
|
||||
// First, we need to delete the index pattern
|
||||
await PageObjects.settings.navigateTo();
|
||||
await PageObjects.settings.clickKibanaIndexPatterns();
|
||||
await PageObjects.settings.clickIndexPatternLogstash();
|
||||
await PageObjects.settings.removeIndexPattern();
|
||||
|
||||
const objects = await PageObjects.settings.getSavedObjectsInTable();
|
||||
const isSavedObjectImported = objects.includes('saved object imported with index pattern');
|
||||
expect(isSavedObjectImported).to.be(true);
|
||||
// Then, import the objects
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_with_index_patterns.json'));
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.settings.clickImportDone();
|
||||
// Wait for all the saves to happen
|
||||
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
|
||||
|
||||
const objects = await PageObjects.settings.getSavedObjectsInTable();
|
||||
const isSavedObjectImported = objects.includes('saved object imported with index pattern');
|
||||
expect(isSavedObjectImported).to.be(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"Log Agents","uiStateJSON":"{}","visState":"{\"title\":\"Log Agents\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{\"text\":\"agent.raw: Descending\"}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"agent.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}"},"id":"082f1d60-a2e7-11e7-bb30-233be9be6a15","migrationVersion":{"visualization":"7.0.0"},"references":[{"id":"f1e4c910-a2e6-11e7-bb30-233be9be6a15","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"visualization","version":1}
|
|
@ -0,0 +1 @@
|
|||
{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"saved object with index pattern conflict","uiStateJSON":"{}","visState":"{\"title\":\"Log Agents\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{\"text\":\"agent.raw: Descending\"}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"agent.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}"},"id":"saved_object_with_index_pattern_conflict","migrationVersion":{"visualization":"7.0.0"},"references":[{"id":"d1e4c910-a2e6-11e7-bb30-233be9be6a15","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"visualization","version":1}
|
|
@ -0,0 +1 @@
|
|||
{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"},"savedSearchRefName":"search_0","title":"saved object connected to saved search","uiStateJSON":"{}","visState":"{\"title\":\"PHP Viz\",\"type\":\"horizontal_bar\",\"params\":{\"type\":\"histogram\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":200},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":75,\"filter\":true,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"histogram\",\"mode\":\"normal\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"showCircles\":true}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}"},"id":"saved_object_connected_to_saved_search","migrationVersion":{"visualization":"7.0.0"},"references":[{"id":"c45e6c50-ba72-11e7-a8f9-ad70f02e633d","name":"search_0","type":"search"}],"type":"visualization","version":1}
|
|
@ -0,0 +1 @@
|
|||
{"attributes":{"description":"AreaChart","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"Shared-Item Visualization AreaChart","uiStateJSON":"{}","visState":"{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}"},"id":"Shared-Item-Visualization-AreaChart","migrationVersion":{"visualization":"7.0.0"},"references":[{"id":"logstash-*","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"visualization","version":1}
|
|
@ -0,0 +1 @@
|
|||
{"attributes":{"columns":["_source"],"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"highlightAll\":true,\"version\":true,\"query\":{\"language\":\"lucene\",\"query\":\"php\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":["@timestamp","desc"],"title":"PHP saved search"},"id":"c45e6c50-ba72-11e7-a8f9-ad70f02e633d","migrationVersion":{"search":"7.0.0"},"references":[{"id":"f1e4c910-a2e6-11e7-bb30-233be9be6a15","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"search","version":1}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,2 @@
|
|||
{"attributes":{"columns":["_source"],"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"highlightAll\":true,\"version\":true,\"query\":{\"language\":\"lucene\",\"query\":\"php\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":["@timestamp","desc"],"title":"PHP saved search"},"id":"c45e6c50-ba72-11e7-a8f9-ad70f02e633d","migrationVersion":{"search":"7.0.0"},"references":[{"id":"f1e4c910-a2e6-11e7-bb30-233be9be6a15","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"search","version":1}
|
||||
{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"},"savedSearchRefName":"search_0","title":"saved object connected to saved search","uiStateJSON":"{}","visState":"{\"title\":\"PHP Viz\",\"type\":\"horizontal_bar\",\"params\":{\"type\":\"histogram\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":200},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":75,\"filter\":true,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"histogram\",\"mode\":\"normal\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"showCircles\":true}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}"},"id":"saved_object_connected_to_saved_search","migrationVersion":{"visualization":"7.0.0"},"references":[{"id":"c45e6c50-ba72-11e7-a8f9-ad70f02e633d","name":"search_0","type":"search"}],"type":"visualization","version":1}
|
|
@ -0,0 +1,2 @@
|
|||
{"attributes":{"columns":["_source"],"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":["@timestamp","desc"],"title":"mysavedsearch"},"id":"6aea5700-ac94-11e8-a651-614b2788174a","migrationVersion":{"search":"7.0.0"},"references":[{"id":"4c3f3c30-ac94-11e8-a651-614b2788174a","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"search","version":1}
|
||||
{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}"},"savedSearchRefName":"search_0","title":"mysavedviz","uiStateJSON":"{}","visState":"{\"title\":\"mysavedviz\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}"},"id":"8411daa0-ac94-11e8-a651-614b2788174a","migrationVersion":{"visualization":"7.0.0"},"references":[{"id":"6aea5700-ac94-11e8-a651-614b2788174a","name":"search_0","type":"search"}],"type":"visualization","version":1}
|
|
@ -7,7 +7,7 @@
|
|||
},
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@elastic/eui": "9.8.0",
|
||||
"@elastic/eui": "9.9.0",
|
||||
"react": "^16.8.0",
|
||||
"react-dom": "^16.8.0"
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
},
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@elastic/eui": "9.8.0",
|
||||
"@elastic/eui": "9.9.0",
|
||||
"react": "^16.8.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
},
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@elastic/eui": "9.8.0",
|
||||
"@elastic/eui": "9.9.0",
|
||||
"react": "^16.8.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
},
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@elastic/eui": "9.8.0",
|
||||
"@elastic/eui": "9.9.0",
|
||||
"react": "^16.8.0",
|
||||
"react-dom": "^16.8.0"
|
||||
}
|
||||
|
|
|
@ -161,7 +161,7 @@
|
|||
"@babel/register": "^7.0.0",
|
||||
"@babel/runtime": "^7.3.4",
|
||||
"@elastic/datemath": "5.0.2",
|
||||
"@elastic/eui": "9.8.0",
|
||||
"@elastic/eui": "9.9.0",
|
||||
"@elastic/javascript-typescript-langserver": "^0.1.21",
|
||||
"@elastic/lsp-extension": "^0.1.1",
|
||||
"@elastic/node-crypto": "0.1.2",
|
||||
|
|
|
@ -56,7 +56,7 @@ Array [
|
|||
"min": 100,
|
||||
},
|
||||
"field": "@timestamp",
|
||||
"interval": "1s",
|
||||
"interval": "30s",
|
||||
"min_doc_count": 0,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -34,7 +34,7 @@ interface Aggs {
|
|||
export type ESResponse = PromiseReturnType<typeof fetch>;
|
||||
export async function fetch({ serviceName, setup }: MetricsRequestArgs) {
|
||||
const { start, end, esFilterQuery, client, config } = setup;
|
||||
const { intervalString } = getBucketSize(start, end, 'auto');
|
||||
const { bucketSize } = getBucketSize(start, end, 'auto');
|
||||
const filters: ESFilter[] = [
|
||||
{ term: { [SERVICE_NAME]: serviceName } },
|
||||
{ term: { [PROCESSOR_EVENT]: 'metric' } },
|
||||
|
@ -56,7 +56,9 @@ export async function fetch({ serviceName, setup }: MetricsRequestArgs) {
|
|||
timeseriesData: {
|
||||
date_histogram: {
|
||||
field: '@timestamp',
|
||||
interval: intervalString,
|
||||
|
||||
// ensure minimum bucket size of 30s since this is the default resolution for metric data
|
||||
interval: `${Math.max(bucketSize, 30)}s`,
|
||||
min_doc_count: 0,
|
||||
extended_bounds: { min: start, max: end }
|
||||
},
|
||||
|
|
|
@ -48,7 +48,7 @@ Array [
|
|||
"min": 100,
|
||||
},
|
||||
"field": "@timestamp",
|
||||
"interval": "1s",
|
||||
"interval": "30s",
|
||||
"min_doc_count": 0,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -30,7 +30,7 @@ interface Aggs {
|
|||
export type ESResponse = PromiseReturnType<typeof fetch>;
|
||||
export async function fetch({ serviceName, setup }: MetricsRequestArgs) {
|
||||
const { start, end, esFilterQuery, client, config } = setup;
|
||||
const { intervalString } = getBucketSize(start, end, 'auto');
|
||||
const { bucketSize } = getBucketSize(start, end, 'auto');
|
||||
const filters: ESFilter[] = [
|
||||
{ term: { [SERVICE_NAME]: serviceName } },
|
||||
{ term: { [PROCESSOR_EVENT]: 'metric' } },
|
||||
|
@ -59,7 +59,9 @@ export async function fetch({ serviceName, setup }: MetricsRequestArgs) {
|
|||
timeseriesData: {
|
||||
date_histogram: {
|
||||
field: '@timestamp',
|
||||
interval: intervalString,
|
||||
|
||||
// ensure minimum bucket size of 30s since this is the default resolution for metric data
|
||||
interval: `${Math.max(bucketSize, 30)}s`,
|
||||
min_doc_count: 0,
|
||||
extended_bounds: { min: start, max: end }
|
||||
},
|
||||
|
|
|
@ -9,42 +9,53 @@ import PropTypes from 'prop-types';
|
|||
import { debounce } from 'lodash';
|
||||
|
||||
export class DomPreview extends React.Component {
|
||||
static container = null;
|
||||
static content = null;
|
||||
static observer = null;
|
||||
|
||||
static propTypes = {
|
||||
elementId: PropTypes.string.isRequired,
|
||||
height: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.container = null;
|
||||
this.content = null;
|
||||
this.observer = null;
|
||||
this.original = null;
|
||||
this.updateTimeout = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const original = document.querySelector(`#${this.props.elementId}`);
|
||||
|
||||
const update = this.update(original);
|
||||
update();
|
||||
|
||||
const slowUpdate = debounce(update, 250);
|
||||
|
||||
this.observer = new MutationObserver(slowUpdate);
|
||||
// configuration of the observer
|
||||
const config = { attributes: true, childList: true, subtree: true };
|
||||
// pass in the target node, as well as the observer options
|
||||
this.observer.observe(original, config);
|
||||
this.update();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.observer.disconnect();
|
||||
clearTimeout(this.updateTimeout);
|
||||
this.observer && this.observer.disconnect(); // observer not guaranteed to exist
|
||||
}
|
||||
|
||||
update = original => () => {
|
||||
update = () => {
|
||||
if (!this.content || !this.container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const thumb = original.cloneNode(true);
|
||||
if (!this.observer) {
|
||||
this.original = this.original || document.querySelector(`#${this.props.elementId}`);
|
||||
if (this.original) {
|
||||
const slowUpdate = debounce(this.update, 100);
|
||||
this.observer = new MutationObserver(slowUpdate);
|
||||
// configuration of the observer
|
||||
const config = { attributes: true, childList: true, subtree: true };
|
||||
// pass in the target node, as well as the observer options
|
||||
this.observer.observe(this.original, config);
|
||||
} else {
|
||||
clearTimeout(this.updateTimeout); // to avoid the assumption that we fully control when `update` is called
|
||||
this.updateTimeout = setTimeout(this.update, 30);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const originalStyle = window.getComputedStyle(original, null);
|
||||
const thumb = this.original.cloneNode(true);
|
||||
|
||||
const originalStyle = window.getComputedStyle(this.original, null);
|
||||
const originalWidth = parseInt(originalStyle.getPropertyValue('width'), 10);
|
||||
const originalHeight = parseInt(originalStyle.getPropertyValue('height'), 10);
|
||||
|
||||
|
@ -61,7 +72,7 @@ export class DomPreview extends React.Component {
|
|||
this.container.style.cssText = `width: ${thumbWidth}px; height: ${thumbHeight}px;`;
|
||||
|
||||
// Copy canvas data
|
||||
const originalCanvas = original.querySelectorAll('canvas');
|
||||
const originalCanvas = this.original.querySelectorAll('canvas');
|
||||
const thumbCanvas = thumb.querySelectorAll('canvas');
|
||||
|
||||
// Cloned canvas elements are blank and need to be explicitly redrawn
|
||||
|
|
|
@ -8,13 +8,12 @@ import { connect } from 'react-redux';
|
|||
import { fetchAllRenderables } from '../../state/actions/elements';
|
||||
import { setRefreshInterval } from '../../state/actions/workpad';
|
||||
import { getInFlight } from '../../state/selectors/resolved_args';
|
||||
import { getRefreshInterval, getElementStats } from '../../state/selectors/workpad';
|
||||
import { getRefreshInterval } from '../../state/selectors/workpad';
|
||||
import { RefreshControl as Component } from './refresh_control';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
inFlight: getInFlight(state),
|
||||
refreshInterval: getRefreshInterval(state),
|
||||
elementStats: getElementStats(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
|
|
|
@ -8,7 +8,6 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { EuiButtonEmpty } from '@elastic/eui';
|
||||
import { Popover } from '../popover';
|
||||
import { loadingIndicator } from '../../lib/loading_indicator';
|
||||
import { AutoRefreshControls } from './auto_refresh_controls';
|
||||
|
||||
const getRefreshInterval = (val = '') => {
|
||||
|
@ -37,21 +36,7 @@ const getRefreshInterval = (val = '') => {
|
|||
}
|
||||
};
|
||||
|
||||
export const RefreshControl = ({
|
||||
inFlight,
|
||||
elementStats,
|
||||
setRefreshInterval,
|
||||
refreshInterval,
|
||||
doRefresh,
|
||||
}) => {
|
||||
const { pending } = elementStats;
|
||||
|
||||
if (inFlight || pending > 0) {
|
||||
loadingIndicator.show();
|
||||
} else {
|
||||
loadingIndicator.hide();
|
||||
}
|
||||
|
||||
export const RefreshControl = ({ inFlight, setRefreshInterval, refreshInterval, doRefresh }) => {
|
||||
const setRefresh = val => setRefreshInterval(getRefreshInterval(val));
|
||||
|
||||
const popoverButton = handleClick => (
|
||||
|
|
|
@ -9,6 +9,11 @@ import { loadingCount } from 'ui/chrome';
|
|||
|
||||
let isActive = false;
|
||||
|
||||
export interface LoadingIndicatorInterface {
|
||||
show: () => void;
|
||||
hide: () => void;
|
||||
}
|
||||
|
||||
export const loadingIndicator = {
|
||||
show: () => {
|
||||
if (!isActive) {
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
/*
|
||||
* 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 { createAction } from 'redux-actions';
|
||||
|
||||
export const setLoading = createAction('setResolvedLoading');
|
||||
export const setValue = createAction('setResolvedValue');
|
||||
export const setValues = createAction('setResolvedValues');
|
||||
export const clearValue = createAction('clearResolvedValue');
|
||||
export const clearValues = createAction('clearResolvedValues');
|
||||
|
||||
export const inFlightActive = createAction('inFlightActive');
|
||||
export const inFlightComplete = createAction('inFlightComplete');
|
45
x-pack/plugins/canvas/public/state/actions/resolved_args.ts
Normal file
45
x-pack/plugins/canvas/public/state/actions/resolved_args.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 { Action } from 'redux';
|
||||
import { createAction } from 'redux-actions';
|
||||
|
||||
export const setLoadingActionType = 'setResolvedLoading';
|
||||
export const setValueActionType = 'setResolvedValue';
|
||||
export const inFlightActiveActionType = 'inFlightActive';
|
||||
export const inFlightCompleteActionType = 'inFlightComplete';
|
||||
|
||||
type InFlightActive = Action<typeof inFlightActiveActionType>;
|
||||
type InFlightComplete = Action<typeof inFlightCompleteActionType>;
|
||||
|
||||
interface SetResolvedLoadingPayload {
|
||||
path: any[];
|
||||
}
|
||||
type SetResolvedLoading = Action<typeof setLoadingActionType> & {
|
||||
payload: SetResolvedLoadingPayload;
|
||||
};
|
||||
|
||||
interface SetResolvedValuePayload {
|
||||
path: any[];
|
||||
value: any;
|
||||
}
|
||||
type SetResolvedValue = Action<typeof setValueActionType> & {
|
||||
payload: SetResolvedValuePayload;
|
||||
};
|
||||
|
||||
export type Action = SetResolvedLoading | SetResolvedValue | InFlightActive | InFlightComplete;
|
||||
|
||||
export const setLoading = createAction<SetResolvedLoadingPayload>(setLoadingActionType);
|
||||
export const setValue = createAction<SetResolvedValuePayload>(setValueActionType);
|
||||
export const setValues = createAction('setResolvedValues');
|
||||
export const clearValue = createAction('clearResolvedValue');
|
||||
export const clearValues = createAction('clearResolvedValues');
|
||||
|
||||
export const inFlightActive = createAction<undefined>(inFlightActiveActionType, () => undefined);
|
||||
export const inFlightComplete = createAction<undefined>(
|
||||
inFlightCompleteActionType,
|
||||
() => undefined
|
||||
);
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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 {
|
||||
inFlightActive,
|
||||
inFlightComplete,
|
||||
setLoading,
|
||||
setValue,
|
||||
} from '../../actions/resolved_args';
|
||||
import { inFlightMiddlewareFactory } from '../in_flight';
|
||||
|
||||
const next = jest.fn();
|
||||
const dispatch = jest.fn();
|
||||
const loadingIndicator = {
|
||||
show: jest.fn(),
|
||||
hide: jest.fn(),
|
||||
};
|
||||
const pendingCache: string[] = [];
|
||||
|
||||
const testMiddleware = inFlightMiddlewareFactory({
|
||||
loadingIndicator,
|
||||
pendingCache,
|
||||
})({ dispatch, getState: jest.fn() })(next);
|
||||
|
||||
describe('inflight middleware', () => {
|
||||
beforeEach(() => {
|
||||
dispatch.mockClear();
|
||||
});
|
||||
|
||||
describe('loading indicator', () => {
|
||||
beforeEach(() => {
|
||||
loadingIndicator.show = jest.fn();
|
||||
loadingIndicator.hide = jest.fn();
|
||||
});
|
||||
|
||||
it('shows the loading indicator on inFlightActive action', () => {
|
||||
const inFlightActiveAction = inFlightActive();
|
||||
|
||||
testMiddleware(inFlightActiveAction);
|
||||
|
||||
expect(loadingIndicator.show).toBeCalled();
|
||||
});
|
||||
|
||||
it('hides the loading indicator on inFlightComplete action', () => {
|
||||
const inFlightCompleteAction = inFlightComplete();
|
||||
|
||||
testMiddleware(inFlightCompleteAction);
|
||||
expect(loadingIndicator.hide).toBeCalled();
|
||||
});
|
||||
|
||||
describe('value', () => {
|
||||
beforeEach(() => {
|
||||
while (pendingCache.length) {
|
||||
pendingCache.pop();
|
||||
}
|
||||
});
|
||||
|
||||
it('dispatches the inFlightAction for loadingValue actions', () => {
|
||||
const path = ['some', 'path'];
|
||||
const loadingAction = setLoading({ path });
|
||||
|
||||
testMiddleware(loadingAction);
|
||||
|
||||
expect(dispatch).toBeCalledWith(inFlightActive());
|
||||
});
|
||||
|
||||
it('adds path to pendingCache for loadingValue actions', () => {
|
||||
const expectedPath = 'path';
|
||||
const path = [expectedPath];
|
||||
const loadingAction = setLoading({ path });
|
||||
|
||||
testMiddleware(loadingAction);
|
||||
|
||||
expect(pendingCache[0]).toBe(expectedPath);
|
||||
});
|
||||
|
||||
it('dispatches inFlight complete if all pending is resolved', () => {
|
||||
const resolvedPath1 = 'path1';
|
||||
const resolvedPath2 = 'path2';
|
||||
|
||||
const setAction1 = setValue({ path: [resolvedPath1], value: {} });
|
||||
const setAction2 = setValue({ path: [resolvedPath2], value: {} });
|
||||
|
||||
pendingCache.push(resolvedPath1);
|
||||
pendingCache.push(resolvedPath2);
|
||||
|
||||
testMiddleware(setAction1);
|
||||
expect(dispatch).not.toBeCalled();
|
||||
|
||||
testMiddleware(setAction2);
|
||||
expect(dispatch).toBeCalledWith(inFlightComplete());
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* 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 { convert } from '../../lib/modify_path';
|
||||
import { setLoading, setValue, inFlightActive, inFlightComplete } from '../actions/resolved_args';
|
||||
|
||||
export const inFlight = ({ dispatch }) => next => {
|
||||
const pendingCache = [];
|
||||
|
||||
return action => {
|
||||
const isLoading = action.type === setLoading.toString();
|
||||
const isSetting = action.type === setValue.toString();
|
||||
|
||||
if (isLoading || isSetting) {
|
||||
const cacheKey = convert(action.payload.path).join('/');
|
||||
|
||||
if (isLoading) {
|
||||
pendingCache.push(cacheKey);
|
||||
dispatch(inFlightActive());
|
||||
} else if (isSetting) {
|
||||
const idx = pendingCache.indexOf(cacheKey);
|
||||
pendingCache.splice(idx, 1);
|
||||
if (pendingCache.length === 0) {
|
||||
dispatch(inFlightComplete());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// execute the action
|
||||
next(action);
|
||||
};
|
||||
};
|
66
x-pack/plugins/canvas/public/state/middleware/in_flight.ts
Normal file
66
x-pack/plugins/canvas/public/state/middleware/in_flight.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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 { Dispatch, Middleware } from 'redux';
|
||||
import {
|
||||
loadingIndicator as defaultLoadingIndicator,
|
||||
LoadingIndicatorInterface,
|
||||
} from '../../lib/loading_indicator';
|
||||
// @ts-ignore
|
||||
import { convert } from '../../lib/modify_path';
|
||||
|
||||
interface InFlightMiddlewareOptions {
|
||||
pendingCache: string[];
|
||||
loadingIndicator: LoadingIndicatorInterface;
|
||||
}
|
||||
|
||||
import {
|
||||
Action as AnyAction,
|
||||
inFlightActive,
|
||||
inFlightActiveActionType,
|
||||
inFlightComplete,
|
||||
inFlightCompleteActionType,
|
||||
setLoadingActionType,
|
||||
setValueActionType,
|
||||
} from '../actions/resolved_args';
|
||||
|
||||
const pathToKey = (path: any[]) => convert(path).join('/');
|
||||
|
||||
export const inFlightMiddlewareFactory = ({
|
||||
loadingIndicator,
|
||||
pendingCache,
|
||||
}: InFlightMiddlewareOptions): Middleware => {
|
||||
return ({ dispatch }) => (next: Dispatch) => {
|
||||
return (action: AnyAction) => {
|
||||
if (action.type === setLoadingActionType) {
|
||||
const cacheKey = pathToKey(action.payload.path);
|
||||
pendingCache.push(cacheKey);
|
||||
dispatch(inFlightActive());
|
||||
} else if (action.type === setValueActionType) {
|
||||
const cacheKey = pathToKey(action.payload.path);
|
||||
const idx = pendingCache.indexOf(cacheKey);
|
||||
if (idx >= 0) {
|
||||
pendingCache.splice(idx, 1);
|
||||
}
|
||||
if (pendingCache.length === 0) {
|
||||
dispatch(inFlightComplete());
|
||||
}
|
||||
} else if (action.type === inFlightActiveActionType) {
|
||||
loadingIndicator.show();
|
||||
} else if (action.type === inFlightCompleteActionType) {
|
||||
loadingIndicator.hide();
|
||||
}
|
||||
|
||||
// execute the action
|
||||
next(action);
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export const inFlight = inFlightMiddlewareFactory({
|
||||
loadingIndicator: defaultLoadingIndicator,
|
||||
pendingCache: [],
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue