diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 3843cc27defd5d..8ad5330f3fda59 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -56,6 +56,11 @@ This page has moved. Please see <>. This page has moved. Please see <>. +[role="exclude",id="add-sample-data"] +== Add sample data + +This page has moved. Please see <>. + [role="exclude",id="tilemap"] == Coordinate map diff --git a/docs/user/getting-started.asciidoc b/docs/user/getting-started.asciidoc index c6fe5b5b92d696..d426ec111351cc 100644 --- a/docs/user/getting-started.asciidoc +++ b/docs/user/getting-started.asciidoc @@ -1,54 +1,65 @@ [[getting-started]] -= Getting Started += Get started [partintro] -- -You’re new to Kibana and want to give it a try. {kib} has sample data sets and -tutorials to help you get started. +Ready to try out {kib} and see what it can do? To quickest way to get started with {kib} is to set up on Cloud, then add a sample data set that helps you get a handle on the full range of {kib} features. [float] -=== Sample data +[[cloud-set-up]] +== Set up on Cloud -You can use the <> to take {kib} for a test ride without having -to go through the process of loading data yourself. With one click, -you can install a sample data set and start interacting with -{kib} visualizations in seconds. You can access the sample data -from the {kib} home page. +To access {kib} in a single click, run our hosted Elasticsearch Service on Elastic Cloud. -[float] +. Log into the link:https://cloud.elastic.co/[Elasticsearch Service Console]. +If you need an account, register for a link:https://www.elastic.co/cloud/elasticsearch-service/signup[free 14-day trial]. + +. Click *Create deployment*, then give your deployment a name. -=== Add data tutorials -{kib} has built-in *Add Data* tutorials to help you set up -data flows in the Elastic Stack. These tutorials are available -from the Kibana home page. In *Add Data to Kibana*, find the data type -you’re interested in, and click its button to view a list of available tutorials. +. To use the default options, click *Create deployment*. You can modify the other deployment options, but the default options are great to get started. + +Be sure to copy down the password for the `elastic` user and Cloud ID information. You'll need that later. [float] -=== Hands-on experience +[[get-data-in]] +== Get data into {kib} + +The easiest way to get data into {kib} is to add a sample data set. + +{kib} has several sample data sets that you can use before loading your own data: + +* *Sample eCommerce orders* includes visualizations for tracking product-related information, +such as cost, revenue, and price. + +* *Sample flight data* includes visualizations for monitoring flight routes. -The following tutorials walk you through searching, analyzing, -and visualizing data. +* *Sample web logs* includes visualizations for monitoring website traffic. -* <>. You'll -learn to filter and query data, edit visualizations, and interact with dashboards. +To use the sample data sets: -* <>. You'll manually load a data set and build -your own visualizations and dashboard. +. Go to the {kib} home page. + +. Click *Load a data set and a {kib} dashboard*. + +. Click *View data* and view the prepackaged dashboards, maps, and more. + +[role="screenshot"] +image::images/add-sample-data.png[] + +NOTE: The timestamps in the sample data sets are relative to when they are installed. +If you uninstall and reinstall a data set, the timestamps change to reflect the most recent installation. [float] -=== Before you begin +[[getting-started-next-steps]] +== Next steps -Make sure you've <> and established -a <>. +* To get a hands-on experience creating visualizations, follow the <> tutorial. -If you are running our hosted Elasticsearch Service on Elastic Cloud, you access Kibana with a single click. (You can {ess-trial}[sign up for a free trial] and start exploring data in minutes.) +* If you're ready to load an actual data set and build a dashboard, follow the <> tutorial. -- -include::{kib-repo-dir}/getting-started/add-sample-data.asciidoc[] - include::{kib-repo-dir}/getting-started/tutorial-sample-data.asciidoc[] include::{kib-repo-dir}/getting-started/tutorial-full-experience.asciidoc[] @@ -60,4 +71,3 @@ include::{kib-repo-dir}/getting-started/tutorial-discovering.asciidoc[] include::{kib-repo-dir}/getting-started/tutorial-visualizing.asciidoc[] include::{kib-repo-dir}/getting-started/tutorial-dashboard.asciidoc[] - diff --git a/docs/user/index.asciidoc b/docs/user/index.asciidoc index 3911d57e05c9a4..ff100d07633686 100644 --- a/docs/user/index.asciidoc +++ b/docs/user/index.asciidoc @@ -1,13 +1,13 @@ include::introduction.asciidoc[] +include::getting-started.asciidoc[] + include::setup.asciidoc[] include::monitoring/configuring-monitoring.asciidoc[] include::security/securing-kibana.asciidoc[] -include::getting-started.asciidoc[] - include::discover.asciidoc[] include::visualize.asciidoc[] diff --git a/src/core/server/elasticsearch/retry_call_cluster.test.ts b/src/core/server/elasticsearch/retry_call_cluster.test.ts index b5a5185ab39d91..4f391f0aba34b3 100644 --- a/src/core/server/elasticsearch/retry_call_cluster.test.ts +++ b/src/core/server/elasticsearch/retry_call_cluster.test.ts @@ -89,6 +89,19 @@ describe('migrationsRetryCallCluster', () => { }); }); + it('retries ES API calls that rejects with snapshot_in_progress_exception', () => { + expect.assertions(1); + const callEsApi = jest.fn(); + let i = 0; + callEsApi.mockImplementation(() => { + return i++ <= 2 + ? Promise.reject({ body: { error: { type: 'snapshot_in_progress_exception' } } }) + : Promise.resolve('success'); + }); + const retried = migrationsRetryCallCluster(callEsApi, mockLogger.get('mock log'), 1); + return expect(retried('endpoint')).resolves.toMatchInlineSnapshot(`"success"`); + }); + it('rejects when ES API calls reject with other errors', async () => { expect.assertions(3); const callEsApi = jest.fn(); diff --git a/src/core/server/elasticsearch/retry_call_cluster.ts b/src/core/server/elasticsearch/retry_call_cluster.ts index ea3cc0b90c0778..901b801159cb6a 100644 --- a/src/core/server/elasticsearch/retry_call_cluster.ts +++ b/src/core/server/elasticsearch/retry_call_cluster.ts @@ -64,7 +64,8 @@ export function migrationsRetryCallCluster( error instanceof esErrors.AuthenticationException || error instanceof esErrors.AuthorizationException || // @ts-ignore - error instanceof esErrors.Gone + error instanceof esErrors.Gone || + error?.body?.error?.type === 'snapshot_in_progress_exception' ); }, timer(delay), @@ -85,15 +86,7 @@ export function migrationsRetryCallCluster( * * @param apiCaller */ - -// TODO: Replace with APICaller from './scoped_cluster_client' once #46668 is merged -export function retryCallCluster( - apiCaller: ( - endpoint: string, - clientParams: Record, - options?: CallAPIOptions - ) => Promise -) { +export function retryCallCluster(apiCaller: APICaller) { return (endpoint: string, clientParams: Record = {}, options?: CallAPIOptions) => { return defer(() => apiCaller(endpoint, clientParams, options)) .pipe( diff --git a/src/core/utils/merge.test.ts b/src/core/utils/merge.test.ts index c857e980dec21a..7ef07a83399ac4 100644 --- a/src/core/utils/merge.test.ts +++ b/src/core/utils/merge.test.ts @@ -17,6 +17,7 @@ * under the License. */ +// eslint-disable-next-line max-classes-per-file import { merge } from './merge'; describe('merge', () => { @@ -62,6 +63,29 @@ describe('merge', () => { expect(merge({ a: 0 }, { a: 1 }, {})).toEqual({ a: 1 }); }); + test('does not merge class instances', () => { + class Folder { + constructor(public readonly path: string) {} + getPath() { + return this.path; + } + } + class File { + constructor(public readonly content: string) {} + getContent() { + return this.content; + } + } + const folder = new Folder('/etc'); + const file = new File('yolo'); + + const result = merge({}, { content: folder }, { content: file }); + expect(result).toStrictEqual({ + content: file, + }); + expect(result.content.getContent()).toBe('yolo'); + }); + test(`doesn't pollute prototypes`, () => { merge({}, JSON.parse('{ "__proto__": { "foo": "bar" } }')); merge({}, JSON.parse('{ "constructor": { "prototype": { "foo": "bar" } } }')); diff --git a/src/core/utils/merge.ts b/src/core/utils/merge.ts index 8e5d9f4860d955..43878c27b1e199 100644 --- a/src/core/utils/merge.ts +++ b/src/core/utils/merge.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import { isPlainObject } from 'lodash'; /** * Deeply merges two objects, omitting undefined values, and not deeply merging Arrays. * @@ -60,7 +60,7 @@ export function merge>( ) as TReturn; } -const isMergable = (obj: any) => typeof obj === 'object' && obj !== null && !Array.isArray(obj); +const isMergable = (obj: any) => isPlainObject(obj); const mergeObjects = , U extends Record>( baseObj: T, diff --git a/src/legacy/core_plugins/data/public/search/aggs/__tests__/buckets/_terms_other_bucket_helper.js b/src/legacy/core_plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.test.ts similarity index 54% rename from src/legacy/core_plugins/data/public/search/aggs/__tests__/buckets/_terms_other_bucket_helper.js rename to src/legacy/core_plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.test.ts index 749dad377f2e25..976ab57c00b631 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/__tests__/buckets/_terms_other_bucket_helper.js +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.test.ts @@ -17,39 +17,73 @@ * under the License. */ -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; import { buildOtherBucketAgg, mergeOtherBucketAggResponse, updateMissingBucket, -} from '../../buckets/_terms_other_bucket_helper'; -import { start as visualizationsStart } from '../../../../../../../core_plugins/visualizations/public/np_ready/public/legacy'; -import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; +} from './_terms_other_bucket_helper'; +import { AggConfigs, CreateAggConfigParams } from '../agg_configs'; +import { BUCKET_TYPES } from './bucket_agg_types'; +import { IBucketAggConfig } from './_bucket_agg_type'; +import { mockDataServices, mockAggTypesRegistry } from '../test_helpers'; -const visConfigSingleTerm = { - type: 'pie', +const indexPattern = { + id: '1234', + title: 'logstash-*', + fields: [ + { + name: 'field', + }, + ], +} as any; + +const singleTerm = { aggs: [ { - type: 'terms', - schema: 'segment', - params: { field: 'machine.os.raw', otherBucket: true, missingBucket: true }, + id: '1', + type: BUCKET_TYPES.TERMS, + params: { + field: { + name: 'machine.os.raw', + indexPattern, + filterable: true, + }, + otherBucket: true, + missingBucket: true, + }, }, ], }; -const visConfigNestedTerm = { - type: 'pie', +const nestedTerm = { aggs: [ { - type: 'terms', - schema: 'segment', - params: { field: 'geo.src', size: 2, otherBucket: false, missingBucket: false }, + id: '1', + type: BUCKET_TYPES.TERMS, + params: { + field: { + name: 'geo.src', + indexPattern, + filterable: true, + }, + size: 2, + otherBucket: false, + missingBucket: false, + }, }, { - type: 'terms', - schema: 'segment', - params: { field: 'machine.os.raw', size: 2, otherBucket: true, missingBucket: true }, + id: '2', + type: BUCKET_TYPES.TERMS, + params: { + field: { + name: 'machine.os.raw', + indexPattern, + filterable: true, + }, + size: 2, + otherBucket: true, + missingBucket: true, + }, }, ], }; @@ -183,28 +217,36 @@ const nestedOtherResponse = { status: 200, }; -describe('Terms Agg Other bucket helper', () => { - let vis; +jest.mock('ui/new_platform'); - function init(aggConfig) { - ngMock.module('kibana'); - ngMock.inject(Private => { - const indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); +describe('Terms Agg Other bucket helper', () => { + const typesRegistry = mockAggTypesRegistry(); + const getAggConfigs = (aggs: CreateAggConfigParams[] = []) => { + return new AggConfigs(indexPattern, [...aggs], { typesRegistry }); + }; - vis = new visualizationsStart.Vis(indexPattern, aggConfig); - }); - } + beforeEach(() => { + mockDataServices(); + }); describe('buildOtherBucketAgg', () => { - it('returns a function', () => { - init(visConfigSingleTerm); - const agg = buildOtherBucketAgg(vis.aggs, vis.aggs.aggs[0], singleTermResponse); - expect(agg).to.be.a('function'); + test('returns a function', () => { + const aggConfigs = getAggConfigs(singleTerm.aggs); + const agg = buildOtherBucketAgg( + aggConfigs, + aggConfigs.aggs[0] as IBucketAggConfig, + singleTermResponse + ); + expect(typeof agg).toBe('function'); }); - it('correctly builds query with single terms agg', () => { - init(visConfigSingleTerm); - const agg = buildOtherBucketAgg(vis.aggs, vis.aggs.aggs[0], singleTermResponse)(); + test('correctly builds query with single terms agg', () => { + const aggConfigs = getAggConfigs(singleTerm.aggs); + const agg = buildOtherBucketAgg( + aggConfigs, + aggConfigs.aggs[0] as IBucketAggConfig, + singleTermResponse + ); const expectedResponse = { aggs: undefined, filters: { @@ -223,13 +265,19 @@ describe('Terms Agg Other bucket helper', () => { }, }, }; - - expect(agg['other-filter']).to.eql(expectedResponse); + expect(agg).toBeDefined(); + if (agg) { + expect(agg()['other-filter']).toEqual(expectedResponse); + } }); - it('correctly builds query for nested terms agg', () => { - init(visConfigNestedTerm); - const agg = buildOtherBucketAgg(vis.aggs, vis.aggs.aggs[1], nestedTermResponse)(); + test('correctly builds query for nested terms agg', () => { + const aggConfigs = getAggConfigs(nestedTerm.aggs); + const agg = buildOtherBucketAgg( + aggConfigs, + aggConfigs.aggs[1] as IBucketAggConfig, + nestedTermResponse + ); const expectedResponse = { 'other-filter': { aggs: undefined, @@ -267,54 +315,84 @@ describe('Terms Agg Other bucket helper', () => { }, }, }; - - expect(agg).to.eql(expectedResponse); + expect(agg).toBeDefined(); + if (agg) { + expect(agg()).toEqual(expectedResponse); + } }); - it('returns false when nested terms agg has no buckets', () => { - init(visConfigNestedTerm); - const agg = buildOtherBucketAgg(vis.aggs, vis.aggs.aggs[1], nestedTermResponseNoResults); - expect(agg).to.eql(false); + test('returns false when nested terms agg has no buckets', () => { + const aggConfigs = getAggConfigs(nestedTerm.aggs); + const agg = buildOtherBucketAgg( + aggConfigs, + aggConfigs.aggs[1] as IBucketAggConfig, + nestedTermResponseNoResults + ); + + expect(agg).toEqual(false); }); }); describe('mergeOtherBucketAggResponse', () => { - it('correctly merges other bucket with single terms agg', () => { - init(visConfigSingleTerm); - const otherAggConfig = buildOtherBucketAgg(vis.aggs, vis.aggs.aggs[0], singleTermResponse)(); - const mergedResponse = mergeOtherBucketAggResponse( - vis.aggs, - singleTermResponse, - singleOtherResponse, - vis.aggs.aggs[0], - otherAggConfig + test('correctly merges other bucket with single terms agg', () => { + const aggConfigs = getAggConfigs(singleTerm.aggs); + const otherAggConfig = buildOtherBucketAgg( + aggConfigs, + aggConfigs.aggs[0] as IBucketAggConfig, + singleTermResponse ); - expect(mergedResponse.aggregations['1'].buckets[3].key).to.equal('__other__'); + expect(otherAggConfig).toBeDefined(); + if (otherAggConfig) { + const mergedResponse = mergeOtherBucketAggResponse( + aggConfigs, + singleTermResponse, + singleOtherResponse, + aggConfigs.aggs[0] as IBucketAggConfig, + otherAggConfig() + ); + expect(mergedResponse.aggregations['1'].buckets[3].key).toEqual('__other__'); + } }); - it('correctly merges other bucket with nested terms agg', () => { - init(visConfigNestedTerm); - const otherAggConfig = buildOtherBucketAgg(vis.aggs, vis.aggs.aggs[1], nestedTermResponse)(); - const mergedResponse = mergeOtherBucketAggResponse( - vis.aggs, - nestedTermResponse, - nestedOtherResponse, - vis.aggs.aggs[1], - otherAggConfig + test('correctly merges other bucket with nested terms agg', () => { + const aggConfigs = getAggConfigs(nestedTerm.aggs); + const otherAggConfig = buildOtherBucketAgg( + aggConfigs, + aggConfigs.aggs[1] as IBucketAggConfig, + nestedTermResponse ); - expect(mergedResponse.aggregations['1'].buckets[1]['2'].buckets[3].key).to.equal('__other__'); + expect(otherAggConfig).toBeDefined(); + if (otherAggConfig) { + const mergedResponse = mergeOtherBucketAggResponse( + aggConfigs, + nestedTermResponse, + nestedOtherResponse, + aggConfigs.aggs[1] as IBucketAggConfig, + otherAggConfig() + ); + + expect(mergedResponse.aggregations['1'].buckets[1]['2'].buckets[3].key).toEqual( + '__other__' + ); + } }); }); describe('updateMissingBucket', () => { - it('correctly updates missing bucket key', () => { - init(visConfigNestedTerm); - const updatedResponse = updateMissingBucket(singleTermResponse, vis.aggs, vis.aggs.aggs[0]); + test('correctly updates missing bucket key', () => { + const aggConfigs = getAggConfigs(nestedTerm.aggs); + const updatedResponse = updateMissingBucket( + singleTermResponse, + aggConfigs, + aggConfigs.aggs[0] as IBucketAggConfig + ); expect( - updatedResponse.aggregations['1'].buckets.find(bucket => bucket.key === '__missing__') - ).to.not.be('undefined'); + updatedResponse.aggregations['1'].buckets.find( + (bucket: Record) => bucket.key === '__missing__' + ) + ).toBeDefined(); }); }); }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.js b/src/legacy/core_plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.ts similarity index 65% rename from src/legacy/core_plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.js rename to src/legacy/core_plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.ts index ddab360161744c..42db37c81eadd6 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.js +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/_terms_other_bucket_helper.ts @@ -17,21 +17,24 @@ * under the License. */ -import _ from 'lodash'; +import { isNumber, keys, values, find, each, cloneDeep, flatten } from 'lodash'; import { esFilters, esQuery } from '../../../../../../../plugins/data/public'; import { AggGroupNames } from '../agg_groups'; +import { IAggConfigs } from '../agg_configs'; +import { IBucketAggConfig } from './_bucket_agg_type'; /** * walks the aggregation DSL and returns DSL starting at aggregation with id of startFromAggId * @param aggNestedDsl: aggregation config DSL (top level) * @param startFromId: id of an aggregation from where we want to get the nested DSL */ -const getNestedAggDSL = (aggNestedDsl, startFromAggId) => { +const getNestedAggDSL = (aggNestedDsl: Record, startFromAggId: string): any => { if (aggNestedDsl[startFromAggId]) { return aggNestedDsl[startFromAggId]; } - const nestedAggs = _.values(aggNestedDsl); + const nestedAggs: Array> = values(aggNestedDsl); let aggs; + for (let i = 0; i < nestedAggs.length; i++) { if (nestedAggs[i].aggs && (aggs = getNestedAggDSL(nestedAggs[i].aggs, startFromAggId))) { return aggs; @@ -46,27 +49,34 @@ const getNestedAggDSL = (aggNestedDsl, startFromAggId) => { * @param aggWithOtherBucket: AggConfig of the aggregation with other bucket enabled * @param key: key from the other bucket request for a specific other bucket */ -const getAggResultBuckets = (aggConfigs, response, aggWithOtherBucket, key) => { +const getAggResultBuckets = ( + aggConfigs: IAggConfigs, + response: any, + aggWithOtherBucket: IBucketAggConfig, + key: string +) => { const keyParts = key.split('-'); let responseAgg = response; for (const i in keyParts) { if (keyParts[i]) { - const responseAggs = _.values(responseAgg); + const responseAggs: Array> = values(responseAgg); // If you have multi aggs, we cannot just assume the first one is the `other` bucket, // so we need to loop over each agg until we find it. for (let aggId = 0; aggId < responseAggs.length; aggId++) { - const agg = responseAggs[aggId]; - const aggKey = _.keys(responseAgg)[aggId]; - const aggConfig = _.find(aggConfigs.aggs, agg => agg.id === aggKey); - const bucket = _.find(agg.buckets, (bucket, bucketObjKey) => { - const bucketKey = aggConfig - .getKey(bucket, Number.isInteger(bucketObjKey) ? null : bucketObjKey) - .toString(); - return bucketKey === keyParts[i]; - }); - if (bucket) { - responseAgg = bucket; - break; + const aggById = responseAggs[aggId]; + const aggKey = keys(responseAgg)[aggId]; + const aggConfig = find(aggConfigs.aggs, agg => agg.id === aggKey); + if (aggConfig) { + const aggResultBucket = find(aggById.buckets, (bucket, bucketObjKey) => { + const bucketKey = aggConfig + .getKey(bucket, isNumber(bucketObjKey) ? undefined : bucketObjKey) + .toString(); + return bucketKey === keyParts[i]; + }); + if (aggResultBucket) { + responseAgg = aggResultBucket; + break; + } } } } @@ -82,21 +92,20 @@ const getAggResultBuckets = (aggConfigs, response, aggWithOtherBucket, key) => { * @param responseAggs: array of aggregations from response * @param aggId: id of the aggregation with missing bucket */ -const getAggConfigResultMissingBuckets = (responseAggs, aggId) => { +const getAggConfigResultMissingBuckets = (responseAggs: any, aggId: string) => { const missingKey = '__missing__'; - let resultBuckets = []; + let resultBuckets: Array> = []; if (responseAggs[aggId]) { - const matchingBucket = responseAggs[aggId].buckets.find(bucket => bucket.key === missingKey); + const matchingBucket = responseAggs[aggId].buckets.find( + (bucket: Record) => bucket.key === missingKey + ); if (matchingBucket) resultBuckets.push(matchingBucket); return resultBuckets; } - _.each(responseAggs, agg => { + each(responseAggs, agg => { if (agg.buckets) { - _.each(agg.buckets, bucket => { - resultBuckets = [ - ...resultBuckets, - ...getAggConfigResultMissingBuckets(bucket, aggId, missingKey), - ]; + each(agg.buckets, bucket => { + resultBuckets = [...resultBuckets, ...getAggConfigResultMissingBuckets(bucket, aggId)]; }); } }); @@ -110,13 +119,24 @@ const getAggConfigResultMissingBuckets = (responseAggs, aggId) => { * @param key: the key for this specific other bucket * @param otherAgg: AggConfig of the aggregation with other bucket */ -const getOtherAggTerms = (requestAgg, key, otherAgg) => { +const getOtherAggTerms = ( + requestAgg: Record, + key: string, + otherAgg: IBucketAggConfig +) => { return requestAgg['other-filter'].filters.filters[key].bool.must_not - .filter(filter => filter.match_phrase && filter.match_phrase[otherAgg.params.field.name]) - .map(filter => filter.match_phrase[otherAgg.params.field.name]); + .filter( + (filter: Record) => + filter.match_phrase && filter.match_phrase[otherAgg.params.field.name] + ) + .map((filter: Record) => filter.match_phrase[otherAgg.params.field.name]); }; -export const buildOtherBucketAgg = (aggConfigs, aggWithOtherBucket, response) => { +export const buildOtherBucketAgg = ( + aggConfigs: IAggConfigs, + aggWithOtherBucket: IBucketAggConfig, + response: any +) => { const bucketAggs = aggConfigs.aggs.filter(agg => agg.type.type === AggGroupNames.Buckets); const index = bucketAggs.findIndex(agg => agg.id === aggWithOtherBucket.id); const aggs = aggConfigs.toDsl(); @@ -130,6 +150,7 @@ export const buildOtherBucketAgg = (aggConfigs, aggWithOtherBucket, response) => params: { filters: [], }, + enabled: false, }, { addToAggConfigs: false, @@ -145,25 +166,31 @@ export const buildOtherBucketAgg = (aggConfigs, aggWithOtherBucket, response) => let noAggBucketResults = false; // recursively create filters for all parent aggregation buckets - const walkBucketTree = (aggIndex, aggs, aggId, filters, key) => { + const walkBucketTree = ( + aggIndex: number, + aggregations: any, + aggId: string, + filters: any[], + key: string + ) => { // make sure there are actually results for the buckets - if (aggs[aggId].buckets.length < 1) { + if (aggregations[aggId].buckets.length < 1) { noAggBucketResults = true; return; } - const agg = aggs[aggId]; + const agg = aggregations[aggId]; const newAggIndex = aggIndex + 1; const newAgg = bucketAggs[newAggIndex]; const currentAgg = bucketAggs[aggIndex]; if (aggIndex < index) { - _.each(agg.buckets, (bucket, bucketObjKey) => { + each(agg.buckets, (bucket: any, bucketObjKey) => { const bucketKey = currentAgg.getKey( bucket, - Number.isInteger(bucketObjKey) ? null : bucketObjKey + isNumber(bucketObjKey) ? undefined : bucketObjKey ); - const filter = _.cloneDeep(bucket.filters) || currentAgg.createFilter(bucketKey); - const newFilters = _.flatten([...filters, filter]); + const filter = cloneDeep(bucket.filters) || currentAgg.createFilter(bucketKey); + const newFilters = flatten([...filters, filter]); walkBucketTree( newAggIndex, bucket, @@ -177,7 +204,7 @@ export const buildOtherBucketAgg = (aggConfigs, aggWithOtherBucket, response) => if ( !aggWithOtherBucket.params.missingBucket || - agg.buckets.some(bucket => bucket.key === '__missing__') + agg.buckets.some((bucket: { key: string }) => bucket.key === '__missing__') ) { filters.push( esFilters.buildExistsFilter( @@ -188,7 +215,7 @@ export const buildOtherBucketAgg = (aggConfigs, aggWithOtherBucket, response) => } // create not filters for all the buckets - _.each(agg.buckets, bucket => { + each(agg.buckets, bucket => { if (bucket.key === '__missing__') return; const filter = currentAgg.createFilter(bucket.key); filter.meta.negate = true; @@ -214,15 +241,15 @@ export const buildOtherBucketAgg = (aggConfigs, aggWithOtherBucket, response) => }; export const mergeOtherBucketAggResponse = ( - aggsConfig, - response, - otherResponse, - otherAgg, - requestAgg + aggsConfig: IAggConfigs, + response: any, + otherResponse: any, + otherAgg: IBucketAggConfig, + requestAgg: Record ) => { - const updatedResponse = _.cloneDeep(response); - _.each(otherResponse.aggregations['other-filter'].buckets, (bucket, key) => { - if (!bucket.doc_count) return; + const updatedResponse = cloneDeep(response); + each(otherResponse.aggregations['other-filter'].buckets, (bucket, key) => { + if (!bucket.doc_count || key === undefined) return; const bucketKey = key.replace(/^-/, ''); const aggResultBuckets = getAggResultBuckets( aggsConfig, @@ -241,7 +268,11 @@ export const mergeOtherBucketAggResponse = ( bucket.filters = [phraseFilter]; bucket.key = '__other__'; - if (aggResultBuckets.some(bucket => bucket.key === '__missing__')) { + if ( + aggResultBuckets.some( + (aggResultBucket: Record) => aggResultBucket.key === '__missing__' + ) + ) { bucket.filters.push( esFilters.buildExistsFilter(otherAgg.params.field, otherAgg.params.field.indexPattern) ); @@ -251,8 +282,12 @@ export const mergeOtherBucketAggResponse = ( return updatedResponse; }; -export const updateMissingBucket = (response, aggConfigs, agg) => { - const updatedResponse = _.cloneDeep(response); +export const updateMissingBucket = ( + response: any, + aggConfigs: IAggConfigs, + agg: IBucketAggConfig +) => { + const updatedResponse = cloneDeep(response); const aggResultBuckets = getAggConfigResultMissingBuckets(updatedResponse.aggregations, agg.id); aggResultBuckets.forEach(bucket => { bucket.key = '__missing__'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/terms.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/terms.ts index 0ed44aa8767442..8fd95c86d8476d 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/terms.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/terms.ts @@ -39,7 +39,6 @@ import { buildOtherBucketAgg, mergeOtherBucketAggResponse, updateMissingBucket, - // @ts-ignore } from './_terms_other_bucket_helper'; import { Schemas } from '../schemas'; import { AggGroupNames } from '../agg_groups'; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts index 8fb6140d55e319..bf185f78941de2 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts @@ -24,9 +24,9 @@ import { syncStates, BaseStateContainer, } from '../../../../../../../plugins/kibana_utils/public'; -import { esFilters, FilterManager, Filter } from '../../../../../../../plugins/data/public'; +import { esFilters, FilterManager, Filter, Query } from '../../../../../../../plugins/data/public'; -interface AppState { +export interface AppState { /** * Columns displayed in the table, cannot be changed by UI, just in discover's main app */ @@ -47,6 +47,7 @@ interface AppState { * Number of records to be fetched after the anchor records (older records) */ successorCount: number; + query?: Query; } interface GlobalState { diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/field_chooser.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/field_chooser.js index a175a1aebebdfa..df970ab5f25843 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/field_chooser.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/field_chooser.js @@ -24,7 +24,11 @@ import './discover_field'; import './discover_field_search_directive'; import './discover_index_pattern_directive'; import fieldChooserTemplate from './field_chooser.html'; -import { IndexPatternFieldList } from '../../../../../../../../plugins/data/public'; +import { + IndexPatternFieldList, + KBN_FIELD_TYPES, +} from '../../../../../../../../plugins/data/public'; +import { getMapsAppUrl, isFieldVisualizable, isMapsAppRegistered } from './lib/visualize_url_utils'; export function createFieldChooserDirective($location, config, $route) { return { @@ -186,8 +190,15 @@ export function createFieldChooserDirective($location, config, $route) { return ''; } + if ( + (field.type === KBN_FIELD_TYPES.GEO_POINT || field.type === KBN_FIELD_TYPES.GEO_SHAPE) && + isMapsAppRegistered() + ) { + return getMapsAppUrl(field, $scope.indexPattern, $scope.state, $scope.columns); + } + let agg = {}; - const isGeoPoint = field.type === 'geo_point'; + const isGeoPoint = field.type === KBN_FIELD_TYPES.GEO_POINT; const type = isGeoPoint ? 'tile_map' : 'histogram'; // If we're visualizing a date field, and our index is time based (and thus has a time filter), // then run a date histogram @@ -243,7 +254,7 @@ export function createFieldChooserDirective($location, config, $route) { $scope.computeDetails = function(field, recompute) { if (_.isUndefined(field.details) || recompute) { field.details = { - visualizeUrl: field.visualizable ? getVisualizeUrl(field) : null, + visualizeUrl: isFieldVisualizable(field) ? getVisualizeUrl(field) : null, ...fieldCalculator.getFieldValueCounts({ hits: $scope.hits, field: field, diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/detail_views/string.html b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/detail_views/string.html index 5d134911fc91b7..333dc472e956d8 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/detail_views/string.html +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/detail_views/string.html @@ -79,7 +79,7 @@ @@ -87,7 +87,7 @@ diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/visualize_url_utils.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/visualize_url_utils.ts new file mode 100644 index 00000000000000..8dbf3cd79ccb16 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/visualize_url_utils.ts @@ -0,0 +1,108 @@ +/* + * 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 uuid from 'uuid/v4'; +// @ts-ignore +import rison from 'rison-node'; +import { + IFieldType, + IIndexPattern, + KBN_FIELD_TYPES, +} from '../../../../../../../../../plugins/data/public'; +import { AppState } from '../../../angular/context_state'; +import { getServices } from '../../../../kibana_services'; + +function getMapsAppBaseUrl() { + const mapsAppVisAlias = getServices() + .visualizations.types.getAliases() + .find(({ name }) => { + return name === 'maps'; + }); + return mapsAppVisAlias ? mapsAppVisAlias.aliasUrl : null; +} + +export function isMapsAppRegistered() { + return getServices() + .visualizations.types.getAliases() + .some(({ name }) => { + return name === 'maps'; + }); +} + +export function isFieldVisualizable(field: IFieldType) { + if ( + (field.type === KBN_FIELD_TYPES.GEO_POINT || field.type === KBN_FIELD_TYPES.GEO_SHAPE) && + isMapsAppRegistered() + ) { + return true; + } + return field.visualizable; +} + +export function getMapsAppUrl( + field: IFieldType, + indexPattern: IIndexPattern, + appState: AppState, + columns: string[] +) { + const mapAppParams = new URLSearchParams(); + + // Copy global state + const locationSplit = window.location.href.split('discover?'); + if (locationSplit.length > 1) { + const discoverParams = new URLSearchParams(locationSplit[1]); + const globalStateUrlValue = discoverParams.get('_g'); + if (globalStateUrlValue) { + mapAppParams.set('_g', globalStateUrlValue); + } + } + + // Copy filters and query in app state + const mapsAppState: any = { + filters: appState.filters || [], + }; + if (appState.query) { + mapsAppState.query = appState.query; + } + // @ts-ignore + mapAppParams.set('_a', rison.encode(mapsAppState)); + + // create initial layer descriptor + const hasColumns = columns && columns.length && columns[0] !== '_source'; + mapAppParams.set( + 'initialLayers', + // @ts-ignore + rison.encode_array([ + { + id: uuid(), + label: indexPattern.title, + sourceDescriptor: { + id: uuid(), + type: 'ES_SEARCH', + geoField: field.name, + tooltipProperties: hasColumns ? columns : [], + indexPatternId: indexPattern.id, + }, + visible: true, + type: 'VECTOR', + }, + ]) + ); + + return getServices().addBasePath(`${getMapsAppBaseUrl()}?${mapAppParams.toString()}`); +} diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index a83d1176a71977..a9f32949628e98 100644 --- a/src/legacy/core_plugins/kibana/public/kibana.js +++ b/src/legacy/core_plugins/kibana/public/kibana.js @@ -26,8 +26,6 @@ import { npSetup } from 'ui/new_platform'; // import the uiExports that we want to "use" import 'uiExports/home'; -import 'uiExports/visTypes'; - import 'uiExports/visualize'; import 'uiExports/savedObjectTypes'; import 'uiExports/fieldFormatEditors'; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_list.test.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_list.test.tsx index 3faf164c365d9d..f547f1dee6a39a 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_list.test.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_list.test.tsx @@ -63,12 +63,31 @@ describe('NumberList', () => { test('should show an order error', () => { defaultProps.numberArray = [3, 1]; + defaultProps.validateAscendingOrder = true; defaultProps.showValidation = true; const comp = mountWithIntl(); expect(comp.find('EuiFormErrorText').length).toBe(1); }); + test('should show a duplicate error', () => { + defaultProps.numberArray = [3, 1, 3]; + defaultProps.disallowDuplicates = true; + defaultProps.showValidation = true; + const comp = mountWithIntl(); + + expect(comp.find('EuiFormErrorText').length).toBeGreaterThan(0); + }); + + test('should show many duplicate errors', () => { + defaultProps.numberArray = [3, 1, 3, 1, 3, 1, 3, 1]; + defaultProps.disallowDuplicates = true; + defaultProps.showValidation = true; + const comp = mountWithIntl(); + + expect(comp.find('EuiFormErrorText').length).toBe(6); + }); + test('should set validity as true', () => { mountWithIntl(); @@ -77,6 +96,7 @@ describe('NumberList', () => { test('should set validity as false when the order is invalid', () => { defaultProps.numberArray = [3, 2]; + defaultProps.validateAscendingOrder = true; const comp = mountWithIntl(); expect(defaultProps.setValidity).lastCalledWith(false); diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_list.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_list.tsx index 8e290ceedfeac1..a43c66c2e08ccf 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_list.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/number_list.tsx @@ -21,17 +21,14 @@ import React, { Fragment, useState, useEffect, useMemo, useCallback } from 'reac import { EuiSpacer, EuiButtonEmpty, EuiFlexItem, EuiFormErrorText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; import { NumberRow, NumberRowModel } from './number_row'; import { parse, EMPTY_STRING, getRange, - validateOrder, - validateValue, getNextModel, getInitModelList, - getUpdatedModels, + getValidatedModels, hasInvalidValues, } from './utils'; import { useValidation } from '../../utils'; @@ -41,6 +38,7 @@ export interface NumberListProps { numberArray: Array; range?: string; showValidation: boolean; + disallowDuplicates?: boolean; unitName: string; validateAscendingOrder?: boolean; onChange(list: Array): void; @@ -54,31 +52,27 @@ function NumberList({ range, showValidation, unitName, - validateAscendingOrder = true, + validateAscendingOrder = false, + disallowDuplicates = false, onChange, setTouched, setValidity, }: NumberListProps) { const numberRange = useMemo(() => getRange(range), [range]); const [models, setModels] = useState(getInitModelList(numberArray)); - const [ascendingError, setAscendingError] = useState(EMPTY_STRING); // set up validity for each model useEffect(() => { - let id: number | undefined; - if (validateAscendingOrder) { - const { isValidOrder, modelIndex } = validateOrder(numberArray); - id = isValidOrder ? undefined : modelIndex; - setAscendingError( - isValidOrder - ? EMPTY_STRING - : i18n.translate('visDefaultEditor.controls.numberList.invalidAscOrderErrorMessage', { - defaultMessage: 'The values should be in ascending order.', - }) - ); - } - setModels(state => getUpdatedModels(numberArray, state, numberRange, id)); - }, [numberArray, numberRange, validateAscendingOrder]); + setModels(state => + getValidatedModels( + numberArray, + state, + numberRange, + validateAscendingOrder, + disallowDuplicates + ) + ); + }, [numberArray, numberRange, validateAscendingOrder, disallowDuplicates]); // responsible for setting up an initial value ([0]) when there is no default value useEffect(() => { @@ -105,12 +99,10 @@ function NumberList({ onUpdate( models.map(model => { if (model.id === id) { - const { isInvalid, error } = validateValue(parsedValue, numberRange); return { id, value: parsedValue, - isInvalid, - error, + isInvalid: false, }; } return model; @@ -155,7 +147,6 @@ function NumberList({ {models.length - 1 !== arrayIndex && } ))} - {showValidation && ascendingError && {ascendingError}} diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/utils.test.ts b/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/utils.test.ts index 89fb5738db379f..9cffaadfc956df 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/utils.test.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/utils.test.ts @@ -19,13 +19,12 @@ import { getInitModelList, - getUpdatedModels, - validateOrder, hasInvalidValues, parse, validateValue, getNextModel, getRange, + getValidatedModels, } from './utils'; import { NumberListRange } from './range'; import { NumberRowModel } from './number_row'; @@ -33,6 +32,7 @@ import { NumberRowModel } from './number_row'; describe('NumberList utils', () => { let modelList: NumberRowModel[]; let range: NumberListRange; + let invalidEntry: NumberRowModel; beforeEach(() => { modelList = [ @@ -46,6 +46,12 @@ describe('NumberList utils', () => { maxInclusive: true, within: jest.fn(() => true), }; + invalidEntry = { + value: expect.any(Number), + isInvalid: true, + error: expect.any(String), + id: expect.any(String), + }; }); describe('getInitModelList', () => { @@ -65,27 +71,27 @@ describe('NumberList utils', () => { }); }); - describe('getUpdatedModels', () => { + describe('getValidatedModels', () => { test('should return model list when number list is empty', () => { - const updatedModelList = getUpdatedModels([], modelList, range); + const updatedModelList = getValidatedModels([], modelList, range); expect(updatedModelList).toEqual([{ value: 0, id: expect.any(String), isInvalid: false }]); }); test('should not update model list when number list is the same', () => { - const updatedModelList = getUpdatedModels([1, 2], modelList, range); + const updatedModelList = getValidatedModels([1, 2], modelList, range); expect(updatedModelList).toEqual(modelList); }); test('should update model list when number list was changed', () => { - const updatedModelList = getUpdatedModels([1, 3], modelList, range); + const updatedModelList = getValidatedModels([1, 3], modelList, range); modelList[1].value = 3; expect(updatedModelList).toEqual(modelList); }); test('should update model list when number list increased', () => { - const updatedModelList = getUpdatedModels([1, 2, 3], modelList, range); + const updatedModelList = getValidatedModels([1, 2, 3], modelList, range); expect(updatedModelList).toEqual([ ...modelList, { value: 3, id: expect.any(String), isInvalid: false }, @@ -93,45 +99,46 @@ describe('NumberList utils', () => { }); test('should update model list when number list decreased', () => { - const updatedModelList = getUpdatedModels([2], modelList, range); + const updatedModelList = getValidatedModels([2], modelList, range); expect(updatedModelList).toEqual([{ value: 2, id: '1', isInvalid: false }]); }); test('should update model list when number list has undefined value', () => { - const updatedModelList = getUpdatedModels([1, undefined], modelList, range); + const updatedModelList = getValidatedModels([1, undefined], modelList, range); modelList[1].value = ''; modelList[1].isInvalid = true; expect(updatedModelList).toEqual(modelList); }); - test('should update model list when number order is invalid', () => { - const updatedModelList = getUpdatedModels([1, 3, 2], modelList, range, 2); - expect(updatedModelList).toEqual([ - modelList[0], - { ...modelList[1], value: 3 }, - { value: 2, id: expect.any(String), isInvalid: true }, - ]); + test('should identify when a number is out of order', () => { + const updatedModelList = getValidatedModels([1, 3, 2], modelList, range, true); + expect(updatedModelList[2]).toEqual(invalidEntry); }); - }); - describe('validateOrder', () => { - test('should return true when order is valid', () => { - expect(validateOrder([1, 2])).toEqual({ - isValidOrder: true, - }); + test('should identify when many numbers are out of order', () => { + const updatedModelList = getValidatedModels([1, 3, 2, 3, 4, 2], modelList, range, true); + expect(updatedModelList[2]).toEqual(invalidEntry); + expect(updatedModelList[5]).toEqual(invalidEntry); }); - test('should return true when a number is undefined', () => { - expect(validateOrder([1, undefined])).toEqual({ - isValidOrder: true, - }); + test('should identify a duplicate', () => { + const updatedModelList = getValidatedModels([1, 2, 3, 6, 2], modelList, range, false, true); + expect(updatedModelList[4]).toEqual(invalidEntry); }); - test('should return false when order is invalid', () => { - expect(validateOrder([2, 1])).toEqual({ - isValidOrder: false, - modelIndex: 1, - }); + test('should identify many duplicates', () => { + const updatedModelList = getValidatedModels( + [2, 2, 2, 3, 4, 5, 2, 2, 3], + modelList, + range, + false, + true + ); + expect(updatedModelList[1]).toEqual(invalidEntry); + expect(updatedModelList[2]).toEqual(invalidEntry); + expect(updatedModelList[6]).toEqual(invalidEntry); + expect(updatedModelList[7]).toEqual(invalidEntry); + expect(updatedModelList[8]).toEqual(invalidEntry); }); }); diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts b/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts index e0f32366fc265f..c2ac63c98cbea0 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts @@ -49,6 +49,7 @@ function validateValue(value: number | '', numberRange: NumberListRange) { if (value === EMPTY_STRING) { result.isInvalid = true; + result.error = EMPTY_STRING; } else if (!numberRange.within(value)) { result.isInvalid = true; result.error = i18n.translate('visDefaultEditor.controls.numberList.invalidRangeErrorMessage', { @@ -60,19 +61,46 @@ function validateValue(value: number | '', numberRange: NumberListRange) { return result; } -function validateOrder(list: Array) { - const result: { isValidOrder: boolean; modelIndex?: number } = { - isValidOrder: true, +function validateValueAscending( + inputValue: number | '', + index: number, + list: Array +) { + const result: { isInvalidOrder: boolean; error?: string } = { + isInvalidOrder: false, }; - list.forEach((inputValue, index, array) => { - const previousModel = array[index - 1]; - if (previousModel !== undefined && inputValue !== undefined && inputValue <= previousModel) { - result.isValidOrder = false; - result.modelIndex = index; - } - }); + const previousModel = list[index - 1]; + if (previousModel !== undefined && inputValue !== undefined && inputValue <= previousModel) { + result.isInvalidOrder = true; + result.error = i18n.translate( + 'visDefaultEditor.controls.numberList.invalidAscOrderErrorMessage', + { + defaultMessage: 'Value is not in ascending order.', + } + ); + } + return result; +} + +function validateValueUnique( + inputValue: number | '', + index: number, + list: Array +) { + const result: { isDuplicate: boolean; error?: string } = { + isDuplicate: false, + }; + if (inputValue && list.indexOf(inputValue) !== index) { + result.isDuplicate = true; + result.error = i18n.translate( + 'visDefaultEditor.controls.numberList.duplicateValueErrorMessage', + { + defaultMessage: 'Duplicate value.', + } + ); + } return result; } @@ -101,11 +129,12 @@ function getInitModelList(list: Array): NumberRowModel[] { : [defaultModel]; } -function getUpdatedModels( +function getValidatedModels( numberList: Array, modelList: NumberRowModel[], numberRange: NumberListRange, - invalidOrderModelIndex?: number + validateAscendingOrder: boolean = false, + disallowDuplicates: boolean = false ): NumberRowModel[] { if (!numberList.length) { return [defaultModel]; @@ -113,12 +142,27 @@ function getUpdatedModels( return numberList.map((number, index) => { const model = modelList[index] || { id: generateId() }; const newValue: NumberRowModel['value'] = number === undefined ? EMPTY_STRING : number; - const { isInvalid, error } = validateValue(newValue, numberRange); + + const valueResult = numberRange ? validateValue(newValue, numberRange) : { isInvalid: false }; + + const ascendingResult = validateAscendingOrder + ? validateValueAscending(newValue, index, numberList) + : { isInvalidOrder: false }; + + const duplicationResult = disallowDuplicates + ? validateValueUnique(newValue, index, numberList) + : { isDuplicate: false }; + + const allErrors = [valueResult.error, ascendingResult.error, duplicationResult.error] + .filter(Boolean) + .join(' '); + return { ...model, value: newValue, - isInvalid: invalidOrderModelIndex === index ? true : isInvalid, - error, + isInvalid: + valueResult.isInvalid || ascendingResult.isInvalidOrder || duplicationResult.isDuplicate, + error: allErrors === EMPTY_STRING ? undefined : allErrors, }; }); } @@ -132,9 +176,8 @@ export { parse, getRange, validateValue, - validateOrder, getNextModel, getInitModelList, - getUpdatedModels, + getValidatedModels, hasInvalidValues, }; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentile_ranks.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentile_ranks.tsx index c6057b7ce2a997..fb7d8d78b28e31 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentile_ranks.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentile_ranks.tsx @@ -62,6 +62,7 @@ function PercentileRanksEditor({ unitName={i18n.translate('visDefaultEditor.controls.percentileRanks.valueUnitNameText', { defaultMessage: 'value', })} + validateAscendingOrder={true} showValidation={showValidation} onChange={setValue} setTouched={setTouched} diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentiles.test.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentiles.test.tsx new file mode 100644 index 00000000000000..020dbb351b4977 --- /dev/null +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentiles.test.tsx @@ -0,0 +1,64 @@ +/* + * 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 { AggParamEditorProps } from '../agg_param_props'; +import { IAggConfig } from '../../legacy_imports'; +import { VisState } from 'src/legacy/core_plugins/visualizations/public'; +import { mount } from 'enzyme'; +import { PercentilesEditor } from './percentiles'; + +describe('PercentilesEditor component', () => { + let setValue: jest.Mock; + let setValidity: jest.Mock; + let setTouched: jest.Mock; + let defaultProps: AggParamEditorProps>; + + beforeEach(() => { + setValue = jest.fn(); + setValidity = jest.fn(); + setTouched = jest.fn(); + + defaultProps = { + agg: {} as IAggConfig, + aggParam: {} as any, + formIsTouched: false, + value: [1, 5, 25, 50, 75, 95, 99], + editorConfig: {}, + showValidation: false, + setValue, + setValidity, + setTouched, + state: {} as VisState, + metricAggs: [] as IAggConfig[], + }; + }); + + it('should set valid state to true after adding a unique percentile', () => { + defaultProps.value = [1, 5, 25, 50, 70]; + mount(); + expect(setValidity).lastCalledWith(true); + }); + + it('should set valid state to false after adding a duplicate percentile', () => { + defaultProps.value = [1, 5, 25, 50, 50]; + mount(); + expect(setValidity).lastCalledWith(false); + }); +}); diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentiles.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentiles.tsx index 74e7957bc19444..9f1f26fe5446fd 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentiles.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentiles.tsx @@ -58,7 +58,7 @@ function PercentilesEditor({ labelledbyId={`visEditorPercentileLabel${agg.id}-legend`} numberArray={value} range="[0,100]" - validateAscendingOrder={false} + disallowDuplicates={true} unitName={i18n.translate('visDefaultEditor.controls.percentileRanks.percentUnitNameText', { defaultMessage: 'percent', })} diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 5dcf51ecc81eb6..a5f4ce2ce3c581 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -286,6 +286,19 @@ export { export { IRequestTypesMap, IResponseTypesMap } from './search'; export * from './search'; +/* + * UI components + */ + +export { + SearchBar, + SearchBarProps, + StatefulSearchBarProps, + FilterBar, + QueryStringInput, + IndexPatternSelect, +} from './ui'; + /** * Types to be shared externally * @public @@ -310,7 +323,7 @@ export { TimefilterContract, TimeHistoryContract, } from './query'; -export * from './ui'; + export { // kbn field types castEsToKbnFieldTypeName, diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index 018c2927031d00..a51362d0ba92ef 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -34,13 +34,14 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { debounce, compact, isEqual } from 'lodash'; import { Toast } from 'src/core/public'; -import { IDataPluginServices, IIndexPattern, SuggestionsComponent, Query } from '../..'; +import { IDataPluginServices, IIndexPattern, Query } from '../..'; import { QuerySuggestion, QuerySuggestionTypes } from '../../autocomplete'; import { withKibana, KibanaReactContextValue, toMountPoint } from '../../../../kibana_react/public'; import { fetchIndexPatterns } from './fetch_index_patterns'; import { QueryLanguageSwitcher } from './language_switcher'; import { PersistedLog, getQueryLog, matchPairs, toUser, fromUser } from '../../query'; +import { SuggestionsComponent } from '..'; interface Props { kibana: KibanaReactContextValue; diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index 66ad4dfb12e977..5083a1e68c6dd3 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -33,12 +33,10 @@ import { IIndexPattern, FilterBar, SavedQuery, - SavedQueryMeta, - SaveQueryForm, - SavedQueryManagementComponent, } from '../..'; import { QueryBarTopRow } from '../query_string_input/query_bar_top_row'; import { SavedQueryAttributes, TimeHistoryContract } from '../../query'; +import { SavedQueryMeta, SavedQueryManagementComponent, SaveQueryForm } from '..'; interface SearchBarInjectedDeps { kibana: KibanaReactContextValue; diff --git a/test/functional/page_objects/discover_page.js b/test/functional/page_objects/discover_page.js index 5ccc5625849d27..080b8c8ee753fc 100644 --- a/test/functional/page_objects/discover_page.js +++ b/test/functional/page_objects/discover_page.js @@ -206,12 +206,6 @@ export function DiscoverPageProvider({ getService, getPageObjects }) { return await testSubjects.getVisibleText('discoverQueryHits'); } - async query(queryString) { - await find.setValue('input[aria-label="Search input"]', queryString); - await find.clickByCssSelector('button[aria-label="Search"]'); - await PageObjects.header.waitUntilLoadingHasFinished(); - } - async getDocHeader() { const header = await find.byCssSelector('thead > tr:nth-child(1)'); return await header.getVisibleText(); diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts index 39ce2b3077c961..a7cd313038d69c 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts @@ -28,8 +28,6 @@ import 'ui/autoload/all'; // Used to run esaggs queries import 'uiExports/fieldFormats'; import 'uiExports/search'; -import 'uiExports/visRequestHandlers'; -import 'uiExports/visResponseHandlers'; // Used for kibana_context function import 'uiExports/savedObjectTypes'; diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/index.js b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/index.js index e15da9daa3cd79..b2497a824ba2b7 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/index.js +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/index.js @@ -20,7 +20,7 @@ export default function(kibana) { return new kibana.Plugin({ uiExports: { - visTypes: ['plugins/kbn_tp_custom_visualizations/self_changing_vis/self_changing_vis'], + hacks: ['plugins/kbn_tp_custom_visualizations/self_changing_vis/self_changing_vis'], }, }); } diff --git a/x-pack/legacy/plugins/canvas/public/legacy_start.ts b/x-pack/legacy/plugins/canvas/public/legacy_start.ts index 21bf5aaa6d8184..d7d1a940d3b43a 100644 --- a/x-pack/legacy/plugins/canvas/public/legacy_start.ts +++ b/x-pack/legacy/plugins/canvas/public/legacy_start.ts @@ -8,9 +8,6 @@ // Import the uiExports that the application uses // These will go away as these plugins are converted to NP import 'ui/autoload/all'; -import 'uiExports/visTypes'; -import 'uiExports/visResponseHandlers'; -import 'uiExports/visRequestHandlers'; import 'uiExports/savedObjectTypes'; import 'uiExports/spyModes'; import 'uiExports/embeddableFactories'; diff --git a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js index e76a204a6f27df..62cd253ff24d94 100644 --- a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js +++ b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js @@ -15,9 +15,7 @@ import { uiModules } from 'ui/modules'; // import the uiExports that we want to "use" import 'uiExports/contextMenuActions'; -import 'uiExports/visTypes'; -import 'uiExports/visResponseHandlers'; -import 'uiExports/visRequestHandlers'; + import 'uiExports/inspectorViews'; import 'uiExports/interpreter'; import 'uiExports/savedObjectTypes'; diff --git a/x-pack/legacy/plugins/lens/index.ts b/x-pack/legacy/plugins/lens/index.ts index bb0bf9b67ee2c4..5eda6c4b4ff7ae 100644 --- a/x-pack/legacy/plugins/lens/index.ts +++ b/x-pack/legacy/plugins/lens/index.ts @@ -33,7 +33,6 @@ export const lens: LegacyPluginInitializer = kibana => { embeddableFactories: [`plugins/${PLUGIN_ID}/legacy`], styleSheetPaths: resolve(__dirname, 'public/index.scss'), mappings, - visTypes: ['plugins/lens/register_vis_type_alias'], savedObjectsManagement: { lens: { defaultSearchField: 'title', diff --git a/x-pack/legacy/plugins/lens/public/legacy.ts b/x-pack/legacy/plugins/lens/public/legacy.ts index 8023bad34de667..1cfd3e198547d5 100644 --- a/x-pack/legacy/plugins/lens/public/legacy.ts +++ b/x-pack/legacy/plugins/lens/public/legacy.ts @@ -5,12 +5,15 @@ */ import { npSetup, npStart } from 'ui/new_platform'; -import { getFormat } from './legacy_imports'; +import { getFormat, visualizations } from './legacy_imports'; export * from './types'; import { plugin } from './index'; const pluginInstance = plugin(); -pluginInstance.setup(npSetup.core, { ...npSetup.plugins, __LEGACY: { formatFactory: getFormat } }); +pluginInstance.setup(npSetup.core, { + ...npSetup.plugins, + __LEGACY: { formatFactory: getFormat, visualizations }, +}); pluginInstance.start(npStart.core, npStart.plugins); diff --git a/x-pack/legacy/plugins/lens/public/legacy_imports.ts b/x-pack/legacy/plugins/lens/public/legacy_imports.ts index 9dcc22ddb1bb70..88f189fe3db5a7 100644 --- a/x-pack/legacy/plugins/lens/public/legacy_imports.ts +++ b/x-pack/legacy/plugins/lens/public/legacy_imports.ts @@ -5,3 +5,5 @@ */ export { getFormat, FormatFactory } from 'ui/visualize/loader/pipeline_helpers/utilities'; +export { setup as visualizations } from '../../../../../src/legacy/core_plugins/visualizations/public/np_ready/public/legacy'; +export { VisualizationsSetup } from '../../../../../src/legacy/core_plugins/visualizations/public'; diff --git a/x-pack/legacy/plugins/lens/public/plugin.tsx b/x-pack/legacy/plugins/lens/public/plugin.tsx index 634d2275598359..7f96268fc2e8c6 100644 --- a/x-pack/legacy/plugins/lens/public/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/plugin.tsx @@ -38,6 +38,8 @@ import { import { FormatFactory } from './legacy_imports'; import { IEmbeddableSetup, IEmbeddableStart } from '../../../../../src/plugins/embeddable/public'; import { EditorFrameStart } from './types'; +import { getLensAliasConfig } from './vis_type_alias'; +import { VisualizationsSetup } from './legacy_imports'; export interface LensPluginSetupDependencies { kibanaLegacy: KibanaLegacySetup; @@ -46,6 +48,7 @@ export interface LensPluginSetupDependencies { embeddable: IEmbeddableSetup; __LEGACY: { formatFactory: FormatFactory; + visualizations: VisualizationsSetup; }; } @@ -81,7 +84,7 @@ export class LensPlugin { expressions, data, embeddable, - __LEGACY: { formatFactory }, + __LEGACY: { formatFactory, visualizations }, }: LensPluginSetupDependencies ) { const editorFrameSetupInterface = this.editorFrameService.setup(core, { @@ -100,6 +103,8 @@ export class LensPlugin { this.datatableVisualization.setup(core, dependencies); this.metricVisualization.setup(core, dependencies); + visualizations.types.registerAlias(getLensAliasConfig()); + kibanaLegacy.registerLegacyApp({ id: 'lens', title: NOT_INTERNATIONALIZED_PRODUCT_NAME, diff --git a/x-pack/legacy/plugins/lens/public/register_vis_type_alias.ts b/x-pack/legacy/plugins/lens/public/vis_type_alias.ts similarity index 89% rename from x-pack/legacy/plugins/lens/public/register_vis_type_alias.ts rename to x-pack/legacy/plugins/lens/public/vis_type_alias.ts index f71796268065b1..c4e0a20110c81f 100644 --- a/x-pack/legacy/plugins/lens/public/register_vis_type_alias.ts +++ b/x-pack/legacy/plugins/lens/public/vis_type_alias.ts @@ -5,10 +5,10 @@ */ import { i18n } from '@kbn/i18n'; -import { setup as visualizations } from '../../../../../src/legacy/core_plugins/visualizations/public/np_ready/public/legacy'; import { getBasePath, getEditPath } from '../../../../plugins/lens/common'; +import { VisTypeAlias } from '../../../../../src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types'; -visualizations.types.registerAlias({ +export const getLensAliasConfig = (): VisTypeAlias => ({ aliasUrl: getBasePath(), name: 'lens', promotion: { diff --git a/x-pack/legacy/plugins/maps/index.js b/x-pack/legacy/plugins/maps/index.js index 5cd5a8731a7033..8048c21fe9333d 100644 --- a/x-pack/legacy/plugins/maps/index.js +++ b/x-pack/legacy/plugins/maps/index.js @@ -78,7 +78,7 @@ export function maps(kibana) { }, mappings, migrations, - visTypes: ['plugins/maps/register_vis_type_alias'], + hacks: ['plugins/maps/register_vis_type_alias'], }, config(Joi) { return Joi.object({ diff --git a/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.js b/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.js index 45ee441716769e..3cae75231d28e3 100644 --- a/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.js +++ b/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.js @@ -9,7 +9,7 @@ import { EMSTMSSource } from '../layers/sources/ems_tms_source'; import chrome from 'ui/chrome'; import { getKibanaTileMap } from '../meta'; -export function getInitialLayers(layerListJSON) { +export function getInitialLayers(layerListJSON, initialLayers = []) { if (layerListJSON) { return JSON.parse(layerListJSON); } @@ -19,7 +19,7 @@ export function getInitialLayers(layerListJSON) { const sourceDescriptor = KibanaTilemapSource.createDescriptor(); const source = new KibanaTilemapSource(sourceDescriptor); const layer = source.createDefaultLayer(); - return [layer.toLayerDescriptor()]; + return [layer.toLayerDescriptor(), ...initialLayers]; } const isEmsEnabled = chrome.getInjected('isEmsEnabled', true); @@ -27,8 +27,8 @@ export function getInitialLayers(layerListJSON) { const descriptor = EMSTMSSource.createDescriptor({ isAutoSelect: true }); const source = new EMSTMSSource(descriptor); const layer = source.createDefaultLayer(); - return [layer.toLayerDescriptor()]; + return [layer.toLayerDescriptor(), ...initialLayers]; } - return []; + return initialLayers; } diff --git a/x-pack/legacy/plugins/maps/public/angular/map_controller.js b/x-pack/legacy/plugins/maps/public/angular/map_controller.js index 95c8ff975b1d61..a8e9ae46a3b9ad 100644 --- a/x-pack/legacy/plugins/maps/public/angular/map_controller.js +++ b/x-pack/legacy/plugins/maps/public/angular/map_controller.js @@ -6,6 +6,7 @@ import _ from 'lodash'; import chrome from 'ui/chrome'; +import rison from 'rison-node'; import 'ui/directives/listen'; import 'ui/directives/storage'; import React from 'react'; @@ -66,6 +67,32 @@ const REACT_ANCHOR_DOM_ELEMENT_ID = 'react-maps-root'; const app = uiModules.get(MAP_APP_PATH, []); +function getInitialLayersFromUrlParam() { + const locationSplit = window.location.href.split('?'); + if (locationSplit.length <= 1) { + return []; + } + const mapAppParams = new URLSearchParams(locationSplit[1]); + if (!mapAppParams.has('initialLayers')) { + return []; + } + + try { + return rison.decode_array(mapAppParams.get('initialLayers')); + } catch (e) { + toastNotifications.addWarning({ + title: i18n.translate('xpack.maps.initialLayers.unableToParseTitle', { + defaultMessage: `Inital layers not added to map`, + }), + text: i18n.translate('xpack.maps.initialLayers.unableToParseMessage', { + defaultMessage: `Unable to parse contents of 'initialLayers' parameter. Error: {errorMsg}`, + values: { errorMsg: e.message }, + }), + }); + return []; + } +} + app.controller( 'GisMapController', ($scope, $route, kbnUrl, localStorage, AppState, globalState) => { @@ -333,7 +360,7 @@ app.controller( store.dispatch(setOpenTOCDetails(_.get(uiState, 'openTOCDetails', []))); } - const layerList = getInitialLayers(savedMap.layerListJSON); + const layerList = getInitialLayers(savedMap.layerListJSON, getInitialLayersFromUrlParam()); initialLayerListConfig = copyPersistentState(layerList); store.dispatch(replaceLayerList(layerList)); store.dispatch(setRefreshConfig($scope.refreshConfig)); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts index 120d644b3b33af..60ebd2578b7c02 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts @@ -236,7 +236,7 @@ describe('helpers', () => { description: '', deletedEventIds: [], eventIdToNoteIds: {}, - eventType: 'raw', + eventType: 'all', filters: [], highlightedDropAndProviderId: '', historyIds: [], @@ -330,7 +330,7 @@ describe('helpers', () => { description: '', deletedEventIds: [], eventIdToNoteIds: {}, - eventType: 'raw', + eventType: 'all', filters: [], highlightedDropAndProviderId: '', historyIds: [], @@ -417,7 +417,7 @@ describe('helpers', () => { description: '', deletedEventIds: [], eventIdToNoteIds: {}, - eventType: 'raw', + eventType: 'all', filters: [], highlightedDropAndProviderId: '', historyIds: [], @@ -539,7 +539,7 @@ describe('helpers', () => { description: '', deletedEventIds: [], eventIdToNoteIds: {}, - eventType: 'raw', + eventType: 'all', filters: [ { $state: { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/pick_events.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/pick_events.tsx index 76f9e6fe3673a2..3117bae7452864 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/pick_events.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/pick_events.tsx @@ -77,6 +77,7 @@ const PickEventTypeComponents: React.FC = ({ return ( { expect(wrapper.find('[data-test-subj="table-pagination"]').exists()).toEqual(false); }); + + test('it defaults to showing `All events`', () => { + const wrapper = mount( + + + + + + ); + + expect(wrapper.find('[data-test-subj="pick-event-type"] button').text()).toEqual( + 'All events' + ); + }); }); describe('event wire up', () => { diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/defaults.ts b/x-pack/legacy/plugins/siem/public/store/timeline/defaults.ts index bbaf2a3fb6e307..7f04bb4c4dad08 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/defaults.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/defaults.ts @@ -14,7 +14,7 @@ export const timelineDefaults: SubsetTimelineModel & Pick; /** @@ -186,22 +189,34 @@ export interface ESTotal { export type AlertHits = SearchResponse['hits']['hits']; export interface LegacyEndpointEvent { - '@timestamp': Date; + '@timestamp': number; endgame: { - event_type_full: string; - event_subtype_full: string; + pid?: number; + ppid?: number; + event_type_full?: string; + event_subtype_full?: string; + event_timestamp?: number; + event_type?: number; unique_pid: number; - unique_ppid: number; - serial_event_id: number; + unique_ppid?: number; + machine_id?: string; + process_name?: string; + process_path?: string; + timestamp_utc?: string; + serial_event_id?: number; }; agent: { id: string; type: string; + version: string; }; + process?: object; + rule?: object; + user?: object; } export interface EndpointEvent { - '@timestamp': Date; + '@timestamp': number; event: { category: string; type: string; @@ -216,6 +231,7 @@ export interface EndpointEvent { }; }; agent: { + id: string; type: string; }; } diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx index 7ab66817a08880..296587706e6acd 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx @@ -11,6 +11,7 @@ import { I18nProvider, FormattedMessage } from '@kbn/i18n/react'; import { Route, Switch, BrowserRouter } from 'react-router-dom'; import { Provider } from 'react-redux'; import { Store } from 'redux'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { RouteCapture } from './view/route_capture'; import { appStoreFactory } from './store'; import { AlertIndex } from './view/alerts'; @@ -24,9 +25,7 @@ import { HeaderNavigation } from './components/header_nav'; export function renderApp(coreStart: CoreStart, { appBasePath, element }: AppMountParameters) { coreStart.http.get('/api/endpoint/hello-world'); const store = appStoreFactory(coreStart); - - ReactDOM.render(, element); - + ReactDOM.render(, element); return () => { ReactDOM.unmountComponentAtNode(element); }; @@ -35,35 +34,46 @@ export function renderApp(coreStart: CoreStart, { appBasePath, element }: AppMou interface RouterProps { basename: string; store: Store; + coreStart: CoreStart; } -const AppRoot: React.FunctionComponent = React.memo(({ basename, store }) => ( - - - - - - - ( -

