From d7601cc7431e1fdf9a1d4bc39640b10c897c6f3d Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Mon, 2 Mar 2020 13:15:29 +0100 Subject: [PATCH 01/34] Retry migration operations which fail due to snapshot in progress (#58884) Co-authored-by: Elastic Machine --- .../server/elasticsearch/retry_call_cluster.test.ts | 13 +++++++++++++ src/core/server/elasticsearch/retry_call_cluster.ts | 13 +++---------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/core/server/elasticsearch/retry_call_cluster.test.ts b/src/core/server/elasticsearch/retry_call_cluster.test.ts index b5a5185ab39d9..4f391f0aba34b 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 ea3cc0b90c077..901b801159cb6 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( From 810a6b474817a19e19e49d144f17014f16221c8d Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Mon, 2 Mar 2020 13:37:20 +0100 Subject: [PATCH 02/34] merge only plain objects (#59011) --- src/core/utils/merge.test.ts | 24 ++++++++++++++++++++++++ src/core/utils/merge.ts | 4 ++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/core/utils/merge.test.ts b/src/core/utils/merge.test.ts index c857e980dec21..7ef07a83399ac 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 8e5d9f4860d95..43878c27b1e19 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, From c9ebeb7cff9a8b1f25623ee4950074f7c7bc24c5 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Mon, 2 Mar 2020 13:07:02 +0000 Subject: [PATCH 03/34] explicit ui exports from data plugin (#57764) * explicit ui exports * Fix imports in data plugin Co-authored-by: Elastic Machine --- src/plugins/data/public/index.ts | 15 ++++++++++++++- .../ui/query_string_input/query_string_input.tsx | 3 ++- .../data/public/ui/search_bar/search_bar.tsx | 4 +--- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 5dcf51ecc81eb..a5f4ce2ce3c58 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 018c2927031d0..a51362d0ba92e 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 66ad4dfb12e97..5083a1e68c6dd 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; From b5dd99c4667e4ebf452073eee922c4bd34da6cce Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Mon, 2 Mar 2020 16:50:44 +0300 Subject: [PATCH 04/34] Converted terms_other_bucket_helper to TS. Migrated tests to jest. (#58143) * Converted terms_other_bucket_helper to TS. Migrated tests to jest. * Fixed some remarks * fix PR comments * Fixed tests * Fixed types Co-authored-by: Elastic Machine Co-authored-by: Alexey Antonov --- .../_terms_other_bucket_helper.test.ts} | 218 ++++++++++++------ ...elper.js => _terms_other_bucket_helper.ts} | 135 +++++++---- .../data/public/search/aggs/buckets/terms.ts | 1 - 3 files changed, 233 insertions(+), 121 deletions(-) rename src/legacy/core_plugins/data/public/search/aggs/{__tests__/buckets/_terms_other_bucket_helper.js => buckets/_terms_other_bucket_helper.test.ts} (54%) rename src/legacy/core_plugins/data/public/search/aggs/buckets/{_terms_other_bucket_helper.js => _terms_other_bucket_helper.ts} (65%) 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 749dad377f2e2..976ab57c00b63 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 ddab360161744..42db37c81eadd 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 0ed44aa876744..8fd95c86d8476 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'; From a3be4e22220df8724d7ceb5667f4af37647042ea Mon Sep 17 00:00:00 2001 From: kqualters-elastic <56408403+kqualters-elastic@users.noreply.github.com> Date: Mon, 2 Mar 2020 09:20:21 -0500 Subject: [PATCH 05/34] [Endpoint] add resolver middleware (#58288) * Add resolver middleware * Update types to match events, use sample events in useCamera tests * add predicate to convert alertdata to legacy endpoint event * Use mock event generator in tests * Guard against events not having agent or endpoint fields Co-authored-by: Robert Austin --- x-pack/plugins/endpoint/common/types.ts | 34 +++- .../public/applications/endpoint/index.tsx | 76 ++++--- .../store/alerts/mock_alert_result_list.ts | 1 + .../endpoint/store/alerts/selectors.ts | 23 ++- .../endpoint/view/alerts/index.test.tsx | 21 +- .../endpoint/view/alerts/index.tsx | 9 +- .../endpoint/view/alerts/resolver.tsx | 35 ++++ .../resolver/models/indexed_process_tree.ts | 20 +- .../resolver/models/process_event.test.ts | 10 +- .../resolver/models/process_event.ts | 20 +- .../models/process_event_test_helpers.ts | 43 ++-- .../embeddables/resolver/store/actions.ts | 31 ++- .../data/__snapshots__/graphing.test.ts.snap | 189 +++++++++--------- .../embeddables/resolver/store/data/action.ts | 4 +- .../resolver/store/data/graphing.test.ts | 73 +++---- .../resolver/store/data/reducer.ts | 10 +- .../resolver/store/data/selectors.ts | 14 +- .../embeddables/resolver/store/index.ts | 10 +- .../embeddables/resolver/store/methods.ts | 5 +- .../embeddables/resolver/store/middleware.ts | 45 +++++ .../embeddables/resolver/store/selectors.ts | 5 + .../public/embeddables/resolver/types.ts | 16 +- .../embeddables/resolver/view/index.tsx | 77 ++++--- .../embeddables/resolver/view/panel.tsx | 17 +- .../resolver/view/process_event_dot.tsx | 7 +- .../resolver/view/use_camera.test.tsx | 49 +++-- x-pack/plugins/endpoint/public/plugin.ts | 11 +- .../routes/resolver/queries/children.test.ts | 8 +- .../resolver/queries/related_events.test.ts | 8 +- .../routes/resolver/utils/pagination.ts | 8 +- 30 files changed, 578 insertions(+), 301 deletions(-) create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index d3df972290759..6d904fda6f747 100644 --- a/x-pack/plugins/endpoint/common/types.ts +++ b/x-pack/plugins/endpoint/common/types.ts @@ -115,6 +115,10 @@ export type AlertEvent = Immutable<{ score: number; }; }; + process?: { + unique_pid: number; + pid: number; + }; host: { hostname: string; ip: string; @@ -122,10 +126,9 @@ export type AlertEvent = Immutable<{ name: string; }; }; - process: { - pid: number; - }; thread: {}; + endpoint?: {}; + endgame?: {}; }>; /** @@ -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 7ab66817a0888..296587706e6ac 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 b90f897ea2229..8eadb3e7fb3df 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 54add85f0fe04..f217e3cda9191 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 37847553d512a..fe362f21a178e 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 6f88727575557..3c229484ede4e 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 0000000000000..c7ef7f73dfe05 --- /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 0eb3505096b4a..6892bf11ecff2 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 3177671a30001..3916396f7402c 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 c8496b8e6e7a5..876168d2ed96a 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 9a6f19adcc101..e88837d325108 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 25f196c76a290..ecba0ec404d44 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 1dc17054b9f47..b88652097eb5c 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 900b9bda571da..f34d7c08ce08c 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 fac70433f14b2..f01136fe20ebf 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 848d814808bac..a3184389a794e 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 75b477dd7c7fc..304abbb06880b 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 b17572bbc4ab4..2a20c73347348 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 8808160c9c631..9f06643626f50 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 0000000000000..900aece60618d --- /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 25d08a8c347ed..708eb684ebd3e 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 6c6936d377dea..4c2a1ea5ac21f 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 d71a4d87b7eab..52a0872f269f5 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 c75b73b4bceaf..84c299698bb32 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 384fbf90ed984..034780c7ba14c 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 f4abb51f062f2..1948c6cae505b 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 355364253b2a5..0e10fe680e9f0 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 2dd2e0c2d1d5f..08a906e2884d6 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 8ef680a168310..a91c87274b8dd 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 33eb698479308..5a64f3ff9ddb6 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; } From 74d0e9297fd2efdc7943d10f2aa1d10ad4c016e9 Mon Sep 17 00:00:00 2001 From: Charlie Pichette <56399229+charlie-pichette@users.noreply.github.com> Date: Mon, 2 Mar 2020 10:46:20 -0500 Subject: [PATCH 06/34] [Endpoint] [Tests] fixes #57946 flaky endpoint policy list test (#58348) * endpoint-161-refactor-management-list-test * fix location of es archive file * issue 57946 fix flaky endpoint policy list test Co-authored-by: Elastic Machine --- x-pack/test/functional/apps/endpoint/policy_list.ts | 5 +++-- x-pack/test/functional/page_objects/endpoint_page.ts | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/x-pack/test/functional/apps/endpoint/policy_list.ts b/x-pack/test/functional/apps/endpoint/policy_list.ts index 658e4dcd13e1e..382963bc2b0c7 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/page_objects/endpoint_page.ts b/x-pack/test/functional/page_objects/endpoint_page.ts index 185b95b00527d..6350f51f707f4 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; + }); + }, }; } From 1eca34126061e7226c543f86f867d95f0f1d8ea1 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Mon, 2 Mar 2020 09:59:29 -0600 Subject: [PATCH 07/34] [DOCS] Rework of main get started page (#58260) * [DOCS] Rework of main gett started page * Redirect for add-sample-data * Link fix * Review comments Co-authored-by: Elastic Machine --- docs/redirects.asciidoc | 5 +++ docs/user/getting-started.asciidoc | 70 +++++++++++++++++------------- docs/user/index.asciidoc | 4 +- 3 files changed, 47 insertions(+), 32 deletions(-) diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 3843cc27defd5..8ad5330f3fda5 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 c6fe5b5b92d69..d426ec111351c 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 3911d57e05c9a..ff100d0763368 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[] From 323bb21df30f3a94a83fcb2e561c46c1b17e0e03 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 2 Mar 2020 11:27:15 -0500 Subject: [PATCH 08/34] [Remote clusters] Add indexManagement as required plugin (#58915) --- x-pack/plugins/remote_clusters/kibana.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/remote_clusters/kibana.json b/x-pack/plugins/remote_clusters/kibana.json index 27ae6802966dd..609d0f67f2c7b 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" From e9abe735f226cde41ec71321f4e7bbc623a93d7d Mon Sep 17 00:00:00 2001 From: Andrew Goldstein Date: Mon, 2 Mar 2020 09:36:14 -0700 Subject: [PATCH 09/34] [SIEM] Default the Timeline events filter to show All events (#58953) ## [SIEM] Default the Timeline events filter to show All events The Timeline events filter introduced in `7.6` to support the [detection engine](https://www.elastic.co/guide/en/siem/guide/current/detection-engine-overview.html) defaulted to filtering by `Raw events`, and thus required manually selecting `All events` or `Signal events` from the dropdown to view signals. The new default is `All events`, per the screenshots below: ### Before ![event-filter-before](https://user-images.githubusercontent.com/4459398/75593223-ecc61500-5a41-11ea-8d7d-8db5eccb1eb4.png) ### After ![event-filter-after](https://user-images.githubusercontent.com/4459398/75593238-f5b6e680-5a41-11ea-9e12-2fc1232f58d1.png) --- .../components/open_timeline/helpers.test.ts | 8 ++-- .../timeline/search_or_filter/pick_events.tsx | 1 + .../components/timeline/timeline.test.tsx | 43 +++++++++++++++++++ .../siem/public/store/timeline/defaults.ts | 2 +- 4 files changed, 49 insertions(+), 5 deletions(-) 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 120d644b3b33a..60ebd2578b7c0 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 76f9e6fe3673a..3117bae745286 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 bbaf2a3fb6e30..7f04bb4c4dad0 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 Date: Mon, 2 Mar 2020 12:01:00 -0500 Subject: [PATCH 10/34] removing references to visTypes uiExports (#58337) --- src/legacy/core_plugins/kibana/public/kibana.js | 2 -- .../plugins/kbn_tp_run_pipeline/public/legacy.ts | 2 -- .../plugins/kbn_tp_custom_visualizations/index.js | 2 +- x-pack/legacy/plugins/canvas/public/legacy_start.ts | 3 --- .../plugins/dashboard_mode/public/dashboard_viewer.js | 4 +--- x-pack/legacy/plugins/lens/index.ts | 1 - x-pack/legacy/plugins/lens/public/legacy.ts | 7 +++++-- x-pack/legacy/plugins/lens/public/legacy_imports.ts | 2 ++ x-pack/legacy/plugins/lens/public/plugin.tsx | 7 ++++++- .../{register_vis_type_alias.ts => vis_type_alias.ts} | 4 ++-- x-pack/legacy/plugins/maps/index.js | 2 +- 11 files changed, 18 insertions(+), 18 deletions(-) rename x-pack/legacy/plugins/lens/public/{register_vis_type_alias.ts => vis_type_alias.ts} (89%) diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index a83d1176a7197..a9f32949628e9 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/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts index 39ce2b3077c96..a7cd313038d69 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 e15da9daa3cd7..b2497a824ba2b 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 21bf5aaa6d818..d7d1a940d3b43 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 e76a204a6f27d..62cd253ff24d9 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 bb0bf9b67ee2c..5eda6c4b4ff7a 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 8023bad34de66..1cfd3e198547d 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 9dcc22ddb1bb7..88f189fe3db5a 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 634d227559835..7f96268fc2e8c 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 f71796268065b..c4e0a20110c81 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 5cd5a8731a703..8048c21fe9333 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({ From 4696736528adb98e6f3ab3f872c3e14e67ca0873 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Mon, 2 Mar 2020 13:26:12 -0500 Subject: [PATCH 11/34] Disallow duplicate percentiles (#57444) (#58299) Added an optional validation step to the number_list component to disallow duplicates, Reworked and consolidated number_list component validations into one method and enabled this option in the percentiles editor. Added unit / integration tests --- .../number_list/number_list.test.tsx | 20 +++++ .../components/number_list/number_list.tsx | 39 ++++------ .../components/number_list/utils.test.ts | 69 +++++++++-------- .../controls/components/number_list/utils.ts | 77 +++++++++++++++---- .../components/controls/percentile_ranks.tsx | 1 + .../components/controls/percentiles.test.tsx | 64 +++++++++++++++ .../components/controls/percentiles.tsx | 2 +- 7 files changed, 199 insertions(+), 73 deletions(-) create mode 100644 src/legacy/core_plugins/vis_default_editor/public/components/controls/percentiles.test.tsx 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 3faf164c365d9..f547f1dee6a39 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 8e290ceedfeac..a43c66c2e08cc 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 89fb5738db379..9cffaadfc956d 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 e0f32366fc265..c2ac63c98cbea 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 c6057b7ce2a99..fb7d8d78b28e3 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 0000000000000..020dbb351b497 --- /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 74e7957bc1944..9f1f26fe5446f 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', })} From 2998ec06fe9ded381fe637ab9940dc20a3742aea Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 2 Mar 2020 11:47:35 -0700 Subject: [PATCH 12/34] [Maps] direct Discover "visualize" to open Maps application (#58549) * [Maps] direct Discover visualize to Maps application * pass initial layers to maps app * add functional test * fix parentheses messed up by lint fix * fix i18n expression * move logic into lib * fix typescript errors * use constant for geo_point and geo_shape, more TS noise * use encode_array in an attempt to make TS happy * another round of TS changes * one more thing Co-authored-by: Elastic Machine --- .../np_ready/angular/context_state.ts | 5 +- .../components/field_chooser/field_chooser.js | 17 ++- .../lib/detail_views/string.html | 4 +- .../field_chooser/lib/visualize_url_utils.ts | 108 ++++++++++++++++++ test/functional/page_objects/discover_page.js | 6 - .../maps/public/angular/get_initial_layers.js | 8 +- .../maps/public/angular/map_controller.js | 29 ++++- x-pack/test/functional/apps/maps/discover.js | 50 ++++++++ x-pack/test/functional/apps/maps/index.js | 1 + 9 files changed, 210 insertions(+), 18 deletions(-) create mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/visualize_url_utils.ts create mode 100644 x-pack/test/functional/apps/maps/discover.js 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 8fb6140d55e31..bf185f78941de 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 a175a1aebebdf..df970ab5f2584 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 5d134911fc91b..333dc472e956d 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 0000000000000..8dbf3cd79ccb1 --- /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/test/functional/page_objects/discover_page.js b/test/functional/page_objects/discover_page.js index 5ccc5625849d2..080b8c8ee753f 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/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.js b/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.js index 45ee441716769..3cae75231d28e 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 95c8ff975b1d6..a8e9ae46a3b9a 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/test/functional/apps/maps/discover.js b/x-pack/test/functional/apps/maps/discover.js new file mode 100644 index 0000000000000..ce33596476755 --- /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 0545fcd1b6453..e8a9d7ba54bc5 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')); }); }); } From 17b3d8036914f67a6a71e24f7cc6bd42852195b7 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 2 Mar 2020 20:14:24 +0100 Subject: [PATCH 13/34] improve graph missing workspace error message (#58876) --- x-pack/legacy/plugins/graph/public/app.js | 20 ++++++++++++------- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/x-pack/legacy/plugins/graph/public/app.js b/x-pack/legacy/plugins/graph/public/app.js index 7010e1fa773ea..df968681a38e2 100644 --- a/x-pack/legacy/plugins/graph/public/app.js +++ b/x-pack/legacy/plugins/graph/public/app.js @@ -132,14 +132,20 @@ export function initGraphApp(angularModule, deps) { template: appTemplate, badge: getReadonlyBadge, resolve: { - savedWorkspace: function($route) { + savedWorkspace: function($rootScope, $route, $location) { return $route.current.params.id - ? savedWorkspaceLoader.get($route.current.params.id).catch(function() { - toastNotifications.addDanger( - i18n.translate('xpack.graph.missingWorkspaceErrorMessage', { - defaultMessage: 'Missing workspace', - }) - ); + ? savedWorkspaceLoader.get($route.current.params.id).catch(function(e) { + toastNotifications.addError(e, { + title: i18n.translate('xpack.graph.missingWorkspaceErrorMessage', { + defaultMessage: "Couldn't load graph with ID", + }), + }); + $rootScope.$eval(() => { + $location.path('/home'); + $location.replace(); + }); + // return promise that never returns to prevent the controller from loading + return new Promise(); }) : savedWorkspaceLoader.get(); }, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b99a54160bb65..21500c4db9c34 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5734,7 +5734,6 @@ "xpack.graph.listing.table.entityNamePlural": "グラフ", "xpack.graph.listing.table.titleColumnName": "タイトル", "xpack.graph.loadWorkspace.missingIndexPatternErrorMessage": "インデックスパターンが見つかりませんでした", - "xpack.graph.missingWorkspaceErrorMessage": "ワークスペースがありません", "xpack.graph.newGraphTitle": "保存されていないグラフ", "xpack.graph.noDataSourceNotificationMessageText": "データソースが見つかりませんでした。{managementIndexPatternsLink} に移動して Elasticsearch インデックスのインデックスパターンを作成してください。", "xpack.graph.noDataSourceNotificationMessageText.managementIndexPatternLinkText": "管理>インデックスパターン", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index bae8fef5ff280..c9e7ea1ec80de 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5734,7 +5734,6 @@ "xpack.graph.listing.table.entityNamePlural": "图表", "xpack.graph.listing.table.titleColumnName": "标题", "xpack.graph.loadWorkspace.missingIndexPatternErrorMessage": "未找到索引模式", - "xpack.graph.missingWorkspaceErrorMessage": "缺少工作空间", "xpack.graph.newGraphTitle": "未保存图表", "xpack.graph.noDataSourceNotificationMessageText": "未找到数据源。前往 {managementIndexPatternsLink},为您的 Elasticsearch 索引创建索引模式。", "xpack.graph.noDataSourceNotificationMessageText.managementIndexPatternLinkText": "管理 > 索引模式", From cbbc963001be1d200c97a9678d59ac0750a3b7c1 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 2 Mar 2020 20:14:37 +0100 Subject: [PATCH 14/34] show timepicker in timelion and tsvb (#58857) --- .../kibana/public/visualize/np_ready/editor/editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js index 415949f88e9d1..2137e413451d2 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js @@ -382,7 +382,7 @@ function VisualizeAppController( $scope.showQueryBarTimePicker = () => { // tsvb loads without an indexPattern initially (TODO investigate). // hide timefilter only if timeFieldName is explicitly undefined. - const hasTimeField = $scope.indexPattern ? !!$scope.indexPattern.timeFieldName : true; + const hasTimeField = vis.indexPattern ? !!vis.indexPattern.timeFieldName : true; return vis.type.options.showTimePicker && hasTimeField; }; From a6b166b69c148cbd3c10ddc96bce10a25c6c5476 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 2 Mar 2020 20:14:51 +0100 Subject: [PATCH 15/34] put params into short url instead of behind it (#58846) --- .../share/public/components/url_panel_content.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/plugins/share/public/components/url_panel_content.tsx b/src/plugins/share/public/components/url_panel_content.tsx index d0d4ce55dc1ac..2b77b6f4592a8 100644 --- a/src/plugins/share/public/components/url_panel_content.tsx +++ b/src/plugins/share/public/components/url_panel_content.tsx @@ -183,7 +183,11 @@ export class UrlPanelContent extends Component { }; private getSnapshotUrl = () => { - return this.props.shareableUrl || window.location.href; + let url = this.props.shareableUrl || window.location.href; + if (this.props.isEmbedded) { + url = this.makeUrlEmbeddable(url); + } + return url; }; private makeUrlEmbeddable = (url: string) => { @@ -200,8 +204,7 @@ export class UrlPanelContent extends Component { return; } - const embeddableUrl = this.makeUrlEmbeddable(url); - return ``; + return ``; }; private setUrl = () => { From 2cf863c27b954c593ffc298d7f4524f328d07d7a Mon Sep 17 00:00:00 2001 From: Caroline Horn <549577+cchaos@users.noreply.github.com> Date: Mon, 2 Mar 2020 14:38:43 -0500 Subject: [PATCH 16/34] [Advanced Settings] Fix a11y of unsaved indicator (#58511) * [Advanced Settings] Fix a11y of unsaved indicator - Reduced size of the indicator bar on the left - Added icons with tooltips to indicated unsaved and invalid states * Snaps * Fix mobile view of bottom bar --- .../management_app/advanced_settings.scss | 30 +- .../field/__snapshots__/field.test.tsx.snap | 432 +++++++++++++++--- .../management_app/components/field/field.tsx | 29 +- .../management_app/components/form/form.tsx | 90 ++-- 4 files changed, 468 insertions(+), 113 deletions(-) diff --git a/src/plugins/advanced_settings/public/management_app/advanced_settings.scss b/src/plugins/advanced_settings/public/management_app/advanced_settings.scss index 016edb2817da8..66ae9cca3f83b 100644 --- a/src/plugins/advanced_settings/public/management_app/advanced_settings.scss +++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.scss @@ -22,40 +22,42 @@ margin-top: $euiSize; } + .mgtAdvancedSettings__fieldTitle { + padding-left: $euiSizeS; + margin-left: -$euiSizeS; + } - padding-left: $euiSizeS; - margin-left: -$euiSizeS; - &--unsaved { + &--unsaved .mgtAdvancedSettings__fieldTitle { // Simulates a left side border without shifting content - box-shadow: -$euiSizeXS 0px $euiColorSecondary; + box-shadow: -$euiSizeXS 0px $euiColorWarning; } - &--invalid { + &--invalid .mgtAdvancedSettings__fieldTitle { // Simulates a left side border without shifting content box-shadow: -$euiSizeXS 0px $euiColorDanger; } - @include internetExplorerOnly() { - min-height: 1px; - } - &Row { - padding-left: $euiSizeS; - } @include internetExplorerOnly { + min-height: 1px; + &Row { min-height: 1px; } } } +.mgtAdvancedSettings__fieldTitleUnsavedIcon { + margin-left: $euiSizeS; +} + .mgtAdvancedSettingsForm__unsavedCount { - @include euiBreakpoint('xs', 's') { + @include euiBreakpoint('xs') { display: none; } } -.mgtAdvancedSettingsForm__unsavedCountMessage{ +.mgtAdvancedSettingsForm__unsavedCountMessage { // Simulates a left side border without shifting content - box-shadow: -$euiSizeXS 0px $euiColorSecondary; + box-shadow: -$euiSizeXS 0px $euiColorWarning; padding-left: $euiSizeS; } diff --git a/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap b/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap index 2f4d806e60244..dba1678339f24 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap +++ b/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap @@ -17,7 +17,12 @@ exports[`Field for array setting should render as read only if saving is disable fullWidth={true} title={

- Array test setting + + Array test setting + +

} @@ -84,7 +89,12 @@ exports[`Field for array setting should render as read only with help text if ov fullWidth={true} title={

- Array test setting + + Array test setting + +

} @@ -139,7 +149,11 @@ exports[`Field for array setting should render custom setting icon if it is cust fullWidth={true} title={

- Array test setting + + Array test setting + +

} > @@ -195,7 +210,12 @@ exports[`Field for array setting should render default value if there is no user fullWidth={true} title={

- Array test setting + + Array test setting + +

} @@ -240,7 +260,11 @@ exports[`Field for array setting should render unsaved value if there are unsave fullWidth={true} title={

- Array test setting + + Array test setting + +

} > @@ -330,7 +361,12 @@ exports[`Field for array setting should render user value if there is user value fullWidth={true} title={

- Array test setting + + Array test setting + +

} @@ -392,7 +428,12 @@ exports[`Field for boolean setting should render as read only if saving is disab fullWidth={true} title={

- Boolean test setting + + Boolean test setting + +

} @@ -465,7 +506,12 @@ exports[`Field for boolean setting should render as read only with help text if fullWidth={true} title={

- Boolean test setting + + Boolean test setting + +

} @@ -526,7 +572,11 @@ exports[`Field for boolean setting should render custom setting icon if it is cu fullWidth={true} title={

- Boolean test setting + + Boolean test setting + +

} > @@ -588,7 +639,12 @@ exports[`Field for boolean setting should render default value if there is no us fullWidth={true} title={

- Boolean test setting + + Boolean test setting + +

} @@ -639,7 +695,11 @@ exports[`Field for boolean setting should render unsaved value if there are unsa fullWidth={true} title={

- Boolean test setting + + Boolean test setting + +

} > @@ -731,7 +798,12 @@ exports[`Field for boolean setting should render user value if there is user val fullWidth={true} title={

- Boolean test setting + + Boolean test setting + +

} @@ -799,7 +871,12 @@ exports[`Field for image setting should render as read only if saving is disable fullWidth={true} title={

- Image test setting + + Image test setting + +

} @@ -868,7 +945,12 @@ exports[`Field for image setting should render as read only with help text if ov fullWidth={true} title={

- Image test setting + + Image test setting + +

} @@ -921,7 +1003,11 @@ exports[`Field for image setting should render custom setting icon if it is cust fullWidth={true} title={

- Image test setting + + Image test setting + +

} > @@ -979,7 +1066,12 @@ exports[`Field for image setting should render default value if there is no user fullWidth={true} title={

- Image test setting + + Image test setting + +

} @@ -1026,7 +1118,11 @@ exports[`Field for image setting should render unsaved value if there are unsave fullWidth={true} title={

- Image test setting + + Image test setting + +

} > @@ -1113,7 +1216,12 @@ exports[`Field for image setting should render user value if there is user value fullWidth={true} title={

- Image test setting + + Image test setting + +

} @@ -1211,7 +1319,12 @@ exports[`Field for json setting should render as read only if saving is disabled fullWidth={true} title={

- Json test setting + + Json test setting + +

} @@ -1302,7 +1415,12 @@ exports[`Field for json setting should render as read only with help text if ove fullWidth={true} title={

- Json test setting + + Json test setting + +

} @@ -1378,7 +1496,11 @@ exports[`Field for json setting should render custom setting icon if it is custo fullWidth={true} title={

- Json test setting + + Json test setting + +

} > @@ -1480,7 +1603,12 @@ exports[`Field for json setting should render default value if there is no user fullWidth={true} title={

- Json test setting + + Json test setting + +

} @@ -1563,7 +1691,11 @@ exports[`Field for json setting should render unsaved value if there are unsaved fullWidth={true} title={

- Json test setting + + Json test setting + +

} > @@ -1677,7 +1816,12 @@ exports[`Field for json setting should render user value if there is user value fullWidth={true} title={

- Json test setting + + Json test setting + +

} @@ -1760,7 +1904,12 @@ exports[`Field for markdown setting should render as read only if saving is disa fullWidth={true} title={

- Markdown test setting + + Markdown test setting + +

} @@ -1848,7 +1997,12 @@ exports[`Field for markdown setting should render as read only with help text if fullWidth={true} title={

- Markdown test setting + + Markdown test setting + +

} @@ -1924,7 +2078,11 @@ exports[`Field for markdown setting should render custom setting icon if it is c fullWidth={true} title={

- Markdown test setting + + Markdown test setting + +

} > @@ -2001,7 +2160,12 @@ exports[`Field for markdown setting should render default value if there is no u fullWidth={true} title={

- Markdown test setting + + Markdown test setting + +

} @@ -2067,7 +2231,11 @@ exports[`Field for markdown setting should render unsaved value if there are uns fullWidth={true} title={

- Markdown test setting + + Markdown test setting + +

} > @@ -2174,7 +2349,12 @@ exports[`Field for markdown setting should render user value if there is user va fullWidth={true} title={

- Markdown test setting + + Markdown test setting + +

} @@ -2257,7 +2437,12 @@ exports[`Field for number setting should render as read only if saving is disabl fullWidth={true} title={

- Number test setting + + Number test setting + +

} @@ -2324,7 +2509,12 @@ exports[`Field for number setting should render as read only with help text if o fullWidth={true} title={

- Number test setting + + Number test setting + +

} @@ -2379,7 +2569,11 @@ exports[`Field for number setting should render custom setting icon if it is cus fullWidth={true} title={

- Number test setting + + Number test setting + +

} > @@ -2435,7 +2630,12 @@ exports[`Field for number setting should render default value if there is no use fullWidth={true} title={

- Number test setting + + Number test setting + +

} @@ -2480,7 +2680,11 @@ exports[`Field for number setting should render unsaved value if there are unsav fullWidth={true} title={

- Number test setting + + Number test setting + +

} > @@ -2566,7 +2777,12 @@ exports[`Field for number setting should render user value if there is user valu fullWidth={true} title={

- Number test setting + + Number test setting + +

} @@ -2628,7 +2844,12 @@ exports[`Field for select setting should render as read only if saving is disabl fullWidth={true} title={

- Select test setting + + Select test setting + +

} @@ -2711,7 +2932,12 @@ exports[`Field for select setting should render as read only with help text if o fullWidth={true} title={

- Select test setting + + Select test setting + +

} @@ -2782,7 +3008,11 @@ exports[`Field for select setting should render custom setting icon if it is cus fullWidth={true} title={

- Select test setting + + Select test setting + +

} > @@ -2854,7 +3085,12 @@ exports[`Field for select setting should render default value if there is no use fullWidth={true} title={

- Select test setting + + Select test setting + +

} @@ -2915,7 +3151,11 @@ exports[`Field for select setting should render unsaved value if there are unsav fullWidth={true} title={

- Select test setting + + Select test setting + +

} > @@ -3017,7 +3264,12 @@ exports[`Field for select setting should render user value if there is user valu fullWidth={true} title={

- Select test setting + + Select test setting + +

} @@ -3095,7 +3347,12 @@ exports[`Field for string setting should render as read only if saving is disabl fullWidth={true} title={

- String test setting + + String test setting + +

} @@ -3162,7 +3419,12 @@ exports[`Field for string setting should render as read only with help text if o fullWidth={true} title={

- String test setting + + String test setting + +

} @@ -3217,7 +3479,11 @@ exports[`Field for string setting should render custom setting icon if it is cus fullWidth={true} title={

- String test setting + + String test setting + +

} > @@ -3273,7 +3540,12 @@ exports[`Field for string setting should render default value if there is no use fullWidth={true} title={

- String test setting + + String test setting + +

} @@ -3318,7 +3590,11 @@ exports[`Field for string setting should render unsaved value if there are unsav fullWidth={true} title={

- String test setting + + String test setting + +

} > @@ -3404,7 +3687,12 @@ exports[`Field for string setting should render user value if there is user valu fullWidth={true} title={

- String test setting + + String test setting + +

} @@ -3466,7 +3754,12 @@ exports[`Field for stringWithValidation setting should render as read only if sa fullWidth={true} title={

- String test validation setting + + String test validation setting + +

} @@ -3533,7 +3826,12 @@ exports[`Field for stringWithValidation setting should render as read only with fullWidth={true} title={

- String test validation setting + + String test validation setting + +

} @@ -3588,7 +3886,11 @@ exports[`Field for stringWithValidation setting should render custom setting ico fullWidth={true} title={

- String test validation setting + + String test validation setting + +

} > @@ -3644,7 +3947,12 @@ exports[`Field for stringWithValidation setting should render default value if t fullWidth={true} title={

- String test validation setting + + String test validation setting + +

} @@ -3689,7 +3997,11 @@ exports[`Field for stringWithValidation setting should render unsaved value if t fullWidth={true} title={

- String test validation setting + + String test validation setting + +

} > @@ -3775,7 +4094,12 @@ exports[`Field for stringWithValidation setting should render user value if ther fullWidth={true} title={

- String test validation setting + + String test validation setting + +

} diff --git a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx index d9c3752d1c0a5..18a1a365709d1 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx @@ -450,9 +450,24 @@ export class Field extends PureComponent { } renderTitle(setting: FieldSetting) { + const { unsavedChanges } = this.props; + const isInvalid = unsavedChanges?.isInvalid; + + const unsavedIconLabel = unsavedChanges + ? isInvalid + ? i18n.translate('advancedSettings.field.invalidIconLabel', { + defaultMessage: 'Invalid', + }) + : i18n.translate('advancedSettings.field.unsavedIconLabel', { + defaultMessage: 'Unsaved', + }) + : undefined; + return (

- {setting.displayName || setting.name} + + {setting.displayName || setting.name} + {setting.isCustom ? ( { ) : ( '' )} + + {unsavedChanges ? ( + + ) : ( + '' + )}

); } diff --git a/src/plugins/advanced_settings/public/management_app/components/form/form.tsx b/src/plugins/advanced_settings/public/management_app/components/form/form.tsx index ef433dd990d33..c859e8fdd7136 100644 --- a/src/plugins/advanced_settings/public/management_app/components/form/form.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/form/form.tsx @@ -331,54 +331,56 @@ export class Form extends PureComponent { }); return ( - +

{this.renderCountOfUnsaved()}

+ - - - - {i18n.translate('advancedSettings.form.cancelButtonLabel', { - defaultMessage: 'Cancel changes', - })} - - - - - - {i18n.translate('advancedSettings.form.saveButtonLabel', { - defaultMessage: 'Save changes', - })} - - - - + + {i18n.translate('advancedSettings.form.cancelButtonLabel', { + defaultMessage: 'Cancel changes', + })} + + + + + + {i18n.translate('advancedSettings.form.saveButtonLabel', { + defaultMessage: 'Save changes', + })} + +
From c90cfe208d790c3816e9d4d8cdc21307882e76ed Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Mon, 2 Mar 2020 15:04:27 -0500 Subject: [PATCH 17/34] [CI] Pipeline refactoring (#56447) --- .ci/Jenkinsfile_coverage | 170 ++++++++++----------- .ci/Jenkinsfile_flaky | 81 +++++----- .ci/es-snapshots/Jenkinsfile_build_es | 2 +- .ci/es-snapshots/Jenkinsfile_verify_es | 79 +++++----- Jenkinsfile | 108 ++++++------- packages/kbn-test/src/junit_report_path.ts | 4 +- test/scripts/jenkins_test_setup_oss.sh | 2 +- test/scripts/jenkins_test_setup_xpack.sh | 2 +- vars/agentInfo.groovy | 4 +- vars/catchErrors.groovy | 8 + vars/githubPr.groovy | 4 +- vars/kibanaPipeline.groovy | 164 +++++--------------- vars/retryWithDelay.groovy | 4 +- vars/retryable.groovy | 2 +- vars/workers.groovy | 147 ++++++++++++++++++ 15 files changed, 406 insertions(+), 375 deletions(-) create mode 100644 vars/catchErrors.groovy create mode 100644 vars/workers.groovy diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage index fa1e141be93ea..6b8dc31bab34e 100644 --- a/.ci/Jenkinsfile_coverage +++ b/.ci/Jenkinsfile_coverage @@ -3,99 +3,91 @@ library 'kibana-pipeline-library' kibanaLibrary.load() // load from the Jenkins instance -stage("Kibana Pipeline") { // This stage is just here to help the BlueOcean UI a little bit - timeout(time: 180, unit: 'MINUTES') { - timestamps { - ansiColor('xterm') { - catchError { +kibanaPipeline(timeoutMinutes: 180) { + catchErrors { + withEnv([ + 'CODE_COVERAGE=1', // Needed for multiple ci scripts, such as remote.ts, test/scripts/*.sh, schema.js, etc. + ]) { + parallel([ + 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), + 'x-pack-intake-agent': { withEnv([ - 'CODE_COVERAGE=1', // Needed for multiple ci scripts, such as remote.ts, test/scripts/*.sh, schema.js, etc. + 'NODE_ENV=test' // Needed for jest tests only ]) { - parallel([ - 'kibana-intake-agent': { - kibanaPipeline.intakeWorker('kibana-intake', './test/scripts/jenkins_unit.sh')() - }, - 'x-pack-intake-agent': { - withEnv([ - 'NODE_ENV=test' // Needed for jest tests only - ]) { - kibanaPipeline.intakeWorker('x-pack-intake', './test/scripts/jenkins_xpack.sh')() - } - }, - 'kibana-oss-agent': kibanaPipeline.withWorkers('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ - 'oss-ciGroup1': kibanaPipeline.getOssCiGroupWorker(1), - 'oss-ciGroup2': kibanaPipeline.getOssCiGroupWorker(2), - 'oss-ciGroup3': kibanaPipeline.getOssCiGroupWorker(3), - 'oss-ciGroup4': kibanaPipeline.getOssCiGroupWorker(4), - 'oss-ciGroup5': kibanaPipeline.getOssCiGroupWorker(5), - 'oss-ciGroup6': kibanaPipeline.getOssCiGroupWorker(6), - 'oss-ciGroup7': kibanaPipeline.getOssCiGroupWorker(7), - 'oss-ciGroup8': kibanaPipeline.getOssCiGroupWorker(8), - 'oss-ciGroup9': kibanaPipeline.getOssCiGroupWorker(9), - 'oss-ciGroup10': kibanaPipeline.getOssCiGroupWorker(10), - 'oss-ciGroup11': kibanaPipeline.getOssCiGroupWorker(11), - 'oss-ciGroup12': kibanaPipeline.getOssCiGroupWorker(12), - ]), - 'kibana-xpack-agent': kibanaPipeline.withWorkers('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ - 'xpack-ciGroup1': kibanaPipeline.getXpackCiGroupWorker(1), - 'xpack-ciGroup2': kibanaPipeline.getXpackCiGroupWorker(2), - 'xpack-ciGroup3': kibanaPipeline.getXpackCiGroupWorker(3), - 'xpack-ciGroup4': kibanaPipeline.getXpackCiGroupWorker(4), - 'xpack-ciGroup5': kibanaPipeline.getXpackCiGroupWorker(5), - 'xpack-ciGroup6': kibanaPipeline.getXpackCiGroupWorker(6), - 'xpack-ciGroup7': kibanaPipeline.getXpackCiGroupWorker(7), - 'xpack-ciGroup8': kibanaPipeline.getXpackCiGroupWorker(8), - 'xpack-ciGroup9': kibanaPipeline.getXpackCiGroupWorker(9), - 'xpack-ciGroup10': kibanaPipeline.getXpackCiGroupWorker(10), - ]), - ]) - kibanaPipeline.jobRunner('tests-l', false) { - kibanaPipeline.downloadCoverageArtifacts() - kibanaPipeline.bash( - ''' - # bootstrap from x-pack folder - source src/dev/ci_setup/setup_env.sh - cd x-pack - yarn kbn bootstrap --prefer-offline - cd .. - # extract archives - mkdir -p /tmp/extracted_coverage - echo extracting intakes - tar -xzf /tmp/downloaded_coverage/coverage/kibana-intake/kibana-coverage.tar.gz -C /tmp/extracted_coverage - tar -xzf /tmp/downloaded_coverage/coverage/x-pack-intake/kibana-coverage.tar.gz -C /tmp/extracted_coverage - echo extracting kibana-oss-tests - tar -xzf /tmp/downloaded_coverage/coverage/kibana-oss-tests/kibana-coverage.tar.gz -C /tmp/extracted_coverage - echo extracting kibana-xpack-tests - tar -xzf /tmp/downloaded_coverage/coverage/kibana-xpack-tests/kibana-coverage.tar.gz -C /tmp/extracted_coverage - # replace path in json files to have valid html report - pwd=$(pwd) - du -sh /tmp/extracted_coverage/target/kibana-coverage/ - echo replacing path in json files - for i in {1..9}; do - sed -i "s|/dev/shm/workspace/kibana|$pwd|g" /tmp/extracted_coverage/target/kibana-coverage/functional/${i}*.json & - done - wait - # merge oss & x-pack reports - echo merging coverage reports - yarn nyc report --temp-dir /tmp/extracted_coverage/target/kibana-coverage/jest --report-dir target/kibana-coverage/jest-combined --reporter=html --reporter=json-summary - yarn nyc report --temp-dir /tmp/extracted_coverage/target/kibana-coverage/functional --report-dir target/kibana-coverage/functional-combined --reporter=html --reporter=json-summary - echo copy mocha reports - mkdir -p target/kibana-coverage/mocha-combined - cp -r /tmp/extracted_coverage/target/kibana-coverage/mocha target/kibana-coverage/mocha-combined - ''', - "run `yarn kbn bootstrap && merge coverage`" - ) - sh 'tar -czf kibana-jest-coverage.tar.gz target/kibana-coverage/jest-combined/*' - kibanaPipeline.uploadCoverageArtifacts("coverage/jest-combined", 'kibana-jest-coverage.tar.gz') - sh 'tar -czf kibana-functional-coverage.tar.gz target/kibana-coverage/functional-combined/*' - kibanaPipeline.uploadCoverageArtifacts("coverage/functional-combined", 'kibana-functional-coverage.tar.gz') - sh 'tar -czf kibana-mocha-coverage.tar.gz target/kibana-coverage/mocha-combined/*' - kibanaPipeline.uploadCoverageArtifacts("coverage/mocha-combined", 'kibana-mocha-coverage.tar.gz') - } + workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh')() } - } - kibanaPipeline.sendMail() + }, + 'kibana-oss-agent': workers.functional('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ + 'oss-ciGroup1': kibanaPipeline.ossCiGroupProcess(1), + 'oss-ciGroup2': kibanaPipeline.ossCiGroupProcess(2), + 'oss-ciGroup3': kibanaPipeline.ossCiGroupProcess(3), + 'oss-ciGroup4': kibanaPipeline.ossCiGroupProcess(4), + 'oss-ciGroup5': kibanaPipeline.ossCiGroupProcess(5), + 'oss-ciGroup6': kibanaPipeline.ossCiGroupProcess(6), + 'oss-ciGroup7': kibanaPipeline.ossCiGroupProcess(7), + 'oss-ciGroup8': kibanaPipeline.ossCiGroupProcess(8), + 'oss-ciGroup9': kibanaPipeline.ossCiGroupProcess(9), + 'oss-ciGroup10': kibanaPipeline.ossCiGroupProcess(10), + 'oss-ciGroup11': kibanaPipeline.ossCiGroupProcess(11), + 'oss-ciGroup12': kibanaPipeline.ossCiGroupProcess(12), + ]), + 'kibana-xpack-agent': workers.functional('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ + 'xpack-ciGroup1': kibanaPipeline.xpackCiGroupProcess(1), + 'xpack-ciGroup2': kibanaPipeline.xpackCiGroupProcess(2), + 'xpack-ciGroup3': kibanaPipeline.xpackCiGroupProcess(3), + 'xpack-ciGroup4': kibanaPipeline.xpackCiGroupProcess(4), + 'xpack-ciGroup5': kibanaPipeline.xpackCiGroupProcess(5), + 'xpack-ciGroup6': kibanaPipeline.xpackCiGroupProcess(6), + 'xpack-ciGroup7': kibanaPipeline.xpackCiGroupProcess(7), + 'xpack-ciGroup8': kibanaPipeline.xpackCiGroupProcess(8), + 'xpack-ciGroup9': kibanaPipeline.xpackCiGroupProcess(9), + 'xpack-ciGroup10': kibanaPipeline.xpackCiGroupProcess(10), + ]), + ]) + workers.base(name: 'coverage-worker', label: 'tests-l', ramDisk: false, bootstrapped: false) { + kibanaPipeline.downloadCoverageArtifacts() + kibanaPipeline.bash( + ''' + # bootstrap from x-pack folder + source src/dev/ci_setup/setup_env.sh + cd x-pack + yarn kbn bootstrap --prefer-offline + cd .. + # extract archives + mkdir -p /tmp/extracted_coverage + echo extracting intakes + tar -xzf /tmp/downloaded_coverage/coverage/kibana-intake/kibana-coverage.tar.gz -C /tmp/extracted_coverage + tar -xzf /tmp/downloaded_coverage/coverage/x-pack-intake/kibana-coverage.tar.gz -C /tmp/extracted_coverage + echo extracting kibana-oss-tests + tar -xzf /tmp/downloaded_coverage/coverage/kibana-oss-tests/kibana-coverage.tar.gz -C /tmp/extracted_coverage + echo extracting kibana-xpack-tests + tar -xzf /tmp/downloaded_coverage/coverage/kibana-xpack-tests/kibana-coverage.tar.gz -C /tmp/extracted_coverage + # replace path in json files to have valid html report + pwd=$(pwd) + du -sh /tmp/extracted_coverage/target/kibana-coverage/ + echo replacing path in json files + for i in {1..9}; do + sed -i "s|/dev/shm/workspace/kibana|$pwd|g" /tmp/extracted_coverage/target/kibana-coverage/functional/${i}*.json & + done + wait + # merge oss & x-pack reports + echo merging coverage reports + yarn nyc report --temp-dir /tmp/extracted_coverage/target/kibana-coverage/jest --report-dir target/kibana-coverage/jest-combined --reporter=html --reporter=json-summary + yarn nyc report --temp-dir /tmp/extracted_coverage/target/kibana-coverage/functional --report-dir target/kibana-coverage/functional-combined --reporter=html --reporter=json-summary + echo copy mocha reports + mkdir -p target/kibana-coverage/mocha-combined + cp -r /tmp/extracted_coverage/target/kibana-coverage/mocha target/kibana-coverage/mocha-combined + ''', + "run `yarn kbn bootstrap && merge coverage`" + ) + sh 'tar -czf kibana-jest-coverage.tar.gz target/kibana-coverage/jest-combined/*' + kibanaPipeline.uploadCoverageArtifacts("coverage/jest-combined", 'kibana-jest-coverage.tar.gz') + sh 'tar -czf kibana-functional-coverage.tar.gz target/kibana-coverage/functional-combined/*' + kibanaPipeline.uploadCoverageArtifacts("coverage/functional-combined", 'kibana-functional-coverage.tar.gz') + sh 'tar -czf kibana-mocha-coverage.tar.gz target/kibana-coverage/mocha-combined/*' + kibanaPipeline.uploadCoverageArtifacts("coverage/mocha-combined", 'kibana-mocha-coverage.tar.gz') } } } + kibanaPipeline.sendMail() } diff --git a/.ci/Jenkinsfile_flaky b/.ci/Jenkinsfile_flaky index f702405aad69e..befb8d259b5b6 100644 --- a/.ci/Jenkinsfile_flaky +++ b/.ci/Jenkinsfile_flaky @@ -21,53 +21,47 @@ def workerFailures = [] currentBuild.displayName += trunc(" ${params.GITHUB_OWNER}:${params.branch_specifier}", 24) currentBuild.description = "${params.CI_GROUP}
Agents: ${AGENT_COUNT}
Executions: ${params.NUMBER_EXECUTIONS}" -stage("Kibana Pipeline") { - timeout(time: 180, unit: 'MINUTES') { - timestamps { - ansiColor('xterm') { - def agents = [:] - for(def agentNumber = 1; agentNumber <= AGENT_COUNT; agentNumber++) { - def agentNumberInside = agentNumber - def agentExecutions = floor(EXECUTIONS/AGENT_COUNT) + (agentNumber <= EXECUTIONS%AGENT_COUNT ? 1 : 0) - agents["agent-${agentNumber}"] = { - catchError { - print "Agent ${agentNumberInside} - ${agentExecutions} executions" - - kibanaPipeline.withWorkers('flaky-test-runner', { - if (NEED_BUILD) { - if (!IS_XPACK) { - kibanaPipeline.buildOss() - if (CI_GROUP == '1') { - runbld("./test/scripts/jenkins_build_kbn_tp_sample_panel_action.sh", "Build kbn tp sample panel action for ciGroup1") - } - } else { - kibanaPipeline.buildXpack() - } - } - }, getWorkerMap(agentNumberInside, agentExecutions, worker, workerFailures))() +kibanaPipeline(timeoutMinutes: 180) { + def agents = [:] + for(def agentNumber = 1; agentNumber <= AGENT_COUNT; agentNumber++) { + def agentNumberInside = agentNumber + def agentExecutions = floor(EXECUTIONS/AGENT_COUNT) + (agentNumber <= EXECUTIONS%AGENT_COUNT ? 1 : 0) + agents["agent-${agentNumber}"] = { + catchErrors { + print "Agent ${agentNumberInside} - ${agentExecutions} executions" + + workers.functional('flaky-test-runner', { + if (NEED_BUILD) { + if (!IS_XPACK) { + kibanaPipeline.buildOss() + if (CI_GROUP == '1') { + runbld("./test/scripts/jenkins_build_kbn_tp_sample_panel_action.sh", "Build kbn tp sample panel action for ciGroup1") + } + } else { + kibanaPipeline.buildXpack() } } - } + }, getWorkerMap(agentNumberInside, agentExecutions, worker, workerFailures))() + } + } + } - parallel(agents) + parallel(agents) - currentBuild.description += ", Failures: ${workerFailures.size()}" + currentBuild.description += ", Failures: ${workerFailures.size()}" - if (workerFailures.size() > 0) { - print "There were ${workerFailures.size()} test suite failures." - print "The executions that failed were:" - print workerFailures.join("\n") - print "Please check 'Test Result' and 'Pipeline Steps' pages for more info" - } - } - } + if (workerFailures.size() > 0) { + print "There were ${workerFailures.size()} test suite failures." + print "The executions that failed were:" + print workerFailures.join("\n") + print "Please check 'Test Result' and 'Pipeline Steps' pages for more info" } } def getWorkerFromParams(isXpack, job, ciGroup) { if (!isXpack) { if (job == 'serverMocha') { - return kibanaPipeline.getPostBuildWorker('serverMocha', { + return kibanaPipeline.functionalTestProcess('serverMocha', { kibanaPipeline.bash( """ source src/dev/ci_setup/setup_env.sh @@ -77,20 +71,20 @@ def getWorkerFromParams(isXpack, job, ciGroup) { ) }) } else if (job == 'firefoxSmoke') { - return kibanaPipeline.getPostBuildWorker('firefoxSmoke', { runbld('./test/scripts/jenkins_firefox_smoke.sh', 'Execute kibana-firefoxSmoke') }) + return kibanaPipeline.functionalTestProcess('firefoxSmoke', './test/scripts/jenkins_firefox_smoke.sh') } else if(job == 'visualRegression') { - return kibanaPipeline.getPostBuildWorker('visualRegression', { runbld('./test/scripts/jenkins_visual_regression.sh', 'Execute kibana-visualRegression') }) + return kibanaPipeline.functionalTestProcess('visualRegression', './test/scripts/jenkins_visual_regression.sh') } else { - return kibanaPipeline.getOssCiGroupWorker(ciGroup) + return kibanaPipeline.ossCiGroupProcess(ciGroup) } } if (job == 'firefoxSmoke') { - return kibanaPipeline.getPostBuildWorker('xpack-firefoxSmoke', { runbld('./test/scripts/jenkins_xpack_firefox_smoke.sh', 'Execute xpack-firefoxSmoke') }) + return kibanaPipeline.functionalTestProcess('xpack-firefoxSmoke', './test/scripts/jenkins_xpack_firefox_smoke.sh') } else if(job == 'visualRegression') { - return kibanaPipeline.getPostBuildWorker('xpack-visualRegression', { runbld('./test/scripts/jenkins_xpack_visual_regression.sh', 'Execute xpack-visualRegression') }) + return kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh') } else { - return kibanaPipeline.getXpackCiGroupWorker(ciGroup) + return kibanaPipeline.xpackCiGroupProcess(ciGroup) } } @@ -105,10 +99,9 @@ def getWorkerMap(agentNumber, numberOfExecutions, worker, workerFailures, maxWor for(def j = 0; j < workerExecutions; j++) { print "Execute agent-${agentNumber} worker-${workerNumber}: ${j}" withEnv([ - "JOB=agent-${agentNumber}-worker-${workerNumber}-${j}", "REMOVE_KIBANA_INSTALL_DIR=1", ]) { - catchError { + catchErrors { try { worker(workerNumber) } catch (ex) { diff --git a/.ci/es-snapshots/Jenkinsfile_build_es b/.ci/es-snapshots/Jenkinsfile_build_es index ad0ad54275e12..a00bcb3bbc946 100644 --- a/.ci/es-snapshots/Jenkinsfile_build_es +++ b/.ci/es-snapshots/Jenkinsfile_build_es @@ -26,7 +26,7 @@ timeout(time: 120, unit: 'MINUTES') { timestamps { ansiColor('xterm') { node('linux && immutable') { - catchError { + catchErrors { def VERSION def SNAPSHOT_ID def DESTINATION diff --git a/.ci/es-snapshots/Jenkinsfile_verify_es b/.ci/es-snapshots/Jenkinsfile_verify_es index 30d52a56547bd..ce472a404c053 100644 --- a/.ci/es-snapshots/Jenkinsfile_verify_es +++ b/.ci/es-snapshots/Jenkinsfile_verify_es @@ -19,50 +19,45 @@ currentBuild.description = "ES: ${SNAPSHOT_VERSION}
Kibana: ${params.branch def SNAPSHOT_MANIFEST = "https://storage.googleapis.com/kibana-ci-es-snapshots-daily/${SNAPSHOT_VERSION}/archives/${SNAPSHOT_ID}/manifest.json" -timeout(time: 120, unit: 'MINUTES') { - timestamps { - ansiColor('xterm') { - catchError { - withEnv(["ES_SNAPSHOT_MANIFEST=${SNAPSHOT_MANIFEST}"]) { - parallel([ - // TODO we just need to run integration tests from intake? - 'kibana-intake-agent': kibanaPipeline.intakeWorker('kibana-intake', './test/scripts/jenkins_unit.sh'), - 'x-pack-intake-agent': kibanaPipeline.intakeWorker('x-pack-intake', './test/scripts/jenkins_xpack.sh'), - 'kibana-oss-agent': kibanaPipeline.withWorkers('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ - 'oss-ciGroup1': kibanaPipeline.getOssCiGroupWorker(1), - 'oss-ciGroup2': kibanaPipeline.getOssCiGroupWorker(2), - 'oss-ciGroup3': kibanaPipeline.getOssCiGroupWorker(3), - 'oss-ciGroup4': kibanaPipeline.getOssCiGroupWorker(4), - 'oss-ciGroup5': kibanaPipeline.getOssCiGroupWorker(5), - 'oss-ciGroup6': kibanaPipeline.getOssCiGroupWorker(6), - 'oss-ciGroup7': kibanaPipeline.getOssCiGroupWorker(7), - 'oss-ciGroup8': kibanaPipeline.getOssCiGroupWorker(8), - 'oss-ciGroup9': kibanaPipeline.getOssCiGroupWorker(9), - 'oss-ciGroup10': kibanaPipeline.getOssCiGroupWorker(10), - 'oss-ciGroup11': kibanaPipeline.getOssCiGroupWorker(11), - 'oss-ciGroup12': kibanaPipeline.getOssCiGroupWorker(12), - ]), - 'kibana-xpack-agent': kibanaPipeline.withWorkers('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ - 'xpack-ciGroup1': kibanaPipeline.getXpackCiGroupWorker(1), - 'xpack-ciGroup2': kibanaPipeline.getXpackCiGroupWorker(2), - 'xpack-ciGroup3': kibanaPipeline.getXpackCiGroupWorker(3), - 'xpack-ciGroup4': kibanaPipeline.getXpackCiGroupWorker(4), - 'xpack-ciGroup5': kibanaPipeline.getXpackCiGroupWorker(5), - 'xpack-ciGroup6': kibanaPipeline.getXpackCiGroupWorker(6), - 'xpack-ciGroup7': kibanaPipeline.getXpackCiGroupWorker(7), - 'xpack-ciGroup8': kibanaPipeline.getXpackCiGroupWorker(8), - 'xpack-ciGroup9': kibanaPipeline.getXpackCiGroupWorker(9), - 'xpack-ciGroup10': kibanaPipeline.getXpackCiGroupWorker(10), - ]), - ]) - } - - promoteSnapshot(SNAPSHOT_VERSION, SNAPSHOT_ID) - } - - kibanaPipeline.sendMail() +kibanaPipeline(timeoutMinutes: 120) { + catchErrors { + withEnv(["ES_SNAPSHOT_MANIFEST=${SNAPSHOT_MANIFEST}"]) { + parallel([ + 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), + 'x-pack-intake-agent': workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh'), + 'kibana-oss-agent': workers.functional('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ + 'oss-ciGroup1': kibanaPipeline.ossCiGroupProcess(1), + 'oss-ciGroup2': kibanaPipeline.ossCiGroupProcess(2), + 'oss-ciGroup3': kibanaPipeline.ossCiGroupProcess(3), + 'oss-ciGroup4': kibanaPipeline.ossCiGroupProcess(4), + 'oss-ciGroup5': kibanaPipeline.ossCiGroupProcess(5), + 'oss-ciGroup6': kibanaPipeline.ossCiGroupProcess(6), + 'oss-ciGroup7': kibanaPipeline.ossCiGroupProcess(7), + 'oss-ciGroup8': kibanaPipeline.ossCiGroupProcess(8), + 'oss-ciGroup9': kibanaPipeline.ossCiGroupProcess(9), + 'oss-ciGroup10': kibanaPipeline.ossCiGroupProcess(10), + 'oss-ciGroup11': kibanaPipeline.ossCiGroupProcess(11), + 'oss-ciGroup12': kibanaPipeline.ossCiGroupProcess(12), + ]), + 'kibana-xpack-agent': workers.functional('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ + 'xpack-ciGroup1': kibanaPipeline.xpackCiGroupProcess(1), + 'xpack-ciGroup2': kibanaPipeline.xpackCiGroupProcess(2), + 'xpack-ciGroup3': kibanaPipeline.xpackCiGroupProcess(3), + 'xpack-ciGroup4': kibanaPipeline.xpackCiGroupProcess(4), + 'xpack-ciGroup5': kibanaPipeline.xpackCiGroupProcess(5), + 'xpack-ciGroup6': kibanaPipeline.xpackCiGroupProcess(6), + 'xpack-ciGroup7': kibanaPipeline.xpackCiGroupProcess(7), + 'xpack-ciGroup8': kibanaPipeline.xpackCiGroupProcess(8), + 'xpack-ciGroup9': kibanaPipeline.xpackCiGroupProcess(9), + 'xpack-ciGroup10': kibanaPipeline.xpackCiGroupProcess(10), + ]), + ]) } + + promoteSnapshot(SNAPSHOT_VERSION, SNAPSHOT_ID) } + + kibanaPipeline.sendMail() } def promoteSnapshot(snapshotVersion, snapshotId) { diff --git a/Jenkinsfile b/Jenkinsfile index 1b4350d5b91e9..85502369b07be 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -3,71 +3,49 @@ library 'kibana-pipeline-library' kibanaLibrary.load() -stage("Kibana Pipeline") { // This stage is just here to help the BlueOcean UI a little bit - timeout(time: 135, unit: 'MINUTES') { - timestamps { - ansiColor('xterm') { - githubPr.withDefaultPrComments { - catchError { - retryable.enable() - parallel([ - 'kibana-intake-agent': kibanaPipeline.intakeWorker('kibana-intake', './test/scripts/jenkins_unit.sh'), - 'x-pack-intake-agent': kibanaPipeline.intakeWorker('x-pack-intake', './test/scripts/jenkins_xpack.sh'), - 'kibana-oss-agent': kibanaPipeline.withWorkers('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ - // 'oss-firefoxSmoke': kibanaPipeline.getPostBuildWorker('firefoxSmoke', { - // retryable('kibana-firefoxSmoke') { - // runbld('./test/scripts/jenkins_firefox_smoke.sh', 'Execute kibana-firefoxSmoke') - // } - // }), - 'oss-ciGroup1': kibanaPipeline.getOssCiGroupWorker(1), - 'oss-ciGroup2': kibanaPipeline.getOssCiGroupWorker(2), - 'oss-ciGroup3': kibanaPipeline.getOssCiGroupWorker(3), - 'oss-ciGroup4': kibanaPipeline.getOssCiGroupWorker(4), - 'oss-ciGroup5': kibanaPipeline.getOssCiGroupWorker(5), - 'oss-ciGroup6': kibanaPipeline.getOssCiGroupWorker(6), - 'oss-ciGroup7': kibanaPipeline.getOssCiGroupWorker(7), - 'oss-ciGroup8': kibanaPipeline.getOssCiGroupWorker(8), - 'oss-ciGroup9': kibanaPipeline.getOssCiGroupWorker(9), - 'oss-ciGroup10': kibanaPipeline.getOssCiGroupWorker(10), - 'oss-ciGroup11': kibanaPipeline.getOssCiGroupWorker(11), - 'oss-ciGroup12': kibanaPipeline.getOssCiGroupWorker(12), - 'oss-accessibility': kibanaPipeline.getPostBuildWorker('accessibility', { - retryable('kibana-accessibility') { - runbld('./test/scripts/jenkins_accessibility.sh', 'Execute kibana-accessibility') - } - }), - // 'oss-visualRegression': kibanaPipeline.getPostBuildWorker('visualRegression', { runbld('./test/scripts/jenkins_visual_regression.sh', 'Execute kibana-visualRegression') }), - ]), - 'kibana-xpack-agent': kibanaPipeline.withWorkers('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ - // 'xpack-firefoxSmoke': kibanaPipeline.getPostBuildWorker('xpack-firefoxSmoke', { - // retryable('xpack-firefoxSmoke') { - // runbld('./test/scripts/jenkins_xpack_firefox_smoke.sh', 'Execute xpack-firefoxSmoke') - // } - // }), - 'xpack-ciGroup1': kibanaPipeline.getXpackCiGroupWorker(1), - 'xpack-ciGroup2': kibanaPipeline.getXpackCiGroupWorker(2), - 'xpack-ciGroup3': kibanaPipeline.getXpackCiGroupWorker(3), - 'xpack-ciGroup4': kibanaPipeline.getXpackCiGroupWorker(4), - 'xpack-ciGroup5': kibanaPipeline.getXpackCiGroupWorker(5), - 'xpack-ciGroup6': kibanaPipeline.getXpackCiGroupWorker(6), - 'xpack-ciGroup7': kibanaPipeline.getXpackCiGroupWorker(7), - 'xpack-ciGroup8': kibanaPipeline.getXpackCiGroupWorker(8), - 'xpack-ciGroup9': kibanaPipeline.getXpackCiGroupWorker(9), - 'xpack-ciGroup10': kibanaPipeline.getXpackCiGroupWorker(10), - 'xpack-accessibility': kibanaPipeline.getPostBuildWorker('xpack-accessibility', { - retryable('xpack-accessibility') { - runbld('./test/scripts/jenkins_xpack_accessibility.sh', 'Execute xpack-accessibility') - } - }), - // 'xpack-visualRegression': kibanaPipeline.getPostBuildWorker('xpack-visualRegression', { runbld('./test/scripts/jenkins_xpack_visual_regression.sh', 'Execute xpack-visualRegression') }), - ]), - ]) - } - } - - retryable.printFlakyFailures() - kibanaPipeline.sendMail() - } +kibanaPipeline(timeoutMinutes: 135) { + githubPr.withDefaultPrComments { + catchError { + retryable.enable() + parallel([ + 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), + 'x-pack-intake-agent': workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh'), + 'kibana-oss-agent': workers.functional('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ + // 'oss-firefoxSmoke': kibanaPipeline.functionalTestProcess('kibana-firefoxSmoke', './test/scripts/jenkins_firefox_smoke.sh'), + 'oss-ciGroup1': kibanaPipeline.ossCiGroupProcess(1), + 'oss-ciGroup2': kibanaPipeline.ossCiGroupProcess(2), + 'oss-ciGroup3': kibanaPipeline.ossCiGroupProcess(3), + 'oss-ciGroup4': kibanaPipeline.ossCiGroupProcess(4), + 'oss-ciGroup5': kibanaPipeline.ossCiGroupProcess(5), + 'oss-ciGroup6': kibanaPipeline.ossCiGroupProcess(6), + 'oss-ciGroup7': kibanaPipeline.ossCiGroupProcess(7), + 'oss-ciGroup8': kibanaPipeline.ossCiGroupProcess(8), + 'oss-ciGroup9': kibanaPipeline.ossCiGroupProcess(9), + 'oss-ciGroup10': kibanaPipeline.ossCiGroupProcess(10), + 'oss-ciGroup11': kibanaPipeline.ossCiGroupProcess(11), + 'oss-ciGroup12': kibanaPipeline.ossCiGroupProcess(12), + 'oss-accessibility': kibanaPipeline.functionalTestProcess('kibana-accessibility', './test/scripts/jenkins_accessibility.sh'), + // 'oss-visualRegression': kibanaPipeline.functionalTestProcess('visualRegression', './test/scripts/jenkins_visual_regression.sh'), + ]), + 'kibana-xpack-agent': workers.functional('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ + // 'xpack-firefoxSmoke': kibanaPipeline.functionalTestProcess('xpack-firefoxSmoke', './test/scripts/jenkins_xpack_firefox_smoke.sh'), + 'xpack-ciGroup1': kibanaPipeline.xpackCiGroupProcess(1), + 'xpack-ciGroup2': kibanaPipeline.xpackCiGroupProcess(2), + 'xpack-ciGroup3': kibanaPipeline.xpackCiGroupProcess(3), + 'xpack-ciGroup4': kibanaPipeline.xpackCiGroupProcess(4), + 'xpack-ciGroup5': kibanaPipeline.xpackCiGroupProcess(5), + 'xpack-ciGroup6': kibanaPipeline.xpackCiGroupProcess(6), + 'xpack-ciGroup7': kibanaPipeline.xpackCiGroupProcess(7), + 'xpack-ciGroup8': kibanaPipeline.xpackCiGroupProcess(8), + 'xpack-ciGroup9': kibanaPipeline.xpackCiGroupProcess(9), + 'xpack-ciGroup10': kibanaPipeline.xpackCiGroupProcess(10), + 'xpack-accessibility': kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh'), + // 'xpack-visualRegression': kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh'), + ]), + ]) } } + + retryable.printFlakyFailures() + kibanaPipeline.sendMail() } diff --git a/packages/kbn-test/src/junit_report_path.ts b/packages/kbn-test/src/junit_report_path.ts index 11eaf3d2b14a5..d46c9455dcff0 100644 --- a/packages/kbn-test/src/junit_report_path.ts +++ b/packages/kbn-test/src/junit_report_path.ts @@ -20,7 +20,9 @@ import { resolve } from 'path'; const job = process.env.JOB ? `job-${process.env.JOB}-` : ''; -const num = process.env.CI_WORKER_NUMBER ? `worker-${process.env.CI_WORKER_NUMBER}-` : ''; +const num = process.env.CI_PARALLEL_PROCESS_NUMBER + ? `worker-${process.env.CI_PARALLEL_PROCESS_NUMBER}-` + : ''; export function makeJunitReportPath(rootDirectory: string, reportName: string) { return resolve( diff --git a/test/scripts/jenkins_test_setup_oss.sh b/test/scripts/jenkins_test_setup_oss.sh index 9e68272053221..7bbb867526384 100644 --- a/test/scripts/jenkins_test_setup_oss.sh +++ b/test/scripts/jenkins_test_setup_oss.sh @@ -4,7 +4,7 @@ source test/scripts/jenkins_test_setup.sh if [[ -z "$CODE_COVERAGE" ]] ; then installDir="$(realpath $PARENT_DIR/kibana/build/oss/kibana-*-SNAPSHOT-linux-x86_64)" - destDir=${installDir}-${CI_WORKER_NUMBER} + destDir=${installDir}-${CI_PARALLEL_PROCESS_NUMBER} cp -R "$installDir" "$destDir" export KIBANA_INSTALL_DIR="$destDir" diff --git a/test/scripts/jenkins_test_setup_xpack.sh b/test/scripts/jenkins_test_setup_xpack.sh index 76fc7cfe6c876..a72e9749ebbd5 100644 --- a/test/scripts/jenkins_test_setup_xpack.sh +++ b/test/scripts/jenkins_test_setup_xpack.sh @@ -4,7 +4,7 @@ source test/scripts/jenkins_test_setup.sh if [[ -z "$CODE_COVERAGE" ]]; then installDir="$PARENT_DIR/install/kibana" - destDir="${installDir}-${CI_WORKER_NUMBER}" + destDir="${installDir}-${CI_PARALLEL_PROCESS_NUMBER}" cp -R "$installDir" "$destDir" export KIBANA_INSTALL_DIR="$destDir" diff --git a/vars/agentInfo.groovy b/vars/agentInfo.groovy index b53ed23f81ff0..166a86c169261 100644 --- a/vars/agentInfo.groovy +++ b/vars/agentInfo.groovy @@ -1,5 +1,5 @@ def print() { - try { + catchError(catchInterruptions: false, buildResult: null) { def startTime = sh(script: "date -d '-3 minutes' -Iseconds | sed s/+/%2B/", returnStdout: true).trim() def endTime = sh(script: "date -d '+1 hour 30 minutes' -Iseconds | sed s/+/%2B/", returnStdout: true).trim() @@ -34,8 +34,6 @@ def print() { echo 'SSH Command:' echo "ssh -F ssh_config \$(hostname --ip-address)" """, label: "Worker/Agent/Node debug links" - } catch(ex) { - print ex.toString() } } diff --git a/vars/catchErrors.groovy b/vars/catchErrors.groovy new file mode 100644 index 0000000000000..460a90b8ec0c0 --- /dev/null +++ b/vars/catchErrors.groovy @@ -0,0 +1,8 @@ +// Basically, this is a shortcut for catchError(catchInterruptions: false) {} +// By default, catchError will swallow aborts/timeouts, which we almost never want +def call(Map params = [:], Closure closure) { + params.catchInterruptions = false + return catchError(params, closure) +} + +return this diff --git a/vars/githubPr.groovy b/vars/githubPr.groovy index 91a4a76894d94..7759edbbf5bfc 100644 --- a/vars/githubPr.groovy +++ b/vars/githubPr.groovy @@ -14,8 +14,8 @@ So, there is only ever one build status comment on a PR at any given time, the most recent one. */ def withDefaultPrComments(closure) { - catchError { - catchError { + catchErrors { + catchErrors { closure() } diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index dd2e626d1c860..2b9b0eba38f46 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -1,92 +1,36 @@ -def withWorkers(machineName, preWorkerClosure = {}, workerClosures = [:]) { - return { - jobRunner('tests-xl', true) { - withGcsArtifactUpload(machineName, { - withPostBuildReporting { - doSetup() - preWorkerClosure() - - def nextWorker = 1 - def worker = { workerClosure -> - def workerNumber = nextWorker - nextWorker++ - - return { - // This delay helps smooth out CPU load caused by ES/Kibana instances starting up at the same time - def delay = (workerNumber-1)*20 - sleep(delay) - - workerClosure(workerNumber) - } - } - - def workers = [:] - workerClosures.each { workerName, workerClosure -> - workers[workerName] = worker(workerClosure) - } - - parallel(workers) - } - }) - } - } -} - -def withWorker(machineName, label, Closure closure) { - return { - jobRunner(label, false) { - withGcsArtifactUpload(machineName) { - withPostBuildReporting { - doSetup() - closure() - } - } - } - } -} - -def intakeWorker(jobName, String script) { - return withWorker(jobName, 'linux && immutable') { - withEnv([ - "JOB=${jobName}", - ]) { - runbld(script, "Execute ${jobName}") - } - } -} - def withPostBuildReporting(Closure closure) { try { closure() } finally { - catchError { + catchErrors { runErrorReporter() } - catchError { + catchErrors { runbld.junit() } - catchError { + catchErrors { publishJunit() } } } -def getPostBuildWorker(name, closure) { - return { workerNumber -> - def kibanaPort = "61${workerNumber}1" - def esPort = "61${workerNumber}2" - def esTransportPort = "61${workerNumber}3" +def functionalTestProcess(String name, Closure closure) { + return { processNumber -> + def kibanaPort = "61${processNumber}1" + def esPort = "61${processNumber}2" + def esTransportPort = "61${processNumber}3" withEnv([ - "CI_WORKER_NUMBER=${workerNumber}", + "CI_PARALLEL_PROCESS_NUMBER=${processNumber}", "TEST_KIBANA_HOST=localhost", "TEST_KIBANA_PORT=${kibanaPort}", "TEST_KIBANA_URL=http://elastic:changeme@localhost:${kibanaPort}", "TEST_ES_URL=http://elastic:changeme@localhost:${esPort}", "TEST_ES_TRANSPORT_PORT=${esTransportPort}", "IS_PIPELINE_JOB=1", + "JOB=${name}", "KBN_NP_PLUGINS_BUILT=true", ]) { closure() @@ -94,8 +38,16 @@ def getPostBuildWorker(name, closure) { } } -def getOssCiGroupWorker(ciGroup) { - return getPostBuildWorker("ciGroup" + ciGroup, { +def functionalTestProcess(String name, String script) { + return functionalTestProcess(name) { + retryable(name) { + runbld(script, "Execute ${name}") + } + } +} + +def ossCiGroupProcess(ciGroup) { + return functionalTestProcess("ciGroup" + ciGroup) { withEnv([ "CI_GROUP=${ciGroup}", "JOB=kibana-ciGroup${ciGroup}", @@ -104,11 +56,11 @@ def getOssCiGroupWorker(ciGroup) { runbld("./test/scripts/jenkins_ci_group.sh", "Execute kibana-ciGroup${ciGroup}") } } - }) + } } -def getXpackCiGroupWorker(ciGroup) { - return getPostBuildWorker("xpack-ciGroup" + ciGroup, { +def xpackCiGroupProcess(ciGroup) { + return functionalTestProcess("xpack-ciGroup" + ciGroup) { withEnv([ "CI_GROUP=${ciGroup}", "JOB=xpack-kibana-ciGroup${ciGroup}", @@ -117,56 +69,6 @@ def getXpackCiGroupWorker(ciGroup) { runbld("./test/scripts/jenkins_xpack_ci_group.sh", "Execute xpack-kibana-ciGroup${ciGroup}") } } - }) -} - -def jobRunner(label, useRamDisk, closure) { - node(label) { - agentInfo.print() - - if (useRamDisk) { - // Move to a temporary workspace, so that we can symlink the real workspace into /dev/shm - def originalWorkspace = env.WORKSPACE - ws('/tmp/workspace') { - sh( - script: """ - mkdir -p /dev/shm/workspace - mkdir -p '${originalWorkspace}' # create all of the directories leading up to the workspace, if they don't exist - rm --preserve-root -rf '${originalWorkspace}' # then remove just the workspace, just in case there's stuff in it - ln -s /dev/shm/workspace '${originalWorkspace}' - """, - label: "Move workspace to RAM - /dev/shm/workspace" - ) - } - } - - def scmVars - - // Try to clone from Github up to 8 times, waiting 15 secs between attempts - retryWithDelay(8, 15) { - scmVars = checkout scm - } - - withEnv([ - "CI=true", - "HOME=${env.JENKINS_HOME}", - "PR_SOURCE_BRANCH=${env.ghprbSourceBranch ?: ''}", - "PR_TARGET_BRANCH=${env.ghprbTargetBranch ?: ''}", - "PR_AUTHOR=${env.ghprbPullAuthorLogin ?: ''}", - "TEST_BROWSER_HEADLESS=1", - "GIT_BRANCH=${scmVars.GIT_BRANCH}", - ]) { - withCredentials([ - string(credentialsId: 'vault-addr', variable: 'VAULT_ADDR'), - string(credentialsId: 'vault-role-id', variable: 'VAULT_ROLE_ID'), - string(credentialsId: 'vault-secret-id', variable: 'VAULT_SECRET_ID'), - ]) { - // scm is configured to check out to the ./kibana directory - dir('kibana') { - closure() - } - } - } } } @@ -210,7 +112,7 @@ def withGcsArtifactUpload(workerName, closure) { try { closure() } finally { - catchError { + catchErrors { ARTIFACT_PATTERNS.each { pattern -> uploadGcsArtifact(uploadPrefix, pattern) } @@ -243,7 +145,7 @@ def sendMail() { } def sendInfraMail() { - catchError { + catchErrors { step([ $class: 'Mailer', notifyEveryUnstableBuild: true, @@ -254,7 +156,7 @@ def sendInfraMail() { } def sendKibanaMail() { - catchError { + catchErrors { def buildStatus = buildUtils.getBuildStatus() if(params.NOTIFY_ON_FAILURE && buildStatus != 'SUCCESS' && buildStatus != 'ABORTED') { emailext( @@ -299,4 +201,18 @@ def runErrorReporter() { ) } +def call(Map params = [:], Closure closure) { + def config = [timeoutMinutes: 135] + params + + stage("Kibana Pipeline") { + timeout(time: config.timeoutMinutes, unit: 'MINUTES') { + timestamps { + ansiColor('xterm') { + closure() + } + } + } + } +} + return this diff --git a/vars/retryWithDelay.groovy b/vars/retryWithDelay.groovy index 70d6f86a63ab2..83fd94c6f2b1e 100644 --- a/vars/retryWithDelay.groovy +++ b/vars/retryWithDelay.groovy @@ -2,7 +2,9 @@ def call(retryTimes, delaySecs, closure) { retry(retryTimes) { try { closure() - } catch (ex) { + } catch (org.jenkinsci.plugins.workflow.steps.FlowInterruptedException ex) { + throw ex // Immediately re-throw build abort exceptions, don't sleep first + } catch (Exception ex) { sleep delaySecs throw ex } diff --git a/vars/retryable.groovy b/vars/retryable.groovy index cc34024958aed..ed84a00ece49d 100644 --- a/vars/retryable.groovy +++ b/vars/retryable.groovy @@ -27,7 +27,7 @@ def getFlakyFailures() { } def printFlakyFailures() { - catchError { + catchErrors { def failures = getFlakyFailures() if (failures && failures.size() > 0) { diff --git a/vars/workers.groovy b/vars/workers.groovy new file mode 100644 index 0000000000000..c5638f2624fe5 --- /dev/null +++ b/vars/workers.groovy @@ -0,0 +1,147 @@ +// "Workers" in this file will spin up an instance, do some setup etc depending on the configuration, and then execute some work that you define +// e.g. workers.base(name: 'my-worker') { sh "echo 'ready to execute some kibana scripts'" } + +/* + The base worker that all of the others use. Will clone the scm (assumed to be kibana), and run kibana bootstrap processes by default. + + Parameters: + label - gobld/agent label to use, e.g. 'linux && immutable' + ramDisk - Should the workspace be mounted in memory? Default: true + bootstrapped - If true, download kibana dependencies, run kbn bootstrap, etc. Default: true + name - Name of the worker for display purposes, filenames, etc. + scm - Jenkins scm configuration for checking out code. Use `null` to disable checkout. Default: inherited from job +*/ +def base(Map params, Closure closure) { + def config = [label: '', ramDisk: true, bootstrapped: true, name: 'unnamed-worker', scm: scm] + params + if (!config.label) { + error "You must specify an agent label, such as 'tests-xl' or 'linux && immutable', when using workers.base()" + } + + node(config.label) { + agentInfo.print() + + if (config.ramDisk) { + // Move to a temporary workspace, so that we can symlink the real workspace into /dev/shm + def originalWorkspace = env.WORKSPACE + ws('/tmp/workspace') { + sh( + script: """ + mkdir -p /dev/shm/workspace + mkdir -p '${originalWorkspace}' # create all of the directories leading up to the workspace, if they don't exist + rm --preserve-root -rf '${originalWorkspace}' # then remove just the workspace, just in case there's stuff in it + ln -s /dev/shm/workspace '${originalWorkspace}' + """, + label: "Move workspace to RAM - /dev/shm/workspace" + ) + } + } + + def scmVars = [:] + + if (config.scm) { + // Try to clone from Github up to 8 times, waiting 15 secs between attempts + retryWithDelay(8, 15) { + scmVars = checkout scm + } + } + + withEnv([ + "CI=true", + "HOME=${env.JENKINS_HOME}", + "PR_SOURCE_BRANCH=${env.ghprbSourceBranch ?: ''}", + "PR_TARGET_BRANCH=${env.ghprbTargetBranch ?: ''}", + "PR_AUTHOR=${env.ghprbPullAuthorLogin ?: ''}", + "TEST_BROWSER_HEADLESS=1", + "GIT_BRANCH=${scmVars.GIT_BRANCH ?: ''}", + ]) { + withCredentials([ + string(credentialsId: 'vault-addr', variable: 'VAULT_ADDR'), + string(credentialsId: 'vault-role-id', variable: 'VAULT_ROLE_ID'), + string(credentialsId: 'vault-secret-id', variable: 'VAULT_SECRET_ID'), + ]) { + // scm is configured to check out to the ./kibana directory + dir('kibana') { + if (config.bootstrapped) { + kibanaPipeline.doSetup() + } + + closure() + } + } + } + } +} + +// Worker for ci processes. Extends the base worker and adds GCS artifact upload, error reporting, junit processing +def ci(Map params, Closure closure) { + def config = [ramDisk: true, bootstrapped: true] + params + + return base(config) { + kibanaPipeline.withGcsArtifactUpload(config.name) { + kibanaPipeline.withPostBuildReporting { + closure() + } + } + } +} + +// Worker for running the current intake jobs. Just runs a single script after bootstrap. +def intake(jobName, String script) { + return { + ci(name: jobName, label: 'linux && immutable', ramDisk: false) { + withEnv(["JOB=${jobName}"]) { + runbld(script, "Execute ${jobName}") + } + } + } +} + +// Worker for running functional tests. Runs a setup process (e.g. the kibana build) then executes a map of closures in parallel (e.g. one for each ciGroup) +def functional(name, Closure setup, Map processes) { + return { + parallelProcesses(name: name, setup: setup, processes: processes, delayBetweenProcesses: 20, label: 'tests-xl') + } +} + +/* + Creates a ci worker that can run a setup process, followed by a group of processes in parallel. + + Parameters: + name: Name of the worker for display purposes, filenames, etc. + setup: Closure to execute after the agent is bootstrapped, before starting the parallel work + processes: Map of closures that will execute in parallel after setup. Each closure is passed a unique number. + delayBetweenProcesses: Number of seconds to wait between starting the parallel processes. Useful to spread the load of heavy init processes, e.g. Elasticsearch starting up. Default: 0 + label: gobld/agent label to use, e.g. 'linux && immutable'. Default: 'tests-xl', a 32 CPU machine used for running many functional test suites in parallel +*/ +def parallelProcesses(Map params) { + def config = [name: 'parallel-worker', setup: {}, processes: [:], delayBetweenProcesses: 0, label: 'tests-xl'] + params + + ci(label: config.label, name: config.name) { + config.setup() + + def nextProcessNumber = 1 + def process = { processName, processClosure -> + def processNumber = nextProcessNumber + nextProcessNumber++ + + return { + if (config.delayBetweenProcesses && config.delayBetweenProcesses > 0) { + // This delay helps smooth out CPU load caused by ES/Kibana instances starting up at the same time + def delay = (processNumber-1)*config.delayBetweenProcesses + sleep(delay) + } + + processClosure(processNumber) + } + } + + def processes = [:] + config.processes.each { processName, processClosure -> + processes[processName] = process(processName, processClosure) + } + + parallel(processes) + } +} + +return this From be0a4c4e229ca3c8bd619cb95965a1049ada6b11 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Mon, 2 Mar 2020 13:20:18 -0700 Subject: [PATCH 18/34] Downgrade "setting up plugin" log to debug (#58776) --- .../integration_tests/generate_plugin.test.js | 1 + src/core/server/plugins/plugin.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js index 771bf43c4020a..d7d4dc14519c3 100644 --- a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js +++ b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js @@ -102,6 +102,7 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug 'start', '--optimize.enabled=false', '--logging.json=false', + '--logging.verbose=true', '--migrations.skip=true', ], cwd: generatedPath, diff --git a/src/core/server/plugins/plugin.ts b/src/core/server/plugins/plugin.ts index d6c774f6fc41c..b372874264eb5 100644 --- a/src/core/server/plugins/plugin.ts +++ b/src/core/server/plugins/plugin.ts @@ -95,7 +95,7 @@ export class PluginWrapper< public async setup(setupContext: CoreSetup, plugins: TPluginsSetup) { this.instance = this.createPluginInstance(); - this.log.info('Setting up plugin'); + this.log.debug('Setting up plugin'); return this.instance.setup(setupContext, plugins); } @@ -112,6 +112,8 @@ export class PluginWrapper< throw new Error(`Plugin "${this.name}" can't be started since it isn't set up.`); } + this.log.debug('Starting plugin'); + const startContract = await this.instance.start(startContext, plugins); this.startDependencies$.next([startContext, plugins]); return startContract; From b7c8e3a25229c4fcc6bf9a2ff9364860927a77ac Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Mon, 2 Mar 2020 20:56:07 +0000 Subject: [PATCH 19/34] Dashboard a11y tests (#58122) * adding comprehensive dashboard tests * fixing delete and adding dima changes * Fixing some of the a11y test failures * Fixing i18n issue * Extracting exit fullscreen logic in a separate function * Fixing typo * Upgrading axe * Fixing failing jest tests * Removing main tag as it was causing a test to fail * Adding focusable=false to a range control as well * Update test/accessibility/apps/dashboard.ts Co-Authored-By: Michail Yasonik * Fixing linting error * Update src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx Co-Authored-By: Michail Yasonik * Add comments Co-authored-by: Bhavya RM Co-authored-by: Michail Yasonik Co-authored-by: Elastic Machine --- package.json | 2 +- .../__snapshots__/list_control.test.tsx.snap | 1 + .../public/components/vis/list_control.tsx | 14 ++ .../__snapshots__/clone_modal.test.js.snap | 1 + .../np_ready/top_nav/clone_modal.tsx | 3 + .../tool_bar_pager_buttons.test.tsx.snap | 2 + .../pager/tool_bar_pager_buttons.tsx | 13 ++ .../validated_range/validated_dual_range.js | 1 + .../query_string_input/language_switcher.tsx | 2 +- test/accessibility/apps/dashboard.ts | 131 ++++++++++++++++-- .../functional/page_objects/dashboard_page.ts | 7 + yarn.lock | 8 +- 12 files changed, 171 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 0f04a2fba3b65..5db93e5ab5ab9 100644 --- a/package.json +++ b/package.json @@ -384,7 +384,7 @@ "@typescript-eslint/parser": "^2.15.0", "angular-mocks": "^1.7.9", "archiver": "^3.1.1", - "axe-core": "^3.3.2", + "axe-core": "^3.4.1", "babel-eslint": "^10.0.3", "babel-jest": "^24.9.0", "babel-plugin-dynamic-import-node": "^2.3.0", diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap b/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap index 99482a4be2d7b..59ae99260cecd 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap +++ b/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap @@ -25,6 +25,7 @@ exports[`renders ListControl 1`] = ` compressed={false} data-test-subj="listControlSelect0" fullWidth={false} + inputRef={[Function]} isClearable={true} isLoading={false} onChange={[Function]} diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx b/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx index d62adfdce56b4..d01cef15ea41b 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx @@ -58,8 +58,17 @@ class ListControlUi extends PureComponent { + if (this.textInput) { + this.textInput.setAttribute('focusable', 'false'); // remove when #59039 is fixed + } this.isMounted = true; }; @@ -67,6 +76,10 @@ class ListControlUi extends PureComponent { + this.textInput = ref; + }; + handleOnChange = (selectedOptions: any[]) => { const selectedValues = selectedOptions.map(({ value }) => { return value; @@ -143,6 +156,7 @@ class ListControlUi extends PureComponent ); } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/__snapshots__/clone_modal.test.js.snap b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/__snapshots__/clone_modal.test.js.snap index f5a00e5435ed6..771d53b73d960 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/__snapshots__/clone_modal.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/__snapshots__/clone_modal.test.js.snap @@ -28,6 +28,7 @@ exports[`renders DashboardCloneModal 1`] = ` { @@ -41,6 +48,12 @@ export function ToolBarPagerButtons(props: Props) { onClick={() => props.onPageNext()} disabled={!props.hasNextPage} data-test-subj="btnNextPage" + aria-label={i18n.translate( + 'kbn.ddiscover.docTable.pager.toolbarPagerButtons.nextButtonAriaLabel', + { + defaultMessage: 'Next page in table', + } + )} > diff --git a/src/legacy/ui/public/validated_range/validated_dual_range.js b/src/legacy/ui/public/validated_range/validated_dual_range.js index 8689397a78333..3b0efba11afcc 100644 --- a/src/legacy/ui/public/validated_range/validated_dual_range.js +++ b/src/legacy/ui/public/validated_range/validated_dual_range.js @@ -92,6 +92,7 @@ export class ValidatedDualRange extends Component { fullWidth={fullWidth} value={this.state.value} onChange={this._onChange} + focusable={false} // remove when #59039 is fixed {...rest} /> diff --git a/src/plugins/data/public/ui/query_string_input/language_switcher.tsx b/src/plugins/data/public/ui/query_string_input/language_switcher.tsx index d86a8a970a8e7..63f6997ce2fc3 100644 --- a/src/plugins/data/public/ui/query_string_input/language_switcher.tsx +++ b/src/plugins/data/public/ui/query_string_input/language_switcher.tsx @@ -68,7 +68,7 @@ export function QueryLanguageSwitcher(props: Props) { return ( { const dashboardName = 'Dashboard Listing A11y'; + const clonedDashboardName = 'Dashboard Listing A11y Copy'; + before(async () => { - await esArchiver.loadIfNeeded('logstash_functional'); - await kibanaServer.uiSettings.update({ - defaultIndex: 'logstash-*', - }); + await PageObjects.common.navigateToUrl('home', 'tutorial_directory/sampleData'); + await PageObjects.home.addSampleDataSet('flights'); + }); + + after(async () => { await PageObjects.common.navigateToApp('dashboard'); + await listingTable.searchForItemWithName(dashboardName); + await listingTable.checkListingSelectAllCheckbox(); + await listingTable.clickDeleteSelected(); + await PageObjects.common.clickConfirmOnModal(); }); it('dashboard', async () => { + await PageObjects.common.navigateToApp('dashboard'); await a11y.testAppSnapshot(); }); it('create dashboard button', async () => { - await PageObjects.dashboard.clickCreateDashboardPrompt(); + await PageObjects.dashboard.clickNewDashboard(); await a11y.testAppSnapshot(); }); @@ -49,9 +58,115 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { await a11y.testAppSnapshot(); }); + it('Open Edit mode', async () => { + await PageObjects.dashboard.switchToEditMode(); + await a11y.testAppSnapshot(); + }); + + it('Open add panel', async () => { + await dashboardAddPanel.clickOpenAddPanel(); + await a11y.testAppSnapshot(); + }); + + it('add a visualization', async () => { + await testSubjects.click('savedObjectTitle[Flights]-Delay-Buckets'); + await a11y.testAppSnapshot(); + }); + + it('add a saved search', async () => { + await dashboardAddPanel.addSavedSearch('[Flights] Flight Log'); + await a11y.testAppSnapshot(); + }); + + it('save the dashboard', async () => { + await PageObjects.dashboard.saveDashboard(dashboardName); + await a11y.testAppSnapshot(); + }); + + it('Open Edit mode', async () => { + await PageObjects.dashboard.switchToEditMode(); + await a11y.testAppSnapshot(); + }); + + it('open options menu', async () => { + await PageObjects.dashboard.openOptions(); + await a11y.testAppSnapshot(); + }); + + it('Should be able to hide panel titles', async () => { + await testSubjects.click('dashboardPanelTitlesCheckbox'); + await a11y.testAppSnapshot(); + }); + + it('Should be able display panels without margins', async () => { + await testSubjects.click('dashboardMarginsCheckbox'); + await a11y.testAppSnapshot(); + }); + + it('Open add panel', async () => { + await dashboardAddPanel.clickOpenAddPanel(); + await a11y.testAppSnapshot(); + }); + + it('Add one more saved object to cancel it', async () => { + await testSubjects.click('savedObjectTitle[Flights]-Average-Ticket-Price'); + await a11y.testAppSnapshot(); + }); + + it('Close add panel', async () => { + await dashboardAddPanel.closeAddPanel(); + await a11y.testAppSnapshot(); + }); + + it('Exit out of edit mode', async () => { + await PageObjects.dashboard.clickCancelOutOfEditMode(); + await a11y.testAppSnapshot(); + }); + + it('Discard changes', async () => { + await PageObjects.common.clickConfirmOnModal(); + await PageObjects.dashboard.getIsInViewMode(); + await a11y.testAppSnapshot(); + }); + + it('Test full screen', async () => { + await PageObjects.dashboard.clickFullScreenMode(); + await a11y.testAppSnapshot(); + }); + + it('Exit out of full screen mode', async () => { + await PageObjects.dashboard.exitFullScreenMode(); + await a11y.testAppSnapshot(); + }); + + it('Make a clone of the dashboard', async () => { + await PageObjects.dashboard.clickClone(); + await a11y.testAppSnapshot(); + }); + + it('Confirm clone with *copy* appended', async () => { + await PageObjects.dashboard.confirmClone(); + await a11y.testAppSnapshot(); + }); + it('Dashboard listing table', async () => { await PageObjects.dashboard.gotoDashboardLandingPage(); await a11y.testAppSnapshot(); }); + + it('Delete a11y clone dashboard', async () => { + await listingTable.searchForItemWithName(clonedDashboardName); + await listingTable.checkListingSelectAllCheckbox(); + await listingTable.clickDeleteSelected(); + await a11y.testAppSnapshot(); + await PageObjects.common.clickConfirmOnModal(); + await listingTable.searchForItemWithName(''); + }); + + // Blocked by https://github.com/elastic/kibana/issues/38980 + it.skip('Open flight dashboard', async () => { + await testSubjects.click('dashboardListingTitleLink-[Flights]-Global-Flight-Dashboard'); + await a11y.testAppSnapshot(); + }); }); } diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index 3fd7ce27e27d4..0f01097cf50dc 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -65,6 +65,13 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide await this.waitForRenderComplete(); } + public async exitFullScreenMode() { + log.debug(`exitFullScreenMode`); + const logoButton = await this.getExitFullScreenLogoButton(); + await logoButton.moveMouseTo(); + await this.clickExitFullScreenTextButton(); + } + public async fullScreenModeMenuItemExists() { return await testSubjects.exists('dashboardFullScreenMode'); } diff --git a/yarn.lock b/yarn.lock index 7f38495c20f4a..338d516a796e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7121,10 +7121,10 @@ aws4@^1.6.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" integrity sha1-g+9cqGCysy5KDe7e6MdxudtXRx4= -axe-core@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-3.3.2.tgz#7baf3c55a5cf1621534a2c38735f5a1bf2f7e1a8" - integrity sha512-lRdxsRt7yNhqpcXQk1ao1BL73OZDzmFCWOG0mC4tGR/r14ohH2payjHwCMQjHGbBKm924eDlmG7utAGHiX/A6g== +axe-core@^3.4.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-3.5.1.tgz#d8d5aaef73f003e8b766ea28bb078343f3622201" + integrity sha512-mwpDgPwWB+5kMHyLjlxh4w25ClJfqSxi+c6LQ4ix349TdCUctMwJNPTkhPD1qP9SYIjFgjeVpVZWCvK9oBGwCg== axios@^0.18.0, axios@^0.18.1: version "0.18.1" From bb6fd0bf4ff8e0a501747aef4b399e997cc25e6b Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 2 Mar 2020 15:15:44 -0700 Subject: [PATCH 20/34] [kbn/optimizer] fix ui/* url rewrites in dist (#58627) * [kbn/optimizer] fix ui/* url rewrites in dist * add tests to verify styles are built correctly and ui-rewrites are happening * clarify change to dirs creation * create tested & shared parsePath helper * update renovate config * split implementation of parsePath for dir and file paths * switch to valid css property Co-authored-by: Elastic Machine --- package.json | 1 + .../mock_repo/plugins/bar/public/index.ts | 1 + .../plugins/bar/public/legacy/styles.scss | 4 + .../mock_repo/src/legacy/ui/public/icon.svg | 1 + .../ui/public/styles/_styling_constants.scss | 1 + .../basic_optimization.test.ts.snap | 527 +----------------- .../basic_optimization.test.ts | 56 +- .../__snapshots__/parse_path.test.ts.snap | 156 ++++++ .../src/worker/parse_path.test.ts | 20 +- .../kbn-optimizer/src/worker/parse_path.ts | 43 ++ .../kbn-optimizer/src/worker/run_compilers.ts | 16 +- .../src/worker/webpack.config.ts | 7 +- renovate.json5 | 8 + yarn.lock | 5 + 14 files changed, 299 insertions(+), 547 deletions(-) create mode 100644 packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/legacy/styles.scss create mode 100644 packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/icon.svg create mode 100644 packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_styling_constants.scss create mode 100644 packages/kbn-optimizer/src/worker/__snapshots__/parse_path.test.ts.snap rename typings/normalize_path/index.d.ts => packages/kbn-optimizer/src/worker/parse_path.test.ts (57%) create mode 100644 packages/kbn-optimizer/src/worker/parse_path.ts diff --git a/package.json b/package.json index 5db93e5ab5ab9..e727d87a83c53 100644 --- a/package.json +++ b/package.json @@ -349,6 +349,7 @@ "@types/mustache": "^0.8.31", "@types/node": "^10.12.27", "@types/node-forge": "^0.9.0", + "@types/normalize-path": "^3.0.0", "@types/numeral": "^0.0.26", "@types/opn": "^5.1.0", "@types/pegjs": "^0.10.1", diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts index 66fa55479f3b9..817c4796562e8 100644 --- a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts @@ -17,6 +17,7 @@ * under the License. */ +import './legacy/styles.scss'; import { fooLibFn } from '../../foo/public/index'; export * from './lib'; export { fooLibFn }; diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/legacy/styles.scss b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/legacy/styles.scss new file mode 100644 index 0000000000000..e71a2d485a2f8 --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/legacy/styles.scss @@ -0,0 +1,4 @@ +body { + width: $globalStyleConstant; + background-image: url("ui/icon.svg"); +} diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/icon.svg b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/icon.svg new file mode 100644 index 0000000000000..ae7d5b958bbad --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_styling_constants.scss b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_styling_constants.scss new file mode 100644 index 0000000000000..83995ca65211b --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_styling_constants.scss @@ -0,0 +1 @@ +$globalStyleConstant: 10; diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index 706f79978beee..1a974d3e81092 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -5,553 +5,56 @@ OptimizerConfig { "bundles": Array [ Bundle { "cache": BundleCache { - "path": /plugins/bar/target/public/.kbn-optimizer-cache, + "path": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/target/public/.kbn-optimizer-cache, "state": undefined, }, - "contextDir": /plugins/bar, + "contextDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar, "entry": "./public/index", "id": "bar", - "outputDir": /plugins/bar/target/public, - "sourceRoot": , + "outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/target/public, + "sourceRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, "type": "plugin", }, Bundle { "cache": BundleCache { - "path": /plugins/foo/target/public/.kbn-optimizer-cache, + "path": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/target/public/.kbn-optimizer-cache, "state": undefined, }, - "contextDir": /plugins/foo, + "contextDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo, "entry": "./public/index", "id": "foo", - "outputDir": /plugins/foo/target/public, - "sourceRoot": , + "outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/target/public, + "sourceRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, "type": "plugin", }, ], "cache": true, - "dist": false, + "dist": true, "inspectWorkers": false, "maxWorkerCount": 1, "plugins": Array [ Object { - "directory": /plugins/bar, + "directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar, "id": "bar", "isUiPlugin": true, }, Object { - "directory": /plugins/baz, + "directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/baz, "id": "baz", "isUiPlugin": false, }, Object { - "directory": /plugins/foo, + "directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo, "id": "foo", "isUiPlugin": true, }, ], "profileWebpack": false, - "repoRoot": , + "repoRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, "watch": false, } `; -exports[`builds expected bundles, saves bundle counts to metadata: bar bundle 1`] = ` -"var __kbnBundles__ = typeof __kbnBundles__ === \\"object\\" ? __kbnBundles__ : {}; __kbnBundles__[\\"plugin/bar\\"] = -/******/ (function(modules) { // webpackBootstrap -/******/ // The module cache -/******/ var installedModules = {}; -/******/ -/******/ // The require function -/******/ function __webpack_require__(moduleId) { -/******/ -/******/ // Check if module is in cache -/******/ if(installedModules[moduleId]) { -/******/ return installedModules[moduleId].exports; -/******/ } -/******/ // Create a new module (and put it into the cache) -/******/ var module = installedModules[moduleId] = { -/******/ i: moduleId, -/******/ l: false, -/******/ exports: {} -/******/ }; -/******/ -/******/ // Execute the module function -/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); -/******/ -/******/ // Flag the module as loaded -/******/ module.l = true; -/******/ -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } -/******/ -/******/ -/******/ // expose the modules object (__webpack_modules__) -/******/ __webpack_require__.m = modules; -/******/ -/******/ // expose the module cache -/******/ __webpack_require__.c = installedModules; -/******/ -/******/ // define getter function for harmony exports -/******/ __webpack_require__.d = function(exports, name, getter) { -/******/ if(!__webpack_require__.o(exports, name)) { -/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); -/******/ } -/******/ }; -/******/ -/******/ // define __esModule on exports -/******/ __webpack_require__.r = function(exports) { -/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { -/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); -/******/ } -/******/ Object.defineProperty(exports, '__esModule', { value: true }); -/******/ }; -/******/ -/******/ // create a fake namespace object -/******/ // mode & 1: value is a module id, require it -/******/ // mode & 2: merge all properties of value into the ns -/******/ // mode & 4: return value when already ns object -/******/ // mode & 8|1: behave like require -/******/ __webpack_require__.t = function(value, mode) { -/******/ if(mode & 1) value = __webpack_require__(value); -/******/ if(mode & 8) return value; -/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; -/******/ var ns = Object.create(null); -/******/ __webpack_require__.r(ns); -/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); -/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); -/******/ return ns; -/******/ }; -/******/ -/******/ // getDefaultExport function for compatibility with non-harmony modules -/******/ __webpack_require__.n = function(module) { -/******/ var getter = module && module.__esModule ? -/******/ function getDefault() { return module['default']; } : -/******/ function getModuleExports() { return module; }; -/******/ __webpack_require__.d(getter, 'a', getter); -/******/ return getter; -/******/ }; -/******/ -/******/ // Object.prototype.hasOwnProperty.call -/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; -/******/ -/******/ // __webpack_public_path__ -/******/ __webpack_require__.p = \\"__REPLACE_WITH_PUBLIC_PATH__\\"; -/******/ -/******/ -/******/ // Load entry module and return exports -/******/ return __webpack_require__(__webpack_require__.s = \\"./public/index.ts\\"); -/******/ }) -/************************************************************************/ -/******/ ({ +exports[`builds expected bundles, saves bundle counts to metadata: bar bundle 1`] = `"var __kbnBundles__=typeof __kbnBundles__===\\"object\\"?__kbnBundles__:{};__kbnBundles__[\\"plugin/bar\\"]=function(modules){var installedModules={};function __webpack_require__(moduleId){if(installedModules[moduleId]){return installedModules[moduleId].exports}var module=installedModules[moduleId]={i:moduleId,l:false,exports:{}};modules[moduleId].call(module.exports,module,module.exports,__webpack_require__);module.l=true;return module.exports}__webpack_require__.m=modules;__webpack_require__.c=installedModules;__webpack_require__.d=function(exports,name,getter){if(!__webpack_require__.o(exports,name)){Object.defineProperty(exports,name,{enumerable:true,get:getter})}};__webpack_require__.r=function(exports){if(typeof Symbol!==\\"undefined\\"&&Symbol.toStringTag){Object.defineProperty(exports,Symbol.toStringTag,{value:\\"Module\\"})}Object.defineProperty(exports,\\"__esModule\\",{value:true})};__webpack_require__.t=function(value,mode){if(mode&1)value=__webpack_require__(value);if(mode&8)return value;if(mode&4&&typeof value===\\"object\\"&&value&&value.__esModule)return value;var ns=Object.create(null);__webpack_require__.r(ns);Object.defineProperty(ns,\\"default\\",{enumerable:true,value:value});if(mode&2&&typeof value!=\\"string\\")for(var key in value)__webpack_require__.d(ns,key,function(key){return value[key]}.bind(null,key));return ns};__webpack_require__.n=function(module){var getter=module&&module.__esModule?function getDefault(){return module[\\"default\\"]}:function getModuleExports(){return module};__webpack_require__.d(getter,\\"a\\",getter);return getter};__webpack_require__.o=function(object,property){return Object.prototype.hasOwnProperty.call(object,property)};__webpack_require__.p=\\"__REPLACE_WITH_PUBLIC_PATH__\\";return __webpack_require__(__webpack_require__.s=4)}([function(module,exports,__webpack_require__){\\"use strict\\";var isOldIE=function isOldIE(){var memo;return function memorize(){if(typeof memo===\\"undefined\\"){memo=Boolean(window&&document&&document.all&&!window.atob)}return memo}}();var getTarget=function getTarget(){var memo={};return function memorize(target){if(typeof memo[target]===\\"undefined\\"){var styleTarget=document.querySelector(target);if(window.HTMLIFrameElement&&styleTarget instanceof window.HTMLIFrameElement){try{styleTarget=styleTarget.contentDocument.head}catch(e){styleTarget=null}}memo[target]=styleTarget}return memo[target]}}();var stylesInDom=[];function getIndexByIdentifier(identifier){var result=-1;for(var i=0;i { await del(TMP_DIR); @@ -51,20 +51,25 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { repoRoot: MOCK_REPO_DIR, pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins')], maxWorkerCount: 1, + dist: true, }); expect(config).toMatchSnapshot('OptimizerConfig'); - const msgs = await runOptimizer(config) - .pipe( - tap(state => { - if (state.event?.type === 'worker stdio') { - // eslint-disable-next-line no-console - console.log('worker', state.event.stream, state.event.chunk.toString('utf8')); + const log = new ToolingLog({ + level: 'error', + writeTo: { + write(chunk) { + if (chunk.endsWith('\n')) { + chunk = chunk.slice(0, -1); } - }), - toArray() - ) + // eslint-disable-next-line no-console + console.error(chunk); + }, + }, + }); + const msgs = await runOptimizer(config) + .pipe(logOptimizerState(log, config), toArray()) .toPromise(); const assert = (statement: string, truth: boolean, altStates?: OptimizerUpdate[]) => { @@ -133,23 +138,31 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { expect(foo.cache.getModuleCount()).toBe(3); expect(foo.cache.getReferencedFiles()).toMatchInlineSnapshot(` Array [ - /plugins/foo/public/ext.ts, - /plugins/foo/public/index.ts, - /plugins/foo/public/lib.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/ext.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/index.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/lib.ts, ] `); const bar = config.bundles.find(b => b.id === 'bar')!; expect(bar).toBeTruthy(); bar.cache.refresh(); - expect(bar.cache.getModuleCount()).toBe(5); + expect(bar.cache.getModuleCount()).toBe( + // code + styles + style/css-loader runtime + 14 + ); + expect(bar.cache.getReferencedFiles()).toMatchInlineSnapshot(` Array [ - /plugins/foo/public/ext.ts, - /plugins/foo/public/index.ts, - /plugins/foo/public/lib.ts, - /plugins/bar/public/index.ts, - /plugins/bar/public/lib.ts, + /node_modules/css-loader/package.json, + /node_modules/style-loader/package.json, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/legacy/styles.scss, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/lib.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/ext.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/index.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/lib.ts, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/legacy/ui/public/icon.svg, ] `); }); @@ -159,6 +172,7 @@ it('uses cache on second run and exist cleanly', async () => { repoRoot: MOCK_REPO_DIR, pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins')], maxWorkerCount: 1, + dist: true, }); const msgs = await runOptimizer(config) diff --git a/packages/kbn-optimizer/src/worker/__snapshots__/parse_path.test.ts.snap b/packages/kbn-optimizer/src/worker/__snapshots__/parse_path.test.ts.snap new file mode 100644 index 0000000000000..2973ac116d6bd --- /dev/null +++ b/packages/kbn-optimizer/src/worker/__snapshots__/parse_path.test.ts.snap @@ -0,0 +1,156 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`parseDirPath() parses / 1`] = ` +Object { + "dirs": Array [], + "filename": undefined, + "root": "/", +} +`; + +exports[`parseDirPath() parses /foo 1`] = ` +Object { + "dirs": Array [ + "foo", + ], + "filename": undefined, + "root": "/", +} +`; + +exports[`parseDirPath() parses /foo/bar/baz 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + "baz", + ], + "filename": undefined, + "root": "/", +} +`; + +exports[`parseDirPath() parses /foo/bar/baz/ 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + "baz", + ], + "filename": undefined, + "root": "/", +} +`; + +exports[`parseDirPath() parses c:\\ 1`] = ` +Object { + "dirs": Array [], + "filename": undefined, + "root": "c:", +} +`; + +exports[`parseDirPath() parses c:\\foo 1`] = ` +Object { + "dirs": Array [ + "foo", + ], + "filename": undefined, + "root": "c:", +} +`; + +exports[`parseDirPath() parses c:\\foo\\bar\\baz 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + "baz", + ], + "filename": undefined, + "root": "c:", +} +`; + +exports[`parseDirPath() parses c:\\foo\\bar\\baz\\ 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + "baz", + ], + "filename": undefined, + "root": "c:", +} +`; + +exports[`parseFilePath() parses /foo 1`] = ` +Object { + "dirs": Array [], + "filename": "foo", + "root": "/", +} +`; + +exports[`parseFilePath() parses /foo/bar/baz 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + ], + "filename": "baz", + "root": "/", +} +`; + +exports[`parseFilePath() parses /foo/bar/baz.json 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + ], + "filename": "baz.json", + "root": "/", +} +`; + +exports[`parseFilePath() parses c:/foo/bar/baz.json 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + ], + "filename": "baz.json", + "root": "c:", +} +`; + +exports[`parseFilePath() parses c:\\foo 1`] = ` +Object { + "dirs": Array [], + "filename": "foo", + "root": "c:", +} +`; + +exports[`parseFilePath() parses c:\\foo\\bar\\baz 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + ], + "filename": "baz", + "root": "c:", +} +`; + +exports[`parseFilePath() parses c:\\foo\\bar\\baz.json 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + ], + "filename": "baz.json", + "root": "c:", +} +`; diff --git a/typings/normalize_path/index.d.ts b/packages/kbn-optimizer/src/worker/parse_path.test.ts similarity index 57% rename from typings/normalize_path/index.d.ts rename to packages/kbn-optimizer/src/worker/parse_path.test.ts index 31e064ca63d90..72197e8c8fb07 100644 --- a/typings/normalize_path/index.d.ts +++ b/packages/kbn-optimizer/src/worker/parse_path.test.ts @@ -17,8 +17,20 @@ * under the License. */ -declare function NormalizePath(path: string, stripTrailing?: boolean): string; +import { parseFilePath, parseDirPath } from './parse_path'; -declare module 'normalize-path' { - export = NormalizePath; -} +const DIRS = ['/', '/foo/bar/baz/', 'c:\\', 'c:\\foo\\bar\\baz\\']; +const AMBIGUOUS = ['/foo', '/foo/bar/baz', 'c:\\foo', 'c:\\foo\\bar\\baz']; +const FILES = ['/foo/bar/baz.json', 'c:/foo/bar/baz.json', 'c:\\foo\\bar\\baz.json']; + +describe('parseFilePath()', () => { + it.each([...FILES, ...AMBIGUOUS])('parses %s', path => { + expect(parseFilePath(path)).toMatchSnapshot(); + }); +}); + +describe('parseDirPath()', () => { + it.each([...DIRS, ...AMBIGUOUS])('parses %s', path => { + expect(parseDirPath(path)).toMatchSnapshot(); + }); +}); diff --git a/packages/kbn-optimizer/src/worker/parse_path.ts b/packages/kbn-optimizer/src/worker/parse_path.ts new file mode 100644 index 0000000000000..88152df55b84f --- /dev/null +++ b/packages/kbn-optimizer/src/worker/parse_path.ts @@ -0,0 +1,43 @@ +/* + * 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 normalizePath from 'normalize-path'; + +/** + * Parse an absolute path, supporting normalized paths from webpack, + * into a list of directories and root + */ +export function parseDirPath(path: string) { + const filePath = parseFilePath(path); + return { + ...filePath, + dirs: [...filePath.dirs, ...(filePath.filename ? [filePath.filename] : [])], + filename: undefined, + }; +} + +export function parseFilePath(path: string) { + const normalized = normalizePath(path); + const [root, ...others] = normalized.split('/'); + return { + root: root === '' ? '/' : root, + dirs: others.slice(0, -1), + filename: others[others.length - 1] || undefined, + }; +} diff --git a/packages/kbn-optimizer/src/worker/run_compilers.ts b/packages/kbn-optimizer/src/worker/run_compilers.ts index 7dcce8a0fae8d..7a8097fd2b2c7 100644 --- a/packages/kbn-optimizer/src/worker/run_compilers.ts +++ b/packages/kbn-optimizer/src/worker/run_compilers.ts @@ -27,9 +27,10 @@ import webpack, { Stats } from 'webpack'; import * as Rx from 'rxjs'; import { mergeMap, map, mapTo, takeUntil } from 'rxjs/operators'; -import { CompilerMsgs, CompilerMsg, maybeMap, Bundle, WorkerConfig } from '../common'; +import { CompilerMsgs, CompilerMsg, maybeMap, Bundle, WorkerConfig, ascending } from '../common'; import { getWebpackConfig } from './webpack.config'; import { isFailureStats, failedStatsToErrorMessage } from './webpack_helpers'; +import { parseFilePath } from './parse_path'; import { isExternalModule, isNormalModule, @@ -108,20 +109,19 @@ const observeCompiler = ( for (const module of normalModules) { const path = getModulePath(module); + const parsedPath = parseFilePath(path); - const parsedPath = Path.parse(path); - const dirSegments = parsedPath.dir.split(Path.sep); - if (!dirSegments.includes('node_modules')) { + if (!parsedPath.dirs.includes('node_modules')) { referencedFiles.add(path); continue; } - const nmIndex = dirSegments.lastIndexOf('node_modules'); - const isScoped = dirSegments[nmIndex + 1].startsWith('@'); + const nmIndex = parsedPath.dirs.lastIndexOf('node_modules'); + const isScoped = parsedPath.dirs[nmIndex + 1].startsWith('@'); referencedFiles.add( Path.join( parsedPath.root, - ...dirSegments.slice(0, nmIndex + 1 + (isScoped ? 2 : 1)), + ...parsedPath.dirs.slice(0, nmIndex + 1 + (isScoped ? 2 : 1)), 'package.json' ) ); @@ -146,7 +146,7 @@ const observeCompiler = ( optimizerCacheKey: workerConfig.optimizerCacheKey, cacheKey: bundle.createCacheKey(files, mtimes), moduleCount: normalModules.length, - files, + files: files.sort(ascending(f => f)), }); return compilerMsgs.compilerSuccess({ diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 3c6ae78bc4d91..5d8ef7626f630 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -30,6 +30,7 @@ import { CleanWebpackPlugin } from 'clean-webpack-plugin'; import * as SharedDeps from '@kbn/ui-shared-deps'; import { Bundle, WorkerConfig } from '../common'; +import { parseDirPath } from './parse_path'; const IS_CODE_COVERAGE = !!process.env.CODE_COVERAGE; const ISTANBUL_PRESET_PATH = require.resolve('@kbn/babel-preset/istanbul_preset'); @@ -135,7 +136,7 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { } // manually force ui/* urls in legacy styles to resolve to ui/legacy/public - if (uri.startsWith('ui/') && base.split(Path.sep).includes('legacy')) { + if (uri.startsWith('ui/') && parseDirPath(base).dirs.includes('legacy')) { return Path.resolve( worker.repoRoot, 'src/legacy/ui/public', @@ -150,7 +151,9 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { { loader: 'sass-loader', options: { - sourceMap: !worker.dist, + // must always be enabled as long as we're using the `resolve-url-loader` to + // rewrite `ui/*` urls. They're dropped by subsequent loaders though + sourceMap: true, prependData(loaderContext: webpack.loader.LoaderContext) { return `@import ${stringifyRequest( loaderContext, diff --git a/renovate.json5 b/renovate.json5 index 58a64a5d0f967..ca2cd2e6bcd93 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -665,6 +665,14 @@ '@types/nodemailer', ], }, + { + groupSlug: 'normalize-path', + groupName: 'normalize-path related packages', + packageNames: [ + 'normalize-path', + '@types/normalize-path', + ], + }, { groupSlug: 'numeral', groupName: 'numeral related packages', diff --git a/yarn.lock b/yarn.lock index 338d516a796e1..e4d5dcce5bca0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4864,6 +4864,11 @@ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== +"@types/normalize-path@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/normalize-path/-/normalize-path-3.0.0.tgz#bb5c46cab77b93350b4cf8d7ff1153f47189ae31" + integrity sha512-Nd8y/5t/7CRakPYiyPzr/IAfYusy1FkcZYFEAcoMZkwpJv2n4Wm+olW+e7xBdHEXhOnWdG9ddbar0gqZWS4x5Q== + "@types/numeral@^0.0.25": version "0.0.25" resolved "https://registry.yarnpkg.com/@types/numeral/-/numeral-0.0.25.tgz#b6f55062827a4787fe4ab151cf3412a468e65271" From 48a33abdeed147932401cc3a24c36669189f67f3 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Mon, 2 Mar 2020 15:51:33 -0700 Subject: [PATCH 21/34] Remove appBasePath from docs + add mock for AppMountParameters (#58775) --- ...lugin-public.appmountparameters.history.md | 1 - ...in-public.appmountparameters.onappleave.md | 4 +-- src/core/CONVENTIONS.md | 4 +-- src/core/TESTING.md | 14 ++++++----- src/core/public/application/types.ts | 5 ++-- src/core/public/mocks.ts | 25 ++++++++++++++++++- 6 files changed, 38 insertions(+), 15 deletions(-) diff --git a/docs/development/core/public/kibana-plugin-public.appmountparameters.history.md b/docs/development/core/public/kibana-plugin-public.appmountparameters.history.md index 9a3fa1a1bb48a..f22e70b0e7bee 100644 --- a/docs/development/core/public/kibana-plugin-public.appmountparameters.history.md +++ b/docs/development/core/public/kibana-plugin-public.appmountparameters.history.md @@ -44,7 +44,6 @@ import { MyPluginDepsStart } from './plugin'; export renderApp = ({ element, history }: AppMountParameters) => { ReactDOM.render( - // pass `appBasePath` to `basename` , diff --git a/docs/development/core/public/kibana-plugin-public.appmountparameters.onappleave.md b/docs/development/core/public/kibana-plugin-public.appmountparameters.onappleave.md index 283ae34f14c54..6c5b89ffda05b 100644 --- a/docs/development/core/public/kibana-plugin-public.appmountparameters.onappleave.md +++ b/docs/development/core/public/kibana-plugin-public.appmountparameters.onappleave.md @@ -26,7 +26,7 @@ import { BrowserRouter, Route } from 'react-router-dom'; import { CoreStart, AppMountParams } from 'src/core/public'; import { MyPluginDepsStart } from './plugin'; -export renderApp = ({ appBasePath, element, onAppLeave }: AppMountParams) => { +export renderApp = ({ element, history, onAppLeave }: AppMountParams) => { const { renderApp, hasUnsavedChanges } = await import('./application'); onAppLeave(actions => { if(hasUnsavedChanges()) { @@ -34,7 +34,7 @@ export renderApp = ({ appBasePath, element, onAppLeave }: AppMountParams) => { } return actions.default(); }); - return renderApp(params); + return renderApp({ element, history }); } ``` diff --git a/src/core/CONVENTIONS.md b/src/core/CONVENTIONS.md index 2769079757bc3..0f592d108c561 100644 --- a/src/core/CONVENTIONS.md +++ b/src/core/CONVENTIONS.md @@ -148,8 +148,8 @@ import { MyAppRoot } from './components/app.ts'; /** * This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle. */ -export const renderApp = (core: CoreStart, deps: MyPluginDepsStart, { element, appBasePath }: AppMountParams) => { - ReactDOM.render(, element); +export const renderApp = (core: CoreStart, deps: MyPluginDepsStart, { element, history }: AppMountParams) => { + ReactDOM.render(, element); return () => ReactDOM.unmountComponentAtNode(element); } ``` diff --git a/src/core/TESTING.md b/src/core/TESTING.md index 9abc2bb77d7d1..cb38dac0e20ce 100644 --- a/src/core/TESTING.md +++ b/src/core/TESTING.md @@ -453,7 +453,7 @@ describe('Plugin', () => { const [coreStartMock, startDepsMock] = await coreSetup.getStartServices(); const unmountMock = jest.fn(); renderAppMock.mockReturnValue(unmountMock); - const params = { element: document.createElement('div'), appBasePath: '/fake/base/path' }; + const params = coreMock.createAppMountParamters('/fake/base/path'); new Plugin(coreMock.createPluginInitializerContext()).setup(coreSetup); // Grab registered mount function @@ -478,7 +478,7 @@ import ReactDOM from 'react-dom'; import { AppMountParams, CoreStart } from 'src/core/public'; import { AppRoot } from './components/app_root'; -export const renderApp = ({ element, appBasePath }: AppMountParams, core: CoreStart, plugins: MyPluginDepsStart) => { +export const renderApp = ({ element, history }: AppMountParams, core: CoreStart, plugins: MyPluginDepsStart) => { // Hide the chrome while this app is mounted for a full screen experience core.chrome.setIsVisible(false); @@ -491,7 +491,7 @@ export const renderApp = ({ element, appBasePath }: AppMountParams, core: CoreSt // Render app ReactDOM.render( - , + , element ); @@ -512,12 +512,14 @@ In testing `renderApp` you should be verifying that: ```typescript /** public/application.test.ts */ +import { createMemoryHistory } from 'history'; +import { ScopedHistory } from 'src/core/public'; import { coreMock } from 'src/core/public/mocks'; import { renderApp } from './application'; describe('renderApp', () => { it('mounts and unmounts UI', () => { - const params = { element: document.createElement('div'), appBasePath: '/fake/base/path' }; + const params = coreMock.createAppMountParamters('/fake/base/path'); const core = coreMock.createStart(); // Verify some expected DOM element is rendered into the element @@ -529,7 +531,7 @@ describe('renderApp', () => { }); it('unsubscribes from uiSettings', () => { - const params = { element: document.createElement('div'), appBasePath: '/fake/base/path' }; + const params = coreMock.createAppMountParamters('/fake/base/path'); const core = coreMock.createStart(); // Create a fake Subject you can use to monitor observers const settings$ = new Subject(); @@ -544,7 +546,7 @@ describe('renderApp', () => { }); it('resets chrome visibility', () => { - const params = { element: document.createElement('div'), appBasePath: '/fake/base/path' }; + const params = coreMock.createAppMountParamters('/fake/base/path'); const core = coreMock.createStart(); // Verify stateful Core API was called on mount diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index facb818c60ff9..318afb652999e 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -347,7 +347,6 @@ export interface AppMountParameters { * * export renderApp = ({ element, history }: AppMountParameters) => { * ReactDOM.render( - * // pass `appBasePath` to `basename` * * * , @@ -429,7 +428,7 @@ export interface AppMountParameters { * import { CoreStart, AppMountParams } from 'src/core/public'; * import { MyPluginDepsStart } from './plugin'; * - * export renderApp = ({ appBasePath, element, onAppLeave }: AppMountParams) => { + * export renderApp = ({ element, history, onAppLeave }: AppMountParams) => { * const { renderApp, hasUnsavedChanges } = await import('./application'); * onAppLeave(actions => { * if(hasUnsavedChanges()) { @@ -437,7 +436,7 @@ export interface AppMountParameters { * } * return actions.default(); * }); - * return renderApp(params); + * return renderApp({ element, history }); * } * ``` */ diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index 8ea672890ca29..c860e9de8334e 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -16,9 +16,15 @@ * specific language governing permissions and limitations * under the License. */ +import { createMemoryHistory } from 'history'; + +// Only import types from '.' to avoid triggering default Jest mocks. +import { CoreContext, PluginInitializerContext, AppMountParameters } from '.'; +// Import values from their individual modules instead. +import { ScopedHistory } from './application'; + import { applicationServiceMock } from './application/application_service.mock'; import { chromeServiceMock } from './chrome/chrome_service.mock'; -import { CoreContext, PluginInitializerContext } from '.'; import { docLinksServiceMock } from './doc_links/doc_links_service.mock'; import { fatalErrorsServiceMock } from './fatal_errors/fatal_errors_service.mock'; import { httpServiceMock } from './http/http_service.mock'; @@ -139,10 +145,27 @@ function createStorageMock() { return storageMock; } +function createAppMountParametersMock(appBasePath = '') { + // Assemble an in-memory history mock using the provided basePath + const rawHistory = createMemoryHistory(); + rawHistory.push(appBasePath); + const history = new ScopedHistory(rawHistory, appBasePath); + + const params: jest.Mocked = { + appBasePath, + element: document.createElement('div'), + history, + onAppLeave: jest.fn(), + }; + + return params; +} + export const coreMock = { createCoreContext, createSetup: createCoreSetupMock, createStart: createCoreStartMock, createPluginInitializerContext: pluginInitializerContextMock, createStorage: createStorageMock, + createAppMountParamters: createAppMountParametersMock, }; From 90b3678dffa20ff7e0ab806861bfa64d46d86cb9 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 2 Mar 2020 16:04:29 -0700 Subject: [PATCH 22/34] [SIEM] [Case] Comments to case view (#58315) --- .../edit_data_provider/translations.ts | 2 +- .../components/formatted_date/index.tsx | 29 +++ .../editable_title.test.tsx.snap | 2 +- .../components/header_page/editable_title.tsx | 64 ++++-- .../components/header_page/translations.ts | 4 +- .../components/markdown_editor/constants.ts | 7 + .../components/markdown_editor/form.tsx | 58 ++++++ .../components/markdown_editor/index.tsx | 121 +++++++++++ .../markdown_editor/translations.ts | 18 ++ .../components/navigation/index.test.tsx | 4 +- .../siem/public/containers/case/api.ts | 38 +++- .../siem/public/containers/case/constants.ts | 2 +- .../siem/public/containers/case/types.ts | 28 ++- .../public/containers/case/use_get_case.tsx | 4 +- .../public/containers/case/use_post_case.tsx | 3 +- .../containers/case/use_post_comment.tsx | 97 +++++++++ .../containers/case/use_update_case.tsx | 68 +++--- .../containers/case/use_update_comment.tsx | 92 ++++++++ .../case/components/add_comment/index.tsx | 73 +++++++ .../case/components/add_comment/schema.tsx | 20 ++ .../components/all_cases/__mock__/index.tsx | 10 + .../case/components/all_cases/columns.tsx | 2 +- .../components/case_view/__mock__/index.tsx | 29 ++- .../case/components/case_view/index.test.tsx | 41 +++- .../pages/case/components/case_view/index.tsx | 196 ++++++------------ .../case/components/case_view/translations.ts | 12 ++ .../pages/case/components/create/index.tsx | 77 ++++--- .../pages/case/components/create/schema.tsx | 6 +- .../description_md_editor/index.tsx | 111 ---------- .../pages/case/components/tag_list/index.tsx | 68 +++--- .../pages/case/components/tag_list/schema.tsx | 2 +- .../components/user_action_tree/index.tsx | 139 ++++++++++--- .../user_action_tree/user_action_avatar.tsx | 18 ++ .../user_action_tree/user_action_item.tsx | 60 ++++++ .../user_action_tree/user_action_markdown.tsx | 89 ++++++++ .../user_action_tree/user_action_title.tsx | 70 +++++++ .../pages/case/components/user_list/index.tsx | 4 +- .../siem/public/pages/case/translations.ts | 38 +++- .../rules/components/add_item_form/index.tsx | 2 +- .../components/description_step/index.tsx | 2 +- .../rules/components/mitre/index.tsx | 2 +- .../rules/components/pick_timeline/index.tsx | 2 +- .../rules/components/query_bar/index.tsx | 2 +- .../components/schedule_item_form/index.tsx | 2 +- .../components/step_about_rule/index.tsx | 2 +- .../components/step_about_rule/schema.tsx | 2 +- .../components/step_define_rule/index.tsx | 2 +- .../components/step_define_rule/schema.tsx | 2 +- .../components/step_schedule_rule/index.tsx | 2 +- .../components/step_schedule_rule/schema.tsx | 2 +- .../detection_engine/rules/create/index.tsx | 2 +- .../detection_engine/rules/edit/index.tsx | 2 +- .../pages/detection_engine/rules/helpers.tsx | 2 +- .../pages/detection_engine/rules/types.ts | 2 +- .../siem/public/pages/home/translations.ts | 2 +- .../siem/public/{pages => }/shared_imports.ts | 8 +- .../api/__tests__/update_comment.test.ts | 19 ++ .../plugins/case/server/routes/api/schema.ts | 5 + .../case/server/routes/api/update_comment.ts | 36 +++- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 61 files changed, 1355 insertions(+), 455 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/components/markdown_editor/constants.ts create mode 100644 x-pack/legacy/plugins/siem/public/components/markdown_editor/form.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/markdown_editor/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/markdown_editor/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx create mode 100644 x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/schema.tsx delete mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/description_md_editor/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_avatar.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_markdown.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx rename x-pack/legacy/plugins/siem/public/{pages => }/shared_imports.ts (52%) diff --git a/x-pack/legacy/plugins/siem/public/components/edit_data_provider/translations.ts b/x-pack/legacy/plugins/siem/public/components/edit_data_provider/translations.ts index dadd349096a53..53d2ffa197327 100644 --- a/x-pack/legacy/plugins/siem/public/components/edit_data_provider/translations.ts +++ b/x-pack/legacy/plugins/siem/public/components/edit_data_provider/translations.ts @@ -18,7 +18,7 @@ export const FIELD = i18n.translate('xpack.siem.editDataProvider.fieldLabel', { defaultMessage: 'Field', }); -export const FIELD_PLACEHOLDER = i18n.translate('xpack.siem.editDataProvider.fieldPlaceholder', { +export const FIELD_PLACEHOLDER = i18n.translate('xpack.siem.editDataProvider.placeholder', { defaultMessage: 'Select a field', }); diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx index f74ee995c965b..d100f89182014 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx @@ -125,3 +125,32 @@ export const FormattedRelativePreferenceDate = ({ value }: { value?: string | nu ); }; + +/** + * Renders a preceding label according to under/over one hour + */ + +export const FormattedRelativePreferenceLabel = ({ + value, + preferenceLabel, + relativeLabel, +}: { + value?: string | number | null; + preferenceLabel?: string | null; + relativeLabel?: string | null; +}) => { + if (value == null) { + return null; + } + const maybeDate = getMaybeDate(value); + if (!maybeDate.isValid()) { + return null; + } + return moment(maybeDate.toDate()) + .add(1, 'hours') + .isBefore(new Date()) ? ( + <>{preferenceLabel} + ) : ( + <>{relativeLabel} + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/editable_title.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/editable_title.test.tsx.snap index 30c70d7f5a2a6..24b1756aade2e 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/editable_title.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/editable_title.test.tsx.snap @@ -15,7 +15,7 @@ exports[`EditableTitle it renders 1`] = ` - css` margin-left: ${theme.eui.euiSize}; `} `; -StyledEuiButtonIcon.displayName = 'StyledEuiButtonIcon'; +const MySpinner = styled(EuiLoadingSpinner)` + ${({ theme }) => css` + margin-left: ${theme.eui.euiSize}; + `} +`; interface Props { isLoading: boolean; @@ -36,24 +41,30 @@ interface Props { const EditableTitleComponent: React.FC = ({ onSubmit, isLoading, title }) => { const [editMode, setEditMode] = useState(false); - const [changedTitle, onTitleChange] = useState(title); + const [changedTitle, onTitleChange] = useState(typeof title === 'string' ? title : ''); const onCancel = useCallback(() => setEditMode(false), []); const onClickEditIcon = useCallback(() => setEditMode(true), []); - const onClickSubmit = useCallback( - (newTitle: string): void => { - onSubmit(newTitle); - setEditMode(false); + const onClickSubmit = useCallback((): void => { + if (changedTitle !== title) { + onSubmit(changedTitle); + } + setEditMode(false); + }, [changedTitle, title]); + + const handleOnChange = useCallback( + (e: ChangeEvent) => { + onTitleChange(e.target.value); }, - [changedTitle] + [onTitleChange] ); return editMode ? ( onTitleChange(e.target.value)} + onChange={handleOnChange} value={`${changedTitle}`} data-test-subj="editable-title-input-field" /> @@ -61,17 +72,23 @@ const EditableTitleComponent: React.FC = ({ onSubmit, isLoading, title }) onClickSubmit(changedTitle as string)} + color="secondary" data-test-subj="editable-title-submit-btn" + fill + iconType="save" + onClick={onClickSubmit} + size="s" > - {i18n.SUBMIT} + {i18n.SAVE} - + {i18n.CANCEL} @@ -84,12 +101,15 @@ const EditableTitleComponent: React.FC = ({ onSubmit, isLoading, title }) </EuiFlexItem> <EuiFlexItem grow={false}> - <StyledEuiButtonIcon - aria-label={i18n.EDIT_TITLE_ARIA(title as string)} - iconType="pencil" - onClick={onClickEditIcon} - data-test-subj="editable-title-edit-icon" - /> + {isLoading && <MySpinner />} + {!isLoading && ( + <MyEuiButtonIcon + aria-label={i18n.EDIT_TITLE_ARIA(title as string)} + iconType="pencil" + onClick={onClickEditIcon} + data-test-subj="editable-title-edit-icon" + /> + )} </EuiFlexItem> </EuiFlexGroup> ); diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/translations.ts b/x-pack/legacy/plugins/siem/public/components/header_page/translations.ts index 2bc2ac492b0b1..764f1e5ac3731 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_page/translations.ts +++ b/x-pack/legacy/plugins/siem/public/components/header_page/translations.ts @@ -6,8 +6,8 @@ import { i18n } from '@kbn/i18n'; -export const SUBMIT = i18n.translate('xpack.siem.header.editableTitle.submit', { - defaultMessage: 'Submit', +export const SAVE = i18n.translate('xpack.siem.header.editableTitle.save', { + defaultMessage: 'Save', }); export const CANCEL = i18n.translate('xpack.siem.header.editableTitle.cancel', { diff --git a/x-pack/legacy/plugins/siem/public/components/markdown_editor/constants.ts b/x-pack/legacy/plugins/siem/public/components/markdown_editor/constants.ts new file mode 100644 index 0000000000000..dc57de5252b3e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/markdown_editor/constants.ts @@ -0,0 +1,7 @@ +/* + * 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 MARKDOWN_HELP_LINK = 'https://www.markdownguide.org/cheat-sheet/'; diff --git a/x-pack/legacy/plugins/siem/public/components/markdown_editor/form.tsx b/x-pack/legacy/plugins/siem/public/components/markdown_editor/form.tsx new file mode 100644 index 0000000000000..3c5287a6fac24 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/markdown_editor/form.tsx @@ -0,0 +1,58 @@ +/* + * 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 { EuiFormRow } from '@elastic/eui'; +import React, { useCallback } from 'react'; + +import { FieldHook, getFieldValidityAndErrorMessage } from '../../shared_imports'; +import { MarkdownEditor } from '.'; + +interface IMarkdownEditorForm { + dataTestSubj: string; + field: FieldHook; + idAria: string; + isDisabled: boolean; + placeholder?: string; + footerContentRight?: React.ReactNode; +} +export const MarkdownEditorForm = ({ + dataTestSubj, + field, + idAria, + isDisabled = false, + placeholder, + footerContentRight, +}: IMarkdownEditorForm) => { + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + const handleContentChange = useCallback( + (newContent: string) => { + field.setValue(newContent); + }, + [field] + ); + + return ( + <EuiFormRow + label={field.label} + labelAppend={field.labelAppend} + helpText={field.helpText} + error={errorMessage} + isInvalid={isInvalid} + fullWidth + data-test-subj={dataTestSubj} + describedByIds={idAria ? [idAria] : undefined} + > + <MarkdownEditor + initialContent={field.value as string} + isDisabled={isDisabled} + footerContentRight={footerContentRight} + onChange={handleContentChange} + placeholder={placeholder} + /> + </EuiFormRow> + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/components/markdown_editor/index.tsx b/x-pack/legacy/plugins/siem/public/components/markdown_editor/index.tsx new file mode 100644 index 0000000000000..8572b447cced8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/markdown_editor/index.tsx @@ -0,0 +1,121 @@ +/* + * 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 { + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiPanel, + EuiTabbedContent, + EuiTextArea, +} from '@elastic/eui'; +import React, { useEffect, useMemo, useState } from 'react'; +import styled, { css } from 'styled-components'; + +import { Markdown } from '../markdown'; +import * as i18n from './translations'; +import { MARKDOWN_HELP_LINK } from './constants'; + +const TextArea = styled(EuiTextArea)` + width: 100%; +`; + +const Container = styled(EuiPanel)` + ${({ theme }) => css` + padding: 0; + background: ${theme.eui.euiColorLightestShade}; + position: relative; + .euiTab { + padding: 10px; + } + .euiFormRow__labelWrapper { + position: absolute; + top: -${theme.eui.euiSizeL}; + } + .euiFormErrorText { + padding: 0 ${theme.eui.euiSizeM}; + } + `} +`; + +const Tabs = styled(EuiTabbedContent)` + width: 100%; +`; + +const Footer = styled(EuiFlexGroup)` + ${({ theme }) => css` + height: 41px; + padding: 0 ${theme.eui.euiSizeM}; + .euiLink { + font-size: ${theme.eui.euiSizeM}; + } + `} +`; + +const MarkdownContainer = styled(EuiPanel)` + min-height: 150px; + overflow: auto; +`; + +/** An input for entering a new case description */ +export const MarkdownEditor = React.memo<{ + placeholder?: string; + footerContentRight?: React.ReactNode; + initialContent: string; + isDisabled?: boolean; + onChange: (description: string) => void; +}>(({ placeholder, footerContentRight, initialContent, isDisabled = false, onChange }) => { + const [content, setContent] = useState(initialContent); + useEffect(() => { + onChange(content); + }, [content]); + const tabs = useMemo( + () => [ + { + id: 'comment', + name: i18n.MARKDOWN, + content: ( + <TextArea + onChange={e => { + setContent(e.target.value); + }} + aria-label={`markdown-editor-comment`} + fullWidth={true} + disabled={isDisabled} + placeholder={placeholder ?? ''} + spellCheck={false} + value={content} + /> + ), + }, + { + id: 'preview', + name: i18n.PREVIEW, + content: ( + <MarkdownContainer data-test-subj="markdown-container" paddingSize="s"> + <Markdown raw={content} /> + </MarkdownContainer> + ), + }, + ], + [content, isDisabled, placeholder] + ); + return ( + <Container> + <Tabs data-test-subj={`markdown-tabs`} size="s" tabs={tabs} initialSelectedTab={tabs[0]} /> + <Footer alignItems="center" gutterSize="none" justifyContent="spaceBetween"> + <EuiFlexItem grow={false}> + <EuiLink href={MARKDOWN_HELP_LINK} external target="_blank"> + {i18n.MARKDOWN_SYNTAX_HELP} + </EuiLink> + </EuiFlexItem> + {footerContentRight && <EuiFlexItem grow={false}>{footerContentRight}</EuiFlexItem>} + </Footer> + </Container> + ); +}); + +MarkdownEditor.displayName = 'MarkdownEditor'; diff --git a/x-pack/legacy/plugins/siem/public/components/markdown_editor/translations.ts b/x-pack/legacy/plugins/siem/public/components/markdown_editor/translations.ts new file mode 100644 index 0000000000000..642c524c48be0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/markdown_editor/translations.ts @@ -0,0 +1,18 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const MARKDOWN_SYNTAX_HELP = i18n.translate('xpack.siem.markdownEditor.markdownInputHelp', { + defaultMessage: 'Markdown syntax help', +}); + +export const MARKDOWN = i18n.translate('xpack.siem.markdownEditor.markdown', { + defaultMessage: 'Markdown', +}); +export const PREVIEW = i18n.translate('xpack.siem.markdownEditor.preview', { + defaultMessage: 'Preview', +}); diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx index e1b3951a2317d..a821d310344d8 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx @@ -70,7 +70,7 @@ describe('SIEM Navigation', () => { disabled: false, href: '#/link-to/case', id: 'case', - name: 'Case', + name: 'Cases', urlKey: 'case', }, detections: { @@ -163,7 +163,7 @@ describe('SIEM Navigation', () => { disabled: false, href: '#/link-to/case', id: 'case', - name: 'Case', + name: 'Cases', urlKey: 'case', }, detections: { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/api.ts index bff3bfd62a85c..f1d87ca58b44b 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/api.ts @@ -5,12 +5,22 @@ */ import { KibanaServices } from '../../lib/kibana'; -import { FetchCasesProps, Case, NewCase, SortFieldCase, AllCases, CaseSnake } from './types'; +import { + AllCases, + Case, + CaseSnake, + Comment, + CommentSnake, + FetchCasesProps, + NewCase, + NewComment, + SortFieldCase, +} from './types'; import { throwIfNotOk } from '../../hooks/api/api'; import { CASES_URL } from './constants'; import { convertToCamelCase, convertAllCasesToCamel } from './utils'; -export const getCase = async (caseId: string, includeComments: boolean): Promise<Case> => { +export const getCase = async (caseId: string, includeComments: boolean = true): Promise<Case> => { const response = await KibanaServices.get().http.fetch(`${CASES_URL}/${caseId}`, { method: 'GET', asResponse: true, @@ -72,3 +82,27 @@ export const updateCaseProperty = async ( await throwIfNotOk(response.response); return convertToCamelCase<Partial<CaseSnake>, Partial<Case>>(response.body!); }; + +export const createComment = async (newComment: NewComment, caseId: string): Promise<Comment> => { + const response = await KibanaServices.get().http.fetch(`${CASES_URL}/${caseId}/comment`, { + method: 'POST', + asResponse: true, + body: JSON.stringify(newComment), + }); + await throwIfNotOk(response.response); + return convertToCamelCase<CommentSnake, Comment>(response.body!); +}; + +export const updateComment = async ( + commentId: string, + commentUpdate: string, + version: string +): Promise<Partial<Comment>> => { + const response = await KibanaServices.get().http.fetch(`${CASES_URL}/comment/${commentId}`, { + method: 'PATCH', + asResponse: true, + body: JSON.stringify({ comment: commentUpdate, version }), + }); + await throwIfNotOk(response.response); + return convertToCamelCase<Partial<CommentSnake>, Partial<Comment>>(response.body!); +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts index c8d668527ae32..031ba1c128a24 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts @@ -11,6 +11,6 @@ export const FETCH_FAILURE = 'FETCH_FAILURE'; export const FETCH_INIT = 'FETCH_INIT'; export const FETCH_SUCCESS = 'FETCH_SUCCESS'; export const POST_NEW_CASE = 'POST_NEW_CASE'; -export const UPDATE_CASE_PROPERTY = 'UPDATE_CASE_PROPERTY'; +export const POST_NEW_COMMENT = 'POST_NEW_COMMENT'; export const UPDATE_FILTER_OPTIONS = 'UPDATE_FILTER_OPTIONS'; export const UPDATE_QUERY_PARAMS = 'UPDATE_QUERY_PARAMS'; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts index 1aea0b0f50a89..75ed6f7c2366d 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -14,8 +14,31 @@ export interface NewCase extends FormData { title: string; } +export interface NewComment extends FormData { + comment: string; +} + +export interface CommentSnake { + comment_id: string; + created_at: string; + created_by: ElasticUserSnake; + comment: string; + updated_at: string; + version: string; +} + +export interface Comment { + commentId: string; + createdAt: string; + createdBy: ElasticUser; + comment: string; + updatedAt: string; + version: string; +} + export interface CaseSnake { case_id: string; + comments: CommentSnake[]; created_at: string; created_by: ElasticUserSnake; description: string; @@ -23,11 +46,12 @@ export interface CaseSnake { tags: string[]; title: string; updated_at: string; - version?: string; + version: string; } export interface Case { caseId: string; + comments: Comment[]; createdAt: string; createdBy: ElasticUser; description: string; @@ -35,7 +59,7 @@ export interface Case { tags: string[]; title: string; updatedAt: string; - version?: string; + version: string; } export interface QueryParams { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx index bf76b69ef22d6..ce71c26078db9 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx @@ -52,6 +52,7 @@ const dataFetchReducer = (state: CaseState, action: Action): CaseState => { const initialData: Case = { caseId: '', createdAt: '', + comments: [], createdBy: { username: '', }, @@ -60,6 +61,7 @@ const initialData: Case = { tags: [], title: '', updatedAt: '', + version: '', }; export const useGetCase = (caseId: string): [CaseState] => { @@ -75,7 +77,7 @@ export const useGetCase = (caseId: string): [CaseState] => { const fetchData = async () => { dispatch({ type: FETCH_INIT }); try { - const response = await getCase(caseId, false); + const response = await getCase(caseId); if (!didCancel) { dispatch({ type: FETCH_SUCCESS, payload: response }); } diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx index 5cf99701977d2..0fcc8a3a1abec 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx @@ -80,8 +80,7 @@ export const usePostCase = (): [NewCaseState, Dispatch<SetStateAction<NewCase>>] const postCase = async () => { dispatch({ type: FETCH_INIT }); try { - const dataWithoutIsNew = state.data; - delete dataWithoutIsNew.isNew; + const { isNew, ...dataWithoutIsNew } = state.data; const response = await createCase(dataWithoutIsNew); dispatch({ type: FETCH_SUCCESS, payload: response }); } catch (error) { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx new file mode 100644 index 0000000000000..d8abda25af286 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx @@ -0,0 +1,97 @@ +/* + * 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, SetStateAction, useEffect, useReducer, useState } from 'react'; +import { useStateToaster } from '../../components/toasters'; +import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import * as i18n from './translations'; +import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS, POST_NEW_COMMENT } from './constants'; +import { Comment, NewComment } from './types'; +import { createComment } from './api'; +import { getTypedPayload } from './utils'; + +interface NewCommentState { + data: NewComment; + newComment?: Comment; + isLoading: boolean; + isError: boolean; + caseId: string; +} +interface Action { + type: string; + payload?: NewComment | Comment; +} + +const dataFetchReducer = (state: NewCommentState, action: Action): NewCommentState => { + switch (action.type) { + case FETCH_INIT: + return { + ...state, + isLoading: true, + isError: false, + }; + case POST_NEW_COMMENT: + return { + ...state, + isLoading: false, + isError: false, + data: getTypedPayload<NewComment>(action.payload), + }; + case FETCH_SUCCESS: + return { + ...state, + isLoading: false, + isError: false, + newComment: getTypedPayload<Comment>(action.payload), + }; + case FETCH_FAILURE: + return { + ...state, + isLoading: false, + isError: true, + }; + default: + throw new Error(); + } +}; +const initialData: NewComment = { + comment: '', +}; + +export const usePostComment = ( + caseId: string +): [NewCommentState, Dispatch<SetStateAction<NewComment>>] => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isLoading: false, + isError: false, + caseId, + data: initialData, + }); + const [formData, setFormData] = useState(initialData); + const [, dispatchToaster] = useStateToaster(); + + useEffect(() => { + dispatch({ type: POST_NEW_COMMENT, payload: formData }); + }, [formData]); + + useEffect(() => { + const postComment = async () => { + dispatch({ type: FETCH_INIT }); + try { + const { isNew, ...dataWithoutIsNew } = state.data; + const response = await createComment(dataWithoutIsNew, state.caseId); + dispatch({ type: FETCH_SUCCESS, payload: response }); + } catch (error) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: FETCH_FAILURE }); + } + }; + if (state.data.isNew) { + postComment(); + } + }, [state.data.isNew]); + return [state, setFormData]; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx index 62e3d87b528c0..ebbb1e14dc237 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect, useReducer } from 'react'; +import { useReducer } from 'react'; import { useStateToaster } from '../../components/toasters'; import { errorToToaster } from '../../components/ml/api/error_to_toaster'; import * as i18n from './translations'; -import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS, UPDATE_CASE_PROPERTY } from './constants'; +import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS } from './constants'; import { Case } from './types'; import { updateCaseProperty } from './api'; import { getTypedPayload } from './utils'; @@ -19,7 +19,7 @@ interface NewCaseState { data: Case; isLoading: boolean; isError: boolean; - updateKey?: UpdateKey | null; + updateKey: UpdateKey | null; } interface UpdateByKey { @@ -29,7 +29,7 @@ interface UpdateByKey { interface Action { type: string; - payload?: Partial<Case> | UpdateByKey; + payload?: Partial<Case> | UpdateKey; } const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => { @@ -39,20 +39,9 @@ const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => ...state, isLoading: true, isError: false, - updateKey: null, - }; - case UPDATE_CASE_PROPERTY: - const { updateKey, updateValue } = getTypedPayload<UpdateByKey>(action.payload); - return { - ...state, - isLoading: false, - isError: false, - data: { - ...state.data, - [updateKey]: updateValue, - }, - updateKey, + updateKey: getTypedPayload<UpdateKey>(action.payload), }; + case FETCH_SUCCESS: return { ...state, @@ -62,12 +51,14 @@ const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => ...state.data, ...getTypedPayload<Case>(action.payload), }, + updateKey: null, }; case FETCH_FAILURE: return { ...state, isLoading: false, isError: true, + updateKey: null, }; default: throw new Error(); @@ -77,40 +68,29 @@ const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => export const useUpdateCase = ( caseId: string, initialData: Case -): [{ data: Case }, (updates: UpdateByKey) => void] => { +): [NewCaseState, (updates: UpdateByKey) => void] => { const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, data: initialData, + updateKey: null, }); const [, dispatchToaster] = useStateToaster(); - const dispatchUpdateCaseProperty = ({ updateKey, updateValue }: UpdateByKey) => { - dispatch({ - type: UPDATE_CASE_PROPERTY, - payload: { updateKey, updateValue }, - }); - }; - - useEffect(() => { - const updateData = async (updateKey: keyof Case) => { - dispatch({ type: FETCH_INIT }); - try { - const response = await updateCaseProperty( - caseId, - { [updateKey]: state.data[updateKey] }, - state.data.version ?? '' // saved object versions are typed as string | undefined, hope that's not true - ); - dispatch({ type: FETCH_SUCCESS, payload: response }); - } catch (error) { - errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); - dispatch({ type: FETCH_FAILURE }); - } - }; - if (state.updateKey) { - updateData(state.updateKey); + const dispatchUpdateCaseProperty = async ({ updateKey, updateValue }: UpdateByKey) => { + dispatch({ type: FETCH_INIT, payload: updateKey }); + try { + const response = await updateCaseProperty( + caseId, + { [updateKey]: updateValue }, + state.data.version ?? '' // saved object versions are typed as string | undefined, hope that's not true + ); + dispatch({ type: FETCH_SUCCESS, payload: response }); + } catch (error) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: FETCH_FAILURE }); } - }, [state.updateKey]); + }; - return [{ data: state.data }, dispatchUpdateCaseProperty]; + return [state, dispatchUpdateCaseProperty]; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx new file mode 100644 index 0000000000000..bc8369117433a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx @@ -0,0 +1,92 @@ +/* + * 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 { useReducer, useRef } from 'react'; +import { useStateToaster } from '../../components/toasters'; +import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import * as i18n from './translations'; +import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS } from './constants'; +import { Comment } from './types'; +import { updateComment } from './api'; +import { getTypedPayload } from './utils'; + +interface CommetUpdateState { + data: Comment[]; + isLoadingIds: string[]; + isError: boolean; +} + +interface CommentUpdate { + update: Partial<Comment>; + commentId: string; +} + +interface Action { + type: string; + payload?: CommentUpdate | string; +} + +const dataFetchReducer = (state: CommetUpdateState, action: Action): CommetUpdateState => { + switch (action.type) { + case FETCH_INIT: + return { + ...state, + isLoadingIds: [...state.isLoadingIds, getTypedPayload<string>(action.payload)], + isError: false, + }; + + case FETCH_SUCCESS: + const updatePayload = getTypedPayload<CommentUpdate>(action.payload); + const foundIndex = state.data.findIndex( + comment => comment.commentId === updatePayload.commentId + ); + state.data[foundIndex] = { ...state.data[foundIndex], ...updatePayload.update }; + return { + ...state, + isLoadingIds: state.isLoadingIds.filter(id => updatePayload.commentId !== id), + isError: false, + data: [...state.data], + }; + case FETCH_FAILURE: + return { + ...state, + isLoadingIds: state.isLoadingIds.filter( + id => getTypedPayload<string>(action.payload) !== id + ), + isError: true, + }; + default: + throw new Error(); + } +}; + +export const useUpdateComment = ( + comments: Comment[] +): [CommetUpdateState, (commentId: string, commentUpdate: string) => void] => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isLoadingIds: [], + isError: false, + data: comments, + }); + const dispatchUpdateComment = useRef<(commentId: string, commentUpdate: string) => void>(); + const [, dispatchToaster] = useStateToaster(); + + dispatchUpdateComment.current = async (commentId: string, commentUpdate: string) => { + dispatch({ type: FETCH_INIT, payload: commentId }); + try { + const currentComment = state.data.find(comment => comment.commentId === commentId) ?? { + version: '', + }; + const response = await updateComment(commentId, commentUpdate, currentComment.version); + dispatch({ type: FETCH_SUCCESS, payload: { update: response, commentId } }); + } catch (error) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: FETCH_FAILURE, payload: commentId }); + } + }; + + return [state, dispatchUpdateComment.current]; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx new file mode 100644 index 0000000000000..c8e0dafcf5742 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx @@ -0,0 +1,73 @@ +/* + * 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, { useCallback } from 'react'; +import { EuiButton, EuiLoadingSpinner } from '@elastic/eui'; +import styled from 'styled-components'; +import { Form, useForm, UseField } from '../../../../shared_imports'; +import { NewComment } from '../../../../containers/case/types'; +import { usePostComment } from '../../../../containers/case/use_post_comment'; +import { schema } from './schema'; +import * as i18n from '../../translations'; +import { MarkdownEditorForm } from '../../../../components/markdown_editor/form'; + +const MySpinner = styled(EuiLoadingSpinner)` + position: absolute; + top: 50%; + left: 50%; +`; + +export const AddComment = React.memo<{ + caseId: string; +}>(({ caseId }) => { + const [{ data, isLoading, newComment }, setFormData] = usePostComment(caseId); + const { form } = useForm({ + defaultValue: data, + options: { stripEmptyFields: false }, + schema, + }); + + const onSubmit = useCallback(async () => { + const { isValid, data: newData } = await form.submit(); + if (isValid && newData.comment) { + setFormData({ ...newData, isNew: true } as NewComment); + } else if (isValid && data.comment) { + setFormData({ ...data, ...newData, isNew: true } as NewComment); + } + }, [form, data]); + + return ( + <> + {isLoading && <MySpinner size="xl" />} + <Form form={form}> + <UseField + path="comment" + component={MarkdownEditorForm} + componentProps={{ + idAria: 'caseComment', + isDisabled: isLoading, + dataTestSubj: 'caseComment', + placeholder: i18n.ADD_COMMENT_HELP_TEXT, + footerContentRight: ( + <EuiButton + iconType="plusInCircle" + isDisabled={isLoading} + isLoading={isLoading} + onClick={onSubmit} + size="s" + > + {i18n.ADD_COMMENT} + </EuiButton> + ), + }} + /> + </Form> + {newComment && + 'TO DO new comment got added but we didnt update the UI yet. Refresh the page to see your comment ;)'} + </> + ); +}); + +AddComment.displayName = 'AddComment'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/schema.tsx new file mode 100644 index 0000000000000..5f30f59149d99 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/schema.tsx @@ -0,0 +1,20 @@ +/* + * 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 { FIELD_TYPES, fieldValidators, FormSchema } from '../../../../shared_imports'; +import * as i18n from '../../translations'; + +const { emptyField } = fieldValidators; + +export const schema: FormSchema = { + comment: { + type: FIELD_TYPES.TEXTAREA, + validations: [ + { + validator: emptyField(i18n.COMMENT_REQUIRED), + }, + ], + }, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx index 98a67304fcf1f..0169493773b74 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx @@ -14,51 +14,61 @@ export const useGetCasesMockState: UseGetCasesState = { caseId: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:23.627Z', createdBy: { username: 'elastic' }, + comments: [], description: 'Security banana Issue', state: 'open', tags: ['defacement'], title: 'Another horrible breach', updatedAt: '2020-02-13T19:44:23.627Z', + version: 'WzQ3LDFd', }, { caseId: '362a5c10-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:13.328Z', createdBy: { username: 'elastic' }, + comments: [], description: 'Security banana Issue', state: 'open', tags: ['phishing'], title: 'Bad email', updatedAt: '2020-02-13T19:44:13.328Z', + version: 'WzQ3LDFd', }, { caseId: '34f8b9e0-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:11.328Z', createdBy: { username: 'elastic' }, + comments: [], description: 'Security banana Issue', state: 'open', tags: ['phishing'], title: 'Bad email', updatedAt: '2020-02-13T19:44:11.328Z', + version: 'WzQ3LDFd', }, { caseId: '31890e90-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:05.563Z', createdBy: { username: 'elastic' }, + comments: [], description: 'Security banana Issue', state: 'closed', tags: ['phishing'], title: 'Uh oh', updatedAt: '2020-02-18T21:32:24.056Z', + version: 'WzQ3LDFd', }, { caseId: '2f5b3210-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:01.901Z', createdBy: { username: 'elastic' }, + comments: [], description: 'Security banana Issue', state: 'open', tags: ['phishing'], title: 'Uh oh', updatedAt: '2020-02-13T19:44:01.901Z', + version: 'WzQ3LDFd', }, ], page: 1, diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx index 4c47bf605051d..9c276d1b24da1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx @@ -19,7 +19,7 @@ const renderStringField = (field: string, dataTestSubj: string) => export const getCasesColumns = (): CasesColumns[] => [ { - name: i18n.CASE_TITLE, + name: i18n.NAME, render: (theCase: Case) => { if (theCase.caseId != null && theCase.title != null) { return <CaseDetailsLink detailName={theCase.caseId}>{theCase.title}</CaseDetailsLink>; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx index 7480c4fc4bb2a..89d321c6d106a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx @@ -11,6 +11,19 @@ export const caseProps: CaseProps = { caseId: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', initialData: { caseId: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', + comments: [ + { + comment: 'Solve this fast!', + commentId: 'a357c6a0-5435-11ea-b427-fb51a1fcb7b8', + createdAt: '2020-02-20T23:06:33.798Z', + createdBy: { + fullName: 'Steph Milovic', + username: 'smilovic', + }, + updatedAt: '2020-02-20T23:06:33.798Z', + version: 'WzQ3LDFd', + }, + ], createdAt: '2020-02-13T19:44:23.627Z', createdBy: { fullName: null, username: 'elastic' }, description: 'Security banana Issue', @@ -18,12 +31,25 @@ export const caseProps: CaseProps = { tags: ['defacement'], title: 'Another horrible breach!!', updatedAt: '2020-02-19T15:02:57.995Z', + version: 'WzQ3LDFd', }, - isLoading: false, }; export const data: Case = { caseId: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', + comments: [ + { + comment: 'Solve this fast!', + commentId: 'a357c6a0-5435-11ea-b427-fb51a1fcb7b8', + createdAt: '2020-02-20T23:06:33.798Z', + createdBy: { + fullName: 'Steph Milovic', + username: 'smilovic', + }, + updatedAt: '2020-02-20T23:06:33.798Z', + version: 'WzQ3LDFd', + }, + ], createdAt: '2020-02-13T19:44:23.627Z', createdBy: { username: 'elastic', fullName: null }, description: 'Security banana Issue', @@ -31,4 +57,5 @@ export const data: Case = { tags: ['defacement'], title: 'Another horrible breach!!', updatedAt: '2020-02-19T15:02:57.995Z', + version: 'WzQ3LDFd', }; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx index a9e694bad705d..1539b3de5a0c1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx @@ -16,7 +16,12 @@ describe('CaseView ', () => { beforeEach(() => { jest.resetAllMocks(); - jest.spyOn(apiHook, 'useUpdateCase').mockReturnValue([{ data }, dispatchUpdateCaseProperty]); + jest + .spyOn(apiHook, 'useUpdateCase') + .mockReturnValue([ + { data, isLoading: false, isError: false, updateKey: null }, + dispatchUpdateCaseProperty, + ]); }); it('should render CaseComponent', () => { @@ -79,4 +84,38 @@ describe('CaseView ', () => { updateValue: 'closed', }); }); + + it('should render comments', () => { + const wrapper = mount( + <TestProviders> + <CaseComponent {...caseProps} /> + </TestProviders> + ); + expect( + wrapper + .find( + `div[data-test-subj="user-action-${data.comments[0].commentId}-avatar"] [data-test-subj="user-action-avatar"]` + ) + .first() + .prop('name') + ).toEqual(data.comments[0].createdBy.fullName); + + expect( + wrapper + .find( + `div[data-test-subj="user-action-${data.comments[0].commentId}"] [data-test-subj="user-action-title"] strong` + ) + .first() + .text() + ).toEqual(data.comments[0].createdBy.username); + + expect( + wrapper + .find( + `div[data-test-subj="user-action-${data.comments[0].commentId}"] [data-test-subj="markdown"]` + ) + .first() + .prop('source') + ).toEqual(data.comments[0].comment); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx index df3e30a698b56..605f9e8fa1713 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback } from 'react'; import { EuiBadge, - EuiButton, - EuiButtonEmpty, EuiButtonToggle, EuiDescriptionList, EuiDescriptionListDescription, @@ -20,13 +18,11 @@ import { import styled, { css } from 'styled-components'; import * as i18n from './translations'; -import { DescriptionMarkdown } from '../description_md_editor'; import { Case } from '../../../../containers/case/types'; import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; import { getCaseUrl } from '../../../../components/link_to'; import { HeaderPage } from '../../../../components/header_page'; import { EditableTitle } from '../../../../components/header_page/editable_title'; -import { Markdown } from '../../../../components/markdown'; import { PropertyActions } from '../property_actions'; import { TagList } from '../tag_list'; import { useGetCase } from '../../../../containers/case/use_get_case'; @@ -34,6 +30,7 @@ import { UserActionTree } from '../user_action_tree'; import { UserList } from '../user_list'; import { useUpdateCase } from '../../../../containers/case/use_update_case'; import { WrapperPage } from '../../../../components/wrapper_page'; +import { getTypedPayload } from '../../../../containers/case/utils'; import { WhitePageWrapper } from '../wrappers'; interface Props { @@ -53,95 +50,71 @@ const MyWrapper = styled(WrapperPage)` padding-bottom: 0; `; +const MyEuiFlexGroup = styled(EuiFlexGroup)` + height: 100%; +`; + export interface CaseProps { caseId: string; initialData: Case; - isLoading: boolean; } -export const CaseComponent = React.memo<CaseProps>(({ caseId, initialData, isLoading }) => { - const [{ data }, dispatchUpdateCaseProperty] = useUpdateCase(caseId, initialData); - const [isEditDescription, setIsEditDescription] = useState(false); - const [isEditTags, setIsEditTags] = useState(false); - const [isCaseOpen, setIsCaseOpen] = useState(data.state === 'open'); - const [description, setDescription] = useState(data.description); - const [title, setTitle] = useState(data.title); - const [tags, setTags] = useState(data.tags); +export const CaseComponent = React.memo<CaseProps>(({ caseId, initialData }) => { + const [{ data, isLoading, updateKey }, dispatchUpdateCaseProperty] = useUpdateCase( + caseId, + initialData + ); const onUpdateField = useCallback( - async (updateKey: keyof Case, updateValue: string | string[]) => { - switch (updateKey) { + (newUpdateKey: keyof Case, updateValue: Case[keyof Case]) => { + switch (newUpdateKey) { case 'title': - if (updateValue.length > 0) { + const titleUpdate = getTypedPayload<string>(updateValue); + if (titleUpdate.length > 0) { dispatchUpdateCaseProperty({ updateKey: 'title', - updateValue, + updateValue: titleUpdate, }); } break; case 'description': - if (updateValue.length > 0) { + const descriptionUpdate = getTypedPayload<string>(updateValue); + if (descriptionUpdate.length > 0) { dispatchUpdateCaseProperty({ updateKey: 'description', - updateValue, + updateValue: descriptionUpdate, }); - setIsEditDescription(false); } break; case 'tags': - setTags(updateValue as string[]); - if (updateValue.length > 0) { + const tagsUpdate = getTypedPayload<string[]>(updateValue); + dispatchUpdateCaseProperty({ + updateKey: 'tags', + updateValue: tagsUpdate, + }); + break; + case 'state': + const stateUpdate = getTypedPayload<string>(updateValue); + if (data.state !== updateValue) { dispatchUpdateCaseProperty({ - updateKey: 'tags', - updateValue, + updateKey: 'state', + updateValue: stateUpdate, }); - setIsEditTags(false); } - break; default: return null; } }, - [dispatchUpdateCaseProperty, title] + [dispatchUpdateCaseProperty, data.state] ); - const onSetIsCaseOpen = useCallback(() => setIsCaseOpen(!isCaseOpen), [ - isCaseOpen, - setIsCaseOpen, - ]); - - useEffect(() => { - const caseState = isCaseOpen ? 'open' : 'closed'; - if (data.state !== caseState) { - dispatchUpdateCaseProperty({ - updateKey: 'state', - updateValue: caseState, - }); - } - }, [isCaseOpen]); - // TO DO refactor each of these const's into their own components const propertyActions = [ - { - iconType: 'documentEdit', - label: 'Edit description', - onClick: () => setIsEditDescription(true), - }, - { - iconType: 'securitySignalResolved', - label: 'Close case', - onClick: () => null, - }, { iconType: 'trash', label: 'Delete case', onClick: () => null, }, - { - iconType: 'importAction', - label: 'Push as ServiceNow incident', - onClick: () => null, - }, { iconType: 'popout', label: 'View ServiceNow incident', @@ -153,66 +126,13 @@ export const CaseComponent = React.memo<CaseProps>(({ caseId, initialData, isLoa onClick: () => null, }, ]; - const userActions = [ - { - avatarName: data.createdBy.username, - title: ( - <EuiFlexGroup alignItems="baseline" gutterSize="none" justifyContent="spaceBetween"> - <EuiFlexItem grow={false}> - <p> - <strong>{`${data.createdBy.username}`}</strong> - {` ${i18n.ADDED_DESCRIPTION} `}{' '} - <FormattedRelativePreferenceDate value={data.createdAt} /> - {/* STEPH FIX come back and add label `on` */} - </p> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <PropertyActions propertyActions={propertyActions} /> - </EuiFlexItem> - </EuiFlexGroup> - ), - children: isEditDescription ? ( - <> - <DescriptionMarkdown - descriptionInputHeight={200} - initialDescription={data.description} - isLoading={isLoading} - onChange={updatedDescription => setDescription(updatedDescription)} - /> - <EuiFlexGroup alignItems="center" gutterSize="s" responsive={false} wrap={true}> - <EuiFlexItem grow={false}> - <EuiButton - fill - isDisabled={isLoading} - isLoading={isLoading} - onClick={() => onUpdateField('description', description)} - > - {i18n.SUBMIT} - </EuiButton> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButtonEmpty onClick={() => setIsEditDescription(false)}> - {i18n.CANCEL} - </EuiButtonEmpty> - </EuiFlexItem> - </EuiFlexGroup> - </> - ) : ( - <Markdown raw={data.description} data-test-subj="case-view-description" /> - ), - }, - ]; - - const onSubmit = useCallback( - newTitle => { - onUpdateField('title', newTitle); - setTitle(newTitle); - }, - [title] + const onSubmit = useCallback(newTitle => onUpdateField('title', newTitle), [onUpdateField]); + const toggleStateCase = useCallback( + e => onUpdateField('state', e.target.checked ? 'open' : 'closed'), + [onUpdateField] ); - - const titleNode = <EditableTitle isLoading={isLoading} title={title} onSubmit={onSubmit} />; + const onSubmitTags = useCallback(newTags => onUpdateField('tags', newTags), [onUpdateField]); return ( <> @@ -223,8 +143,14 @@ export const CaseComponent = React.memo<CaseProps>(({ caseId, initialData, isLoa text: i18n.BACK_TO_ALL, }} data-test-subj="case-view-title" - titleNode={titleNode} - title={title} + titleNode={ + <EditableTitle + isLoading={isLoading && updateKey === 'title'} + title={data.title} + onSubmit={onSubmit} + /> + } + title={data.title} > <EuiFlexGroup gutterSize="l" justifyContent="flexEnd"> <EuiFlexItem grow={false}> @@ -234,7 +160,7 @@ export const CaseComponent = React.memo<CaseProps>(({ caseId, initialData, isLoa <EuiDescriptionListTitle>{i18n.STATUS}</EuiDescriptionListTitle> <EuiDescriptionListDescription> <EuiBadge - color={isCaseOpen ? 'secondary' : 'danger'} + color={data.state === 'open' ? 'secondary' : 'danger'} data-test-subj="case-view-state" > {data.state} @@ -258,10 +184,11 @@ export const CaseComponent = React.memo<CaseProps>(({ caseId, initialData, isLoa <EuiFlexItem> <EuiButtonToggle data-test-subj="toggle-case-state" - label={isCaseOpen ? 'Close case' : 'Reopen case'} - iconType={isCaseOpen ? 'checkInCircleFilled' : 'magnet'} - onChange={onSetIsCaseOpen} - isSelected={isCaseOpen} + iconType={data.state === 'open' ? 'checkInCircleFilled' : 'magnet'} + isLoading={isLoading && updateKey === 'state'} + isSelected={data.state === 'open'} + label={data.state === 'open' ? 'Close case' : 'Reopen case'} + onChange={toggleStateCase} /> </EuiFlexItem> <EuiFlexItem grow={false}> @@ -276,7 +203,11 @@ export const CaseComponent = React.memo<CaseProps>(({ caseId, initialData, isLoa <MyWrapper> <EuiFlexGroup> <EuiFlexItem grow={6}> - <UserActionTree userActions={userActions} /> + <UserActionTree + data={data} + isLoadingDescription={isLoading && updateKey === 'description'} + onUpdateField={onUpdateField} + /> </EuiFlexItem> <EuiFlexItem grow={2}> <UserList @@ -286,14 +217,9 @@ export const CaseComponent = React.memo<CaseProps>(({ caseId, initialData, isLoa /> <TagList data-test-subj="case-view-tag-list" - tags={tags} - iconAction={{ - 'aria-label': title, - iconType: 'pencil', - onSubmit: newTags => onUpdateField('tags', newTags), - onClick: isEdit => setIsEditTags(isEdit), - }} - isEditTags={isEditTags} + tags={data.tags} + onSubmit={onSubmitTags} + isLoading={isLoading && updateKey === 'tags'} /> </EuiFlexItem> </EuiFlexGroup> @@ -310,15 +236,15 @@ export const CaseView = React.memo(({ caseId }: Props) => { } if (isLoading) { return ( - <EuiFlexGroup justifyContent="center" alignItems="center"> + <MyEuiFlexGroup justifyContent="center" alignItems="center"> <EuiFlexItem grow={false}> <EuiLoadingSpinner size="xl" /> </EuiFlexItem> - </EuiFlexGroup> + </MyEuiFlexGroup> ); } - return <CaseComponent caseId={caseId} initialData={data} isLoading={isLoading} />; + return <CaseComponent caseId={caseId} initialData={data} />; }); CaseView.displayName = 'CaseView'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts index f45c52533d2e7..82b5e771e2151 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts @@ -32,6 +32,18 @@ export const EDITED_DESCRIPTION = i18n.translate( } ); +export const EDIT_DESCRIPTION = i18n.translate('xpack.siem.case.caseView.edit.description', { + defaultMessage: 'Edit description', +}); + +export const EDIT_COMMENT = i18n.translate('xpack.siem.case.caseView.edit.comment', { + defaultMessage: 'Edit comment', +}); + +export const ON = i18n.translate('xpack.siem.case.caseView.actionLabel.on', { + defaultMessage: 'on', +}); + export const ADDED_COMMENT = i18n.translate('xpack.siem.case.caseView.actionLabel.addComment', { defaultMessage: 'added comment', }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx index 7d79e287b22e7..65d7256fd6e20 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx @@ -3,38 +3,48 @@ * 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, { useCallback } from 'react'; +import React, { useCallback, useState } from 'react'; import { EuiButton, + EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, - EuiHorizontalRule, EuiLoadingSpinner, EuiPanel, } from '@elastic/eui'; -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import { Redirect } from 'react-router-dom'; -import { Field, Form, getUseField, useForm } from '../../../shared_imports'; +import { Field, Form, getUseField, useForm, UseField } from '../../../../shared_imports'; import { NewCase } from '../../../../containers/case/types'; import { usePostCase } from '../../../../containers/case/use_post_case'; import { schema } from './schema'; import * as i18n from '../../translations'; import { SiemPageName } from '../../../home/types'; -import { DescriptionMarkdown } from '../description_md_editor'; +import { MarkdownEditorForm } from '../../../../components/markdown_editor/form'; export const CommonUseField = getUseField({ component: Field }); -const TagContainer = styled.div` - margin-top: 16px; +const ContainerBig = styled.div` + ${({ theme }) => css` + margin-top: ${theme.eui.euiSizeXL}; + `} +`; + +const Container = styled.div` + ${({ theme }) => css` + margin-top: ${theme.eui.euiSize}; + `} `; const MySpinner = styled(EuiLoadingSpinner)` position: absolute; top: 50%; left: 50%; + z-index: 99; `; export const Create = React.memo(() => { const [{ data, isLoading, newCase }, setFormData] = usePostCase(); + const [isCancel, setIsCancel] = useState(false); const { form } = useForm({ defaultValue: data, options: { stripEmptyFields: false }, @@ -43,14 +53,19 @@ export const Create = React.memo(() => { const onSubmit = useCallback(async () => { const { isValid, data: newData } = await form.submit(); - if (isValid) { + if (isValid && newData.description) { setFormData({ ...newData, isNew: true } as NewCase); + } else if (isValid && data.description) { + setFormData({ ...data, ...newData, isNew: true } as NewCase); } - }, [form]); + }, [form, data]); if (newCase && newCase.caseId) { return <Redirect to={`/${SiemPageName.case}/${newCase.caseId}`} />; } + if (isCancel) { + return <Redirect to={`/${SiemPageName.case}`} />; + } return ( <EuiPanel> {isLoading && <MySpinner size="xl" />} @@ -62,18 +77,11 @@ export const Create = React.memo(() => { 'data-test-subj': 'caseTitle', euiFieldProps: { fullWidth: false, + disabled: isLoading, }, - isDisabled: isLoading, }} /> - <DescriptionMarkdown - descriptionInputHeight={200} - formHook={true} - initialDescription={data.description} - isLoading={isLoading} - onChange={description => setFormData({ ...data, description })} - /> - <TagContainer> + <Container> <CommonUseField path="tags" componentProps={{ @@ -82,14 +90,24 @@ export const Create = React.memo(() => { euiFieldProps: { fullWidth: true, placeholder: '', + isDisabled: isLoading, }, + }} + /> + </Container> + <ContainerBig> + <UseField + path="description" + component={MarkdownEditorForm} + componentProps={{ + idAria: 'caseDescription', + dataTestSubj: 'caseDescription', isDisabled: isLoading, }} /> - </TagContainer> + </ContainerBig> </Form> - <> - <EuiHorizontalRule margin="m" /> + <Container> <EuiFlexGroup alignItems="center" justifyContent="flexEnd" @@ -97,12 +115,23 @@ export const Create = React.memo(() => { responsive={false} > <EuiFlexItem grow={false}> - <EuiButton fill isDisabled={isLoading} isLoading={isLoading} onClick={onSubmit}> - {i18n.SUBMIT} + <EuiButtonEmpty size="s" onClick={() => setIsCancel(true)} iconType="cross"> + {i18n.CANCEL} + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton + fill + iconType="plusInCircle" + isDisabled={isLoading} + isLoading={isLoading} + onClick={onSubmit} + > + {i18n.CREATE_CASE} </EuiButton> </EuiFlexItem> </EuiFlexGroup> - </> + </Container> </EuiPanel> ); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx index 1b5df72a6671c..c81a31f0d4f3f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FIELD_TYPES, fieldValidators, FormSchema } from '../../../shared_imports'; +import { FIELD_TYPES, fieldValidators, FormSchema } from '../../../../shared_imports'; import { OptionalFieldLabel } from './optional_field_label'; import * as i18n from '../../translations'; @@ -13,7 +13,7 @@ const { emptyField } = fieldValidators; export const schema: FormSchema = { title: { type: FIELD_TYPES.TEXT, - label: i18n.CASE_TITLE, + label: i18n.NAME, validations: [ { validator: emptyField(i18n.TITLE_REQUIRED), @@ -21,7 +21,7 @@ export const schema: FormSchema = { ], }, description: { - type: FIELD_TYPES.TEXTAREA, + label: i18n.DESCRIPTION, validations: [ { validator: emptyField(i18n.DESCRIPTION_REQUIRED), diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/description_md_editor/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/description_md_editor/index.tsx deleted file mode 100644 index 44062a5a1d589..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/description_md_editor/index.tsx +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiFlexItem, EuiPanel, EuiTabbedContent, EuiTextArea } from '@elastic/eui'; -import React, { useState } from 'react'; -import styled from 'styled-components'; - -import { Markdown } from '../../../../components/markdown'; -import * as i18n from '../../translations'; -import { MarkdownHint } from '../../../../components/markdown/markdown_hint'; -import { CommonUseField } from '../create'; - -const TextArea = styled(EuiTextArea)<{ height: number }>` - min-height: ${({ height }) => `${height}px`}; - width: 100%; -`; - -TextArea.displayName = 'TextArea'; - -const DescriptionContainer = styled.div` - margin-top: 15px; - margin-bottom: 15px; -`; - -const DescriptionMarkdownTabs = styled(EuiTabbedContent)` - width: 100%; -`; - -DescriptionMarkdownTabs.displayName = 'DescriptionMarkdownTabs'; - -const MarkdownContainer = styled(EuiPanel)<{ height: number }>` - height: ${({ height }) => height}px; - overflow: auto; -`; - -MarkdownContainer.displayName = 'MarkdownContainer'; - -/** An input for entering a new case description */ -export const DescriptionMarkdown = React.memo<{ - descriptionInputHeight: number; - initialDescription: string; - isLoading: boolean; - formHook?: boolean; - onChange: (description: string) => void; -}>(({ initialDescription, isLoading, descriptionInputHeight, onChange, formHook = false }) => { - const [description, setDescription] = useState(initialDescription); - const tabs = [ - { - id: 'description', - name: i18n.DESCRIPTION, - content: formHook ? ( - <CommonUseField - path="description" - onChange={e => { - setDescription(e as string); - onChange(e as string); - }} - componentProps={{ - idAria: 'caseDescription', - 'data-test-subj': 'caseDescription', - isDisabled: isLoading, - spellcheck: false, - }} - /> - ) : ( - <TextArea - onChange={e => { - setDescription(e.target.value); - onChange(e.target.value); - }} - fullWidth={true} - height={descriptionInputHeight} - aria-label={i18n.DESCRIPTION} - disabled={isLoading} - spellCheck={false} - value={description} - /> - ), - }, - { - id: 'preview', - name: i18n.PREVIEW, - content: ( - <MarkdownContainer - data-test-subj="markdown-container" - height={descriptionInputHeight} - paddingSize="s" - > - <Markdown raw={description} /> - </MarkdownContainer> - ), - }, - ]; - return ( - <DescriptionContainer> - <DescriptionMarkdownTabs - data-test-subj="new-description-tabs" - tabs={tabs} - initialSelectedTab={tabs[0]} - /> - <EuiFlexItem grow={true}> - <MarkdownHint show={description.trim().length > 0} /> - </EuiFlexItem> - </DescriptionContainer> - ); -}); - -DescriptionMarkdown.displayName = 'DescriptionMarkdown'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx index 6634672cb6a77..3513d4de12aa1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useState } from 'react'; import { EuiText, EuiHorizontalRule, @@ -14,24 +14,18 @@ import { EuiButton, EuiButtonEmpty, EuiButtonIcon, + EuiLoadingSpinner, } from '@elastic/eui'; import styled, { css } from 'styled-components'; import * as i18n from '../../translations'; -import { Form, useForm } from '../../../shared_imports'; +import { Form, useForm } from '../../../../shared_imports'; import { schema } from './schema'; import { CommonUseField } from '../create'; -interface IconAction { - 'aria-label': string; - iconType: string; - onClick: (b: boolean) => void; - onSubmit: (a: string[]) => void; -} - interface TagListProps { + isLoading: boolean; + onSubmit: (a: string[]) => void; tags: string[]; - iconAction?: IconAction; - isEditTags?: boolean; } const MyFlexGroup = styled(EuiFlexGroup)` @@ -43,37 +37,35 @@ const MyFlexGroup = styled(EuiFlexGroup)` `} `; -export const TagList = React.memo(({ tags, isEditTags, iconAction }: TagListProps) => { +export const TagList = React.memo(({ isLoading, onSubmit, tags }: TagListProps) => { const { form } = useForm({ defaultValue: { tags }, options: { stripEmptyFields: false }, schema, }); + const [isEditTags, setIsEditTags] = useState(false); - const onSubmit = useCallback(async () => { + const onSubmitTags = useCallback(async () => { const { isValid, data: newData } = await form.submit(); - if (isValid && iconAction) { - iconAction.onSubmit(newData.tags); - iconAction.onClick(false); + if (isValid && newData.tags) { + onSubmit(newData.tags); + setIsEditTags(false); } - }, [form]); + }, [form, onSubmit]); - const onActionClick = useCallback( - (cb: (b: boolean) => void, onClickBool: boolean) => cb(onClickBool), - [iconAction] - ); return ( <EuiText> <EuiFlexGroup alignItems="center" gutterSize="xs" justifyContent="spaceBetween"> <EuiFlexItem grow={false}> <h4>{i18n.TAGS}</h4> </EuiFlexItem> - {iconAction && ( + {isLoading && <EuiLoadingSpinner />} + {!isLoading && ( <EuiFlexItem grow={false}> <EuiButtonIcon - aria-label={iconAction['aria-label']} - iconType={iconAction.iconType} - onClick={() => onActionClick(iconAction.onClick, true)} + aria-label={'tags'} + iconType={'pencil'} + onClick={setIsEditTags.bind(null, true)} /> </EuiFlexItem> )} @@ -88,7 +80,7 @@ export const TagList = React.memo(({ tags, isEditTags, iconAction }: TagListProp <EuiBadge color="hollow">{tag}</EuiBadge> </EuiFlexItem> ))} - {isEditTags && iconAction && ( + {isEditTags && ( <EuiFlexGroup direction="column"> <EuiFlexItem> <Form form={form}> @@ -106,14 +98,22 @@ export const TagList = React.memo(({ tags, isEditTags, iconAction }: TagListProp </Form> </EuiFlexItem> <EuiFlexItem> - <EuiButton fill onClick={onSubmit}> - {i18n.SUBMIT} - </EuiButton> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButtonEmpty onClick={() => onActionClick(iconAction.onClick, false)}> - {i18n.CANCEL} - </EuiButtonEmpty> + <EuiFlexGroup gutterSize="s" alignItems="center"> + <EuiFlexItem grow={false}> + <EuiButton color="secondary" fill iconType="save" onClick={onSubmitTags} size="s"> + {i18n.SAVE} + </EuiButton> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButtonEmpty + iconType="cross" + onClick={setIsEditTags.bind(null, false)} + size="s" + > + {i18n.CANCEL} + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> </EuiFlexItem> </EuiFlexGroup> )} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/schema.tsx index dfc9c61cd5f0c..26a89408069fb 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/schema.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { FormSchema } from '../../../shared_imports'; +import { FormSchema } from '../../../../shared_imports'; import { schema as createSchema } from '../create/schema'; export const schema: FormSchema = { diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx index 8df98a4cef0e8..6599151f9d4fd 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx @@ -4,18 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { ReactNode } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiAvatar, EuiPanel, EuiText } from '@elastic/eui'; +import React, { ReactNode, useCallback, useMemo, useState } from 'react'; +import { EuiFlexGroup } from '@elastic/eui'; import styled, { css } from 'styled-components'; +import * as i18n from '../case_view/translations'; + +import { Case } from '../../../../containers/case/types'; +import { useUpdateComment } from '../../../../containers/case/use_update_comment'; +import { UserActionItem } from './user_action_item'; +import { UserActionMarkdown } from './user_action_markdown'; +import { AddComment } from '../add_comment'; export interface UserActionItem { avatarName: string; children?: ReactNode; - title: ReactNode; + skipPanel?: boolean; + title?: ReactNode; } export interface UserActionTreeProps { - userActions: UserActionItem[]; + data: Case; + isLoadingDescription: boolean; + onUpdateField: (updateKey: keyof Case, updateValue: string | string[]) => void; } const UserAction = styled(EuiFlexGroup)` @@ -48,35 +58,110 @@ const UserAction = styled(EuiFlexGroup)` border-bottom: ${theme.eui.euiBorderThin}; border-radius: ${theme.eui.euiBorderRadius} ${theme.eui.euiBorderRadius} 0 0; } - .userAction__content { - padding: ${theme.eui.euiSizeM} ${theme.eui.euiSizeL}; - } .euiText--small * { margin-bottom: 0; } `} `; -const renderUserActions = (userActions: UserActionItem[]) => { - return userActions.map(({ avatarName, children, title }, key) => ( - <UserAction key={key} gutterSize={'none'}> - <EuiFlexItem grow={false}> - <EuiAvatar className="userAction__circle" name={avatarName} /> - </EuiFlexItem> - <EuiFlexItem> - <EuiPanel className="userAction__panel" paddingSize="none"> - <EuiText size="s" className="userAction__title"> - {title} - </EuiText> - {children && <div className="userAction__content">{children}</div>} - </EuiPanel> - </EuiFlexItem> - </UserAction> - )); -}; +const DescriptionId = 'description'; +const NewId = 'newComent'; + +export const UserActionTree = React.memo( + ({ data, onUpdateField, isLoadingDescription }: UserActionTreeProps) => { + const [{ data: comments, isLoadingIds }, dispatchUpdateComment] = useUpdateComment( + data.comments + ); + + const [manageMarkdownEditIds, setManangeMardownEditIds] = useState<string[]>([]); + + const handleManageMarkdownEditId = useCallback( + (id: string) => { + if (!manageMarkdownEditIds.includes(id)) { + setManangeMardownEditIds([...manageMarkdownEditIds, id]); + } else { + setManangeMardownEditIds(manageMarkdownEditIds.filter(myId => id !== myId)); + } + }, + [manageMarkdownEditIds] + ); + + const handleSaveComment = useCallback( + (id: string, content: string) => { + handleManageMarkdownEditId(id); + dispatchUpdateComment(id, content); + }, + [handleManageMarkdownEditId, dispatchUpdateComment] + ); + + const MarkdownDescription = useMemo( + () => ( + <UserActionMarkdown + id={DescriptionId} + content={data.description} + isEditable={manageMarkdownEditIds.includes(DescriptionId)} + onSaveContent={(content: string) => { + handleManageMarkdownEditId(DescriptionId); + onUpdateField(DescriptionId, content); + }} + onChangeEditable={handleManageMarkdownEditId} + /> + ), + [data.description, handleManageMarkdownEditId, manageMarkdownEditIds, onUpdateField] + ); + + const MarkdownNewComment = useMemo(() => <AddComment caseId={data.caseId} />, [data.caseId]); -export const UserActionTree = React.memo(({ userActions }: UserActionTreeProps) => ( - <div>{renderUserActions(userActions)}</div> -)); + return ( + <UserAction data-test-subj="user-action-description" gutterSize={'none'}> + <UserActionItem + createdAt={data.createdAt} + id={DescriptionId} + isEditable={manageMarkdownEditIds.includes(DescriptionId)} + isLoading={isLoadingDescription} + labelAction={i18n.EDIT_DESCRIPTION} + labelTitle={i18n.ADDED_DESCRIPTION} + fullName={data.createdBy.fullName ?? data.createdBy.username} + markdown={MarkdownDescription} + onEdit={handleManageMarkdownEditId.bind(null, DescriptionId)} + userName={data.createdBy.username} + /> + {comments.map(comment => ( + <UserActionItem + key={comment.commentId} + createdAt={comment.createdAt} + id={comment.commentId} + isEditable={manageMarkdownEditIds.includes(comment.commentId)} + isLoading={isLoadingIds.includes(comment.commentId)} + labelAction={i18n.EDIT_COMMENT} + labelTitle={i18n.ADDED_COMMENT} + fullName={comment.createdBy.fullName ?? comment.createdBy.username} + markdown={ + <UserActionMarkdown + id={comment.commentId} + content={comment.comment} + isEditable={manageMarkdownEditIds.includes(comment.commentId)} + onChangeEditable={handleManageMarkdownEditId} + onSaveContent={handleSaveComment.bind(null, comment.commentId)} + /> + } + onEdit={handleManageMarkdownEditId.bind(null, comment.commentId)} + userName={comment.createdBy.username} + /> + ))} + <UserActionItem + createdAt={new Date().toISOString()} + id={NewId} + isEditable={true} + isLoading={isLoadingIds.includes(NewId)} + fullName="to be determined" + markdown={MarkdownNewComment} + onEdit={handleManageMarkdownEditId.bind(null, NewId)} + userName="to be determined" + /> + </UserAction> + ); + } +); UserActionTree.displayName = 'UserActionTree'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_avatar.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_avatar.tsx new file mode 100644 index 0000000000000..f3276bd50e72c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_avatar.tsx @@ -0,0 +1,18 @@ +/* + * 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 { EuiAvatar } from '@elastic/eui'; +import React from 'react'; + +interface UserActionAvatarProps { + name: string; +} + +export const UserActionAvatar = ({ name }: UserActionAvatarProps) => { + return ( + <EuiAvatar data-test-subj={`user-action-avatar`} className="userAction__circle" name={name} /> + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx new file mode 100644 index 0000000000000..816e500827590 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx @@ -0,0 +1,60 @@ +/* + * 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 { EuiFlexItem, EuiPanel } from '@elastic/eui'; +import React from 'react'; + +import { UserActionAvatar } from './user_action_avatar'; +import { UserActionTitle } from './user_action_title'; + +interface UserActionItemProps { + createdAt: string; + id: string; + isEditable: boolean; + isLoading: boolean; + labelAction?: string; + labelTitle?: string; + fullName: string; + markdown: React.ReactNode; + onEdit: (id: string) => void; + userName: string; +} + +export const UserActionItem = ({ + createdAt, + id, + isEditable, + isLoading, + labelAction, + labelTitle, + fullName, + markdown, + onEdit, + userName, +}: UserActionItemProps) => ( + <> + <EuiFlexItem data-test-subj={`user-action-${id}-avatar`} grow={false}> + <UserActionAvatar name={fullName ?? userName} /> + </EuiFlexItem> + <EuiFlexItem data-test-subj={`user-action-${id}`}> + {isEditable && markdown} + {!isEditable && ( + <EuiPanel className="userAction__panel" paddingSize="none"> + <UserActionTitle + createdAt={createdAt} + id={id} + isLoading={isLoading} + labelAction={labelAction ?? ''} + labelTitle={labelTitle ?? ''} + userName={userName} + onEdit={onEdit} + /> + {markdown} + </EuiPanel> + )} + </EuiFlexItem> + </> +); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_markdown.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_markdown.tsx new file mode 100644 index 0000000000000..6a50bf24e9d7e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_markdown.tsx @@ -0,0 +1,89 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiButton } from '@elastic/eui'; +import React, { useCallback, useState } from 'react'; +import styled, { css } from 'styled-components'; + +import { MarkdownEditor } from '../../../../components/markdown_editor'; +import * as i18n from '../case_view/translations'; +import { Markdown } from '../../../../components/markdown'; + +const ContentWrapper = styled.div` + ${({ theme }) => css` + padding: ${theme.eui.euiSizeM} ${theme.eui.euiSizeL}; + `} +`; + +interface UserActionMarkdownProps { + content: string; + id: string; + isEditable: boolean; + onChangeEditable: (id: string) => void; + onSaveContent: (content: string) => void; +} + +export const UserActionMarkdown = ({ + id, + content, + isEditable, + onChangeEditable, + onSaveContent, +}: UserActionMarkdownProps) => { + const [myContent, setMyContent] = useState(content); + + const handleCancelAction = useCallback(() => { + onChangeEditable(id); + }, [id, onChangeEditable]); + + const handleSaveAction = useCallback(() => { + if (myContent !== content) { + onSaveContent(content); + } + onChangeEditable(id); + }, [content, id, myContent, onChangeEditable, onSaveContent]); + + const handleOnChange = useCallback(() => { + if (myContent !== content) { + setMyContent(content); + } + }, [content, myContent]); + + const renderButtons = useCallback( + ({ cancelAction, saveAction }) => { + return ( + <EuiFlexGroup gutterSize="s" alignItems="center"> + <EuiFlexItem grow={false}> + <EuiButtonEmpty size="s" onClick={cancelAction} iconType="cross"> + {i18n.CANCEL} + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton color="secondary" fill iconType="save" onClick={saveAction} size="s"> + {i18n.SAVE} + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + ); + }, + [handleCancelAction, handleSaveAction] + ); + + return isEditable ? ( + <MarkdownEditor + footerContentRight={renderButtons({ + cancelAction: handleCancelAction, + saveAction: handleSaveAction, + })} + initialContent={content} + onChange={handleOnChange} + /> + ) : ( + <ContentWrapper> + <Markdown raw={content} data-test-subj="case-view-description" /> + </ContentWrapper> + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx new file mode 100644 index 0000000000000..6ad60fb9f963e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx @@ -0,0 +1,70 @@ +/* + * 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 { EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import styled from 'styled-components'; + +import { + FormattedRelativePreferenceDate, + FormattedRelativePreferenceLabel, +} from '../../../../components/formatted_date'; +import * as i18n from '../case_view/translations'; +import { PropertyActions } from '../property_actions'; + +const MySpinner = styled(EuiLoadingSpinner)` + .euiLoadingSpinner { + margin-top: 1px; // yes it matters! + } +`; + +interface UserActionTitleProps { + createdAt: string; + id: string; + isLoading: boolean; + labelAction: string; + labelTitle: string; + userName: string; + onEdit: (id: string) => void; +} + +export const UserActionTitle = ({ + createdAt, + id, + isLoading, + labelAction, + labelTitle, + userName, + onEdit, +}: UserActionTitleProps) => { + const propertyActions = useMemo(() => { + return [ + { + iconType: 'documentEdit', + label: labelAction, + onClick: () => onEdit(id), + }, + ]; + }, [id, onEdit]); + return ( + <EuiText size="s" className="userAction__title" data-test-subj={`user-action-title`}> + <EuiFlexGroup alignItems="baseline" gutterSize="none" justifyContent="spaceBetween"> + <EuiFlexItem grow={false}> + <p> + <strong>{userName}</strong> + {` ${labelTitle} `} + <FormattedRelativePreferenceLabel value={createdAt} preferenceLabel={`${i18n.ON} `} /> + <FormattedRelativePreferenceDate value={createdAt} /> + </p> + </EuiFlexItem> + <EuiFlexItem grow={false}> + {isLoading && <MySpinner />} + {!isLoading && <PropertyActions propertyActions={propertyActions} />} + </EuiFlexItem> + </EuiFlexGroup> + </EuiText> + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx index 33e0a9541c5b4..abb49122dc142 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx @@ -32,12 +32,12 @@ const MyFlexGroup = styled(EuiFlexGroup)` `; const renderUsers = (users: ElasticUser[]) => { - return users.map(({ username }, key) => ( + return users.map(({ fullName, username }, key) => ( <MyFlexGroup key={key} justifyContent="spaceBetween"> <EuiFlexItem grow={false}> <EuiFlexGroup gutterSize="xs"> <EuiFlexItem> - <MyAvatar name={username} /> + <MyAvatar name={fullName ? fullName : username} /> </EuiFlexItem> <EuiFlexItem> <p> diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts index 265af0bde547f..5f0509586fc81 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts @@ -14,8 +14,8 @@ export const CANCEL = i18n.translate('xpack.siem.case.caseView.cancel', { defaultMessage: 'Cancel', }); -export const CASE_TITLE = i18n.translate('xpack.siem.case.caseView.caseTitle', { - defaultMessage: 'Case Title', +export const NAME = i18n.translate('xpack.siem.case.caseView.name', { + defaultMessage: 'Name', }); export const CREATED_AT = i18n.translate('xpack.siem.case.caseView.createdAt', { @@ -45,6 +45,13 @@ export const DESCRIPTION_REQUIRED = i18n.translate( } ); +export const COMMENT_REQUIRED = i18n.translate( + 'xpack.siem.case.caseView.commentFieldRequiredError', + { + defaultMessage: 'A comment is required.', + } +); + export const EDIT = i18n.translate('xpack.siem.case.caseView.edit', { defaultMessage: 'Edit', }); @@ -58,15 +65,11 @@ export const LAST_UPDATED = i18n.translate('xpack.siem.case.caseView.updatedAt', }); export const PAGE_SUBTITLE = i18n.translate('xpack.siem.case.caseView.pageSubtitle', { - defaultMessage: 'Case Workflow Management within the Elastic SIEM', + defaultMessage: 'Cases within the Elastic SIEM', }); export const PAGE_TITLE = i18n.translate('xpack.siem.case.pageTitle', { - defaultMessage: 'Case Workflows', -}); - -export const PREVIEW = i18n.translate('xpack.siem.case.caseView.preview', { - defaultMessage: 'Preview', + defaultMessage: 'Cases', }); export const STATE = i18n.translate('xpack.siem.case.caseView.state', { @@ -77,6 +80,10 @@ export const SUBMIT = i18n.translate('xpack.siem.case.caseView.submit', { defaultMessage: 'Submit', }); +export const CREATE_CASE = i18n.translate('xpack.siem.case.caseView.createCase', { + defaultMessage: 'Create case', +}); + export const TAGS = i18n.translate('xpack.siem.case.caseView.tags', { defaultMessage: 'Tags', }); @@ -104,3 +111,18 @@ export const CONFIGURE_CASES_PAGE_TITLE = i18n.translate( export const CONFIGURE_CASES_BUTTON = i18n.translate('xpack.siem.case.configureCasesButton', { defaultMessage: 'Configure cases', }); + +export const ADD_COMMENT = i18n.translate('xpack.siem.case.caseView.comment.addComment', { + defaultMessage: 'Add comment', +}); + +export const ADD_COMMENT_HELP_TEXT = i18n.translate( + 'xpack.siem.case.caseView.comment.addCommentHelpText', + { + defaultMessage: 'Add a new comment...', + } +); + +export const SAVE = i18n.translate('xpack.siem.case.caseView.description.save', { + defaultMessage: 'Save', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx index cc5e9b38eb2f8..abbaa6d6192ee 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx @@ -18,7 +18,7 @@ import React, { ChangeEvent, useCallback, useEffect, useState, useRef } from 're import styled from 'styled-components'; import * as RuleI18n from '../../translations'; -import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../../shared_imports'; interface AddItemProps { addText: string; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx index 1cc7bba5558db..f921c29c06ab0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx @@ -19,7 +19,7 @@ import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/searc import { useKibana } from '../../../../../lib/kibana'; import { IMitreEnterpriseAttack } from '../../types'; import { FieldValueTimeline } from '../pick_timeline'; -import { FormSchema } from '../../../../shared_imports'; +import { FormSchema } from '../../../../../shared_imports'; import { ListItems } from './types'; import { buildQueryBarDescription, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx index b49126c8c0fe0..e87dba251ed6d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx @@ -20,7 +20,7 @@ import styled from 'styled-components'; import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; import * as Rulei18n from '../../translations'; -import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../../shared_imports'; import { threatDefault } from '../step_about_rule/default_value'; import { IMitreEnterpriseAttack } from '../../types'; import { MyAddItemButton } from '../add_item_form'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx index 56cb02c9ec817..923ec3a7f0066 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx @@ -8,7 +8,7 @@ import { EuiFormRow } from '@elastic/eui'; import React, { useCallback, useEffect, useState } from 'react'; import { SearchTimelineSuperSelect } from '../../../../../components/timeline/search_super_select'; -import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../../shared_imports'; export interface FieldValueTimeline { id: string | null; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx index fbe854c1ee346..5886a76182eec 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx @@ -29,7 +29,7 @@ import { convertKueryToElasticSearchQuery } from '../../../../../lib/keury'; import { useKibana } from '../../../../../lib/kibana'; import { TimelineModel } from '../../../../../store/timeline/model'; import { useSavedQueryServices } from '../../../../../utils/saved_query_services'; -import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../../shared_imports'; import * as i18n from './translations'; export interface FieldValueQueryBar { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx index ffb6c4eda3243..1b7d17016f83c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx @@ -16,7 +16,7 @@ import { isEmpty } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; -import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../../shared_imports'; import * as I18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx index 431d793d6e68a..d93c057506ca7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx @@ -30,7 +30,7 @@ import { getUseField, UseField, useForm, -} from '../../../../shared_imports'; +} from '../../../../../shared_imports'; import { defaultRiskScoreBySeverity, severityOptions, SeverityValue } from './data'; import { stepAboutDefaultValue } from './default_value'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx index 27887bcbbe600..42cf1e0d95649 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx @@ -13,7 +13,7 @@ import { FormSchema, ValidationFunc, ERROR_CODE, -} from '../../../../shared_imports'; +} from '../../../../../shared_imports'; import { isMitreAttackInvalid } from '../mitre/helpers'; import { OptionalFieldLabel } from '../optional_field_label'; import { isUrlInvalid } from './helpers'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx index 773eb44efb26c..837bc79e968e8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx @@ -33,7 +33,7 @@ import { getUseField, UseField, useForm, -} from '../../../../shared_imports'; +} from '../../../../../shared_imports'; import { schema } from './schema'; import * as i18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx index bb178d7197069..e202ff030cd90 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx @@ -17,7 +17,7 @@ import { fieldValidators, FormSchema, ValidationFunc, -} from '../../../../shared_imports'; +} from '../../../../../shared_imports'; import { CUSTOM_QUERY_REQUIRED, INVALID_CUSTOM_QUERY, INDEX_HELPER_TEXT } from './translations'; const { emptyField } = fieldValidators; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx index 2e2c7e068dd85..e9632966fdfaf 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx @@ -12,7 +12,7 @@ import { setFieldValue } from '../../helpers'; import { RuleStep, RuleStepProps, ScheduleStepRule } from '../../types'; import { StepRuleDescription } from '../description_step'; import { ScheduleItem } from '../schedule_item_form'; -import { Form, UseField, useForm } from '../../../../shared_imports'; +import { Form, UseField, useForm } from '../../../../../shared_imports'; import { StepContentWrapper } from '../step_content_wrapper'; import { schema } from './schema'; import * as I18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx index 9932e4f6ef435..8fbfdf5f25a51 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { OptionalFieldLabel } from '../optional_field_label'; -import { FormSchema } from '../../../../shared_imports'; +import { FormSchema } from '../../../../../shared_imports'; export const schema: FormSchema = { interval: { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx index c985045b1897b..d816c7e867057 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx @@ -17,7 +17,7 @@ import { displaySuccessToast, useStateToaster } from '../../../../components/toa import { SpyRoute } from '../../../../utils/route/spy_routes'; import { useUserInfo } from '../../components/user_info'; import { AccordionTitle } from '../components/accordion_title'; -import { FormData, FormHook } from '../../../shared_imports'; +import { FormData, FormHook } from '../../../../shared_imports'; import { StepAboutRule } from '../components/step_about_rule'; import { StepDefineRule } from '../components/step_define_rule'; import { StepScheduleRule } from '../components/step_schedule_rule'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx index 0fac4641e54a7..5e0e4223e3e27 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx @@ -26,7 +26,7 @@ import { displaySuccessToast, useStateToaster } from '../../../../components/toa import { SpyRoute } from '../../../../utils/route/spy_routes'; import { useUserInfo } from '../../components/user_info'; import { DetectionEngineHeaderPage } from '../../components/detection_engine_header_page'; -import { FormHook, FormData } from '../../../shared_imports'; +import { FormHook, FormData } from '../../../../shared_imports'; import { StepPanel } from '../components/step_panel'; import { StepAboutRule } from '../components/step_about_rule'; import { StepDefineRule } from '../components/step_define_rule'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx index 3fab456d856ca..85f3bcbd236e9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx @@ -11,7 +11,7 @@ import { useLocation } from 'react-router-dom'; import { Filter } from '../../../../../../../../src/plugins/data/public'; import { Rule } from '../../../containers/detection_engine/rules'; -import { FormData, FormHook, FormSchema } from '../../shared_imports'; +import { FormData, FormHook, FormSchema } from '../../../shared_imports'; import { AboutStepRule, DefineStepRule, IMitreEnterpriseAttack, ScheduleStepRule } from './types'; interface GetStepsData { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts index b2650dcc2b77e..34df20de1e461 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts @@ -6,7 +6,7 @@ import { Filter } from '../../../../../../../../src/plugins/data/common'; import { FieldValueQueryBar } from './components/query_bar'; -import { FormData, FormHook } from '../../shared_imports'; +import { FormData, FormHook } from '../../../shared_imports'; import { FieldValueTimeline } from './components/pick_timeline'; export interface EuiBasicTableSortTypes { diff --git a/x-pack/legacy/plugins/siem/public/pages/home/translations.ts b/x-pack/legacy/plugins/siem/public/pages/home/translations.ts index 581c81d9f98a0..f2bcaa07b1a25 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/home/translations.ts @@ -27,5 +27,5 @@ export const TIMELINES = i18n.translate('xpack.siem.navigation.timelines', { }); export const CASE = i18n.translate('xpack.siem.navigation.case', { - defaultMessage: 'Case', + defaultMessage: 'Cases', }); diff --git a/x-pack/legacy/plugins/siem/public/pages/shared_imports.ts b/x-pack/legacy/plugins/siem/public/shared_imports.ts similarity index 52% rename from x-pack/legacy/plugins/siem/public/pages/shared_imports.ts rename to x-pack/legacy/plugins/siem/public/shared_imports.ts index a41f121b36926..edd7812b3bd16 100644 --- a/x-pack/legacy/plugins/siem/public/pages/shared_imports.ts +++ b/x-pack/legacy/plugins/siem/public/shared_imports.ts @@ -17,7 +17,7 @@ export { UseField, useForm, ValidationFunc, -} from '../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; -export { Field } from '../../../../../../src/plugins/es_ui_shared/static/forms/components'; -export { fieldValidators } from '../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; -export { ERROR_CODE } from '../../../../../../src/plugins/es_ui_shared/static/forms/helpers/field_validators/types'; +} from '../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; +export { Field } from '../../../../../src/plugins/es_ui_shared/static/forms/components'; +export { fieldValidators } from '../../../../../src/plugins/es_ui_shared/static/forms/helpers'; +export { ERROR_CODE } from '../../../../../src/plugins/es_ui_shared/static/forms/helpers/field_validators/types'; diff --git a/x-pack/plugins/case/server/routes/api/__tests__/update_comment.test.ts b/x-pack/plugins/case/server/routes/api/__tests__/update_comment.test.ts index 5bfd121691ab4..6b4e3c194eb82 100644 --- a/x-pack/plugins/case/server/routes/api/__tests__/update_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/__tests__/update_comment.test.ts @@ -28,6 +28,7 @@ describe('UPDATE comment', () => { }, body: { comment: 'Update my comment', + version: 'WzEsMV0=', }, }); @@ -37,6 +38,24 @@ describe('UPDATE comment', () => { expect(response.status).toEqual(200); expect(response.payload.comment).toEqual('Update my comment'); }); + it(`Fails with 409 if version does not match`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases/comment/{id}', + method: 'patch', + params: { + id: 'mock-comment-1', + }, + body: { + comment: 'Update my comment', + version: 'badv=', + }, + }); + + const theContext = createRouteContext(createMockSavedObjectsRepository(mockCaseComments)); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(409); + }); it(`Returns an error if updateComment throws`, async () => { const request = httpServerMock.createKibanaRequest({ path: '/api/cases/comment/{id}', diff --git a/x-pack/plugins/case/server/routes/api/schema.ts b/x-pack/plugins/case/server/routes/api/schema.ts index 468abc8e7226f..765f9c722219f 100644 --- a/x-pack/plugins/case/server/routes/api/schema.ts +++ b/x-pack/plugins/case/server/routes/api/schema.ts @@ -15,6 +15,11 @@ export const NewCommentSchema = schema.object({ comment: schema.string(), }); +export const UpdateCommentArguments = schema.object({ + comment: schema.string(), + version: schema.string(), +}); + export const CommentSchema = schema.object({ comment: schema.string(), created_at: schema.string(), diff --git a/x-pack/plugins/case/server/routes/api/update_comment.ts b/x-pack/plugins/case/server/routes/api/update_comment.ts index 815f44a14e2e7..9f99253f76629 100644 --- a/x-pack/plugins/case/server/routes/api/update_comment.ts +++ b/x-pack/plugins/case/server/routes/api/update_comment.ts @@ -5,9 +5,12 @@ */ import { schema } from '@kbn/config-schema'; +import { SavedObject } from 'kibana/server'; +import Boom from 'boom'; import { wrapError } from './utils'; -import { NewCommentSchema } from './schema'; +import { UpdateCommentArguments } from './schema'; import { RouteDeps } from '.'; +import { CommentAttributes } from './types'; export function initUpdateCommentApi({ caseService, router }: RouteDeps) { router.patch( @@ -17,20 +20,45 @@ export function initUpdateCommentApi({ caseService, router }: RouteDeps) { params: schema.object({ id: schema.string(), }), - body: NewCommentSchema, + body: UpdateCommentArguments, }, }, async (context, request, response) => { + let theComment: SavedObject<CommentAttributes>; + try { + theComment = await caseService.getComment({ + client: context.core.savedObjects.client, + commentId: request.params.id, + }); + } catch (error) { + return response.customError(wrapError(error)); + } + if (request.body.version !== theComment.version) { + return response.customError( + wrapError( + Boom.conflict( + 'This comment has been updated. Please refresh before saving additional updates.' + ) + ) + ); + } + if (request.body.comment === theComment.attributes.comment) { + return response.customError( + wrapError(Boom.notAcceptable('Comment is identical to current version.')) + ); + } try { const updatedComment = await caseService.updateComment({ client: context.core.savedObjects.client, commentId: request.params.id, updatedAttributes: { - ...request.body, + comment: request.body.comment, updated_at: new Date().toISOString(), }, }); - return response.ok({ body: updatedComment.attributes }); + return response.ok({ + body: { ...updatedComment.attributes, version: updatedComment.version }, + }); } catch (error) { return response.customError(wrapError(error)); } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 21500c4db9c34..a97cf608abc71 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10986,7 +10986,6 @@ "xpack.siem.editDataProvider.doesNotExistLabel": "存在しません", "xpack.siem.editDataProvider.existsLabel": "存在する", "xpack.siem.editDataProvider.fieldLabel": "フィールド", - "xpack.siem.editDataProvider.fieldPlaceholder": "フィールドを選択", "xpack.siem.editDataProvider.isLabel": "が", "xpack.siem.editDataProvider.isNotLabel": "is not", "xpack.siem.editDataProvider.operatorLabel": "演算子", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c9e7ea1ec80de..e6055680e1240 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10986,7 +10986,6 @@ "xpack.siem.editDataProvider.doesNotExistLabel": "不存在", "xpack.siem.editDataProvider.existsLabel": "存在", "xpack.siem.editDataProvider.fieldLabel": "字段", - "xpack.siem.editDataProvider.fieldPlaceholder": "选择字段", "xpack.siem.editDataProvider.isLabel": "是", "xpack.siem.editDataProvider.isNotLabel": "不是", "xpack.siem.editDataProvider.operatorLabel": "运算符", From 2378d8a0fdd425920d8321aaae729e0dee95013b Mon Sep 17 00:00:00 2001 From: Nathan L Smith <nathan.smith@elastic.co> Date: Mon, 2 Mar 2020 17:36:43 -0600 Subject: [PATCH 23/34] Service map language icons (#58633) Add icons as described in #56235. Also: * Add double-border and ghost "shadow" on nodes * Add framework name capability to popover metrics --- .../app/ServiceMap/Cytoscape.stories.tsx | 188 ++++++++++++++---- .../app/ServiceMap/Popover/Contents.tsx | 6 +- .../ServiceMap/Popover/Popover.stories.tsx | 1 + .../Popover/ServiceMetricFetcher.tsx | 10 +- .../ServiceMap/Popover/ServiceMetricList.tsx | 34 ++-- .../app/ServiceMap/cytoscapeOptions.ts | 10 +- .../app/ServiceMap/get_cytoscape_elements.ts | 3 +- .../public/components/app/ServiceMap/icons.ts | 55 +++-- .../app/ServiceMap/icons/default.svg | 3 + .../app/ServiceMap/icons/dot-net.svg | 127 ++++++++++++ .../components/app/ServiceMap/icons/go.svg | 11 + .../components/app/ServiceMap/icons/java.svg | 7 + .../app/ServiceMap/icons/nodejs.svg | 46 +++++ .../components/app/ServiceMap/icons/php.svg | 18 ++ .../app/ServiceMap/icons/python.svg | 19 ++ .../components/app/ServiceMap/icons/ruby.svg | 125 ++++++++++++ .../components/app/ServiceMap/icons/rumjs.svg | 3 + .../elasticsearch_fieldnames.test.ts.snap | 6 + .../apm/common/elasticsearch_fieldnames.ts | 1 + x-pack/plugins/apm/common/service_map.ts | 1 + .../server/lib/service_map/get_service_map.ts | 14 +- x-pack/plugins/apm/server/lib/services/map.ts | 88 -------- 22 files changed, 599 insertions(+), 177 deletions(-) create mode 100644 x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/default.svg create mode 100644 x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg create mode 100644 x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/go.svg create mode 100644 x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/java.svg create mode 100644 x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/nodejs.svg create mode 100644 x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/php.svg create mode 100644 x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/python.svg create mode 100644 x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/ruby.svg create mode 100644 x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/rumjs.svg delete mode 100644 x-pack/plugins/apm/server/lib/services/map.ts diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx index 731555694bff7..52941391ca364 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx @@ -4,51 +4,52 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiCard, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { storiesOf } from '@storybook/react'; import cytoscape from 'cytoscape'; import React from 'react'; import { Cytoscape } from './Cytoscape'; - -const elements: cytoscape.ElementDefinition[] = [ - { - data: { - id: 'opbeans-python', - label: 'opbeans-python', - agentName: 'python', - type: 'service' - } - }, - { - data: { - id: 'opbeans-node', - label: 'opbeans-node', - agentName: 'nodejs', - type: 'service' - } - }, - { - data: { - id: 'opbeans-ruby', - label: 'opbeans-ruby', - agentName: 'ruby', - type: 'service' - } - }, - { data: { source: 'opbeans-python', target: 'opbeans-node' } }, - { - data: { - bidirectional: true, - source: 'opbeans-python', - target: 'opbeans-ruby' - } - } -]; -const height = 300; -const serviceName = 'opbeans-python'; +import { iconForNode } from './icons'; storiesOf('app/ServiceMap/Cytoscape', module).add( 'example', () => { + const elements: cytoscape.ElementDefinition[] = [ + { + data: { + id: 'opbeans-python', + label: 'opbeans-python', + agentName: 'python', + type: 'service' + } + }, + { + data: { + id: 'opbeans-node', + label: 'opbeans-node', + agentName: 'nodejs', + type: 'service' + } + }, + { + data: { + id: 'opbeans-ruby', + label: 'opbeans-ruby', + agentName: 'ruby', + type: 'service' + } + }, + { data: { source: 'opbeans-python', target: 'opbeans-node' } }, + { + data: { + bidirectional: true, + source: 'opbeans-python', + target: 'opbeans-ruby' + } + } + ]; + const height = 300; + const serviceName = 'opbeans-python'; return ( <Cytoscape elements={elements} @@ -59,6 +60,119 @@ storiesOf('app/ServiceMap/Cytoscape', module).add( }, { info: { + propTables: false, + source: false + } + } +); + +storiesOf('app/ServiceMap/Cytoscape', module).add( + 'node icons', + () => { + const cy = cytoscape(); + const elements = [ + { data: { id: 'default', label: 'default', type: undefined } }, + { data: { id: 'cache', label: 'cache', type: 'cache' } }, + { data: { id: 'database', label: 'database', type: 'database' } }, + { data: { id: 'external', label: 'external', type: 'external' } }, + { data: { id: 'messaging', label: 'messaging', type: 'messaging' } }, + + { + data: { + id: 'dotnet', + label: 'dotnet service', + type: 'service', + agentName: 'dotnet' + } + }, + { + data: { + id: 'go', + label: 'go service', + type: 'service', + agentName: 'go' + } + }, + { + data: { + id: 'java', + label: 'java service', + type: 'service', + agentName: 'java' + } + }, + { + data: { + id: 'js-base', + label: 'js-base service', + type: 'service', + agentName: 'js-base' + } + }, + { + data: { + id: 'nodejs', + label: 'nodejs service', + type: 'service', + agentName: 'nodejs' + } + }, + { + data: { + id: 'php', + label: 'php service', + type: 'service', + agentName: 'php' + } + }, + { + data: { + id: 'python', + label: 'python service', + type: 'service', + agentName: 'python' + } + }, + { + data: { + id: 'ruby', + label: 'ruby service', + type: 'service', + agentName: 'ruby' + } + } + ]; + cy.add(elements); + + return ( + <EuiFlexGroup gutterSize="l" wrap={true}> + {cy.nodes().map(node => ( + <EuiFlexItem key={node.data('id')}> + <EuiCard + description={ + <pre> + agentName: {node.data('agentName') || 'undefined'}, type:{' '} + {node.data('type') || 'undefined'} + </pre> + } + icon={ + <img + alt={node.data('label')} + src={iconForNode(node)} + height={80} + width={80} + /> + } + title={node.data('label')} + /> + </EuiFlexItem> + ))} + </EuiFlexGroup> + ); + }, + { + info: { + propTables: false, source: false } } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx index f1c53673c8755..405bd855898b7 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx @@ -35,6 +35,7 @@ export function Contents({ onFocusClick, selectedNodeServiceName }: ContentsProps) { + const frameworkName = selectedNodeData.frameworkName; return ( <EuiFlexGroup direction="column" @@ -49,7 +50,10 @@ export function Contents({ </EuiFlexItem> <EuiFlexItem> {isService ? ( - <ServiceMetricFetcher serviceName={selectedNodeServiceName} /> + <ServiceMetricFetcher + frameworkName={frameworkName} + serviceName={selectedNodeServiceName} + /> ) : ( <Info {...selectedNodeData} /> )} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx index e5962afd76eb8..23e9e737be9a6 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx @@ -16,6 +16,7 @@ storiesOf('app/ServiceMap/Popover/ServiceMetricList', module) avgRequestsPerMinute={164.47222031860858} avgCpuUsage={0.32809666568309237} avgMemoryUsage={0.5504868173242986} + frameworkName="Spring" numInstances={2} isLoading={false} /> diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx index b0a5e892b5a7e..697aa6a1b652b 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx @@ -11,10 +11,12 @@ import { useUrlParams } from '../../../../hooks/useUrlParams'; import { ServiceMetricList } from './ServiceMetricList'; interface ServiceMetricFetcherProps { + frameworkName?: string; serviceName: string; } export function ServiceMetricFetcher({ + frameworkName, serviceName }: ServiceMetricFetcherProps) { const { @@ -37,5 +39,11 @@ export function ServiceMetricFetcher({ ); const isLoading = status === 'loading'; - return <ServiceMetricList {...data} isLoading={isLoading} />; + return ( + <ServiceMetricList + {...data} + frameworkName={frameworkName} + isLoading={isLoading} + /> + ); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx index 3a6b4c5ebcaac..056af68cc8173 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx @@ -30,6 +30,10 @@ function LoadingSpinner() { ); } +const BadgeRow = styled(EuiFlexItem)` + padding-bottom: ${lightTheme.gutterTypes.gutterSmall}; +`; + const ItemRow = styled('tr')` line-height: 2; `; @@ -44,6 +48,7 @@ const ItemDescription = styled('td')` `; interface ServiceMetricListProps extends ServiceNodeMetrics { + frameworkName?: string; isLoading: boolean; } @@ -53,6 +58,7 @@ export function ServiceMetricList({ avgErrorsPerMinute, avgCpuUsage, avgMemoryUsage, + frameworkName, numInstances, isLoading }: ServiceMetricListProps) { @@ -106,23 +112,27 @@ export function ServiceMetricList({ : null } ]; + const showBadgeRow = frameworkName || numInstances > 1; + return isLoading ? ( <LoadingSpinner /> ) : ( <> - {numInstances && numInstances > 1 && ( - <EuiFlexItem> - <div> - <EuiBadge iconType="apps" color="hollow"> - {i18n.translate('xpack.apm.serviceMap.numInstancesMetric', { - values: { numInstances }, - defaultMessage: '{numInstances} instances' - })} - </EuiBadge> - </div> - </EuiFlexItem> + {showBadgeRow && ( + <BadgeRow> + <EuiFlexGroup gutterSize="none"> + {frameworkName && <EuiBadge>{frameworkName}</EuiBadge>} + {numInstances > 1 && ( + <EuiBadge iconType="apps" color="hollow"> + {i18n.translate('xpack.apm.serviceMap.numInstancesMetric', { + values: { numInstances }, + defaultMessage: '{numInstances} instances' + })} + </EuiBadge> + )} + </EuiFlexGroup> + </BadgeRow> )} - <table> <tbody> {listItems.map( diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts index af5bd17f71ca4..8411169dbc944 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts @@ -42,19 +42,23 @@ const style: cytoscape.Stylesheet[] = [ 'background-image': (el: cytoscape.NodeSingular) => iconForNode(el) ?? defaultIcon, 'background-height': (el: cytoscape.NodeSingular) => - isService(el) ? '85%' : '40%', + isService(el) ? '60%' : '40%', 'background-width': (el: cytoscape.NodeSingular) => - isService(el) ? '85%' : '40%', + isService(el) ? '60%' : '40%', 'border-color': (el: cytoscape.NodeSingular) => el.hasClass('primary') || el.selected() ? theme.euiColorPrimary : theme.euiColorMediumShade, - 'border-width': 1, + 'border-width': 2, color: theme.textColors.default, // theme.euiFontFamily doesn't work here for some reason, so we're just // specifying a subset of the fonts for the label text. 'font-family': 'Inter UI, Segoe UI, Helvetica, Arial, sans-serif', 'font-size': theme.euiFontSizeXS, + ghost: 'yes', + 'ghost-offset-x': 0, + 'ghost-offset-y': 2, + 'ghost-opacity': 0.15, height: nodeHeight, label: 'data(label)', 'min-zoomed-font-size': theme.euiSizeL, diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/get_cytoscape_elements.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/get_cytoscape_elements.ts index 2403ed047cbc0..bc619b1ecdfe5 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/get_cytoscape_elements.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/get_cytoscape_elements.ts @@ -105,7 +105,8 @@ export function getCytoscapeElements( `/services/${node['service.name']}/service-map`, search ), - agentName: node['agent.name'] || node['agent.name'], + agentName: node['agent.name'], + frameworkName: node['service.framework.name'], type: 'service' }; } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts index c637d145639ce..1b57cd52082d8 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts @@ -4,26 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import theme from '@elastic/eui/dist/eui_theme_light.json'; import cytoscape from 'cytoscape'; import databaseIcon from './icons/database.svg'; import documentsIcon from './icons/documents.svg'; +import dotNetIcon from './icons/dot-net.svg'; import globeIcon from './icons/globe.svg'; +import goIcon from './icons/go.svg'; +import javaIcon from './icons/java.svg'; +import nodeJsIcon from './icons/nodejs.svg'; +import phpIcon from './icons/php.svg'; +import pythonIcon from './icons/python.svg'; +import rubyIcon from './icons/ruby.svg'; +import rumJsIcon from './icons/rumjs.svg'; +import defaultIconImport from './icons/default.svg'; -function getAvatarIcon( - text = '', - backgroundColor = 'transparent', - foregroundColor = 'white' -) { - return ( - 'data:image/svg+xml;utf8,' + - encodeURIComponent(`<svg width="80" height="80" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> - <circle cx="40" cy="40" fill="${backgroundColor}" r="40" stroke-width="0" /> - <text fill="${foregroundColor}" font-family="'Inter UI', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica', 'Arial', sans-serif" font-size="36" text-anchor="middle" x="40" xml:space="preserve" y="52">${text}</text> -</svg> -`) - ); -} +export const defaultIcon = defaultIconImport; // The colors here are taken from the logos of the corresponding technologies const icons: { [key: string]: string } = { @@ -34,18 +29,17 @@ const icons: { [key: string]: string } = { resource: globeIcon }; -const serviceAbbreviations: { [key: string]: string } = { - dotnet: '.N', - go: 'Go', - java: 'Jv', - 'js-base': 'JS', - nodejs: 'No', - python: 'Py', - ruby: 'Rb' +const serviceIcons: { [key: string]: string } = { + dotnet: dotNetIcon, + go: goIcon, + java: javaIcon, + 'js-base': rumJsIcon, + nodejs: nodeJsIcon, + php: phpIcon, + python: pythonIcon, + ruby: rubyIcon }; -export const defaultIcon = getAvatarIcon(); - // IE 11 does not properly load some SVGs, which causes a runtime error and the // map to not work at all. We would prefer to do some kind of feature detection // rather than browser detection, but IE 11 does support SVG, just not well @@ -61,15 +55,12 @@ export function iconForNode(node: cytoscape.NodeSingular) { const type = node.data('type'); if (type === 'service') { - return getAvatarIcon( - serviceAbbreviations[node.data('agentName') as string], - node.selected() || node.hasClass('primary') - ? theme.euiColorPrimary - : theme.euiColorDarkestShade - ); + return serviceIcons[node.data('agentName') as string]; } else if (isIE11) { return defaultIcon; - } else { + } else if (icons[type]) { return icons[type]; + } else { + return defaultIcon; } } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/default.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/default.svg new file mode 100644 index 0000000000000..08bc5331e083b --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/default.svg @@ -0,0 +1,3 @@ +<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M16.75 6.165a1.5 1.5 0 00-1.5 0l-7.392 4.268a1.5 1.5 0 00-.75 1.3v8.535a1.5 1.5 0 00.75 1.299l7.392 4.268a1.5 1.5 0 001.5 0l7.392-4.268a1.5 1.5 0 00.75-1.299v-8.536a1.5 1.5 0 00-.75-1.299L16.75 6.165zm.75-1.299l7.392 4.268a3 3 0 011.5 2.598v8.536a3 3 0 01-1.5 2.598L17.5 27.134a3 3 0 01-3 0l-7.392-4.268a3 3 0 01-1.5-2.598v-8.536a3 3 0 011.5-2.598L14.5 4.866a3 3 0 013 0z" fill="#98A2B3"/> +</svg> diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg new file mode 100644 index 0000000000000..9f7427f0e1001 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg @@ -0,0 +1,127 @@ +<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M11.164 13.586c1.145 3.503 1.58 9.753 4.93 9.753.254 0 .512-.025.77-.074-3.045-.71-3.405-6.892-5.264-10.093-.148.135-.293.273-.436.414" fill="url(#paint0_linear)"/> + <path d="M11.6 13.172c1.859 3.201 2.22 9.383 5.265 10.093.239-.044.479-.11.719-.195-2.733-1.339-3.489-7.341-5.6-10.231-.13.108-.258.22-.384.333" fill="url(#paint1_linear)"/> + <path d="M14.278 11.268a4.14 4.14 0 00-.772.074c-.678.128-1.367.42-2.06.862.189.187.37.4.539.635.706-.586 1.407-1.02 2.106-1.28.255-.095.518-.168.786-.218a2.42 2.42 0 00-.6-.073" fill="#14559A"/> + <path d="M19.718 21.836c.29-.233.571-.478.84-.737-1.164-3.487-1.58-9.826-4.954-9.826-.241 0-.485.023-.727.068 3.072.764 3.466 7.45 4.84 10.495" fill="url(#paint2_linear)"/> + <path d="M14.877 11.34a2.425 2.425 0 00-.6-.072l1.327.005a3.95 3.95 0 00-.727.068" fill="#3092C4"/> + <path d="M19.659 22.577a5.018 5.018 0 01-.38-.411 6.977 6.977 0 01-1.695.904 2.609 2.609 0 001.17.269c.63 0 1.129-.075 1.553-.278a2.95 2.95 0 01-.648-.484" fill="#1969BC"/> + <path d="M14.09 11.56c2.753 1.44 2.992 7.959 5.189 10.606.15-.106.295-.216.438-.33-1.374-3.045-1.767-9.731-4.84-10.495-.268.05-.53.124-.786.22" fill="url(#paint3_linear)"/> + <path d="M11.985 12.839c2.11 2.89 2.866 8.892 5.599 10.231a6.975 6.975 0 001.695-.904C17.082 19.519 16.843 13 14.09 11.56c-.699.26-1.4.693-2.106 1.279" fill="url(#paint4_linear)"/> + <path d="M9.814 13.502c-.331.748-.67 1.73-1.078 3.014.813-1.145 1.623-2.131 2.428-2.93a8.789 8.789 0 00-.359-.935c-.345.265-.676.55-.991.85" fill="url(#paint5_linear)"/> + <path d="M11.099 12.435c-.1.07-.197.142-.293.216.128.28.247.594.358.935.143-.14.288-.279.436-.414a5.808 5.808 0 00-.501-.737" fill="#2B74B1"/> + <path d="M11.445 12.204a9.562 9.562 0 00-.347.23c.18.223.346.47.502.738.126-.114.254-.225.385-.333a5.094 5.094 0 00-.54-.635" fill="#125A9E"/> + <path d="M30.218 11.001c-1.556 6.004-4.807 10.825-7.533 12.04h-.005c-.05.023-.1.044-.148.064-.006.004-.012.004-.018.007l-.041.016c-.007.004-.013.005-.02.007-.022.009-.045.015-.067.024-.01.005-.02.007-.028.01a.36.36 0 01-.034.011l-.033.012-.03.01-.057.017c-.009 0-.016.005-.025.007l-.04.01c-.008.005-.016.006-.026.008a.934.934 0 01-.043.01l-.052.012c.125.046.257.07.39.069 2.585 0 5.19-4.632 9.503-12.335h-1.693v.001z" fill="url(#paint6_linear)"/> + <path d="M8.175 11.512c.002 0 .004-.004.005-.004.002 0 .005 0 .006-.004h.003l.042-.016c.004 0 .006 0 .008-.004.004 0 .008-.004.011-.005l.045-.016h.003c.034-.01.066-.023.099-.035.004 0 .009-.004.015-.004l.042-.012c.007-.004.013-.004.02-.007l.042-.012c.007 0 .012-.004.017-.005.047-.013.094-.025.142-.036.006 0 .012-.005.02-.005a.274.274 0 01.04-.008c.007-.004.014-.004.021-.006.014-.004.027-.005.042-.008h.01l.086-.016h.018c.013-.004.026-.004.04-.007.007 0 .015-.004.022-.004.013 0 .025-.004.038-.006.007 0 .013 0 .021-.004.03-.004.061-.005.093-.008a2.87 2.87 0 00-.275-.014c-2.91 0-6.921 5.4-8.728 12.395h.349a101.37 101.37 0 001.572-2.922c1.265-4.954 3.842-8.333 6.131-9.228" fill="url(#paint7_linear)"/> + <path d="M9.814 13.502a12.1 12.1 0 01.991-.851 4.68 4.68 0 00-.236-.457c-.266.323-.51.75-.755 1.308M10.053 11.553c.19.16.36.379.516.641.05-.06.099-.116.15-.168a2.921 2.921 0 00-.666-.473" fill="#0D82CA"/> + <path d="M2.044 20.74c2.654-5.115 3.912-8.352 6.131-9.228-2.288.895-4.866 4.274-6.131 9.227" fill="url(#paint8_linear)"/> + <path d="M10.72 12.026c-.053.053-.102.108-.151.168.087.148.166.3.236.457.097-.074.195-.146.293-.216a3.989 3.989 0 00-.379-.409" fill="#127BCA"/> + <path d="M3.249 23.638c-.017.004-.033.004-.048.006h-.01c-.013 0-.028.004-.04.004h-.007c-.033.004-.066.004-.098.007h-.01c2.686-.075 3.914-1.42 4.524-3.371.463-1.48.843-2.725 1.177-3.77C7.61 18.097 6.48 19.985 5.345 22.1c-.548 1.02-1.382 1.445-2.096 1.537" fill="url(#paint9_linear)"/> + <path d="M3.249 23.637c.714-.09 1.548-.516 2.096-1.536 1.136-2.114 2.267-4.002 3.39-5.586.41-1.284.747-2.266 1.079-3.014-2.241 2.133-4.49 5.679-6.565 10.136" fill="url(#paint10_linear)"/> + <path d="M2.044 20.74c-.475.915-.995 1.89-1.573 2.922h1.013c.128-.985.315-1.96.56-2.922" fill="#05A1E6"/> + <path d="M9.033 11.29c-.008 0-.014.005-.021.005-.013.004-.025.004-.038.005-.008 0-.015 0-.023.004-.014.004-.026.005-.04.007-.006 0-.012 0-.017.004l-.086.015h-.01a.315.315 0 01-.042.009c-.007.004-.014.004-.021.005-.014.004-.028.006-.04.008-.008.005-.014.005-.02.005a4.64 4.64 0 00-.142.037c-.005 0-.01.004-.017.005a2.21 2.21 0 00-.043.012c-.006.004-.013.004-.02.006a1.055 1.055 0 00-.042.013c-.005 0-.01.004-.015.004-.033.012-.065.024-.099.034l-.045.016c-.006.004-.011.005-.018.008-.015.004-.03.01-.042.015-.005.004-.01.005-.014.007-2.22.876-3.477 4.113-6.132 9.228a24.646 24.646 0 00-.56 2.921h.143c.4 0 .513-.004.974-.004h.446c.032-.004.064-.004.097-.006h.007c.013 0 .027-.005.04-.005h.01c.015 0 .032-.003.048-.005 2.075-4.457 4.325-8.003 6.565-10.136.247-.558.49-.984.755-1.307a2.476 2.476 0 00-.516-.642s-.005 0-.005-.004l-.032-.015-.031-.016a.344.344 0 01-.03-.015c-.014-.004-.025-.01-.036-.015l-.029-.012a1.243 1.243 0 01-.058-.025l-.025-.01a.868.868 0 01-.044-.017c-.008-.004-.016-.005-.024-.008l-.06-.02h-.006c-.023-.008-.046-.014-.07-.02-.006-.005-.01-.005-.016-.006l-.06-.016c-.004 0-.01-.004-.013-.004a2.13 2.13 0 00-.148-.033c-.005 0-.01-.004-.015-.004a.86.86 0 00-.064-.01c-.005-.004-.008-.004-.013-.004a1.609 1.609 0 00-.076-.01h-.013c-.02-.004-.04-.004-.058-.006-.032.004-.063.005-.093.008" fill="url(#paint11_linear)"/> + <path d="M24.138 14.325c-.51 1.636-.924 2.985-1.284 4.096 1.401-1.929 2.781-4.354 4.096-7.154-1.534.482-2.351 1.584-2.812 3.058z" fill="url(#paint12_linear)"/> + <path d="M22.898 22.94a4.26 4.26 0 01-.213.102c2.726-1.216 5.977-6.038 7.533-12.04h-.315c-3.576 6.388-4.727 10.664-7.005 11.937z" fill="url(#paint13_linear)"/> + <path d="M21.119 22.403c.593-.724 1.076-1.954 1.735-3.982-.76 1.045-1.526 1.943-2.293 2.675 0 .004 0 .004-.004.007.167.5.35.943.56 1.3" fill="#079AE1"/> + <path d="M21.119 22.403a2.43 2.43 0 01-.812.658 2.295 2.295 0 00.967.274h.009c.02 0 .043 0 .065.004h.222c.008 0 .017 0 .025-.004.016 0 .033 0 .049-.004h.024c.017 0 .035-.004.053-.006h.004c.006 0 .011-.004.017-.004.018-.004.038-.006.056-.009h.017l.064-.01h.01l.128-.027c-.352-.129-.647-.433-.9-.865" fill="#1969BC"/> + <path d="M16.093 23.339c.255 0 .513-.025.772-.075.239-.043.479-.11.719-.193a2.614 2.614 0 001.17.268h-2.66z" fill="#1E5CB3"/> + <path d="M18.754 23.339c.63 0 1.129-.075 1.553-.278a2.298 2.298 0 00.967.274h.009c.02 0 .043 0 .065.004h.077-2.673.002z" fill="#1E5CB3"/> + <path d="M21.426 23.339h.146c.008 0 .017 0 .025-.004.016 0 .032 0 .05-.004h.024c.016 0 .035-.004.052-.006h.005c.005 0 .01-.004.016-.004.018-.004.038-.006.056-.009.006 0 .011 0 .018-.004l.064-.01h.01c.043-.008.085-.016.127-.027.125.046.257.07.39.069l-.982.004-.001-.005z" fill="#1D60B5"/> + <path d="M20.559 21.103v-.004c-.269.258-.55.504-.84.736-.144.114-.29.224-.44.33.123.147.249.285.38.411.201.194.415.358.649.484a2.424 2.424 0 00.812-.658c-.21-.357-.393-.799-.56-1.3" fill="#175FAB"/> + <path d="M28.904 11.001h-1.588c-.085.004-.169.007-.251.013-.039.083-.079.166-.117.25-1.314 2.8-2.695 5.225-4.096 7.153-.659 2.028-1.142 3.259-1.735 3.983.253.432.548.736.899.865.01-.004.018-.004.027-.006h.006c.006 0 .013-.004.02-.004l.044-.011c.009-.004.016-.005.026-.008l.039-.01c.008-.004.016-.005.024-.007l.059-.017a.169.169 0 01.029-.01l.033-.012.033-.01c.01-.005.02-.007.028-.01.023-.01.045-.016.068-.025.007 0 .013-.006.02-.007l.041-.016c.005-.004.011-.005.018-.007.048-.02.098-.04.147-.063h.005a4.21 4.21 0 00.214-.102c2.278-1.273 3.429-5.55 7.005-11.938h-1 .002z" fill="url(#paint14_linear)"/> + <path d="M9.126 11.282c.006 0 .01 0 .016.004a.13.13 0 01.042.005h.014l.075.01a1.473 1.473 0 00.077.015c.005 0 .01.003.016.003.049.01.1.02.147.033.005 0 .01.004.013.004l.06.017c.006 0 .01.004.016.005.024.006.047.012.07.02.001 0 .002 0 .005.004l.061.02c.009.005.016.006.024.008.014.007.03.013.044.018.008 0 .016.006.025.01.019.008.039.015.058.024l.03.012a1.675 1.675 0 00.066.03l.03.016c.254.124.488.29.704.492.078-.082.16-.157.25-.226a2.745 2.745 0 00-1.843-.518" fill="#7DCBEC"/> + <path d="M10.72 12.026c.135.127.262.264.378.409.114-.08.23-.157.347-.231a3.65 3.65 0 00-.477-.405c-.088.07-.172.145-.249.227" fill="#5EC5ED"/> + <path d="M9.126 11.282a2.749 2.749 0 011.842.518c.5-.394 1.103-.532 1.94-.532H8.851c.094 0 .185.006.275.014" fill="url(#paint15_linear)"/> + <path d="M12.908 11.268c-.838 0-1.44.138-1.94.531.17.121.33.257.477.405.694-.442 1.383-.735 2.061-.862.254-.049.512-.073.771-.074h-1.369z" fill="url(#paint16_linear)"/> + <path d="M22.854 18.421c.36-1.11.773-2.46 1.285-4.096.46-1.475 1.277-2.577 2.81-3.058.04-.082.079-.167.118-.25-2.499.15-3.667 1.42-4.255 3.303-1.028 3.288-1.65 5.418-2.25 6.776.765-.732 1.531-1.63 2.292-2.675" fill="url(#paint17_linear)"/> + <path d="M26.52 22.463h-.283v.88h-.115v-.88h-.285v-.105h.683v.105zm1.16.88h-.115v-.661c0-.052.004-.116.01-.191a.68.68 0 01-.031.096l-.335.756h-.057l-.336-.75a.526.526 0 01-.03-.102h-.004c.004.04.005.104.005.192v.66h-.11v-.985h.152l.302.688a.902.902 0 01.045.118h.004l.047-.121.309-.685h.145v.985z" fill="#000"/> + <defs> + <linearGradient id="paint0_linear" x1="14.014" y1="11.188" x2="14.014" y2="26.351" gradientUnits="userSpaceOnUse"> + <stop stop-color="#0994DC"/> + <stop offset=".35" stop-color="#66CEF5"/> + <stop offset=".846" stop-color="#127BCA"/> + <stop offset="1" stop-color="#127BCA"/> + </linearGradient> + <linearGradient id="paint1_linear" x1="14.592" y1="10.804" x2="14.592" y2="26.353" gradientUnits="userSpaceOnUse"> + <stop stop-color="#0E76BC"/> + <stop offset=".36" stop-color="#36AEE8"/> + <stop offset=".846" stop-color="#00ADEF"/> + <stop offset="1" stop-color="#00ADEF"/> + </linearGradient> + <linearGradient id="paint2_linear" x1="17.717" y1="22.99" x2="17.717" y2="10.617" gradientUnits="userSpaceOnUse"> + <stop stop-color="#1C63B7"/> + <stop offset=".5" stop-color="#33BDF2"/> + <stop offset="1" stop-color="#33BDF2" stop-opacity=".42"/> + </linearGradient> + <linearGradient id="paint3_linear" x1="16.904" y1="9.228" x2="16.904" y2="25.373" gradientUnits="userSpaceOnUse"> + <stop stop-color="#166AB8"/> + <stop offset=".4" stop-color="#36AEE8"/> + <stop offset=".846" stop-color="#0798DD"/> + <stop offset="1" stop-color="#0798DD"/> + </linearGradient> + <linearGradient id="paint4_linear" x1="15.632" y1="9.313" x2="15.632" y2="26.48" gradientUnits="userSpaceOnUse"> + <stop stop-color="#124379"/> + <stop offset=".39" stop-color="#1487CB"/> + <stop offset=".78" stop-color="#165197"/> + <stop offset="1" stop-color="#165197"/> + </linearGradient> + <linearGradient id="paint5_linear" x1="8.736" y1="14.583" x2="11.164" y2="14.583" gradientUnits="userSpaceOnUse"> + <stop stop-color="#33BDF2" stop-opacity=".698"/> + <stop offset="1" stop-color="#1DACD8"/> + </linearGradient> + <linearGradient id="paint6_linear" x1="26.965" y1="22.679" x2="26.965" y2="10.627" gradientUnits="userSpaceOnUse"> + <stop stop-color="#136AB4"/> + <stop offset=".6" stop-color="#59CAF5" stop-opacity=".549"/> + <stop offset="1" stop-color="#59CAF5" stop-opacity=".235"/> + </linearGradient> + <linearGradient id="paint7_linear" x1=".123" y1="17.463" x2="9.126" y2="17.463" gradientUnits="userSpaceOnUse"> + <stop stop-color="#05A1E6" stop-opacity=".247"/> + <stop offset="1" stop-color="#05A1E6"/> + </linearGradient> + <linearGradient id="paint8_linear" x1="5.109" y1="22.983" x2="5.109" y2="10.642" gradientUnits="userSpaceOnUse"> + <stop stop-color="#318ED5"/> + <stop offset="1" stop-color="#38A7E4"/> + </linearGradient> + <linearGradient id="paint9_linear" x1="5.886" y1="23.026" x2="5.886" y2="11.03" gradientUnits="userSpaceOnUse"> + <stop stop-color="#05A1E6"/> + <stop offset="1" stop-color="#05A1E6" stop-opacity=".549"/> + </linearGradient> + <linearGradient id="paint10_linear" x1="6.531" y1="23.639" x2="6.531" y2="13.502" gradientUnits="userSpaceOnUse"> + <stop stop-color="#1959A6"/> + <stop offset=".5" stop-color="#05A1E6"/> + <stop offset=".918" stop-color="#7EC5EA"/> + <stop offset="1" stop-color="#7EC5EA"/> + </linearGradient> + <linearGradient id="paint11_linear" x1="6.92" y1="22.991" x2="3.484" y2="12.49" gradientUnits="userSpaceOnUse"> + <stop stop-color="#165096"/> + <stop offset="1" stop-color="#0D82CA"/> + </linearGradient> + <linearGradient id="paint12_linear" x1="24.901" y1="16.775" x2="24.901" y2="11.031" gradientUnits="userSpaceOnUse"> + <stop stop-color="#05A1E6"/> + <stop offset=".874" stop-color="#0495D6"/> + <stop offset="1" stop-color="#0495D6"/> + </linearGradient> + <linearGradient id="paint13_linear" x1="27.601" y1="10.5" x2="22.248" y2="22.386" gradientUnits="userSpaceOnUse"> + <stop stop-color="#38A7E4" stop-opacity=".329"/> + <stop offset=".962" stop-color="#0E88D3"/> + <stop offset="1" stop-color="#0E88D3"/> + </linearGradient> + <linearGradient id="paint14_linear" x1="25.511" y1="10.965" x2="25.511" y2="23.496" gradientUnits="userSpaceOnUse"> + <stop stop-color="#168CD4"/> + <stop offset=".5" stop-color="#1C87CC"/> + <stop offset="1" stop-color="#154B8D"/> + </linearGradient> + <linearGradient id="paint15_linear" x1="9.527" y1="10.707" x2="9.582" y2="12.022" gradientUnits="userSpaceOnUse"> + <stop stop-color="#97D6EE"/> + <stop offset=".703" stop-color="#55C1EA"/> + <stop offset="1" stop-color="#55C1EA"/> + </linearGradient> + <linearGradient id="paint16_linear" x1="11.196" y1="12.145" x2="11.442" y2="10.753" gradientUnits="userSpaceOnUse"> + <stop stop-color="#7ACCEC"/> + <stop offset="1" stop-color="#3FB7ED"/> + </linearGradient> + <linearGradient id="paint17_linear" x1="23.814" y1="11.658" x2="23.814" y2="23.914" gradientUnits="userSpaceOnUse"> + <stop stop-color="#1DA7E7"/> + <stop offset="1" stop-color="#37ABE7" stop-opacity="0"/> + </linearGradient> + </defs> +</svg> diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/go.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/go.svg new file mode 100644 index 0000000000000..fb171e2813fac --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/go.svg @@ -0,0 +1,11 @@ +<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"> + <g clip-path="url(#clip0)" fill="#00ADD8"> + <path d="M2.414 13.15c-.062 0-.077-.031-.046-.078l.324-.417c.031-.047.109-.078.17-.078h5.525c.062 0 .077.047.046.093l-.263.402c-.03.047-.108.093-.155.093l-5.601-.015zM.077 14.573c-.061 0-.077-.03-.046-.077l.325-.418c.03-.046.108-.077.17-.077h7.056c.062 0 .093.046.078.093l-.124.371c-.016.062-.078.093-.14.093l-7.319.015zm3.745 1.424c-.062 0-.077-.046-.046-.093l.216-.387c.031-.046.093-.093.155-.093h3.095c.062 0 .093.047.093.109l-.031.371c0 .062-.062.108-.109.108l-3.373-.015zm16.062-3.126c-.975.248-1.64.433-2.6.681-.232.062-.247.077-.448-.155-.232-.263-.403-.433-.728-.588-.975-.48-1.918-.34-2.8.232-1.053.681-1.594 1.687-1.579 2.94.016 1.238.867 2.26 2.09 2.43 1.051.14 1.933-.232 2.63-1.021.139-.17.263-.356.418-.573H13.88c-.325 0-.402-.201-.294-.464.201-.48.573-1.285.79-1.687a.418.418 0 01.386-.247h5.633c-.031.417-.031.835-.093 1.253a6.598 6.598 0 01-1.27 3.033c-1.113 1.47-2.568 2.383-4.41 2.63-1.516.201-2.924-.093-4.162-1.02-1.145-.867-1.795-2.013-1.965-3.436-.201-1.687.294-3.203 1.315-4.534 1.1-1.439 2.554-2.352 4.333-2.677 1.455-.263 2.847-.093 4.1.758.82.542 1.409 1.285 1.796 2.182.093.14.03.217-.155.263z"/> + <path d="M25.006 21.428c-1.408-.03-2.693-.433-3.776-1.361-.913-.79-1.485-1.795-1.671-2.987-.279-1.748.201-3.296 1.253-4.673 1.13-1.486 2.492-2.26 4.333-2.584 1.578-.279 3.064-.124 4.41.789 1.223.836 1.98 1.965 2.182 3.45.263 2.09-.34 3.792-1.78 5.246-1.02 1.037-2.274 1.687-3.713 1.981-.418.077-.836.093-1.238.14zm3.683-6.251c-.016-.201-.016-.356-.047-.51-.278-1.533-1.686-2.4-3.157-2.059-1.439.325-2.367 1.238-2.707 2.693-.279 1.207.309 2.429 1.423 2.924.851.371 1.702.325 2.522-.093 1.223-.634 1.888-1.625 1.966-2.955z"/> + </g> + <defs> + <clipPath id="clip0"> + <path fill="#fff" d="M0 9.333h32v12.275H0z"/> + </clipPath> + </defs> +</svg> diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/java.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/java.svg new file mode 100644 index 0000000000000..52a410e2eaa1a --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/java.svg @@ -0,0 +1,7 @@ +<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path fill-rule="evenodd" clip-rule="evenodd" d="M11.729 24.747s-1.243.71.885.951c2.58.29 3.897.248 6.738-.28 0 0 .748.46 1.792.858-6.371 2.685-14.419-.155-9.415-1.53zm-.779-3.503s-1.394 1.014.736 1.231c2.755.28 4.93.303 8.695-.41 0 0 .52.52 1.338.803-7.702 2.215-16.28.174-10.769-1.624z" fill="#3174B9"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M17.512 15.3c1.57 1.778-.411 3.377-.411 3.377s3.986-2.023 2.155-4.557c-1.71-2.362-3.02-3.536 4.077-7.583 0 0-11.141 2.735-5.82 8.763z" fill="#CA3132"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M25.938 27.338s.92.746-1.013 1.323c-3.677 1.095-15.304 1.425-18.534.044-1.16-.497 1.016-1.186 1.701-1.332.714-.151 1.122-.124 1.122-.124-1.291-.894-8.346 1.756-3.583 2.516 12.988 2.071 23.675-.932 20.307-2.427zm-13.611-9.724s-5.914 1.381-2.094 1.884c1.613.212 4.827.163 7.823-.084a61.883 61.883 0 004.905-.634s-.862.363-1.487.782c-6.007 1.554-17.608.83-14.268-.758 2.824-1.343 5.12-1.19 5.12-1.19zm10.61 5.831c6.105-3.12 3.282-6.117 1.311-5.713a4.702 4.702 0 00-.698.184s.18-.276.522-.395c3.898-1.347 6.896 3.974-1.257 6.082 0 0 .094-.084.122-.158z" fill="#3174B9"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M19.256 0s3.38 3.326-3.207 8.44c-5.283 4.103-1.205 6.442-.002 9.115-3.084-2.736-5.347-5.144-3.83-7.386C14.448 6.879 20.62 5.283 19.257 0z" fill="#CA3132"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M12.927 31.9c5.86.368 14.86-.205 15.073-2.932 0 0-.41 1.033-4.843 1.853-5.002.926-11.172.819-14.83.225 0 0 .75.61 4.6.853z" fill="#3174B9"/> +</svg> diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/nodejs.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/nodejs.svg new file mode 100644 index 0000000000000..d327b1ba65ad2 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/nodejs.svg @@ -0,0 +1,46 @@ +<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"> + <mask id="a" maskUnits="userSpaceOnUse" x="5" y="4" width="22" height="24"> + <path d="M16.101 4.15a1.151 1.151 0 00-1.139 0L5.56 9.579c-.358.201-.559.58-.559.983v10.878c0 .402.223.782.559.983l9.403 5.427a1.152 1.152 0 001.14 0l9.403-5.427c.357-.201.559-.581.559-.983V10.56c0-.402-.224-.782-.559-.983l-9.404-5.427z" fill="#fff"/> + </mask> + <g mask="url(#a)"> + <path d="M16.101 4.15a1.151 1.151 0 00-1.139 0L5.56 9.579c-.358.201-.559.58-.559.983v10.878c0 .402.223.782.559.983l9.403 5.427a1.152 1.152 0 001.14 0l9.403-5.427c.357-.201.559-.581.559-.983V10.56c0-.402-.224-.782-.559-.983l-9.404-5.427z" fill="url(#paint0_linear)"/> + <path d="M25.527 9.577L16.08 4.15a1.55 1.55 0 00-.29-.112L5.2 22.175c.088.107.194.198.313.268l9.448 5.428c.268.157.581.201.871.112L25.773 9.8a1.173 1.173 0 00-.246-.223z" fill="url(#paint1_linear)"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M25.55 22.422c.267-.157.468-.425.558-.715L15.744 4.017c-.268-.045-.559-.023-.804.133L5.559 9.556l10.118 18.45c.147-.024.29-.07.424-.134l9.448-5.45z" fill="url(#paint2_linear)"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M25.55 22.422l-9.426 5.428a1.349 1.349 0 01-.424.134l.178.335 10.453-6.053v-.134l-.268-.447c-.044.313-.245.58-.514.737z" fill="url(#paint3_linear)"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M25.55 22.422l-9.426 5.428a1.349 1.349 0 01-.424.134l.178.335 10.453-6.053v-.134l-.268-.447c-.044.313-.245.58-.514.737z" fill="url(#paint4_linear)"/> + </g> + <defs> + <linearGradient id="paint0_linear" x1="19.363" y1="8.197" x2="9.056" y2="24.392" gradientUnits="userSpaceOnUse"> + <stop stop-color="#41873F"/> + <stop offset=".329" stop-color="#418B3D"/> + <stop offset=".635" stop-color="#419637"/> + <stop offset=".932" stop-color="#3FA92D"/> + <stop offset="1" stop-color="#3FAE2A"/> + </linearGradient> + <linearGradient id="paint1_linear" x1="14.104" y1="17.273" x2="39.918" y2="3.249" gradientUnits="userSpaceOnUse"> + <stop offset=".138" stop-color="#41873F"/> + <stop offset=".403" stop-color="#54A044"/> + <stop offset=".714" stop-color="#66B848"/> + <stop offset=".908" stop-color="#6CC04A"/> + </linearGradient> + <linearGradient id="paint2_linear" x1="4.657" y1="16" x2="26.416" y2="16" gradientUnits="userSpaceOnUse"> + <stop offset=".092" stop-color="#6CC04A"/> + <stop offset=".286" stop-color="#66B848"/> + <stop offset=".597" stop-color="#54A044"/> + <stop offset=".862" stop-color="#41873F"/> + </linearGradient> + <linearGradient id="paint3_linear" x1="4.657" y1="25.02" x2="26.416" y2="25.02" gradientUnits="userSpaceOnUse"> + <stop offset=".092" stop-color="#6CC04A"/> + <stop offset=".286" stop-color="#66B848"/> + <stop offset=".597" stop-color="#54A044"/> + <stop offset=".862" stop-color="#41873F"/> + </linearGradient> + <linearGradient id="paint4_linear" x1="29.586" y1="7.683" x2="24.073" y2="36.568" gradientUnits="userSpaceOnUse"> + <stop stop-color="#41873F"/> + <stop offset=".329" stop-color="#418B3D"/> + <stop offset=".635" stop-color="#419637"/> + <stop offset=".932" stop-color="#3FA92D"/> + <stop offset="1" stop-color="#3FAE2A"/> + </linearGradient> + </defs> +</svg> diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/php.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/php.svg new file mode 100644 index 0000000000000..c8af5dc331269 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/php.svg @@ -0,0 +1,18 @@ +<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M.45 15.63c0 4.518 6.962 8.18 15.55 8.18 8.588 0 15.55-3.662 15.55-8.18S24.588 7.45 16 7.45C7.412 7.45.45 11.112.45 15.63z" fill="url(#paint0_radial)"/> + <path d="M16 23.203c8.253 0 14.943-3.39 14.943-7.573 0-4.182-6.69-7.573-14.943-7.573-8.252 0-14.943 3.39-14.943 7.573 0 4.182 6.69 7.573 14.943 7.573z" fill="#777BB3"/> + <path d="M8.898 16.569c.679 0 1.186-.125 1.506-.372.317-.244.536-.667.651-1.257.107-.552.066-.937-.121-1.145-.192-.212-.606-.32-1.232-.32H8.618l-.601 3.094h.881zm-3.546 3.81a.168.168 0 01-.166-.2l1.593-8.198a.169.169 0 01.166-.137h3.434c1.079 0 1.882.293 2.387.871.508.581.664 1.394.466 2.414-.08.416-.22.802-.413 1.148a3.841 3.841 0 01-.76.952c-.372.349-.793.601-1.25.75-.45.146-1.029.22-1.719.22H7.7l-.397 2.044a.169.169 0 01-.166.136H5.352z" fill="#000"/> + <path d="M8.757 13.644h.946c.754 0 1.017.166 1.106.264.148.164.175.51.08 1-.106.548-.304.937-.588 1.155-.29.224-.762.337-1.403.337h-.676l.535-2.756zm1.622-1.969H6.945c-.162 0-.3.115-.331.274L5.02 20.146a.338.338 0 00.332.402h1.785c.162 0 .3-.115.332-.273l.37-1.907H9.09c.708 0 1.304-.077 1.77-.229.481-.156.923-.42 1.314-.786.324-.298.59-.632.792-.993.202-.362.347-.765.432-1.198.209-1.075.039-1.935-.505-2.558-.538-.616-1.384-.928-2.514-.928zm-2.567 5.062h1.086c.72 0 1.257-.135 1.609-.406.353-.272.59-.725.714-1.36.118-.608.064-1.038-.162-1.289-.226-.25-.678-.375-1.356-.375H8.479l-.667 3.43zm2.567-4.724c1.033 0 1.786.271 2.26.813.474.543.616 1.3.427 2.271-.078.4-.209.766-.394 1.098a3.663 3.663 0 01-.727.91 3.154 3.154 0 01-1.189.713c-.436.142-.99.213-1.666.213H7.56l-.423 2.18H5.352l1.593-8.198h3.434" fill="#fff"/> + <path d="M17.326 18.2a.168.168 0 01-.165-.201l.705-3.627c.067-.345.05-.593-.047-.697-.06-.064-.238-.172-.765-.172h-1.277l-.886 4.56a.169.169 0 01-.166.137h-1.771a.169.169 0 01-.166-.201L14.38 9.8a.169.169 0 01.166-.136h1.771a.169.169 0 01.166.2l-.384 1.98h1.373c1.047 0 1.756.184 2.17.563.42.387.552 1.007.39 1.84l-.741 3.815a.169.169 0 01-.166.137h-1.8z" fill="#000"/> + <path d="M16.318 9.496h-1.771c-.162 0-.3.115-.332.273l-1.593 8.198a.337.337 0 00.331.401h1.772c.162 0 .3-.114.331-.273l.86-4.423h1.138c.526 0 .637.113.641.117.032.035.074.194.005.55l-.705 3.628a.337.337 0 00.331.401h1.8c.162 0 .3-.114.331-.273l.742-3.814c.174-.896.025-1.568-.443-1.997-.445-.41-1.192-.609-2.283-.609h-1.169l.346-1.777a.337.337 0 00-.332-.402zm0 .338l-.423 2.179h1.578c.993 0 1.678.173 2.055.52.377.346.49.907.34 1.683l-.742 3.815h-1.8l.705-3.627c.08-.413.05-.694-.088-.844-.14-.15-.436-.225-.89-.225h-1.415l-.913 4.696h-1.772l1.594-8.197h1.771z" fill="#fff"/> + <path d="M22.836 16.569c.679 0 1.185-.125 1.506-.372.317-.244.536-.667.65-1.257.108-.552.067-.937-.12-1.145-.192-.212-.606-.32-1.232-.32h-1.084l-.601 3.094h.881zm-3.546 3.81a.168.168 0 01-.166-.2l1.593-8.198a.169.169 0 01.166-.137h3.434c1.079 0 1.882.293 2.387.871.508.581.664 1.393.466 2.414-.081.416-.22.802-.413 1.148-.194.346-.45.667-.76.952-.372.349-.793.601-1.25.75-.45.146-1.029.22-1.719.22h-1.39l-.397 2.044a.169.169 0 01-.166.136H19.29z" fill="#000"/> + <path d="M22.695 13.644h.945c.755 0 1.017.166 1.107.264.147.164.175.51.08 1-.107.548-.304.937-.588 1.155-.29.224-.762.337-1.403.337h-.676l.535-2.756zm1.622-1.969h-3.434c-.162 0-.3.115-.331.274l-1.594 8.197a.338.338 0 00.332.402h1.785c.162 0 .3-.115.331-.273l.37-1.907h1.252c.708 0 1.304-.077 1.77-.229.481-.156.923-.42 1.314-.787.324-.297.59-.631.792-.992.202-.362.347-.765.431-1.198.21-1.075.04-1.936-.504-2.558-.538-.616-1.384-.928-2.514-.928zm-2.567 5.062h1.086c.72 0 1.257-.135 1.609-.406.352-.272.59-.725.713-1.36.119-.608.065-1.038-.161-1.289-.226-.25-.678-.375-1.357-.375h-1.223l-.667 3.43zm2.567-4.724c1.033 0 1.786.271 2.26.813.474.543.616 1.3.427 2.271-.078.4-.21.766-.394 1.098a3.66 3.66 0 01-.727.91 3.154 3.154 0 01-1.189.713c-.436.142-.99.213-1.666.213h-1.53l-.423 2.18H19.29l1.593-8.198h3.434" fill="#fff"/> + <defs> + <radialGradient id="paint0_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(9.786 10.326) scale(20.4194)"> + <stop stop-color="#AEB2D5"/> + <stop offset=".3" stop-color="#AEB2D5"/> + <stop offset=".75" stop-color="#484C89"/> + <stop offset="1" stop-color="#484C89"/> + </radialGradient> + </defs> +</svg> diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/python.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/python.svg new file mode 100644 index 0000000000000..9b8d0a2836c28 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/python.svg @@ -0,0 +1,19 @@ +<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"> + <g clip-path="url(#clip0)"> + <path d="M15.897 4.007c-6.078 0-5.698 2.635-5.698 2.635l.007 2.73h5.8v.82H7.901s-3.889-.44-3.889 5.691c0 6.133 3.394 5.915 3.394 5.915h2.026v-2.845s-.109-3.395 3.34-3.395h5.753s3.231.053 3.231-3.123v-5.25s.491-3.178-5.86-3.178zm-3.198 1.836a1.043 1.043 0 11.002 2.086 1.043 1.043 0 01-.002-2.086z" fill="url(#paint0_linear)"/> + <path d="M16.07 27.822c6.077 0 5.698-2.635 5.698-2.635l-.007-2.73h-5.8v-.82h8.103s3.89.44 3.89-5.692c0-6.132-3.395-5.915-3.395-5.915h-2.026v2.846s.11 3.394-3.34 3.394h-5.752s-3.232-.052-3.232 3.124v5.25s-.49 3.178 5.86 3.178zm3.198-1.836a1.04 1.04 0 01-1.044-1.043 1.041 1.041 0 011.443-.965 1.042 1.042 0 010 1.929 1.04 1.04 0 01-.4.079z" fill="url(#paint1_linear)"/> + </g> + <defs> + <linearGradient id="paint0_linear" x1="6.314" y1="6.149" x2="18.178" y2="17.894" gradientUnits="userSpaceOnUse"> + <stop stop-color="#387EB8"/> + <stop offset="1" stop-color="#366994"/> + </linearGradient> + <linearGradient id="paint1_linear" x1="13.596" y1="13.691" x2="26.337" y2="25.735" gradientUnits="userSpaceOnUse"> + <stop stop-color="#FFE052"/> + <stop offset="1" stop-color="#FFC331"/> + </linearGradient> + <clipPath id="clip0"> + <path fill="#fff" d="M4 4h24v23.904H4z"/> + </clipPath> + </defs> +</svg> diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/ruby.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/ruby.svg new file mode 100644 index 0000000000000..fdc54b91f9c29 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/ruby.svg @@ -0,0 +1,125 @@ +<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"> + <g clip-path="url(#clip0)"> + <path d="M22.51 19.726l-13.64 8.1 17.662-1.198 1.36-17.81-5.381 10.908z" fill="url(#paint0_linear)"/> + <path d="M26.561 26.616l-1.518-10.478-4.135 5.46 5.653 5.018z" fill="url(#paint1_linear)"/> + <path d="M26.582 26.616l-11.122-.873-6.532 2.06 17.654-1.187z" fill="url(#paint2_linear)"/> + <path d="M8.944 27.806l2.779-9.102-6.114 1.307 3.335 7.795z" fill="url(#paint3_linear)"/> + <path d="M20.907 21.628l-2.556-10.014-7.317 6.858 9.873 3.156z" fill="url(#paint4_linear)"/> + <path d="M27.313 11.755l-6.916-5.648-1.926 6.226 8.842-.578z" fill="url(#paint5_linear)"/> + <path d="M24.078 4.093L20.011 6.34l-2.566-2.278 6.633.03z" fill="url(#paint6_linear)"/> + <path d="M4 23.064l1.704-3.107-1.378-3.702L4 23.065z" fill="url(#paint7_linear)"/> + <path d="M4.234 16.138l1.387 3.933 6.026-1.352 6.88-6.394 1.941-6.166L17.411 4l-5.198 1.945c-1.637 1.523-4.815 4.537-4.93 4.593-.113.058-2.098 3.81-3.049 5.6z" fill="#fff"/> + <path d="M9.104 9.071c3.549-3.519 8.124-5.598 9.88-3.826 1.754 1.771-.107 6.076-3.656 9.594s-8.067 5.711-9.822 3.94c-1.756-1.77.048-6.19 3.598-9.708z" fill="url(#paint8_linear)"/> + <path d="M8.944 27.802l2.757-9.13 9.155 2.94c-3.31 3.104-6.992 5.729-11.912 6.19z" fill="url(#paint9_linear)"/> + <path d="M18.539 12.308l2.35 9.31c2.765-2.908 5.247-6.034 6.462-9.9l-8.812.59z" fill="url(#paint10_linear)"/> + <path d="M27.327 11.765c.94-2.839 1.158-6.911-3.278-7.667l-3.64 2.01 6.918 5.657z" fill="url(#paint11_linear)"/> + <path d="M4 23.023c.13 4.686 3.51 4.755 4.95 4.796l-3.326-7.767L4 23.023z" fill="#9E1209"/> + <path d="M18.552 12.322c2.125 1.306 6.407 3.929 6.494 3.977.135.076 1.846-2.886 2.234-4.56l-8.728.583z" fill="url(#paint12_radial)"/> + <path d="M11.697 18.671l3.686 7.11c2.179-1.182 3.885-2.621 5.448-4.164l-9.134-2.946z" fill="url(#paint13_radial)"/> + <path d="M5.61 20.062l-.522 6.217c.985 1.346 2.34 1.463 3.762 1.358-1.028-2.56-3.083-7.68-3.24-7.575z" fill="url(#paint14_linear)"/> + <path d="M20.388 6.123l7.321 1.028c-.39-1.656-1.59-2.724-3.635-3.058l-3.686 2.03z" fill="url(#paint15_linear)"/> + </g> + <defs> + <linearGradient id="paint0_linear" x1="24.992" y1="29.993" x2="19.957" y2="21.091" gradientUnits="userSpaceOnUse"> + <stop stop-color="#FB7655"/> + <stop offset=".41" stop-color="#E42B1E"/> + <stop offset=".99" stop-color="#900"/> + <stop offset="1" stop-color="#900"/> + </linearGradient> + <linearGradient id="paint1_linear" x1="27.503" y1="22.518" x2="20.425" y2="21.135" gradientUnits="userSpaceOnUse"> + <stop stop-color="#871101"/> + <stop offset=".99" stop-color="#911209"/> + <stop offset="1" stop-color="#911209"/> + </linearGradient> + <linearGradient id="paint2_linear" x1="22.305" y1="30.263" x2="22.214" y2="25.774" gradientUnits="userSpaceOnUse"> + <stop stop-color="#871101"/> + <stop offset=".99" stop-color="#911209"/> + <stop offset="1" stop-color="#911209"/> + </linearGradient> + <linearGradient id="paint3_linear" x1="8.666" y1="19.362" x2="10.771" y2="25.533" gradientUnits="userSpaceOnUse"> + <stop stop-color="#fff"/> + <stop offset=".23" stop-color="#E57252"/> + <stop offset=".46" stop-color="#DE3B20"/> + <stop offset=".99" stop-color="#A60003"/> + <stop offset="1" stop-color="#A60003"/> + </linearGradient> + <linearGradient id="paint4_linear" x1="15.593" y1="13.251" x2="15.975" y2="19.93" gradientUnits="userSpaceOnUse"> + <stop stop-color="#fff"/> + <stop offset=".23" stop-color="#E4714E"/> + <stop offset=".56" stop-color="#BE1A0D"/> + <stop offset=".99" stop-color="#A80D00"/> + <stop offset="1" stop-color="#A80D00"/> + </linearGradient> + <linearGradient id="paint5_linear" x1="21.739" y1="7.078" x2="22.297" y2="11.928" gradientUnits="userSpaceOnUse"> + <stop stop-color="#fff"/> + <stop offset=".18" stop-color="#E46342"/> + <stop offset=".4" stop-color="#C82410"/> + <stop offset=".99" stop-color="#A80D00"/> + <stop offset="1" stop-color="#A80D00"/> + </linearGradient> + <linearGradient id="paint6_linear" x1="18.347" y1="5.392" x2="19.134" y2="2.055" gradientUnits="userSpaceOnUse"> + <stop stop-color="#fff"/> + <stop offset=".54" stop-color="#C81F11"/> + <stop offset=".99" stop-color="#BF0905"/> + <stop offset="1" stop-color="#BF0905"/> + </linearGradient> + <linearGradient id="paint7_linear" x1="4.471" y1="17.694" x2="6.529" y2="18.984" gradientUnits="userSpaceOnUse"> + <stop stop-color="#fff"/> + <stop offset=".31" stop-color="#DE4024"/> + <stop offset=".99" stop-color="#BF190B"/> + <stop offset="1" stop-color="#BF190B"/> + </linearGradient> + <linearGradient id="paint8_linear" x1="1.762" y1="22.704" x2="20.255" y2="3.635" gradientUnits="userSpaceOnUse"> + <stop stop-color="#BD0012"/> + <stop offset=".07" stop-color="#fff"/> + <stop offset=".17" stop-color="#fff"/> + <stop offset=".27" stop-color="#C82F1C"/> + <stop offset=".33" stop-color="#820C01"/> + <stop offset=".46" stop-color="#A31601"/> + <stop offset=".72" stop-color="#B31301"/> + <stop offset=".99" stop-color="#E82609"/> + <stop offset="1" stop-color="#E82609"/> + </linearGradient> + <linearGradient id="paint9_linear" x1="15.948" y1="24.625" x2="10.714" y2="22.427" gradientUnits="userSpaceOnUse"> + <stop stop-color="#8C0C01"/> + <stop offset=".54" stop-color="#990C00"/> + <stop offset=".99" stop-color="#A80D0E"/> + <stop offset="1" stop-color="#A80D0E"/> + </linearGradient> + <linearGradient id="paint10_linear" x1="25.529" y1="17.93" x2="20.138" y2="14.101" gradientUnits="userSpaceOnUse"> + <stop stop-color="#7E110B"/> + <stop offset=".99" stop-color="#9E0C00"/> + <stop offset="1" stop-color="#9E0C00"/> + </linearGradient> + <linearGradient id="paint11_linear" x1="27.349" y1="9.781" x2="24.814" y2="7.207" gradientUnits="userSpaceOnUse"> + <stop stop-color="#79130D"/> + <stop offset=".99" stop-color="#9E120B"/> + <stop offset="1" stop-color="#9E120B"/> + </linearGradient> + <linearGradient id="paint14_linear" x1="7.216" y1="27.797" x2="2.671" y2="24.024" gradientUnits="userSpaceOnUse"> + <stop stop-color="#8B2114"/> + <stop offset=".43" stop-color="#9E100A"/> + <stop offset=".99" stop-color="#B3100C"/> + <stop offset="1" stop-color="#B3100C"/> + </linearGradient> + <linearGradient id="paint15_linear" x1="22.648" y1="5.181" x2="23.939" y2="8.445" gradientUnits="userSpaceOnUse"> + <stop stop-color="#B31000"/> + <stop offset=".44" stop-color="#910F08"/> + <stop offset=".99" stop-color="#791C12"/> + <stop offset="1" stop-color="#791C12"/> + </linearGradient> + <radialGradient id="paint12_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(6.07279 0 0 3.17318 21.345 13.573)"> + <stop stop-color="#A80D00"/> + <stop offset=".99" stop-color="#7E0E08"/> + <stop offset="1" stop-color="#7E0E08"/> + </radialGradient> + <radialGradient id="paint13_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(8.07284 0 0 6.28431 12.935 21.576)"> + <stop stop-color="#A30C00"/> + <stop offset=".99" stop-color="#800E08"/> + <stop offset="1" stop-color="#800E08"/> + </radialGradient> + <clipPath id="clip0"> + <path fill="#fff" d="M4 4h24v23.904H4z"/> + </clipPath> + </defs> +</svg> diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/rumjs.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/rumjs.svg new file mode 100644 index 0000000000000..87043159ed8c3 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/rumjs.svg @@ -0,0 +1,3 @@ +<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M5 20.245l2.438-1.485c.47.84.898 1.55 1.924 1.55.984 0 1.604-.387 1.604-1.894V8.172h2.994V18.46c0 3.12-1.818 4.54-4.47 4.54-2.394 0-3.784-1.248-4.49-2.754zm10.586-.323l2.438-1.42c.642 1.055 1.476 1.83 2.95 1.83 1.241 0 2.032-.625 2.032-1.486 0-1.033-.812-1.398-2.18-2l-.75-.324c-2.159-.925-3.592-2.087-3.592-4.54 0-2.26 1.711-3.982 4.384-3.982 1.903 0 3.272.667 4.255 2.41l-2.33 1.507c-.514-.925-1.07-1.291-1.925-1.291-.877 0-1.433.56-1.433 1.29 0 .905.556 1.27 1.84 1.83l.748.323C24.567 15.167 26 16.286 26 18.803 26 21.515 23.883 23 21.039 23c-2.78 0-4.576-1.334-5.453-3.078" fill="#000"/> +</svg> diff --git a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap index 8f87b3473b2e4..b4b4e7866e9b7 100644 --- a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap +++ b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap @@ -66,6 +66,8 @@ exports[`Error SERVICE_AGENT_NAME 1`] = `"java"`; exports[`Error SERVICE_ENVIRONMENT 1`] = `undefined`; +exports[`Error SERVICE_FRAMEWORK_NAME 1`] = `undefined`; + exports[`Error SERVICE_NAME 1`] = `"service name"`; exports[`Error SERVICE_NODE_NAME 1`] = `undefined`; @@ -176,6 +178,8 @@ exports[`Span SERVICE_AGENT_NAME 1`] = `"java"`; exports[`Span SERVICE_ENVIRONMENT 1`] = `undefined`; +exports[`Span SERVICE_FRAMEWORK_NAME 1`] = `undefined`; + exports[`Span SERVICE_NAME 1`] = `"service name"`; exports[`Span SERVICE_NODE_NAME 1`] = `undefined`; @@ -286,6 +290,8 @@ exports[`Transaction SERVICE_AGENT_NAME 1`] = `"java"`; exports[`Transaction SERVICE_ENVIRONMENT 1`] = `undefined`; +exports[`Transaction SERVICE_FRAMEWORK_NAME 1`] = `undefined`; + exports[`Transaction SERVICE_NAME 1`] = `"service name"`; exports[`Transaction SERVICE_NODE_NAME 1`] = `undefined`; diff --git a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts index ce2db4964a412..14233aad0f53c 100644 --- a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts +++ b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts @@ -7,6 +7,7 @@ export const SERVICE_NAME = 'service.name'; export const SERVICE_ENVIRONMENT = 'service.environment'; export const SERVICE_AGENT_NAME = 'agent.name'; +export const SERVICE_FRAMEWORK_NAME = 'service.framework.name'; export const SERVICE_NODE_NAME = 'service.node.name'; export const SERVICE_VERSION = 'service.version'; export const URL_FULL = 'url.full'; diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts index 548b29346e483..f4354baa97655 100644 --- a/x-pack/plugins/apm/common/service_map.ts +++ b/x-pack/plugins/apm/common/service_map.ts @@ -10,6 +10,7 @@ import { ILicense } from '../../licensing/public'; export interface ServiceConnectionNode { 'service.name': string; 'service.environment': string | null; + 'service.framework.name': string | null; 'agent.name': string; } export interface ExternalConnectionNode { diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts index 04e2a43a4b8f1..85d71784b55c7 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts @@ -16,7 +16,8 @@ import { getServicesProjection } from '../../../common/projections/services'; import { mergeProjection } from '../../../common/projections/util/merge_projection'; import { SERVICE_AGENT_NAME, - SERVICE_NAME + SERVICE_NAME, + SERVICE_FRAMEWORK_NAME } from '../../../common/elasticsearch_fieldnames'; export interface IEnvOptions { @@ -92,6 +93,11 @@ async function getServicesData(options: IEnvOptions) { terms: { field: SERVICE_AGENT_NAME } + }, + service_framework_name: { + terms: { + field: SERVICE_FRAMEWORK_NAME + } } } } @@ -109,7 +115,11 @@ async function getServicesData(options: IEnvOptions) { 'service.name': bucket.key as string, 'agent.name': (bucket.agent_name.buckets[0]?.key as string | undefined) || '', - 'service.environment': options.environment || null + 'service.environment': options.environment || null, + 'service.framework.name': + (bucket.service_framework_name.buckets[0]?.key as + | string + | undefined) || null }; }) || [] ); diff --git a/x-pack/plugins/apm/server/lib/services/map.ts b/x-pack/plugins/apm/server/lib/services/map.ts deleted file mode 100644 index 97bb925674e26..0000000000000 --- a/x-pack/plugins/apm/server/lib/services/map.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import cytoscape from 'cytoscape'; -import { PromiseReturnType } from '../../../typings/common'; - -// This response right now just returns experimental data. -export type ServiceMapResponse = PromiseReturnType<typeof getServiceMap>; -export async function getServiceMap(): Promise<cytoscape.ElementDefinition[]> { - return [ - { data: { id: 'client', agentName: 'js-base' } }, - { data: { id: 'opbeans-node', agentName: 'nodejs' } }, - { data: { id: 'opbeans-python', agentName: 'python' } }, - { data: { id: 'opbeans-java', agentName: 'java' } }, - { data: { id: 'opbeans-ruby', agentName: 'ruby' } }, - { data: { id: 'opbeans-go', agentName: 'go' } }, - { data: { id: 'opbeans-go-2', agentName: 'go' } }, - { data: { id: 'opbeans-dotnet', agentName: 'dotnet' } }, - { data: { id: 'database', agentName: 'database' } }, - { data: { id: 'external API', agentName: 'external' } }, - - { - data: { - id: 'opbeans-client~opbeans-node', - source: 'client', - target: 'opbeans-node' - } - }, - { - data: { - id: 'opbeans-client~opbeans-python', - source: 'client', - target: 'opbeans-python' - } - }, - { - data: { - id: 'opbeans-python~opbeans-go', - source: 'opbeans-python', - target: 'opbeans-go' - } - }, - { - data: { - id: 'opbeans-python~opbeans-go-2', - source: 'opbeans-python', - target: 'opbeans-go-2' - } - }, - { - data: { - id: 'opbeans-python~opbeans-dotnet', - source: 'opbeans-python', - target: 'opbeans-dotnet' - } - }, - { - data: { - id: 'opbeans-node~opbeans-java', - source: 'opbeans-node', - target: 'opbeans-java' - } - }, - { - data: { - id: 'opbeans-node~database', - source: 'opbeans-node', - target: 'database' - } - }, - { - data: { - id: 'opbeans-go-2~opbeans-ruby', - source: 'opbeans-go-2', - target: 'opbeans-ruby' - } - }, - { - data: { - id: 'opbeans-go-2~external API', - source: 'opbeans-go-2', - target: 'external API' - } - } - ]; -} From ffab68d01bd4cf1abee7a8278329f30dada065c4 Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Mon, 2 Mar 2020 19:32:04 -0500 Subject: [PATCH 24/34] [Endpoint] Alert Details Overview (#58412) --- x-pack/plugins/endpoint/common/types.ts | 144 ++++++++++++++--- .../endpoint/store/alerts/action.ts | 9 +- .../store/alerts/alert_details.test.ts | 65 ++++++++ .../endpoint/store/alerts/middleware.ts | 11 +- .../store/alerts/mock_alert_result_list.ts | 153 ++++++++++++++++-- .../endpoint/store/alerts/reducer.ts | 6 + .../endpoint/store/alerts/selectors.ts | 25 ++- .../public/applications/endpoint/types.ts | 11 +- .../endpoint/view/alerts/details/index.ts | 7 + .../details/metadata/file_accordion.tsx | 80 +++++++++ .../details/metadata/general_accordion.tsx | 68 ++++++++ .../details/metadata/hash_accordion.tsx | 49 ++++++ .../details/metadata/host_accordion.tsx | 55 +++++++ .../view/alerts/details/metadata/index.ts | 12 ++ .../metadata/source_process_accordion.tsx | 97 +++++++++++ .../source_process_token_accordion.tsx | 45 ++++++ .../view/alerts/details/overview/index.tsx | 94 +++++++++++ .../details/overview/metadata_panel.tsx | 40 +++++ .../endpoint/view/alerts/formatted_date.tsx | 22 +++ .../endpoint/view/alerts/index.test.tsx | 3 - .../endpoint/view/alerts/index.tsx | 97 ++++++----- .../endpoint/view/alerts/resolver.tsx | 1 + 22 files changed, 986 insertions(+), 108 deletions(-) create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_details.test.ts create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/index.ts create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/file_accordion.tsx create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/general_accordion.tsx create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/hash_accordion.tsx create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/host_accordion.tsx create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/index.ts create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/source_process_accordion.tsx create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/source_process_token_accordion.tsx create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/index.tsx create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/metadata_panel.tsx create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/formatted_date.tsx diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index 6d904fda6f747..d804350a9002d 100644 --- a/x-pack/plugins/endpoint/common/types.ts +++ b/x-pack/plugins/endpoint/common/types.ts @@ -96,6 +96,59 @@ export interface EndpointResultList { request_page_index: number; } +export interface OSFields { + full: string; + name: string; + version: string; + variant: string; +} +export interface HostFields { + id: string; + hostname: string; + ip: string[]; + mac: string[]; + os: OSFields; +} +export interface HashFields { + md5: string; + sha1: string; + sha256: string; +} +export interface MalwareClassifierFields { + identifier: string; + score: number; + threshold: number; + version: string; +} +export interface PrivilegesFields { + description: string; + name: string; + enabled: boolean; +} +export interface ThreadFields { + id: number; + service_name: string; + start: number; + start_address: number; + start_address_module: string; +} +export interface DllFields { + pe: { + architecture: string; + imphash: string; + }; + code_signature: { + subject_name: string; + trusted: boolean; + }; + compile_time: number; + hash: HashFields; + malware_classifier: MalwareClassifierFields; + mapped_address: number; + mapped_size: number; + path: string; +} + /** * Describes an Alert Event. * Should be in line with ECS schema. @@ -109,26 +162,78 @@ export type AlertEvent = Immutable<{ event: { id: string; action: string; + category: string; + kind: string; + dataset: string; + module: string; + type: string; }; - file_classification: { - malware_classification: { - score: number; + process: { + code_signature: { + subject_name: string; + trusted: boolean; }; - }; - process?: { - unique_pid: number; + command_line: string; + domain: string; pid: number; + ppid: number; + entity_id: string; + parent: { + pid: number; + entity_id: string; + }; + name: string; + hash: HashFields; + pe: { + imphash: string; + }; + executable: string; + sid: string; + start: number; + malware_classifier: MalwareClassifierFields; + token: { + domain: string; + type: string; + user: string; + sid: string; + integrity_level: number; + integrity_level_name: string; + privileges: PrivilegesFields[]; + }; + thread: ThreadFields[]; + uptime: number; + user: string; }; - host: { - hostname: string; - ip: string; - os: { - name: string; + file: { + owner: string; + name: string; + path: string; + accessed: number; + mtime: number; + created: number; + size: number; + hash: HashFields; + pe: { + imphash: string; + }; + code_signature: { + trusted: boolean; + subject_name: string; }; + malware_classifier: { + features: { + data: { + buffer: string; + decompressed_size: number; + encoding: string; + }; + }; + } & MalwareClassifierFields; + temp_file_path: string; }; + host: HostFields; thread: {}; - endpoint?: {}; - endgame?: {}; + dll: DllFields[]; }>; /** @@ -161,18 +266,7 @@ export interface EndpointMetadata { id: string; name: string; }; - host: { - id: string; - hostname: string; - ip: string[]; - mac: string[]; - os: { - name: string; - full: string; - version: string; - variant: string; - }; - }; + host: HostFields; } /** diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/action.ts index a628a95003a7f..6c6310a7349ed 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/action.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/action.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Immutable } from '../../../../../common/types'; +import { Immutable, AlertData } from '../../../../../common/types'; import { AlertListData } from '../../types'; interface ServerReturnedAlertsData { @@ -12,4 +12,9 @@ interface ServerReturnedAlertsData { readonly payload: Immutable<AlertListData>; } -export type AlertAction = ServerReturnedAlertsData; +interface ServerReturnedAlertDetailsData { + readonly type: 'serverReturnedAlertDetailsData'; + readonly payload: Immutable<AlertData>; +} + +export type AlertAction = ServerReturnedAlertsData | ServerReturnedAlertDetailsData; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_details.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_details.test.ts new file mode 100644 index 0000000000000..4edc31831eb14 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_details.test.ts @@ -0,0 +1,65 @@ +/* + * 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 { Store, createStore, applyMiddleware } from 'redux'; +import { History } from 'history'; +import { alertListReducer } from './reducer'; +import { AlertListState } from '../../types'; +import { alertMiddlewareFactory } from './middleware'; +import { AppAction } from '../action'; +import { coreMock } from 'src/core/public/mocks'; +import { createBrowserHistory } from 'history'; + +describe('alert details tests', () => { + let store: Store<AlertListState, AppAction>; + let coreStart: ReturnType<typeof coreMock.createStart>; + let history: History<never>; + /** + * A function that waits until a selector returns true. + */ + let selectorIsTrue: (selector: (state: AlertListState) => boolean) => Promise<void>; + beforeEach(() => { + coreStart = coreMock.createStart(); + history = createBrowserHistory(); + const middleware = alertMiddlewareFactory(coreStart); + store = createStore(alertListReducer, applyMiddleware(middleware)); + + selectorIsTrue = async selector => { + // If the selector returns true, we're done + while (selector(store.getState()) !== true) { + // otherwise, wait til the next state change occurs + await new Promise(resolve => { + const unsubscribe = store.subscribe(() => { + unsubscribe(); + resolve(); + }); + }); + } + }; + }); + describe('when the user is on the alert list page with a selected alert in the url', () => { + beforeEach(() => { + const firstResponse: Promise<unknown> = Promise.resolve(1); + const secondResponse: Promise<unknown> = Promise.resolve(2); + coreStart.http.get.mockReturnValueOnce(firstResponse).mockReturnValueOnce(secondResponse); + + // Simulates user navigating to the /alerts page + store.dispatch({ + type: 'userChangedUrl', + payload: { + ...history.location, + pathname: '/alerts', + search: '?selected_alert=q9ncfh4q9ctrmc90umcq4', + }, + }); + }); + + it('should return alert details data', async () => { + // wait for alertDetails to be defined + await selectorIsTrue(state => state.alertDetails !== undefined); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts index 76a6867418bd8..2cb381e901b4e 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertResultList } from '../../../../../common/types'; +import { AlertResultList, AlertData } from '../../../../../common/types'; import { AppAction } from '../action'; import { MiddlewareFactory, AlertListState } from '../../types'; -import { isOnAlertPage, apiQueryParams } from './selectors'; +import { isOnAlertPage, apiQueryParams, hasSelectedAlert, uiQueryParams } from './selectors'; export const alertMiddlewareFactory: MiddlewareFactory<AlertListState> = coreStart => { return api => next => async (action: AppAction) => { @@ -19,5 +19,12 @@ export const alertMiddlewareFactory: MiddlewareFactory<AlertListState> = coreSta }); api.dispatch({ type: 'serverReturnedAlertsData', payload: response }); } + if (action.type === 'userChangedUrl' && isOnAlertPage(state) && hasSelectedAlert(state)) { + const uiParams = uiQueryParams(state); + const response: AlertData = await coreStart.http.get( + `/api/endpoint/alerts/${uiParams.selected_alert}` + ); + api.dispatch({ type: 'serverReturnedAlertDetailsData', payload: response }); + } }; }; 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 8eadb3e7fb3df..7db94fc9d4266 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 @@ -32,29 +32,152 @@ export const mockAlertResultList: (options?: { id: 'ced9c68e-b94a-4d66-bb4c-6106514f0a2f', version: '3.0.0', }, - event: { - id: '2f1c0928-3876-4e11-acbb-9199257c7b1c', - action: 'open', - }, - file_classification: { - malware_classification: { - score: 3, - }, - }, - process: { - pid: 107, - unique_pid: 1, - }, host: { + id: 'xrctvybuni', hostname: 'HD-c15-bc09190a', - ip: '10.179.244.14', + ip: ['10.179.244.14'], + mac: ['xsertcyvbunimkn56edtyf'], os: { - name: 'Windows', + full: 'Windows 10', + name: 'windows', + version: '10', + variant: '3', }, }, thread: {}, prev: null, next: null, + event: { + id: '2f1c0928-3876-4e11-acbb-9199257c7b1c', + action: 'creation', + category: 'malware', + dataset: 'endpoint', + kind: 'alert', + module: 'endpoint', + type: 'creation', + }, + file: { + accessed: 1542789400, + created: 1542789400, + hash: { + md5: '4ace3baaa509d08510405e1b169e325b', + sha1: '27fb21cf5db95ffca43b234affa99becc4023b9d', + sha256: '6ed1c836dbf099be7845bdab7671def2c157643761b52251e04e9b6ee109ec75', + }, + pe: { + imphash: '835d619dfdf3cc727cebd91300ab3462', + }, + mtime: 1542789400, + owner: 'Administrators', + name: 'test name', + path: 'C:\\Windows\\TEMP\\tmp0000008f\\tmp00001be5', + size: 188416, + code_signature: { + subject_name: 'Cybereason Inc', + trusted: false, + }, + malware_classifier: { + features: { + data: { + buffer: + 'eAHtnU1oHHUUwHsQ7MGDiIIUD4sH8WBBxJtopiLoUY0pYo2ZTbJJ0yQ17m4+ms/NRzeVWpuUWCL4sWlEYvFQ8KJQ6NCTEA8eRD30sIo3PdSriLi7837Pko3LbHZ2M5m+XObHm/d/X////83O7jCZvzacHBpPplNdfalkdjSdyty674Ft59dN71Dpb9v5eKh8LMEHjsCF2wIfVlRKsHROYPGkQO5+gY2vBSYYdWZFYGwEO/cITHMqkxPYnBBY+07gtCuQ9gSGigJ5lPPYGXcE+jA4z3Ad1ZtAUiDUyrEEPYzqRnIKgxd/Rgc7gygPo5wn95PouN7OeEYJ1UXiJgRmvscgp/LOziIkkSyT+xRVnXhZ4DKh5goCkzidRHkGO4uvCyw9LDDtCay8ILCAzrJOJaGuZwUuvSewivJVIPsklq8JbL4qMJsTSCcExrGs83WKU295ZFo5lr2TaZbcUw5FeJy8tgTeLpCy2iGeS67ABXzlgbEi1UC5FxcZnA4y/CLK82Qxi847FGGZRTLsCUxR1aWEwOp1AmOjDRYYzgwusL9WfqBiGJxnVAanixTq7Dp22LBdlWMJzlOx8wmBK2Rx5WmBLJIRwtAijOQE+ooCb2B5xBOYRtlfNeXpLpA7oyZRTqHzGenkmIJPnhBIMrzTwSA6H93CO5l+c1NA99f6IwLH8fUKdjTmDpTbgS50+gGVnECnE4PpooC2guPoaPADSHrcncNHmEHtAFkq3+EI+A37zsrrTvH3WTkvJLoOTyBp10wx2JcgVCRahA4NrICE4a+hrMXsA3qAHItW188E8ejO7XV3eh/KCYwxlamEwCgL8lN2wTntfrhY/U0g/5KAdvUpT+AszWqBdqH7VLeeZrExK9Cv1UgIDKA8g/cx7QAEP+AhAfRaMKB2HOJh+BSFSqKjSytNGBlc6PrpxvK7lCVDxbSG3Z7AhCMwx6gelwgLAltXBXJUTH29j+U1LHdipx/QprfKfGnF0sBpdBYxmEQyTzW0h6/0khcuhhJYRufym+i4VKMocJMs/KvfoW3/UJb4PeZOSZVONThZz4djP/75TAXa/CVfOvX3RgVLIDreLPN1pP1osW7lGmHsEhjBOzf+EPBE4vndvWz5xb/cChxGcv1LAb+tluALKnZ47isf1MXvz1ZMlsCXbXtPceqhrcp1ps6YHwQeBXLEPCf7q23tl9uJui0bGBgYRAccv7uXr/g5Af+2oNTrpgTa/vnpjBvpLAwM4gRBPvIZGBgYGBgYGBgYGBgYGBgYGBgYGBgYNAOc9oMXs4GBgYFBcNBnww5QzDXgRtPSaZ5lg/itsRaslgZ3bnWEEVnhMetIBwiiVnlbCbWrEftrt11zdwWnseFW1QO63w1is3ptD1pV9xG0t+zvfUrzrvh380qwXWAVCw6h78GIfG7ZlzltXu6hd+y92fECRFhjuH3bXG8N43oXEHperdzvUbteaDxhVTUeq25fqhG1X6Ai8mtF6BDXz2wR+dzSgg4Qsxls5T11XMG+82y8GkG+b7kL69xg7mF1SFvhBgYGsYH/Xi7HE+PVkiB2jt1bNZxT+k4558jR53ydz5//1m1KOgYGBgYGBgYGEQfnsYaG2z1sdPJS79XQSu91ndobOAHCaN5vNzUk1bceQVzUpbw3iOuT+UFmR18bHrp3gyhDC56lCd1y85w2+HSNUwVhhdGC7blLf+bV/fqtvhMg1NDjCcugB1QXswbs8ekj/v1BgzFHBIIsyP+HfwFdMpzu', + decompressed_size: 27831, + encoding: 'zlib', + }, + }, + identifier: 'endpointpe', + score: 1, + threshold: 0.66, + version: '3.0.33', + }, + temp_file_path: 'C:\\Windows\\TEMP\\1bb9abfc-ca14-47b2-9f2c-10c323df42f9', + }, + process: { + pid: 1076, + ppid: 432, + entity_id: 'wertqwer', + parent: { + pid: 432, + entity_id: 'adsfsdaf', + }, + name: 'test name', + code_signature: { + subject_name: 'Cybereason Inc', + trusted: true, + }, + command_line: '"C:\\Program Files\\Cybereason ActiveProbe\\AmSvc.exe"', + domain: 'NT AUTHORITY', + executable: 'C:\\Program Files\\Cybereason ActiveProbe\\AmSvc.exe', + hash: { + md5: '1f2d082566b0fc5f2c238a5180db7451', + sha1: 'ca85243c0af6a6471bdaa560685c51eefd6dbc0d', + sha256: '8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2', + }, + pe: { + imphash: 'c30d230b81c734e82e86e2e2fe01cd01', + }, + malware_classifier: { + identifier: 'Whitelisted', + score: 0, + threshold: 0, + version: '3.0.0', + }, + thread: [ + { + id: 1652, + service_name: 'CybereasonAntiMalware', + start: 1542788400, + start_address: 8791698721056, + start_address_module: 'C:\\Program Files\\Cybereason ActiveProbe\\gzfltum.dll', + }, + ], + sid: 'S-1-5-18', + start: 1542788400, + token: { + domain: 'NT AUTHORITY', + integrity_level: 16384, + integrity_level_name: 'system', + privileges: [ + { + description: 'Replace a process level token', + enabled: false, + name: 'SeAssignPrimaryTokenPrivilege', + }, + ], + sid: 'S-1-5-18', + type: 'tokenPrimary', + user: 'SYSTEM', + }, + uptime: 1025, + user: 'SYSTEM', + }, + dll: [ + { + pe: { + architecture: 'x64', + imphash: 'c30d230b81c734e82e86e2e2fe01cd01', + }, + code_signature: { + subject_name: 'Cybereason Inc', + trusted: true, + }, + compile_time: 1534424710, + hash: { + md5: '1f2d082566b0fc5f2c238a5180db7451', + sha1: 'ca85243c0af6a6471bdaa560685c51eefd6dbc0d', + sha256: '8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2', + }, + malware_classifier: { + identifier: 'Whitelisted', + score: 0, + threshold: 0, + version: '3.0.0', + }, + mapped_address: 5362483200, + mapped_size: 0, + path: 'C:\\Program Files\\Cybereason ActiveProbe\\AmSvc.exe', + }, + ], }); } const mock: AlertResultList = { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/reducer.ts index 77d7397d72581..ee172fa80f1fe 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/reducer.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/reducer.ts @@ -11,6 +11,7 @@ import { AppAction } from '../action'; const initialState = (): AlertListState => { return { alerts: [], + alertDetails: undefined, pageSize: 10, pageIndex: 0, total: 0, @@ -43,6 +44,11 @@ export const alertListReducer: Reducer<AlertListState, AppAction> = ( ...state, location: action.payload, }; + } else if (action.type === 'serverReturnedAlertDetailsData') { + return { + ...state, + alertDetails: action.payload, + }; } return state; 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 f217e3cda9191..7ce7c2d08691e 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 @@ -15,7 +15,7 @@ import { AlertsAPIQueryParams, CreateStructuredSelector, } from '../../types'; -import { Immutable, LegacyEndpointEvent } from '../../../../../common/types'; +import { Immutable } from '../../../../../common/types'; const createStructuredSelector: CreateStructuredSelector = createStructuredSelectorWithBadType; /** @@ -23,6 +23,8 @@ const createStructuredSelector: CreateStructuredSelector = createStructuredSelec */ export const alertListData = (state: AlertListState) => state.alerts; +export const selectedAlertDetailsData = (state: AlertListState) => state.alertDetails; + /** * Returns the alert list pagination data from state */ @@ -96,20 +98,11 @@ export const hasSelectedAlert: (state: AlertListState) => boolean = createSelect /** * 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: ( +export const selectedAlertIsLegacyEndpointEvent: ( 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; +) => boolean = createSelector(selectedAlertDetailsData, function(event) { + if (event === undefined) { + return false; } -); + return 'endgame' in event; +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts index 6498462a8fc06..b46785d3190e5 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts @@ -93,19 +93,22 @@ export type AlertListData = AlertResultList; export interface AlertListState { /** Array of alert items. */ - alerts: ImmutableArray<AlertData>; + readonly alerts: ImmutableArray<AlertData>; /** The total number of alerts on the page. */ - total: number; + readonly total: number; /** Number of alerts per page. */ - pageSize: number; + readonly pageSize: number; /** Page number, starting at 0. */ - pageIndex: number; + readonly pageIndex: number; /** Current location object from React Router history. */ readonly location?: Immutable<EndpointAppLocation>; + + /** Specific Alert data to be shown in the details view */ + readonly alertDetails?: Immutable<AlertData>; } /** diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/index.ts new file mode 100644 index 0000000000000..1c78309474737 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { AlertDetailsOverview } from './overview'; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/file_accordion.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/file_accordion.tsx new file mode 100644 index 0000000000000..ac67e54f38779 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/file_accordion.tsx @@ -0,0 +1,80 @@ +/* + * 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, { memo, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiAccordion, EuiDescriptionList } from '@elastic/eui'; +import { Immutable, AlertData } from '../../../../../../../common/types'; +import { FormattedDate } from '../../formatted_date'; + +export const FileAccordion = memo(({ alertData }: { alertData: Immutable<AlertData> }) => { + const columns = useMemo(() => { + return [ + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.fileName', { + defaultMessage: 'File Name', + }), + description: alertData.file.name, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.filePath', { + defaultMessage: 'File Path', + }), + description: alertData.file.path, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.fileSize', { + defaultMessage: 'File Size', + }), + description: alertData.file.size, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.fileCreated', { + defaultMessage: 'File Created', + }), + description: <FormattedDate timestamp={alertData.file.created} />, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.fileModified', { + defaultMessage: 'File Modified', + }), + description: <FormattedDate timestamp={alertData.file.mtime} />, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.fileAccessed', { + defaultMessage: 'File Accessed', + }), + description: <FormattedDate timestamp={alertData.file.accessed} />, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.signer', { + defaultMessage: 'Signer', + }), + description: alertData.file.code_signature.subject_name, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.owner', { + defaultMessage: 'Owner', + }), + description: alertData.file.owner, + }, + ]; + }, [alertData]); + + return ( + <EuiAccordion + id="alertDetailsFileAccordion" + buttonContent={i18n.translate( + 'xpack.endpoint.application.endpoint.alertDetails.accordionTitles.file', + { + defaultMessage: 'File', + } + )} + paddingSize="l" + > + <EuiDescriptionList type="column" listItems={columns} /> + </EuiAccordion> + ); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/general_accordion.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/general_accordion.tsx new file mode 100644 index 0000000000000..070c78c968585 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/general_accordion.tsx @@ -0,0 +1,68 @@ +/* + * 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, { memo, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiAccordion, EuiDescriptionList } from '@elastic/eui'; +import { Immutable, AlertData } from '../../../../../../../common/types'; +import { FormattedDate } from '../../formatted_date'; + +export const GeneralAccordion = memo(({ alertData }: { alertData: Immutable<AlertData> }) => { + const columns = useMemo(() => { + return [ + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.alertType', { + defaultMessage: 'Alert Type', + }), + description: alertData.event.category, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.eventType', { + defaultMessage: 'Event Type', + }), + description: alertData.event.kind, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.status', { + defaultMessage: 'Status', + }), + description: 'TODO', + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.dateCreated', { + defaultMessage: 'Date Created', + }), + description: <FormattedDate timestamp={alertData['@timestamp']} />, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.malwareScore', { + defaultMessage: 'MalwareScore', + }), + description: alertData.file.malware_classifier.score, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.fileName', { + defaultMessage: 'File Name', + }), + description: alertData.file.name, + }, + ]; + }, [alertData]); + return ( + <EuiAccordion + id="alertDetailsAlertAccordion" + buttonContent={i18n.translate( + 'xpack.endpoint.application.endpoint.alertDetails.accordionTitles.alert', + { + defaultMessage: 'Alert', + } + )} + paddingSize="l" + initialIsOpen={true} + > + <EuiDescriptionList type="column" listItems={columns} /> + </EuiAccordion> + ); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/hash_accordion.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/hash_accordion.tsx new file mode 100644 index 0000000000000..b2be083ce8f59 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/hash_accordion.tsx @@ -0,0 +1,49 @@ +/* + * 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, { memo, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiAccordion, EuiDescriptionList } from '@elastic/eui'; +import { Immutable, AlertData } from '../../../../../../../common/types'; + +export const HashAccordion = memo(({ alertData }: { alertData: Immutable<AlertData> }) => { + const columns = useMemo(() => { + return [ + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.md5', { + defaultMessage: 'MD5', + }), + description: alertData.file.hash.md5, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.sha1', { + defaultMessage: 'SHA1', + }), + description: alertData.file.hash.sha1, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.sha256', { + defaultMessage: 'SHA256', + }), + description: alertData.file.hash.sha256, + }, + ]; + }, [alertData]); + + return ( + <EuiAccordion + id="alertDetailsHashAccordion" + buttonContent={i18n.translate( + 'xpack.endpoint.application.endpoint.alertDetails.accordionTitles.hash', + { + defaultMessage: 'Hash', + } + )} + paddingSize="l" + > + <EuiDescriptionList type="column" listItems={columns} /> + </EuiAccordion> + ); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/host_accordion.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/host_accordion.tsx new file mode 100644 index 0000000000000..4108781f0a79b --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/host_accordion.tsx @@ -0,0 +1,55 @@ +/* + * 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, { memo, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiAccordion, EuiDescriptionList } from '@elastic/eui'; +import { Immutable, AlertData } from '../../../../../../../common/types'; + +export const HostAccordion = memo(({ alertData }: { alertData: Immutable<AlertData> }) => { + const columns = useMemo(() => { + return [ + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.hostName', { + defaultMessage: 'Host Name', + }), + description: alertData.host.hostname, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.hostIP', { + defaultMessage: 'Host IP', + }), + description: alertData.host.ip.join(', '), + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.status', { + defaultMessage: 'Status', + }), + description: 'TODO', + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.os', { + defaultMessage: 'OS', + }), + description: alertData.host.os.name, + }, + ]; + }, [alertData]); + + return ( + <EuiAccordion + id="alertDetailsHostAccordion" + buttonContent={i18n.translate( + 'xpack.endpoint.application.endpoint.alertDetails.accordionTitles.host', + { + defaultMessage: 'Host', + } + )} + paddingSize="l" + > + <EuiDescriptionList type="column" listItems={columns} /> + </EuiAccordion> + ); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/index.ts new file mode 100644 index 0000000000000..1eb755242d701 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/index.ts @@ -0,0 +1,12 @@ +/* + * 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 { GeneralAccordion } from './general_accordion'; +export { HostAccordion } from './host_accordion'; +export { HashAccordion } from './hash_accordion'; +export { FileAccordion } from './file_accordion'; +export { SourceProcessAccordion } from './source_process_accordion'; +export { SourceProcessTokenAccordion } from './source_process_token_accordion'; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/source_process_accordion.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/source_process_accordion.tsx new file mode 100644 index 0000000000000..4c961ad4b4964 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/source_process_accordion.tsx @@ -0,0 +1,97 @@ +/* + * 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, { memo, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiAccordion, EuiDescriptionList } from '@elastic/eui'; +import { Immutable, AlertData } from '../../../../../../../common/types'; + +export const SourceProcessAccordion = memo(({ alertData }: { alertData: Immutable<AlertData> }) => { + const columns = useMemo(() => { + return [ + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.processID', { + defaultMessage: 'Process ID', + }), + description: alertData.process.pid, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.processName', { + defaultMessage: 'Process Name', + }), + description: alertData.process.name, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.processPath', { + defaultMessage: 'Process Path', + }), + description: alertData.process.executable, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.md5', { + defaultMessage: 'MD5', + }), + description: alertData.process.hash.md5, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.sha1', { + defaultMessage: 'SHA1', + }), + description: alertData.process.hash.sha1, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.sha256', { + defaultMessage: 'SHA256', + }), + description: alertData.process.hash.sha256, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.malwareScore', { + defaultMessage: 'MalwareScore', + }), + description: alertData.process.malware_classifier.score, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.parentProcessID', { + defaultMessage: 'Parent Process ID', + }), + description: alertData.process.parent.pid, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.signer', { + defaultMessage: 'Signer', + }), + description: alertData.process.code_signature.subject_name, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.username', { + defaultMessage: 'Username', + }), + description: alertData.process.token.user, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.domain', { + defaultMessage: 'Domain', + }), + description: alertData.process.token.domain, + }, + ]; + }, [alertData]); + + return ( + <EuiAccordion + id="alertDetailsSourceProcessAccordion" + buttonContent={i18n.translate( + 'xpack.endpoint.application.endpoint.alertDetails.accordionTitles.sourceProcess', + { + defaultMessage: 'Source Process', + } + )} + paddingSize="l" + > + <EuiDescriptionList type="column" listItems={columns} /> + </EuiAccordion> + ); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/source_process_token_accordion.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/source_process_token_accordion.tsx new file mode 100644 index 0000000000000..7d75d4478afb3 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/metadata/source_process_token_accordion.tsx @@ -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 React, { memo, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiAccordion, EuiDescriptionList } from '@elastic/eui'; +import { Immutable, AlertData } from '../../../../../../../common/types'; + +export const SourceProcessTokenAccordion = memo( + ({ alertData }: { alertData: Immutable<AlertData> }) => { + const columns = useMemo(() => { + return [ + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.sid', { + defaultMessage: 'SID', + }), + description: alertData.process.token.sid, + }, + { + title: i18n.translate('xpack.endpoint.application.endpoint.alertDetails.integrityLevel', { + defaultMessage: 'Integrity Level', + }), + description: alertData.process.token.integrity_level, + }, + ]; + }, [alertData]); + + return ( + <EuiAccordion + id="alertDetailsSourceProcessTokenAccordion" + buttonContent={i18n.translate( + 'xpack.endpoint.application.endpoint.alertDetails.accordionTitles.sourceProcessToken', + { + defaultMessage: 'Source Process Token', + } + )} + paddingSize="l" + > + <EuiDescriptionList type="column" listItems={columns} /> + </EuiAccordion> + ); + } +); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/index.tsx new file mode 100644 index 0000000000000..080c70ca43bae --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/index.tsx @@ -0,0 +1,94 @@ +/* + * 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, { memo, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiSpacer, EuiTitle, EuiText, EuiHealth, EuiTabbedContent } from '@elastic/eui'; +import { useAlertListSelector } from '../../hooks/use_alerts_selector'; +import * as selectors from '../../../../store/alerts/selectors'; +import { MetadataPanel } from './metadata_panel'; +import { FormattedDate } from '../../formatted_date'; +import { AlertDetailResolver } from '../../resolver'; + +export const AlertDetailsOverview = memo(() => { + const alertDetailsData = useAlertListSelector(selectors.selectedAlertDetailsData); + if (alertDetailsData === undefined) { + return null; + } + const selectedAlertIsLegacyEndpointEvent = useAlertListSelector( + selectors.selectedAlertIsLegacyEndpointEvent + ); + + const tabs = useMemo(() => { + return [ + { + id: 'overviewMetadata', + name: i18n.translate( + 'xpack.endpoint.application.endpoint.alertDetails.overview.tabs.overview', + { + defaultMessage: 'Overview', + } + ), + content: ( + <> + <EuiSpacer /> + <MetadataPanel /> + </> + ), + }, + { + id: 'overviewResolver', + name: i18n.translate( + 'xpack.endpoint.application.endpoint.alertDetails.overview.tabs.resolver', + { + defaultMessage: 'Resolver', + } + ), + content: ( + <> + <EuiSpacer /> + {selectedAlertIsLegacyEndpointEvent && <AlertDetailResolver />} + </> + ), + }, + ]; + }, [selectedAlertIsLegacyEndpointEvent]); + + return ( + <> + <section className="details-overview-summary"> + <EuiTitle size="s"> + <h3> + <FormattedMessage + id="xpack.endpoint.application.endpoint.alertDetails.overview.title" + defaultMessage="Detected Malicious File" + /> + </h3> + </EuiTitle> + <EuiSpacer /> + <EuiText> + <p> + <FormattedMessage + id="xpack.endpoint.application.endpoint.alertDetails.overview.summary" + defaultMessage="MalwareScore detected the opening of a document on {hostname} on {date}" + values={{ + hostname: alertDetailsData.host.hostname, + date: <FormattedDate timestamp={alertDetailsData['@timestamp']} />, + }} + /> + </p> + </EuiText> + <EuiSpacer /> + <EuiText> + Endpoint Status: <EuiHealth color="success">Online</EuiHealth> + </EuiText> + <EuiText>Alert Status: Open</EuiText> + <EuiSpacer /> + </section> + <EuiTabbedContent tabs={tabs} initialSelectedTab={tabs[0]} /> + </> + ); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/metadata_panel.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/metadata_panel.tsx new file mode 100644 index 0000000000000..556d7bea2e310 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/metadata_panel.tsx @@ -0,0 +1,40 @@ +/* + * 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, { memo } from 'react'; +import { EuiSpacer } from '@elastic/eui'; +import { useAlertListSelector } from '../../hooks/use_alerts_selector'; +import * as selectors from '../../../../store/alerts/selectors'; +import { + GeneralAccordion, + HostAccordion, + HashAccordion, + FileAccordion, + SourceProcessAccordion, + SourceProcessTokenAccordion, +} from '../metadata'; + +export const MetadataPanel = memo(() => { + const alertDetailsData = useAlertListSelector(selectors.selectedAlertDetailsData); + if (alertDetailsData === undefined) { + return null; + } + return ( + <section className="overview-metadata-panel"> + <GeneralAccordion alertData={alertDetailsData} /> + <EuiSpacer /> + <HostAccordion alertData={alertDetailsData} /> + <EuiSpacer /> + <HashAccordion alertData={alertDetailsData} /> + <EuiSpacer /> + <FileAccordion alertData={alertDetailsData} /> + <EuiSpacer /> + <SourceProcessAccordion alertData={alertDetailsData} /> + <EuiSpacer /> + <SourceProcessTokenAccordion alertData={alertDetailsData} /> + </section> + ); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/formatted_date.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/formatted_date.tsx new file mode 100644 index 0000000000000..731bd31b26cef --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/formatted_date.tsx @@ -0,0 +1,22 @@ +/* + * 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, { memo } from 'react'; +import { FormattedDate as ReactIntlFormattedDate } from '@kbn/i18n/react'; + +export const FormattedDate = memo(({ timestamp }: { timestamp: number }) => { + const date = new Date(timestamp); + return ( + <ReactIntlFormattedDate + value={date} + year="numeric" + month="2-digit" + day="2-digit" + hour="2-digit" + minute="2-digit" + second="2-digit" + /> + ); +}); 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 fe362f21a178e..aae44824c3164 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 @@ -140,9 +140,6 @@ 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 3c229484ede4e..5d405f8c6fbae 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 @@ -17,15 +17,21 @@ import { EuiFlyoutBody, EuiTitle, EuiBadge, + EuiLoadingSpinner, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiPageContentBody, + EuiLink, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useHistory, Link } from 'react-router-dom'; -import { FormattedDate } from 'react-intl'; +import { useHistory } from 'react-router-dom'; +import { FormattedMessage } from '@kbn/i18n/react'; 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'; +import { AlertDetailsOverview } from './details'; +import { FormattedDate } from './formatted_date'; export const AlertIndex = memo(() => { const history = useHistory(); @@ -87,7 +93,6 @@ 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 => { @@ -119,10 +124,10 @@ export const AlertIndex = memo(() => { history.push(urlFromQueryParams(paramsWithoutSelectedAlert)); }, [history, queryParams]); - const datesForRows: Map<AlertData, Date> = useMemo(() => { + const timestampForRows: Map<AlertData, number> = useMemo(() => { return new Map( alertListData.map(alertData => { - return [alertData, new Date(alertData['@timestamp'])]; + return [alertData, alertData['@timestamp']]; }) ); }, [alertListData]); @@ -136,9 +141,11 @@ export const AlertIndex = memo(() => { const row = alertListData[rowIndex % pageSize]; if (columnId === 'alert_type') { return ( - <Link + <EuiLink data-testid="alertTypeCellLink" - to={urlFromQueryParams({ ...queryParams, selected_alert: row.event.id })} + onClick={() => + history.push(urlFromQueryParams({ ...queryParams, selected_alert: row.id })) + } > {i18n.translate( 'xpack.endpoint.application.endpoint.alerts.alertType.maliciousFileDescription', @@ -146,7 +153,7 @@ export const AlertIndex = memo(() => { defaultMessage: 'Malicious File', } )} - </Link> + </EuiLink> ); } else if (columnId === 'event_type') { return row.event.action; @@ -157,19 +164,9 @@ export const AlertIndex = memo(() => { } else if (columnId === 'host_name') { return row.host.hostname; } else if (columnId === 'timestamp') { - const date = datesForRows.get(row)!; - if (date && isFinite(date.getTime())) { - return ( - <FormattedDate - value={date} - year="numeric" - month="2-digit" - day="2-digit" - hour="2-digit" - minute="2-digit" - second="2-digit" - /> - ); + const timestamp = timestampForRows.get(row)!; + if (timestamp) { + return <FormattedDate timestamp={timestamp} />; } else { return ( <EuiBadge color="warning"> @@ -185,11 +182,11 @@ export const AlertIndex = memo(() => { } else if (columnId === 'archived') { return null; } else if (columnId === 'malware_score') { - return row.file_classification.malware_classification.score; + return row.file.malware_classifier.score; } return null; }; - }, [alertListData, datesForRows, pageSize, queryParams, total]); + }, [total, alertListData, pageSize, history, queryParams, timestampForRows]); const pagination = useMemo(() => { return { @@ -201,6 +198,16 @@ export const AlertIndex = memo(() => { }; }, [onChangeItemsPerPage, onChangePage, pageIndex, pageSize]); + const columnVisibility = useMemo( + () => ({ + visibleColumns, + setVisibleColumns, + }), + [setVisibleColumns, visibleColumns] + ); + + const selectedAlertData = useAlertListSelector(selectors.selectedAlertDetailsData); + return ( <> {hasSelectedAlert && ( @@ -215,29 +222,37 @@ export const AlertIndex = memo(() => { </EuiTitle> </EuiFlyoutHeader> <EuiFlyoutBody> - <AlertDetailResolver selectedEvent={selectedEvent} /> + {selectedAlertData ? <AlertDetailsOverview /> : <EuiLoadingSpinner size="xl" />} </EuiFlyoutBody> </EuiFlyout> )} <EuiPage data-test-subj="alertListPage" data-testid="alertListPage"> <EuiPageBody> <EuiPageContent> - <EuiDataGrid - aria-label="Alert List" - rowCount={total} - columns={columns} - columnVisibility={useMemo( - () => ({ - visibleColumns, - setVisibleColumns, - }), - [setVisibleColumns, visibleColumns] - )} - renderCellValue={renderCellValue} - pagination={pagination} - data-test-subj="alertListGrid" - data-testid="alertListGrid" - /> + <EuiPageContentHeader> + <EuiPageContentHeaderSection> + <EuiTitle size="l"> + <h1> + <FormattedMessage + id="xpack.endpoint.alertList.viewTitle" + defaultMessage="Alerts" + /> + </h1> + </EuiTitle> + </EuiPageContentHeaderSection> + </EuiPageContentHeader> + <EuiPageContentBody> + <EuiDataGrid + aria-label="Alert List" + rowCount={total} + columns={columns} + columnVisibility={columnVisibility} + renderCellValue={renderCellValue} + pagination={pagination} + data-test-subj="alertListGrid" + data-testid="alertListGrid" + /> + </EuiPageContentBody> </EuiPageContent> </EuiPageBody> </EuiPage> 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 index c7ef7f73dfe05..ec1dab45d50ab 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx @@ -18,6 +18,7 @@ export const AlertDetailResolver = styled( ({ className, selectedEvent }: { className?: string; selectedEvent?: LegacyEndpointEvent }) => { const context = useKibana<EndpointPluginServices>(); const { store } = storeFactory(context); + return ( <div className={className} data-test-subj="alertResolver" data-testid="alertResolver"> <Provider store={store}> From 41deda35841fc609a58aba5fc87a477e589b522c Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Tue, 3 Mar 2020 10:28:40 +0300 Subject: [PATCH 25/34] Fix monaco editor styling (#58888) * Fix monaco editor styling * Change line highlight border --- src/plugins/kibana_react/public/code_editor/editor_theme.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/plugins/kibana_react/public/code_editor/editor_theme.ts b/src/plugins/kibana_react/public/code_editor/editor_theme.ts index 6e30135686797..586b4a568c348 100644 --- a/src/plugins/kibana_react/public/code_editor/editor_theme.ts +++ b/src/plugins/kibana_react/public/code_editor/editor_theme.ts @@ -101,6 +101,11 @@ export function createTheme( 'editor.selectionBackground': selectionBackgroundColor, 'editorWidget.border': euiTheme.euiColorLightShade, 'editorWidget.background': euiTheme.euiColorLightestShade, + 'editorCursor.foreground': euiTheme.euiColorDarkestShade, + 'editorSuggestWidget.selectedBackground': euiTheme.euiColorLightShade, + 'list.hoverBackground': euiTheme.euiColorLightShade, + 'list.highlightForeground': euiTheme.euiColorPrimary, + 'editor.lineHighlightBorder': euiTheme.euiColorLightestShade, }, }; } From 421d9d502b45b04569c48d0c42e48872fdaffc98 Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Tue, 3 Mar 2020 10:31:02 +0300 Subject: [PATCH 26/34] [Vis Editor] Fix field combo box search value (#58601) * Fix field combo box search value * Fix inconsistent behavior * Apply validation for agg_select --- .../vis_default_editor/public/components/agg_select.tsx | 7 +++++-- .../public/components/controls/field.tsx | 8 ++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_select.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/agg_select.tsx index a2cec61b122ef..9a408c2d98b22 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_select.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/agg_select.tsx @@ -17,7 +17,7 @@ * under the License. */ import { get, has } from 'lodash'; -import React, { useEffect, useCallback } from 'react'; +import React, { useEffect, useCallback, useState } from 'react'; import { EuiComboBox, EuiComboBoxOptionProps, EuiFormRow, EuiLink, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -52,6 +52,7 @@ function DefaultEditorAggSelect({ isSubAggregation, onChangeAggType, }: DefaultEditorAggSelectProps) { + const [isDirty, setIsDirty] = useState(false); const { services } = useKibana(); const selectedOptions: ComboBoxGroupedOptions<IAggType> = value ? [{ label: value.title, target: value }] @@ -100,7 +101,7 @@ function DefaultEditorAggSelect({ ); } - const isValid = !!value && !errors.length; + const isValid = !!value && !errors.length && !isDirty; const onChange = useCallback( (options: EuiComboBoxOptionProps[]) => { @@ -111,6 +112,7 @@ function DefaultEditorAggSelect({ }, [setValue] ); + const onSearchChange = useCallback(searchValue => setIsDirty(Boolean(searchValue)), []); const setTouched = useCallback( () => onChangeAggType({ type: AGG_TYPE_ACTION_KEYS.TOUCHED, payload: true }), @@ -151,6 +153,7 @@ function DefaultEditorAggSelect({ singleSelection={{ asPlainText: true }} onBlur={setTouched} onChange={onChange} + onSearchChange={onSearchChange} data-test-subj="defaultEditorAggSelect" isClearable={false} isInvalid={showValidation ? !isValid : false} diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.tsx index 8bf7bc384b07a..d605fb203f4d3 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.tsx @@ -18,7 +18,7 @@ */ import { get } from 'lodash'; -import React, { useEffect } from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; import { EuiComboBox, EuiComboBoxOptionProps, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -50,6 +50,7 @@ function FieldParamEditor({ setValidity, setValue, }: FieldParamEditorProps) { + const [isDirty, setIsDirty] = useState(false); const selectedOptions: ComboBoxGroupedOptions<IndexPatternField> = value ? [{ label: value.displayName || value.name, target: value }] : []; @@ -79,7 +80,7 @@ function FieldParamEditor({ ); } - const isValid = !!value && !errors.length; + const isValid = !!value && !errors.length && !isDirty; useValidation(setValidity, isValid); @@ -98,6 +99,8 @@ function FieldParamEditor({ } }, []); + const onSearchChange = useCallback(searchValue => setIsDirty(Boolean(searchValue)), []); + return ( <EuiFormRow label={customLabel || label} @@ -119,6 +122,7 @@ function FieldParamEditor({ isInvalid={showValidation ? !isValid : false} onChange={onChange} onBlur={setTouched} + onSearchChange={onSearchChange} data-test-subj="visDefaultEditorField" fullWidth={true} /> From ecbcceb74d8e37d069b6dafc2136908f664b7bb6 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger <walter@elastic.co> Date: Tue, 3 Mar 2020 08:59:00 +0100 Subject: [PATCH 27/34] [ML] Transform: Fix advanced editor initialization. (#59006) Fixes regression introduced by #58015 to correctly initialize the transform wizard advanced editor with the current configuration. --- .../step_define/step_define_form.tsx | 5 ++++ .../apps/transform/creation_index_pattern.ts | 29 +++++++++++++++++++ .../services/transform_ui/wizard.ts | 14 +++++++++ 3 files changed, 48 insertions(+) diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index 3adb74e4704dc..bde832894632c 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -404,6 +404,10 @@ export const StepDefineForm: FC<Props> = React.memo(({ overrides = {}, onChange xJson: advancedEditorConfig, } = useXJsonMode(stringifiedPivotConfig); + useEffect(() => { + setAdvancedEditorConfig(stringifiedPivotConfig); + }, [setAdvancedEditorConfig, stringifiedPivotConfig]); + // source config const stringifiedSourceConfig = JSON.stringify(previewRequest.source.query, null, 2); const [ @@ -797,6 +801,7 @@ export const StepDefineForm: FC<Props> = React.memo(({ overrides = {}, onChange > <EuiPanel grow={false} paddingSize="none"> <EuiCodeEditor + data-test-subj="transformAdvancedPivotEditor" mode={xJsonMode} width="100%" value={advancedEditorConfig} diff --git a/x-pack/test/functional/apps/transform/creation_index_pattern.ts b/x-pack/test/functional/apps/transform/creation_index_pattern.ts index 6e35b0c1a81ca..5b54bfdafdbdb 100644 --- a/x-pack/test/functional/apps/transform/creation_index_pattern.ts +++ b/x-pack/test/functional/apps/transform/creation_index_pattern.ts @@ -57,6 +57,28 @@ export default function({ getService }: FtrProviderContext) { return `user-${this.transformId}`; }, expected: { + pivotAdvancedEditorValue: { + group_by: { + 'category.keyword': { + terms: { + field: 'category.keyword', + }, + }, + order_date: { + date_histogram: { + field: 'order_date', + calendar_interval: '1m', + }, + }, + }, + aggregations: { + 'products.base_price.avg': { + avg: { + field: 'products.base_price', + }, + }, + }, + }, pivotPreview: { column: 0, values: [`Men's Accessories`], @@ -152,6 +174,13 @@ export default function({ getService }: FtrProviderContext) { await transform.wizard.assertAdvancedPivotEditorSwitchCheckState(false); }); + it('displays the advanced configuration', async () => { + await transform.wizard.enabledAdvancedPivotEditor(); + await transform.wizard.assertAdvancedPivotEditorContent( + testData.expected.pivotAdvancedEditorValue + ); + }); + it('loads the pivot preview', async () => { await transform.wizard.assertPivotPreviewLoaded(); }); diff --git a/x-pack/test/functional/services/transform_ui/wizard.ts b/x-pack/test/functional/services/transform_ui/wizard.ts index e823117ad7016..aca08f7083aa8 100644 --- a/x-pack/test/functional/services/transform_ui/wizard.ts +++ b/x-pack/test/functional/services/transform_ui/wizard.ts @@ -8,6 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export function TransformWizardProvider({ getService }: FtrProviderContext) { + const aceEditor = getService('aceEditor'); const testSubjects = getService('testSubjects'); const comboBox = getService('comboBox'); const retry = getService('retry'); @@ -273,6 +274,12 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { await this.assertAggregationEntryExists(index, expectedLabel); }, + async assertAdvancedPivotEditorContent(expectedValue: Record<string, any>) { + const advancedEditorString = await aceEditor.getValue('transformAdvancedPivotEditor'); + const advancedEditorValue = JSON.parse(advancedEditorString); + expect(advancedEditorValue).to.eql(expectedValue); + }, + async assertAdvancedPivotEditorSwitchExists() { await testSubjects.existOrFail(`transformAdvancedPivotEditorSwitch`, { allowHidden: true }); }, @@ -287,6 +294,13 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { ); }, + async enabledAdvancedPivotEditor() { + await this.assertAdvancedPivotEditorSwitchCheckState(false); + await testSubjects.click('transformAdvancedPivotEditorSwitch'); + await this.assertAdvancedPivotEditorSwitchCheckState(true); + await testSubjects.existOrFail('transformAdvancedPivotEditor'); + }, + async assertTransformIdInputExists() { await testSubjects.existOrFail('transformIdInput'); }, From bb55e8a21cc50cda8cf8753064b49f433a796451 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger <walter@elastic.co> Date: Tue, 3 Mar 2020 09:01:45 +0100 Subject: [PATCH 28/34] [ML] Transforms: Migrate server plugin to NP. (#58714) Migrate transform legacy server to NP. - Create server plugin/index for transform in x-pack/plugins. - Move all legacy/server files to plugins/transform --- x-pack/.i18nrc.json | 2 +- x-pack/legacy/plugins/transform/index.ts | 19 +- .../legacy/plugins/transform/server/plugin.ts | 17 - .../transform/server/routes/api/app.ts | 99 ----- .../server/routes/api/register_routes.ts | 14 - .../routes/api/transform_audit_messages.ts | 84 ---- .../transform/server/routes/api/transforms.ts | 261 ------------- .../legacy/plugins/transform/server/shim.ts | 46 --- x-pack/plugins/transform/kibana.json | 15 + .../server/client/elasticsearch_transform.ts | 0 x-pack/plugins/transform/server/index.ts | 11 + x-pack/plugins/transform/server/plugin.ts | 89 +++++ .../server/routes/api/error_utils.ts | 16 +- .../transform/server/routes/api/privileges.ts | 85 +++++ .../transform/server/routes/api/schema.ts | 16 + .../transform/server/routes/api/transforms.ts | 360 ++++++++++++++++++ .../routes/api/transforms_audit_messages.ts | 91 +++++ .../plugins/transform/server/routes/index.ts | 24 ++ .../transform/server/services/index.ts | 7 + .../transform/server/services/license.ts | 91 +++++ x-pack/plugins/transform/server/types.ts | 18 + 21 files changed, 824 insertions(+), 541 deletions(-) delete mode 100644 x-pack/legacy/plugins/transform/server/plugin.ts delete mode 100644 x-pack/legacy/plugins/transform/server/routes/api/app.ts delete mode 100644 x-pack/legacy/plugins/transform/server/routes/api/register_routes.ts delete mode 100644 x-pack/legacy/plugins/transform/server/routes/api/transform_audit_messages.ts delete mode 100644 x-pack/legacy/plugins/transform/server/routes/api/transforms.ts delete mode 100644 x-pack/legacy/plugins/transform/server/shim.ts create mode 100644 x-pack/plugins/transform/kibana.json rename x-pack/{legacy => }/plugins/transform/server/client/elasticsearch_transform.ts (100%) create mode 100644 x-pack/plugins/transform/server/index.ts create mode 100644 x-pack/plugins/transform/server/plugin.ts rename x-pack/{legacy => }/plugins/transform/server/routes/api/error_utils.ts (80%) create mode 100644 x-pack/plugins/transform/server/routes/api/privileges.ts create mode 100644 x-pack/plugins/transform/server/routes/api/schema.ts create mode 100644 x-pack/plugins/transform/server/routes/api/transforms.ts create mode 100644 x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts create mode 100644 x-pack/plugins/transform/server/routes/index.ts create mode 100644 x-pack/plugins/transform/server/services/index.ts create mode 100644 x-pack/plugins/transform/server/services/license.ts create mode 100644 x-pack/plugins/transform/server/types.ts diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 51099815ec938..8f5a5ea4f10e4 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -39,7 +39,7 @@ "xpack.snapshotRestore": "legacy/plugins/snapshot_restore", "xpack.spaces": ["legacy/plugins/spaces", "plugins/spaces"], "xpack.taskManager": "legacy/plugins/task_manager", - "xpack.transform": "legacy/plugins/transform", + "xpack.transform": ["legacy/plugins/transform", "plugins/transform"], "xpack.triggersActionsUI": "plugins/triggers_actions_ui", "xpack.upgradeAssistant": "plugins/upgrade_assistant", "xpack.uptime": "legacy/plugins/uptime", diff --git a/x-pack/legacy/plugins/transform/index.ts b/x-pack/legacy/plugins/transform/index.ts index d0799f46cbd25..10f4732152c43 100644 --- a/x-pack/legacy/plugins/transform/index.ts +++ b/x-pack/legacy/plugins/transform/index.ts @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Legacy } from 'kibana'; import { resolve } from 'path'; + import { PLUGIN } from './common/constants'; -import { Plugin as TransformPlugin } from './server/plugin'; -import { createServerShim } from './server/shim'; export function transform(kibana: any) { return new kibana.Plugin({ @@ -20,20 +18,5 @@ export function transform(kibana: any) { styleSheetPaths: resolve(__dirname, 'public/app/index.scss'), managementSections: ['plugins/transform'], }, - init(server: Legacy.Server) { - const { core, plugins } = createServerShim(server, PLUGIN.ID); - const transformPlugin = new TransformPlugin(); - - // Start plugin - transformPlugin.start(core, plugins); - - // Register license checker - plugins.license.registerLicenseChecker( - server, - PLUGIN.ID, - PLUGIN.getI18nName(), - PLUGIN.MINIMUM_LICENSE_REQUIRED - ); - }, }); } diff --git a/x-pack/legacy/plugins/transform/server/plugin.ts b/x-pack/legacy/plugins/transform/server/plugin.ts deleted file mode 100644 index f9264ee1f2507..0000000000000 --- a/x-pack/legacy/plugins/transform/server/plugin.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { API_BASE_PATH } from '../common/constants'; -import { registerRoutes } from './routes/api/register_routes'; -import { Core, Plugins } from './shim'; - -export class Plugin { - public start(core: Core, plugins: Plugins): void { - const router = core.http.createRouter(API_BASE_PATH); - - // Register routes - registerRoutes(router, plugins); - } -} diff --git a/x-pack/legacy/plugins/transform/server/routes/api/app.ts b/x-pack/legacy/plugins/transform/server/routes/api/app.ts deleted file mode 100644 index c3189794b6eb0..0000000000000 --- a/x-pack/legacy/plugins/transform/server/routes/api/app.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { Router, RouterRouteHandler } from '../../../../../server/lib/create_router'; -import { wrapCustomError } from '../../../../../server/lib/create_router/error_wrappers'; -import { APP_CLUSTER_PRIVILEGES, APP_INDEX_PRIVILEGES } from '../../../common/constants'; -// NOTE: now we import it from our "public" folder, but when the Authorisation lib -// will move to the "es_ui_shared" plugin, it will be imported from its "static" folder -import { Privileges } from '../../../public/app/lib/authorization'; -import { Plugins } from '../../shim'; - -let xpackMainPlugin: any; - -export function registerAppRoutes(router: Router, plugins: Plugins) { - xpackMainPlugin = plugins.xpack_main; - router.get('privileges', getPrivilegesHandler); -} - -export function getXpackMainPlugin() { - return xpackMainPlugin; -} - -const extractMissingPrivileges = (privilegesObject: { [key: string]: boolean } = {}): string[] => - Object.keys(privilegesObject).reduce((privileges: string[], privilegeName: string): string[] => { - if (!privilegesObject[privilegeName]) { - privileges.push(privilegeName); - } - return privileges; - }, []); - -export const getPrivilegesHandler: RouterRouteHandler = async ( - req, - callWithRequest -): Promise<Privileges> => { - const xpackInfo = getXpackMainPlugin() && getXpackMainPlugin().info; - if (!xpackInfo) { - // xpackInfo is updated via poll, so it may not be available until polling has begun. - // In this rare situation, tell the client the service is temporarily unavailable. - throw wrapCustomError(new Error('Security info unavailable'), 503); - } - - const privilegesResult: Privileges = { - hasAllPrivileges: true, - missingPrivileges: { - cluster: [], - index: [], - }, - }; - - const securityInfo = xpackInfo && xpackInfo.isAvailable() && xpackInfo.feature('security'); - if (!securityInfo || !securityInfo.isAvailable() || !securityInfo.isEnabled()) { - // If security isn't enabled, let the user use app. - return privilegesResult; - } - - // Get cluster priviliges - const { has_all_requested: hasAllPrivileges, cluster } = await callWithRequest( - 'transport.request', - { - path: '/_security/user/_has_privileges', - method: 'POST', - body: { - cluster: APP_CLUSTER_PRIVILEGES, - }, - } - ); - - // Find missing cluster privileges and set overall app privileges - privilegesResult.missingPrivileges.cluster = extractMissingPrivileges(cluster); - privilegesResult.hasAllPrivileges = hasAllPrivileges; - - // Get all index privileges the user has - const { indices } = await callWithRequest('transport.request', { - path: '/_security/user/_privileges', - method: 'GET', - }); - - // Check if they have all the required index privileges for at least one index - const oneIndexWithAllPrivileges = indices.find(({ privileges }: { privileges: string[] }) => { - if (privileges.includes('all')) { - return true; - } - - const indexHasAllPrivileges = APP_INDEX_PRIVILEGES.every(privilege => - privileges.includes(privilege) - ); - - return indexHasAllPrivileges; - }); - - // If they don't, return list of required index privileges - if (!oneIndexWithAllPrivileges) { - privilegesResult.missingPrivileges.index = [...APP_INDEX_PRIVILEGES]; - } - - return privilegesResult; -}; diff --git a/x-pack/legacy/plugins/transform/server/routes/api/register_routes.ts b/x-pack/legacy/plugins/transform/server/routes/api/register_routes.ts deleted file mode 100644 index c01647c598d86..0000000000000 --- a/x-pack/legacy/plugins/transform/server/routes/api/register_routes.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { Router } from '../../../../../server/lib/create_router'; -import { Plugins } from '../../shim'; -import { registerAppRoutes } from './app'; -import { registerTransformsRoutes } from './transforms'; - -export const registerRoutes = (router: Router, plugins: Plugins): void => { - registerAppRoutes(router, plugins); - registerTransformsRoutes(router, plugins); -}; diff --git a/x-pack/legacy/plugins/transform/server/routes/api/transform_audit_messages.ts b/x-pack/legacy/plugins/transform/server/routes/api/transform_audit_messages.ts deleted file mode 100644 index c4b5fbd4d3b60..0000000000000 --- a/x-pack/legacy/plugins/transform/server/routes/api/transform_audit_messages.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; - -import { AuditMessage } from '../../../common/types/messages'; - -const ML_DF_NOTIFICATION_INDEX_PATTERN = '.transform-notifications-read'; -const SIZE = 500; - -interface BoolQuery { - bool: { [key: string]: any }; -} - -export function transformAuditMessagesProvider(callWithRequest: CallCluster) { - // search for audit messages, - // transformId is optional. without it, all transforms will be listed. - async function getTransformAuditMessages(transformId: string) { - const query: BoolQuery = { - bool: { - filter: [ - { - bool: { - must_not: { - term: { - level: 'activity', - }, - }, - }, - }, - ], - }, - }; - - // if no transformId specified, load all of the messages - if (transformId !== undefined) { - query.bool.filter.push({ - bool: { - should: [ - { - term: { - transform_id: '', // catch system messages - }, - }, - { - term: { - transform_id: transformId, // messages for specified transformId - }, - }, - ], - }, - }); - } - - try { - const resp = await callWithRequest('search', { - index: ML_DF_NOTIFICATION_INDEX_PATTERN, - ignore_unavailable: true, - rest_total_hits_as_int: true, - size: SIZE, - body: { - sort: [{ timestamp: { order: 'desc' } }, { transform_id: { order: 'asc' } }], - query, - }, - }); - - let messages = []; - if (resp.hits.total !== 0) { - messages = resp.hits.hits.map((hit: AuditMessage) => hit._source); - messages.reverse(); - } - return messages; - } catch (e) { - throw e; - } - } - - return { - getTransformAuditMessages, - }; -} diff --git a/x-pack/legacy/plugins/transform/server/routes/api/transforms.ts b/x-pack/legacy/plugins/transform/server/routes/api/transforms.ts deleted file mode 100644 index 6e833854a24c9..0000000000000 --- a/x-pack/legacy/plugins/transform/server/routes/api/transforms.ts +++ /dev/null @@ -1,261 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; -import { Router, RouterRouteHandler } from '../../../../../server/lib/create_router'; -import { wrapEsError } from '../../../../../server/lib/create_router/error_wrappers'; -import { Plugins } from '../../shim'; -import { TRANSFORM_STATE } from '../../../public/app/common'; -import { - TransformEndpointRequest, - TransformEndpointResult, -} from '../../../public/app/hooks/use_api_types'; -import { TransformId } from '../../../public/app/common/transform'; -import { isRequestTimeout, fillResultsWithTimeouts } from './error_utils'; -import { transformAuditMessagesProvider } from './transform_audit_messages'; - -enum TRANSFORM_ACTIONS { - STOP = 'stop', - START = 'start', - DELETE = 'delete', -} - -interface StartOptions { - transformId: TransformId; -} - -interface StopOptions { - transformId: TransformId; - force: boolean; - waitForCompletion?: boolean; -} - -export function registerTransformsRoutes(router: Router, plugins: Plugins) { - router.get('transforms', getTransformHandler); - router.get('transforms/{transformId}', getTransformHandler); - router.get('transforms/_stats', getTransformStatsHandler); - router.get('transforms/{transformId}/_stats', getTransformStatsHandler); - router.get('transforms/{transformId}/messages', getTransformMessagesHandler); - router.put('transforms/{transformId}', putTransformHandler); - router.post('delete_transforms', deleteTransformsHandler); - router.post('transforms/_preview', previewTransformHandler); - router.post('start_transforms', startTransformsHandler); - router.post('stop_transforms', stopTransformsHandler); - router.post('es_search', esSearchHandler); -} - -const getTransformHandler: RouterRouteHandler = async (req, callWithRequest) => { - const { transformId } = req.params; - const options = { - ...(transformId !== undefined ? { transformId } : {}), - }; - - try { - return await callWithRequest('transform.getTransforms', options); - } catch (e) { - return { error: wrapEsError(e) }; - } -}; - -const getTransformStatsHandler: RouterRouteHandler = async (req, callWithRequest) => { - const { transformId } = req.params; - const options = { - ...(transformId !== undefined ? { transformId } : {}), - }; - - try { - return await callWithRequest('transform.getTransformsStats', options); - } catch (e) { - return { error: wrapEsError(e) }; - } -}; - -const deleteTransformsHandler: RouterRouteHandler = async (req, callWithRequest) => { - const transformsInfo = req.payload as TransformEndpointRequest[]; - - try { - return await deleteTransforms(transformsInfo, callWithRequest); - } catch (e) { - return { error: wrapEsError(e) }; - } -}; - -const putTransformHandler: RouterRouteHandler = async (req, callWithRequest) => { - const { transformId } = req.params; - - const response: { - transformsCreated: Array<{ transform: string }>; - errors: any[]; - } = { - transformsCreated: [], - errors: [], - }; - - await callWithRequest('transform.createTransform', { body: req.payload, transformId }) - .then(() => response.transformsCreated.push({ transform: transformId })) - .catch(e => - response.errors.push({ - id: transformId, - error: wrapEsError(e), - }) - ); - - return response; -}; - -async function deleteTransforms( - transformsInfo: TransformEndpointRequest[], - callWithRequest: CallCluster -) { - const results: TransformEndpointResult = {}; - - for (const transformInfo of transformsInfo) { - const transformId = transformInfo.id; - try { - if (transformInfo.state === TRANSFORM_STATE.FAILED) { - try { - await callWithRequest('transform.stopTransform', { - transformId, - force: true, - waitForCompletion: true, - } as StopOptions); - } catch (e) { - if (isRequestTimeout(e)) { - return fillResultsWithTimeouts({ - results, - id: transformId, - items: transformsInfo, - action: TRANSFORM_ACTIONS.DELETE, - }); - } - } - } - - await callWithRequest('transform.deleteTransform', { transformId }); - results[transformId] = { success: true }; - } catch (e) { - if (isRequestTimeout(e)) { - return fillResultsWithTimeouts({ - results, - id: transformInfo.id, - items: transformsInfo, - action: TRANSFORM_ACTIONS.DELETE, - }); - } - results[transformId] = { success: false, error: JSON.stringify(e) }; - } - } - return results; -} - -const previewTransformHandler: RouterRouteHandler = async (req, callWithRequest) => { - try { - return await callWithRequest('transform.getTransformsPreview', { body: req.payload }); - } catch (e) { - return wrapEsError(e); - } -}; - -const startTransformsHandler: RouterRouteHandler = async (req, callWithRequest) => { - const { transformsInfo } = req.payload as { - transformsInfo: TransformEndpointRequest[]; - }; - - try { - return await startTransforms(transformsInfo, callWithRequest); - } catch (e) { - return wrapEsError(e); - } -}; - -async function startTransforms( - transformsInfo: TransformEndpointRequest[], - callWithRequest: CallCluster -) { - const results: TransformEndpointResult = {}; - - for (const transformInfo of transformsInfo) { - const transformId = transformInfo.id; - try { - await callWithRequest('transform.startTransform', { transformId } as StartOptions); - results[transformId] = { success: true }; - } catch (e) { - if (isRequestTimeout(e)) { - return fillResultsWithTimeouts({ - results, - id: transformId, - items: transformsInfo, - action: TRANSFORM_ACTIONS.START, - }); - } - results[transformId] = { success: false, error: JSON.stringify(e) }; - } - } - return results; -} - -const stopTransformsHandler: RouterRouteHandler = async (req, callWithRequest) => { - const { transformsInfo } = req.payload as { - transformsInfo: TransformEndpointRequest[]; - }; - - try { - return await stopTransforms(transformsInfo, callWithRequest); - } catch (e) { - return wrapEsError(e); - } -}; - -async function stopTransforms( - transformsInfo: TransformEndpointRequest[], - callWithRequest: CallCluster -) { - const results: TransformEndpointResult = {}; - - for (const transformInfo of transformsInfo) { - const transformId = transformInfo.id; - try { - await callWithRequest('transform.stopTransform', { - transformId, - force: - transformInfo.state !== undefined - ? transformInfo.state === TRANSFORM_STATE.FAILED - : false, - waitForCompletion: true, - } as StopOptions); - results[transformId] = { success: true }; - } catch (e) { - if (isRequestTimeout(e)) { - return fillResultsWithTimeouts({ - results, - id: transformId, - items: transformsInfo, - action: TRANSFORM_ACTIONS.STOP, - }); - } - results[transformId] = { success: false, error: JSON.stringify(e) }; - } - } - return results; -} - -const getTransformMessagesHandler: RouterRouteHandler = async (req, callWithRequest) => { - const { getTransformAuditMessages } = transformAuditMessagesProvider(callWithRequest); - const { transformId } = req.params; - - try { - return await getTransformAuditMessages(transformId); - } catch (e) { - return wrapEsError(e); - } -}; - -const esSearchHandler: RouterRouteHandler = async (req, callWithRequest) => { - try { - return await callWithRequest('search', req.payload); - } catch (e) { - return { error: wrapEsError(e) }; - } -}; diff --git a/x-pack/legacy/plugins/transform/server/shim.ts b/x-pack/legacy/plugins/transform/server/shim.ts deleted file mode 100644 index 8f477d86441f4..0000000000000 --- a/x-pack/legacy/plugins/transform/server/shim.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Legacy } from 'kibana'; -import { createRouter, Router } from '../../../server/lib/create_router'; -import { registerLicenseChecker } from '../../../server/lib/register_license_checker'; -import { elasticsearchJsPlugin } from './client/elasticsearch_transform'; -export interface Core { - http: { - createRouter(basePath: string): Router; - }; -} - -export interface Plugins { - license: { - registerLicenseChecker: typeof registerLicenseChecker; - }; - xpack_main: any; - elasticsearch: any; -} - -export function createServerShim( - server: Legacy.Server, - pluginId: string -): { core: Core; plugins: Plugins } { - return { - core: { - http: { - createRouter: (basePath: string) => - createRouter(server, pluginId, basePath, { - plugins: [elasticsearchJsPlugin], - }), - }, - }, - plugins: { - license: { - registerLicenseChecker, - }, - xpack_main: server.plugins.xpack_main, - elasticsearch: server.plugins.elasticsearch, - }, - }; -} diff --git a/x-pack/plugins/transform/kibana.json b/x-pack/plugins/transform/kibana.json new file mode 100644 index 0000000000000..87e38f83ef640 --- /dev/null +++ b/x-pack/plugins/transform/kibana.json @@ -0,0 +1,15 @@ +{ + "id": "transform", + "version": "kibana", + "server": true, + "ui": false, + "requiredPlugins": [ + "home", + "licensing", + "management" + ], + "optionalPlugins": [ + "usageCollection" + ], + "configPath": ["xpack", "transform"] +} diff --git a/x-pack/legacy/plugins/transform/server/client/elasticsearch_transform.ts b/x-pack/plugins/transform/server/client/elasticsearch_transform.ts similarity index 100% rename from x-pack/legacy/plugins/transform/server/client/elasticsearch_transform.ts rename to x-pack/plugins/transform/server/client/elasticsearch_transform.ts diff --git a/x-pack/plugins/transform/server/index.ts b/x-pack/plugins/transform/server/index.ts new file mode 100644 index 0000000000000..7b7cf3ee44fb5 --- /dev/null +++ b/x-pack/plugins/transform/server/index.ts @@ -0,0 +1,11 @@ +/* + * 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 { PluginInitializerContext } from 'src/core/server'; + +import { TransformServerPlugin } from './plugin'; + +export const plugin = (ctx: PluginInitializerContext) => new TransformServerPlugin(ctx); diff --git a/x-pack/plugins/transform/server/plugin.ts b/x-pack/plugins/transform/server/plugin.ts new file mode 100644 index 0000000000000..7da991bc02b37 --- /dev/null +++ b/x-pack/plugins/transform/server/plugin.ts @@ -0,0 +1,89 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { + CoreSetup, + Plugin, + IScopedClusterClient, + Logger, + PluginInitializerContext, +} from 'src/core/server'; + +import { LicenseType } from '../../licensing/common/types'; + +import { elasticsearchJsPlugin } from './client/elasticsearch_transform'; +import { Dependencies } from './types'; +import { ApiRoutes } from './routes'; +import { License } from './services'; + +declare module 'kibana/server' { + interface RequestHandlerContext { + transform?: { + dataClient: IScopedClusterClient; + }; + } +} + +const basicLicense: LicenseType = 'basic'; + +const PLUGIN = { + id: 'transform', + minimumLicenseType: basicLicense, + getI18nName: (): string => + i18n.translate('xpack.transform.appTitle', { + defaultMessage: 'Transforms', + }), +}; + +export class TransformServerPlugin implements Plugin<{}, void, any, any> { + private readonly apiRoutes: ApiRoutes; + private readonly license: License; + private readonly logger: Logger; + + constructor(initContext: PluginInitializerContext) { + this.logger = initContext.logger.get(); + this.apiRoutes = new ApiRoutes(); + this.license = new License(); + } + + setup({ elasticsearch, http }: CoreSetup, { licensing }: Dependencies): {} { + const router = http.createRouter(); + + this.license.setup( + { + pluginId: PLUGIN.id, + minimumLicenseType: PLUGIN.minimumLicenseType, + defaultErrorMessage: i18n.translate('xpack.transform.licenseCheckErrorMessage', { + defaultMessage: 'License check failed', + }), + }, + { + licensing, + logger: this.logger, + } + ); + + this.apiRoutes.setup({ + router, + license: this.license, + }); + + // Can access via new platform router's handler function 'context' parameter - context.transform.client + const transformClient = elasticsearch.createClient('transform', { + plugins: [elasticsearchJsPlugin], + }); + http.registerRouteHandlerContext('transform', (context, request) => { + return { + dataClient: transformClient.asScoped(request), + }; + }); + + return {}; + } + + start() {} + stop() {} +} diff --git a/x-pack/legacy/plugins/transform/server/routes/api/error_utils.ts b/x-pack/plugins/transform/server/routes/api/error_utils.ts similarity index 80% rename from x-pack/legacy/plugins/transform/server/routes/api/error_utils.ts rename to x-pack/plugins/transform/server/routes/api/error_utils.ts index 094c0308ff20f..d09152bf1a603 100644 --- a/x-pack/legacy/plugins/transform/server/routes/api/error_utils.ts +++ b/x-pack/plugins/transform/server/routes/api/error_utils.ts @@ -4,11 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ +import { boomify, isBoom } from 'boom'; + import { i18n } from '@kbn/i18n'; + +import { ResponseError, CustomHttpResponseOptions } from 'src/core/server'; + import { TransformEndpointRequest, TransformEndpointResult, -} from '../../../public/app/hooks/use_api_types'; +} from '../../../../../legacy/plugins/transform/public/app/hooks/use_api_types'; const REQUEST_TIMEOUT = 'RequestTimeout'; @@ -71,3 +76,12 @@ export function fillResultsWithTimeouts({ results, id, items, action }: Params) return accumResults; }, newResults); } + +export function wrapError(error: any): CustomHttpResponseOptions<ResponseError> { + const boom = isBoom(error) ? error : boomify(error, { statusCode: error.status }); + return { + body: boom, + headers: boom.output.headers, + statusCode: boom.output.statusCode, + }; +} diff --git a/x-pack/plugins/transform/server/routes/api/privileges.ts b/x-pack/plugins/transform/server/routes/api/privileges.ts new file mode 100644 index 0000000000000..6003a88ffa40c --- /dev/null +++ b/x-pack/plugins/transform/server/routes/api/privileges.ts @@ -0,0 +1,85 @@ +/* + * 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 { + APP_CLUSTER_PRIVILEGES, + APP_INDEX_PRIVILEGES, +} from '../../../../../legacy/plugins/transform/common/constants'; +// NOTE: now we import it from our "public" folder, but when the Authorisation lib +// will move to the "es_ui_shared" plugin, it will be imported from its "static" folder +import { Privileges } from '../../../../../legacy/plugins/transform/public/app/lib/authorization'; + +import { RouteDependencies } from '../../types'; +import { addBasePath } from '../index'; + +export function registerPrivilegesRoute({ router, license }: RouteDependencies) { + router.get( + { path: addBasePath('privileges'), validate: {} }, + license.guardApiRoute(async (ctx, req, res) => { + const privilegesResult: Privileges = { + hasAllPrivileges: true, + missingPrivileges: { + cluster: [], + index: [], + }, + }; + + if (license.getStatus().isSecurityEnabled === false) { + // If security isn't enabled, let the user use app. + return res.ok({ body: privilegesResult }); + } + + // Get cluster priviliges + const { + has_all_requested: hasAllPrivileges, + cluster, + } = await ctx.transform!.dataClient.callAsCurrentUser('transport.request', { + path: '/_security/user/_has_privileges', + method: 'POST', + body: { + cluster: APP_CLUSTER_PRIVILEGES, + }, + }); + + // Find missing cluster privileges and set overall app privileges + privilegesResult.missingPrivileges.cluster = extractMissingPrivileges(cluster); + privilegesResult.hasAllPrivileges = hasAllPrivileges; + + // Get all index privileges the user has + const { indices } = await ctx.transform!.dataClient.callAsCurrentUser('transport.request', { + path: '/_security/user/_privileges', + method: 'GET', + }); + + // Check if they have all the required index privileges for at least one index + const oneIndexWithAllPrivileges = indices.find(({ privileges }: { privileges: string[] }) => { + if (privileges.includes('all')) { + return true; + } + + const indexHasAllPrivileges = APP_INDEX_PRIVILEGES.every(privilege => + privileges.includes(privilege) + ); + + return indexHasAllPrivileges; + }); + + // If they don't, return list of required index privileges + if (!oneIndexWithAllPrivileges) { + privilegesResult.missingPrivileges.index = [...APP_INDEX_PRIVILEGES]; + } + + return res.ok({ body: privilegesResult }); + }) + ); +} + +const extractMissingPrivileges = (privilegesObject: { [key: string]: boolean } = {}): string[] => + Object.keys(privilegesObject).reduce((privileges: string[], privilegeName: string): string[] => { + if (!privilegesObject[privilegeName]) { + privileges.push(privilegeName); + } + return privileges; + }, []); diff --git a/x-pack/plugins/transform/server/routes/api/schema.ts b/x-pack/plugins/transform/server/routes/api/schema.ts new file mode 100644 index 0000000000000..0b994406d324d --- /dev/null +++ b/x-pack/plugins/transform/server/routes/api/schema.ts @@ -0,0 +1,16 @@ +/* + * 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 { schema } from '@kbn/config-schema'; + +export const schemaTransformId = { + params: schema.object({ + transformId: schema.string(), + }), +}; + +export interface SchemaTransformId { + transformId: string; +} diff --git a/x-pack/plugins/transform/server/routes/api/transforms.ts b/x-pack/plugins/transform/server/routes/api/transforms.ts new file mode 100644 index 0000000000000..7aaae1f1c7039 --- /dev/null +++ b/x-pack/plugins/transform/server/routes/api/transforms.ts @@ -0,0 +1,360 @@ +/* + * 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 { schema } from '@kbn/config-schema'; + +import { RequestHandler } from 'kibana/server'; +import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import { wrapEsError } from '../../../../../legacy/server/lib/create_router/error_wrappers'; + +import { TRANSFORM_STATE } from '../../../../../legacy/plugins/transform/public/app/common'; +import { + TransformEndpointRequest, + TransformEndpointResult, +} from '../../../../../legacy/plugins/transform/public/app/hooks/use_api_types'; +import { TransformId } from '../../../../../legacy/plugins/transform/public/app/common/transform'; + +import { RouteDependencies } from '../../types'; + +import { addBasePath } from '../index'; + +import { isRequestTimeout, fillResultsWithTimeouts, wrapError } from './error_utils'; +import { schemaTransformId, SchemaTransformId } from './schema'; +import { registerTransformsAuditMessagesRoutes } from './transforms_audit_messages'; + +enum TRANSFORM_ACTIONS { + STOP = 'stop', + START = 'start', + DELETE = 'delete', +} + +interface StopOptions { + transformId: TransformId; + force: boolean; + waitForCompletion?: boolean; +} + +export function registerTransformsRoutes(routeDependencies: RouteDependencies) { + const { router, license } = routeDependencies; + router.get( + { path: addBasePath('transforms'), validate: false }, + license.guardApiRoute(async (ctx, req, res) => { + const options = {}; + try { + const transforms = await getTransforms( + options, + ctx.transform!.dataClient.callAsCurrentUser + ); + return res.ok({ body: transforms }); + } catch (e) { + return res.customError(wrapError(wrapEsError(e))); + } + }) + ); + router.get( + { + path: addBasePath('transforms/{transformId}'), + validate: schemaTransformId, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { transformId } = req.params as SchemaTransformId; + const options = { + ...(transformId !== undefined ? { transformId } : {}), + }; + try { + const transforms = await getTransforms( + options, + ctx.transform!.dataClient.callAsCurrentUser + ); + return res.ok({ body: transforms }); + } catch (e) { + return res.customError(wrapError(wrapEsError(e))); + } + }) + ); + router.get( + { path: addBasePath('transforms/_stats'), validate: false }, + license.guardApiRoute(async (ctx, req, res) => { + const options = {}; + try { + const stats = await ctx.transform!.dataClient.callAsCurrentUser( + 'transform.getTransformsStats', + options + ); + return res.ok({ body: stats }); + } catch (e) { + return res.customError(wrapError(wrapEsError(e))); + } + }) + ); + router.get( + { + path: addBasePath('transforms/{transformId}/_stats'), + validate: schemaTransformId, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { transformId } = req.params as SchemaTransformId; + const options = { + ...(transformId !== undefined ? { transformId } : {}), + }; + try { + const stats = await ctx.transform!.dataClient.callAsCurrentUser( + 'transform.getTransformsStats', + options + ); + return res.ok({ body: stats }); + } catch (e) { + return res.customError(wrapError(wrapEsError(e))); + } + }) + ); + registerTransformsAuditMessagesRoutes(routeDependencies); + router.put( + { + path: addBasePath('transforms/{transformId}'), + validate: { + ...schemaTransformId, + body: schema.maybe(schema.any()), + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { transformId } = req.params as SchemaTransformId; + + const response: { + transformsCreated: Array<{ transform: string }>; + errors: any[]; + } = { + transformsCreated: [], + errors: [], + }; + + await ctx + .transform!.dataClient.callAsCurrentUser('transform.createTransform', { + body: req.body, + transformId, + }) + .then(() => response.transformsCreated.push({ transform: transformId })) + .catch(e => + response.errors.push({ + id: transformId, + error: wrapEsError(e), + }) + ); + + return res.ok({ body: response }); + }) + ); + router.post( + { + path: addBasePath('delete_transforms'), + validate: { + body: schema.maybe(schema.any()), + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const transformsInfo = req.body as TransformEndpointRequest[]; + + try { + return res.ok({ + body: await deleteTransforms(transformsInfo, ctx.transform!.dataClient.callAsCurrentUser), + }); + } catch (e) { + return res.customError(wrapError(wrapEsError(e))); + } + }) + ); + router.post( + { + path: addBasePath('transforms/_preview'), + validate: { + body: schema.maybe(schema.any()), + }, + }, + license.guardApiRoute(previewTransformHandler) + ); + router.post( + { + path: addBasePath('start_transforms'), + validate: { + body: schema.maybe(schema.any()), + }, + }, + license.guardApiRoute(startTransformsHandler) + ); + router.post( + { + path: addBasePath('stop_transforms'), + validate: { + body: schema.maybe(schema.any()), + }, + }, + license.guardApiRoute(stopTransformsHandler) + ); + router.post( + { + path: addBasePath('es_search'), + validate: { + body: schema.maybe(schema.any()), + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + try { + return res.ok({ + body: await ctx.transform!.dataClient.callAsCurrentUser('search', req.body), + }); + } catch (e) { + return res.customError(wrapError(wrapEsError(e))); + } + }) + ); +} + +const getTransforms = async (options: { transformId?: string }, callAsCurrentUser: CallCluster) => { + return await callAsCurrentUser('transform.getTransforms', options); +}; + +async function deleteTransforms( + transformsInfo: TransformEndpointRequest[], + callAsCurrentUser: CallCluster +) { + const results: TransformEndpointResult = {}; + + for (const transformInfo of transformsInfo) { + const transformId = transformInfo.id; + try { + if (transformInfo.state === TRANSFORM_STATE.FAILED) { + try { + await callAsCurrentUser('transform.stopTransform', { + transformId, + force: true, + waitForCompletion: true, + } as StopOptions); + } catch (e) { + if (isRequestTimeout(e)) { + return fillResultsWithTimeouts({ + results, + id: transformId, + items: transformsInfo, + action: TRANSFORM_ACTIONS.DELETE, + }); + } + } + } + + await callAsCurrentUser('transform.deleteTransform', { transformId }); + results[transformId] = { success: true }; + } catch (e) { + if (isRequestTimeout(e)) { + return fillResultsWithTimeouts({ + results, + id: transformInfo.id, + items: transformsInfo, + action: TRANSFORM_ACTIONS.DELETE, + }); + } + results[transformId] = { success: false, error: JSON.stringify(e) }; + } + } + return results; +} + +const previewTransformHandler: RequestHandler = async (ctx, req, res) => { + try { + return res.ok({ + body: await ctx.transform!.dataClient.callAsCurrentUser('transform.getTransformsPreview', { + body: req.body, + }), + }); + } catch (e) { + return res.customError(wrapError(wrapEsError(e))); + } +}; + +const startTransformsHandler: RequestHandler = async (ctx, req, res) => { + const { transformsInfo } = req.body as { + transformsInfo: TransformEndpointRequest[]; + }; + + try { + return res.ok({ + body: await startTransforms(transformsInfo, ctx.transform!.dataClient.callAsCurrentUser), + }); + } catch (e) { + return res.customError(wrapError(wrapEsError(e))); + } +}; + +async function startTransforms( + transformsInfo: TransformEndpointRequest[], + callAsCurrentUser: CallCluster +) { + const results: TransformEndpointResult = {}; + + for (const transformInfo of transformsInfo) { + const transformId = transformInfo.id; + try { + await callAsCurrentUser('transform.startTransform', { transformId } as SchemaTransformId); + results[transformId] = { success: true }; + } catch (e) { + if (isRequestTimeout(e)) { + return fillResultsWithTimeouts({ + results, + id: transformId, + items: transformsInfo, + action: TRANSFORM_ACTIONS.START, + }); + } + results[transformId] = { success: false, error: JSON.stringify(e) }; + } + } + return results; +} + +const stopTransformsHandler: RequestHandler = async (ctx, req, res) => { + const { transformsInfo } = req.body as { + transformsInfo: TransformEndpointRequest[]; + }; + + try { + return res.ok({ + body: await stopTransforms(transformsInfo, ctx.transform!.dataClient.callAsCurrentUser), + }); + } catch (e) { + return res.customError(wrapError(wrapEsError(e))); + } +}; + +async function stopTransforms( + transformsInfo: TransformEndpointRequest[], + callAsCurrentUser: CallCluster +) { + const results: TransformEndpointResult = {}; + + for (const transformInfo of transformsInfo) { + const transformId = transformInfo.id; + try { + await callAsCurrentUser('transform.stopTransform', { + transformId, + force: + transformInfo.state !== undefined + ? transformInfo.state === TRANSFORM_STATE.FAILED + : false, + waitForCompletion: true, + } as StopOptions); + results[transformId] = { success: true }; + } catch (e) { + if (isRequestTimeout(e)) { + return fillResultsWithTimeouts({ + results, + id: transformId, + items: transformsInfo, + action: TRANSFORM_ACTIONS.STOP, + }); + } + results[transformId] = { success: false, error: JSON.stringify(e) }; + } + } + return results; +} diff --git a/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts b/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts new file mode 100644 index 0000000000000..422fdec7ab77e --- /dev/null +++ b/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts @@ -0,0 +1,91 @@ +/* + * 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 { AuditMessage } from '../../../../../legacy/plugins/transform/common/types/messages'; +import { wrapEsError } from '../../../../../legacy/server/lib/create_router/error_wrappers'; + +import { RouteDependencies } from '../../types'; + +import { addBasePath } from '../index'; + +import { wrapError } from './error_utils'; +import { schemaTransformId, SchemaTransformId } from './schema'; + +const ML_DF_NOTIFICATION_INDEX_PATTERN = '.transform-notifications-read'; +const SIZE = 500; + +interface BoolQuery { + bool: { [key: string]: any }; +} + +export function registerTransformsAuditMessagesRoutes({ router, license }: RouteDependencies) { + router.get( + { path: addBasePath('transforms/{transformId}/messages'), validate: schemaTransformId }, + license.guardApiRoute(async (ctx, req, res) => { + const { transformId } = req.params as SchemaTransformId; + + // search for audit messages, + // transformId is optional. without it, all transforms will be listed. + const query: BoolQuery = { + bool: { + filter: [ + { + bool: { + must_not: { + term: { + level: 'activity', + }, + }, + }, + }, + ], + }, + }; + + // if no transformId specified, load all of the messages + if (transformId !== undefined) { + query.bool.filter.push({ + bool: { + should: [ + { + term: { + transform_id: '', // catch system messages + }, + }, + { + term: { + transform_id: transformId, // messages for specified transformId + }, + }, + ], + }, + }); + } + + try { + const resp = await ctx.transform!.dataClient.callAsCurrentUser('search', { + index: ML_DF_NOTIFICATION_INDEX_PATTERN, + ignore_unavailable: true, + rest_total_hits_as_int: true, + size: SIZE, + body: { + sort: [{ timestamp: { order: 'desc' } }, { transform_id: { order: 'asc' } }], + query, + }, + }); + + let messages = []; + if (resp.hits.total !== 0) { + messages = resp.hits.hits.map((hit: AuditMessage) => hit._source); + messages.reverse(); + } + return res.ok({ body: messages }); + } catch (e) { + return res.customError(wrapError(wrapEsError(e))); + } + }) + ); +} diff --git a/x-pack/plugins/transform/server/routes/index.ts b/x-pack/plugins/transform/server/routes/index.ts new file mode 100644 index 0000000000000..953490920cbcb --- /dev/null +++ b/x-pack/plugins/transform/server/routes/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { RouteDependencies } from '../types'; + +import { registerPrivilegesRoute } from './api/privileges'; +import { registerTransformsRoutes } from './api/transforms'; + +import { API_BASE_PATH } from '../../../../legacy/plugins/transform/common/constants'; + +export const addBasePath = (uri: string): string => `${API_BASE_PATH}${uri}`; + +export class ApiRoutes { + setup(dependencies: RouteDependencies) { + registerPrivilegesRoute(dependencies); + registerTransformsRoutes(dependencies); + } + + start() {} + stop() {} +} diff --git a/x-pack/plugins/transform/server/services/index.ts b/x-pack/plugins/transform/server/services/index.ts new file mode 100644 index 0000000000000..b7a45e59549eb --- /dev/null +++ b/x-pack/plugins/transform/server/services/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { License } from './license'; diff --git a/x-pack/plugins/transform/server/services/license.ts b/x-pack/plugins/transform/server/services/license.ts new file mode 100644 index 0000000000000..93346160c6f44 --- /dev/null +++ b/x-pack/plugins/transform/server/services/license.ts @@ -0,0 +1,91 @@ +/* + * 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 { Logger } from 'src/core/server'; +import { + IKibanaResponse, + KibanaRequest, + KibanaResponseFactory, + RequestHandler, + RequestHandlerContext, +} from 'kibana/server'; + +import { LicensingPluginSetup, LicenseType, LICENSE_CHECK_STATE } from '../../../licensing/server'; + +export interface LicenseStatus { + isValid: boolean; + isSecurityEnabled: boolean; + message?: string; +} + +interface SetupSettings { + pluginId: string; + minimumLicenseType: LicenseType; + defaultErrorMessage: string; +} + +export class License { + private licenseStatus: LicenseStatus = { + isValid: false, + isSecurityEnabled: false, + message: 'Invalid License', + }; + + setup( + { pluginId, minimumLicenseType, defaultErrorMessage }: SetupSettings, + { licensing, logger }: { licensing: LicensingPluginSetup; logger: Logger } + ) { + licensing.license$.subscribe(license => { + const { state, message } = license.check(pluginId, minimumLicenseType); + const hasRequiredLicense = state === LICENSE_CHECK_STATE.Valid; + + const securityFeature = license.getFeature('security'); + const isSecurityEnabled = + securityFeature !== undefined && + securityFeature.isAvailable === true && + securityFeature.isEnabled === true; + + if (hasRequiredLicense) { + this.licenseStatus = { isValid: true, isSecurityEnabled }; + } else { + this.licenseStatus = { + isValid: false, + isSecurityEnabled, + message: message || defaultErrorMessage, + }; + if (message) { + logger.info(message); + } + } + }); + } + + guardApiRoute(handler: RequestHandler<unknown, unknown, any, any>) { + const license = this; + + return function licenseCheck( + ctx: RequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory + ): IKibanaResponse<any> | Promise<IKibanaResponse<any>> { + const licenseStatus = license.getStatus(); + + if (!licenseStatus.isValid) { + return response.customError({ + body: { + message: licenseStatus.message || '', + }, + statusCode: 403, + }); + } + + return handler(ctx, request, response); + }; + } + + getStatus() { + return this.licenseStatus; + } +} diff --git a/x-pack/plugins/transform/server/types.ts b/x-pack/plugins/transform/server/types.ts new file mode 100644 index 0000000000000..5fcc23a6d9f48 --- /dev/null +++ b/x-pack/plugins/transform/server/types.ts @@ -0,0 +1,18 @@ +/* + * 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 { IRouter } from 'src/core/server'; +import { LicensingPluginSetup } from '../../licensing/server'; +import { License } from './services'; + +export interface Dependencies { + licensing: LicensingPluginSetup; +} + +export interface RouteDependencies { + router: IRouter; + license: License; +} From 4712faefb76facc4c7ae400aa140fe2a0ac0dfde Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens <jloleysens@gmail.com> Date: Tue, 3 Mar 2020 09:06:00 +0100 Subject: [PATCH 29/34] Fix the namespace for indices autocompletion (#59043) --- .../spec_definitions/spec/overrides/indices.put_settings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.put_settings.json b/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.put_settings.json index 78a5bbdbcf6c2..2e1e3024665a4 100644 --- a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.put_settings.json +++ b/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.put_settings.json @@ -1,5 +1,5 @@ { - "put_settings": { + "indices.put_settings": { "data_autocomplete_rules": { "refresh_interval": "1s", "number_of_shards": 1, @@ -105,4 +105,4 @@ } } } -} \ No newline at end of file +} From 166716a405d737bdb33b6c4a5cc75107a59d80b4 Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Tue, 3 Mar 2020 12:39:52 +0300 Subject: [PATCH 30/34] [Visualize] Move linked search to react component (#58590) * Move linked_search to react * Use i18n from start contract * Move linked search to the editor * Updating layout and fixing truncation * Fix functional test, add a tooltip Co-authored-by: Caroline Horn <549577+cchaos@users.noreply.github.com> --- .../visualize/np_ready/editor/_editor.scss | 4 - .../visualize/np_ready/editor/editor.html | 24 --- .../visualize/np_ready/editor/editor.js | 20 +- .../np_ready/editor/visualization_editor.js | 1 + .../public/visualize/np_ready/types.d.ts | 4 + .../vis_default_editor/public/_sidebar.scss | 6 +- .../public/components/sidebar/sidebar.tsx | 26 +-- .../components/sidebar/sidebar_title.tsx | 175 ++++++++++++++++++ .../public/default_editor.tsx | 5 +- .../functional/page_objects/visualize_page.ts | 3 +- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 12 files changed, 216 insertions(+), 56 deletions(-) create mode 100644 src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar_title.tsx diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/_editor.scss b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/_editor.scss index 2f48ecc322fea..3a542cacc44be 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/_editor.scss +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/_editor.scss @@ -22,10 +22,6 @@ a tilemap in an iframe: https://github.com/elastic/kibana/issues/16457 */ } } -.visEditor__linkedMessage { - padding: $euiSizeS; -} - .visEditor__content { @include flex-parent(); width: 100%; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html index 4979d9dc89a0c..9dbb05ea95b48 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html @@ -1,28 +1,4 @@ <visualize-app class="app-container visEditor visEditor--{{ vis.type.name }}"> - <!-- Linked search. --> - <div - ng-show="isVisible" - ng-if="vis.type.requiresSearch && linked" - class="fullWidth visEditor__linkedMessage" - > - <div class="kuiVerticalRhythmSmall"> - {{ ::'kbn.visualize.linkedToSearchInfoText' | i18n: { defaultMessage: 'Linked to Saved Search' } }} - <a - href="#/discover/{{savedVis.savedSearch.id}}" - > - {{ savedVis.savedSearch.title }} - </a> -   - <a - data-test-subj="unlinkSavedSearch" - href="" - ng-dblclick="unlink()" - tooltip="{{ ::'kbn.visualize.linkedToSearch.unlinkButtonTooltip' | i18n: { defaultMessage: 'Double click to unlink from Saved Search' } }}" - > - <span aria-hidden="true" class="kuiIcon fa-chain-broken"></span> - </a> - </div> - </div> <!-- Local nav. diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js index 2137e413451d2..293327f3f72f9 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js @@ -551,6 +551,20 @@ function VisualizeAppController( updateStateFromSavedQuery(newSavedQuery); }); + $scope.$watch('linked', linked => { + if (linked && !savedVis.savedSearchId) { + savedVis.savedSearchId = savedVis.searchSource.id; + vis.savedSearchId = savedVis.searchSource.id; + + $scope.$broadcast('render'); + } else if (!linked && savedVis.savedSearchId) { + delete savedVis.savedSearchId; + delete vis.savedSearchId; + + $scope.$broadcast('render'); + } + }); + /** * Called when the user clicks "Save" button. */ @@ -638,9 +652,7 @@ function VisualizeAppController( ); } - $scope.unlink = function() { - if (!$scope.linked) return; - + const unlinkFromSavedSearch = () => { const searchSourceParent = searchSource.getParent(); const searchSourceGrandparent = searchSourceParent.getParent(); @@ -681,6 +693,8 @@ function VisualizeAppController( ); }; + vis.on('unlinkFromSavedSearch', unlinkFromSavedSearch); + addHelpMenuToAppChrome(chrome, docLinks); init(); diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js index 65c25b8cf705d..f2d9cbe2ad84c 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js @@ -43,6 +43,7 @@ export function initVisEditorDirective(app, deps) { filters: $scope.filters, query: $scope.query, appState: $scope.appState, + linked: !!$scope.savedObj.savedSearchId, }); }; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts index d95939170419b..8ca603eb11459 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts @@ -59,6 +59,10 @@ export interface EditorRenderProps { uiState: PersistedState; timeRange: TimeRange; query?: Query; + /** + * Flag to determine if visualiztion is linked to the saved search + */ + linked: boolean; } export interface SavedVisualizations { diff --git a/src/legacy/core_plugins/vis_default_editor/public/_sidebar.scss b/src/legacy/core_plugins/vis_default_editor/public/_sidebar.scss index a38c729cb4622..ed92dc1dae884 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/_sidebar.scss +++ b/src/legacy/core_plugins/vis_default_editor/public/_sidebar.scss @@ -33,8 +33,7 @@ // NAVIGATION // -.visEditorSidebar__indexPattern { - @include euiTextTruncate; +.visEditorSidebar__titleContainer { padding: $euiSizeS $euiSizeXL $euiSizeS $euiSizeS; // Extra padding on the right for the collapse button } @@ -43,7 +42,8 @@ border-bottom: $euiBorderThin; } -.visEditorSidebar__nav { +.visEditorSidebar__nav, +.visEditorSidebar__linkedSearch { flex-grow: 0; } diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx index d3b843eaaec9f..425245fe91fed 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx @@ -20,13 +20,16 @@ import React, { useMemo, useState, useCallback, KeyboardEventHandler, useEffect } from 'react'; import { get, isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { keyCodes, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; +import { keyCodes, EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + import { Vis } from 'src/legacy/core_plugins/visualizations/public'; import { AggGroupNames } from '../../legacy_imports'; import { DefaultEditorNavBar, OptionTab } from './navbar'; import { DefaultEditorControls } from './controls'; import { setStateParamValue, useEditorReducer, useEditorFormState, discardChanges } from './state'; import { DefaultEditorAggCommonProps } from '../agg_common_props'; +import { SidebarTitle } from './sidebar_title'; +import { SavedSearch } from '../../../../kibana/public/discover/np_ready/types'; import { PersistedState } from '../../../../../../plugins/visualizations/public'; interface DefaultEditorSideBarProps { @@ -35,6 +38,8 @@ interface DefaultEditorSideBarProps { optionTabs: OptionTab[]; uiState: PersistedState; vis: Vis; + isLinkedSearch: boolean; + savedSearch?: SavedSearch; } function DefaultEditorSideBar({ @@ -43,6 +48,8 @@ function DefaultEditorSideBar({ optionTabs, uiState, vis, + isLinkedSearch, + savedSearch, }: DefaultEditorSideBarProps) { const [selectedTab, setSelectedTab] = useState(optionTabs[0].name); const [isDirty, setDirty] = useState(false); @@ -161,21 +168,8 @@ function DefaultEditorSideBar({ name="visualizeEditor" onKeyDownCapture={onSubmit} > - {vis.type.requiresSearch && vis.type.options.showIndexSelection ? ( - <EuiTitle size="xs" className="visEditorSidebar__indexPattern"> - <h2 - title={i18n.translate('visDefaultEditor.sidebar.indexPatternAriaLabel', { - defaultMessage: 'Index pattern: {title}', - values: { - title: vis.indexPattern.title, - }, - })} - > - {vis.indexPattern.title} - </h2> - </EuiTitle> - ) : ( - <div className="visEditorSidebar__indexPatternPlaceholder" /> + {vis.type.requiresSearch && ( + <SidebarTitle isLinkedSearch={isLinkedSearch} savedSearch={savedSearch} vis={vis} /> )} {optionTabs.length > 1 && ( diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar_title.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar_title.tsx new file mode 100644 index 0000000000000..3fd82f1c4a2b6 --- /dev/null +++ b/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar_title.tsx @@ -0,0 +1,175 @@ +/* + * 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, { useCallback, useState } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPopover, + EuiPopoverTitle, + EuiText, + EuiTitle, + EuiToolTip, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { Vis } from 'src/legacy/core_plugins/visualizations/public'; +import { SavedSearch } from '../../../../kibana/public/discover/np_ready/types'; + +interface LinkedSearchProps { + savedSearch: SavedSearch; + vis: Vis; +} + +interface SidebarTitleProps { + isLinkedSearch: boolean; + savedSearch?: SavedSearch; + vis: Vis; +} + +export function LinkedSearch({ savedSearch, vis }: LinkedSearchProps) { + const [showPopover, setShowPopover] = useState(false); + const closePopover = useCallback(() => setShowPopover(false), []); + const onClickButtonLink = useCallback(() => setShowPopover(v => !v), []); + const onClickUnlikFromSavedSearch = useCallback(() => { + setShowPopover(false); + vis.emit('unlinkFromSavedSearch'); + }, [vis]); + + const linkButtonAriaLabel = i18n.translate( + 'visDefaultEditor.sidebar.savedSearch.linkButtonAriaLabel', + { + defaultMessage: 'Link to saved search. Click to learn more or break link.', + } + ); + + return ( + <EuiFlexGroup + alignItems="center" + className="visEditorSidebar__titleContainer visEditorSidebar__linkedSearch" + gutterSize="xs" + responsive={false} + > + <EuiFlexItem grow={false}> + <EuiIcon type="search" /> + </EuiFlexItem> + + <EuiFlexItem grow={false} className="eui-textTruncate"> + <EuiTitle size="xs" className="eui-textTruncate"> + <h2 + title={i18n.translate('visDefaultEditor.sidebar.savedSearch.titleAriaLabel', { + defaultMessage: 'Saved search: {title}', + values: { + title: savedSearch.title, + }, + })} + > + {savedSearch.title} + </h2> + </EuiTitle> + </EuiFlexItem> + + <EuiFlexItem grow={false}> + <EuiPopover + anchorPosition="downRight" + button={ + <EuiToolTip content={linkButtonAriaLabel}> + <EuiButtonIcon + aria-label={linkButtonAriaLabel} + data-test-subj="showUnlinkSavedSearchPopover" + iconType="link" + onClick={onClickButtonLink} + /> + </EuiToolTip> + } + isOpen={showPopover} + closePopover={closePopover} + panelPaddingSize="s" + > + <EuiPopoverTitle> + <FormattedMessage + id="visDefaultEditor.sidebar.savedSearch.popoverTitle" + defaultMessage="Linked to saved search" + /> + </EuiPopoverTitle> + <div style={{ width: 260 }}> + <EuiText size="s"> + <p> + <EuiButtonEmpty flush="left" href={`#/discover/${savedSearch.id}`} size="xs"> + <FormattedMessage + id="visDefaultEditor.sidebar.savedSearch.goToDiscoverButtonText" + defaultMessage="View this search in Discover" + /> + </EuiButtonEmpty> + </p> + <p> + <FormattedMessage + id="visDefaultEditor.sidebar.savedSearch.popoverHelpText" + defaultMessage="Subsequent modifications to this saved search are reflected in the visualization. To disable automatic updates, remove the link." + /> + </p> + <p> + <EuiButton + color="danger" + data-test-subj="unlinkSavedSearch" + fullWidth + onClick={onClickUnlikFromSavedSearch} + size="s" + > + <FormattedMessage + id="visDefaultEditor.sidebar.savedSearch.unlinkSavedSearchButtonText" + defaultMessage="Remove link to saved search" + /> + </EuiButton> + </p> + </EuiText> + </div> + </EuiPopover> + </EuiFlexItem> + </EuiFlexGroup> + ); +} + +function SidebarTitle({ savedSearch, vis, isLinkedSearch }: SidebarTitleProps) { + return isLinkedSearch && savedSearch ? ( + <LinkedSearch savedSearch={savedSearch} vis={vis} /> + ) : vis.type.options.showIndexSelection ? ( + <EuiTitle size="xs" className="visEditorSidebar__titleContainer eui-textTruncate"> + <h2 + title={i18n.translate('visDefaultEditor.sidebar.indexPatternAriaLabel', { + defaultMessage: 'Index pattern: {title}', + values: { + title: vis.indexPattern.title, + }, + })} + > + {vis.indexPattern.title} + </h2> + </EuiTitle> + ) : ( + <div className="visEditorSidebar__indexPatternPlaceholder" /> + ); +} + +export { SidebarTitle }; diff --git a/src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx b/src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx index 7eee54006f684..fa3213d244e7e 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx @@ -40,12 +40,13 @@ function DefaultEditor({ appState, optionTabs, query, + linked, }: DefaultEditorControllerState & Omit<EditorRenderProps, 'data' | 'core'>) { const visRef = useRef<HTMLDivElement>(null); const visHandler = useRef<VisualizeEmbeddable | null>(null); const [isCollapsed, setIsCollapsed] = useState(false); const [factory, setFactory] = useState<VisualizeEmbeddableFactory | null>(null); - const { vis } = savedObj; + const { vis, savedSearch } = savedObj; const onClickCollapse = useCallback(() => { setIsCollapsed(value => !value); @@ -117,6 +118,8 @@ function DefaultEditor({ optionTabs={optionTabs} vis={vis} uiState={uiState} + isLinkedSearch={linked} + savedSearch={savedSearch} /> </Panel> </PanelsContainer> diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index e54e3d1d01154..82ef3dc800f6c 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -209,7 +209,8 @@ export function VisualizePageProvider({ getService, getPageObjects }: FtrProvide } public async clickUnlinkSavedSearch() { - await testSubjects.doubleClick('unlinkSavedSearch'); + await testSubjects.click('showUnlinkSavedSearchPopover'); + await testSubjects.click('unlinkSavedSearch'); await header.waitUntilLoadingHasFinished(); } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a97cf608abc71..cadebcad93510 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1568,9 +1568,7 @@ "kbn.visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage": "indexPattern または savedSearchId が必要です", "kbn.visualize.editor.createBreadcrumb": "作成", "kbn.visualize.experimentalVisInfoText": "このビジュアライゼーションは実験的なものです。", - "kbn.visualize.linkedToSearch.unlinkButtonTooltip": "保存された検索からリンクを解除するにはダブルクリックします", "kbn.visualize.linkedToSearch.unlinkSuccessNotificationText": "保存された検索「{searchTitle}」からリンクが解除されました", - "kbn.visualize.linkedToSearchInfoText": "保存された検索にリンクされています", "kbn.visualize.listing.betaTitle": "ベータ", "kbn.visualize.listing.betaTooltip": "このビジュアライゼーションはベータ段階で、変更される可能性があります。デザインとコードはオフィシャル GA 機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャル GA 機能の SLA が適用されません", "kbn.visualize.listing.breadcrumb": "可視化", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index e6055680e1240..ff72f20b24864 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1568,9 +1568,7 @@ "kbn.visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage": "必须提供 indexPattern 或 savedSearchId", "kbn.visualize.editor.createBreadcrumb": "创建", "kbn.visualize.experimentalVisInfoText": "此可视化标记为“实验”。", - "kbn.visualize.linkedToSearch.unlinkButtonTooltip": "双击可取消与“已保存搜索”的链接", "kbn.visualize.linkedToSearch.unlinkSuccessNotificationText": "取消与已保存搜索 “{searchTitle}” 的链接", - "kbn.visualize.linkedToSearchInfoText": "链接到“已保存搜索”", "kbn.visualize.listing.betaTitle": "公测版", "kbn.visualize.listing.betaTooltip": "此可视化为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束", "kbn.visualize.listing.breadcrumb": "可视化", From 64ffae3ec5f5b5c977c4b0702ead681866bf4972 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet <pierre.gayvallet@elastic.co> Date: Tue, 3 Mar 2020 10:54:03 +0100 Subject: [PATCH 31/34] Add core metrics service (#58623) * create base service and collectors * wire the service into server, add mock * add collector tests * add main collector test * export metric types from server * add service and server tests * updates generated doc * improve doc * nits and comments * add disconnected requests test --- .../core/server/kibana-plugin-server.md | 5 + ...rver.metricsservicesetup.getopsmetrics_.md | 24 +++ ...ibana-plugin-server.metricsservicesetup.md | 20 ++ ...erver.opsmetrics.concurrent_connections.md | 13 ++ .../server/kibana-plugin-server.opsmetrics.md | 24 +++ .../kibana-plugin-server.opsmetrics.os.md | 13 ++ ...kibana-plugin-server.opsmetrics.process.md | 13 ++ ...ibana-plugin-server.opsmetrics.requests.md | 13 ++ ...plugin-server.opsmetrics.response_times.md | 13 ++ ...ibana-plugin-server.opsosmetrics.distro.md | 13 ++ ...lugin-server.opsosmetrics.distrorelease.md | 13 ++ .../kibana-plugin-server.opsosmetrics.load.md | 17 ++ .../kibana-plugin-server.opsosmetrics.md | 26 +++ ...ibana-plugin-server.opsosmetrics.memory.md | 17 ++ ...ana-plugin-server.opsosmetrics.platform.md | 13 ++ ...gin-server.opsosmetrics.platformrelease.md | 13 ++ ...in-server.opsosmetrics.uptime_in_millis.md | 13 ++ ...rver.opsprocessmetrics.event_loop_delay.md | 13 ++ .../kibana-plugin-server.opsprocessmetrics.md | 23 +++ ...-plugin-server.opsprocessmetrics.memory.md | 20 ++ ...ana-plugin-server.opsprocessmetrics.pid.md | 13 ++ ...rver.opsprocessmetrics.uptime_in_millis.md | 13 ++ ...opsservermetrics.concurrent_connections.md | 13 ++ .../kibana-plugin-server.opsservermetrics.md | 22 +++ ...plugin-server.opsservermetrics.requests.md | 17 ++ ...-server.opsservermetrics.response_times.md | 16 ++ package.json | 1 + src/core/server/index.ts | 8 + src/core/server/internal_types.ts | 2 + src/core/server/legacy/legacy_service.test.ts | 2 + src/core/server/metrics/collectors/index.ts | 23 +++ src/core/server/metrics/collectors/os.test.ts | 99 ++++++++++ src/core/server/metrics/collectors/os.ts | 60 ++++++ .../server/metrics/collectors/process.test.ts | 81 ++++++++ src/core/server/metrics/collectors/process.ts | 52 +++++ src/core/server/metrics/collectors/server.ts | 80 ++++++++ src/core/server/metrics/collectors/types.ts | 110 +++++++++++ src/core/server/metrics/index.ts | 29 +++ .../server_collector.test.ts | 183 ++++++++++++++++++ .../server/metrics/metrics_service.mock.ts | 67 +++++++ .../metrics/metrics_service.test.mocks.ts | 25 +++ .../server/metrics/metrics_service.test.ts | 134 +++++++++++++ src/core/server/metrics/metrics_service.ts | 86 ++++++++ src/core/server/metrics/ops_config.ts | 29 +++ .../ops_metrics_collector.test.mocks.ts | 39 ++++ .../metrics/ops_metrics_collector.test.ts | 59 ++++++ .../server/metrics/ops_metrics_collector.ts | 52 +++++ src/core/server/metrics/types.ts | 66 +++++++ src/core/server/mocks.ts | 5 +- src/core/server/server.api.md | 62 ++++++ src/core/server/server.test.mocks.ts | 6 + src/core/server/server.test.ts | 9 + src/core/server/server.ts | 9 + 53 files changed, 1790 insertions(+), 1 deletion(-) create mode 100644 docs/development/core/server/kibana-plugin-server.metricsservicesetup.getopsmetrics_.md create mode 100644 docs/development/core/server/kibana-plugin-server.metricsservicesetup.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsmetrics.concurrent_connections.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsmetrics.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsmetrics.os.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsmetrics.process.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsmetrics.requests.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsmetrics.response_times.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsosmetrics.distro.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsosmetrics.distrorelease.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsosmetrics.load.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsosmetrics.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsosmetrics.memory.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsosmetrics.platform.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsosmetrics.platformrelease.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsosmetrics.uptime_in_millis.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsprocessmetrics.event_loop_delay.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsprocessmetrics.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsprocessmetrics.memory.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsprocessmetrics.pid.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsprocessmetrics.uptime_in_millis.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsservermetrics.concurrent_connections.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsservermetrics.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsservermetrics.requests.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsservermetrics.response_times.md create mode 100644 src/core/server/metrics/collectors/index.ts create mode 100644 src/core/server/metrics/collectors/os.test.ts create mode 100644 src/core/server/metrics/collectors/os.ts create mode 100644 src/core/server/metrics/collectors/process.test.ts create mode 100644 src/core/server/metrics/collectors/process.ts create mode 100644 src/core/server/metrics/collectors/server.ts create mode 100644 src/core/server/metrics/collectors/types.ts create mode 100644 src/core/server/metrics/index.ts create mode 100644 src/core/server/metrics/integration_tests/server_collector.test.ts create mode 100644 src/core/server/metrics/metrics_service.mock.ts create mode 100644 src/core/server/metrics/metrics_service.test.mocks.ts create mode 100644 src/core/server/metrics/metrics_service.test.ts create mode 100644 src/core/server/metrics/metrics_service.ts create mode 100644 src/core/server/metrics/ops_config.ts create mode 100644 src/core/server/metrics/ops_metrics_collector.test.mocks.ts create mode 100644 src/core/server/metrics/ops_metrics_collector.test.ts create mode 100644 src/core/server/metrics/ops_metrics_collector.ts create mode 100644 src/core/server/metrics/types.ts diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 15a1fd0506256..c948c89920796 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -88,11 +88,16 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- --> | [Logger](./kibana-plugin-server.logger.md) | Logger exposes all the necessary methods to log any type of information and this is the interface used by the logging consumers including plugins. | | [LoggerFactory](./kibana-plugin-server.loggerfactory.md) | The single purpose of <code>LoggerFactory</code> interface is to define a way to retrieve a context-based logger instance. | | [LogMeta](./kibana-plugin-server.logmeta.md) | Contextual metadata | +| [MetricsServiceSetup](./kibana-plugin-server.metricsservicesetup.md) | APIs to retrieves metrics gathered and exposed by the core platform. | | [OnPostAuthToolkit](./kibana-plugin-server.onpostauthtoolkit.md) | A tool set defining an outcome of OnPostAuth interceptor for incoming request. | | [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | | [OnPreResponseExtensions](./kibana-plugin-server.onpreresponseextensions.md) | Additional data to extend a response. | | [OnPreResponseInfo](./kibana-plugin-server.onpreresponseinfo.md) | Response status code. | | [OnPreResponseToolkit](./kibana-plugin-server.onpreresponsetoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | +| [OpsMetrics](./kibana-plugin-server.opsmetrics.md) | Regroups metrics gathered by all the collectors. This contains metrics about the os/runtime, the kibana process and the http server. | +| [OpsOsMetrics](./kibana-plugin-server.opsosmetrics.md) | OS related metrics | +| [OpsProcessMetrics](./kibana-plugin-server.opsprocessmetrics.md) | Process related metrics | +| [OpsServerMetrics](./kibana-plugin-server.opsservermetrics.md) | server related metrics | | [PackageInfo](./kibana-plugin-server.packageinfo.md) | | | [Plugin](./kibana-plugin-server.plugin.md) | The interface that should be returned by a <code>PluginInitializer</code>. | | [PluginConfigDescriptor](./kibana-plugin-server.pluginconfigdescriptor.md) | Describes a plugin configuration properties. | diff --git a/docs/development/core/server/kibana-plugin-server.metricsservicesetup.getopsmetrics_.md b/docs/development/core/server/kibana-plugin-server.metricsservicesetup.getopsmetrics_.md new file mode 100644 index 0000000000000..454b8c905451e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.metricsservicesetup.getopsmetrics_.md @@ -0,0 +1,24 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [MetricsServiceSetup](./kibana-plugin-server.metricsservicesetup.md) > [getOpsMetrics$](./kibana-plugin-server.metricsservicesetup.getopsmetrics_.md) + +## MetricsServiceSetup.getOpsMetrics$ property + +Retrieve an observable emitting the [OpsMetrics](./kibana-plugin-server.opsmetrics.md) gathered. The observable will emit an initial value during core's `start` phase, and a new value every fixed interval of time, based on the `opts.interval` configuration property. + +<b>Signature:</b> + +```typescript +getOpsMetrics$: () => Observable<OpsMetrics>; +``` + +## Example + + +```ts +core.metrics.getOpsMetrics$().subscribe(metrics => { + // do something with the metrics +}) + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.metricsservicesetup.md b/docs/development/core/server/kibana-plugin-server.metricsservicesetup.md new file mode 100644 index 0000000000000..270c56402a390 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.metricsservicesetup.md @@ -0,0 +1,20 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [MetricsServiceSetup](./kibana-plugin-server.metricsservicesetup.md) + +## MetricsServiceSetup interface + +APIs to retrieves metrics gathered and exposed by the core platform. + +<b>Signature:</b> + +```typescript +export interface MetricsServiceSetup +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [getOpsMetrics$](./kibana-plugin-server.metricsservicesetup.getopsmetrics_.md) | <code>() => Observable<OpsMetrics></code> | Retrieve an observable emitting the [OpsMetrics](./kibana-plugin-server.opsmetrics.md) gathered. The observable will emit an initial value during core's <code>start</code> phase, and a new value every fixed interval of time, based on the <code>opts.interval</code> configuration property. | + diff --git a/docs/development/core/server/kibana-plugin-server.opsmetrics.concurrent_connections.md b/docs/development/core/server/kibana-plugin-server.opsmetrics.concurrent_connections.md new file mode 100644 index 0000000000000..cfd39a551ad34 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsmetrics.concurrent_connections.md @@ -0,0 +1,13 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsMetrics](./kibana-plugin-server.opsmetrics.md) > [concurrent\_connections](./kibana-plugin-server.opsmetrics.concurrent_connections.md) + +## OpsMetrics.concurrent\_connections property + +number of current concurrent connections to the server + +<b>Signature:</b> + +```typescript +concurrent_connections: OpsServerMetrics['concurrent_connections']; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsmetrics.md b/docs/development/core/server/kibana-plugin-server.opsmetrics.md new file mode 100644 index 0000000000000..e23bd8d431d3f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsmetrics.md @@ -0,0 +1,24 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsMetrics](./kibana-plugin-server.opsmetrics.md) + +## OpsMetrics interface + +Regroups metrics gathered by all the collectors. This contains metrics about the os/runtime, the kibana process and the http server. + +<b>Signature:</b> + +```typescript +export interface OpsMetrics +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [concurrent\_connections](./kibana-plugin-server.opsmetrics.concurrent_connections.md) | <code>OpsServerMetrics['concurrent_connections']</code> | number of current concurrent connections to the server | +| [os](./kibana-plugin-server.opsmetrics.os.md) | <code>OpsOsMetrics</code> | OS related metrics | +| [process](./kibana-plugin-server.opsmetrics.process.md) | <code>OpsProcessMetrics</code> | Process related metrics | +| [requests](./kibana-plugin-server.opsmetrics.requests.md) | <code>OpsServerMetrics['requests']</code> | server requests stats | +| [response\_times](./kibana-plugin-server.opsmetrics.response_times.md) | <code>OpsServerMetrics['response_times']</code> | server response time stats | + diff --git a/docs/development/core/server/kibana-plugin-server.opsmetrics.os.md b/docs/development/core/server/kibana-plugin-server.opsmetrics.os.md new file mode 100644 index 0000000000000..993a1d7a2d7b7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsmetrics.os.md @@ -0,0 +1,13 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsMetrics](./kibana-plugin-server.opsmetrics.md) > [os](./kibana-plugin-server.opsmetrics.os.md) + +## OpsMetrics.os property + +OS related metrics + +<b>Signature:</b> + +```typescript +os: OpsOsMetrics; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsmetrics.process.md b/docs/development/core/server/kibana-plugin-server.opsmetrics.process.md new file mode 100644 index 0000000000000..53d3a33d66e06 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsmetrics.process.md @@ -0,0 +1,13 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsMetrics](./kibana-plugin-server.opsmetrics.md) > [process](./kibana-plugin-server.opsmetrics.process.md) + +## OpsMetrics.process property + +Process related metrics + +<b>Signature:</b> + +```typescript +process: OpsProcessMetrics; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsmetrics.requests.md b/docs/development/core/server/kibana-plugin-server.opsmetrics.requests.md new file mode 100644 index 0000000000000..9cd6b85e507f0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsmetrics.requests.md @@ -0,0 +1,13 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsMetrics](./kibana-plugin-server.opsmetrics.md) > [requests](./kibana-plugin-server.opsmetrics.requests.md) + +## OpsMetrics.requests property + +server requests stats + +<b>Signature:</b> + +```typescript +requests: OpsServerMetrics['requests']; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsmetrics.response_times.md b/docs/development/core/server/kibana-plugin-server.opsmetrics.response_times.md new file mode 100644 index 0000000000000..358699071b1c3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsmetrics.response_times.md @@ -0,0 +1,13 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsMetrics](./kibana-plugin-server.opsmetrics.md) > [response\_times](./kibana-plugin-server.opsmetrics.response_times.md) + +## OpsMetrics.response\_times property + +server response time stats + +<b>Signature:</b> + +```typescript +response_times: OpsServerMetrics['response_times']; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsosmetrics.distro.md b/docs/development/core/server/kibana-plugin-server.opsosmetrics.distro.md new file mode 100644 index 0000000000000..338164f173d02 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsosmetrics.distro.md @@ -0,0 +1,13 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsOsMetrics](./kibana-plugin-server.opsosmetrics.md) > [distro](./kibana-plugin-server.opsosmetrics.distro.md) + +## OpsOsMetrics.distro property + +The os distrib. Only present for linux platforms + +<b>Signature:</b> + +```typescript +distro?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsosmetrics.distrorelease.md b/docs/development/core/server/kibana-plugin-server.opsosmetrics.distrorelease.md new file mode 100644 index 0000000000000..24c5a1f00b64c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsosmetrics.distrorelease.md @@ -0,0 +1,13 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsOsMetrics](./kibana-plugin-server.opsosmetrics.md) > [distroRelease](./kibana-plugin-server.opsosmetrics.distrorelease.md) + +## OpsOsMetrics.distroRelease property + +The os distrib release, prefixed by the os distrib. Only present for linux platforms + +<b>Signature:</b> + +```typescript +distroRelease?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsosmetrics.load.md b/docs/development/core/server/kibana-plugin-server.opsosmetrics.load.md new file mode 100644 index 0000000000000..0bf17502ce34e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsosmetrics.load.md @@ -0,0 +1,17 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsOsMetrics](./kibana-plugin-server.opsosmetrics.md) > [load](./kibana-plugin-server.opsosmetrics.load.md) + +## OpsOsMetrics.load property + +cpu load metrics + +<b>Signature:</b> + +```typescript +load: { + '1m': number; + '5m': number; + '15m': number; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsosmetrics.md b/docs/development/core/server/kibana-plugin-server.opsosmetrics.md new file mode 100644 index 0000000000000..0fb4e59fdf539 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsosmetrics.md @@ -0,0 +1,26 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsOsMetrics](./kibana-plugin-server.opsosmetrics.md) + +## OpsOsMetrics interface + +OS related metrics + +<b>Signature:</b> + +```typescript +export interface OpsOsMetrics +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [distro](./kibana-plugin-server.opsosmetrics.distro.md) | <code>string</code> | The os distrib. Only present for linux platforms | +| [distroRelease](./kibana-plugin-server.opsosmetrics.distrorelease.md) | <code>string</code> | The os distrib release, prefixed by the os distrib. Only present for linux platforms | +| [load](./kibana-plugin-server.opsosmetrics.load.md) | <code>{</code><br/><code> '1m': number;</code><br/><code> '5m': number;</code><br/><code> '15m': number;</code><br/><code> }</code> | cpu load metrics | +| [memory](./kibana-plugin-server.opsosmetrics.memory.md) | <code>{</code><br/><code> total_in_bytes: number;</code><br/><code> free_in_bytes: number;</code><br/><code> used_in_bytes: number;</code><br/><code> }</code> | system memory usage metrics | +| [platform](./kibana-plugin-server.opsosmetrics.platform.md) | <code>NodeJS.Platform</code> | The os platform | +| [platformRelease](./kibana-plugin-server.opsosmetrics.platformrelease.md) | <code>string</code> | The os platform release, prefixed by the platform name | +| [uptime\_in\_millis](./kibana-plugin-server.opsosmetrics.uptime_in_millis.md) | <code>number</code> | the OS uptime | + diff --git a/docs/development/core/server/kibana-plugin-server.opsosmetrics.memory.md b/docs/development/core/server/kibana-plugin-server.opsosmetrics.memory.md new file mode 100644 index 0000000000000..4a1becaeeaec7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsosmetrics.memory.md @@ -0,0 +1,17 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsOsMetrics](./kibana-plugin-server.opsosmetrics.md) > [memory](./kibana-plugin-server.opsosmetrics.memory.md) + +## OpsOsMetrics.memory property + +system memory usage metrics + +<b>Signature:</b> + +```typescript +memory: { + total_in_bytes: number; + free_in_bytes: number; + used_in_bytes: number; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsosmetrics.platform.md b/docs/development/core/server/kibana-plugin-server.opsosmetrics.platform.md new file mode 100644 index 0000000000000..411d0fc546dc0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsosmetrics.platform.md @@ -0,0 +1,13 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsOsMetrics](./kibana-plugin-server.opsosmetrics.md) > [platform](./kibana-plugin-server.opsosmetrics.platform.md) + +## OpsOsMetrics.platform property + +The os platform + +<b>Signature:</b> + +```typescript +platform: NodeJS.Platform; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsosmetrics.platformrelease.md b/docs/development/core/server/kibana-plugin-server.opsosmetrics.platformrelease.md new file mode 100644 index 0000000000000..1071b4a38f588 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsosmetrics.platformrelease.md @@ -0,0 +1,13 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsOsMetrics](./kibana-plugin-server.opsosmetrics.md) > [platformRelease](./kibana-plugin-server.opsosmetrics.platformrelease.md) + +## OpsOsMetrics.platformRelease property + +The os platform release, prefixed by the platform name + +<b>Signature:</b> + +```typescript +platformRelease: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsosmetrics.uptime_in_millis.md b/docs/development/core/server/kibana-plugin-server.opsosmetrics.uptime_in_millis.md new file mode 100644 index 0000000000000..dfff1a1f1da0b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsosmetrics.uptime_in_millis.md @@ -0,0 +1,13 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsOsMetrics](./kibana-plugin-server.opsosmetrics.md) > [uptime\_in\_millis](./kibana-plugin-server.opsosmetrics.uptime_in_millis.md) + +## OpsOsMetrics.uptime\_in\_millis property + +the OS uptime + +<b>Signature:</b> + +```typescript +uptime_in_millis: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.event_loop_delay.md b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.event_loop_delay.md new file mode 100644 index 0000000000000..f61c8b0995324 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.event_loop_delay.md @@ -0,0 +1,13 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsProcessMetrics](./kibana-plugin-server.opsprocessmetrics.md) > [event\_loop\_delay](./kibana-plugin-server.opsprocessmetrics.event_loop_delay.md) + +## OpsProcessMetrics.event\_loop\_delay property + +node event loop delay + +<b>Signature:</b> + +```typescript +event_loop_delay: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.md b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.md new file mode 100644 index 0000000000000..92fd8471cce7d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.md @@ -0,0 +1,23 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsProcessMetrics](./kibana-plugin-server.opsprocessmetrics.md) + +## OpsProcessMetrics interface + +Process related metrics + +<b>Signature:</b> + +```typescript +export interface OpsProcessMetrics +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [event\_loop\_delay](./kibana-plugin-server.opsprocessmetrics.event_loop_delay.md) | <code>number</code> | node event loop delay | +| [memory](./kibana-plugin-server.opsprocessmetrics.memory.md) | <code>{</code><br/><code> heap: {</code><br/><code> total_in_bytes: number;</code><br/><code> used_in_bytes: number;</code><br/><code> size_limit: number;</code><br/><code> };</code><br/><code> resident_set_size_in_bytes: number;</code><br/><code> }</code> | process memory usage | +| [pid](./kibana-plugin-server.opsprocessmetrics.pid.md) | <code>number</code> | pid of the kibana process | +| [uptime\_in\_millis](./kibana-plugin-server.opsprocessmetrics.uptime_in_millis.md) | <code>number</code> | uptime of the kibana process | + diff --git a/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.memory.md b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.memory.md new file mode 100644 index 0000000000000..5c1a8de70dc01 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.memory.md @@ -0,0 +1,20 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsProcessMetrics](./kibana-plugin-server.opsprocessmetrics.md) > [memory](./kibana-plugin-server.opsprocessmetrics.memory.md) + +## OpsProcessMetrics.memory property + +process memory usage + +<b>Signature:</b> + +```typescript +memory: { + heap: { + total_in_bytes: number; + used_in_bytes: number; + size_limit: number; + }; + resident_set_size_in_bytes: number; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.pid.md b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.pid.md new file mode 100644 index 0000000000000..a34187f372018 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.pid.md @@ -0,0 +1,13 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsProcessMetrics](./kibana-plugin-server.opsprocessmetrics.md) > [pid](./kibana-plugin-server.opsprocessmetrics.pid.md) + +## OpsProcessMetrics.pid property + +pid of the kibana process + +<b>Signature:</b> + +```typescript +pid: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.uptime_in_millis.md b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.uptime_in_millis.md new file mode 100644 index 0000000000000..24db2f017a663 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.uptime_in_millis.md @@ -0,0 +1,13 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsProcessMetrics](./kibana-plugin-server.opsprocessmetrics.md) > [uptime\_in\_millis](./kibana-plugin-server.opsprocessmetrics.uptime_in_millis.md) + +## OpsProcessMetrics.uptime\_in\_millis property + +uptime of the kibana process + +<b>Signature:</b> + +```typescript +uptime_in_millis: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsservermetrics.concurrent_connections.md b/docs/development/core/server/kibana-plugin-server.opsservermetrics.concurrent_connections.md new file mode 100644 index 0000000000000..ade79fedfa1b5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsservermetrics.concurrent_connections.md @@ -0,0 +1,13 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsServerMetrics](./kibana-plugin-server.opsservermetrics.md) > [concurrent\_connections](./kibana-plugin-server.opsservermetrics.concurrent_connections.md) + +## OpsServerMetrics.concurrent\_connections property + +number of current concurrent connections to the server + +<b>Signature:</b> + +```typescript +concurrent_connections: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsservermetrics.md b/docs/development/core/server/kibana-plugin-server.opsservermetrics.md new file mode 100644 index 0000000000000..4e35c02bd9f28 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsservermetrics.md @@ -0,0 +1,22 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsServerMetrics](./kibana-plugin-server.opsservermetrics.md) + +## OpsServerMetrics interface + +server related metrics + +<b>Signature:</b> + +```typescript +export interface OpsServerMetrics +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [concurrent\_connections](./kibana-plugin-server.opsservermetrics.concurrent_connections.md) | <code>number</code> | number of current concurrent connections to the server | +| [requests](./kibana-plugin-server.opsservermetrics.requests.md) | <code>{</code><br/><code> disconnects: number;</code><br/><code> total: number;</code><br/><code> statusCodes: Record<number, number>;</code><br/><code> }</code> | server requests stats | +| [response\_times](./kibana-plugin-server.opsservermetrics.response_times.md) | <code>{</code><br/><code> avg_in_millis: number;</code><br/><code> max_in_millis: number;</code><br/><code> }</code> | server response time stats | + diff --git a/docs/development/core/server/kibana-plugin-server.opsservermetrics.requests.md b/docs/development/core/server/kibana-plugin-server.opsservermetrics.requests.md new file mode 100644 index 0000000000000..5ad2abc869557 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsservermetrics.requests.md @@ -0,0 +1,17 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsServerMetrics](./kibana-plugin-server.opsservermetrics.md) > [requests](./kibana-plugin-server.opsservermetrics.requests.md) + +## OpsServerMetrics.requests property + +server requests stats + +<b>Signature:</b> + +```typescript +requests: { + disconnects: number; + total: number; + statusCodes: Record<number, number>; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsservermetrics.response_times.md b/docs/development/core/server/kibana-plugin-server.opsservermetrics.response_times.md new file mode 100644 index 0000000000000..5008efc6ad4da --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsservermetrics.response_times.md @@ -0,0 +1,16 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsServerMetrics](./kibana-plugin-server.opsservermetrics.md) > [response\_times](./kibana-plugin-server.opsservermetrics.response_times.md) + +## OpsServerMetrics.response\_times property + +server response time stats + +<b>Signature:</b> + +```typescript +response_times: { + avg_in_millis: number; + max_in_millis: number; + }; +``` diff --git a/package.json b/package.json index e727d87a83c53..2c401724c72cd 100644 --- a/package.json +++ b/package.json @@ -323,6 +323,7 @@ "@types/fetch-mock": "^7.3.1", "@types/flot": "^0.0.31", "@types/getopts": "^2.0.1", + "@types/getos": "^3.0.0", "@types/glob": "^7.1.1", "@types/globby": "^8.0.0", "@types/graphql": "^0.13.2", diff --git a/src/core/server/index.ts b/src/core/server/index.ts index e45d4f28edcc3..de6cdb2d7acd7 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -245,6 +245,14 @@ export { StringValidationRegexString, } from './ui_settings'; +export { + OpsMetrics, + OpsOsMetrics, + OpsServerMetrics, + OpsProcessMetrics, + MetricsServiceSetup, +} from './metrics'; + export { RecursiveReadonly } from '../utils'; export { diff --git a/src/core/server/internal_types.ts b/src/core/server/internal_types.ts index ff68d1544d119..37d1061dc618d 100644 --- a/src/core/server/internal_types.ts +++ b/src/core/server/internal_types.ts @@ -30,6 +30,7 @@ import { } from './saved_objects'; import { InternalUiSettingsServiceSetup, InternalUiSettingsServiceStart } from './ui_settings'; import { UuidServiceSetup } from './uuid'; +import { InternalMetricsServiceSetup } from './metrics'; /** @internal */ export interface InternalCoreSetup { @@ -40,6 +41,7 @@ export interface InternalCoreSetup { uiSettings: InternalUiSettingsServiceSetup; savedObjects: InternalSavedObjectsServiceSetup; uuid: UuidServiceSetup; + metrics: InternalMetricsServiceSetup; } /** diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index 46436461505c0..50468db8a504d 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -43,6 +43,7 @@ import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service. import { capabilitiesServiceMock } from '../capabilities/capabilities_service.mock'; import { setupMock as renderingServiceMock } from '../rendering/__mocks__/rendering_service'; import { uuidServiceMock } from '../uuid/uuid_service.mock'; +import { metricsServiceMock } from '../metrics/metrics_service.mock'; import { findLegacyPluginSpecs } from './plugins'; import { LegacyVars, LegacyServiceSetupDeps, LegacyServiceStartDeps } from './types'; import { LegacyService } from './legacy_service'; @@ -93,6 +94,7 @@ beforeEach(() => { }, }, rendering: renderingServiceMock, + metrics: metricsServiceMock.createInternalSetupContract(), uuid: uuidSetup, }, plugins: { 'plugin-id': 'plugin-value' }, diff --git a/src/core/server/metrics/collectors/index.ts b/src/core/server/metrics/collectors/index.ts new file mode 100644 index 0000000000000..f58ab02e63881 --- /dev/null +++ b/src/core/server/metrics/collectors/index.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { OpsProcessMetrics, OpsOsMetrics, OpsServerMetrics, MetricsCollector } from './types'; +export { OsMetricsCollector } from './os'; +export { ProcessMetricsCollector } from './process'; +export { ServerMetricsCollector } from './server'; diff --git a/src/core/server/metrics/collectors/os.test.ts b/src/core/server/metrics/collectors/os.test.ts new file mode 100644 index 0000000000000..7d5a6da90b7d6 --- /dev/null +++ b/src/core/server/metrics/collectors/os.test.ts @@ -0,0 +1,99 @@ +/* + * 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. + */ + +jest.mock('getos', () => (cb: Function) => cb(null, { dist: 'distrib', release: 'release' })); + +import os from 'os'; +import { OsMetricsCollector } from './os'; + +describe('OsMetricsCollector', () => { + let collector: OsMetricsCollector; + + beforeEach(() => { + collector = new OsMetricsCollector(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('collects platform info from the os package', async () => { + const platform = 'darwin'; + const release = '10.14.1'; + + jest.spyOn(os, 'platform').mockImplementation(() => platform); + jest.spyOn(os, 'release').mockImplementation(() => release); + + const metrics = await collector.collect(); + + expect(metrics.platform).toBe(platform); + expect(metrics.platformRelease).toBe(`${platform}-${release}`); + }); + + it('collects distribution info when platform is linux', async () => { + const platform = 'linux'; + + jest.spyOn(os, 'platform').mockImplementation(() => platform); + + const metrics = await collector.collect(); + + expect(metrics.distro).toBe('distrib'); + expect(metrics.distroRelease).toBe('distrib-release'); + }); + + it('collects memory info from the os package', async () => { + const totalMemory = 1457886; + const freeMemory = 456786; + + jest.spyOn(os, 'totalmem').mockImplementation(() => totalMemory); + jest.spyOn(os, 'freemem').mockImplementation(() => freeMemory); + + const metrics = await collector.collect(); + + expect(metrics.memory.total_in_bytes).toBe(totalMemory); + expect(metrics.memory.free_in_bytes).toBe(freeMemory); + expect(metrics.memory.used_in_bytes).toBe(totalMemory - freeMemory); + }); + + it('collects uptime info from the os package', async () => { + const uptime = 325; + + jest.spyOn(os, 'uptime').mockImplementation(() => uptime); + + const metrics = await collector.collect(); + + expect(metrics.uptime_in_millis).toBe(uptime * 1000); + }); + + it('collects load info from the os package', async () => { + const oneMinLoad = 1; + const fiveMinLoad = 2; + const fifteenMinLoad = 3; + + jest.spyOn(os, 'loadavg').mockImplementation(() => [oneMinLoad, fiveMinLoad, fifteenMinLoad]); + + const metrics = await collector.collect(); + + expect(metrics.load).toEqual({ + '1m': oneMinLoad, + '5m': fiveMinLoad, + '15m': fifteenMinLoad, + }); + }); +}); diff --git a/src/core/server/metrics/collectors/os.ts b/src/core/server/metrics/collectors/os.ts new file mode 100644 index 0000000000000..d3d9bb0be86fa --- /dev/null +++ b/src/core/server/metrics/collectors/os.ts @@ -0,0 +1,60 @@ +/* + * 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 os from 'os'; +import getosAsync, { LinuxOs } from 'getos'; +import { promisify } from 'util'; +import { OpsOsMetrics, MetricsCollector } from './types'; + +const getos = promisify(getosAsync); + +export class OsMetricsCollector implements MetricsCollector<OpsOsMetrics> { + public async collect(): Promise<OpsOsMetrics> { + const platform = os.platform(); + const load = os.loadavg(); + + const metrics: OpsOsMetrics = { + platform, + platformRelease: `${platform}-${os.release()}`, + load: { + '1m': load[0], + '5m': load[1], + '15m': load[2], + }, + memory: { + total_in_bytes: os.totalmem(), + free_in_bytes: os.freemem(), + used_in_bytes: os.totalmem() - os.freemem(), + }, + uptime_in_millis: os.uptime() * 1000, + }; + + if (platform === 'linux') { + try { + const distro = (await getos()) as LinuxOs; + metrics.distro = distro.dist; + metrics.distroRelease = `${distro.dist}-${distro.release}`; + } catch (e) { + // ignore errors + } + } + + return metrics; + } +} diff --git a/src/core/server/metrics/collectors/process.test.ts b/src/core/server/metrics/collectors/process.test.ts new file mode 100644 index 0000000000000..a437d799371f1 --- /dev/null +++ b/src/core/server/metrics/collectors/process.test.ts @@ -0,0 +1,81 @@ +/* + * 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 v8, { HeapInfo } from 'v8'; +import { ProcessMetricsCollector } from './process'; + +describe('ProcessMetricsCollector', () => { + let collector: ProcessMetricsCollector; + + beforeEach(() => { + collector = new ProcessMetricsCollector(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('collects pid from the process', async () => { + const metrics = await collector.collect(); + + expect(metrics.pid).toEqual(process.pid); + }); + + it('collects event loop delay', async () => { + const metrics = await collector.collect(); + + expect(metrics.event_loop_delay).toBeGreaterThan(0); + }); + + it('collects uptime info from the process', async () => { + const uptime = 58986; + jest.spyOn(process, 'uptime').mockImplementation(() => uptime); + + const metrics = await collector.collect(); + + expect(metrics.uptime_in_millis).toEqual(uptime * 1000); + }); + + it('collects memory info from the process', async () => { + const heapTotal = 58986; + const heapUsed = 4688; + const heapSizeLimit = 5788; + const rss = 5865; + jest.spyOn(process, 'memoryUsage').mockImplementation(() => ({ + rss, + heapTotal, + heapUsed, + external: 0, + })); + + jest.spyOn(v8, 'getHeapStatistics').mockImplementation( + () => + ({ + heap_size_limit: heapSizeLimit, + } as HeapInfo) + ); + + const metrics = await collector.collect(); + + expect(metrics.memory.heap.total_in_bytes).toEqual(heapTotal); + expect(metrics.memory.heap.used_in_bytes).toEqual(heapUsed); + expect(metrics.memory.heap.size_limit).toEqual(heapSizeLimit); + expect(metrics.memory.resident_set_size_in_bytes).toEqual(rss); + }); +}); diff --git a/src/core/server/metrics/collectors/process.ts b/src/core/server/metrics/collectors/process.ts new file mode 100644 index 0000000000000..aa68abaf74e41 --- /dev/null +++ b/src/core/server/metrics/collectors/process.ts @@ -0,0 +1,52 @@ +/* + * 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 v8 from 'v8'; +import { Bench } from 'hoek'; +import { OpsProcessMetrics, MetricsCollector } from './types'; + +export class ProcessMetricsCollector implements MetricsCollector<OpsProcessMetrics> { + public async collect(): Promise<OpsProcessMetrics> { + const heapStats = v8.getHeapStatistics(); + const memoryUsage = process.memoryUsage(); + const [eventLoopDelay] = await Promise.all([getEventLoopDelay()]); + return { + memory: { + heap: { + total_in_bytes: memoryUsage.heapTotal, + used_in_bytes: memoryUsage.heapUsed, + size_limit: heapStats.heap_size_limit, + }, + resident_set_size_in_bytes: memoryUsage.rss, + }, + pid: process.pid, + event_loop_delay: eventLoopDelay, + uptime_in_millis: process.uptime() * 1000, + }; + } +} + +const getEventLoopDelay = (): Promise<number> => { + const bench = new Bench(); + return new Promise(resolve => { + setImmediate(() => { + return resolve(bench.elapsed()); + }); + }); +}; diff --git a/src/core/server/metrics/collectors/server.ts b/src/core/server/metrics/collectors/server.ts new file mode 100644 index 0000000000000..e46ac2f653df6 --- /dev/null +++ b/src/core/server/metrics/collectors/server.ts @@ -0,0 +1,80 @@ +/* + * 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 { ResponseObject, Server as HapiServer } from 'hapi'; +import { OpsServerMetrics, MetricsCollector } from './types'; + +interface ServerResponseTime { + count: number; + total: number; + max: number; +} + +export class ServerMetricsCollector implements MetricsCollector<OpsServerMetrics> { + private readonly requests: OpsServerMetrics['requests'] = { + disconnects: 0, + total: 0, + statusCodes: {}, + }; + private readonly responseTimes: ServerResponseTime = { + count: 0, + total: 0, + max: 0, + }; + + constructor(private readonly server: HapiServer) { + this.server.ext('onRequest', (request, h) => { + this.requests.total++; + request.events.once('disconnect', () => { + this.requests.disconnects++; + }); + return h.continue; + }); + this.server.events.on('response', request => { + const statusCode = (request.response as ResponseObject)?.statusCode; + if (statusCode) { + if (!this.requests.statusCodes[statusCode]) { + this.requests.statusCodes[statusCode] = 0; + } + this.requests.statusCodes[statusCode]++; + } + + const duration = Date.now() - request.info.received; + this.responseTimes.count++; + this.responseTimes.total += duration; + this.responseTimes.max = Math.max(this.responseTimes.max, duration); + }); + } + + public async collect(): Promise<OpsServerMetrics> { + const connections = await new Promise<number>(resolve => { + this.server.listener.getConnections((_, count) => { + resolve(count); + }); + }); + + return { + requests: this.requests, + response_times: { + avg_in_millis: this.responseTimes.total / Math.max(this.responseTimes.count, 1), + max_in_millis: this.responseTimes.max, + }, + concurrent_connections: connections, + }; + } +} diff --git a/src/core/server/metrics/collectors/types.ts b/src/core/server/metrics/collectors/types.ts new file mode 100644 index 0000000000000..5a83bc70af3c1 --- /dev/null +++ b/src/core/server/metrics/collectors/types.ts @@ -0,0 +1,110 @@ +/* + * 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. + */ + +/** Base interface for all metrics gatherers */ +export interface MetricsCollector<T> { + collect(): Promise<T>; +} + +/** + * Process related metrics + * @public + */ +export interface OpsProcessMetrics { + /** process memory usage */ + memory: { + /** heap memory usage */ + heap: { + /** total heap available */ + total_in_bytes: number; + /** used heap */ + used_in_bytes: number; + /** v8 heap size limit */ + size_limit: number; + }; + /** node rss */ + resident_set_size_in_bytes: number; + }; + /** node event loop delay */ + event_loop_delay: number; + /** pid of the kibana process */ + pid: number; + /** uptime of the kibana process */ + uptime_in_millis: number; +} + +/** + * OS related metrics + * @public + */ +export interface OpsOsMetrics { + /** The os platform */ + platform: NodeJS.Platform; + /** The os platform release, prefixed by the platform name */ + platformRelease: string; + /** The os distrib. Only present for linux platforms */ + distro?: string; + /** The os distrib release, prefixed by the os distrib. Only present for linux platforms */ + distroRelease?: string; + /** cpu load metrics */ + load: { + /** load for last minute */ + '1m': number; + /** load for last 5 minutes */ + '5m': number; + /** load for last 15 minutes */ + '15m': number; + }; + /** system memory usage metrics */ + memory: { + /** total memory available */ + total_in_bytes: number; + /** current free memory */ + free_in_bytes: number; + /** current used memory */ + used_in_bytes: number; + }; + /** the OS uptime */ + uptime_in_millis: number; +} + +/** + * server related metrics + * @public + */ +export interface OpsServerMetrics { + /** server response time stats */ + response_times: { + /** average response time */ + avg_in_millis: number; + /** maximum response time */ + max_in_millis: number; + }; + /** server requests stats */ + requests: { + /** number of disconnected requests since startup */ + disconnects: number; + /** total number of requests handled since startup */ + total: number; + /** number of request handled per response status code */ + statusCodes: Record<number, number>; + }; + /** number of current concurrent connections to the server */ + concurrent_connections: number; +} diff --git a/src/core/server/metrics/index.ts b/src/core/server/metrics/index.ts new file mode 100644 index 0000000000000..fdcf637c0cd7b --- /dev/null +++ b/src/core/server/metrics/index.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { + InternalMetricsServiceStart, + InternalMetricsServiceSetup, + MetricsServiceSetup, + MetricsServiceStart, + OpsMetrics, +} from './types'; +export { OpsProcessMetrics, OpsServerMetrics, OpsOsMetrics } from './collectors'; +export { MetricsService } from './metrics_service'; +export { opsConfig } from './ops_config'; diff --git a/src/core/server/metrics/integration_tests/server_collector.test.ts b/src/core/server/metrics/integration_tests/server_collector.test.ts new file mode 100644 index 0000000000000..a387de80212d9 --- /dev/null +++ b/src/core/server/metrics/integration_tests/server_collector.test.ts @@ -0,0 +1,183 @@ +/* + * 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 { Subject } from 'rxjs'; +import { take } from 'rxjs/operators'; +import supertest from 'supertest'; +import { Server as HapiServer } from 'hapi'; +import { createHttpServer } from '../../http/test_utils'; +import { HttpService, IRouter } from '../../http'; +import { contextServiceMock } from '../../context/context_service.mock'; +import { ServerMetricsCollector } from '../collectors/server'; + +describe('ServerMetricsCollector', () => { + let server: HttpService; + let collector: ServerMetricsCollector; + let hapiServer: HapiServer; + let router: IRouter; + + const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + const sendGet = (path: string) => supertest(hapiServer.listener).get(path); + + beforeEach(async () => { + server = createHttpServer(); + const contextSetup = contextServiceMock.createSetupContract(); + const httpSetup = await server.setup({ context: contextSetup }); + hapiServer = httpSetup.server; + router = httpSetup.createRouter('/'); + collector = new ServerMetricsCollector(hapiServer); + }); + + afterEach(async () => { + await server.stop(); + }); + + it('collect requests infos', async () => { + router.get({ path: '/', validate: false }, async (ctx, req, res) => { + return res.ok({ body: '' }); + }); + await server.start(); + + let metrics = await collector.collect(); + + expect(metrics.requests).toEqual({ + total: 0, + disconnects: 0, + statusCodes: {}, + }); + + await sendGet('/'); + await sendGet('/'); + await sendGet('/not-found'); + + metrics = await collector.collect(); + + expect(metrics.requests).toEqual({ + total: 3, + disconnects: 0, + statusCodes: { + '200': 2, + '404': 1, + }, + }); + }); + + it('collect disconnects requests infos', async () => { + const never = new Promise(resolve => undefined); + + router.get({ path: '/', validate: false }, async (ctx, req, res) => { + return res.ok({ body: '' }); + }); + router.get({ path: '/disconnect', validate: false }, async (ctx, req, res) => { + await never; + return res.ok({ body: '' }); + }); + await server.start(); + + await sendGet('/'); + const discoReq1 = sendGet('/disconnect').end(); + const discoReq2 = sendGet('/disconnect').end(); + await delay(20); + + let metrics = await collector.collect(); + expect(metrics.requests).toEqual( + expect.objectContaining({ + total: 3, + disconnects: 0, + }) + ); + + discoReq1.abort(); + await delay(20); + + metrics = await collector.collect(); + expect(metrics.requests).toEqual( + expect.objectContaining({ + total: 3, + disconnects: 1, + }) + ); + + discoReq2.abort(); + await delay(20); + + metrics = await collector.collect(); + expect(metrics.requests).toEqual( + expect.objectContaining({ + total: 3, + disconnects: 2, + }) + ); + }); + + it('collect response times', async () => { + router.get({ path: '/no-delay', validate: false }, async (ctx, req, res) => { + return res.ok({ body: '' }); + }); + router.get({ path: '/500-ms', validate: false }, async (ctx, req, res) => { + await delay(500); + return res.ok({ body: '' }); + }); + router.get({ path: '/250-ms', validate: false }, async (ctx, req, res) => { + await delay(250); + return res.ok({ body: '' }); + }); + await server.start(); + + await Promise.all([sendGet('/no-delay'), sendGet('/250-ms')]); + let metrics = await collector.collect(); + + expect(metrics.response_times.avg_in_millis).toBeGreaterThanOrEqual(125); + expect(metrics.response_times.max_in_millis).toBeGreaterThanOrEqual(250); + + await Promise.all([sendGet('/500-ms'), sendGet('/500-ms')]); + metrics = await collector.collect(); + + expect(metrics.response_times.avg_in_millis).toBeGreaterThanOrEqual(250); + expect(metrics.response_times.max_in_millis).toBeGreaterThanOrEqual(500); + }); + + it('collect connection count', async () => { + const waitSubject = new Subject(); + + router.get({ path: '/', validate: false }, async (ctx, req, res) => { + await waitSubject.pipe(take(1)).toPromise(); + return res.ok({ body: '' }); + }); + await server.start(); + + let metrics = await collector.collect(); + expect(metrics.concurrent_connections).toEqual(0); + + sendGet('/').end(() => null); + await delay(20); + metrics = await collector.collect(); + expect(metrics.concurrent_connections).toEqual(1); + + sendGet('/').end(() => null); + await delay(20); + metrics = await collector.collect(); + expect(metrics.concurrent_connections).toEqual(2); + + waitSubject.next('go'); + await delay(20); + metrics = await collector.collect(); + expect(metrics.concurrent_connections).toEqual(0); + }); +}); diff --git a/src/core/server/metrics/metrics_service.mock.ts b/src/core/server/metrics/metrics_service.mock.ts new file mode 100644 index 0000000000000..cc53a4e27d571 --- /dev/null +++ b/src/core/server/metrics/metrics_service.mock.ts @@ -0,0 +1,67 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MetricsService } from './metrics_service'; +import { + InternalMetricsServiceSetup, + InternalMetricsServiceStart, + MetricsServiceSetup, + MetricsServiceStart, +} from './types'; + +const createSetupContractMock = () => { + const setupContract: jest.Mocked<MetricsServiceSetup> = { + getOpsMetrics$: jest.fn(), + }; + return setupContract; +}; + +const createInternalSetupContractMock = () => { + const setupContract: jest.Mocked<InternalMetricsServiceSetup> = createSetupContractMock(); + return setupContract; +}; + +const createStartContractMock = () => { + const startContract: jest.Mocked<MetricsServiceStart> = {}; + return startContract; +}; + +const createInternalStartContractMock = () => { + const startContract: jest.Mocked<InternalMetricsServiceStart> = createStartContractMock(); + return startContract; +}; + +type MetricsServiceContract = PublicMethodsOf<MetricsService>; + +const createMock = () => { + const mocked: jest.Mocked<MetricsServiceContract> = { + setup: jest.fn().mockReturnValue(createInternalSetupContractMock()), + start: jest.fn().mockReturnValue(createInternalStartContractMock()), + stop: jest.fn(), + }; + return mocked; +}; + +export const metricsServiceMock = { + create: createMock, + createSetupContract: createSetupContractMock, + createStartContract: createStartContractMock, + createInternalSetupContract: createInternalSetupContractMock, + createInternalStartContract: createInternalStartContractMock, +}; diff --git a/src/core/server/metrics/metrics_service.test.mocks.ts b/src/core/server/metrics/metrics_service.test.mocks.ts new file mode 100644 index 0000000000000..8e91775283042 --- /dev/null +++ b/src/core/server/metrics/metrics_service.test.mocks.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const mockOpsCollector = { + collect: jest.fn(), +}; +jest.doMock('./ops_metrics_collector', () => ({ + OpsMetricsCollector: jest.fn().mockImplementation(() => mockOpsCollector), +})); diff --git a/src/core/server/metrics/metrics_service.test.ts b/src/core/server/metrics/metrics_service.test.ts new file mode 100644 index 0000000000000..10d6761adbe7d --- /dev/null +++ b/src/core/server/metrics/metrics_service.test.ts @@ -0,0 +1,134 @@ +/* + * 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 moment from 'moment'; +import { mockOpsCollector } from './metrics_service.test.mocks'; +import { MetricsService } from './metrics_service'; +import { mockCoreContext } from '../core_context.mock'; +import { configServiceMock } from '../config/config_service.mock'; +import { httpServiceMock } from '../http/http_service.mock'; +import { take } from 'rxjs/operators'; + +const testInterval = 100; + +const dummyMetrics = { metricA: 'value', metricB: 'otherValue' }; + +describe('MetricsService', () => { + const httpMock = httpServiceMock.createSetupContract(); + let metricsService: MetricsService; + + beforeEach(() => { + jest.useFakeTimers(); + + const configService = configServiceMock.create({ + atPath: { interval: moment.duration(testInterval) }, + }); + const coreContext = mockCoreContext.create({ configService }); + metricsService = new MetricsService(coreContext); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.clearAllTimers(); + }); + + describe('#start', () => { + it('invokes setInterval with the configured interval', async () => { + await metricsService.setup({ http: httpMock }); + await metricsService.start(); + + expect(setInterval).toHaveBeenCalledTimes(1); + expect(setInterval).toHaveBeenCalledWith(expect.any(Function), testInterval); + }); + + it('emits the metrics at start', async () => { + mockOpsCollector.collect.mockResolvedValue(dummyMetrics); + + const { getOpsMetrics$ } = await metricsService.setup({ + http: httpMock, + }); + + await metricsService.start(); + + expect(mockOpsCollector.collect).toHaveBeenCalledTimes(1); + expect( + await getOpsMetrics$() + .pipe(take(1)) + .toPromise() + ).toEqual(dummyMetrics); + }); + + it('collects the metrics at every interval', async () => { + mockOpsCollector.collect.mockResolvedValue(dummyMetrics); + + await metricsService.setup({ http: httpMock }); + + await metricsService.start(); + + expect(mockOpsCollector.collect).toHaveBeenCalledTimes(1); + + jest.advanceTimersByTime(testInterval); + expect(mockOpsCollector.collect).toHaveBeenCalledTimes(2); + + jest.advanceTimersByTime(testInterval); + expect(mockOpsCollector.collect).toHaveBeenCalledTimes(3); + }); + + it('throws when called before setup', async () => { + await expect(metricsService.start()).rejects.toThrowErrorMatchingInlineSnapshot( + `"#setup() needs to be run first"` + ); + }); + }); + + describe('#stop', () => { + it('stops the metrics interval', async () => { + const { getOpsMetrics$ } = await metricsService.setup({ http: httpMock }); + await metricsService.start(); + + expect(mockOpsCollector.collect).toHaveBeenCalledTimes(1); + + jest.advanceTimersByTime(testInterval); + expect(mockOpsCollector.collect).toHaveBeenCalledTimes(2); + + await metricsService.stop(); + jest.advanceTimersByTime(10 * testInterval); + expect(mockOpsCollector.collect).toHaveBeenCalledTimes(2); + + getOpsMetrics$().subscribe({ complete: () => {} }); + }); + + it('completes the metrics observable', async () => { + const { getOpsMetrics$ } = await metricsService.setup({ http: httpMock }); + await metricsService.start(); + + let completed = false; + + getOpsMetrics$().subscribe({ + complete: () => { + completed = true; + }, + }); + + await metricsService.stop(); + + expect(completed).toEqual(true); + }); + }); +}); diff --git a/src/core/server/metrics/metrics_service.ts b/src/core/server/metrics/metrics_service.ts new file mode 100644 index 0000000000000..1aed89a4aad60 --- /dev/null +++ b/src/core/server/metrics/metrics_service.ts @@ -0,0 +1,86 @@ +/* + * 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 { ReplaySubject } from 'rxjs'; +import { first, shareReplay } from 'rxjs/operators'; +import { CoreService } from '../../types'; +import { CoreContext } from '../core_context'; +import { Logger } from '../logging'; +import { InternalHttpServiceSetup } from '../http'; +import { InternalMetricsServiceSetup, InternalMetricsServiceStart, OpsMetrics } from './types'; +import { OpsMetricsCollector } from './ops_metrics_collector'; +import { opsConfig, OpsConfigType } from './ops_config'; + +interface MetricsServiceSetupDeps { + http: InternalHttpServiceSetup; +} + +/** @internal */ +export class MetricsService + implements CoreService<InternalMetricsServiceSetup, InternalMetricsServiceStart> { + private readonly logger: Logger; + private metricsCollector?: OpsMetricsCollector; + private collectInterval?: NodeJS.Timeout; + private metrics$ = new ReplaySubject<OpsMetrics>(1); + + constructor(private readonly coreContext: CoreContext) { + this.logger = coreContext.logger.get('metrics'); + } + + public async setup({ http }: MetricsServiceSetupDeps): Promise<InternalMetricsServiceSetup> { + this.metricsCollector = new OpsMetricsCollector(http.server); + + const metricsObservable = this.metrics$.pipe(shareReplay(1)); + + return { + getOpsMetrics$: () => metricsObservable, + }; + } + + public async start(): Promise<InternalMetricsServiceStart> { + if (!this.metricsCollector) { + throw new Error('#setup() needs to be run first'); + } + const config = await this.coreContext.configService + .atPath<OpsConfigType>(opsConfig.path) + .pipe(first()) + .toPromise(); + + await this.refreshMetrics(); + + this.collectInterval = setInterval(() => { + this.refreshMetrics(); + }, config.interval.asMilliseconds()); + + return {}; + } + + private async refreshMetrics() { + this.logger.debug('Refreshing metrics'); + const metrics = await this.metricsCollector!.collect(); + this.metrics$.next(metrics); + } + + public async stop() { + if (this.collectInterval) { + clearInterval(this.collectInterval); + } + this.metrics$.complete(); + } +} diff --git a/src/core/server/metrics/ops_config.ts b/src/core/server/metrics/ops_config.ts new file mode 100644 index 0000000000000..bd6ae5cc5474d --- /dev/null +++ b/src/core/server/metrics/ops_config.ts @@ -0,0 +1,29 @@ +/* + * 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 { schema, TypeOf } from '@kbn/config-schema'; + +export const opsConfig = { + path: 'ops', + schema: schema.object({ + interval: schema.duration({ defaultValue: '5s' }), + }), +}; + +export type OpsConfigType = TypeOf<typeof opsConfig.schema>; diff --git a/src/core/server/metrics/ops_metrics_collector.test.mocks.ts b/src/core/server/metrics/ops_metrics_collector.test.mocks.ts new file mode 100644 index 0000000000000..8265796d57970 --- /dev/null +++ b/src/core/server/metrics/ops_metrics_collector.test.mocks.ts @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const mockOsCollector = { + collect: jest.fn(), +}; +jest.doMock('./collectors/os', () => ({ + OsMetricsCollector: jest.fn().mockImplementation(() => mockOsCollector), +})); + +export const mockProcessCollector = { + collect: jest.fn(), +}; +jest.doMock('./collectors/process', () => ({ + ProcessMetricsCollector: jest.fn().mockImplementation(() => mockProcessCollector), +})); + +export const mockServerCollector = { + collect: jest.fn(), +}; +jest.doMock('./collectors/server', () => ({ + ServerMetricsCollector: jest.fn().mockImplementation(() => mockServerCollector), +})); diff --git a/src/core/server/metrics/ops_metrics_collector.test.ts b/src/core/server/metrics/ops_metrics_collector.test.ts new file mode 100644 index 0000000000000..04302a195fb6c --- /dev/null +++ b/src/core/server/metrics/ops_metrics_collector.test.ts @@ -0,0 +1,59 @@ +/* + * 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 { + mockOsCollector, + mockProcessCollector, + mockServerCollector, +} from './ops_metrics_collector.test.mocks'; +import { httpServiceMock } from '../http/http_service.mock'; +import { OpsMetricsCollector } from './ops_metrics_collector'; + +describe('OpsMetricsCollector', () => { + let collector: OpsMetricsCollector; + + beforeEach(() => { + const hapiServer = httpServiceMock.createSetupContract().server; + collector = new OpsMetricsCollector(hapiServer); + + mockOsCollector.collect.mockResolvedValue('osMetrics'); + }); + + it('gathers metrics from the underlying collectors', async () => { + mockOsCollector.collect.mockResolvedValue('osMetrics'); + mockProcessCollector.collect.mockResolvedValue('processMetrics'); + mockServerCollector.collect.mockResolvedValue({ + requests: 'serverRequestsMetrics', + response_times: 'serverTimingMetrics', + }); + + const metrics = await collector.collect(); + + expect(mockOsCollector.collect).toHaveBeenCalledTimes(1); + expect(mockProcessCollector.collect).toHaveBeenCalledTimes(1); + expect(mockServerCollector.collect).toHaveBeenCalledTimes(1); + + expect(metrics).toEqual({ + process: 'processMetrics', + os: 'osMetrics', + requests: 'serverRequestsMetrics', + response_times: 'serverTimingMetrics', + }); + }); +}); diff --git a/src/core/server/metrics/ops_metrics_collector.ts b/src/core/server/metrics/ops_metrics_collector.ts new file mode 100644 index 0000000000000..04344f21f57f7 --- /dev/null +++ b/src/core/server/metrics/ops_metrics_collector.ts @@ -0,0 +1,52 @@ +/* + * 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 { Server as HapiServer } from 'hapi'; +import { + ProcessMetricsCollector, + OsMetricsCollector, + ServerMetricsCollector, + MetricsCollector, +} from './collectors'; +import { OpsMetrics } from './types'; + +export class OpsMetricsCollector implements MetricsCollector<OpsMetrics> { + private readonly processCollector: ProcessMetricsCollector; + private readonly osCollector: OsMetricsCollector; + private readonly serverCollector: ServerMetricsCollector; + + constructor(server: HapiServer) { + this.processCollector = new ProcessMetricsCollector(); + this.osCollector = new OsMetricsCollector(); + this.serverCollector = new ServerMetricsCollector(server); + } + + public async collect(): Promise<OpsMetrics> { + const [process, os, server] = await Promise.all([ + this.processCollector.collect(), + this.osCollector.collect(), + this.serverCollector.collect(), + ]); + return { + process, + os, + ...server, + }; + } +} diff --git a/src/core/server/metrics/types.ts b/src/core/server/metrics/types.ts new file mode 100644 index 0000000000000..5c8f18fff380d --- /dev/null +++ b/src/core/server/metrics/types.ts @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable } from 'rxjs'; +import { OpsProcessMetrics, OpsOsMetrics, OpsServerMetrics } from './collectors'; + +/** + * APIs to retrieves metrics gathered and exposed by the core platform. + * + * @public + */ +export interface MetricsServiceSetup { + /** + * Retrieve an observable emitting the {@link OpsMetrics} gathered. + * The observable will emit an initial value during core's `start` phase, and a new value every fixed interval of time, + * based on the `opts.interval` configuration property. + * + * @example + * ```ts + * core.metrics.getOpsMetrics$().subscribe(metrics => { + * // do something with the metrics + * }) + * ``` + */ + getOpsMetrics$: () => Observable<OpsMetrics>; +} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface MetricsServiceStart {} + +export type InternalMetricsServiceSetup = MetricsServiceSetup; +export type InternalMetricsServiceStart = MetricsServiceStart; + +/** + * Regroups metrics gathered by all the collectors. + * This contains metrics about the os/runtime, the kibana process and the http server. + * + * @public + */ +export interface OpsMetrics { + /** Process related metrics */ + process: OpsProcessMetrics; + /** OS related metrics */ + os: OpsOsMetrics; + /** server response time stats */ + response_times: OpsServerMetrics['response_times']; + /** server requests stats */ + requests: OpsServerMetrics['requests']; + /** number of current concurrent connections to the server */ + concurrent_connections: OpsServerMetrics['concurrent_connections']; +} diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 96b28ab5827e1..037f3bbed67e0 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -30,6 +30,8 @@ import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; import { SharedGlobalConfig } from './plugins'; import { InternalCoreSetup, InternalCoreStart } from './internal_types'; import { capabilitiesServiceMock } from './capabilities/capabilities_service.mock'; +import { metricsServiceMock } from './metrics/metrics_service.mock'; +import { uuidServiceMock } from './uuid/uuid_service.mock'; export { httpServerMock } from './http/http_server.mocks'; export { sessionStorageMock } from './http/cookie_session_storage.mocks'; @@ -40,7 +42,7 @@ export { loggingServiceMock } from './logging/logging_service.mock'; export { savedObjectsRepositoryMock } from './saved_objects/service/lib/repository.mock'; export { typeRegistryMock as savedObjectsTypeRegistryMock } from './saved_objects/saved_objects_type_registry.mock'; export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; -import { uuidServiceMock } from './uuid/uuid_service.mock'; +export { metricsServiceMock } from './metrics/metrics_service.mock'; export function pluginInitializerContextConfigMock<T>(config: T) { const globalConfig: SharedGlobalConfig = { @@ -153,6 +155,7 @@ function createInternalCoreSetupMock() { uiSettings: uiSettingsServiceMock.createSetupContract(), savedObjects: savedObjectsServiceMock.createInternalSetupContract(), uuid: uuidServiceMock.createSetupContract(), + metrics: metricsServiceMock.createInternalSetupContract(), }; return setupDeps; } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 42bc1ce214b19..445ed16ec7829 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1176,6 +1176,11 @@ export interface LogRecord { timestamp: Date; } +// @public +export interface MetricsServiceSetup { + getOpsMetrics$: () => Observable<OpsMetrics>; +} + // @public (undocumented) export type MIGRATION_ASSISTANCE_INDEX_ACTION = 'upgrade' | 'reindex'; @@ -1227,6 +1232,63 @@ export interface OnPreResponseToolkit { next: (responseExtensions?: OnPreResponseExtensions) => OnPreResponseResult; } +// @public +export interface OpsMetrics { + concurrent_connections: OpsServerMetrics['concurrent_connections']; + os: OpsOsMetrics; + process: OpsProcessMetrics; + requests: OpsServerMetrics['requests']; + response_times: OpsServerMetrics['response_times']; +} + +// @public +export interface OpsOsMetrics { + distro?: string; + distroRelease?: string; + load: { + '1m': number; + '5m': number; + '15m': number; + }; + memory: { + total_in_bytes: number; + free_in_bytes: number; + used_in_bytes: number; + }; + platform: NodeJS.Platform; + platformRelease: string; + uptime_in_millis: number; +} + +// @public +export interface OpsProcessMetrics { + event_loop_delay: number; + memory: { + heap: { + total_in_bytes: number; + used_in_bytes: number; + size_limit: number; + }; + resident_set_size_in_bytes: number; + }; + pid: number; + uptime_in_millis: number; +} + +// @public +export interface OpsServerMetrics { + concurrent_connections: number; + requests: { + disconnects: number; + total: number; + statusCodes: Record<number, number>; + }; + response_times: { + avg_in_millis: number; + max_in_millis: number; + }; +} + // @public (undocumented) export interface PackageInfo { // (undocumented) diff --git a/src/core/server/server.test.mocks.ts b/src/core/server/server.test.mocks.ts index 038c4651ff5a7..53d1b742a6494 100644 --- a/src/core/server/server.test.mocks.ts +++ b/src/core/server/server.test.mocks.ts @@ -79,3 +79,9 @@ export const mockUuidService = uuidServiceMock.create(); jest.doMock('./uuid/uuid_service', () => ({ UuidService: jest.fn(() => mockUuidService), })); + +import { metricsServiceMock } from './metrics/metrics_service.mock'; +export const mockMetricsService = metricsServiceMock.create(); +jest.doMock('./metrics/metrics_service', () => ({ + MetricsService: jest.fn(() => mockMetricsService), +})); diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index 161dd3759a218..a4b5a9d81df20 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -28,6 +28,7 @@ import { mockEnsureValidConfiguration, mockUiSettingsService, mockRenderingService, + mockMetricsService, } from './server.test.mocks'; import { BehaviorSubject } from 'rxjs'; @@ -61,6 +62,7 @@ test('sets up services on "setup"', async () => { expect(mockSavedObjectsService.setup).not.toHaveBeenCalled(); expect(mockUiSettingsService.setup).not.toHaveBeenCalled(); expect(mockRenderingService.setup).not.toHaveBeenCalled(); + expect(mockMetricsService.setup).not.toHaveBeenCalled(); await server.setup(); @@ -71,6 +73,7 @@ test('sets up services on "setup"', async () => { expect(mockSavedObjectsService.setup).toHaveBeenCalledTimes(1); expect(mockUiSettingsService.setup).toHaveBeenCalledTimes(1); expect(mockRenderingService.setup).toHaveBeenCalledTimes(1); + expect(mockMetricsService.setup).toHaveBeenCalledTimes(1); }); test('injects legacy dependency to context#setup()', async () => { @@ -107,6 +110,7 @@ test('runs services on "start"', async () => { expect(mockLegacyService.start).not.toHaveBeenCalled(); expect(mockSavedObjectsService.start).not.toHaveBeenCalled(); expect(mockUiSettingsService.start).not.toHaveBeenCalled(); + expect(mockMetricsService.start).not.toHaveBeenCalled(); await server.start(); @@ -114,6 +118,7 @@ test('runs services on "start"', async () => { expect(mockLegacyService.start).toHaveBeenCalledTimes(1); expect(mockSavedObjectsService.start).toHaveBeenCalledTimes(1); expect(mockUiSettingsService.start).toHaveBeenCalledTimes(1); + expect(mockMetricsService.start).toHaveBeenCalledTimes(1); }); test('does not fail on "setup" if there are unused paths detected', async () => { @@ -135,6 +140,7 @@ test('stops services on "stop"', async () => { expect(mockLegacyService.stop).not.toHaveBeenCalled(); expect(mockSavedObjectsService.stop).not.toHaveBeenCalled(); expect(mockUiSettingsService.stop).not.toHaveBeenCalled(); + expect(mockMetricsService.stop).not.toHaveBeenCalled(); await server.stop(); @@ -144,6 +150,7 @@ test('stops services on "stop"', async () => { expect(mockLegacyService.stop).toHaveBeenCalledTimes(1); expect(mockSavedObjectsService.stop).toHaveBeenCalledTimes(1); expect(mockUiSettingsService.stop).toHaveBeenCalledTimes(1); + expect(mockMetricsService.stop).toHaveBeenCalledTimes(1); }); test(`doesn't setup core services if config validation fails`, async () => { @@ -159,6 +166,7 @@ test(`doesn't setup core services if config validation fails`, async () => { expect(mockLegacyService.setup).not.toHaveBeenCalled(); expect(mockUiSettingsService.setup).not.toHaveBeenCalled(); expect(mockRenderingService.setup).not.toHaveBeenCalled(); + expect(mockMetricsService.setup).not.toHaveBeenCalled(); }); test(`doesn't setup core services if legacy config validation fails`, async () => { @@ -178,4 +186,5 @@ test(`doesn't setup core services if legacy config validation fails`, async () = expect(mockLegacyService.setup).not.toHaveBeenCalled(); expect(mockSavedObjectsService.stop).not.toHaveBeenCalled(); expect(mockUiSettingsService.setup).not.toHaveBeenCalled(); + expect(mockMetricsService.setup).not.toHaveBeenCalled(); }); diff --git a/src/core/server/server.ts b/src/core/server/server.ts index db2493b38d6e0..8603f5fba1da8 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -34,6 +34,7 @@ import { Logger, LoggerFactory } from './logging'; import { UiSettingsService } from './ui_settings'; import { PluginsService, config as pluginsConfig } from './plugins'; import { SavedObjectsService } from '../server/saved_objects'; +import { MetricsService, opsConfig } from './metrics'; import { config as cspConfig } from './csp'; import { config as elasticsearchConfig } from './elasticsearch'; @@ -67,6 +68,7 @@ export class Server { private readonly savedObjects: SavedObjectsService; private readonly uiSettings: UiSettingsService; private readonly uuid: UuidService; + private readonly metrics: MetricsService; private coreStart?: InternalCoreStart; @@ -89,6 +91,7 @@ export class Server { this.uiSettings = new UiSettingsService(core); this.capabilities = new CapabilitiesService(core); this.uuid = new UuidService(core); + this.metrics = new MetricsService(core); } public async setup() { @@ -137,6 +140,8 @@ export class Server { legacyPlugins, }); + const metricsSetup = await this.metrics.setup({ http: httpSetup }); + const coreSetup: InternalCoreSetup = { capabilities: capabilitiesSetup, context: contextServiceSetup, @@ -145,6 +150,7 @@ export class Server { uiSettings: uiSettingsSetup, savedObjects: savedObjectsSetup, uuid: uuidSetup, + metrics: metricsSetup, }; const pluginsSetup = await this.plugins.setup(coreSetup); @@ -193,6 +199,7 @@ export class Server { await this.http.start(); await this.rendering.start(); + await this.metrics.start(); return this.coreStart; } @@ -207,6 +214,7 @@ export class Server { await this.http.stop(); await this.uiSettings.stop(); await this.rendering.stop(); + await this.metrics.stop(); } private registerDefaultRoute(httpSetup: InternalHttpServiceSetup) { @@ -260,6 +268,7 @@ export class Server { [savedObjectsConfig.path, savedObjectsConfig.schema], [savedObjectsMigrationConfig.path, savedObjectsMigrationConfig.schema], [uiSettingsConfig.path, uiSettingsConfig.schema], + [opsConfig.path, opsConfig.schema], ]; this.configService.addDeprecationProvider(rootConfigPath, coreDeprecationProvider); From f7dd5fe4d42e9673256d2c0423fdcfc4e28cdf91 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger <walter@elastic.co> Date: Tue, 3 Mar 2020 11:05:57 +0100 Subject: [PATCH 32/34] [ML] Transforms: Remove beta badges. (#59060) Transforms will be GA in 7.7. This PR removes the beta related UI elements. --- .../clone_transform/clone_transform_section.tsx | 13 ------------- .../create_transform/create_transform_section.tsx | 14 -------------- .../transform_management_section.tsx | 14 -------------- .../plugins/translations/translations/ja-JP.json | 4 ---- .../plugins/translations/translations/zh-CN.json | 4 ---- 5 files changed, 49 deletions(-) diff --git a/x-pack/legacy/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx b/x-pack/legacy/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx index 8f58bc94e7c12..c5c46dcac6c95 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx @@ -11,7 +11,6 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { - EuiBetaBadge, EuiButtonEmpty, EuiCallOut, EuiFlexGroup, @@ -140,18 +139,6 @@ export const CloneTransformSection: FC<Props> = ({ match }) => { id="xpack.transform.transformsWizard.cloneTransformTitle" defaultMessage="Clone transform" /> - <span> </span> - <EuiBetaBadge - label={i18n.translate('xpack.transform.transformsWizard.betaBadgeLabel', { - defaultMessage: `Beta`, - })} - tooltipContent={i18n.translate( - 'xpack.transform.transformsWizard.betaBadgeTooltipContent', - { - defaultMessage: `Transforms are a beta feature. We'd love to hear your feedback.`, - } - )} - /> </h1> </EuiFlexItem> <EuiFlexItem grow={false}> diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx index e92ba256256a4..5196f281adf0a 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx @@ -8,10 +8,8 @@ import React, { useEffect, FC } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; import { - EuiBetaBadge, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, @@ -52,18 +50,6 @@ export const CreateTransformSection: FC<Props> = ({ match }) => { id="xpack.transform.transformsWizard.createTransformTitle" defaultMessage="Create transform" /> - <span> </span> - <EuiBetaBadge - label={i18n.translate('xpack.transform.transformsWizard.betaBadgeLabel', { - defaultMessage: `Beta`, - })} - tooltipContent={i18n.translate( - 'xpack.transform.transformsWizard.betaBadgeTooltipContent', - { - defaultMessage: `Transforms are a beta feature. We'd love to hear your feedback.`, - } - )} - /> </h1> </EuiFlexItem> <EuiFlexItem grow={false}> diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx index 1573d4c53c0cf..8c174098fb623 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx @@ -7,10 +7,8 @@ import React, { FC, Fragment, useEffect, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; import { - EuiBetaBadge, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, @@ -84,18 +82,6 @@ export const TransformManagement: FC = () => { id="xpack.transform.transformList.transformTitle" defaultMessage="Transforms" /> - <span> </span> - <EuiBetaBadge - label={i18n.translate('xpack.transform.transformList.betaBadgeLabel', { - defaultMessage: `Beta`, - })} - tooltipContent={i18n.translate( - 'xpack.transform.transformList.betaBadgeTooltipContent', - { - defaultMessage: `Transforms are a beta feature. We'd love to hear your feedback.`, - } - )} - /> </h1> </EuiFlexItem> <EuiFlexItem grow={false}> diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index cadebcad93510..7fdffbec78311 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -12503,8 +12503,6 @@ "xpack.transform.toastText.modalTitle": "詳細を入力", "xpack.transform.toastText.openModalButtonText": "詳細を表示", "xpack.transform.transformForm.sizeNotationPlaceholder": "例: {example1}、{example2}、{example3}、{example4}", - "xpack.transform.transformList.betaBadgeLabel": "ベータ", - "xpack.transform.transformList.betaBadgeTooltipContent": "変換はベータ機能です。フィードバックをお待ちしています。", "xpack.transform.transformList.bulkDeleteModalBody": "{count, plural, one {この} other {これらの}} {count} 件の{count, plural, one {変換} other {変換}}を削除してよろしいですか?変換の送信先インデックスとオプションの Kibana インデックスパターンは削除されません。", "xpack.transform.transformList.bulkDeleteModalTitle": "{count} 件の{count, plural, one {変換} other {変換}}を削除", "xpack.transform.transformList.bulkStartModalTitle": "{count} 件の{count, plural, one {変換} other {変換}}を開始", @@ -12549,8 +12547,6 @@ "xpack.transform.transformList.transformDetails.tabs.transformSettingsLabel": "ジョブの詳細", "xpack.transform.transformList.transformDocsLinkText": "変換ドキュメント", "xpack.transform.transformList.transformTitle": "データフレームジョブ", - "xpack.transform.transformsWizard.betaBadgeLabel": "ベータ", - "xpack.transform.transformsWizard.betaBadgeTooltipContent": "変換はベータ機能です。フィードバックをお待ちしています。", "xpack.transform.transformsWizard.createTransformTitle": "変換の作成", "xpack.transform.transformsWizard.stepCreateTitle": "作成", "xpack.transform.transformsWizard.stepDefineTitle": "ピボットの定義", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ff72f20b24864..438a8f9197508 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -12503,8 +12503,6 @@ "xpack.transform.toastText.modalTitle": "错误详细信息", "xpack.transform.toastText.openModalButtonText": "查看详情", "xpack.transform.transformForm.sizeNotationPlaceholder": "示例:{example1}、{example2}、{example3}、{example4}", - "xpack.transform.transformList.betaBadgeLabel": "公测版", - "xpack.transform.transformList.betaBadgeTooltipContent": "转换为公测版功能。我们很乐意听取您的反馈意见。", "xpack.transform.transformList.bulkDeleteModalBody": "是否确定要删除{count, plural, one {这} other {这}} {count} 个 {count, plural, one {转换} other {转换}}?转换的目标索引和可选 Kibana 索引模式将不会删除。", "xpack.transform.transformList.bulkDeleteModalTitle": "删除 {count} 个 {count, plural, one {转换} other {转换}}?", "xpack.transform.transformList.bulkStartModalTitle": "启动 {count} 个 {count, plural, one {转换} other {转换}}?", @@ -12549,8 +12547,6 @@ "xpack.transform.transformList.transformDetails.tabs.transformSettingsLabel": "作业详情", "xpack.transform.transformList.transformDocsLinkText": "转换文档", "xpack.transform.transformList.transformTitle": "数据帧作业", - "xpack.transform.transformsWizard.betaBadgeLabel": "公测版", - "xpack.transform.transformsWizard.betaBadgeTooltipContent": "转换为公测版功能。我们很乐意听取您的反馈意见。", "xpack.transform.transformsWizard.createTransformTitle": "创建转换", "xpack.transform.transformsWizard.stepCreateTitle": "创建", "xpack.transform.transformsWizard.stepDefineTitle": "定义透视", From e5362d36a3bae9d55b9b89a462cd4a0ba3a43210 Mon Sep 17 00:00:00 2001 From: Maryia Lapata <mary.lopato@gmail.com> Date: Tue, 3 Mar 2020 13:58:05 +0300 Subject: [PATCH 33/34] [NP] Remove visualize reference in saved object save modal (#59016) * Remove visualize reference in saved object save modal * Rename attribute Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> --- .../top_nav/__snapshots__/save_modal.test.js.snap | 1 + .../public/dashboard/np_ready/top_nav/save_modal.tsx | 1 + .../public/discover/np_ready/angular/discover.js | 1 + .../kibana/public/visualize/np_ready/editor/editor.js | 1 + .../public/save_modal/saved_object_save_modal.test.tsx | 2 ++ .../public/save_modal/saved_object_save_modal.tsx | 10 +++------- .../plugins/graph/public/components/save_modal.tsx | 1 + x-pack/legacy/plugins/lens/public/app_plugin/app.tsx | 1 + .../plugins/maps/public/angular/map_controller.js | 1 + 9 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/__snapshots__/save_modal.test.js.snap b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/__snapshots__/save_modal.test.js.snap index aa9eaf09c7e0a..7ac2e2d9dd317 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/__snapshots__/save_modal.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/__snapshots__/save_modal.test.js.snap @@ -59,6 +59,7 @@ exports[`renders DashboardSaveModal 1`] = ` </React.Fragment> } showCopyOnSave={true} + showDescription={false} title="dash title" /> `; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/save_modal.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/save_modal.tsx index 026784fcae06f..4a4fcb7e1adc8 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/save_modal.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/save_modal.tsx @@ -147,6 +147,7 @@ export class DashboardSaveModal extends React.Component<Props, State> { showCopyOnSave={this.props.showCopyOnSave} objectType="dashboard" options={this.renderDashboardSaveOptions()} + showDescription={false} /> ); } diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js index 1ac54ad5dabee..bb693ab860221 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js @@ -305,6 +305,7 @@ function discoverController( defaultMessage: 'Save your Discover search so you can use it in visualizations and dashboards', })} + showDescription={false} /> ); showSaveModal(saveModal, core.i18n.Context); diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js index 293327f3f72f9..2d2552b5e2f30 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js @@ -189,6 +189,7 @@ function VisualizeAppController( objectType="visualization" confirmButtonLabel={confirmButtonLabel} description={savedVis.description} + showDescription={true} /> ); showSaveModal(saveModal, I18nContext); diff --git a/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.test.tsx b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.test.tsx index 65bd8e1d48e03..15400087c2641 100644 --- a/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.test.tsx +++ b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.test.tsx @@ -31,6 +31,7 @@ describe('SavedObjectSaveModal', () => { title={'Saved Object title'} showCopyOnSave={false} objectType="visualization" + showDescription={true} /> ); expect(wrapper).toMatchSnapshot(); @@ -44,6 +45,7 @@ describe('SavedObjectSaveModal', () => { title={'Saved Object title'} showCopyOnSave={false} objectType="visualization" + showDescription={true} confirmButtonLabel="Save and done" /> ); diff --git a/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx index 275a9da96a2c4..1d145bc97bdb4 100644 --- a/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx +++ b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx @@ -40,11 +40,6 @@ import React from 'react'; import { EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -// TODO: can't import from '../../../../legacy/core_plugins/visualizations/public/' directly, -// because yarn build:types fails after trying to emit type declarations for whole visualizations plugin -// Bunch of errors like this: 'Return type of exported function has or is using private name 'SavedVis'' -import { VISUALIZE_EMBEDDABLE_TYPE } from '../../../../legacy/core_plugins/visualizations/public/np_ready/public/embeddable/constants'; - export interface OnSaveProps { newTitle: string; newCopyOnSave: boolean; @@ -62,6 +57,7 @@ interface Props { confirmButtonLabel?: React.ReactNode; options?: React.ReactNode; description?: string; + showDescription: boolean; } interface State { @@ -112,7 +108,7 @@ export class SavedObjectSaveModal extends React.Component<Props, State> { {this.renderDuplicateTitleCallout(duplicateWarningId)} <EuiForm> - {this.props.objectType !== VISUALIZE_EMBEDDABLE_TYPE && this.props.description && ( + {!this.props.showDescription && this.props.description && ( <EuiFormRow> <EuiText color="subdued">{this.props.description}</EuiText> </EuiFormRow> @@ -164,7 +160,7 @@ export class SavedObjectSaveModal extends React.Component<Props, State> { } private renderViewDescription = () => { - if (this.props.objectType !== VISUALIZE_EMBEDDABLE_TYPE) { + if (!this.props.showDescription) { return; } diff --git a/x-pack/legacy/plugins/graph/public/components/save_modal.tsx b/x-pack/legacy/plugins/graph/public/components/save_modal.tsx index 3dede69d0ca93..a7329c10e93d7 100644 --- a/x-pack/legacy/plugins/graph/public/components/save_modal.tsx +++ b/x-pack/legacy/plugins/graph/public/components/save_modal.tsx @@ -49,6 +49,7 @@ export function SaveModal({ objectType={i18n.translate('xpack.graph.topNavMenu.save.objectType', { defaultMessage: 'graph', })} + showDescription={false} options={ <> <EuiFormRow diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx index a212cb0a1a879..a0c6e4c21a34b 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -374,6 +374,7 @@ export function App({ objectType={i18n.translate('xpack.lens.app.saveModalType', { defaultMessage: 'Lens visualization', })} + showDescription={false} confirmButtonLabel={confirmButton} /> )} 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 a8e9ae46a3b9a..84ead42d3374e 100644 --- a/x-pack/legacy/plugins/maps/public/angular/map_controller.js +++ b/x-pack/legacy/plugins/maps/public/angular/map_controller.js @@ -595,6 +595,7 @@ app.controller( title={savedMap.title} showCopyOnSave={savedMap.id ? true : false} objectType={MAP_SAVED_OBJECT_TYPE} + showDescription={false} /> ); showSaveModal(saveModal, npStart.core.i18n.Context); From 4ef594c20896c84380bb2e6ad1a7730ca2c43608 Mon Sep 17 00:00:00 2001 From: James Gowdy <jgowdy@elastic.co> Date: Tue, 3 Mar 2020 11:59:35 +0000 Subject: [PATCH 34/34] [ML] Adding indices_options to datafeed (#59119) * [ML] Adding indices_options to datafeed * adding extra checks to the schema * updating expand_wildcards --- .../common/job_creator/configs/datafeed.ts | 8 ++++++++ .../ml/server/routes/schemas/datafeeds_schema.ts | 15 ++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/datafeed.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/datafeed.ts index e35f3056ce434..538b225926f65 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/datafeed.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/datafeed.ts @@ -25,6 +25,7 @@ export interface Datafeed { script_fields?: object; scroll_size?: number; delayed_data_check_config?: object; + indices_options?: IndicesOptions; } export interface ChunkingConfig { @@ -42,3 +43,10 @@ interface Aggregation { aggs?: { [key: string]: any }; }; } + +interface IndicesOptions { + expand_wildcards?: 'all' | 'open' | 'closed' | 'hidden' | 'none'; + ignore_unavailable?: boolean; + allow_no_indices?: boolean; + ignore_throttled?: boolean; +} diff --git a/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts b/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts index 02677dcb107c2..ee49da6538460 100644 --- a/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts @@ -17,7 +17,12 @@ export const datafeedConfigSchema = schema.object({ feed_id: schema.maybe(schema.string()), aggregations: schema.maybe(schema.any()), aggs: schema.maybe(schema.any()), - chunking_config: schema.maybe(schema.any()), + chunking_config: schema.maybe( + schema.object({ + mode: schema.maybe(schema.string()), + time_span: schema.maybe(schema.string()), + }) + ), frequency: schema.maybe(schema.string()), indices: schema.arrayOf(schema.string()), indexes: schema.maybe(schema.arrayOf(schema.string())), @@ -28,4 +33,12 @@ export const datafeedConfigSchema = schema.object({ script_fields: schema.maybe(schema.any()), scroll_size: schema.maybe(schema.number()), delayed_data_check_config: schema.maybe(schema.any()), + indices_options: schema.maybe( + schema.object({ + expand_wildcards: schema.maybe(schema.arrayOf(schema.string())), + ignore_unavailable: schema.maybe(schema.boolean()), + allow_no_indices: schema.maybe(schema.boolean()), + ignore_throttled: schema.maybe(schema.boolean()), + }) + ), });