- -

- )} - /> - - } /> - - ( - - )} - /> -
-
-
-
-
-)); +const AppRoot: React.FunctionComponent = React.memo( + ({ basename, store, coreStart: { http } }) => ( + + + + + + + + ( +

+ +

+ )} + /> + + + + ( + + )} + /> +
+
+
+
+
+
+ ) +); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/mock_alert_result_list.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/mock_alert_result_list.ts index b90f897ea2229c..8eadb3e7fb3dfd 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/mock_alert_result_list.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/mock_alert_result_list.ts @@ -43,6 +43,7 @@ export const mockAlertResultList: (options?: { }, process: { pid: 107, + unique_pid: 1, }, host: { hostname: 'HD-c15-bc09190a', diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts index 54add85f0fe04c..f217e3cda91913 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts @@ -9,13 +9,13 @@ import { createSelector, createStructuredSelector as createStructuredSelectorWithBadType, } from 'reselect'; -import { Immutable } from '../../../../../common/types'; import { AlertListState, AlertingIndexUIQueryParams, AlertsAPIQueryParams, CreateStructuredSelector, } from '../../types'; +import { Immutable, LegacyEndpointEvent } from '../../../../../common/types'; const createStructuredSelector: CreateStructuredSelector = createStructuredSelectorWithBadType; /** @@ -92,3 +92,24 @@ export const hasSelectedAlert: (state: AlertListState) => boolean = createSelect uiQueryParams, ({ selected_alert: selectedAlert }) => selectedAlert !== undefined ); + +/** + * Determine if the alert event is most likely compatible with LegacyEndpointEvent. + */ +function isAlertEventLegacyEndpointEvent(event: { endgame?: {} }): event is LegacyEndpointEvent { + return event.endgame !== undefined && 'unique_pid' in event.endgame; +} + +export const selectedEvent: ( + state: AlertListState +) => LegacyEndpointEvent | undefined = createSelector( + uiQueryParams, + alertListData, + ({ selected_alert: selectedAlert }, alertList) => { + const found = alertList.find(alert => alert.event.id === selectedAlert); + if (!found) { + return found; + } + return isAlertEventLegacyEndpointEvent(found) ? found : undefined; + } +); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.test.tsx index 37847553d512ad..fe362f21a178ea 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.test.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.test.tsx @@ -11,6 +11,7 @@ import { I18nProvider } from '@kbn/i18n/react'; import { AlertIndex } from './index'; import { appStoreFactory } from '../../store'; import { coreMock } from 'src/core/public/mocks'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; import { fireEvent, waitForElement, act } from '@testing-library/react'; import { RouteCapture } from '../route_capture'; import { createMemoryHistory, MemoryHistory } from 'history'; @@ -44,6 +45,7 @@ describe('when on the alerting page', () => { * Create a store, with the middleware disabled. We don't want side effects being created by our code in this test. */ store = appStoreFactory(coreMock.createStart(), true); + /** * Render the test component, use this after setting up anything in `beforeEach`. */ @@ -56,13 +58,15 @@ describe('when on the alerting page', () => { */ return reactTestingLibrary.render( - - - - - - - + + + + + + + + + ); }; @@ -136,6 +140,9 @@ describe('when on the alerting page', () => { it('should show the flyout', async () => { await render().findByTestId('alertDetailFlyout'); }); + it('should render resolver', async () => { + await render().findByTestId('alertResolver'); + }); describe('when the user clicks the close button on the flyout', () => { let renderResult: reactTestingLibrary.RenderResult; beforeEach(async () => { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx index 6f88727575557d..3c229484ede4e0 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx @@ -25,6 +25,7 @@ import { urlFromQueryParams } from './url_from_query_params'; import { AlertData } from '../../../../../common/types'; import * as selectors from '../../store/alerts/selectors'; import { useAlertListSelector } from './hooks/use_alerts_selector'; +import { AlertDetailResolver } from './resolver'; export const AlertIndex = memo(() => { const history = useHistory(); @@ -86,6 +87,7 @@ export const AlertIndex = memo(() => { const alertListData = useAlertListSelector(selectors.alertListData); const hasSelectedAlert = useAlertListSelector(selectors.hasSelectedAlert); const queryParams = useAlertListSelector(selectors.uiQueryParams); + const selectedEvent = useAlertListSelector(selectors.selectedEvent); const onChangeItemsPerPage = useCallback( newPageSize => { @@ -132,12 +134,11 @@ export const AlertIndex = memo(() => { } const row = alertListData[rowIndex % pageSize]; - if (columnId === 'alert_type') { return ( {i18n.translate( 'xpack.endpoint.application.endpoint.alerts.alertType.maliciousFileDescription', @@ -213,7 +214,9 @@ export const AlertIndex = memo(() => { - + + + )} diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx new file mode 100644 index 00000000000000..c7ef7f73dfe05b --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx @@ -0,0 +1,35 @@ +/* + * 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 React from 'react'; +import styled from 'styled-components'; +import { Provider } from 'react-redux'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { Resolver } from '../../../../embeddables/resolver/view'; +import { EndpointPluginServices } from '../../../../plugin'; +import { LegacyEndpointEvent } from '../../../../../common/types'; +import { storeFactory } from '../../../../embeddables/resolver/store'; + +export const AlertDetailResolver = styled( + React.memo( + ({ className, selectedEvent }: { className?: string; selectedEvent?: LegacyEndpointEvent }) => { + const context = useKibana(); + const { store } = storeFactory(context); + return ( +
+ + + +
+ ); + } + ) +)` + height: 100%; + width: 100%; + display: flex; + flex-grow: 1; +`; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts index 0eb3505096b4a5..6892bf11ecff2e 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts @@ -5,15 +5,16 @@ */ import { uniquePidForProcess, uniqueParentPidForProcess } from './process_event'; -import { IndexedProcessTree, ProcessEvent } from '../types'; +import { IndexedProcessTree } from '../types'; +import { LegacyEndpointEvent } from '../../../../common/types'; import { levelOrder as baseLevelOrder } from '../lib/tree_sequencers'; /** * Create a new IndexedProcessTree from an array of ProcessEvents */ -export function factory(processes: ProcessEvent[]): IndexedProcessTree { - const idToChildren = new Map(); - const idToValue = new Map(); +export function factory(processes: LegacyEndpointEvent[]): IndexedProcessTree { + const idToChildren = new Map(); + const idToValue = new Map(); for (const process of processes) { idToValue.set(uniquePidForProcess(process), process); @@ -35,7 +36,10 @@ export function factory(processes: ProcessEvent[]): IndexedProcessTree { /** * Returns an array with any children `ProcessEvent`s of the passed in `process` */ -export function children(tree: IndexedProcessTree, process: ProcessEvent): ProcessEvent[] { +export function children( + tree: IndexedProcessTree, + process: LegacyEndpointEvent +): LegacyEndpointEvent[] { const id = uniquePidForProcess(process); const processChildren = tree.idToChildren.get(id); return processChildren === undefined ? [] : processChildren; @@ -46,8 +50,8 @@ export function children(tree: IndexedProcessTree, process: ProcessEvent): Proce */ export function parent( tree: IndexedProcessTree, - childProcess: ProcessEvent -): ProcessEvent | undefined { + childProcess: LegacyEndpointEvent +): LegacyEndpointEvent | undefined { const uniqueParentPid = uniqueParentPidForProcess(childProcess); if (uniqueParentPid === undefined) { return undefined; @@ -70,7 +74,7 @@ export function root(tree: IndexedProcessTree) { if (size(tree) === 0) { return null; } - let current: ProcessEvent = tree.idToProcess.values().next().value; + let current: LegacyEndpointEvent = tree.idToProcess.values().next().value; while (parent(tree, current) !== undefined) { current = parent(tree, current)!; } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.test.ts index 3177671a30001d..3916396f7402ce 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.test.ts @@ -4,22 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ import { eventType } from './process_event'; -import { ProcessEvent } from '../types'; +import { LegacyEndpointEvent } from '../../../../common/types'; import { mockProcessEvent } from './process_event_test_helpers'; describe('process event', () => { describe('eventType', () => { - let event: ProcessEvent; + let event: LegacyEndpointEvent; beforeEach(() => { event = mockProcessEvent({ - data_buffer: { - node_id: 1, + endgame: { + unique_pid: 1, event_type_full: 'process_event', }, }); }); it("returns the right value when the subType is 'creation_event'", () => { - event.data_buffer.event_subtype_full = 'creation_event'; + event.endgame.event_subtype_full = 'creation_event'; expect(eventType(event)).toEqual('processCreated'); }); }); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts index c8496b8e6e7a5d..876168d2ed96ac 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts @@ -4,23 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ProcessEvent } from '../types'; +import { LegacyEndpointEvent } from '../../../../common/types'; /** * Returns true if the process's eventType is either 'processCreated' or 'processRan'. * Resolver will only render 'graphable' process events. */ -export function isGraphableProcess(event: ProcessEvent) { - return eventType(event) === 'processCreated' || eventType(event) === 'processRan'; +export function isGraphableProcess(passedEvent: LegacyEndpointEvent) { + return eventType(passedEvent) === 'processCreated' || eventType(passedEvent) === 'processRan'; } /** * Returns a custom event type for a process event based on the event's metadata. */ -export function eventType(event: ProcessEvent) { +export function eventType(passedEvent: LegacyEndpointEvent) { const { - data_buffer: { event_type_full: type, event_subtype_full: subType }, - } = event; + endgame: { event_type_full: type, event_subtype_full: subType }, + } = passedEvent; if (type === 'process_event') { if (subType === 'creation_event' || subType === 'fork_event' || subType === 'exec_event') { @@ -41,13 +41,13 @@ export function eventType(event: ProcessEvent) { /** * Returns the process event's pid */ -export function uniquePidForProcess(event: ProcessEvent) { - return event.data_buffer.node_id; +export function uniquePidForProcess(event: LegacyEndpointEvent) { + return event.endgame.unique_pid; } /** * Returns the process event's parent pid */ -export function uniqueParentPidForProcess(event: ProcessEvent) { - return event.data_buffer.source_id; +export function uniqueParentPidForProcess(event: LegacyEndpointEvent) { + return event.endgame.unique_ppid; } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts index 9a6f19adcc1017..e88837d325108d 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts @@ -4,33 +4,46 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ProcessEvent } from '../types'; -type DeepPartial = { [K in keyof T]?: DeepPartial }; +import { LegacyEndpointEvent } from '../../../../common/types'; +type DeepPartial = { [K in keyof T]?: DeepPartial }; /** * Creates a mock process event given the 'parts' argument, which can * include all or some process event fields as determined by the ProcessEvent type. * The only field that must be provided is the event's 'node_id' field. * The other fields are populated by the function unless provided in 'parts' */ -export function mockProcessEvent( - parts: { - data_buffer: { node_id: ProcessEvent['data_buffer']['node_id'] }; - } & DeepPartial -): ProcessEvent { - const { data_buffer: dataBuffer } = parts; +export function mockProcessEvent(parts: { + endgame: { + unique_pid: LegacyEndpointEvent['endgame']['unique_pid']; + unique_ppid?: LegacyEndpointEvent['endgame']['unique_ppid']; + process_name?: LegacyEndpointEvent['endgame']['process_name']; + event_subtype_full?: LegacyEndpointEvent['endgame']['event_subtype_full']; + event_type_full?: LegacyEndpointEvent['endgame']['event_type_full']; + } & DeepPartial; +}): LegacyEndpointEvent { + const { endgame: dataBuffer } = parts; return { - event_timestamp: 1, - event_type: 1, - machine_id: '', - ...parts, - data_buffer: { - timestamp_utc: '2019-09-24 01:47:47Z', + endgame: { + ...dataBuffer, + event_timestamp: 1, + event_type: 1, + unique_ppid: 0, + unique_pid: 1, + machine_id: '', event_subtype_full: 'creation_event', event_type_full: 'process_event', process_name: '', process_path: '', - ...dataBuffer, + timestamp_utc: '', + serial_event_id: 1, + }, + '@timestamp': 1582233383000, + agent: { + type: '', + id: '', + version: '', }, + ...parts, }; } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts index 25f196c76a2904..ecba0ec404d44d 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts @@ -3,9 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ProcessEvent } from '../types'; import { CameraAction } from './camera'; import { DataAction } from './data'; +import { LegacyEndpointEvent } from '../../../../common/types'; /** * When the user wants to bring a process node front-and-center on the map. @@ -16,7 +16,7 @@ interface UserBroughtProcessIntoView { /** * Used to identify the process node that should be brought into view. */ - readonly process: ProcessEvent; + readonly process: LegacyEndpointEvent; /** * The time (since epoch in milliseconds) when the action was dispatched. */ @@ -24,4 +24,29 @@ interface UserBroughtProcessIntoView { }; } -export type ResolverAction = CameraAction | DataAction | UserBroughtProcessIntoView; +/** + * Used when the alert list selects an alert and the flyout shows resolver. + */ +interface UserChangedSelectedEvent { + readonly type: 'userChangedSelectedEvent'; + readonly payload: { + /** + * Optional because they could have unselected the event. + */ + selectedEvent?: LegacyEndpointEvent; + }; +} + +/** + * Triggered by middleware when the data for resolver needs to be loaded. Used to set state in redux to 'loading'. + */ +interface AppRequestedResolverData { + readonly type: 'appRequestedResolverData'; +} + +export type ResolverAction = + | CameraAction + | DataAction + | UserBroughtProcessIntoView + | UserChangedSelectedEvent + | AppRequestedResolverData; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap index 1dc17054b9f47c..b88652097eb5c5 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap @@ -12,17 +12,18 @@ Object { "edgeLineSegments": Array [], "processNodePositions": Map { Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "creation_event", "event_type_full": "process_event", - "node_id": 0, "process_name": "", - "process_path": "", - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 0, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 0, -0.8164965809277259, @@ -167,136 +168,137 @@ Object { ], "processNodePositions": Map { Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "creation_event", "event_type_full": "process_event", - "node_id": 0, "process_name": "", - "process_path": "", - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 0, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 0, -0.8164965809277259, ], Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "already_running", "event_type_full": "process_event", - "node_id": 1, - "process_name": "", - "process_path": "", - "source_id": 0, - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 1, + "unique_ppid": 0, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 0, -82.46615467370032, ], Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "creation_event", "event_type_full": "process_event", - "node_id": 2, - "process_name": "", - "process_path": "", - "source_id": 0, - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 2, + "unique_ppid": 0, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 141.4213562373095, -0.8164965809277259, ], Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "creation_event", "event_type_full": "process_event", - "node_id": 3, - "process_name": "", - "process_path": "", - "source_id": 1, - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 3, + "unique_ppid": 1, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 35.35533905932738, -143.70339824327976, ], Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "creation_event", "event_type_full": "process_event", - "node_id": 4, - "process_name": "", - "process_path": "", - "source_id": 1, - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 4, + "unique_ppid": 1, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 106.06601717798213, -102.87856919689347, ], Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "creation_event", "event_type_full": "process_event", - "node_id": 5, - "process_name": "", - "process_path": "", - "source_id": 2, - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 5, + "unique_ppid": 2, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 176.7766952966369, -62.053740150507174, ], Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "creation_event", "event_type_full": "process_event", - "node_id": 6, - "process_name": "", - "process_path": "", - "source_id": 2, - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 6, + "unique_ppid": 2, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 247.48737341529164, -21.228911104120883, ], Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "creation_event", "event_type_full": "process_event", - "node_id": 7, - "process_name": "", - "process_path": "", - "source_id": 6, - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 7, + "unique_ppid": 6, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 318.1980515339464, -62.05374015050717, @@ -321,34 +323,35 @@ Object { ], "processNodePositions": Map { Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "creation_event", "event_type_full": "process_event", - "node_id": 0, "process_name": "", - "process_path": "", - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 0, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 0, -0.8164965809277259, ], Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "already_running", "event_type_full": "process_event", - "node_id": 1, - "process_name": "", - "process_path": "", - "source_id": 0, - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 1, + "unique_ppid": 0, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 70.71067811865476, -41.641325627314025, diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts index 900b9bda571dae..f34d7c08ce08cc 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ProcessEvent } from '../../types'; +import { LegacyEndpointEvent } from '../../../../../common/types'; interface ServerReturnedResolverData { readonly type: 'serverReturnedResolverData'; readonly payload: { readonly data: { readonly result: { - readonly search_results: readonly ProcessEvent[]; + readonly search_results: readonly LegacyEndpointEvent[]; }; }; }; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts index fac70433f14b29..f01136fe20ebf9 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts @@ -7,20 +7,21 @@ import { Store, createStore } from 'redux'; import { DataAction } from './action'; import { dataReducer } from './reducer'; -import { DataState, ProcessEvent } from '../../types'; +import { DataState } from '../../types'; +import { LegacyEndpointEvent } from '../../../../../common/types'; import { graphableProcesses, processNodePositionsAndEdgeLineSegments } from './selectors'; import { mockProcessEvent } from '../../models/process_event_test_helpers'; describe('resolver graph layout', () => { - let processA: ProcessEvent; - let processB: ProcessEvent; - let processC: ProcessEvent; - let processD: ProcessEvent; - let processE: ProcessEvent; - let processF: ProcessEvent; - let processG: ProcessEvent; - let processH: ProcessEvent; - let processI: ProcessEvent; + let processA: LegacyEndpointEvent; + let processB: LegacyEndpointEvent; + let processC: LegacyEndpointEvent; + let processD: LegacyEndpointEvent; + let processE: LegacyEndpointEvent; + let processF: LegacyEndpointEvent; + let processG: LegacyEndpointEvent; + let processH: LegacyEndpointEvent; + let processI: LegacyEndpointEvent; let store: Store; beforeEach(() => { @@ -37,75 +38,75 @@ describe('resolver graph layout', () => { * */ processA = mockProcessEvent({ - data_buffer: { + endgame: { process_name: '', event_type_full: 'process_event', event_subtype_full: 'creation_event', - node_id: 0, + unique_pid: 0, }, }); processB = mockProcessEvent({ - data_buffer: { + endgame: { event_type_full: 'process_event', event_subtype_full: 'already_running', - node_id: 1, - source_id: 0, + unique_pid: 1, + unique_ppid: 0, }, }); processC = mockProcessEvent({ - data_buffer: { + endgame: { event_type_full: 'process_event', event_subtype_full: 'creation_event', - node_id: 2, - source_id: 0, + unique_pid: 2, + unique_ppid: 0, }, }); processD = mockProcessEvent({ - data_buffer: { + endgame: { event_type_full: 'process_event', event_subtype_full: 'creation_event', - node_id: 3, - source_id: 1, + unique_pid: 3, + unique_ppid: 1, }, }); processE = mockProcessEvent({ - data_buffer: { + endgame: { event_type_full: 'process_event', event_subtype_full: 'creation_event', - node_id: 4, - source_id: 1, + unique_pid: 4, + unique_ppid: 1, }, }); processF = mockProcessEvent({ - data_buffer: { + endgame: { event_type_full: 'process_event', event_subtype_full: 'creation_event', - node_id: 5, - source_id: 2, + unique_pid: 5, + unique_ppid: 2, }, }); processG = mockProcessEvent({ - data_buffer: { + endgame: { event_type_full: 'process_event', event_subtype_full: 'creation_event', - node_id: 6, - source_id: 2, + unique_pid: 6, + unique_ppid: 2, }, }); processH = mockProcessEvent({ - data_buffer: { + endgame: { event_type_full: 'process_event', event_subtype_full: 'creation_event', - node_id: 7, - source_id: 6, + unique_pid: 7, + unique_ppid: 6, }, }); processI = mockProcessEvent({ - data_buffer: { + endgame: { event_type_full: 'process_event', event_subtype_full: 'termination_event', - node_id: 8, - source_id: 0, + unique_pid: 8, + unique_ppid: 0, }, }); store = createStore(dataReducer, undefined); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts index 848d814808bac4..a3184389a794e8 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts @@ -6,11 +6,11 @@ import { Reducer } from 'redux'; import { DataState, ResolverAction } from '../../types'; -import { sampleData } from './sample'; function initialState(): DataState { return { - results: sampleData.data.result.search_results, + results: [], + isLoading: false, }; } @@ -24,6 +24,12 @@ export const dataReducer: Reducer = (state = initialS return { ...state, results: search_results, + isLoading: false, + }; + } else if (action.type === 'appRequestedResolverData') { + return { + ...state, + isLoading: true, }; } else { return state; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index 75b477dd7c7fcd..304abbb06880b2 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -7,7 +7,6 @@ import { createSelector } from 'reselect'; import { DataState, - ProcessEvent, IndexedProcessTree, ProcessWidths, ProcessPositions, @@ -15,6 +14,7 @@ import { ProcessWithWidthMetadata, Matrix3, } from '../../types'; +import { LegacyEndpointEvent } from '../../../../../common/types'; import { Vector2 } from '../../types'; import { add as vector2Add, applyMatrix3 } from '../../lib/vector2'; import { isGraphableProcess } from '../../models/process_event'; @@ -29,6 +29,10 @@ import { const unit = 100; const distanceBetweenNodesInUnits = 1; +export function isLoading(state: DataState) { + return state.isLoading; +} + /** * An isometric projection is a method for representing three dimensional objects in 2 dimensions. * More information about isometric projections can be found here https://en.wikipedia.org/wiki/Isometric_projection. @@ -108,7 +112,7 @@ export const graphableProcesses = createSelector( * */ function widthsOfProcessSubtrees(indexedProcessTree: IndexedProcessTree): ProcessWidths { - const widths = new Map(); + const widths = new Map(); if (size(indexedProcessTree) === 0) { return widths; @@ -309,13 +313,13 @@ function processPositions( indexedProcessTree: IndexedProcessTree, widths: ProcessWidths ): ProcessPositions { - const positions = new Map(); + const positions = new Map(); /** * This algorithm iterates the tree in level order. It keeps counters that are reset for each parent. * By keeping track of the last parent node, we can know when we are dealing with a new set of siblings and * reset the counters. */ - let lastProcessedParentNode: ProcessEvent | undefined; + let lastProcessedParentNode: LegacyEndpointEvent | undefined; /** * Nodes are positioned relative to their siblings. We walk this in level order, so we handle * children left -> right. @@ -420,7 +424,7 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( * Transform the positions of nodes and edges so they seem like they are on an isometric grid. */ const transformedEdgeLineSegments: EdgeLineSegment[] = []; - const transformedPositions = new Map(); + const transformedPositions = new Map(); for (const [processEvent, position] of positions) { transformedPositions.set(processEvent, applyMatrix3(position, isometricTransformMatrix)); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts index b17572bbc4ab46..2a20c73347348f 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts @@ -6,17 +6,21 @@ import { createStore, applyMiddleware, Store } from 'redux'; import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly'; +import { KibanaReactContextValue } from '../../../../../../../src/plugins/kibana_react/public'; import { ResolverAction, ResolverState } from '../types'; +import { EndpointPluginServices } from '../../../plugin'; import { resolverReducer } from './reducer'; +import { resolverMiddlewareFactory } from './middleware'; -export const storeFactory = (): { store: Store } => { +export const storeFactory = ( + context?: KibanaReactContextValue +): { store: Store } => { const actionsBlacklist: Array = ['userMovedPointer']; const composeEnhancers = composeWithDevTools({ name: 'Resolver', actionsBlacklist, }); - - const middlewareEnhancer = applyMiddleware(); + const middlewareEnhancer = applyMiddleware(resolverMiddlewareFactory(context)); const store = createStore(resolverReducer, composeEnhancers(middlewareEnhancer)); return { diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/methods.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/methods.ts index 8808160c9c631a..9f06643626f500 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/methods.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/methods.ts @@ -6,7 +6,8 @@ import { animatePanning } from './camera/methods'; import { processNodePositionsAndEdgeLineSegments } from './selectors'; -import { ResolverState, ProcessEvent } from '../types'; +import { ResolverState } from '../types'; +import { LegacyEndpointEvent } from '../../../../common/types'; const animationDuration = 1000; @@ -16,7 +17,7 @@ const animationDuration = 1000; export function animateProcessIntoView( state: ResolverState, startTime: number, - process: ProcessEvent + process: LegacyEndpointEvent ): ResolverState { const { processNodePositions } = processNodePositionsAndEdgeLineSegments(state); const position = processNodePositions.get(process); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts new file mode 100644 index 00000000000000..900aece60618d3 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts @@ -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 { Dispatch, MiddlewareAPI } from 'redux'; +import { KibanaReactContextValue } from '../../../../../../../src/plugins/kibana_react/public'; +import { EndpointPluginServices } from '../../../plugin'; +import { ResolverState, ResolverAction } from '../types'; + +type MiddlewareFactory = ( + context?: KibanaReactContextValue +) => ( + api: MiddlewareAPI, S> +) => (next: Dispatch) => (action: ResolverAction) => unknown; + +export const resolverMiddlewareFactory: MiddlewareFactory = context => { + return api => next => async (action: ResolverAction) => { + next(action); + if (action.type === 'userChangedSelectedEvent') { + if (context?.services.http) { + api.dispatch({ type: 'appRequestedResolverData' }); + const uniquePid = action.payload.selectedEvent?.endgame?.unique_pid; + const legacyEndpointID = action.payload.selectedEvent?.agent?.id; + const [{ lifecycle }, { children }, { events: relatedEvents }] = await Promise.all([ + context.services.http.get(`/api/endpoint/resolver/${uniquePid}`, { + query: { legacyEndpointID }, + }), + context.services.http.get(`/api/endpoint/resolver/${uniquePid}/children`, { + query: { legacyEndpointID }, + }), + context.services.http.get(`/api/endpoint/resolver/${uniquePid}/related`, { + query: { legacyEndpointID }, + }), + ]); + const response = [...lifecycle, ...children, ...relatedEvents]; + api.dispatch({ + type: 'serverReturnedResolverData', + payload: { data: { result: { search_results: response } } }, + }); + } + } + }; +}; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts index 25d08a8c347ed5..708eb684ebd3e6 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts @@ -68,6 +68,11 @@ function dataStateSelector(state: ResolverState) { return state.data; } +/** + * Whether or not the resolver is pending fetching data + */ +export const isLoading = composeSelectors(dataStateSelector, dataSelectors.isLoading); + /** * Calls the `secondSelector` with the result of the `selector`. Use this when re-exporting a * concern-specific selector. `selector` should return the concern-specific state. diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index 6c6936d377deac..4c2a1ea5ac21f2 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -8,6 +8,7 @@ import { Store } from 'redux'; import { ResolverAction } from './store/actions'; export { ResolverAction } from './store/actions'; +import { LegacyEndpointEvent } from '../../../common/types'; /** * Redux state for the Resolver feature. Properties on this interface are populated via multiple reducers using redux's `combineReducers`. @@ -114,7 +115,8 @@ export type CameraState = { * State for `data` reducer which handles receiving Resolver data from the backend. */ export interface DataState { - readonly results: readonly ProcessEvent[]; + readonly results: readonly LegacyEndpointEvent[]; + isLoading: boolean; } export type Vector2 = readonly [number, number]; @@ -182,21 +184,21 @@ export interface IndexedProcessTree { /** * Map of ID to a process's children */ - idToChildren: Map; + idToChildren: Map; /** * Map of ID to process */ - idToProcess: Map; + idToProcess: Map; } /** * A map of ProcessEvents (representing process nodes) to the 'width' of their subtrees as calculated by `widthsOfProcessSubtrees` */ -export type ProcessWidths = Map; +export type ProcessWidths = Map; /** * Map of ProcessEvents (representing process nodes) to their positions. Calculated by `processPositions` */ -export type ProcessPositions = Map; +export type ProcessPositions = Map; /** * An array of vectors2 forming an polyline. Used to connect process nodes in the graph. */ @@ -206,11 +208,11 @@ export type EdgeLineSegment = Vector2[]; * Used to provide precalculated info from `widthsOfProcessSubtrees`. These 'width' values are used in the layout of the graph. */ export type ProcessWithWidthMetadata = { - process: ProcessEvent; + process: LegacyEndpointEvent; width: number; } & ( | { - parent: ProcessEvent; + parent: LegacyEndpointEvent; parentWidth: number; isOnlyChild: boolean; firstChildWidth: number; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx index d71a4d87b7eab6..52a0872f269f5a 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -4,15 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { useSelector } from 'react-redux'; +import React, { useLayoutEffect } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; import styled from 'styled-components'; +import { EuiLoadingSpinner } from '@elastic/eui'; import * as selectors from '../store/selectors'; import { EdgeLine } from './edge_line'; import { Panel } from './panel'; import { GraphControls } from './graph_controls'; import { ProcessEventDot } from './process_event_dot'; import { useCamera } from './use_camera'; +import { ResolverAction } from '../types'; +import { LegacyEndpointEvent } from '../../../../common/types'; const StyledPanel = styled(Panel)` position: absolute; @@ -31,35 +34,57 @@ const StyledGraphControls = styled(GraphControls)` `; export const Resolver = styled( - React.memo(function Resolver({ className }: { className?: string }) { + React.memo(function Resolver({ + className, + selectedEvent, + }: { + className?: string; + selectedEvent?: LegacyEndpointEvent; + }) { const { processNodePositions, edgeLineSegments } = useSelector( selectors.processNodePositionsAndEdgeLineSegments ); + const dispatch: (action: ResolverAction) => unknown = useDispatch(); const { projectionMatrix, ref, onMouseDown } = useCamera(); + const isLoading = useSelector(selectors.isLoading); + useLayoutEffect(() => { + dispatch({ + type: 'userChangedSelectedEvent', + payload: { selectedEvent }, + }); + }, [dispatch, selectedEvent]); return (
-
- {Array.from(processNodePositions).map(([processEvent, position], index) => ( - - ))} - {edgeLineSegments.map(([startPosition, endPosition], index) => ( - - ))} -
- - + {isLoading ? ( +
+ +
+ ) : ( + <> +
+ {Array.from(processNodePositions).map(([processEvent, position], index) => ( + + ))} + {edgeLineSegments.map(([startPosition, endPosition], index) => ( + + ))} +
+ + + + )}
); }) @@ -72,6 +97,12 @@ export const Resolver = styled( display: flex; flex-grow: 1; } + .loading-container { + display: flex; + align-items: center; + justify-content: center; + flex-grow: 1; + } /** * The placeholder components use absolute positioning. */ diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/panel.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/panel.tsx index c75b73b4bceafd..84c299698bb32c 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/panel.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/panel.tsx @@ -11,7 +11,7 @@ import euiVars from '@elastic/eui/dist/eui_theme_light.json'; import { useSelector } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { SideEffectContext } from './side_effect_context'; -import { ProcessEvent } from '../types'; +import { LegacyEndpointEvent } from '../../../../common/types'; import { useResolverDispatch } from './use_resolver_dispatch'; import * as selectors from '../store/selectors'; @@ -38,7 +38,7 @@ export const Panel = memo(function Event({ className }: { className?: string }) interface ProcessTableView { name: string; timestamp?: Date; - event: ProcessEvent; + event: LegacyEndpointEvent; } const { processNodePositions } = useSelector(selectors.processNodePositionsAndEdgeLineSegments); @@ -47,11 +47,16 @@ export const Panel = memo(function Event({ className }: { className?: string }) const processTableView: ProcessTableView[] = useMemo( () => [...processNodePositions.keys()].map(processEvent => { - const { data_buffer } = processEvent; - const date = new Date(data_buffer.timestamp_utc); + let dateTime; + if (processEvent.endgame.timestamp_utc) { + const date = new Date(processEvent.endgame.timestamp_utc); + if (isFinite(date.getTime())) { + dateTime = date; + } + } return { - name: data_buffer.process_name, - timestamp: isFinite(date.getTime()) ? date : undefined, + name: processEvent.endgame.process_name ? processEvent.endgame.process_name : '', + timestamp: dateTime, event: processEvent, }; }), diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx index 384fbf90ed9847..034780c7ba14c8 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx @@ -7,7 +7,8 @@ import React from 'react'; import styled from 'styled-components'; import { applyMatrix3 } from '../lib/vector2'; -import { Vector2, ProcessEvent, Matrix3 } from '../types'; +import { Vector2, Matrix3 } from '../types'; +import { LegacyEndpointEvent } from '../../../../common/types'; /** * A placeholder view for a process node. @@ -31,7 +32,7 @@ export const ProcessEventDot = styled( /** * An event which contains details about the process node. */ - event: ProcessEvent; + event: LegacyEndpointEvent; /** * projectionMatrix which can be used to convert `position` to screen coordinates. */ @@ -48,7 +49,7 @@ export const ProcessEventDot = styled( }; return ( - name: {event.data_buffer.process_name} + name: {event.endgame.process_name}
x: {position[0]}
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx index f4abb51f062f2a..1948c6cae505bc 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx @@ -10,16 +10,12 @@ import { useCamera } from './use_camera'; import { Provider } from 'react-redux'; import * as selectors from '../store/selectors'; import { storeFactory } from '../store'; -import { - Matrix3, - ResolverAction, - ResolverStore, - ProcessEvent, - SideEffectSimulator, -} from '../types'; +import { Matrix3, ResolverAction, ResolverStore, SideEffectSimulator } from '../types'; +import { LegacyEndpointEvent } from '../../../../common/types'; import { SideEffectContext } from './side_effect_context'; import { applyMatrix3 } from '../lib/vector2'; import { sideEffectSimulator } from './side_effect_simulator'; +import { mockProcessEvent } from '../models/process_event_test_helpers'; describe('useCamera on an unpainted element', () => { let element: HTMLElement; @@ -28,6 +24,7 @@ describe('useCamera on an unpainted element', () => { let reactRenderResult: RenderResult; let store: ResolverStore; let simulator: SideEffectSimulator; + beforeEach(async () => { ({ store } = storeFactory()); @@ -136,17 +133,45 @@ describe('useCamera on an unpainted element', () => { expect(simulator.mock.requestAnimationFrame).not.toHaveBeenCalled(); }); describe('when the camera begins animation', () => { - let process: ProcessEvent; + let process: LegacyEndpointEvent; beforeEach(() => { - // At this time, processes are provided via mock data. In the future, this test will have to provide those mocks. - const processes: ProcessEvent[] = [ + const events: LegacyEndpointEvent[] = []; + const numberOfEvents: number = Math.floor(Math.random() * 10 + 1); + + for (let index = 0; index < numberOfEvents; index++) { + const uniquePpid = index === 0 ? undefined : index - 1; + events.push( + mockProcessEvent({ + endgame: { + unique_pid: index, + unique_ppid: uniquePpid, + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + }, + }) + ); + } + const serverResponseAction: ResolverAction = { + type: 'serverReturnedResolverData', + payload: { + data: { + result: { + search_results: events, + }, + }, + }, + }; + act(() => { + store.dispatch(serverResponseAction); + }); + const processes: LegacyEndpointEvent[] = [ ...selectors .processNodePositionsAndEdgeLineSegments(store.getState()) .processNodePositions.keys(), ]; process = processes[processes.length - 1]; simulator.controls.time = 0; - const action: ResolverAction = { + const cameraAction: ResolverAction = { type: 'userBroughtProcessIntoView', payload: { time: simulator.controls.time, @@ -154,7 +179,7 @@ describe('useCamera on an unpainted element', () => { }, }; act(() => { - store.dispatch(action); + store.dispatch(cameraAction); }); }); diff --git a/x-pack/plugins/endpoint/public/plugin.ts b/x-pack/plugins/endpoint/public/plugin.ts index 355364253b2a52..0e10fe680e9f04 100644 --- a/x-pack/plugins/endpoint/public/plugin.ts +++ b/x-pack/plugins/endpoint/public/plugin.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; +import { Plugin, CoreSetup, AppMountParameters, CoreStart } from 'kibana/public'; import { IEmbeddableSetup } from 'src/plugins/embeddable/public'; import { i18n } from '@kbn/i18n'; import { ResolverEmbeddableFactory } from './embeddables/resolver'; @@ -17,6 +17,15 @@ export interface EndpointPluginSetupDependencies { export interface EndpointPluginStartDependencies {} // eslint-disable-line @typescript-eslint/no-empty-interface +/** + * Functionality that the endpoint plugin uses from core. + */ +export interface EndpointPluginServices extends Partial { + http: CoreStart['http']; + overlays: CoreStart['overlays'] | undefined; + notifications: CoreStart['notifications'] | undefined; +} + export class EndpointPlugin implements Plugin< diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts index 2dd2e0c2d1d5f8..08a906e2884d68 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts @@ -8,7 +8,7 @@ import { EndpointAppConstants } from '../../../../common/types'; describe('children events query', () => { it('generates the correct legacy queries', () => { - const timestamp = new Date(); + const timestamp = new Date().getTime(); expect( new ChildrenQuery('awesome-id', { size: 1, timestamp, eventID: 'foo' }).build('5') ).toStrictEqual({ @@ -38,7 +38,7 @@ describe('children events query', () => { }, }, }, - search_after: [timestamp.getTime(), 'foo'], + search_after: [timestamp, 'foo'], size: 1, sort: [{ '@timestamp': 'asc' }, { 'endgame.serial_event_id': 'asc' }], }, @@ -47,7 +47,7 @@ describe('children events query', () => { }); it('generates the correct non-legacy queries', () => { - const timestamp = new Date(); + const timestamp = new Date().getTime(); expect( new ChildrenQuery(undefined, { size: 1, timestamp, eventID: 'bar' }).build('baz') @@ -84,7 +84,7 @@ describe('children events query', () => { }, }, }, - search_after: [timestamp.getTime(), 'bar'], + search_after: [timestamp, 'bar'], size: 1, sort: [{ '@timestamp': 'asc' }, { 'event.id': 'asc' }], }, diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.test.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.test.ts index 8ef680a168310d..a91c87274b8dd5 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.test.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.test.ts @@ -8,7 +8,7 @@ import { EndpointAppConstants } from '../../../../common/types'; describe('related events query', () => { it('generates the correct legacy queries', () => { - const timestamp = new Date(); + const timestamp = new Date().getTime(); expect( new RelatedEventsQuery('awesome-id', { size: 1, timestamp, eventID: 'foo' }).build('5') ).toStrictEqual({ @@ -39,7 +39,7 @@ describe('related events query', () => { }, }, }, - search_after: [timestamp.getTime(), 'foo'], + search_after: [timestamp, 'foo'], size: 1, sort: [{ '@timestamp': 'asc' }, { 'endgame.serial_event_id': 'asc' }], }, @@ -48,7 +48,7 @@ describe('related events query', () => { }); it('generates the correct non-legacy queries', () => { - const timestamp = new Date(); + const timestamp = new Date().getTime(); expect( new RelatedEventsQuery(undefined, { size: 1, timestamp, eventID: 'bar' }).build('baz') @@ -86,7 +86,7 @@ describe('related events query', () => { }, }, }, - search_after: [timestamp.getTime(), 'bar'], + search_after: [timestamp, 'bar'], size: 1, sort: [{ '@timestamp': 'asc' }, { 'event.id': 'asc' }], }, diff --git a/x-pack/plugins/endpoint/server/routes/resolver/utils/pagination.ts b/x-pack/plugins/endpoint/server/routes/resolver/utils/pagination.ts index 33eb6984793087..5a64f3ff9ddb61 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/utils/pagination.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/utils/pagination.ts @@ -11,12 +11,12 @@ import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/public export interface PaginationParams { size: number; - timestamp?: Date; + timestamp?: number; eventID?: string; } interface PaginationCursor { - timestamp: Date; + timestamp: number; eventID: string; } @@ -35,7 +35,7 @@ function urlDecodeCursor(value: string): PaginationCursor { const { timestamp, eventID } = JSON.parse(data); // take some extra care to only grab the things we want // convert the timestamp string to date object - return { timestamp: new Date(timestamp), eventID }; + return { timestamp, eventID }; } export function getPaginationParams(limit: number, after?: string): PaginationParams { @@ -62,7 +62,7 @@ export function paginate(pagination: PaginationParams, field: string, query: Jso query.aggs = { total: { value_count: { field } } }; query.size = size; if (timestamp && eventID) { - query.search_after = [timestamp.getTime(), eventID] as Array; + query.search_after = [timestamp, eventID] as Array; } return query; } diff --git a/x-pack/plugins/remote_clusters/kibana.json b/x-pack/plugins/remote_clusters/kibana.json index 27ae6802966dd3..609d0f67f2c7b6 100644 --- a/x-pack/plugins/remote_clusters/kibana.json +++ b/x-pack/plugins/remote_clusters/kibana.json @@ -7,7 +7,8 @@ ], "requiredPlugins": [ "licensing", - "management" + "management", + "indexManagement" ], "optionalPlugins": [ "usageCollection" diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/error.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/error.ts new file mode 100644 index 00000000000000..b7bc197fbd162b --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/error.ts @@ -0,0 +1,35 @@ +/* + * 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 { + AccessForbidden, + IndexNotFound, + CannotCreateIndex, + ReindexTaskCannotBeDeleted, + ReindexTaskFailed, + ReindexAlreadyInProgress, + MultipleReindexJobsFound, +} from './error_symbols'; + +export class ReindexError extends Error { + constructor(message: string, public readonly symbol: symbol) { + super(message); + } +} + +export const createErrorFactory = (symbol: symbol) => (message: string) => { + return new ReindexError(message, symbol); +}; + +export const error = { + indexNotFound: createErrorFactory(IndexNotFound), + accessForbidden: createErrorFactory(AccessForbidden), + cannotCreateIndex: createErrorFactory(CannotCreateIndex), + reindexTaskFailed: createErrorFactory(ReindexTaskFailed), + reindexTaskCannotBeDeleted: createErrorFactory(ReindexTaskCannotBeDeleted), + reindexAlreadyInProgress: createErrorFactory(ReindexAlreadyInProgress), + multipleReindexJobsFound: createErrorFactory(MultipleReindexJobsFound), +}; diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/error_symbols.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/error_symbols.ts new file mode 100644 index 00000000000000..9e49d280d1be22 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/error_symbols.ts @@ -0,0 +1,15 @@ +/* + * 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. + */ + +export const AccessForbidden = Symbol('AccessForbidden'); +export const IndexNotFound = Symbol('IndexNotFound'); +export const CannotCreateIndex = Symbol('CannotCreateIndex'); + +export const ReindexTaskFailed = Symbol('ReindexTaskFailed'); +export const ReindexTaskCannotBeDeleted = Symbol('ReindexTaskCannotBeDeleted'); +export const ReindexAlreadyInProgress = Symbol('ReindexAlreadyInProgress'); + +export const MultipleReindexJobsFound = Symbol('MultipleReindexJobsFound'); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts index 8f1df5b34372b3..b274743bdf2791 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts @@ -3,8 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import Boom from 'boom'; import { APICaller, Logger } from 'src/core/server'; import { first } from 'rxjs/operators'; @@ -24,6 +22,8 @@ import { import { ReindexActions } from './reindex_actions'; import { LicensingPluginSetup } from '../../../../licensing/server'; +import { error } from './error'; + const VERSION_REGEX = new RegExp(/^([1-9]+)\.([0-9]+)\.([0-9]+)/); const ML_INDICES = ['.ml-state', '.ml-anomalies', '.ml-config']; const WATCHER_INDICES = ['.watches', '.triggered-watches']; @@ -284,7 +284,7 @@ export const reindexServiceFactory = ( const flatSettings = await actions.getFlatSettings(indexName); if (!flatSettings) { - throw Boom.notFound(`Index ${indexName} does not exist.`); + throw error.indexNotFound(`Index ${indexName} does not exist.`); } const { settings, mappings } = transformFlatSettings(flatSettings); @@ -298,7 +298,7 @@ export const reindexServiceFactory = ( }); if (!createIndex.acknowledged) { - throw Boom.badImplementation(`Index could not be created: ${newIndexName}`); + throw error.cannotCreateIndex(`Index could not be created: ${newIndexName}`); } return actions.updateReindexOp(reindexOp, { @@ -363,7 +363,7 @@ export const reindexServiceFactory = ( if (taskResponse.task.status.created < count) { // Include the entire task result in the error message. This should be guaranteed // to be JSON-serializable since it just came back from Elasticsearch. - throw Boom.badData(`Reindexing failed: ${JSON.stringify(taskResponse)}`); + throw error.reindexTaskFailed(`Reindexing failed: ${JSON.stringify(taskResponse)}`); } // Update the status @@ -380,7 +380,7 @@ export const reindexServiceFactory = ( }); if (deleteTaskResp.result !== 'deleted') { - throw Boom.badImplementation(`Could not delete reindexing task ${taskId}`); + throw error.reindexTaskCannotBeDeleted(`Could not delete reindexing task ${taskId}`); } return reindexOp; @@ -414,7 +414,7 @@ export const reindexServiceFactory = ( }); if (!aliasResponse.acknowledged) { - throw Boom.badImplementation(`Index aliases could not be created.`); + throw error.cannotCreateIndex(`Index aliases could not be created.`); } return actions.updateReindexOp(reindexOp, { @@ -520,7 +520,7 @@ export const reindexServiceFactory = ( async createReindexOperation(indexName: string) { const indexExists = await callAsUser('indices.exists', { index: indexName }); if (!indexExists) { - throw Boom.notFound(`Index ${indexName} does not exist in this cluster.`); + throw error.indexNotFound(`Index ${indexName} does not exist in this cluster.`); } const existingReindexOps = await actions.findReindexOperations(indexName); @@ -533,7 +533,9 @@ export const reindexServiceFactory = ( // Delete the existing one if it failed or was cancelled to give a chance to retry. await actions.deleteReindexOp(existingOp); } else { - throw Boom.badImplementation(`A reindex operation already in-progress for ${indexName}`); + throw error.reindexAlreadyInProgress( + `A reindex operation already in-progress for ${indexName}` + ); } } @@ -547,7 +549,9 @@ export const reindexServiceFactory = ( if (findResponse.total === 0) { return null; } else if (findResponse.total > 1) { - throw Boom.badImplementation(`More than one reindex operation found for ${indexName}`); + throw error.multipleReindexJobsFound( + `More than one reindex operation found for ${indexName}` + ); } return findResponse.saved_objects[0]; diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices.ts index a910145474061f..72c2f2c29b72ea 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices.ts @@ -5,7 +5,12 @@ */ import { schema } from '@kbn/config-schema'; -import { Logger, ElasticsearchServiceSetup, SavedObjectsClient } from 'src/core/server'; +import { + Logger, + ElasticsearchServiceSetup, + SavedObjectsClient, + kibanaResponseFactory, +} from '../../../../../src/core/server'; import { ReindexStatus } from '../../common/types'; import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; import { reindexServiceFactory, ReindexWorker } from '../lib/reindexing'; @@ -13,6 +18,16 @@ import { CredentialStore } from '../lib/reindexing/credential_store'; import { reindexActionsFactory } from '../lib/reindexing/reindex_actions'; import { RouteDependencies } from '../types'; import { LicensingPluginSetup } from '../../../licensing/server'; +import { ReindexError } from '../lib/reindexing/error'; +import { + AccessForbidden, + IndexNotFound, + CannotCreateIndex, + ReindexAlreadyInProgress, + ReindexTaskCannotBeDeleted, + ReindexTaskFailed, + MultipleReindexJobsFound, +} from '../lib/reindexing/error_symbols'; interface CreateReindexWorker { logger: Logger; @@ -33,6 +48,29 @@ export function createReindexWorker({ return new ReindexWorker(savedObjects, credentialStore, adminClient, logger, licensing); } +const mapAnyErrorToKibanaHttpResponse = (e: any) => { + if (e instanceof ReindexError) { + switch (e.symbol) { + case AccessForbidden: + return kibanaResponseFactory.forbidden({ body: e.message }); + case IndexNotFound: + return kibanaResponseFactory.notFound({ body: e.message }); + case CannotCreateIndex: + case ReindexTaskCannotBeDeleted: + return kibanaResponseFactory.internalError({ body: e.message }); + case ReindexTaskFailed: + // Bad data + return kibanaResponseFactory.customError({ body: e.message, statusCode: 422 }); + case ReindexAlreadyInProgress: + case MultipleReindexJobsFound: + return kibanaResponseFactory.badRequest({ body: e.message }); + default: + // nothing matched + } + } + return kibanaResponseFactory.internalError({ body: e }); +}; + export function registerReindexIndicesRoutes( { credentialStore, router, licensing, log }: RouteDependencies, getWorker: () => ReindexWorker @@ -94,7 +132,7 @@ export function registerReindexIndicesRoutes( return response.ok({ body: reindexOp.attributes }); } catch (e) { - return response.internalError({ body: e }); + return mapAnyErrorToKibanaHttpResponse(e); } } ) @@ -150,15 +188,7 @@ export function registerReindexIndicesRoutes( }, }); } catch (e) { - if (!e.isBoom) { - return response.internalError({ body: e }); - } - return response.customError({ - body: { - message: e.message, - }, - statusCode: e.statusCode, - }); + return mapAnyErrorToKibanaHttpResponse(e); } } ) @@ -201,15 +231,7 @@ export function registerReindexIndicesRoutes( return response.ok({ body: { acknowledged: true } }); } catch (e) { - if (!e.isBoom) { - return response.internalError({ body: e }); - } - return response.customError({ - body: { - message: e.message, - }, - statusCode: e.statusCode, - }); + return mapAnyErrorToKibanaHttpResponse(e); } } ) diff --git a/x-pack/test/functional/apps/endpoint/policy_list.ts b/x-pack/test/functional/apps/endpoint/policy_list.ts index 658e4dcd13e1ed..382963bc2b0c72 100644 --- a/x-pack/test/functional/apps/endpoint/policy_list.ts +++ b/x-pack/test/functional/apps/endpoint/policy_list.ts @@ -11,10 +11,11 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); // FLAKY: https://github.com/elastic/kibana/issues/57946 - describe.skip('Endpoint Policy List', function() { + describe('Endpoint Policy List', function() { this.tags(['ciGroup7']); before(async () => { await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/policy'); + await pageObjects.endpoint.waitForTableToHaveData('policyTable'); }); it('loads the Policy List Page', async () => { @@ -26,7 +27,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows policy count total', async () => { const policyTotal = await testSubjects.getVisibleText('policyTotalCount'); - expect(policyTotal).to.equal('0 Policies'); + expect(policyTotal).to.equal('100 Policies'); }); it('includes policy list table', async () => { await testSubjects.existOrFail('policyTable'); diff --git a/x-pack/test/functional/apps/maps/discover.js b/x-pack/test/functional/apps/maps/discover.js new file mode 100644 index 00000000000000..ce335964767551 --- /dev/null +++ b/x-pack/test/functional/apps/maps/discover.js @@ -0,0 +1,50 @@ +/* + * 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 expect from '@kbn/expect'; + +export default function({ getService, getPageObjects }) { + const queryBar = getService('queryBar'); + const PageObjects = getPageObjects(['common', 'discover', 'header', 'maps', 'timePicker']); + + describe('discover visualize button', () => { + beforeEach(async () => { + await PageObjects.common.navigateToApp('discover'); + }); + + it('should link geo_shape fields to Maps application', async () => { + await PageObjects.discover.selectIndexPattern('geo_shapes*'); + await PageObjects.discover.clickFieldListItem('geometry'); + await PageObjects.discover.clickFieldListItemVisualize('geometry'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.maps.waitForLayersToLoad(); + const doesLayerExist = await PageObjects.maps.doesLayerExist('geo_shapes*'); + expect(doesLayerExist).to.equal(true); + const hits = await PageObjects.maps.getHits(); + expect(hits).to.equal('4'); + }); + + it('should link geo_point fields to Maps application with time and query context', async () => { + await PageObjects.discover.selectIndexPattern('logstash-*'); + await PageObjects.timePicker.setAbsoluteRange( + 'Sep 22, 2015 @ 00:00:00.000', + 'Sep 22, 2015 @ 04:00:00.000' + ); + await queryBar.setQuery('machine.os.raw : "ios"'); + await queryBar.submitQuery(); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await PageObjects.discover.clickFieldListItem('geo.coordinates'); + await PageObjects.discover.clickFieldListItemVisualize('geo.coordinates'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.maps.waitForLayersToLoad(); + const doesLayerExist = await PageObjects.maps.doesLayerExist('logstash-*'); + expect(doesLayerExist).to.equal(true); + const hits = await PageObjects.maps.getHits(); + expect(hits).to.equal('7'); + }); + }); +} diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/index.js index 0545fcd1b6453a..e8a9d7ba54bc55 100644 --- a/x-pack/test/functional/apps/maps/index.js +++ b/x-pack/test/functional/apps/maps/index.js @@ -45,6 +45,7 @@ export default function({ loadTestFile, getService }) { loadTestFile(require.resolve('./import_geojson')); loadTestFile(require.resolve('./layer_errors')); loadTestFile(require.resolve('./embeddable')); + loadTestFile(require.resolve('./discover')); }); }); } diff --git a/x-pack/test/functional/page_objects/endpoint_page.ts b/x-pack/test/functional/page_objects/endpoint_page.ts index 185b95b00527d5..6350f51f707f49 100644 --- a/x-pack/test/functional/page_objects/endpoint_page.ts +++ b/x-pack/test/functional/page_objects/endpoint_page.ts @@ -9,6 +9,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; export function EndpointPageProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); + const retry = getService('retry'); return { /** @@ -58,5 +59,13 @@ export function EndpointPageProvider({ getService }: FtrProviderContext) { ) ); }, + + async waitForTableToHaveData(dataTestSubj: string) { + await retry.waitForWithTimeout('table to have data', 2000, async () => { + const tableData = await this.getEndpointAppTableData(dataTestSubj); + if (tableData[1][0] === 'No items found') return false; + return true; + }); + }, }; }