diff --git a/.ci/Jenkinsfile_baseline_trigger b/.ci/Jenkinsfile_baseline_trigger index cc9fb47ca4993..221b7a44e30df 100644 --- a/.ci/Jenkinsfile_baseline_trigger +++ b/.ci/Jenkinsfile_baseline_trigger @@ -16,6 +16,12 @@ kibanaLibrary.load() withGithubCredentials { branches.each { branch -> + if (branch == '6.8') { + // skip 6.8, it is tracked but we don't need snapshots for it and haven't backported + // the baseline capture scripts to it. + return; + } + stage(branch) { def commits = getCommits(branch, MAXIMUM_COMMITS_TO_CHECK, MAXIMUM_COMMITS_TO_BUILD) diff --git a/src/core/server/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts index 7d37af833d4c1..ba6662db3655e 100644 --- a/src/core/server/http/http_server.mocks.ts +++ b/src/core/server/http/http_server.mocks.ts @@ -89,7 +89,12 @@ function createKibanaRequestMock

({ settings: { tags: routeTags, auth: routeAuthRequired, app: kibanaRouteState }, }, raw: { - req: { socket }, + req: { + socket, + // these are needed to avoid an error when consuming KibanaRequest.events + on: jest.fn(), + off: jest.fn(), + }, }, }), { diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index 51f11b15f2e09..676cee1954c59 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -33,6 +33,7 @@ import { OnPreRoutingToolkit } from './lifecycle/on_pre_routing'; import { AuthToolkit } from './lifecycle/auth'; import { sessionStorageMock } from './cookie_session_storage.mocks'; import { OnPostAuthToolkit } from './lifecycle/on_post_auth'; +import { OnPreAuthToolkit } from './lifecycle/on_pre_auth'; import { OnPreResponseToolkit } from './lifecycle/on_pre_response'; type BasePathMocked = jest.Mocked; @@ -175,15 +176,19 @@ const createHttpServiceMock = () => { return mocked; }; -const createOnPreAuthToolkitMock = (): jest.Mocked => ({ +const createOnPreAuthToolkitMock = (): jest.Mocked => ({ next: jest.fn(), - rewriteUrl: jest.fn(), }); const createOnPostAuthToolkitMock = (): jest.Mocked => ({ next: jest.fn(), }); +const createOnPreRoutingToolkitMock = (): jest.Mocked => ({ + next: jest.fn(), + rewriteUrl: jest.fn(), +}); + const createAuthToolkitMock = (): jest.Mocked => ({ authenticated: jest.fn(), notHandled: jest.fn(), @@ -205,6 +210,7 @@ export const httpServiceMock = { createOnPreAuthToolkit: createOnPreAuthToolkitMock, createOnPostAuthToolkit: createOnPostAuthToolkitMock, createOnPreResponseToolkit: createOnPreResponseToolkitMock, + createOnPreRoutingToolkit: createOnPreRoutingToolkitMock, createAuthToolkit: createAuthToolkitMock, createRouter: mockRouter.create, }; diff --git a/src/plugins/data/public/search/search_source/normalize_sort_request.test.ts b/src/plugins/data/public/search/search_source/normalize_sort_request.test.ts index d47aab80ee0bc..10004b87ca690 100644 --- a/src/plugins/data/public/search/search_source/normalize_sort_request.test.ts +++ b/src/plugins/data/public/search/search_source/normalize_sort_request.test.ts @@ -23,13 +23,23 @@ import { IIndexPattern } from '../..'; describe('SearchSource#normalizeSortRequest', function () { const scriptedField = { - name: 'script string', + name: 'script number', type: 'number', scripted: true, sortable: true, script: 'foo', lang: 'painless', }; + const stringScriptedField = { + ...scriptedField, + name: 'script string', + type: 'string', + }; + const booleanScriptedField = { + ...scriptedField, + name: 'script boolean', + type: 'boolean', + }; const murmurScriptedField = { ...scriptedField, sortable: false, @@ -37,7 +47,7 @@ describe('SearchSource#normalizeSortRequest', function () { type: 'murmur3', }; const indexPattern = { - fields: [scriptedField, murmurScriptedField], + fields: [scriptedField, stringScriptedField, booleanScriptedField, murmurScriptedField], } as IIndexPattern; it('should return an array', function () { @@ -106,6 +116,54 @@ describe('SearchSource#normalizeSortRequest', function () { ]); }); + it('should use script based sorting with string type', function () { + const result = normalizeSortRequest( + [ + { + [stringScriptedField.name]: SortDirection.asc, + }, + ], + indexPattern + ); + + expect(result).toEqual([ + { + _script: { + script: { + source: stringScriptedField.script, + lang: stringScriptedField.lang, + }, + type: 'string', + order: SortDirection.asc, + }, + }, + ]); + }); + + it('should use script based sorting with boolean type as string type', function () { + const result = normalizeSortRequest( + [ + { + [booleanScriptedField.name]: SortDirection.asc, + }, + ], + indexPattern + ); + + expect(result).toEqual([ + { + _script: { + script: { + source: booleanScriptedField.script, + lang: booleanScriptedField.lang, + }, + type: 'string', + order: SortDirection.asc, + }, + }, + ]); + }); + it('should use script based sorting only on sortable types', function () { const result = normalizeSortRequest( [ diff --git a/src/plugins/data/public/search/search_source/normalize_sort_request.ts b/src/plugins/data/public/search/search_source/normalize_sort_request.ts index 9a0cf371ce81d..b00d28b38d670 100644 --- a/src/plugins/data/public/search/search_source/normalize_sort_request.ts +++ b/src/plugins/data/public/search/search_source/normalize_sort_request.ts @@ -69,7 +69,7 @@ function normalize( // The ES API only supports sort scripts of type 'number' and 'string' function castSortType(type: string) { - if (['number', 'string'].includes(type)) { + if (['number'].includes(type)) { return 'number'; } else if (['string', 'boolean'].includes(type)) { return 'string'; diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axis_options.test.tsx.snap b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axis_options.test.tsx.snap index b89d193c7c751..36f5480e85406 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axis_options.test.tsx.snap +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axis_options.test.tsx.snap @@ -102,7 +102,7 @@ exports[`ValueAxisOptions component should init with the default set of props 1` value="" /> diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.ts b/src/plugins/visualizations/public/legacy/build_pipeline.ts index e74a83d91fabf..d3fe814f3b010 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.ts @@ -255,7 +255,7 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = { input_control_vis: (params) => { return `input_control_vis ${prepareJson('visConfig', params)}`; }, - metrics: (params, schemas, uiState = {}) => { + metrics: ({ title, ...params }, schemas, uiState = {}) => { const paramsJson = prepareJson('params', params); const uiStateJson = prepareJson('uiState', uiState); diff --git a/test/functional/apps/dashboard/dashboard_filter_bar.js b/test/functional/apps/dashboard/dashboard_filter_bar.js index f3241568bbb3e..dd0318ea5c0d7 100644 --- a/test/functional/apps/dashboard/dashboard_filter_bar.js +++ b/test/functional/apps/dashboard/dashboard_filter_bar.js @@ -30,8 +30,7 @@ export default function ({ getService, getPageObjects }) { const browser = getService('browser'); const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'visualize', 'timePicker']); - // FLAKY: https://github.com/elastic/kibana/issues/71987 - describe.skip('dashboard filter bar', () => { + describe('dashboard filter bar', () => { before(async () => { await esArchiver.load('dashboard/current/kibana'); await kibanaServer.uiSettings.replace({ @@ -69,6 +68,7 @@ export default function ({ getService, getPageObjects }) { it('uses default index pattern on an empty dashboard', async () => { await testSubjects.click('addFilter'); await dashboardExpect.fieldSuggestions(['bytes']); + await filterBar.ensureFieldEditorModalIsClosed(); }); it('shows index pattern of vis when one is added', async () => { @@ -77,6 +77,7 @@ export default function ({ getService, getPageObjects }) { await filterBar.ensureFieldEditorModalIsClosed(); await testSubjects.click('addFilter'); await dashboardExpect.fieldSuggestions(['animal']); + await filterBar.ensureFieldEditorModalIsClosed(); }); it('works when a vis with no index pattern is added', async () => { diff --git a/test/functional/apps/dashboard/dashboard_snapshots.js b/test/functional/apps/dashboard/dashboard_snapshots.js index 20bc30c889d65..787e839aa08a5 100644 --- a/test/functional/apps/dashboard/dashboard_snapshots.js +++ b/test/functional/apps/dashboard/dashboard_snapshots.js @@ -28,8 +28,7 @@ export default function ({ getService, getPageObjects, updateBaselines }) { const dashboardPanelActions = getService('dashboardPanelActions'); const dashboardAddPanel = getService('dashboardAddPanel'); - // FLAKY: https://github.com/elastic/kibana/issues/52854 - describe.skip('dashboard snapshots', function describeIndexTests() { + describe('dashboard snapshots', function describeIndexTests() { before(async function () { await esArchiver.load('dashboard/current/kibana'); await kibanaServer.uiSettings.replace({ diff --git a/test/functional/apps/visualize/_area_chart.js b/test/functional/apps/visualize/_area_chart.js index 41e56986f677b..4321f0df89250 100644 --- a/test/functional/apps/visualize/_area_chart.js +++ b/test/functional/apps/visualize/_area_chart.js @@ -298,7 +298,7 @@ export default function ({ getService, getPageObjects }) { }); }); - describe.skip('switch between Y axis scale types', () => { + describe('switch between Y axis scale types', () => { before(initAreaChart); const axisId = 'ValueAxis-1'; @@ -308,57 +308,25 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visEditor.selectYAxisScaleType(axisId, 'log'); await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = [ - '2', - '3', - '5', - '7', - '10', - '20', - '30', - '50', - '70', - '100', - '200', - '300', - '500', - '700', - '1,000', - '2,000', - '3,000', - '5,000', - '7,000', - ]; - expect(labels).to.eql(expectedLabels); + const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(); + const minLabel = 2; + const maxLabel = 5000; + const numberOfLabels = 10; + expect(labels.length).to.be.greaterThan(numberOfLabels); + expect(labels[0]).to.eql(minLabel); + expect(labels[labels.length - 1]).to.be.greaterThan(maxLabel); }); it('should show filtered ticks on selecting log scale', async () => { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = [ - '2', - '3', - '5', - '7', - '10', - '20', - '30', - '50', - '70', - '100', - '200', - '300', - '500', - '700', - '1,000', - '2,000', - '3,000', - '5,000', - '7,000', - ]; - expect(labels).to.eql(expectedLabels); + const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(); + const minLabel = 2; + const maxLabel = 5000; + const numberOfLabels = 10; + expect(labels.length).to.be.greaterThan(numberOfLabels); + expect(labels[0]).to.eql(minLabel); + expect(labels[labels.length - 1]).to.be.greaterThan(maxLabel); }); it('should show ticks on selecting square root scale', async () => { diff --git a/test/functional/apps/visualize/_line_chart.js b/test/functional/apps/visualize/_line_chart.js index a492f3858b524..24e4ef4a7fe25 100644 --- a/test/functional/apps/visualize/_line_chart.js +++ b/test/functional/apps/visualize/_line_chart.js @@ -181,7 +181,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visChart.waitForVisualization(); }); - describe.skip('switch between Y axis scale types', () => { + describe('switch between Y axis scale types', () => { before(initLineChart); const axisId = 'ValueAxis-1'; @@ -191,57 +191,25 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visEditor.selectYAxisScaleType(axisId, 'log'); await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = [ - '2', - '3', - '5', - '7', - '10', - '20', - '30', - '50', - '70', - '100', - '200', - '300', - '500', - '700', - '1,000', - '2,000', - '3,000', - '5,000', - '7,000', - ]; - expect(labels).to.eql(expectedLabels); + const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(); + const minLabel = 2; + const maxLabel = 5000; + const numberOfLabels = 10; + expect(labels.length).to.be.greaterThan(numberOfLabels); + expect(labels[0]).to.eql(minLabel); + expect(labels[labels.length - 1]).to.be.greaterThan(maxLabel); }); it('should show filtered ticks on selecting log scale', async () => { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = [ - '2', - '3', - '5', - '7', - '10', - '20', - '30', - '50', - '70', - '100', - '200', - '300', - '500', - '700', - '1,000', - '2,000', - '3,000', - '5,000', - '7,000', - ]; - expect(labels).to.eql(expectedLabels); + const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(); + const minLabel = 2; + const maxLabel = 5000; + const numberOfLabels = 10; + expect(labels.length).to.be.greaterThan(numberOfLabels); + expect(labels[0]).to.eql(minLabel); + expect(labels[labels.length - 1]).to.be.greaterThan(maxLabel); }); it('should show ticks on selecting square root scale', async () => { diff --git a/test/functional/apps/visualize/_vertical_bar_chart.js b/test/functional/apps/visualize/_vertical_bar_chart.js index f1d83908b9b6d..ff0423eb531da 100644 --- a/test/functional/apps/visualize/_vertical_bar_chart.js +++ b/test/functional/apps/visualize/_vertical_bar_chart.js @@ -28,19 +28,28 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['visualize', 'visEditor', 'visChart', 'timePicker']); describe('vertical bar chart', function () { + const vizName1 = 'Visualization VerticalBarChart'; + + const initBarChart = async () => { + log.debug('navigateToApp visualize'); + await PageObjects.visualize.navigateToNewVisualization(); + log.debug('clickVerticalBarChart'); + await PageObjects.visualize.clickVerticalBarChart(); + await PageObjects.visualize.clickNewSearch(); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + log.debug('Bucket = X-Axis'); + await PageObjects.visEditor.clickBucket('X-axis'); + log.debug('Aggregation = Date Histogram'); + await PageObjects.visEditor.selectAggregation('Date Histogram'); + log.debug('Field = @timestamp'); + await PageObjects.visEditor.selectField('@timestamp'); + // leaving Interval set to Auto + await PageObjects.visEditor.clickGo(); + }; + describe('bar charts x axis tick labels', () => { it('should show tick labels also after rotation of the chart', async function () { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickVerticalBarChart(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visEditor.clickBucket('X-axis'); - log.debug('Aggregation = Date Histogram'); - await PageObjects.visEditor.selectAggregation('Date Histogram'); - log.debug('Field = @timestamp'); - await PageObjects.visEditor.selectField('@timestamp'); - // leaving Interval set to Auto - await PageObjects.visEditor.clickGo(); + await initBarChart(); const bottomLabels = await PageObjects.visChart.getXAxisLabels(); log.debug(`${bottomLabels.length} tick labels on bottom x axis`); @@ -62,6 +71,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visEditor.clickBucket('X-axis'); await PageObjects.visEditor.selectAggregation('Date Range'); await PageObjects.visEditor.selectField('@timestamp'); + await PageObjects.visEditor.clickGo(); const bottomLabels = await PageObjects.visChart.getXAxisLabels(); expect(bottomLabels.length).to.be(1); @@ -95,519 +105,456 @@ export default function ({ getService, getPageObjects }) { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/22322 - describe.skip('vertical bar chart flaky part', function () { - const vizName1 = 'Visualization VerticalBarChart'; + it('should save and load', async function () { + await initBarChart(); + await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb(vizName1); - const initBarChart = async () => { - log.debug('navigateToApp visualize'); - await PageObjects.visualize.navigateToNewVisualization(); - log.debug('clickVerticalBarChart'); - await PageObjects.visualize.clickVerticalBarChart(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - log.debug('Bucket = X-Axis'); - await PageObjects.visEditor.clickBucket('X-axis'); - log.debug('Aggregation = Date Histogram'); - await PageObjects.visEditor.selectAggregation('Date Histogram'); - log.debug('Field = @timestamp'); - await PageObjects.visEditor.selectField('@timestamp'); - // leaving Interval set to Auto - await PageObjects.visEditor.clickGo(); - }; + await PageObjects.visualize.loadSavedVisualization(vizName1); + await PageObjects.visChart.waitForVisualization(); + }); - before(initBarChart); + it('should have inspector enabled', async function () { + await inspector.expectIsEnabled(); + }); - it('should save and load', async function () { - await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb(vizName1); + it('should show correct chart', async function () { + const expectedChartValues = [ + 37, + 202, + 740, + 1437, + 1371, + 751, + 188, + 31, + 42, + 202, + 683, + 1361, + 1415, + 707, + 177, + 27, + 32, + 175, + 707, + 1408, + 1355, + 726, + 201, + 29, + ]; + + // Most recent failure on Jenkins usually indicates the bar chart is still being drawn? + // return arguments[0].getAttribute(arguments[1]);","args":[{"ELEMENT":"592"},"fill"]}] arguments[0].getAttribute is not a function + // try sleeping a bit before getting that data + await retry.try(async () => { + const data = await PageObjects.visChart.getBarChartData(); + log.debug('data=' + data); + log.debug('data.length=' + data.length); + expect(data).to.eql(expectedChartValues); + }); + }); - await PageObjects.visualize.loadSavedVisualization(vizName1); - await PageObjects.visChart.waitForVisualization(); + it('should show correct data', async function () { + // this is only the first page of the tabular data. + const expectedChartData = [ + ['2015-09-20 00:00', '37'], + ['2015-09-20 03:00', '202'], + ['2015-09-20 06:00', '740'], + ['2015-09-20 09:00', '1,437'], + ['2015-09-20 12:00', '1,371'], + ['2015-09-20 15:00', '751'], + ['2015-09-20 18:00', '188'], + ['2015-09-20 21:00', '31'], + ['2015-09-21 00:00', '42'], + ['2015-09-21 03:00', '202'], + ['2015-09-21 06:00', '683'], + ['2015-09-21 09:00', '1,361'], + ['2015-09-21 12:00', '1,415'], + ['2015-09-21 15:00', '707'], + ['2015-09-21 18:00', '177'], + ['2015-09-21 21:00', '27'], + ['2015-09-22 00:00', '32'], + ['2015-09-22 03:00', '175'], + ['2015-09-22 06:00', '707'], + ['2015-09-22 09:00', '1,408'], + ]; + + await inspector.open(); + await inspector.expectTableData(expectedChartData); + await inspector.close(); + }); + + it('should have `drop partial buckets` option', async () => { + const fromTime = 'Sep 20, 2015 @ 06:31:44.000'; + const toTime = 'Sep 22, 2015 @ 18:31:44.000'; + + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + + let expectedChartValues = [ + 82, + 218, + 341, + 440, + 480, + 517, + 522, + 446, + 403, + 321, + 258, + 172, + 95, + 55, + 38, + 24, + 3, + 4, + 11, + 14, + 17, + 38, + 49, + 115, + 152, + 216, + 315, + 402, + 446, + 513, + 520, + 474, + 421, + 307, + 230, + 170, + 99, + 48, + 30, + 15, + 10, + 2, + 8, + 7, + 17, + 34, + 37, + 104, + 153, + 241, + 313, + 404, + 492, + 512, + 503, + 473, + 379, + 293, + 277, + 156, + 56, + ]; + + // Most recent failure on Jenkins usually indicates the bar chart is still being drawn? + // return arguments[0].getAttribute(arguments[1]);","args":[{"ELEMENT":"592"},"fill"]}] arguments[0].getAttribute is not a function + // try sleeping a bit before getting that data + await retry.try(async () => { + const data = await PageObjects.visChart.getBarChartData(); + log.debug('data=' + data); + log.debug('data.length=' + data.length); + expect(data).to.eql(expectedChartValues); }); - it('should have inspector enabled', async function () { - await inspector.expectIsEnabled(); + await PageObjects.visEditor.toggleOpenEditor(2); + await PageObjects.visEditor.clickDropPartialBuckets(); + await PageObjects.visEditor.clickGo(); + + expectedChartValues = [ + 218, + 341, + 440, + 480, + 517, + 522, + 446, + 403, + 321, + 258, + 172, + 95, + 55, + 38, + 24, + 3, + 4, + 11, + 14, + 17, + 38, + 49, + 115, + 152, + 216, + 315, + 402, + 446, + 513, + 520, + 474, + 421, + 307, + 230, + 170, + 99, + 48, + 30, + 15, + 10, + 2, + 8, + 7, + 17, + 34, + 37, + 104, + 153, + 241, + 313, + 404, + 492, + 512, + 503, + 473, + 379, + 293, + 277, + 156, + ]; + + // Most recent failure on Jenkins usually indicates the bar chart is still being drawn? + // return arguments[0].getAttribute(arguments[1]);","args":[{"ELEMENT":"592"},"fill"]}] arguments[0].getAttribute is not a function + // try sleeping a bit before getting that data + await retry.try(async () => { + const data = await PageObjects.visChart.getBarChartData(); + log.debug('data=' + data); + log.debug('data.length=' + data.length); + expect(data).to.eql(expectedChartValues); }); + }); - it('should show correct chart', async function () { - const expectedChartValues = [ - 37, - 202, - 740, - 1437, - 1371, - 751, - 188, - 31, - 42, - 202, - 683, - 1361, - 1415, - 707, - 177, - 27, - 32, - 175, - 707, - 1408, - 1355, - 726, - 201, - 29, - ]; + describe('switch between Y axis scale types', () => { + before(initBarChart); + const axisId = 'ValueAxis-1'; - // Most recent failure on Jenkins usually indicates the bar chart is still being drawn? - // return arguments[0].getAttribute(arguments[1]);","args":[{"ELEMENT":"592"},"fill"]}] arguments[0].getAttribute is not a function - // try sleeping a bit before getting that data - await retry.try(async () => { - const data = await PageObjects.visChart.getBarChartData(); - log.debug('data=' + data); - log.debug('data.length=' + data.length); - expect(data).to.eql(expectedChartValues); - }); + it('should show ticks on selecting log scale', async () => { + await PageObjects.visEditor.clickMetricsAndAxes(); + await PageObjects.visEditor.clickYAxisOptions(axisId); + await PageObjects.visEditor.selectYAxisScaleType(axisId, 'log'); + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(); + const minLabel = 2; + const maxLabel = 5000; + const numberOfLabels = 10; + expect(labels.length).to.be.greaterThan(numberOfLabels); + expect(labels[0]).to.eql(minLabel); + expect(labels[labels.length - 1]).to.be.greaterThan(maxLabel); }); - it('should show correct data', async function () { - // this is only the first page of the tabular data. - const expectedChartData = [ - ['2015-09-20 00:00', '37'], - ['2015-09-20 03:00', '202'], - ['2015-09-20 06:00', '740'], - ['2015-09-20 09:00', '1,437'], - ['2015-09-20 12:00', '1,371'], - ['2015-09-20 15:00', '751'], - ['2015-09-20 18:00', '188'], - ['2015-09-20 21:00', '31'], - ['2015-09-21 00:00', '42'], - ['2015-09-21 03:00', '202'], - ['2015-09-21 06:00', '683'], - ['2015-09-21 09:00', '1,361'], - ['2015-09-21 12:00', '1,415'], - ['2015-09-21 15:00', '707'], - ['2015-09-21 18:00', '177'], - ['2015-09-21 21:00', '27'], - ['2015-09-22 00:00', '32'], - ['2015-09-22 03:00', '175'], - ['2015-09-22 06:00', '707'], - ['2015-09-22 09:00', '1,408'], - ]; - - await inspector.open(); - await inspector.expectTableData(expectedChartData); - await inspector.close(); + it('should show filtered ticks on selecting log scale', async () => { + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(); + const minLabel = 2; + const maxLabel = 5000; + const numberOfLabels = 10; + expect(labels.length).to.be.greaterThan(numberOfLabels); + expect(labels[0]).to.eql(minLabel); + expect(labels[labels.length - 1]).to.be.greaterThan(maxLabel); }); - it('should have `drop partial buckets` option', async () => { - const fromTime = 'Sep 20, 2015 @ 06:31:44.000'; - const toTime = 'Sep 22, 2015 @ 18:31:44.000'; - - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); - - let expectedChartValues = [ - 82, - 218, - 341, - 440, - 480, - 517, - 522, - 446, - 403, - 321, - 258, - 172, - 95, - 55, - 38, - 24, - 3, - 4, - 11, - 14, - 17, - 38, - 49, - 115, - 152, - 216, - 315, - 402, - 446, - 513, - 520, - 474, - 421, - 307, - 230, - 170, - 99, - 48, - 30, - 15, - 10, - 2, - 8, - 7, - 17, - 34, - 37, - 104, - 153, - 241, - 313, - 404, - 492, - 512, - 503, - 473, - 379, - 293, - 277, - 156, - 56, + it('should show ticks on selecting square root scale', async () => { + await PageObjects.visEditor.selectYAxisScaleType(axisId, 'square root'); + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); + const expectedLabels = [ + '0', + '200', + '400', + '600', + '800', + '1,000', + '1,200', + '1,400', + '1,600', ]; + expect(labels).to.eql(expectedLabels); + }); - // Most recent failure on Jenkins usually indicates the bar chart is still being drawn? - // return arguments[0].getAttribute(arguments[1]);","args":[{"ELEMENT":"592"},"fill"]}] arguments[0].getAttribute is not a function - // try sleeping a bit before getting that data - await retry.try(async () => { - const data = await PageObjects.visChart.getBarChartData(); - log.debug('data=' + data); - log.debug('data.length=' + data.length); - expect(data).to.eql(expectedChartValues); - }); - - await PageObjects.visEditor.toggleOpenEditor(2); - await PageObjects.visEditor.clickDropPartialBuckets(); + it('should show filtered ticks on selecting square root scale', async () => { + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); + const expectedLabels = ['200', '400', '600', '800', '1,000', '1,200', '1,400']; + expect(labels).to.eql(expectedLabels); + }); - expectedChartValues = [ - 218, - 341, - 440, - 480, - 517, - 522, - 446, - 403, - 321, - 258, - 172, - 95, - 55, - 38, - 24, - 3, - 4, - 11, - 14, - 17, - 38, - 49, - 115, - 152, - 216, - 315, - 402, - 446, - 513, - 520, - 474, - 421, - 307, - 230, - 170, - 99, - 48, - 30, - 15, - 10, - 2, - 8, - 7, - 17, - 34, - 37, - 104, - 153, - 241, - 313, - 404, - 492, - 512, - 503, - 473, - 379, - 293, - 277, - 156, + it('should show ticks on selecting linear scale', async () => { + await PageObjects.visEditor.selectYAxisScaleType(axisId, 'linear'); + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); + log.debug(labels); + const expectedLabels = [ + '0', + '200', + '400', + '600', + '800', + '1,000', + '1,200', + '1,400', + '1,600', ]; + expect(labels).to.eql(expectedLabels); + }); - // Most recent failure on Jenkins usually indicates the bar chart is still being drawn? - // return arguments[0].getAttribute(arguments[1]);","args":[{"ELEMENT":"592"},"fill"]}] arguments[0].getAttribute is not a function - // try sleeping a bit before getting that data - await retry.try(async () => { - const data = await PageObjects.visChart.getBarChartData(); - log.debug('data=' + data); - log.debug('data.length=' + data.length); - expect(data).to.eql(expectedChartValues); - }); + it('should show filtered ticks on selecting linear scale', async () => { + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); + const expectedLabels = ['200', '400', '600', '800', '1,000', '1,200', '1,400']; + expect(labels).to.eql(expectedLabels); }); + }); - describe('switch between Y axis scale types', () => { - before(initBarChart); + describe('vertical bar in percent mode', async () => { + it('should show ticks with percentage values', async function () { const axisId = 'ValueAxis-1'; + await PageObjects.visEditor.clickMetricsAndAxes(); + await PageObjects.visEditor.clickYAxisOptions(axisId); + await PageObjects.visEditor.selectYAxisMode('percentage'); + await PageObjects.visEditor.changeYAxisShowCheckbox(axisId, true); + await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); + await PageObjects.visEditor.clickGo(); + const labels = await PageObjects.visChart.getYAxisLabels(); + expect(labels[0]).to.eql('0%'); + expect(labels[labels.length - 1]).to.eql('100%'); + }); + }); - it('should show ticks on selecting log scale', async () => { - await PageObjects.visEditor.clickMetricsAndAxes(); - await PageObjects.visEditor.clickYAxisOptions(axisId); - await PageObjects.visEditor.selectYAxisScaleType(axisId, 'log'); - await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = [ - '2', - '3', - '4', - '5', - '7', - '9', - '20', - '30', - '40', - '50', - '70', - '90', - '200', - '300', - '400', - '500', - '700', - '900', - '2,000', - '3,000', - '4,000', - '5,000', - '7,000', - ]; - expect(labels).to.eql(expectedLabels); - }); - - it('should show filtered ticks on selecting log scale', async () => { - await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = [ - '2', - '3', - '4', - '5', - '7', - '9', - '20', - '30', - '40', - '50', - '70', - '90', - '200', - '300', - '400', - '500', - '700', - '900', - '2,000', - '3,000', - '4,000', - '5,000', - '7,000', - ]; - expect(labels).to.eql(expectedLabels); - }); - - it('should show ticks on selecting square root scale', async () => { - await PageObjects.visEditor.selectYAxisScaleType(axisId, 'square root'); - await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = [ - '0', - '200', - '400', - '600', - '800', - '1,000', - '1,200', - '1,400', - '1,600', - ]; - expect(labels).to.eql(expectedLabels); - }); - - it('should show filtered ticks on selecting square root scale', async () => { - await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = ['200', '400', '600', '800', '1,000', '1,200', '1,400']; - expect(labels).to.eql(expectedLabels); - }); - - it('should show ticks on selecting linear scale', async () => { - await PageObjects.visEditor.selectYAxisScaleType(axisId, 'linear'); - await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); - log.debug(labels); - const expectedLabels = [ - '0', - '200', - '400', - '600', - '800', - '1,000', - '1,200', - '1,400', - '1,600', - ]; - expect(labels).to.eql(expectedLabels); - }); - - it('should show filtered ticks on selecting linear scale', async () => { - await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = ['200', '400', '600', '800', '1,000', '1,200', '1,400']; - expect(labels).to.eql(expectedLabels); - }); + describe('vertical bar with Split series', function () { + before(initBarChart); + + it('should show correct series', async function () { + await PageObjects.visEditor.toggleOpenEditor(2, 'false'); + await PageObjects.visEditor.clickBucket('Split series'); + await PageObjects.visEditor.selectAggregation('Terms'); + await PageObjects.visEditor.selectField('response.raw'); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); + await PageObjects.visEditor.clickGo(); + + const expectedEntries = ['200', '404', '503']; + const legendEntries = await PageObjects.visChart.getLegendEntries(); + expect(legendEntries).to.eql(expectedEntries); }); - describe('vertical bar in percent mode', async () => { - it('should show ticks with percentage values', async function () { - const axisId = 'ValueAxis-1'; - await PageObjects.visEditor.clickMetricsAndAxes(); - await PageObjects.visEditor.clickYAxisOptions(axisId); - await PageObjects.visEditor.selectYAxisMode('percentage'); - await PageObjects.visEditor.changeYAxisShowCheckbox(axisId, true); - await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); - expect(labels[0]).to.eql('0%'); - expect(labels[labels.length - 1]).to.eql('100%'); - }); + it('should allow custom sorting of series', async () => { + await PageObjects.visEditor.toggleOpenEditor(1, 'false'); + await PageObjects.visEditor.selectCustomSortMetric(3, 'Min', 'bytes'); + await PageObjects.visEditor.clickGo(); + + const expectedEntries = ['404', '200', '503']; + const legendEntries = await PageObjects.visChart.getLegendEntries(); + expect(legendEntries).to.eql(expectedEntries); }); - describe('vertical bar with Split series', function () { - before(initBarChart); - - it('should show correct series', async function () { - await PageObjects.visEditor.toggleOpenEditor(2, 'false'); - await PageObjects.visEditor.clickBucket('Split series'); - await PageObjects.visEditor.selectAggregation('Terms'); - await PageObjects.visEditor.selectField('response.raw'); - await PageObjects.visChart.waitForVisualizationRenderingStabilized(); - await PageObjects.visEditor.clickGo(); - - const expectedEntries = ['200', '404', '503']; - const legendEntries = await PageObjects.visChart.getLegendEntries(); - expect(legendEntries).to.eql(expectedEntries); - }); - - it('should allow custom sorting of series', async () => { - await PageObjects.visEditor.toggleOpenEditor(1, 'false'); - await PageObjects.visEditor.selectCustomSortMetric(3, 'Min', 'bytes'); - await PageObjects.visEditor.clickGo(); - - const expectedEntries = ['404', '200', '503']; - const legendEntries = await PageObjects.visChart.getLegendEntries(); - expect(legendEntries).to.eql(expectedEntries); - }); - - it('should correctly filter by legend', async () => { - await PageObjects.visChart.filterLegend('200'); - await PageObjects.visChart.waitForVisualization(); - const legendEntries = await PageObjects.visChart.getLegendEntries(); - const expectedEntries = ['200']; - expect(legendEntries).to.eql(expectedEntries); - await filterBar.removeFilter('response.raw'); - await PageObjects.visChart.waitForVisualization(); - }); + it('should correctly filter by legend', async () => { + await PageObjects.visChart.filterLegend('200'); + await PageObjects.visChart.waitForVisualization(); + const legendEntries = await PageObjects.visChart.getLegendEntries(); + const expectedEntries = ['200']; + expect(legendEntries).to.eql(expectedEntries); + await filterBar.removeFilter('response.raw'); + await PageObjects.visChart.waitForVisualization(); }); + }); - describe('vertical bar with multiple splits', function () { - before(initBarChart); - - it('should show correct series', async function () { - await PageObjects.visEditor.toggleOpenEditor(2, 'false'); - await PageObjects.visEditor.clickBucket('Split series'); - await PageObjects.visEditor.selectAggregation('Terms'); - await PageObjects.visEditor.selectField('response.raw'); - await PageObjects.visChart.waitForVisualizationRenderingStabilized(); - - await PageObjects.visEditor.toggleOpenEditor(3, 'false'); - await PageObjects.visEditor.clickBucket('Split series'); - await PageObjects.visEditor.selectAggregation('Terms'); - await PageObjects.visEditor.selectField('machine.os'); - await PageObjects.visChart.waitForVisualizationRenderingStabilized(); - await PageObjects.visEditor.clickGo(); - - const expectedEntries = [ - '200 - win 8', - '200 - win xp', - '200 - ios', - '200 - osx', - '200 - win 7', - '404 - ios', - '503 - ios', - '503 - osx', - '503 - win 7', - '503 - win 8', - '503 - win xp', - '404 - osx', - '404 - win 7', - '404 - win 8', - '404 - win xp', - ]; - const legendEntries = await PageObjects.visChart.getLegendEntries(); - expect(legendEntries).to.eql(expectedEntries); - }); - - it('should show correct series when disabling first agg', async function () { - // this will avoid issues with the play tooltip covering the disable agg button - await testSubjects.scrollIntoView('metricsAggGroup'); - await PageObjects.visEditor.toggleDisabledAgg(3); - await PageObjects.visEditor.clickGo(); - - const expectedEntries = ['win 8', 'win xp', 'ios', 'osx', 'win 7']; - const legendEntries = await PageObjects.visChart.getLegendEntries(); - expect(legendEntries).to.eql(expectedEntries); - }); + describe('vertical bar with multiple splits', function () { + before(initBarChart); + + it('should show correct series', async function () { + await PageObjects.visEditor.toggleOpenEditor(2, 'false'); + await PageObjects.visEditor.clickBucket('Split series'); + await PageObjects.visEditor.selectAggregation('Terms'); + await PageObjects.visEditor.selectField('response.raw'); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); + + await PageObjects.visEditor.toggleOpenEditor(3, 'false'); + await PageObjects.visEditor.clickBucket('Split series'); + await PageObjects.visEditor.selectAggregation('Terms'); + await PageObjects.visEditor.selectField('machine.os'); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); + await PageObjects.visEditor.clickGo(); + + const expectedEntries = [ + '200 - win 8', + '200 - win xp', + '200 - ios', + '200 - osx', + '200 - win 7', + '404 - ios', + '503 - ios', + '503 - osx', + '503 - win 7', + '503 - win 8', + '503 - win xp', + '404 - osx', + '404 - win 7', + '404 - win 8', + '404 - win xp', + ]; + const legendEntries = await PageObjects.visChart.getLegendEntries(); + expect(legendEntries).to.eql(expectedEntries); + }); + + it('should show correct series when disabling first agg', async function () { + // this will avoid issues with the play tooltip covering the disable agg button + await testSubjects.scrollIntoView('metricsAggGroup'); + await PageObjects.visEditor.toggleDisabledAgg(3); + await PageObjects.visEditor.clickGo(); + + const expectedEntries = ['win 8', 'win xp', 'ios', 'osx', 'win 7']; + const legendEntries = await PageObjects.visChart.getLegendEntries(); + expect(legendEntries).to.eql(expectedEntries); }); + }); + + describe('vertical bar with derivative', function () { + before(initBarChart); + + it('should show correct series', async function () { + await PageObjects.visEditor.toggleOpenEditor(2, 'false'); + await PageObjects.visEditor.toggleOpenEditor(1); + await PageObjects.visEditor.selectAggregation('Derivative', 'metrics'); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); + await PageObjects.visEditor.clickGo(); + + const expectedEntries = ['Derivative of Count']; + const legendEntries = await PageObjects.visChart.getLegendEntries(); + expect(legendEntries).to.eql(expectedEntries); + }); + + it('should show an error if last bucket aggregation is terms', async () => { + await PageObjects.visEditor.toggleOpenEditor(2, 'false'); + await PageObjects.visEditor.clickBucket('Split series'); + await PageObjects.visEditor.selectAggregation('Terms'); + await PageObjects.visEditor.selectField('response.raw'); - describe('vertical bar with derivative', function () { - before(initBarChart); - - it('should show correct series', async function () { - await PageObjects.visEditor.toggleOpenEditor(2, 'false'); - await PageObjects.visEditor.toggleOpenEditor(1); - await PageObjects.visEditor.selectAggregation('Derivative', 'metrics'); - await PageObjects.visChart.waitForVisualizationRenderingStabilized(); - await PageObjects.visEditor.clickGo(); - - const expectedEntries = ['Derivative of Count']; - const legendEntries = await PageObjects.visChart.getLegendEntries(); - expect(legendEntries).to.eql(expectedEntries); - }); - - it('should show an error if last bucket aggregation is terms', async () => { - await PageObjects.visEditor.toggleOpenEditor(2, 'false'); - await PageObjects.visEditor.clickBucket('Split series'); - await PageObjects.visEditor.selectAggregation('Terms'); - await PageObjects.visEditor.selectField('response.raw'); - - const errorMessage = await PageObjects.visEditor.getBucketErrorMessage(); - expect(errorMessage).to.contain('Last bucket aggregation must be "Date Histogram"'); - }); + const errorMessage = await PageObjects.visEditor.getBucketErrorMessage(); + expect(errorMessage).to.contain('Last bucket aggregation must be "Date Histogram"'); }); }); }); diff --git a/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.js b/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.js index 42dfd791321a1..f95781c9bbb33 100644 --- a/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.js +++ b/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.js @@ -25,8 +25,7 @@ export default function ({ getService, getPageObjects }) { const inspector = getService('inspector'); const PageObjects = getPageObjects(['common', 'visualize', 'header', 'visEditor', 'visChart']); - // FLAKY: https://github.com/elastic/kibana/issues/22322 - describe.skip('vertical bar chart with index without time filter', function () { + describe('vertical bar chart with index without time filter', function () { const vizName1 = 'Visualization VerticalBarChart without time filter'; const initBarChart = async () => { @@ -46,6 +45,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visEditor.selectField('@timestamp'); await PageObjects.visEditor.setInterval('3h', { type: 'custom' }); await PageObjects.visChart.waitForVisualizationRenderingStabilized(); + await PageObjects.visEditor.clickGo(); }; before(initBarChart); @@ -129,7 +129,7 @@ export default function ({ getService, getPageObjects }) { await inspector.expectTableData(expectedChartData); }); - describe.skip('switch between Y axis scale types', () => { + describe('switch between Y axis scale types', () => { before(initBarChart); const axisId = 'ValueAxis-1'; @@ -139,57 +139,25 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visEditor.selectYAxisScaleType(axisId, 'log'); await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = [ - '2', - '3', - '5', - '7', - '10', - '20', - '30', - '50', - '70', - '100', - '200', - '300', - '500', - '700', - '1,000', - '2,000', - '3,000', - '5,000', - '7,000', - ]; - expect(labels).to.eql(expectedLabels); + const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(); + const minLabel = 2; + const maxLabel = 5000; + const numberOfLabels = 10; + expect(labels.length).to.be.greaterThan(numberOfLabels); + expect(labels[0]).to.eql(minLabel); + expect(labels[labels.length - 1]).to.be.greaterThan(maxLabel); }); it('should show filtered ticks on selecting log scale', async () => { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = [ - '2', - '3', - '5', - '7', - '10', - '20', - '30', - '50', - '70', - '100', - '200', - '300', - '500', - '700', - '1,000', - '2,000', - '3,000', - '5,000', - '7,000', - ]; - expect(labels).to.eql(expectedLabels); + const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(); + const minLabel = 2; + const maxLabel = 5000; + const numberOfLabels = 10; + expect(labels.length).to.be.greaterThan(numberOfLabels); + expect(labels[0]).to.eql(minLabel); + expect(labels[labels.length - 1]).to.be.greaterThan(maxLabel); }); it('should show ticks on selecting square root scale', async () => { diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 8c5a99204bab6..d6a96eb651d02 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -407,7 +407,11 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo async closeToastIfExists() { const toastShown = await find.existsByCssSelector('.euiToast'); if (toastShown) { - await find.clickByCssSelector('.euiToast__closeButton'); + try { + await find.clickByCssSelector('.euiToast__closeButton'); + } catch (err) { + // ignore errors, toast clear themselves after timeout + } } } diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index 7c325ba6d4aec..b662fd62e4b02 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -294,6 +294,7 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide await this.enterDashboardTitleAndClickSave(dashboardName, saveOptions); if (saveOptions.needsConfirm) { + await this.ensureDuplicateTitleCallout(); await this.clickSave(); } diff --git a/test/functional/page_objects/visualize_chart_page.ts b/test/functional/page_objects/visualize_chart_page.ts index 590631ad48b00..ade78cbb810d5 100644 --- a/test/functional/page_objects/visualize_chart_page.ts +++ b/test/functional/page_objects/visualize_chart_page.ts @@ -51,6 +51,10 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr .map((tick) => $(tick).text().trim()); } + public async getYAxisLabelsAsNumbers() { + return (await this.getYAxisLabels()).map((label) => Number(label.replace(',', ''))); + } + /** * Gets the chart data and scales it based on chart height and label. * @param dataLabel data-label value diff --git a/vars/githubPr.groovy b/vars/githubPr.groovy index 9dad2f88de2ec..da5348749f668 100644 --- a/vars/githubPr.groovy +++ b/vars/githubPr.groovy @@ -15,25 +15,43 @@ */ def withDefaultPrComments(closure) { catchErrors { + // sendCommentOnError() needs to know if comments are enabled, so lets track it with a global + // isPr() just ensures this functionality is skipped for non-PR builds + buildState.set('PR_COMMENTS_ENABLED', isPr()) catchErrors { closure() } + sendComment(true) + } +} - if (!params.ENABLE_GITHUB_PR_COMMENTS || !isPr()) { - return - } +def sendComment(isFinal = false) { + if (!buildState.get('PR_COMMENTS_ENABLED')) { + return + } - def status = buildUtils.getBuildStatus() - if (status == "ABORTED") { - return; - } + def status = buildUtils.getBuildStatus() + if (status == "ABORTED") { + return + } + + def lastComment = getLatestBuildComment() + def info = getLatestBuildInfo(lastComment) ?: [:] + info.builds = (info.builds ?: []).takeRight(5) // Rotate out old builds + + // If two builds are running at the same time, the first one should not post a comment after the second one + if (info.number && info.number.toInteger() > env.BUILD_NUMBER.toInteger()) { + return + } + + def shouldUpdateComment = !!info.builds.find { it.number == env.BUILD_NUMBER } - def lastComment = getLatestBuildComment() - def info = getLatestBuildInfo(lastComment) ?: [:] - info.builds = (info.builds ?: []).takeRight(5) // Rotate out old builds + def message = getNextCommentMessage(info, isFinal) - def message = getNextCommentMessage(info) - postComment(message) + if (shouldUpdateComment) { + updateComment(lastComment.id, message) + } else { + createComment(message) if (lastComment && lastComment.user.login == 'kibanamachine') { deleteComment(lastComment.id) @@ -41,6 +59,19 @@ def withDefaultPrComments(closure) { } } +def sendCommentOnError(Closure closure) { + try { + closure() + } catch (ex) { + // If this is the first failed step, it's likely that the error hasn't propagated up far enough to mark the build as a failure + currentBuild.result = 'FAILURE' + catchErrors { + sendComment(false) + } + throw ex + } +} + // Checks whether or not this currently executing build was triggered via a PR in the elastic/kibana repo def isPr() { return !!(env.ghprbPullId && env.ghprbPullLink && env.ghprbPullLink =~ /\/elastic\/kibana\//) @@ -66,7 +97,7 @@ def getLatestBuildInfo() { } def getLatestBuildInfo(comment) { - return comment ? getBuildInfoFromComment(comment) : null + return comment ? getBuildInfoFromComment(comment.body) : null } def createBuildInfo() { @@ -137,14 +168,25 @@ def getTestFailuresMessage() { return messages.join("\n") } -def getNextCommentMessage(previousCommentInfo = [:]) { +def getNextCommentMessage(previousCommentInfo = [:], isFinal = false) { def info = previousCommentInfo ?: [:] info.builds = previousCommentInfo.builds ?: [] + // When we update an in-progress comment, we need to remove the old version from the history + info.builds = info.builds.findAll { it.number != env.BUILD_NUMBER } + def messages = [] def status = buildUtils.getBuildStatus() - if (status == 'SUCCESS') { + if (!isFinal) { + def failuresPart = status != 'SUCCESS' ? ', with failures' : '' + messages << """ + ## :hourglass_flowing_sand: Build in-progress${failuresPart} + * [continuous-integration/kibana-ci/pull-request](${env.BUILD_URL}) + * Commit: ${getCommitHash()} + * This comment will update when the build is complete + """ + } else if (status == 'SUCCESS') { messages << """ ## :green_heart: Build Succeeded * [continuous-integration/kibana-ci/pull-request](${env.BUILD_URL}) @@ -172,7 +214,9 @@ def getNextCommentMessage(previousCommentInfo = [:]) { * [Pipeline Steps](${env.BUILD_URL}flowGraphTable) (look for red circles / failed steps) * [Interpreting CI Failures](https://www.elastic.co/guide/en/kibana/current/interpreting-ci-failures.html) """ + } + if (status != 'SUCCESS' && status != 'UNSTABLE') { try { def steps = getFailedSteps() if (steps?.size() > 0) { @@ -186,7 +230,10 @@ def getNextCommentMessage(previousCommentInfo = [:]) { } messages << getTestFailuresMessage() - messages << ciStats.getMetricsReport() + + if (isFinal) { + messages << ciStats.getMetricsReport() + } if (info.builds && info.builds.size() > 0) { messages << getHistoryText(info.builds) @@ -208,7 +255,7 @@ def getNextCommentMessage(previousCommentInfo = [:]) { .join("\n\n") } -def postComment(message) { +def createComment(message) { if (!isPr()) { error "Trying to post a GitHub PR comment on a non-PR or non-elastic PR build" } @@ -224,6 +271,20 @@ def getComments() { } } +def updateComment(commentId, message) { + if (!isPr()) { + error "Trying to post a GitHub PR comment on a non-PR or non-elastic PR build" + } + + withGithubCredentials { + def path = "repos/elastic/kibana/issues/comments/${commentId}" + def json = toJSON([ body: message ]).toString() + + def resp = githubApi([ path: path ], [ method: "POST", data: json, headers: [ "X-HTTP-Method-Override": "PATCH" ] ]) + return toJSON(resp) + } +} + def deleteComment(commentId) { withGithubCredentials { def path = "repos/elastic/kibana/issues/comments/${commentId}" diff --git a/vars/jenkinsApi.groovy b/vars/jenkinsApi.groovy index 1ea4c3dd76b8d..57818593ffeb2 100644 --- a/vars/jenkinsApi.groovy +++ b/vars/jenkinsApi.groovy @@ -10,7 +10,7 @@ def getSteps() { def getFailedSteps() { def steps = getSteps() - def failedSteps = steps?.findAll { it.iconColor == "red" && it._class == "org.jenkinsci.plugins.workflow.cps.nodes.StepAtomNode" } + def failedSteps = steps?.findAll { (it.iconColor == "red" || it.iconColor == "red_anime") && it._class == "org.jenkinsci.plugins.workflow.cps.nodes.StepAtomNode" } failedSteps.each { step -> step.logs = "${env.BUILD_URL}execution/node/${step.id}/log".toString() } diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index f43fe9f96c3ef..410578886a01d 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -35,7 +35,9 @@ def functionalTestProcess(String name, Closure closure) { "JOB=${name}", "KBN_NP_PLUGINS_BUILT=true", ]) { - closure() + githubPr.sendCommentOnError { + closure() + } } } } @@ -180,26 +182,32 @@ def bash(script, label) { } def doSetup() { - retryWithDelay(2, 15) { - try { - runbld("./test/scripts/jenkins_setup.sh", "Setup Build Environment and Dependencies") - } catch (ex) { + githubPr.sendCommentOnError { + retryWithDelay(2, 15) { try { - // Setup expects this directory to be missing, so we need to remove it before we do a retry - bash("rm -rf ../elasticsearch", "Remove elasticsearch sibling directory, if it exists") - } finally { - throw ex + runbld("./test/scripts/jenkins_setup.sh", "Setup Build Environment and Dependencies") + } catch (ex) { + try { + // Setup expects this directory to be missing, so we need to remove it before we do a retry + bash("rm -rf ../elasticsearch", "Remove elasticsearch sibling directory, if it exists") + } finally { + throw ex + } } } } } def buildOss() { - runbld("./test/scripts/jenkins_build_kibana.sh", "Build OSS/Default Kibana") + githubPr.sendCommentOnError { + runbld("./test/scripts/jenkins_build_kibana.sh", "Build OSS/Default Kibana") + } } def buildXpack() { - runbld("./test/scripts/jenkins_xpack_build_kibana.sh", "Build X-Pack Kibana") + githubPr.sendCommentOnError { + runbld("./test/scripts/jenkins_xpack_build_kibana.sh", "Build X-Pack Kibana") + } } def runErrorReporter() { diff --git a/vars/workers.groovy b/vars/workers.groovy index 8b7e8525a7ce3..74ce86516e863 100644 --- a/vars/workers.groovy +++ b/vars/workers.groovy @@ -126,7 +126,9 @@ def intake(jobName, String script) { return { ci(name: jobName, size: 's-highmem', ramDisk: true) { withEnv(["JOB=${jobName}"]) { - runbld(script, "Execute ${jobName}") + githubPr.sendCommentOnError { + runbld(script, "Execute ${jobName}") + } } } } diff --git a/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts b/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts index 73cc2d273ec69..fc7445ab4a225 100644 --- a/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts +++ b/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts @@ -3,6 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { PROCESSOR_EVENT } from '../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../common/processor_event'; import { Setup } from '../helpers/setup_request'; export async function hasData({ setup }: { setup: Setup }) { @@ -15,7 +17,24 @@ export async function hasData({ setup }: { setup: Setup }) { indices['apm_oss.metricsIndices'], ], terminateAfter: 1, - size: 0, + body: { + size: 0, + query: { + bool: { + filter: [ + { + terms: { + [PROCESSOR_EVENT]: [ + ProcessorEvent.error, + ProcessorEvent.metric, + ProcessorEvent.transaction, + ], + }, + }, + ], + }, + }, + }, }; const response = await client.search(params); diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts index e3d688b694380..b4d98ec41fc2d 100644 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts @@ -12,6 +12,7 @@ import { TRANSACTION_TYPE, USER_AGENT_NAME, TRANSACTION_DURATION, + TRANSACTION_NAME, } from '../../../../common/elasticsearch_fieldnames'; import { rangeFilter } from '../../../../common/utils/range_filter'; import { getBucketSize } from '../../helpers/get_bucket_size'; @@ -23,15 +24,20 @@ export type ESResponse = PromiseReturnType; export function fetcher(options: Options) { const { end, client, indices, start, uiFiltersES } = options.setup; - const { serviceName } = options; + const { serviceName, transactionName } = options; const { intervalString } = getBucketSize(start, end, 'auto'); + const transactionNameFilter = transactionName + ? [{ term: { [TRANSACTION_NAME]: transactionName } }] + : []; + const filter: ESFilter[] = [ { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { term: { [SERVICE_NAME]: serviceName } }, { term: { [TRANSACTION_TYPE]: TRANSACTION_PAGE_LOAD } }, { range: rangeFilter(start, end) }, ...uiFiltersES, + ...transactionNameFilter, ]; const params = { diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.ts index 000890f52ebe6..e3a0d9e26142a 100644 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.ts @@ -16,6 +16,7 @@ import { transformer } from './transformer'; export interface Options { serviceName: string; setup: Setup & SetupTimeRange & SetupUIFilters; + transactionName?: string; } export type AvgDurationByBrowserAPIResponse = Array<{ diff --git a/x-pack/plugins/apm/server/routes/transaction_groups.ts b/x-pack/plugins/apm/server/routes/transaction_groups.ts index 813d757c7c33e..c667ce4f07e93 100644 --- a/x-pack/plugins/apm/server/routes/transaction_groups.ts +++ b/x-pack/plugins/apm/server/routes/transaction_groups.ts @@ -164,7 +164,6 @@ export const transactionGroupsAvgDurationByBrowser = createRoute(() => ({ }), query: t.intersection([ t.partial({ - transactionType: t.string, transactionName: t.string, }), uiFiltersRt, @@ -174,10 +173,12 @@ export const transactionGroupsAvgDurationByBrowser = createRoute(() => ({ handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName } = context.params.path; + const { transactionName } = context.params.query; return getTransactionAvgDurationByBrowser({ serviceName, setup, + transactionName, }); }, })); diff --git a/x-pack/plugins/canvas/CONTRIBUTING.md b/x-pack/plugins/canvas/CONTRIBUTING.md new file mode 100644 index 0000000000000..538cc1592c3bc --- /dev/null +++ b/x-pack/plugins/canvas/CONTRIBUTING.md @@ -0,0 +1,139 @@ +# Contributing to Canvas + +Canvas is a plugin for Kibana, therefore its [contribution guidelines](../../../CONTRIBUTING.md) apply to Canvas development, as well. This document contains Canvas-specific guidelines that extend from the Kibana guidelines. + +- [Active Migrations](#active_migrations) +- [i18n](#i18n) +- [Component Code Structure](#component_code_structure) +- [Storybook](#storybook) + +## Active Migrations + +When editing code in Canvas, be aware of the following active migrations, (generally, taken when a file is touched): + +- Convert file(s) to Typescript. +- Convert React classes to Functional components, (where applicable). +- Add Storybook stories for components, (and thus Storyshots). +- Remove `recompose` in favor of React hooks. +- Apply improved component structure. +- Write tests. + +## i18n + +i18n syntax in Kibana can be a bit verbose in code: + +```js + i18n('pluginNamespace.messageId', { + defaultMessage: 'Default message string literal, {key}', + values: { + key: 'value', + }, + description: 'Message context or description', + }); +``` + +To keep the code terse, Canvas uses i18n "dictionaries": abstracted, static singletons of accessor methods which return a given string: + +```js + +// i18n/components.ts +export const ComponentStrings = { + // ... + AssetManager: { + getCopyAssetMessage: (id: string) => + i18n.translate('xpack.canvas.assetModal.copyAssetMessage', { + defaultMessage: `Copied '{id}' to clipboard`, + values: { + id, + }, + }), + // ... + }, + // ... +}; + +// asset_manager.tsx +import { ComponentStrings } from '../../../i18n'; +const { AssetManager: strings } = ComponentStrings; + +const text = ( + + {strings.getSpaceUsedText(percentageUsed)} + +); + +``` + +These singletons can then be changed at will, as well as audited for unused methods, (and therefore unused i18n strings). + +## Component Code Structure + +Canvas uses Redux. Component code is divided into React components and Redux containers. This way, components can be reused, their containers can be edited, and both can be tested independently. + +Canvas is actively migrating to a structure which uses the `index.ts` file as a thin exporting index, rather than functional code: + +``` +- components + - foo <- directory representing the component + - foo.ts <- redux container + - foo.component.tsx <- react component + - foo.scss + - index.ts <- thin exporting index, (no redux) + - bar <- directory representing the component + - bar.ts + - bar.component.tsx + - bar.scss + - bar_dep.ts <- redux sub container + - bar_dep.component.tsx <- sub component + - index.ts +``` + +The exporting file would be: + +``` +export { Foo } from './foo'; +export { Foo as FooComponent } from './foo.component'; +``` + +### Why? + +Canvas has been using an "index-export" structure that has served it well, until recently. In this structure, the `index.ts` file serves as the primary export of the Redux component, (and holds that code). The component is then named-- `component.tsx`-- and consumed in the `index` file. + +The problem we've run into is when you have sub-components which are also connected to Redux. To maintain this structure, each sub-component and its Redux container would then be stored in a subdirectory, (with only two files in it). + +> NOTE: if a PR touches component code that is in the older structure, it should be migrated to the structure above. + +## Storybook + +Canvas uses [Storybook](https://storybook.js.org) to test and develop components. This has a number of benefits: + +- Developing components without needing to start ES + Kibana. +- Testing components interactively without starting ES + Kibana. +- Automatic Storyshot integration with Jest + +### Using Storybook + +The Canvas Storybook instance can be started by running `node scripts/storybook` from the Canvas root directory. It has a number of options: + +``` +node scripts/storybook + + Storybook runner for Canvas. + + Options: + --clean Forces a clean of the Storybook DLL and exits. + --dll Cleans and builds the Storybook dependency DLL and exits. + --stats Produces a Webpack stats file. + --site Produces a site deployment of this Storybook. + --verbose, -v Log verbosely + --debug Log debug messages (less than verbose) + --quiet Only log errors + --silent Don't log anything + --help Show this message +``` + +### What about `kbn-storybook`? + +Canvas wants to move to the Kibana Storybook instance as soon as feasible. There are few tweaks Canvas makes to Storybook, so we're actively working with the maintainers to make that migration successful. + +In the meantime, people can test our progress by running `node scripts/storybook_new` from the Canvas root. diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/index.ts index 1d1df54d62f70..479770a9d0a81 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/index.ts @@ -5,8 +5,9 @@ */ import { functions as commonFunctions } from '../common'; +import { functions as externalFunctions } from '../external'; import { location } from './location'; import { markdown } from './markdown'; import { urlparam } from './urlparam'; -export const functions = [location, markdown, urlparam, ...commonFunctions]; +export const functions = [location, markdown, urlparam, ...commonFunctions, ...externalFunctions]; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/index.ts index 36fa6497ab6f3..79538941bbbfa 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/index.ts @@ -48,10 +48,6 @@ import { rounddate } from './rounddate'; import { rowCount } from './rowCount'; import { repeatImage } from './repeatImage'; import { revealImage } from './revealImage'; -import { savedLens } from './saved_lens'; -import { savedMap } from './saved_map'; -import { savedSearch } from './saved_search'; -import { savedVisualization } from './saved_visualization'; import { seriesStyle } from './seriesStyle'; import { shape } from './shape'; import { sort } from './sort'; @@ -110,10 +106,6 @@ export const functions = [ revealImage, rounddate, rowCount, - savedLens, - savedMap, - savedSearch, - savedVisualization, seriesStyle, shape, sort, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/index.ts new file mode 100644 index 0000000000000..17682c5a72074 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/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. + */ + +import { savedLens } from './saved_lens'; +import { savedMap } from './saved_map'; +import { savedSearch } from './saved_search'; +import { savedVisualization } from './saved_visualization'; + +export const functions = [savedLens, savedMap, savedSearch, savedVisualization]; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.test.ts similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.test.ts rename to x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.test.ts diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.ts rename to x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/saved_map.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.test.ts similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/functions/common/saved_map.test.ts rename to x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.test.ts diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts rename to x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/saved_search.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.test.ts similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/functions/common/saved_search.test.ts rename to x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.test.ts diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/saved_search.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.ts similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/functions/common/saved_search.ts rename to x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.ts diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.test.ts similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.test.ts rename to x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.test.ts diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts rename to x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/core.js b/x-pack/plugins/canvas/canvas_plugin_src/renderers/core.js new file mode 100644 index 0000000000000..ba4a564848788 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/core.js @@ -0,0 +1,37 @@ +/* + * 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 { debug } from './debug'; +import { error } from './error'; +import { image } from './image'; +import { markdown } from './markdown'; +import { metric } from './metric'; +import { pie } from './pie'; +import { plot } from './plot'; +import { progress } from './progress'; +import { repeatImage } from './repeat_image'; +import { revealImage } from './reveal_image'; +import { shape } from './shape'; +import { table } from './table'; +import { text } from './text'; + +export const renderFunctions = [ + debug, + error, + image, + markdown, + metric, + pie, + plot, + progress, + repeatImage, + revealImage, + shape, + table, + text, +]; + +export const renderFunctionFactories = []; diff --git a/x-pack/plugins/canvas/public/components/dom_preview/index.js b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/index.ts similarity index 60% rename from x-pack/plugins/canvas/public/components/dom_preview/index.js rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/index.ts index 283f92c7ecd9b..fb47d61484c47 100644 --- a/x-pack/plugins/canvas/public/components/dom_preview/index.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/index.ts @@ -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 { embeddableRendererFactory } from './embeddable'; -import { DomPreview as Component } from './dom_preview'; - -export const DomPreview = Component; +export const renderFunctions = []; +export const renderFunctionFactories = [embeddableRendererFactory]; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.test.ts index c4a9a22be3202..7bcfd6bef4620 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.test.ts @@ -5,7 +5,7 @@ */ import { toExpression } from './lens'; -import { SavedLensInput } from '../../../functions/common/saved_lens'; +import { SavedLensInput } from '../../../functions/external/saved_lens'; import { fromExpression, Ast } from '@kbn/interpreter/common'; const baseEmbeddableInput = { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.ts index 445cb7480ff80..5bb45c5ca129e 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedLensInput } from '../../../functions/common/saved_lens'; +import { SavedLensInput } from '../../../functions/external/saved_lens'; export function toExpression(input: SavedLensInput): string { const expressionParts = [] as string[]; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/advanced_filter/component/__examples__/__snapshots__/advanced_filter.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/__examples__/__snapshots__/advanced_filter.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/advanced_filter/component/__examples__/__snapshots__/advanced_filter.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/__examples__/__snapshots__/advanced_filter.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/advanced_filter/component/__examples__/advanced_filter.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/__examples__/advanced_filter.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/advanced_filter/component/__examples__/advanced_filter.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/__examples__/advanced_filter.stories.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/advanced_filter/component/advanced_filter.scss b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/advanced_filter.scss similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/advanced_filter/component/advanced_filter.scss rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/advanced_filter.scss diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/advanced_filter/component/advanced_filter.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/advanced_filter.tsx similarity index 96% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/advanced_filter/component/advanced_filter.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/advanced_filter.tsx index e4d4510d40f53..8cb8b53ab6976 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/advanced_filter/component/advanced_filter.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/advanced_filter.tsx @@ -7,7 +7,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import PropTypes from 'prop-types'; import React, { FunctionComponent } from 'react'; -import { ComponentStrings } from '../../../../i18n'; +import { ComponentStrings } from '../../../../../i18n'; const { AdvancedFilter: strings } = ComponentStrings; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/advanced_filter/component/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/index.ts similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/advanced_filter/component/index.ts rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/index.ts diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/advanced_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/index.tsx similarity index 89% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/advanced_filter/index.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/index.tsx index 29692b0c02a41..b481a4b498928 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/advanced_filter/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/index.tsx @@ -6,9 +6,9 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { RendererFactory } from '../../../types'; +import { RendererFactory } from '../../../../types'; import { AdvancedFilter } from './component'; -import { RendererStrings } from '../../../i18n'; +import { RendererStrings } from '../../../../i18n'; const { advancedFilter: strings } = RendererStrings; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/component/__examples__/__snapshots__/dropdown_filter.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__examples__/__snapshots__/dropdown_filter.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/component/__examples__/__snapshots__/dropdown_filter.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__examples__/__snapshots__/dropdown_filter.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/component/__examples__/dropdown_filter.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__examples__/dropdown_filter.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/component/__examples__/dropdown_filter.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__examples__/dropdown_filter.stories.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/component/dropdown_filter.scss b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.scss similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/component/dropdown_filter.scss rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.scss diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/component/dropdown_filter.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.tsx similarity index 97% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/component/dropdown_filter.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.tsx index 9cade90bd5870..ac08d3dd25d85 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/component/dropdown_filter.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.tsx @@ -7,7 +7,7 @@ import { EuiIcon } from '@elastic/eui'; import PropTypes from 'prop-types'; import React, { ChangeEvent, FocusEvent, FunctionComponent } from 'react'; -import { ComponentStrings } from '../../../../i18n'; +import { ComponentStrings } from '../../../../../i18n'; const { DropdownFilter: strings } = ComponentStrings; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/component/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/index.ts similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/component/index.ts rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/index.ts diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx similarity index 93% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/index.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx index ee07f7d4402f4..bfc36932a8a07 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx @@ -8,10 +8,10 @@ import { fromExpression, toExpression, Ast } from '@kbn/interpreter/common'; import { get } from 'lodash'; import React from 'react'; import ReactDOM from 'react-dom'; -import { syncFilterExpression } from '../../../public/lib/sync_filter_expression'; -import { RendererFactory } from '../../../types'; +import { syncFilterExpression } from '../../../../public/lib/sync_filter_expression'; +import { RendererFactory } from '../../../../types'; import { DropdownFilter } from './component'; -import { RendererStrings } from '../../../i18n'; +import { RendererStrings } from '../../../../i18n'; const { dropdownFilter: strings } = RendererStrings; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/index.js b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/index.js new file mode 100644 index 0000000000000..0654bf0f704e9 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/index.js @@ -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. + */ + +import { advancedFilter } from './advanced_filter'; +import { dropdownFilter } from './dropdown_filter'; +import { timeFilterFactory } from './time_filter'; + +export const renderFunctions = [advancedFilter, dropdownFilter]; +export const renderFunctionFactories = [timeFilterFactory]; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/__examples__/__snapshots__/time_filter.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__examples__/__snapshots__/time_filter.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/__examples__/__snapshots__/time_filter.stories.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__examples__/__snapshots__/time_filter.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/__examples__/time_filter.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__examples__/time_filter.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/__examples__/time_filter.stories.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/__examples__/time_filter.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/page_preview/index.js b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/index.tsx similarity index 84% rename from x-pack/plugins/canvas/public/components/page_preview/index.js rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/index.tsx index d72d6403dd5be..cdea7d6591592 100644 --- a/x-pack/plugins/canvas/public/components/page_preview/index.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/index.tsx @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { PagePreview } from './page_preview'; +export { TimeFilter } from './time_filter'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/time_filter.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/time_filter.tsx similarity index 98% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/time_filter.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/time_filter.tsx index 487f17fb89d12..3cb192cedde83 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/time_filter.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/components/time_filter.tsx @@ -8,7 +8,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { get } from 'lodash'; import { fromExpression } from '@kbn/interpreter/common'; -import { UnitStrings } from '../../../../i18n/units'; +import { UnitStrings } from '../../../../../i18n/units'; const { quickRanges: strings } = UnitStrings; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx similarity index 82% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx index 25278adcf4529..03bf18830a761 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx @@ -7,14 +7,14 @@ import ReactDOM from 'react-dom'; import React from 'react'; import { toExpression } from '@kbn/interpreter/common'; -import { UI_SETTINGS } from '../../../../../../src/plugins/data/public'; -import { syncFilterExpression } from '../../../public/lib/sync_filter_expression'; -import { RendererStrings } from '../../../i18n'; +import { UI_SETTINGS } from '../../../../../../../src/plugins/data/public'; +import { syncFilterExpression } from '../../../../public/lib/sync_filter_expression'; +import { RendererStrings } from '../../../../i18n'; import { TimeFilter } from './components'; -import { StartInitializer } from '../../plugin'; -import { RendererHandlers } from '../../../types'; -import { Arguments } from '../../functions/common/timefilterControl'; -import { RendererFactory } from '../../../types'; +import { StartInitializer } from '../../../plugin'; +import { RendererHandlers } from '../../../../types'; +import { Arguments } from '../../../functions/common/timefilterControl'; +import { RendererFactory } from '../../../../types'; const { timeFilter: strings } = RendererStrings; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/time_filter.scss b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/time_filter.scss similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/time_filter.scss rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/time_filter.scss diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/index.js b/x-pack/plugins/canvas/canvas_plugin_src/renderers/index.js index 263f2d8ec30b5..38f10afd50cf9 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/index.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/index.js @@ -4,40 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { advancedFilter } from './advanced_filter'; -import { debug } from './debug'; -import { dropdownFilter } from './dropdown_filter'; -import { embeddableRendererFactory } from './embeddable/embeddable'; -import { error } from './error'; -import { image } from './image'; -import { markdown } from './markdown'; -import { metric } from './metric'; -import { pie } from './pie'; -import { plot } from './plot'; -import { progress } from './progress'; -import { repeatImage } from './repeat_image'; -import { revealImage } from './reveal_image'; -import { shape } from './shape'; -import { table } from './table'; -import { text } from './text'; -import { timeFilterFactory } from './time_filter'; +import { + renderFunctions as embeddableFunctions, + renderFunctionFactories as embeddableFactories, +} from './embeddable'; -export const renderFunctions = [ - advancedFilter, - debug, - dropdownFilter, - error, - image, - markdown, - metric, - pie, - plot, - progress, - repeatImage, - revealImage, - shape, - table, - text, -]; +import { + renderFunctions as filterFunctions, + renderFunctionFactories as filterFactories, +} from './filters'; + +import { renderFunctions as coreFunctions, renderFunctionFactories as coreFactories } from './core'; -export const renderFunctionFactories = [embeddableRendererFactory, timeFilterFactory]; +export const renderFunctions = [...coreFunctions, ...filterFunctions, ...embeddableFunctions]; +export const renderFunctionFactories = [ + ...coreFactories, + ...embeddableFactories, + ...filterFactories, +]; diff --git a/x-pack/plugins/canvas/i18n/components.ts b/x-pack/plugins/canvas/i18n/components.ts index 78083f26a38b1..9b1d60f38eb5e 100644 --- a/x-pack/plugins/canvas/i18n/components.ts +++ b/x-pack/plugins/canvas/i18n/components.ts @@ -110,26 +110,24 @@ export const ComponentStrings = { i18n.translate('xpack.canvas.asset.thumbnailAltText', { defaultMessage: 'Asset thumbnail', }), - }, - AssetManager: { - getButtonLabel: () => - i18n.translate('xpack.canvas.assetManager.manageButtonLabel', { - defaultMessage: 'Manage assets', - }), getConfirmModalButtonLabel: () => - i18n.translate('xpack.canvas.assetManager.confirmModalButtonLabel', { + i18n.translate('xpack.canvas.asset.confirmModalButtonLabel', { defaultMessage: 'Remove', }), getConfirmModalMessageText: () => - i18n.translate('xpack.canvas.assetManager.confirmModalDetail', { + i18n.translate('xpack.canvas.asset.confirmModalDetail', { defaultMessage: 'Are you sure you want to remove this asset?', }), getConfirmModalTitle: () => - i18n.translate('xpack.canvas.assetManager.confirmModalTitle', { + i18n.translate('xpack.canvas.asset.confirmModalTitle', { defaultMessage: 'Remove Asset', }), }, - AssetModal: { + AssetManager: { + getButtonLabel: () => + i18n.translate('xpack.canvas.assetManager.manageButtonLabel', { + defaultMessage: 'Manage assets', + }), getDescription: () => i18n.translate('xpack.canvas.assetModal.modalDescription', { defaultMessage: @@ -162,6 +160,13 @@ export const ComponentStrings = { percentageUsed, }, }), + getCopyAssetMessage: (id: string) => + i18n.translate('xpack.canvas.assetModal.copyAssetMessage', { + defaultMessage: `Copied '{id}' to clipboard`, + values: { + id, + }, + }), }, AssetPicker: { getAssetAltText: () => @@ -567,6 +572,22 @@ export const ComponentStrings = { pageNumber, }, }), + getAddPageTooltip: () => + i18n.translate('xpack.canvas.pageManager.addPageTooltip', { + defaultMessage: 'Add a new page to this workpad', + }), + getConfirmRemoveTitle: () => + i18n.translate('xpack.canvas.pageManager.confirmRemoveTitle', { + defaultMessage: 'Remove Page', + }), + getConfirmRemoveDescription: () => + i18n.translate('xpack.canvas.pageManager.confirmRemoveDescription', { + defaultMessage: 'Are you sure you want to remove this page?', + }), + getConfirmRemoveButtonLabel: () => + i18n.translate('xpack.canvas.pageManager.removeButtonLabel', { + defaultMessage: 'Remove', + }), }, PagePreviewPageControls: { getClonePageAriaLabel: () => diff --git a/x-pack/plugins/canvas/i18n/functions/dict/saved_lens.ts b/x-pack/plugins/canvas/i18n/functions/dict/saved_lens.ts index 1efcbc9d3a18e..e146a6ca68449 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/saved_lens.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/saved_lens.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { savedLens } from '../../../canvas_plugin_src/functions/common/saved_lens'; +import { savedLens } from '../../../canvas_plugin_src/functions/external/saved_lens'; import { FunctionHelp } from '../function_help'; import { FunctionFactory } from '../../../types'; diff --git a/x-pack/plugins/canvas/i18n/functions/dict/saved_map.ts b/x-pack/plugins/canvas/i18n/functions/dict/saved_map.ts index 53bcd481f185f..8615565897434 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/saved_map.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/saved_map.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { savedMap } from '../../../canvas_plugin_src/functions/common/saved_map'; +import { savedMap } from '../../../canvas_plugin_src/functions/external/saved_map'; import { FunctionHelp } from '../function_help'; import { FunctionFactory } from '../../../types'; diff --git a/x-pack/plugins/canvas/i18n/functions/dict/saved_search.ts b/x-pack/plugins/canvas/i18n/functions/dict/saved_search.ts index 718deea5df788..865a7753d9818 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/saved_search.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/saved_search.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { savedSearch } from '../../../canvas_plugin_src/functions/common/saved_search'; +import { savedSearch } from '../../../canvas_plugin_src/functions/external/saved_search'; import { FunctionHelp } from '../function_help'; import { FunctionFactory } from '../../../types'; diff --git a/x-pack/plugins/canvas/i18n/functions/dict/saved_visualization.ts b/x-pack/plugins/canvas/i18n/functions/dict/saved_visualization.ts index 21a2e1c1b8800..30f88b51e7576 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/saved_visualization.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/saved_visualization.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { savedVisualization } from '../../../canvas_plugin_src/functions/common/saved_visualization'; +import { savedVisualization } from '../../../canvas_plugin_src/functions/external/saved_visualization'; import { FunctionHelp } from '../function_help'; import { FunctionFactory } from '../../../types'; diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.stories.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.stories.storyshot index 14791cd3d8b25..87205b363e697 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.stories.storyshot @@ -355,3 +355,181 @@ exports[`Storyshots components/Assets/Asset marker 1`] = ` `; + +exports[`Storyshots components/Assets/Asset redux 1`] = ` +

+
+
+
+
+ Asset thumbnail +
+
+
+
+

+ + airplane + +
+ + + ( + 1 + kb) + + +

+
+
+
+
+ + + +
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ + + +
+
+
+
+
+`; diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset_manager.stories.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset_manager.stories.storyshot index 1b8f1480759f6..11c5681ebf79e 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset_manager.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset_manager.stories.storyshot @@ -229,6 +229,545 @@ Array [ ] `; +exports[`Storyshots components/Assets/AssetManager redux 1`] = ` +Array [ +
, +
, +
+
+ +
+
+
+ Manage workpad assets +
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+

+ Below are the image assets in this workpad. Any assets that are currently in use cannot be determined at this time. To reclaim space, delete assets. +

+
+
+
+
+
+
+
+
+ Asset thumbnail +
+
+
+
+

+ + airplane + +
+ + + ( + 1 + kb) + + +

+
+
+
+
+ + + +
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ + + +
+
+
+
+
+
+
+
+ Asset thumbnail +
+
+
+
+

+ + marker + +
+ + + ( + 1 + kb) + + +

+
+
+
+
+ + + +
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+ +
+
+
+ 0% space used +
+
+
+ +
+
+
+
, +
, +] +`; + exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` Array [
{story()}
) - .add('airplane', () => ( - - )) - .add('marker', () => ( - - )); diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.stories.tsx b/x-pack/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.stories.tsx index cb42823ccab7b..1434ef60cf0d8 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.stories.tsx +++ b/x-pack/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.stories.tsx @@ -7,42 +7,32 @@ import { action } from '@storybook/addon-actions'; import { storiesOf } from '@storybook/react'; import React from 'react'; -import { AssetType } from '../../../../types'; -import { AssetManager } from '../asset_manager'; -const AIRPLANE: AssetType = { - '@created': '2018-10-13T16:44:44.648Z', - id: 'airplane', - type: 'dataurl', - value: - 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1Ni4zMSA1Ni4zMSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMDc4YTA7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPlBsYW5lIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNDkuNTEsNDguOTMsNDEuMjYsMjIuNTIsNTMuNzYsMTBhNS4yOSw1LjI5LDAsMCwwLTcuNDgtNy40N2wtMTIuNSwxMi41TDcuMzgsNi43OUEuNy43LDAsMCwwLDYuNjksN0wxLjIsMTIuNDVhLjcuNywwLDAsMCwwLDFMMTkuODUsMjlsLTcuMjQsNy4yNC03Ljc0LS42YS43MS43MSwwLDAsMC0uNTMuMkwxLjIxLDM5YS42Ny42NywwLDAsMCwuMDgsMUw5LjQ1LDQ2bC4wNywwYy4xMS4xMy4yMi4yNi4zNC4zOHMuMjUuMjMuMzguMzRhLjM2LjM2LDAsMCwwLDAsLjA3TDE2LjMzLDU1YS42OC42OCwwLDAsMCwxLC4wN0wyMC40OSw1MmEuNjcuNjcsMCwwLDAsLjE5LS41NGwtLjU5LTcuNzQsNy4yNC03LjI0TDQyLjg1LDU1LjA2YS42OC42OCwwLDAsMCwxLDBsNS41LTUuNUEuNjYuNjYsMCwwLDAsNDkuNTEsNDguOTNaIi8+PC9nPjwvZz48L3N2Zz4=', -}; +import { AssetManager, AssetManagerComponent } from '../'; -const MARKER: AssetType = { - '@created': '2018-10-13T16:44:44.648Z', - id: 'marker', - type: 'dataurl', - value: - 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzOC4zOSA1Ny41NyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMTliOGY7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPkxvY2F0aW9uIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMTkuMTksMUExOC4xOSwxOC4xOSwwLDAsMCwyLjk0LDI3LjM2aDBhMTkuNTEsMTkuNTEsMCwwLDAsMSwxLjc4TDE5LjE5LDU1LjU3LDM0LjM4LDI5LjIxQTE4LjE5LDE4LjE5LDAsMCwwLDE5LjE5LDFabTAsMjMuMjlhNS41Myw1LjUzLDAsMSwxLDUuNTMtNS41M0E1LjUzLDUuNTMsMCwwLDEsMTkuMTksMjQuMjlaIi8+PC9nPjwvZz48L3N2Zz4=', -}; +import { Provider, AIRPLANE, MARKER } from './provider'; storiesOf('components/Assets/AssetManager', module) + .add('redux: AssetManager', () => ( + + + + )) .add('no assets', () => ( - + + + )) .add('two assets', () => ( - + + + )); diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/provider.tsx b/x-pack/plugins/canvas/public/components/asset_manager/__examples__/provider.tsx new file mode 100644 index 0000000000000..1cd7562b59c47 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/asset_manager/__examples__/provider.tsx @@ -0,0 +1,110 @@ +/* + * 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. + */ + +/* eslint-disable no-console */ + +/* + This Provider is temporary. See https://github.com/elastic/kibana/pull/69357 +*/ + +import React, { FC } from 'react'; +import { applyMiddleware, createStore, Dispatch, Store } from 'redux'; +import thunkMiddleware from 'redux-thunk'; +import { Provider as ReduxProvider } from 'react-redux'; + +// @ts-expect-error untyped local +import { appReady } from '../../../../public/state/middleware/app_ready'; +// @ts-expect-error untyped local +import { resolvedArgs } from '../../../../public/state/middleware/resolved_args'; + +// @ts-expect-error untyped local +import { getRootReducer } from '../../../../public/state/reducers'; + +// @ts-expect-error Untyped local +import { getDefaultWorkpad } from '../../../../public/state/defaults'; +import { State, AssetType } from '../../../../types'; + +export const AIRPLANE: AssetType = { + '@created': '2018-10-13T16:44:44.648Z', + id: 'airplane', + type: 'dataurl', + value: + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1Ni4zMSA1Ni4zMSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMDc4YTA7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPlBsYW5lIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNDkuNTEsNDguOTMsNDEuMjYsMjIuNTIsNTMuNzYsMTBhNS4yOSw1LjI5LDAsMCwwLTcuNDgtNy40N2wtMTIuNSwxMi41TDcuMzgsNi43OUEuNy43LDAsMCwwLDYuNjksN0wxLjIsMTIuNDVhLjcuNywwLDAsMCwwLDFMMTkuODUsMjlsLTcuMjQsNy4yNC03Ljc0LS42YS43MS43MSwwLDAsMC0uNTMuMkwxLjIxLDM5YS42Ny42NywwLDAsMCwuMDgsMUw5LjQ1LDQ2bC4wNywwYy4xMS4xMy4yMi4yNi4zNC4zOHMuMjUuMjMuMzguMzRhLjM2LjM2LDAsMCwwLDAsLjA3TDE2LjMzLDU1YS42OC42OCwwLDAsMCwxLC4wN0wyMC40OSw1MmEuNjcuNjcsMCwwLDAsLjE5LS41NGwtLjU5LTcuNzQsNy4yNC03LjI0TDQyLjg1LDU1LjA2YS42OC42OCwwLDAsMCwxLDBsNS41LTUuNUEuNjYuNjYsMCwwLDAsNDkuNTEsNDguOTNaIi8+PC9nPjwvZz48L3N2Zz4=', +}; + +export const MARKER: AssetType = { + '@created': '2018-10-13T16:44:44.648Z', + id: 'marker', + type: 'dataurl', + value: + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzOC4zOSA1Ny41NyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMTliOGY7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPkxvY2F0aW9uIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMTkuMTksMUExOC4xOSwxOC4xOSwwLDAsMCwyLjk0LDI3LjM2aDBhMTkuNTEsMTkuNTEsMCwwLDAsMSwxLjc4TDE5LjE5LDU1LjU3LDM0LjM4LDI5LjIxQTE4LjE5LDE4LjE5LDAsMCwwLDE5LjE5LDFabTAsMjMuMjlhNS41Myw1LjUzLDAsMSwxLDUuNTMtNS41M0E1LjUzLDUuNTMsMCwwLDEsMTkuMTksMjQuMjlaIi8+PC9nPjwvZz48L3N2Zz4=', +}; + +export const state: State = { + app: { + basePath: '/', + ready: true, + serverFunctions: [], + }, + assets: { + AIRPLANE, + MARKER, + }, + transient: { + canUserWrite: true, + zoomScale: 1, + elementStats: { + total: 0, + ready: 0, + pending: 0, + error: 0, + }, + inFlight: false, + fullScreen: false, + selectedTopLevelNodes: [], + resolvedArgs: {}, + refresh: { + interval: 0, + }, + autoplay: { + enabled: false, + interval: 10000, + }, + }, + persistent: { + schemaVersion: 2, + workpad: getDefaultWorkpad(), + }, +}; + +// @ts-expect-error untyped local +import { elementsRegistry } from '../../../lib/elements_registry'; +import { image } from '../../../../canvas_plugin_src/elements/image'; +elementsRegistry.register(image); + +export const patchDispatch: (store: Store, dispatch: Dispatch) => Dispatch = (store, dispatch) => ( + action +) => { + const previousState = store.getState(); + const returnValue = dispatch(action); + const newState = store.getState(); + + console.group(action.type || '(thunk)'); + console.log('Previous State', previousState); + console.log('New State', newState); + console.groupEnd(); + + return returnValue; +}; + +export const Provider: FC = ({ children }) => { + const middleware = applyMiddleware(thunkMiddleware); + const reducer = getRootReducer(state); + const store = createStore(reducer, state, middleware); + store.dispatch = patchDispatch(store, store.dispatch); + + return {children}; +}; diff --git a/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx b/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx new file mode 100644 index 0000000000000..a04d37cf7f9fc --- /dev/null +++ b/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx @@ -0,0 +1,147 @@ +/* + * 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, { FC, useState } from 'react'; +import { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiImage, + EuiPanel, + EuiSpacer, + EuiText, + EuiTextColor, + EuiToolTip, +} from '@elastic/eui'; + +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; + +import { ConfirmModal } from '../confirm_modal'; +import { Clipboard } from '../clipboard'; +import { Download } from '../download'; +import { AssetType } from '../../../types'; + +import { ComponentStrings } from '../../../i18n'; + +const { Asset: strings } = ComponentStrings; + +interface Props { + /** The asset to be rendered */ + asset: AssetType; + /** The function to execute when the user clicks 'Create' */ + onCreate: (assetId: string) => void; + /** The function to execute when the user clicks 'Delete' */ + onDelete: (asset: AssetType) => void; +} + +export const Asset: FC = ({ asset, onCreate, onDelete }) => { + const { services } = useKibana(); + const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false); + + const onCopy = (result: boolean) => + result && services.canvas.notify.success(`Copied '${asset.id}' to clipboard`); + + const confirmModal = ( + { + setIsConfirmModalVisible(false); + onDelete(asset); + }} + onCancel={() => setIsConfirmModalVisible(false)} + /> + ); + + const createImage = ( + + + onCreate(asset.id)} + /> + + + ); + + const downloadAsset = ( + + + + + + + + ); + + const copyAsset = ( + + + + + + + + ); + + const deleteAsset = ( + + + setIsConfirmModalVisible(true)} + /> + + + ); + + const thumbnail = ( +
+ +
+ ); + + const assetLabel = ( + +

+ {asset.id} +
+ + ({Math.round(asset.value.length / 1024)} kb) + +

+
+ ); + + return ( + + + {thumbnail} + + {assetLabel} + + + {createImage} + {downloadAsset} + {copyAsset} + {deleteAsset} + + + {isConfirmModalVisible ? confirmModal : null} + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/asset_manager/asset.tsx b/x-pack/plugins/canvas/public/components/asset_manager/asset.tsx index b0eaecc7b5203..1a3ce8419aff6 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/asset.tsx +++ b/x-pack/plugins/canvas/public/components/asset_manager/asset.tsx @@ -3,124 +3,59 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { - EuiButtonIcon, - EuiFlexGroup, - EuiFlexItem, - EuiImage, - EuiPanel, - EuiSpacer, - EuiText, - EuiTextColor, - EuiToolTip, -} from '@elastic/eui'; -import React, { FunctionComponent } from 'react'; - -import { ComponentStrings } from '../../../i18n'; - -import { Clipboard } from '../clipboard'; -import { Download } from '../download'; -import { AssetType } from '../../../types'; - -const { Asset: strings } = ComponentStrings; - -interface Props { - /** The asset to be rendered */ - asset: AssetType; - /** The function to execute when the user clicks 'Create' */ - onCreate: (asset: AssetType) => void; - /** The function to execute when the user clicks 'Copy' */ - onCopy: (asset: AssetType) => void; - /** The function to execute when the user clicks 'Delete' */ - onDelete: (asset: AssetType) => void; -} - -export const Asset: FunctionComponent = (props) => { - const { asset, onCreate, onCopy, onDelete } = props; - - const createImage = ( - - - onCreate(asset)} - /> - - - ); - - const downloadAsset = ( - - - - - - - - ); - - const copyAsset = ( - - - result && onCopy(asset)}> - - - - - ); - - const deleteAsset = ( - - - onDelete(asset)} - /> - - - ); - - const thumbnail = ( -
- -
- ); - - const assetLabel = ( - -

- {asset.id} -
- - ({Math.round(asset.value.length / 1024)} kb) - -

-
- ); - - return ( - - - {thumbnail} - - {assetLabel} - - - {createImage} - {downloadAsset} - {copyAsset} - {deleteAsset} - - - - ); -}; +import { Dispatch } from 'redux'; +import { connect } from 'react-redux'; +import { set } from '@elastic/safer-lodash-set'; + +import { fromExpression, toExpression } from '@kbn/interpreter/common'; + +// @ts-expect-error untyped local +import { elementsRegistry } from '../../lib/elements_registry'; +// @ts-expect-error untyped local +import { addElement } from '../../state/actions/elements'; +import { getSelectedPage } from '../../state/selectors/workpad'; +// @ts-expect-error untyped local +import { removeAsset } from '../../state/actions/assets'; +import { State, ExpressionAstExpression, AssetType } from '../../../types'; + +import { Asset as Component } from './asset.component'; + +export const Asset = connect( + (state: State) => ({ + selectedPage: getSelectedPage(state), + }), + (dispatch: Dispatch) => ({ + onCreate: (pageId: string) => (assetId: string) => { + const imageElement = elementsRegistry.get('image'); + const elementAST = fromExpression(imageElement.expression); + const selector = ['chain', '0', 'arguments', 'dataurl']; + const subExp: ExpressionAstExpression[] = [ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'asset', + arguments: { + _: [assetId], + }, + }, + ], + }, + ]; + const newAST = set(elementAST, selector, subExp); + imageElement.expression = toExpression(newAST); + dispatch(addElement(pageId, imageElement)); + }, + onDelete: (asset: AssetType) => dispatch(removeAsset(asset.id)), + }), + (stateProps, dispatchProps, ownProps) => { + const { onCreate, onDelete } = dispatchProps; + + return { + ...ownProps, + onCreate: onCreate(stateProps.selectedPage), + onDelete, + }; + } +)(Component); diff --git a/x-pack/plugins/canvas/public/components/asset_manager/asset_modal.tsx b/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx similarity index 69% rename from x-pack/plugins/canvas/public/components/asset_manager/asset_modal.tsx rename to x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx index cb61bf1dc26c4..98f3d8b48829d 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/asset_modal.tsx +++ b/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx @@ -3,6 +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 React, { FC, useState } from 'react'; +import PropTypes from 'prop-types'; import { EuiButton, EuiEmptyPrompt, @@ -21,48 +24,29 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; -import PropTypes from 'prop-types'; -import React, { FunctionComponent } from 'react'; - -import { ComponentStrings } from '../../../i18n'; import { ASSET_MAX_SIZE } from '../../../common/lib/constants'; import { Loading } from '../loading'; import { Asset } from './asset'; import { AssetType } from '../../../types'; +import { ComponentStrings } from '../../../i18n'; -const { AssetModal: strings } = ComponentStrings; +const { AssetManager: strings } = ComponentStrings; interface Props { /** The assets to display within the modal */ - assetValues: AssetType[]; - /** Indicates if assets are being loaded */ - isLoading: boolean; + assets: AssetType[]; /** Function to invoke when the modal is closed */ onClose: () => void; - /** Function to invoke when a file is uploaded */ - onFileUpload: (assets: FileList | null) => void; - /** Function to invoke when an asset is copied */ - onAssetCopy: (asset: AssetType) => void; - /** Function to invoke when an asset is created */ - onAssetCreate: (asset: AssetType) => void; - /** Function to invoke when an asset is deleted */ - onAssetDelete: (asset: AssetType) => void; + onAddAsset: (file: File) => void; } -export const AssetModal: FunctionComponent = (props) => { - const { - assetValues, - isLoading, - onAssetCopy, - onAssetCreate, - onAssetDelete, - onClose, - onFileUpload, - } = props; +export const AssetManager: FC = (props) => { + const { assets, onClose, onAddAsset } = props; + const [isLoading, setIsLoading] = useState(false); const assetsTotal = Math.round( - assetValues.reduce((total, { value }) => total + value.length, 0) / 1024 + assets.reduce((total, { value }) => total + value.length, 0) / 1024 ); const percentageUsed = Math.round((assetsTotal / ASSET_MAX_SIZE) * 100); @@ -77,10 +61,22 @@ export const AssetModal: FunctionComponent = (props) => { ); + const onFileUpload = (files: FileList | null) => { + if (files === null) { + return; + } + + setIsLoading(true); + + Promise.all(Array.from(files).map((file) => onAddAsset(file))).finally(() => { + setIsLoading(false); + }); + }; + return ( onClose()} className="canvasAssetManager canvasModal--fixedSize" maxWidth="1000px" > @@ -110,16 +106,10 @@ export const AssetModal: FunctionComponent = (props) => {

{strings.getDescription()}

- {assetValues.length ? ( + {assets.length ? ( - {assetValues.map((asset) => ( - + {assets.map((asset) => ( + ))} ) : ( @@ -143,7 +133,7 @@ export const AssetModal: FunctionComponent = (props) => { - + onClose()}> {strings.getModalCloseButtonLabel()} @@ -152,12 +142,8 @@ export const AssetModal: FunctionComponent = (props) => { ); }; -AssetModal.propTypes = { - assetValues: PropTypes.array, - isLoading: PropTypes.bool, +AssetManager.propTypes = { + assets: PropTypes.arrayOf(PropTypes.object).isRequired, onClose: PropTypes.func.isRequired, - onFileUpload: PropTypes.func.isRequired, - onAssetCopy: PropTypes.func.isRequired, - onAssetCreate: PropTypes.func.isRequired, - onAssetDelete: PropTypes.func.isRequired, + onAddAsset: PropTypes.func.isRequired, }; diff --git a/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.ts b/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.ts new file mode 100644 index 0000000000000..f9bcfb266006c --- /dev/null +++ b/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.ts @@ -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 { Dispatch } from 'redux'; +import { connect } from 'react-redux'; +import { get } from 'lodash'; + +import { getId } from '../../lib/get_id'; +// @ts-expect-error untyped local +import { findExistingAsset } from '../../lib/find_existing_asset'; +import { VALID_IMAGE_TYPES } from '../../../common/lib/constants'; +import { encode } from '../../../common/lib/dataurl'; +// @ts-expect-error untyped local +import { elementsRegistry } from '../../lib/elements_registry'; +// @ts-expect-error untyped local +import { addElement } from '../../state/actions/elements'; +import { getAssets } from '../../state/selectors/assets'; +// @ts-expect-error untyped local +import { removeAsset, createAsset } from '../../state/actions/assets'; +import { State, AssetType } from '../../../types'; + +import { AssetManager as Component } from './asset_manager.component'; + +export const AssetManager = connect( + (state: State) => ({ + assets: getAssets(state), + }), + (dispatch: Dispatch) => ({ + onAddAsset: (type: string, content: string) => { + // make the ID here and pass it into the action + const assetId = getId('asset'); + dispatch(createAsset(type, content, assetId)); + + // then return the id, so the caller knows the id that will be created + return assetId; + }, + }), + (stateProps, dispatchProps, ownProps) => { + const { assets } = stateProps; + const { onAddAsset } = dispatchProps; + + // pull values out of assets object + // have to cast to AssetType[] because TS doesn't know about filtering + const assetValues = Object.values(assets).filter((asset) => !!asset) as AssetType[]; + + return { + ...ownProps, + assets: assetValues, + onAddAsset: (file: File) => { + const [type, subtype] = get(file, 'type', '').split('/'); + if (type === 'image' && VALID_IMAGE_TYPES.indexOf(subtype) >= 0) { + return encode(file).then((dataurl) => { + const dataurlType = 'dataurl'; + const existingId = findExistingAsset(dataurlType, dataurl, assetValues); + + if (existingId) { + return existingId; + } + + return onAddAsset(dataurlType, dataurl); + }); + } + + return false; + }, + }; + } +)(Component); diff --git a/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.tsx b/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.tsx deleted file mode 100644 index cb177591fd650..0000000000000 --- a/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.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 PropTypes from 'prop-types'; -import React, { Fragment, PureComponent } from 'react'; - -import { ComponentStrings } from '../../../i18n'; - -import { ConfirmModal } from '../confirm_modal'; -import { AssetType } from '../../../types'; -import { AssetModal } from './asset_modal'; - -const { AssetManager: strings } = ComponentStrings; - -export interface Props { - /** A list of assets, if available */ - assetValues: AssetType[]; - /** Function to invoke when an asset is selected to be added as an element to the workpad */ - onAddImageElement: (id: string) => void; - /** Function to invoke when an asset is deleted */ - onAssetDelete: (id: string | null) => void; - /** Function to invoke when an asset is copied */ - onAssetCopy: () => void; - /** Function to invoke when an asset is added */ - onAssetAdd: (asset: File) => void; - /** Function to invoke when an asset modal is closed */ - onClose: () => void; -} - -interface State { - /** The id of the asset to delete, if applicable. Is set if the viewer clicks the delete icon */ - deleteId: string | null; - /** Indicates if the modal is currently loading */ - isLoading: boolean; -} - -export class AssetManager extends PureComponent { - public static propTypes = { - assetValues: PropTypes.array, - onAddImageElement: PropTypes.func.isRequired, - onAssetAdd: PropTypes.func.isRequired, - onAssetCopy: PropTypes.func.isRequired, - onAssetDelete: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - }; - - public static defaultProps = { - assetValues: [], - }; - - public state = { - deleteId: null, - isLoading: false, - }; - - public render() { - const { isLoading } = this.state; - const { assetValues, onAssetCopy, onAddImageElement, onClose } = this.props; - - const assetModal = ( - { - onAddImageElement(createdAsset.id); - onClose(); - }} - onAssetDelete={(asset: AssetType) => this.setState({ deleteId: asset.id })} - onClose={onClose} - onFileUpload={this.handleFileUpload} - /> - ); - - const confirmModal = ( - - ); - - return ( - - {assetModal} - {confirmModal} - - ); - } - - private resetDelete = () => this.setState({ deleteId: null }); - - private doDelete = () => { - this.resetDelete(); - this.props.onAssetDelete(this.state.deleteId); - }; - - private handleFileUpload = (files: FileList | null) => { - if (files == null) return; - this.setState({ isLoading: true }); - Promise.all(Array.from(files).map((file) => this.props.onAssetAdd(file))).finally(() => { - this.setState({ isLoading: false }); - }); - }; -} diff --git a/x-pack/plugins/canvas/public/components/asset_manager/index.ts b/x-pack/plugins/canvas/public/components/asset_manager/index.ts index 9b4406f607867..5d586c07f4e4e 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/index.ts +++ b/x-pack/plugins/canvas/public/components/asset_manager/index.ts @@ -4,107 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { compose, withProps } from 'recompose'; -import { set } from '@elastic/safer-lodash-set'; -import { get } from 'lodash'; -import { fromExpression, toExpression } from '@kbn/interpreter/common'; -import { getAssets } from '../../state/selectors/assets'; -// @ts-expect-error untyped local -import { removeAsset, createAsset } from '../../state/actions/assets'; -// @ts-expect-error untyped local -import { elementsRegistry } from '../../lib/elements_registry'; -// @ts-expect-error untyped local -import { addElement } from '../../state/actions/elements'; -import { getSelectedPage } from '../../state/selectors/workpad'; -import { encode } from '../../../common/lib/dataurl'; -import { getId } from '../../lib/get_id'; -// @ts-expect-error untyped local -import { findExistingAsset } from '../../lib/find_existing_asset'; -import { VALID_IMAGE_TYPES } from '../../../common/lib/constants'; -import { withKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { WithKibanaProps } from '../../'; -import { AssetManager as Component, Props as AssetManagerProps } from './asset_manager'; - -import { State, ExpressionAstExpression, AssetType } from '../../../types'; - -const mapStateToProps = (state: State) => ({ - assets: getAssets(state), - selectedPage: getSelectedPage(state), -}); - -const mapDispatchToProps = (dispatch: (action: any) => void) => ({ - onAddImageElement: (pageId: string) => (assetId: string) => { - const imageElement = elementsRegistry.get('image'); - const elementAST = fromExpression(imageElement.expression); - const selector = ['chain', '0', 'arguments', 'dataurl']; - const subExp: ExpressionAstExpression[] = [ - { - type: 'expression', - chain: [ - { - type: 'function', - function: 'asset', - arguments: { - _: [assetId], - }, - }, - ], - }, - ]; - const newAST = set(elementAST, selector, subExp); - imageElement.expression = toExpression(newAST); - dispatch(addElement(pageId, imageElement)); - }, - onAssetAdd: (type: string, content: string) => { - // make the ID here and pass it into the action - const assetId = getId('asset'); - dispatch(createAsset(type, content, assetId)); - - // then return the id, so the caller knows the id that will be created - return assetId; - }, - onAssetDelete: (assetId: string) => dispatch(removeAsset(assetId)), -}); - -const mergeProps = ( - stateProps: ReturnType, - dispatchProps: ReturnType, - ownProps: AssetManagerProps -) => { - const { assets, selectedPage } = stateProps; - const { onAssetAdd } = dispatchProps; - const assetValues = Object.values(assets); // pull values out of assets object - - return { - ...ownProps, - ...dispatchProps, - onAddImageElement: dispatchProps.onAddImageElement(stateProps.selectedPage), - selectedPage, - assetValues, - onAssetAdd: (file: File) => { - const [type, subtype] = get(file, 'type', '').split('/'); - if (type === 'image' && VALID_IMAGE_TYPES.indexOf(subtype) >= 0) { - return encode(file).then((dataurl) => { - const dataurlType = 'dataurl'; - const existingId = findExistingAsset(dataurlType, dataurl, assetValues); - if (existingId) { - return existingId; - } - return onAssetAdd(dataurlType, dataurl); - }); - } - - return false; - }, - }; -}; - -export const AssetManager = compose( - connect(mapStateToProps, mapDispatchToProps, mergeProps), - withKibana, - withProps(({ kibana }: WithKibanaProps) => ({ - onAssetCopy: (asset: AssetType) => - kibana.services.canvas.notify.success(`Copied '${asset.id}' to clipboard`), - })) -)(Component); +export { Asset } from './asset'; +export { Asset as AssetComponent } from './asset.component'; +export { AssetManager } from './asset_manager'; +export { AssetManager as AssetManagerComponent } from './asset_manager.component'; diff --git a/x-pack/plugins/canvas/public/components/dom_preview/dom_preview.js b/x-pack/plugins/canvas/public/components/dom_preview/dom_preview.tsx similarity index 71% rename from x-pack/plugins/canvas/public/components/dom_preview/dom_preview.js rename to x-pack/plugins/canvas/public/components/dom_preview/dom_preview.tsx index f74862af8d105..6ec0276c2f49f 100644 --- a/x-pack/plugins/canvas/public/components/dom_preview/dom_preview.js +++ b/x-pack/plugins/canvas/public/components/dom_preview/dom_preview.tsx @@ -4,30 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { debounce } from 'lodash'; -export class DomPreview extends React.Component { +interface Props { + elementId: string; + height: number; +} + +export class DomPreview extends PureComponent { static propTypes = { elementId: PropTypes.string.isRequired, height: PropTypes.number.isRequired, }; + _container: HTMLDivElement | null = null; + _content: HTMLDivElement | null = null; + _observer: MutationObserver | null = null; + _original: Element | null = null; + _updateTimeout: number = 0; + componentDidMount() { this.update(); } componentWillUnmount() { clearTimeout(this._updateTimeout); - this._observer && this._observer.disconnect(); // observer not guaranteed to exist - } - _container = null; - _content = null; - _observer = null; - _original = null; - _updateTimeout = null; + if (this._observer) { + this._observer.disconnect(); // observer not guaranteed to exist + } + } update = () => { if (!this._content || !this._container) { @@ -38,7 +46,10 @@ export class DomPreview extends React.Component { const originalChanged = currentOriginal !== this._original; if (originalChanged) { - this._observer && this._observer.disconnect(); + if (this._observer) { + this._observer.disconnect(); + } + this._original = currentOriginal; if (this._original) { @@ -50,12 +61,16 @@ export class DomPreview extends React.Component { this._observer.observe(this._original, config); } else { clearTimeout(this._updateTimeout); // to avoid the assumption that we fully control when `update` is called - this._updateTimeout = setTimeout(this.update, 30); + this._updateTimeout = window.setTimeout(this.update, 30); return; } } - const thumb = this._original.cloneNode(true); + if (!this._original) { + return; + } + + const thumb = this._original.cloneNode(true) as HTMLDivElement; thumb.id += '-thumb'; const originalStyle = window.getComputedStyle(this._original, null); @@ -66,9 +81,10 @@ export class DomPreview extends React.Component { const scale = thumbHeight / originalHeight; const thumbWidth = originalWidth * scale; - if (this._content.hasChildNodes()) { + if (this._content.firstChild) { this._content.removeChild(this._content.firstChild); } + this._content.appendChild(thumb); this._content.style.cssText = `transform: scale(${scale}); transform-origin: top left;`; @@ -76,13 +92,16 @@ export class DomPreview extends React.Component { // Copy canvas data const originalCanvas = this._original.querySelectorAll('canvas'); - const thumbCanvas = thumb.querySelectorAll('canvas'); + const thumbCanvas = (thumb as Element).querySelectorAll('canvas'); // Cloned canvas elements are blank and need to be explicitly redrawn if (originalCanvas.length > 0) { - Array.from(originalCanvas).map((img, i) => - thumbCanvas[i].getContext('2d').drawImage(img, 0, 0) - ); + Array.from(originalCanvas).map((img, i) => { + const context = thumbCanvas[i].getContext('2d'); + if (context) { + context.drawImage(img, 0, 0); + } + }); } }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/index.tsx b/x-pack/plugins/canvas/public/components/dom_preview/index.ts similarity index 78% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/index.tsx rename to x-pack/plugins/canvas/public/components/dom_preview/index.ts index 85ea754de670d..19980b7c2cfe5 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/index.tsx +++ b/x-pack/plugins/canvas/public/components/dom_preview/index.ts @@ -4,6 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TimeFilter } from './time_filter'; - -export { TimeFilter }; +export { DomPreview } from './dom_preview'; diff --git a/x-pack/plugins/canvas/public/components/link/index.js b/x-pack/plugins/canvas/public/components/link/index.ts similarity index 100% rename from x-pack/plugins/canvas/public/components/link/index.js rename to x-pack/plugins/canvas/public/components/link/index.ts diff --git a/x-pack/plugins/canvas/public/components/link/link.js b/x-pack/plugins/canvas/public/components/link/link.js deleted file mode 100644 index d973164190592..0000000000000 --- a/x-pack/plugins/canvas/public/components/link/link.js +++ /dev/null @@ -1,63 +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 React from 'react'; -import PropTypes from 'prop-types'; -import { EuiLink } from '@elastic/eui'; - -import { ComponentStrings } from '../../../i18n'; - -const { Link: strings } = ComponentStrings; - -const isModifiedEvent = (ev) => !!(ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey); - -export class Link extends React.PureComponent { - static propTypes = { - target: PropTypes.string, - onClick: PropTypes.func, - name: PropTypes.string.isRequired, - params: PropTypes.object, - children: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.node]).isRequired, - }; - - static contextTypes = { - router: PropTypes.object, - }; - - navigateTo = (name, params) => (ev) => { - if (this.props.onClick) { - this.props.onClick(ev); - } - - if ( - !ev.defaultPrevented && // onClick prevented default - ev.button === 0 && // ignore everything but left clicks - !this.props.target && // let browser handle "target=_blank" etc. - !isModifiedEvent(ev) // ignore clicks with modifier keys - ) { - ev.preventDefault(); - this.context.router.navigateTo(name, params); - } - }; - - render() { - try { - const { name, params, children, ...linkArgs } = this.props; - const { router } = this.context; - const href = router.getFullPath(router.create(name, params)); - const props = { - ...linkArgs, - href, - onClick: this.navigateTo(name, params), - }; - - return {children}; - } catch (e) { - console.error(e); - return
{strings.getErrorMessage(e.message)}
; - } - } -} diff --git a/x-pack/plugins/canvas/public/components/link/link.tsx b/x-pack/plugins/canvas/public/components/link/link.tsx new file mode 100644 index 0000000000000..b0289fba842d1 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/link/link.tsx @@ -0,0 +1,72 @@ +/* + * 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, { FC, MouseEvent, useContext } from 'react'; +import PropTypes from 'prop-types'; +import { EuiLink, EuiLinkProps } from '@elastic/eui'; +import { RouterContext } from '../router'; + +import { ComponentStrings } from '../../../i18n'; + +const { Link: strings } = ComponentStrings; + +const isModifiedEvent = (ev: MouseEvent) => + !!(ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey); + +interface Props { + name: string; + params: Record; +} + +export const Link: FC = ({ + onClick, + target, + name, + params, + children, + ...linkArgs +}) => { + const router = useContext(RouterContext); + + if (router) { + const navigateTo = (ev: MouseEvent) => { + if (onClick) { + onClick(ev); + } + + if ( + !ev.defaultPrevented && // onClick prevented default + ev.button === 0 && // ignore everything but left clicks + !target && // let browser handle "target=_blank" etc. + !isModifiedEvent(ev) // ignore clicks with modifier keys + ) { + ev.preventDefault(); + router.navigateTo(name, params); + } + }; + + try { + return ( + + {children} + + ); + } catch (e) { + return
{strings.getErrorMessage(e.message)}
; + } + } + + return
{strings.getErrorMessage('Router Undefined')}
; +}; + +Link.contextTypes = { + router: PropTypes.object, +}; + +Link.propTypes = { + name: PropTypes.string.isRequired, + params: PropTypes.object, +}; diff --git a/x-pack/plugins/canvas/public/components/page_manager/index.js b/x-pack/plugins/canvas/public/components/page_manager/index.js deleted file mode 100644 index a198b7b8c3d8c..0000000000000 --- a/x-pack/plugins/canvas/public/components/page_manager/index.js +++ /dev/null @@ -1,37 +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 { connect } from 'react-redux'; -import { compose, withState } from 'recompose'; -import * as pageActions from '../../state/actions/pages'; -import { canUserWrite } from '../../state/selectors/app'; -import { getSelectedPage, getWorkpad, getPages, isWriteable } from '../../state/selectors/workpad'; -import { DEFAULT_WORKPAD_CSS } from '../../../common/lib/constants'; -import { PageManager as Component } from './page_manager'; - -const mapStateToProps = (state) => { - const { id, css } = getWorkpad(state); - - return { - isWriteable: isWriteable(state) && canUserWrite(state), - pages: getPages(state), - selectedPage: getSelectedPage(state), - workpadId: id, - workpadCSS: css || DEFAULT_WORKPAD_CSS, - }; -}; - -const mapDispatchToProps = (dispatch) => ({ - addPage: () => dispatch(pageActions.addPage()), - movePage: (id, position) => dispatch(pageActions.movePage(id, position)), - duplicatePage: (id) => dispatch(pageActions.duplicatePage(id)), - removePage: (id) => dispatch(pageActions.removePage(id)), -}); - -export const PageManager = compose( - connect(mapStateToProps, mapDispatchToProps), - withState('deleteId', 'setDeleteId', null) -)(Component); diff --git a/x-pack/plugins/canvas/public/components/page_manager/index.ts b/x-pack/plugins/canvas/public/components/page_manager/index.ts new file mode 100644 index 0000000000000..d19540cd6a687 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/page_manager/index.ts @@ -0,0 +1,31 @@ +/* + * 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 } from 'redux'; +import { connect } from 'react-redux'; +// @ts-expect-error untyped local +import * as pageActions from '../../state/actions/pages'; +import { canUserWrite } from '../../state/selectors/app'; +import { getSelectedPage, getWorkpad, getPages, isWriteable } from '../../state/selectors/workpad'; +import { DEFAULT_WORKPAD_CSS } from '../../../common/lib/constants'; +import { PageManager as Component } from './page_manager'; +import { State } from '../../../types'; + +const mapStateToProps = (state: State) => ({ + isWriteable: isWriteable(state) && canUserWrite(state), + pages: getPages(state), + selectedPage: getSelectedPage(state), + workpadId: getWorkpad(state).id, + workpadCSS: getWorkpad(state).css || DEFAULT_WORKPAD_CSS, +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + onAddPage: () => dispatch(pageActions.addPage()), + onMovePage: (id: string, position: number) => dispatch(pageActions.movePage(id, position)), + onRemovePage: (id: string) => dispatch(pageActions.removePage(id)), +}); + +export const PageManager = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/plugins/canvas/public/components/page_manager/page_manager.js b/x-pack/plugins/canvas/public/components/page_manager/page_manager.tsx similarity index 63% rename from x-pack/plugins/canvas/public/components/page_manager/page_manager.js rename to x-pack/plugins/canvas/public/components/page_manager/page_manager.tsx index 3e2ff9dfe2b22..edc0d6201495b 100644 --- a/x-pack/plugins/canvas/public/components/page_manager/page_manager.js +++ b/x-pack/plugins/canvas/public/components/page_manager/page_manager.tsx @@ -4,38 +4,63 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment } from 'react'; +import React, { Fragment, Component } from 'react'; import PropTypes from 'prop-types'; import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; -import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; +import { DragDropContext, Droppable, Draggable, DragDropContextProps } from 'react-beautiful-dnd'; +// @ts-expect-error untyped dependency import Style from 'style-it'; + import { ConfirmModal } from '../confirm_modal'; import { Link } from '../link'; import { PagePreview } from '../page_preview'; import { ComponentStrings } from '../../../i18n'; +import { CanvasPage } from '../../../types'; const { PageManager: strings } = ComponentStrings; -export class PageManager extends React.PureComponent { +export interface Props { + isWriteable: boolean; + onAddPage: () => void; + onMovePage: (pageId: string, position: number) => void; + onPreviousPage: () => void; + onRemovePage: (pageId: string) => void; + pages: CanvasPage[]; + selectedPage?: string; + workpadCSS?: string; + workpadId: string; +} + +interface State { + showTrayPop: boolean; + removeId: string | null; +} + +export class PageManager extends Component { static propTypes = { isWriteable: PropTypes.bool.isRequired, + onAddPage: PropTypes.func.isRequired, + onMovePage: PropTypes.func.isRequired, + onPreviousPage: PropTypes.func.isRequired, + onRemovePage: PropTypes.func.isRequired, pages: PropTypes.array.isRequired, - workpadId: PropTypes.string.isRequired, - addPage: PropTypes.func.isRequired, - movePage: PropTypes.func.isRequired, - previousPage: PropTypes.func.isRequired, - duplicatePage: PropTypes.func.isRequired, - removePage: PropTypes.func.isRequired, selectedPage: PropTypes.string, - deleteId: PropTypes.string, - setDeleteId: PropTypes.func.isRequired, workpadCSS: PropTypes.string, + workpadId: PropTypes.string.isRequired, }; - state = { - showTrayPop: true, - }; + constructor(props: Props) { + super(props); + this.state = { + showTrayPop: true, + removeId: null, + }; + } + + _isMounted: boolean = false; + _activePageRef: HTMLDivElement | null = null; + _pageListRef: HTMLDivElement | null = null; componentDidMount() { // keep track of whether or not the component is mounted, to prevent rogue setState calls @@ -44,11 +69,13 @@ export class PageManager extends React.PureComponent { // gives the tray pop animation time to finish setTimeout(() => { this.scrollToActivePage(); - this._isMounted && this.setState({ showTrayPop: false }); + if (this._isMounted) { + this.setState({ showTrayPop: false }); + } }, 1000); } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: Props) { // scrolls to the active page on the next tick, otherwise new pages don't scroll completely into view if (prevProps.selectedPage !== this.props.selectedPage) { setTimeout(this.scrollToActivePage, 0); @@ -60,33 +87,33 @@ export class PageManager extends React.PureComponent { } scrollToActivePage = () => { - if (this.activePageRef && this.pageListRef) { + if (this._activePageRef && this._pageListRef) { // not all target browsers support element.scrollTo // TODO: replace this with something more cross-browser, maybe scrollIntoView - if (!this.pageListRef.scrollTo) { + if (!this._pageListRef.scrollTo) { return; } - const pageOffset = this.activePageRef.offsetLeft; + const pageOffset = this._activePageRef.offsetLeft; const { left: pageLeft, right: pageRight, width: pageWidth, - } = this.activePageRef.getBoundingClientRect(); + } = this._activePageRef.getBoundingClientRect(); const { left: listLeft, right: listRight, width: listWidth, - } = this.pageListRef.getBoundingClientRect(); + } = this._pageListRef.getBoundingClientRect(); if (pageLeft < listLeft) { - this.pageListRef.scrollTo({ + this._pageListRef.scrollTo({ left: pageOffset, behavior: 'smooth', }); } if (pageRight > listRight) { - this.pageListRef.scrollTo({ + this._pageListRef.scrollTo({ left: pageOffset - listWidth + pageWidth, behavior: 'smooth', }); @@ -94,22 +121,29 @@ export class PageManager extends React.PureComponent { } }; - confirmDelete = (pageId) => { - this._isMounted && this.props.setDeleteId(pageId); + onConfirmRemove = (removeId: string) => { + if (this._isMounted) { + this.setState({ removeId }); + } }; - resetDelete = () => this._isMounted && this.props.setDeleteId(null); + resetRemove = () => this._isMounted && this.setState({ removeId: null }); + + doRemove = () => { + const { onPreviousPage, onRemovePage, selectedPage } = this.props; + const { removeId } = this.state; + this.resetRemove(); + + if (removeId === selectedPage) { + onPreviousPage(); + } - doDelete = () => { - const { previousPage, removePage, deleteId, selectedPage } = this.props; - this.resetDelete(); - if (deleteId === selectedPage) { - previousPage(); + if (removeId !== null) { + onRemovePage(removeId); } - removePage(deleteId); }; - onDragEnd = ({ draggableId: pageId, source, destination }) => { + onDragEnd: DragDropContextProps['onDragEnd'] = ({ draggableId: pageId, source, destination }) => { // dropped outside the list if (!destination) { return; @@ -117,18 +151,11 @@ export class PageManager extends React.PureComponent { const position = destination.index - source.index; - this.props.movePage(pageId, position); + this.props.onMovePage(pageId, position); }; - renderPage = (page, i) => { - const { - isWriteable, - selectedPage, - workpadId, - movePage, - duplicatePage, - workpadCSS, - } = this.props; + renderPage = (page: CanvasPage, i: number) => { + const { isWriteable, selectedPage, workpadId, workpadCSS } = this.props; const pageNumber = i + 1; return ( @@ -141,7 +168,7 @@ export class PageManager extends React.PureComponent { }`} ref={(el) => { if (page.id === selectedPage) { - this.activePageRef = el; + this._activePageRef = el; } provided.innerRef(el); }} @@ -163,16 +190,7 @@ export class PageManager extends React.PureComponent { {Style.it( workpadCSS,
- +
)} @@ -185,8 +203,8 @@ export class PageManager extends React.PureComponent { }; render() { - const { pages, addPage, deleteId, isWriteable } = this.props; - const { showTrayPop } = this.state; + const { pages, onAddPage, isWriteable } = this.props; + const { showTrayPop, removeId } = this.state; return ( @@ -200,7 +218,7 @@ export class PageManager extends React.PureComponent { showTrayPop ? 'canvasPageManager--trayPop' : '' }`} ref={(el) => { - this.pageListRef = el; + this._pageListRef = el; provided.innerRef(el); }} {...provided.droppableProps} @@ -216,11 +234,11 @@ export class PageManager extends React.PureComponent {
); })} diff --git a/x-pack/plugins/monitoring/server/config.test.ts b/x-pack/plugins/monitoring/server/config.test.ts index 16d52f684109e..32b8691bd6049 100644 --- a/x-pack/plugins/monitoring/server/config.test.ts +++ b/x-pack/plugins/monitoring/server/config.test.ts @@ -27,33 +27,6 @@ describe('config schema', () => { }, "enabled": true, }, - "elasticsearch": Object { - "apiVersion": "master", - "customHeaders": Object {}, - "healthCheck": Object { - "delay": "PT2.5S", - }, - "ignoreVersionMismatch": false, - "logFetchCount": 10, - "logQueries": false, - "pingTimeout": "PT30S", - "preserveHost": true, - "requestHeadersWhitelist": Array [ - "authorization", - ], - "requestTimeout": "PT30S", - "shardTimeout": "PT30S", - "sniffInterval": false, - "sniffOnConnectionFault": false, - "sniffOnStart": false, - "ssl": Object { - "alwaysPresentCertificate": false, - "keystore": Object {}, - "truststore": Object {}, - "verificationMode": "full", - }, - "startupTimeout": "PT5S", - }, "enabled": true, "kibana": Object { "collection": Object { @@ -125,9 +98,6 @@ describe('createConfig()', () => { it('should wrap in Elasticsearch config', async () => { const config = createConfig( configSchema.validate({ - elasticsearch: { - hosts: 'http://localhost:9200', - }, ui: { elasticsearch: { hosts: 'http://localhost:9200', @@ -135,7 +105,6 @@ describe('createConfig()', () => { }, }) ); - expect(config.elasticsearch.hosts).toEqual(['http://localhost:9200']); expect(config.ui.elasticsearch.hosts).toEqual(['http://localhost:9200']); }); @@ -147,9 +116,6 @@ describe('createConfig()', () => { }; const config = createConfig( configSchema.validate({ - elasticsearch: { - ssl, - }, ui: { elasticsearch: { ssl, @@ -162,7 +128,6 @@ describe('createConfig()', () => { key: 'contents-of-packages/kbn-dev-utils/certs/elasticsearch.key', certificateAuthorities: ['contents-of-packages/kbn-dev-utils/certs/ca.crt'], }); - expect(config.elasticsearch.ssl).toEqual(expected); expect(config.ui.elasticsearch.ssl).toEqual(expected); }); }); diff --git a/x-pack/plugins/monitoring/server/config.ts b/x-pack/plugins/monitoring/server/config.ts index a430be8da6a5f..789211c43db31 100644 --- a/x-pack/plugins/monitoring/server/config.ts +++ b/x-pack/plugins/monitoring/server/config.ts @@ -21,7 +21,6 @@ export const monitoringElasticsearchConfigSchema = elasticsearchConfigSchema.ext export const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), - elasticsearch: monitoringElasticsearchConfigSchema, ui: schema.object({ enabled: schema.boolean({ defaultValue: true }), ccs: schema.object({ @@ -86,7 +85,6 @@ export type MonitoringConfig = ReturnType; export function createConfig(config: TypeOf) { return { ...config, - elasticsearch: new ElasticsearchConfig(config.elasticsearch as ElasticsearchConfigType), ui: { ...config.ui, elasticsearch: new MonitoringElasticsearchConfig(config.ui.elasticsearch), diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/__tests__/bulk_uploader.js b/x-pack/plugins/monitoring/server/kibana_monitoring/__tests__/bulk_uploader.js index 3421f5d3830d6..da12bde966091 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/__tests__/bulk_uploader.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/__tests__/bulk_uploader.js @@ -6,10 +6,8 @@ import { noop } from 'lodash'; import sinon from 'sinon'; -import moment from 'moment'; import expect from '@kbn/expect'; import { BulkUploader } from '../bulk_uploader'; -import { MONITORING_SYSTEM_API_VERSION } from '../../../common/constants'; const FETCH_INTERVAL = 300; const CHECK_DELAY = 500; @@ -314,92 +312,5 @@ describe('BulkUploader', () => { done(); }, CHECK_DELAY); }); - - it('uses a direct connection to the monitoring cluster, when configured', (done) => { - const dateInIndex = '2020.02.10'; - const oldNow = moment.now; - moment.now = () => 1581310800000; - const prodClusterUuid = '1sdfd5'; - const prodCluster = { - callWithInternalUser: sinon - .stub() - .withArgs('monitoring.bulk') - .callsFake((arg) => { - let resolution = null; - if (arg === 'info') { - resolution = { cluster_uuid: prodClusterUuid }; - } - return new Promise((resolve) => resolve(resolution)); - }), - }; - const monitoringCluster = { - callWithInternalUser: sinon - .stub() - .withArgs('bulk') - .callsFake(() => { - return new Promise((resolve) => setTimeout(resolve, CHECK_DELAY + 1)); - }), - }; - - const collectorFetch = sinon.stub().returns({ - type: 'kibana_stats', - result: { type: 'kibana_stats', payload: { testData: 12345 } }, - }); - - const collectors = new MockCollectorSet(server, [ - { - fetch: collectorFetch, - isReady: () => true, - formatForBulkUpload: (result) => result, - isUsageCollector: false, - }, - ]); - const customServer = { - ...server, - elasticsearchPlugin: { - createCluster: () => monitoringCluster, - getCluster: (name) => { - if (name === 'admin' || name === 'data') { - return prodCluster; - } - return monitoringCluster; - }, - }, - config: { - get: (key) => { - if (key === 'monitoring.elasticsearch') { - return { - hosts: ['http://localhost:9200'], - username: 'tester', - password: 'testing', - ssl: {}, - }; - } - return null; - }, - }, - }; - const kbnServerStatus = { toJSON: () => ({ overall: { state: 'green' } }) }; - const kbnServerVersion = 'master'; - const uploader = new BulkUploader({ - ...customServer, - interval: FETCH_INTERVAL, - kbnServerStatus, - kbnServerVersion, - }); - uploader.start(collectors); - setTimeout(() => { - uploader.stop(); - const firstCallArgs = monitoringCluster.callWithInternalUser.firstCall.args; - expect(firstCallArgs[0]).to.be('bulk'); - expect(firstCallArgs[1].body[0].index._index).to.be( - `.monitoring-kibana-${MONITORING_SYSTEM_API_VERSION}-${dateInIndex}` - ); - expect(firstCallArgs[1].body[1].type).to.be('kibana_stats'); - expect(firstCallArgs[1].body[1].cluster_uuid).to.be(prodClusterUuid); - moment.now = oldNow; - done(); - }, CHECK_DELAY); - }); }); }); diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js b/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js index 6035837bac85d..b23b4fc888120 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { defaultsDeep, uniq, compact, get } from 'lodash'; +import { defaultsDeep, uniq, compact } from 'lodash'; import { TELEMETRY_COLLECTION_INTERVAL, @@ -12,7 +12,6 @@ import { } from '../../common/constants'; import { sendBulkPayload, monitoringBulk } from './lib'; -import { hasMonitoringCluster } from '../es_client/instantiate_client'; /* * Handles internal Kibana stats collection and uploading data to Monitoring @@ -31,13 +30,11 @@ import { hasMonitoringCluster } from '../es_client/instantiate_client'; * @param {Object} xpackInfo server.plugins.xpack_main.info object */ export class BulkUploader { - constructor({ config, log, interval, elasticsearch, kibanaStats }) { + constructor({ log, interval, elasticsearch, kibanaStats }) { if (typeof interval !== 'number') { throw new Error('interval number of milliseconds is required'); } - this._hasDirectConnectionToMonitoringCluster = false; - this._productionClusterUuid = null; this._timer = null; // Hold sending and fetching usage until monitoring.bulk is successful. This means that we // send usage data on the second tick. But would save a lot of bandwidth fetching usage on @@ -54,15 +51,6 @@ export class BulkUploader { plugins: [monitoringBulk], }); - if (hasMonitoringCluster(config.elasticsearch)) { - this._log.info(`Detected direct connection to monitoring cluster`); - this._hasDirectConnectionToMonitoringCluster = true; - this._cluster = elasticsearch.legacy.createClient('monitoring-direct', config.elasticsearch); - elasticsearch.legacy.client.callAsInternalUser('info').then((data) => { - this._productionClusterUuid = get(data, 'cluster_uuid'); - }); - } - this.kibanaStats = kibanaStats; this.kibanaStatusGetter = null; } @@ -181,14 +169,7 @@ export class BulkUploader { } async _onPayload(payload) { - return await sendBulkPayload( - this._cluster, - this._interval, - payload, - this._log, - this._hasDirectConnectionToMonitoringCluster, - this._productionClusterUuid - ); + return await sendBulkPayload(this._cluster, this._interval, payload, this._log); } getKibanaStats(type) { diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/lib/send_bulk_payload.js b/x-pack/plugins/monitoring/server/kibana_monitoring/lib/send_bulk_payload.js index 9607b45d7e408..66799e4aa651a 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/lib/send_bulk_payload.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/lib/send_bulk_payload.js @@ -3,64 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import moment from 'moment'; -import { chunk, get } from 'lodash'; -import { - MONITORING_SYSTEM_API_VERSION, - KIBANA_SYSTEM_ID, - KIBANA_STATS_TYPE_MONITORING, - KIBANA_SETTINGS_TYPE, -} from '../../../common/constants'; - -const SUPPORTED_TYPES = [KIBANA_STATS_TYPE_MONITORING, KIBANA_SETTINGS_TYPE]; -export function formatForNormalBulkEndpoint(payload, productionClusterUuid) { - const dateSuffix = moment.utc().format('YYYY.MM.DD'); - return chunk(payload, 2).reduce((accum, chunk) => { - const type = get(chunk[0], 'index._type'); - if (!type || !SUPPORTED_TYPES.includes(type)) { - return accum; - } - - const { timestamp } = chunk[1]; - - accum.push({ - index: { - _index: `.monitoring-kibana-${MONITORING_SYSTEM_API_VERSION}-${dateSuffix}`, - }, - }); - accum.push({ - [type]: chunk[1], - type, - timestamp, - cluster_uuid: productionClusterUuid, - }); - return accum; - }, []); -} +import { MONITORING_SYSTEM_API_VERSION, KIBANA_SYSTEM_ID } from '../../../common/constants'; /* * Send the Kibana usage data to the ES Monitoring Bulk endpoint */ -export async function sendBulkPayload( - cluster, - interval, - payload, - log, - hasDirectConnectionToMonitoringCluster = false, - productionClusterUuid = null -) { - if (hasDirectConnectionToMonitoringCluster) { - if (productionClusterUuid === null) { - log.warn( - `Unable to determine production cluster uuid to use for shipping monitoring data. Kibana monitoring data will appear in a standalone cluster in the Stack Monitoring UI.` - ); - } - const formattedPayload = formatForNormalBulkEndpoint(payload, productionClusterUuid); - return await cluster.callAsInternalUser('bulk', { - body: formattedPayload, - }); - } - +export async function sendBulkPayload(cluster, interval, payload) { return cluster.callAsInternalUser('monitoring.bulk', { system_id: KIBANA_SYSTEM_ID, system_api_version: MONITORING_SYSTEM_API_VERSION, diff --git a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts index b63f2a09041b3..9227354520b6e 100644 --- a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts @@ -13,7 +13,6 @@ export const scheduleTaskFnFactory: ScheduleTaskFnFactory> = function createJobFactoryFn(reporting) { const config = reporting.getConfig(); - const setupDeps = reporting.getPluginSetupDeps(); const crypto = cryptoFactory(config.get('encryptionKey')); return async function scheduleTask( @@ -32,7 +31,7 @@ export const scheduleTaskFnFactory: ScheduleTaskFnFactory> = function createJobFactoryFn(reporting) { const config = reporting.getConfig(); - const setupDeps = reporting.getPluginSetupDeps(); const crypto = cryptoFactory(config.get('encryptionKey')); return async function scheduleTaskFn( @@ -26,7 +25,7 @@ export const scheduleTaskFnFactory: ScheduleTaskFnFactory { let exclude: boolean; @@ -295,20 +298,95 @@ describe('build_exceptions_query', () => { const item: EntryNested = { field: 'parent', type: 'nested', - entries: [makeMatchEntry({ field: 'nestedField', operator: 'included' })], + entries: [ + { + ...getEntryMatchMock(), + field: 'nestedField', + operator: 'included', + value: 'value-1', + }, + ], }; const result = buildNested({ item, language: 'kuery' }); expect(result).toEqual('parent:{ nestedField:"value-1" }'); }); + test('it returns formatted query when entry item is "exists"', () => { + const item: EntryNested = { + field: 'parent', + type: 'nested', + entries: [{ ...getEntryExistsMock(), field: 'nestedField', operator: 'included' }], + }; + const result = buildNested({ item, language: 'kuery' }); + + expect(result).toEqual('parent:{ nestedField:* }'); + }); + + test('it returns formatted query when entry item is "exists" and operator is "excluded"', () => { + const item: EntryNested = { + field: 'parent', + type: 'nested', + entries: [{ ...getEntryExistsMock(), field: 'nestedField', operator: 'excluded' }], + }; + const result = buildNested({ item, language: 'kuery' }); + + expect(result).toEqual('parent:{ not nestedField:* }'); + }); + + test('it returns formatted query when entry item is "match_any"', () => { + const item: EntryNested = { + field: 'parent', + type: 'nested', + entries: [ + { + ...getEntryMatchAnyMock(), + field: 'nestedField', + operator: 'included', + value: ['value1', 'value2'], + }, + ], + }; + const result = buildNested({ item, language: 'kuery' }); + + expect(result).toEqual('parent:{ nestedField:("value1" or "value2") }'); + }); + + test('it returns formatted query when entry item is "match_any" and operator is "excluded"', () => { + const item: EntryNested = { + field: 'parent', + type: 'nested', + entries: [ + { + ...getEntryMatchAnyMock(), + field: 'nestedField', + operator: 'excluded', + value: ['value1', 'value2'], + }, + ], + }; + const result = buildNested({ item, language: 'kuery' }); + + expect(result).toEqual('parent:{ not nestedField:("value1" or "value2") }'); + }); + test('it returns formatted query when multiple items in nested entry', () => { const item: EntryNested = { field: 'parent', type: 'nested', entries: [ - makeMatchEntry({ field: 'nestedField', operator: 'included' }), - makeMatchEntry({ field: 'nestedFieldB', operator: 'included', value: 'value-2' }), + { + ...getEntryMatchMock(), + field: 'nestedField', + operator: 'included', + value: 'value-1', + }, + { + ...getEntryMatchMock(), + field: 'nestedFieldB', + operator: 'included', + value: 'value-2', + }, ], }; const result = buildNested({ item, language: 'kuery' }); @@ -514,7 +592,7 @@ describe('build_exceptions_query', () => { entries, }); const expectedQuery = - 'b:("value-1" OR "value-2") AND parent:{ nestedField:"value-3" } AND NOT _exists_e'; + 'b:("value-1" OR "value-2") AND parent:{ NOT nestedField:"value-3" } AND NOT _exists_e'; expect(query).toEqual(expectedQuery); }); @@ -576,7 +654,7 @@ describe('build_exceptions_query', () => { language: 'kuery', entries, }); - const expectedQuery = 'b:* and parent:{ c:"value-1" and d:"value-2" } and e:*'; + const expectedQuery = 'b:* and parent:{ not c:"value-1" and d:"value-2" } and e:*'; expect(query).toEqual(expectedQuery); }); @@ -642,7 +720,8 @@ describe('build_exceptions_query', () => { language: 'kuery', entries, }); - const expectedQuery = 'b:"value" and parent:{ c:"valueC" and d:"valueD" } and e:"valueE"'; + const expectedQuery = + 'b:"value" and parent:{ not c:"valueC" and not d:"valueD" } and e:"valueE"'; expect(query).toEqual(expectedQuery); }); @@ -684,7 +763,7 @@ describe('build_exceptions_query', () => { language: 'kuery', entries, }); - const expectedQuery = 'not b:("value-1" or "value-2") and parent:{ c:"valueC" }'; + const expectedQuery = 'not b:("value-1" or "value-2") and parent:{ not c:"valueC" }'; expect(query).toEqual(expectedQuery); }); @@ -800,7 +879,7 @@ describe('build_exceptions_query', () => { exclude, }); const expectedQuery = - '(some.parentField:{ nested.field:"some value" } and some.not.nested.field:"some value") or (b:("value-1" or "value-2") and parent:{ c:"valueC" and d:"valueD" } and e:("value-1" or "value-2"))'; + '(some.parentField:{ nested.field:"some value" } and some.not.nested.field:"some value") or (b:("value-1" or "value-2") and parent:{ not c:"valueC" and not d:"valueD" } and e:("value-1" or "value-2"))'; expect(query).toEqual([{ query: expectedQuery, language: 'kuery' }]); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.ts b/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.ts index fc4fbae02b8fb..ff492dcda3b66 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.ts @@ -126,7 +126,7 @@ export const buildNested = ({ }): string => { const { field, entries } = item; const and = getLanguageBooleanOperator({ language, value: 'and' }); - const values = entries.map((entry) => `${entry.field}:"${entry.value}"`); + const values = entries.map((entry) => evaluateValues({ item: entry, language })); return `${field}:{ ${values.join(` ${and} `)} }`; }; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_version_number.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_version_number.ts index bbba7c5b8f3bb..a2f5ca3da1b70 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_version_number.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_version_number.ts @@ -19,3 +19,5 @@ export const DefaultVersionNumber = new t.Type; diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index e311e358e6146..6ea0c36328eed 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -9,3 +9,5 @@ export const alertsIndexPattern = 'logs-endpoint.alerts-*'; export const metadataIndexPattern = 'metrics-endpoint.metadata-*'; export const policyIndexPattern = 'metrics-endpoint.policy-*'; export const telemetryIndexPattern = 'metrics-endpoint.telemetry-*'; +export const LIMITED_CONCURRENCY_ENDPOINT_ROUTE_TAG = 'endpoint:limited-concurrency'; +export const LIMITED_CONCURRENCY_ENDPOINT_COUNT = 100; diff --git a/x-pack/plugins/security_solution/common/shared_exports.ts b/x-pack/plugins/security_solution/common/shared_exports.ts index 1b5b17ef35cae..bd1086a3f21e9 100644 --- a/x-pack/plugins/security_solution/common/shared_exports.ts +++ b/x-pack/plugins/security_solution/common/shared_exports.ts @@ -7,6 +7,10 @@ export { NonEmptyString } from './detection_engine/schemas/types/non_empty_string'; export { DefaultUuid } from './detection_engine/schemas/types/default_uuid'; export { DefaultStringArray } from './detection_engine/schemas/types/default_string_array'; +export { + DefaultVersionNumber, + DefaultVersionNumberDecoded, +} from './detection_engine/schemas/types/default_version_number'; export { exactCheck } from './exact_check'; export { getPaths, foldLeftRight } from './test_utils'; export { validate, validateEither } from './validate'; diff --git a/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts b/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts index 84ca1e20e9576..cd4573817cc27 100644 --- a/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts @@ -153,7 +153,7 @@ describe('Events Viewer', () => { }); }); - context('Events columns', () => { + context.skip('Events columns', () => { before(() => { loginAndWaitForPage(HOSTS_URL); openEvents(); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx index ed844b5130c77..fab2b1e4a7463 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field.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 React, { useMemo, useCallback } from 'react'; +import React, { useState, useMemo, useCallback } from 'react'; import { EuiComboBoxOptionOption, EuiComboBox } from '@elastic/eui'; import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; @@ -19,6 +19,7 @@ interface OperatorProps { isClearable: boolean; fieldTypeFilter?: string[]; fieldInputWidth?: number; + isRequired?: boolean; onChange: (a: IFieldType[]) => void; } @@ -29,10 +30,12 @@ export const FieldComponent: React.FC = ({ isLoading = false, isDisabled = false, isClearable = false, + isRequired = false, fieldTypeFilter = [], fieldInputWidth = 190, onChange, }): JSX.Element => { + const [touched, setIsTouched] = useState(false); const getLabel = useCallback((field): string => field.name, []); const optionsMemo = useMemo((): IFieldType[] => { if (indexPattern != null) { @@ -74,6 +77,8 @@ export const FieldComponent: React.FC = ({ isLoading={isLoading} isDisabled={isDisabled} isClearable={isClearable} + isInvalid={isRequired ? touched && selectedField == null : false} + onFocus={() => setIsTouched(true)} singleSelection={{ asPlainText: true }} data-test-subj="fieldAutocompleteComboBox" style={{ width: `${fieldInputWidth}px` }} diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx index 1ff5d770521f3..90e195b6e95a0 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx @@ -15,7 +15,7 @@ import { getField } from '../../../../../../../src/plugins/data/common/index_pat import { ListSchema } from '../../../lists_plugin_deps'; import { getFoundListSchemaMock } from '../../../../../lists/common/schemas/response/found_list_schema.mock'; import { getListResponseMock } from '../../../../../lists/common/schemas/response/list_schema.mock'; -import { DATE_NOW } from '../../../../../lists/common/constants.mock'; +import { DATE_NOW, VERSION, IMMUTABLE } from '../../../../../lists/common/constants.mock'; import { AutocompleteFieldListsComponent } from './field_value_lists'; @@ -221,6 +221,8 @@ describe('AutocompleteFieldListsComponent', () => { type: 'ip', updated_at: DATE_NOW, updated_by: 'some user', + version: VERSION, + immutable: IMMUTABLE, }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx index a9d85452651b5..cd90d6eb85623 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx @@ -18,6 +18,7 @@ interface AutocompleteFieldListsProps { isLoading: boolean; isDisabled: boolean; isClearable: boolean; + isRequired?: boolean; onChange: (arg: ListSchema) => void; } @@ -28,8 +29,10 @@ export const AutocompleteFieldListsComponent: React.FC { + const [touched, setIsTouched] = useState(false); const { http } = useKibana().services; const [lists, setLists] = useState([]); const { loading, result, start } = useFindLists(); @@ -97,6 +100,8 @@ export const AutocompleteFieldListsComponent: React.FC setIsTouched(true)} singleSelection={{ asPlainText: true }} sortMatchesBy="startsWith" data-test-subj="valuesAutocompleteComboBox listsComboxBox" diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx index a082811920f88..992005b3be8bc 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.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 React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { EuiComboBoxOptionOption, EuiComboBox } from '@elastic/eui'; import { uniq } from 'lodash'; @@ -22,6 +22,7 @@ interface AutocompleteFieldMatchProps { isLoading: boolean; isDisabled: boolean; isClearable: boolean; + isRequired?: boolean; fieldInputWidth?: number; onChange: (arg: string) => void; } @@ -34,9 +35,11 @@ export const AutocompleteFieldMatchComponent: React.FC { + const [touched, setIsTouched] = useState(false); const [isLoadingSuggestions, suggestions, updateSuggestions] = useFieldValueAutocomplete({ selectedField, operatorType: OperatorTypeEnum.MATCH, @@ -96,7 +99,8 @@ export const AutocompleteFieldMatchComponent: React.FC setIsTouched(true)} sortMatchesBy="startsWith" data-test-subj="valuesAutocompleteComboBox matchComboxBox" style={fieldInputWidth ? { width: `${fieldInputWidth}px` } : {}} diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx index 461d49dddfdef..27807a752c141 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.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 React, { useCallback, useMemo } from 'react'; +import React, { useState, useCallback, useMemo } from 'react'; import { EuiComboBoxOptionOption, EuiComboBox } from '@elastic/eui'; import { uniq } from 'lodash'; @@ -22,6 +22,7 @@ interface AutocompleteFieldMatchAnyProps { isLoading: boolean; isDisabled: boolean; isClearable: boolean; + isRequired?: boolean; onChange: (arg: string[]) => void; } @@ -33,8 +34,10 @@ export const AutocompleteFieldMatchAnyComponent: React.FC { + const [touched, setIsTouched] = useState(false); const [isLoadingSuggestions, suggestions, updateSuggestions] = useFieldValueAutocomplete({ selectedField, operatorType: OperatorTypeEnum.MATCH_ANY, @@ -92,7 +95,8 @@ export const AutocompleteFieldMatchAnyComponent: React.FC setIsTouched(true)} delimiter=", " data-test-subj="valuesAutocompleteComboBox matchAnyComboxBox" fullWidth diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts index cb07d99913107..b25bb245c6792 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts @@ -54,16 +54,16 @@ describe('helpers', () => { }); describe('#validateParams', () => { - test('returns true if value is undefined', () => { + test('returns false if value is undefined', () => { const isValid = validateParams(undefined, getField('@timestamp')); - expect(isValid).toBeTruthy(); + expect(isValid).toBeFalsy(); }); - test('returns true if value is empty string', () => { + test('returns false if value is empty string', () => { const isValid = validateParams('', getField('@timestamp')); - expect(isValid).toBeTruthy(); + expect(isValid).toBeFalsy(); }); test('returns true if type is "date" and value is valid', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts index 16659593784db..a65f1fa35d3c2 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts @@ -36,7 +36,7 @@ export const validateParams = ( ): boolean => { // Box would show error state if empty otherwise if (params == null || params === '') { - return true; + return false; } const types = field != null && field.esTypes != null ? field.esTypes : []; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index 049953e21febd..833688ae57993 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -15,11 +15,17 @@ import { wait as waitFor } from '@testing-library/react'; import { mockEventViewerResponse } from './mock'; import { StatefulEventsViewer } from '.'; +import { EventsViewer } from './events_viewer'; import { defaultHeaders } from './default_headers'; import { useFetchIndexPatterns } from '../../../detections/containers/detection_engine/rules/fetch_index_patterns'; import { mockBrowserFields, mockDocValueFields } from '../../containers/source/mock'; import { eventsDefaultModel } from './default_model'; import { useMountAppended } from '../../utils/use_mount_appended'; +import { inputsModel } from '../../store/inputs'; +import { TimelineId } from '../../../../common/types/timeline'; +import { KqlMode } from '../../../timelines/store/timeline/model'; +import { SortDirection } from '../../../timelines/components/timeline/body/sort'; +import { AlertsTableFilterGroup } from '../../../detections/components/alerts_table/alerts_filter_group'; jest.mock('../../components/url_state/normalize_time_range.ts'); @@ -40,6 +46,39 @@ const defaultMocks = { isLoading: false, }; +const utilityBar = (refetch: inputsModel.Refetch, totalCount: number) => ( +
+); + +const eventsViewerDefaultProps = { + browserFields: {}, + columns: [], + dataProviders: [], + deletedEventIds: [], + docValueFields: [], + end: to, + filters: [], + id: TimelineId.detectionsPage, + indexPattern: mockIndexPattern, + isLive: false, + isLoadingIndexPattern: false, + itemsPerPage: 10, + itemsPerPageOptions: [], + kqlMode: 'filter' as KqlMode, + onChangeItemsPerPage: jest.fn(), + query: { + query: '', + language: 'kql', + }, + start: from, + sort: { + columnId: 'foo', + sortDirection: 'none' as SortDirection, + }, + toggleColumn: jest.fn(), + utilityBar, +}; + describe('EventsViewer', () => { const mount = useMountAppended(); @@ -213,4 +252,212 @@ describe('EventsViewer', () => { }); }); }); + + describe('headerFilterGroup', () => { + test('it renders the provided headerFilterGroup', async () => { + const wrapper = mount( + + + } + /> + + + ); + + await waitFor(() => { + wrapper.update(); + + expect(wrapper.find(`[data-test-subj="alerts-table-filter-group"]`).exists()).toBe(true); + }); + }); + + test('it has a visible HeaderFilterGroupWrapper when Resolver is NOT showing, because graphEventId is undefined', async () => { + const wrapper = mount( + + + } + /> + + + ); + + await waitFor(() => { + wrapper.update(); + + expect( + wrapper.find(`[data-test-subj="header-filter-group-wrapper"]`).first() + ).not.toHaveStyleRule('visibility', 'hidden'); + }); + }); + + test('it has a visible HeaderFilterGroupWrapper when Resolver is NOT showing, because graphEventId is an empty string', async () => { + const wrapper = mount( + + + } + /> + + + ); + + await waitFor(() => { + wrapper.update(); + + expect( + wrapper.find(`[data-test-subj="header-filter-group-wrapper"]`).first() + ).not.toHaveStyleRule('visibility', 'hidden'); + }); + }); + + test('it does NOT have a visible HeaderFilterGroupWrapper when Resolver is showing, because graphEventId is a valid id', async () => { + const wrapper = mount( + + + } + /> + + + ); + + await waitFor(() => { + wrapper.update(); + + expect( + wrapper.find(`[data-test-subj="header-filter-group-wrapper"]`).first() + ).toHaveStyleRule('visibility', 'hidden'); + }); + }); + + test('it (still) renders an invisible headerFilterGroup (to maintain state while hidden) when Resolver is showing, because graphEventId is a valid id', async () => { + const wrapper = mount( + + + } + /> + + + ); + + await waitFor(() => { + wrapper.update(); + + expect(wrapper.find(`[data-test-subj="alerts-table-filter-group"]`).exists()).toBe(true); + }); + }); + }); + + describe('utilityBar', () => { + test('it renders the provided utilityBar when Resolver is NOT showing, because graphEventId is undefined', async () => { + const wrapper = mount( + + + + + + ); + + await waitFor(() => { + wrapper.update(); + + expect(wrapper.find(`[data-test-subj="mock-utility-bar"]`).exists()).toBe(true); + }); + }); + + test('it renders the provided utilityBar when Resolver is NOT showing, because graphEventId is an empty string', async () => { + const wrapper = mount( + + + + + + ); + + await waitFor(() => { + wrapper.update(); + + expect(wrapper.find(`[data-test-subj="mock-utility-bar"]`).exists()).toBe(true); + }); + }); + + test('it does NOT render the provided utilityBar when Resolver is showing, because graphEventId is a valid id', async () => { + const wrapper = mount( + + + + + + ); + + await waitFor(() => { + wrapper.update(); + + expect(wrapper.find(`[data-test-subj="mock-utility-bar"]`).exists()).toBe(false); + }); + }); + }); + + describe('header inspect button', () => { + test('it renders the inspect button when Resolver is NOT showing, because graphEventId is undefined', async () => { + const wrapper = mount( + + + + + + ); + + await waitFor(() => { + wrapper.update(); + + expect(wrapper.find(`[data-test-subj="inspect-icon-button"]`).exists()).toBe(true); + }); + }); + + test('it renders the inspect button when Resolver is NOT showing, because graphEventId is an empty string', async () => { + const wrapper = mount( + + + + + + ); + + await waitFor(() => { + wrapper.update(); + + expect(wrapper.find(`[data-test-subj="inspect-icon-button"]`).exists()).toBe(true); + }); + }); + + test('it does NOT render the inspect button when Resolver is showing, because graphEventId is a valid id', async () => { + const wrapper = mount( + + + + + + ); + + await waitFor(() => { + wrapper.update(); + + expect(wrapper.find(`[data-test-subj="inspect-icon-button"]`).exists()).toBe(false); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index 3f474da102ca4..bc036b38524ba 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -22,7 +22,7 @@ import { StatefulBody } from '../../../timelines/components/timeline/body/statef import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; import { OnChangeItemsPerPage } from '../../../timelines/components/timeline/events'; import { Footer, footerHeight } from '../../../timelines/components/timeline/footer'; -import { combineQueries } from '../../../timelines/components/timeline/helpers'; +import { combineQueries, resolverIsShowing } from '../../../timelines/components/timeline/helpers'; import { TimelineRefetch } from '../../../timelines/components/timeline/refetch_timeline'; import { EventDetailsWidthProvider } from './event_details_width_context'; import * as i18n from './translations'; @@ -73,6 +73,16 @@ const EventsContainerLoading = styled.div` overflow: auto; `; +/** + * Hides stateful headerFilterGroup implementations, but prevents the component + * from being unmounted, to preserve the state of the component + */ +const HeaderFilterGroupWrapper = styled.header<{ show: boolean }>` + ${({ show }) => css` + ${show ? '' : 'visibility: hidden;'}; + `} +`; + interface Props { browserFields: BrowserFields; columns: ColumnHeaderOptions[]; @@ -234,14 +244,21 @@ const EventsViewerComponent: React.FC = ({ return ( <> - {headerFilterGroup} + {headerFilterGroup && ( + + {headerFilterGroup} + + )} - {utilityBar && ( + {utilityBar && !resolverIsShowing(graphEventId) && ( {utilityBar?.(refetch, totalCountMinusDeleted)} )} @@ -307,6 +324,7 @@ export const EventsViewer = React.memo( prevProps.deletedEventIds === nextProps.deletedEventIds && prevProps.end === nextProps.end && deepEqual(prevProps.filters, nextProps.filters) && + prevProps.headerFilterGroup === nextProps.headerFilterGroup && prevProps.height === nextProps.height && prevProps.id === nextProps.id && deepEqual(prevProps.indexPattern, nextProps.indexPattern) && diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index e630645ef8c4e..6e77cd7082d56 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -114,9 +114,12 @@ export const AddExceptionModal = memo(function AddExceptionModal({ const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex(); const [ { isLoading: isSignalIndexPatternLoading, indexPatterns: signalIndexPatterns }, - ] = useFetchIndexPatterns(signalIndexName !== null ? [signalIndexName] : []); + ] = useFetchIndexPatterns(signalIndexName !== null ? [signalIndexName] : [], 'signals'); - const [{ isLoading: isIndexPatternLoading, indexPatterns }] = useFetchIndexPatterns(ruleIndices); + const [{ isLoading: isIndexPatternLoading, indexPatterns }] = useFetchIndexPatterns( + ruleIndices, + 'rules' + ); const onError = useCallback( (error: Error) => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.test.tsx index 9ca7a371ce81b..0f3b6ec2e94e4 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_exception_item.test.tsx @@ -12,10 +12,8 @@ import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { ExceptionListItemComponent } from './builder_exception_item'; import { fields } from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks.ts'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { - getEntryMatchMock, - getEntryMatchAnyMock, -} from '../../../../../../lists/common/schemas/types/entries.mock'; +import { getEntryMatchMock } from '../../../../../../lists/common/schemas/types/entry_match.mock'; +import { getEntryMatchAnyMock } from '../../../../../../lists/common/schemas/types/entry_match_any.mock'; describe('ExceptionListItemComponent', () => { describe('and badge logic', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx index 0f5000c8c0abe..7bf279168a9a0 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx @@ -117,6 +117,7 @@ export const EntryItemComponent: React.FC = ({ isDisabled={isLoading} onChange={handleFieldChange} data-test-subj="exceptionBuilderEntryField" + isRequired /> ); @@ -170,6 +171,7 @@ export const EntryItemComponent: React.FC = ({ isClearable={false} indexPattern={indexPattern} onChange={handleFieldMatchValueChange} + isRequired data-test-subj="exceptionBuilderEntryFieldMatch" /> ); @@ -185,6 +187,7 @@ export const EntryItemComponent: React.FC = ({ isClearable={false} indexPattern={indexPattern} onChange={handleFieldMatchAnyValueChange} + isRequired data-test-subj="exceptionBuilderEntryFieldMatchAny" /> ); @@ -199,6 +202,7 @@ export const EntryItemComponent: React.FC = ({ isDisabled={isLoading} isClearable={false} onChange={handleFieldListValueChange} + isRequired data-test-subj="exceptionBuilderEntryFieldList" /> ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index d07a8b5f0d2f6..2d12cfbec160a 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -97,9 +97,12 @@ export const EditExceptionModal = memo(function EditExceptionModal({ const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex(); const [ { isLoading: isSignalIndexPatternLoading, indexPatterns: signalIndexPatterns }, - ] = useFetchIndexPatterns(signalIndexName !== null ? [signalIndexName] : []); + ] = useFetchIndexPatterns(signalIndexName !== null ? [signalIndexName] : [], 'signals'); - const [{ isLoading: isIndexPatternLoading, indexPatterns }] = useFetchIndexPatterns(ruleIndices); + const [{ isLoading: isIndexPatternLoading, indexPatterns }] = useFetchIndexPatterns( + ruleIndices, + 'rules' + ); const onError = useCallback( (error) => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index 7171d3c6b815e..dace2eb5f0672 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -38,18 +38,20 @@ import { existsOperator, doesNotExistOperator, } from '../autocomplete/operators'; -import { OperatorTypeEnum, OperatorEnum } from '../../../lists_plugin_deps'; +import { OperatorTypeEnum, OperatorEnum, EntryNested } from '../../../lists_plugin_deps'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { - getEntryExistsMock, - getEntryListMock, - getEntryMatchMock, - getEntryMatchAnyMock, - getEntriesArrayMock, -} from '../../../../../lists/common/schemas/types/entries.mock'; +import { getEntryMatchMock } from '../../../../../lists/common/schemas/types/entry_match.mock'; +import { getEntryMatchAnyMock } from '../../../../../lists/common/schemas/types/entry_match_any.mock'; +import { getEntryExistsMock } from '../../../../../lists/common/schemas/types/entry_exists.mock'; +import { getEntryListMock } from '../../../../../lists/common/schemas/types/entry_list.mock'; import { getCommentsArrayMock } from '../../../../../lists/common/schemas/types/comments.mock'; +import { getEntriesArrayMock } from '../../../../../lists/common/schemas/types/entries.mock'; import { ENTRIES } from '../../../../../lists/common/constants.mock'; -import { ExceptionListItemSchema, EntriesArray } from '../../../../../lists/common/schemas'; +import { + CreateExceptionListItemSchema, + ExceptionListItemSchema, + EntriesArray, +} from '../../../../../lists/common/schemas'; import { IIndexPattern } from 'src/plugins/data/common'; describe('Exception helpers', () => { @@ -251,8 +253,8 @@ describe('Exception helpers', () => { { fieldName: 'host.name.host.name', isNested: true, - operator: 'is', - value: 'some host name', + operator: 'is one of', + value: ['some host name'], }, ]; expect(result).toEqual(expected); @@ -482,7 +484,7 @@ describe('Exception helpers', () => { }); describe('#filterExceptionItems', () => { - test('it removes empty entry items', () => { + test('it removes entry items with "value" of "undefined"', () => { const { entries, ...rest } = getExceptionListItemSchemaMock(); const mockEmptyException: EmptyEntry = { field: 'host.name', @@ -500,6 +502,85 @@ describe('Exception helpers', () => { expect(exceptions).toEqual([getExceptionListItemSchemaMock()]); }); + test('it removes "match" entry items with "value" of empty string', () => { + const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; + const mockEmptyException: EmptyEntry = { + field: 'host.name', + type: OperatorTypeEnum.MATCH, + operator: OperatorEnum.INCLUDED, + value: '', + }; + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); + }); + + test('it removes "match" entry items with "field" of empty string', () => { + const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; + const mockEmptyException: EmptyEntry = { + field: '', + type: OperatorTypeEnum.MATCH, + operator: OperatorEnum.INCLUDED, + value: 'some value', + }; + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); + }); + + test('it removes "match_any" entry items with "field" of empty string', () => { + const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; + const mockEmptyException: EmptyEntry = { + field: '', + type: OperatorTypeEnum.MATCH_ANY, + operator: OperatorEnum.INCLUDED, + value: ['some value'], + }; + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); + }); + + test('it removes "nested" entry items with "field" of empty string', () => { + const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; + const mockEmptyException: EntryNested = { + field: '', + type: OperatorTypeEnum.NESTED, + entries: [{ ...getEntryMatchMock() }], + }; + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); + }); + test('it removes `temporaryId` from items', () => { const { meta, ...rest } = getNewExceptionItem({ listType: 'detection', @@ -509,7 +590,7 @@ describe('Exception helpers', () => { }); const exceptions = filterExceptionItems([{ ...rest, meta }]); - expect(exceptions).toEqual([{ ...rest, meta: undefined }]); + expect(exceptions).toEqual([{ ...rest, entries: [], meta: undefined }]); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index 3d028431de8ff..4d8fc5f68870b 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -39,6 +39,7 @@ import { EntryNested, } from '../../../lists_plugin_deps'; import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; +import { validate } from '../../../../common/validate'; import { TimelineNonEcsData } from '../../../graphql/types'; import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; @@ -348,11 +349,22 @@ export const filterExceptionItems = ( ): Array => { return exceptions.reduce>( (acc, exception) => { - const entries = exception.entries.filter((t) => entry.is(t) || entriesNested.is(t)); + const entries = exception.entries.filter((t) => { + const [validatedEntry] = validate(t, entry); + const [validatedNestedEntry] = validate(t, entriesNested); + + if (validatedEntry != null || validatedNestedEntry != null) { + return true; + } + + return false; + }); + const item = { ...exception, entries }; + if (exceptionListItemSchema.is(item)) { return [...acc, item]; - } else if (createExceptionListItemSchema.is(item) && item.meta != null) { + } else if (createExceptionListItemSchema.is(item)) { const { meta, ...rest } = item; const itemSansMetaId: CreateExceptionListItemSchema = { ...rest, meta: undefined }; return [...acc, itemSansMetaId]; diff --git a/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx b/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx index 65901ec589daf..b52438486406e 100644 --- a/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx +++ b/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx @@ -39,16 +39,27 @@ const Wrapper = styled.aside<{ isSticky?: boolean }>` `; Wrapper.displayName = 'Wrapper'; +const FiltersGlobalContainer = styled.header<{ show: boolean }>` + ${({ show }) => css` + ${show ? '' : 'display: none;'}; + `} +`; + +FiltersGlobalContainer.displayName = 'FiltersGlobalContainer'; + export interface FiltersGlobalProps { children: React.ReactNode; + show?: boolean; } -export const FiltersGlobal = React.memo(({ children }) => ( +export const FiltersGlobal = React.memo(({ children, show = true }) => ( {({ style, isSticky }) => ( - - {children} - + + + {children} + + )} )); diff --git a/x-pack/plugins/security_solution/public/common/components/header_section/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_section/index.test.tsx index b33ce22651d65..32f6216be63f2 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_section/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_section/index.test.tsx @@ -131,4 +131,28 @@ describe('HeaderSection', () => { .exists() ).toBe(true); }); + + test('it renders an inspect button when an `id` is provided', () => { + const wrapper = mount( + + +

{'Test children'}

+
+
+ ); + + expect(wrapper.find('[data-test-subj="inspect-icon-button"]').first().exists()).toBe(true); + }); + + test('it does NOT an inspect button when an `id` is NOT provided', () => { + const wrapper = mount( + + +

{'Test children'}

+
+
+ ); + + expect(wrapper.find('[data-test-subj="inspect-icon-button"]').first().exists()).toBe(false); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_filter_group/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_filter_group/index.tsx index ba64868b70817..a227d2d3c3a8e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_filter_group/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_filter_group/index.tsx @@ -36,7 +36,7 @@ const AlertsTableFilterGroupComponent: React.FC = ({ onFilterGroupChanged }, [setFilterGroup, onFilterGroupChanged]); return ( - + (container: ReactWrapper

, file: File) => Promise = async ( @@ -26,7 +26,7 @@ const mockSelectFile:

(container: ReactWrapper

, file: File) => Promise { if (fileChange) { - fileChange(([file] as unknown) as FormEvent); + fileChange(({ item: () => file } as unknown) as FormEvent); } }); }; @@ -83,6 +83,29 @@ describe('ValueListsForm', () => { expect(onError).toHaveBeenCalledWith('whoops'); }); + it('disables upload and displays an error if file has invalid extension', async () => { + const badMockFile = ({ + name: 'foo.pdf', + type: 'application/pdf', + } as unknown) as File; + + const container = mount( + + + + ); + + await mockSelectFile(container, badMockFile); + + expect( + container.find('button[data-test-subj="value-lists-form-import-action"]').prop('disabled') + ).toEqual(true); + + expect(container.find('div[data-test-subj="value-list-file-picker-row"]').text()).toContain( + 'File must be one of the following types: [text/csv, text/plain]' + ); + }); + it('calls onSuccess if import succeeds', async () => { mockUseImportList.mockImplementation(() => ({ start: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx index b8416c3242e4a..aab665289e80d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx @@ -46,6 +46,7 @@ const options: ListTypeOptions[] = [ ]; const defaultListType: Type = 'keyword'; +const validFileTypes = ['text/csv', 'text/plain']; export interface ValueListsFormProps { onError: (error: Error) => void; @@ -54,23 +55,29 @@ export interface ValueListsFormProps { export const ValueListsFormComponent: React.FC = ({ onError, onSuccess }) => { const ctrl = useRef(new AbortController()); - const [files, setFiles] = useState(null); + const [file, setFile] = useState(null); const [type, setType] = useState(defaultListType); const filePickerRef = useRef(null); const { http } = useKibana().services; const { start: importList, ...importState } = useImportList(); + const fileIsValid = !file || validFileTypes.some((fileType) => file.type === fileType); + // EuiRadioGroup's onChange only infers 'string' from our options const handleRadioChange = useCallback((t: string) => setType(t as Type), [setType]); + const handleFileChange = useCallback((files: FileList | null) => { + setFile(files?.item(0) ?? null); + }, []); + const resetForm = useCallback(() => { if (filePickerRef.current?.fileInput) { filePickerRef.current.fileInput.value = ''; filePickerRef.current.handleChange(); } - setFiles(null); + setFile(null); setType(defaultListType); - }, [setType]); + }, []); const handleCancel = useCallback(() => { ctrl.current.abort(); @@ -91,17 +98,17 @@ export const ValueListsFormComponent: React.FC = ({ onError ); const handleImport = useCallback(() => { - if (!importState.loading && files && files.length) { + if (!importState.loading && file) { ctrl.current = new AbortController(); importList({ - file: files[0], + file, listId: undefined, http, signal: ctrl.current.signal, type, }); } - }, [importState.loading, files, importList, http, type]); + }, [importState.loading, file, importList, http, type]); useEffect(() => { if (!importState.loading && importState.result) { @@ -117,14 +124,22 @@ export const ValueListsFormComponent: React.FC = ({ onError return ( - + @@ -151,7 +166,7 @@ export const ValueListsFormComponent: React.FC = ({ onError {i18n.UPLOAD_BUTTON} diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx index d7d4be6d951b8..dc72260439090 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx @@ -46,6 +46,7 @@ export const ValueListsModalComponent: React.FC = ({ const { start: findLists, ...lists } = useFindLists(); const { start: deleteList, result: deleteResult } = useDeleteList(); const [exportListId, setExportListId] = useState(); + const [deletingListIds, setDeletingListIds] = useState([]); const { addError, addSuccess } = useAppToasts(); const fetchLists = useCallback(() => { @@ -54,16 +55,18 @@ export const ValueListsModalComponent: React.FC = ({ const handleDelete = useCallback( ({ id }: { id: string }) => { + setDeletingListIds([...deletingListIds, id]); deleteList({ http, id }); }, - [deleteList, http] + [deleteList, deletingListIds, http] ); useEffect(() => { - if (deleteResult != null) { + if (deleteResult != null && deletingListIds.length > 0) { + setDeletingListIds([...deletingListIds.filter((id) => id !== deleteResult.id)]); fetchLists(); } - }, [deleteResult, fetchLists]); + }, [deleteResult, deletingListIds, fetchLists]); const handleExport = useCallback( async ({ ids }: { ids: string[] }) => @@ -116,6 +119,12 @@ export const ValueListsModalComponent: React.FC = ({ return null; } + const tableItems = (lists.result?.data ?? []).map((item) => ({ + ...item, + isExporting: item.id === exportListId, + isDeleting: deletingListIds.includes(item.id), + })); + const pagination = { pageIndex, pageSize, @@ -133,7 +142,7 @@ export const ValueListsModalComponent: React.FC = ({ + lists.map((list) => ({ + ...list, + isDeleting: false, + isExporting: false, + })); describe('ValueListsTable', () => { it('renders a row for each list', () => { const lists = Array(3).fill(getListResponseMock()); + const items = buildItems(lists); const container = mount( { it('calls onChange when pagination is modified', () => { const lists = Array(6).fill(getListResponseMock()); + const items = buildItems(lists); const onChange = jest.fn(); const container = mount( { it('calls onExport when export is clicked', () => { const lists = Array(3).fill(getListResponseMock()); + const items = buildItems(lists); const onExport = jest.fn(); const container = mount( { it('calls onDelete when delete is clicked', () => { const lists = Array(3).fill(getListResponseMock()); + const items = buildItems(lists); const onDelete = jest.fn(); const container = mount( ; -type ActionCallback = (item: ListSchema) => void; +import { buildColumns } from './table_helpers'; +import { TableProps, TableItemCallback } from './types'; export interface ValueListsTableProps { - lists: TableProps['items']; + items: TableProps['items']; loading: boolean; onChange: TableProps['onChange']; - onExport: ActionCallback; - onDelete: ActionCallback; + onExport: TableItemCallback; + onDelete: TableItemCallback; pagination: Exclude; } -const buildColumns = ( - onExport: ActionCallback, - onDelete: ActionCallback -): TableProps['columns'] => [ - { - field: 'name', - name: i18n.COLUMN_FILE_NAME, - truncateText: true, - }, - { - field: 'created_at', - name: i18n.COLUMN_UPLOAD_DATE, - /* eslint-disable-next-line react/display-name */ - render: (value: ListSchema['created_at']) => ( - - ), - width: '30%', - }, - { - field: 'created_by', - name: i18n.COLUMN_CREATED_BY, - truncateText: true, - width: '20%', - }, - { - name: i18n.COLUMN_ACTIONS, - actions: [ - { - name: i18n.ACTION_EXPORT_NAME, - description: i18n.ACTION_EXPORT_DESCRIPTION, - icon: 'exportAction', - type: 'icon', - onClick: onExport, - 'data-test-subj': 'action-export-value-list', - }, - { - name: i18n.ACTION_DELETE_NAME, - description: i18n.ACTION_DELETE_DESCRIPTION, - icon: 'trash', - type: 'icon', - onClick: onDelete, - 'data-test-subj': 'action-delete-value-list', - }, - ], - width: '15%', - }, -]; - export const ValueListsTableComponent: React.FC = ({ - lists, + items, loading, onChange, onExport, @@ -87,7 +36,7 @@ export const ValueListsTableComponent: React.FC = ({ theme.eui.euiSizeXS}; + vertical-align: middle; +`; + +const ActionButton: React.FC<{ + content: string; + dataTestSubj: string; + icon: IconType; + isLoading: boolean; + item: TableItem; + onClick: TableItemCallback; +}> = ({ content, dataTestSubj, icon, item, onClick, isLoading }) => ( + + {isLoading ? ( + + ) : ( + onClick(item)} + /> + )} + +); + +export const buildColumns = ( + onExport: TableItemCallback, + onDelete: TableItemCallback +): TableProps['columns'] => [ + { + field: 'name', + name: i18n.COLUMN_FILE_NAME, + truncateText: true, + }, + { + field: 'created_at', + name: i18n.COLUMN_UPLOAD_DATE, + render: (value: ListSchema['created_at']) => ( + + ), + width: '30%', + }, + { + field: 'created_by', + name: i18n.COLUMN_CREATED_BY, + truncateText: true, + width: '20%', + }, + { + name: i18n.COLUMN_ACTIONS, + actions: [ + { + render: (item) => ( + + ), + }, + { + render: (item) => ( + + ), + }, + ], + width: '15%', + }, +]; diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/translations.ts b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/translations.ts index dca6e43a98143..91f3f3797f422 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/translations.ts @@ -24,6 +24,12 @@ export const FILE_PICKER_PROMPT = i18n.translate( } ); +export const FILE_PICKER_INVALID_FILE_TYPE = (fileTypes: string): string => + i18n.translate('xpack.securitySolution.lists.uploadValueListExtensionValidationMessage', { + values: { fileTypes }, + defaultMessage: 'File must be one of the following types: [{fileTypes}]', + }); + export const CLOSE_BUTTON = i18n.translate( 'xpack.securitySolution.lists.closeValueListsModalTitle', { diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/types.ts b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/types.ts new file mode 100644 index 0000000000000..f85e275247728 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/types.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 { EuiBasicTableProps } from '@elastic/eui'; + +import { ListSchema } from '../../../../../lists/common/schemas/response'; + +export interface TableItem extends ListSchema { + isDeleting: boolean; + isExporting: boolean; +} +export type TableProps = EuiBasicTableProps; +export type TableItemCallback = (item: TableItem) => void; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx index 6257a9980e00c..c0997a5e62908 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx @@ -38,7 +38,14 @@ const DEFAULT_BROWSER_FIELDS = {}; const DEFAULT_INDEX_PATTERNS = { fields: [], title: '' }; const DEFAULT_DOC_VALUE_FIELDS: DocValueFields[] = []; -export const useFetchIndexPatterns = (defaultIndices: string[] = []): Return => { +// Fun fact: When using this hook multiple times within a component (e.g. add_exception_modal & edit_exception_modal), +// the apolloClient will perform queryDeduplication and prevent the first query from executing. A deep compare is not +// performed on `indices`, so another field must be passed to circumvent this. +// For details, see https://github.com/apollographql/react-apollo/issues/2202 +export const useFetchIndexPatterns = ( + defaultIndices: string[] = [], + queryDeduplication?: string +): Return => { const apolloClient = useApolloClient(); const [indices, setIndices] = useState(defaultIndices); @@ -74,6 +81,7 @@ export const useFetchIndexPatterns = (defaultIndices: string[] = []): Return => variables: { sourceId: 'default', defaultIndex: indices, + ...(queryDeduplication != null ? { queryDeduplication } : {}), }, context: { fetchOptions: { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx index e7a8c4854fa9e..110620fad7eba 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx @@ -82,6 +82,7 @@ describe('DetectionEnginePageComponent', () => { = ({ filters, + graphEventId, query, setAbsoluteRangeDatePicker, }) => { @@ -151,7 +156,7 @@ export const DetectionEnginePageComponent: React.FC = ({ {indicesExist ? ( - + @@ -232,13 +237,19 @@ export const DetectionEnginePageComponent: React.FC = ({ const makeMapStateToProps = () => { const getGlobalInputs = inputsSelectors.globalSelector(); + const getTimeline = timelineSelectors.getTimelineByIdSelector(); return (state: State) => { const globalInputs: InputsRange = getGlobalInputs(state); const { query, filters } = globalInputs; + const timeline: TimelineModel = + getTimeline(state, TimelineId.detectionsPage) ?? timelineDefaults; + const { graphEventId } = timeline; + return { query, filters, + graphEventId, }; }; }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx index a251c617e542a..5e6587dab1736 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx @@ -82,6 +82,7 @@ describe('RuleDetailsPageComponent', () => { { export const RuleDetailsPageComponent: FC = ({ filters, + graphEventId, query, setAbsoluteRangeDatePicker, }) => { @@ -351,7 +356,7 @@ export const RuleDetailsPageComponent: FC = ({ {indicesExist ? ( - + @@ -541,13 +546,19 @@ RuleDetailsPageComponent.displayName = 'RuleDetailsPageComponent'; const makeMapStateToProps = () => { const getGlobalInputs = inputsSelectors.globalSelector(); + const getTimeline = timelineSelectors.getTimelineByIdSelector(); return (state: State) => { const globalInputs: InputsRange = getGlobalInputs(state); const { query, filters } = globalInputs; + const timeline: TimelineModel = + getTimeline(state, TimelineId.detectionsRulesDetailsPage) ?? timelineDefaults; + const { graphEventId } = timeline; + return { query, filters, + graphEventId, }; }; }; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx index 447d003625c8f..781aa711ff0d9 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; +import { EuiHorizontalRule, EuiSpacer, EuiWindowEvent } from '@elastic/eui'; +import { noop } from 'lodash/fp'; import React, { useEffect, useCallback, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { StickyContainer } from 'react-sticky'; @@ -44,6 +45,13 @@ import { navTabsHostDetails } from './nav_tabs'; import { HostDetailsProps } from './types'; import { type } from './utils'; import { getHostDetailsPageFilters } from './helpers'; +import { showGlobalFilters } from '../../../timelines/components/timeline/helpers'; +import { useFullScreen } from '../../../common/containers/use_full_screen'; +import { Display } from '../display'; +import { timelineSelectors } from '../../../timelines/store/timeline'; +import { TimelineModel } from '../../../timelines/store/timeline/model'; +import { TimelineId } from '../../../../common/types/timeline'; +import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; const HostOverviewManage = manageQuery(HostOverview); const KpiHostDetailsManage = manageQuery(KpiHostsComponent); @@ -51,6 +59,7 @@ const KpiHostDetailsManage = manageQuery(KpiHostsComponent); const HostDetailsComponent = React.memo( ({ filters, + graphEventId, query, setAbsoluteRangeDatePicker, setHostDetailsTablesActivePageToZero, @@ -58,6 +67,7 @@ const HostDetailsComponent = React.memo( hostDetailsPagePath, }) => { const { to, from, deleteQuery, setQuery, isInitializing } = useGlobalTime(); + const { globalFullScreen } = useFullScreen(); useEffect(() => { setHostDetailsTablesActivePageToZero(); }, [setHostDetailsTablesActivePageToZero, detailName]); @@ -93,90 +103,93 @@ const HostDetailsComponent = React.memo( <> {indicesExist ? ( - + + - - - } - title={detailName} - /> - - - {({ hostOverview, loading, id, inspect, refetch }) => ( - - {({ isLoadingAnomaliesData, anomaliesData }) => ( - { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }} - /> - )} - - )} - - - - - - {({ kpiHostDetails, id, inspect, loading, refetch }) => ( - - )} - - - - - - - + + + + } + title={detailName} + /> + + + {({ hostOverview, loading, id, inspect, refetch }) => ( + + {({ isLoadingAnomaliesData, anomaliesData }) => ( + { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }} + /> + )} + + )} + + + + + + {({ kpiHostDetails, id, inspect, loading, refetch }) => ( + + )} + + + + + + + + { const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); - return (state: State) => ({ - query: getGlobalQuerySelector(state), - filters: getGlobalFiltersQuerySelector(state), - }); + const getTimeline = timelineSelectors.getTimelineByIdSelector(); + return (state: State) => { + const timeline: TimelineModel = + getTimeline(state, TimelineId.hostsPageEvents) ?? timelineDefaults; + const { graphEventId } = timeline; + + return { + query: getGlobalQuerySelector(state), + filters: getGlobalFiltersQuerySelector(state), + graphEventId, + }; + }; }; const mapDispatchToProps = { diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx index a3885eac5377c..1219effa5ff6d 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx @@ -26,6 +26,7 @@ import { KpiHostsQuery } from '../containers/kpi_hosts'; import { useFullScreen } from '../../common/containers/use_full_screen'; import { useGlobalTime } from '../../common/containers/use_global_time'; import { useWithSource } from '../../common/containers/source'; +import { TimelineId } from '../../../common/types/timeline'; import { LastEventIndexKey } from '../../graphql/types'; import { useKibana } from '../../common/lib/kibana'; import { convertToBuildEsQuery } from '../../common/lib/keury'; @@ -44,11 +45,15 @@ import { HostsComponentProps } from './types'; import { filterHostData } from './navigation'; import { hostsModel } from '../store'; import { HostsTableType } from '../store/model'; +import { showGlobalFilters } from '../../timelines/components/timeline/helpers'; +import { timelineSelectors } from '../../timelines/store/timeline'; +import { timelineDefaults } from '../../timelines/store/timeline/defaults'; +import { TimelineModel } from '../../timelines/store/timeline/model'; const KpiHostsComponentManage = manageQuery(KpiHostsComponent); export const HostsComponent = React.memo( - ({ filters, query, setAbsoluteRangeDatePicker, hostsPagePath }) => { + ({ filters, graphEventId, query, setAbsoluteRangeDatePicker, hostsPagePath }) => { const { to, from, deleteQuery, setQuery, isInitializing } = useGlobalTime(); const { globalFullScreen } = useFullScreen(); const capabilities = useMlCapabilities(); @@ -93,7 +98,7 @@ export const HostsComponent = React.memo( {indicesExist ? ( - + @@ -167,10 +172,22 @@ HostsComponent.displayName = 'HostsComponent'; const makeMapStateToProps = () => { const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); - const mapStateToProps = (state: State) => ({ - query: getGlobalQuerySelector(state), - filters: getGlobalFiltersQuerySelector(state), - }); + const getTimeline = timelineSelectors.getTimelineByIdSelector(); + const mapStateToProps = (state: State) => { + const hostsPageEventsTimeline: TimelineModel = + getTimeline(state, TimelineId.hostsPageEvents) ?? timelineDefaults; + const { graphEventId: hostsPageEventsGraphEventId } = hostsPageEventsTimeline; + + const hostsPageExternalAlertsTimeline: TimelineModel = + getTimeline(state, TimelineId.hostsPageExternalAlerts) ?? timelineDefaults; + const { graphEventId: hostsPageExternalAlertsGraphEventId } = hostsPageExternalAlertsTimeline; + + return { + query: getGlobalQuerySelector(state), + filters: getGlobalFiltersQuerySelector(state), + graphEventId: hostsPageEventsGraphEventId ?? hostsPageExternalAlertsGraphEventId, + }; + }; return mapStateToProps; }; diff --git a/x-pack/plugins/security_solution/public/network/pages/network.tsx b/x-pack/plugins/security_solution/public/network/pages/network.tsx index 0def110c45a14..ca8da4eb711e5 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.tsx @@ -41,6 +41,11 @@ import { OverviewEmpty } from '../../overview/components/overview_empty'; import * as i18n from './translations'; import { NetworkComponentProps } from './types'; import { NetworkRouteType } from './navigation/types'; +import { showGlobalFilters } from '../../timelines/components/timeline/helpers'; +import { timelineSelectors } from '../../timelines/store/timeline'; +import { TimelineId } from '../../../common/types/timeline'; +import { timelineDefaults } from '../../timelines/store/timeline/defaults'; +import { TimelineModel } from '../../timelines/store/timeline/model'; const KpiNetworkComponentManage = manageQuery(KpiNetworkComponent); const sourceId = 'default'; @@ -48,6 +53,7 @@ const sourceId = 'default'; const NetworkComponent = React.memo( ({ filters, + graphEventId, query, setAbsoluteRangeDatePicker, networkPagePath, @@ -100,7 +106,7 @@ const NetworkComponent = React.memo( {indicesExist ? ( - + @@ -189,10 +195,18 @@ NetworkComponent.displayName = 'NetworkComponent'; const makeMapStateToProps = () => { const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); - const mapStateToProps = (state: State) => ({ - query: getGlobalQuerySelector(state), - filters: getGlobalFiltersQuerySelector(state), - }); + const getTimeline = timelineSelectors.getTimelineByIdSelector(); + const mapStateToProps = (state: State) => { + const timeline: TimelineModel = + getTimeline(state, TimelineId.networkPageExternalAlerts) ?? timelineDefaults; + const { graphEventId } = timeline; + + return { + query: getGlobalQuerySelector(state), + filters: getGlobalFiltersQuerySelector(state), + graphEventId, + }; + }; return mapStateToProps; }; diff --git a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.test.ts b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.test.ts index bd534dcb989e3..40be175c9fdbb 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.test.ts @@ -5,7 +5,7 @@ */ import { IsometricTaxiLayout } from '../../types'; import { LegacyEndpointEvent } from '../../../../common/endpoint/types'; -import { isometricTaxiLayout } from './isometric_taxi_layout'; +import { isometricTaxiLayoutFactory } from './isometric_taxi_layout'; import { mockProcessEvent } from '../../models/process_event_test_helpers'; import { factory } from './index'; @@ -107,7 +107,7 @@ describe('resolver graph layout', () => { unique_ppid: 0, }, }); - layout = () => isometricTaxiLayout(factory(events)); + layout = () => isometricTaxiLayoutFactory(factory(events)); events = []; }); describe('when rendering no nodes', () => { diff --git a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts index 11c888d1462f8..1fc2ea0150aee 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import * as vector2 from '../../models/vector2'; import { IndexedProcessTree, Vector2, @@ -17,14 +16,17 @@ import { } from '../../types'; import * as event from '../../../../common/endpoint/models/event'; import { ResolverEvent } from '../../../../common/endpoint/types'; -import * as model from './index'; +import * as vector2 from '../vector2'; +import * as indexedProcessTreeModel from './index'; import { getFriendlyElapsedTime as elapsedTime } from '../../lib/date'; import { uniquePidForProcess } from '../process_event'; /** * Graph the process tree */ -export function isometricTaxiLayout(indexedProcessTree: IndexedProcessTree): IsometricTaxiLayout { +export function isometricTaxiLayoutFactory( + indexedProcessTree: IndexedProcessTree +): IsometricTaxiLayout { /** * Walk the tree in reverse level order, calculating the 'width' of subtrees. */ @@ -83,8 +85,8 @@ export function isometricTaxiLayout(indexedProcessTree: IndexedProcessTree): Iso */ function ariaLevels(indexedProcessTree: IndexedProcessTree): Map { const map: Map = new Map(); - for (const node of model.levelOrder(indexedProcessTree)) { - const parentNode = model.parent(indexedProcessTree, node); + for (const node of indexedProcessTreeModel.levelOrder(indexedProcessTree)) { + const parentNode = indexedProcessTreeModel.parent(indexedProcessTree, node); if (parentNode === undefined) { // nodes at the root have a level of 1 map.set(node, 1); @@ -143,16 +145,19 @@ function ariaLevels(indexedProcessTree: IndexedProcessTree): Map(); - if (model.size(indexedProcessTree) === 0) { + if (indexedProcessTreeModel.size(indexedProcessTree) === 0) { return widths; } const processesInReverseLevelOrder: ResolverEvent[] = [ - ...model.levelOrder(indexedProcessTree), + ...indexedProcessTreeModel.levelOrder(indexedProcessTree), ].reverse(); for (const process of processesInReverseLevelOrder) { - const children = model.children(indexedProcessTree, uniquePidForProcess(process)); + const children = indexedProcessTreeModel.children( + indexedProcessTree, + uniquePidForProcess(process) + ); const sumOfWidthOfChildren = function sumOfWidthOfChildren() { return children.reduce(function sum(currentValue, child) { @@ -229,7 +234,10 @@ function processEdgeLineSegments( metadata: edgeLineMetadata, }; - const siblings = model.children(indexedProcessTree, uniquePidForProcess(parent)); + const siblings = indexedProcessTreeModel.children( + indexedProcessTree, + uniquePidForProcess(parent) + ); const isFirstChild = process === siblings[0]; if (metadata.isOnlyChild) { @@ -384,8 +392,8 @@ function* levelOrderWithWidths( tree: IndexedProcessTree, widths: ProcessWidths ): Iterable { - for (const process of model.levelOrder(tree)) { - const parent = model.parent(tree, process); + for (const process of indexedProcessTreeModel.levelOrder(tree)) { + const parent = indexedProcessTreeModel.parent(tree, process); const width = widths.get(process); if (width === undefined) { @@ -423,7 +431,7 @@ function* levelOrderWithWidths( parentWidth, }; - const siblings = model.children(tree, uniquePidForProcess(parent)); + const siblings = indexedProcessTreeModel.children(tree, uniquePidForProcess(parent)); if (siblings.length === 1) { metadata.isOnlyChild = true; metadata.lastChildWidth = width; @@ -479,3 +487,32 @@ const distanceBetweenNodesInUnits = 2; * The distance in pixels (at scale 1) between nodes. Change this to space out nodes more */ const distanceBetweenNodes = distanceBetweenNodesInUnits * unit; + +export function nodePosition(model: IsometricTaxiLayout, node: ResolverEvent): Vector2 | undefined { + return model.processNodePositions.get(node); +} + +/** + * Return a clone of `model` with all positions incremented by `translation`. + * Use this to move the layout around. + * e.g. + * ``` + * translated(layout, [100, -200]) // return a copy of `layout`, thats been moved 100 to the right and 200 up + * ``` + */ +export function translated(model: IsometricTaxiLayout, translation: Vector2): IsometricTaxiLayout { + return { + processNodePositions: new Map( + [...model.processNodePositions.entries()].map(([node, position]) => [ + node, + vector2.add(position, translation), + ]) + ), + edgeLineSegments: model.edgeLineSegments.map(({ points, metadata }) => ({ + points: points.map((point) => vector2.add(point, translation)), + metadata, + })), + // these are unchanged + ariaLevels: model.ariaLevels, + }; +} diff --git a/x-pack/plugins/security_solution/public/resolver/store/actions.ts b/x-pack/plugins/security_solution/public/resolver/store/actions.ts index ac11ab8c88681..418eb0d837276 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/actions.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/actions.ts @@ -69,12 +69,9 @@ interface AppDetectedMissingEventData { */ interface UserFocusedOnResolverNode { readonly type: 'userFocusedOnResolverNode'; - readonly payload: { - /** - * Used to identify the process node that the user focused on (in the DOM) - */ - readonly nodeId: string; - }; + + /** focused nodeID */ + readonly payload: string; } /** @@ -85,16 +82,10 @@ interface UserFocusedOnResolverNode { */ interface UserSelectedResolverNode { readonly type: 'userSelectedResolverNode'; - readonly payload: { - /** - * The HTML ID used to identify the process node's element that the user selected - */ - readonly nodeId: string; - /** - * The process entity_id for the process the node represents - */ - readonly selectedProcessId: string; - }; + /** + * The nodeID (aka entity_id) that was select. + */ + readonly payload: string; } /** diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index 4098c6fc6c5dd..40138d3f2fd3c 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -30,8 +30,9 @@ import { ResolverRelatedEvents, } from '../../../../common/endpoint/types'; import * as resolverTreeModel from '../../models/resolver_tree'; -import { isometricTaxiLayout } from '../../models/indexed_process_tree/isometric_taxi_layout'; +import * as isometricTaxiLayoutModel from '../../models/indexed_process_tree/isometric_taxi_layout'; import { allEventCategories } from '../../../../common/endpoint/models/event'; +import * as vector2 from '../../models/vector2'; /** * If there is currently a request. @@ -70,6 +71,21 @@ const resolverTreeResponse = (state: DataState): ResolverTree | undefined => { } }; +/** + * the node ID of the node representing the databaseDocumentID. + * NB: this could be stale if the last response is stale + */ +export const originID: (state: DataState) => string | undefined = createSelector( + resolverTreeResponse, + function (resolverTree?) { + if (resolverTree) { + // This holds the entityID (aka nodeID) of the node related to the last fetched `_id` + return resolverTree.entityID; + } + return undefined; + } +); + /** * Process events that will be displayed as terminated. */ @@ -317,13 +333,45 @@ export function databaseDocumentIDToFetch(state: DataState): string | null { } } -export const layout = createSelector(tree, function processNodePositionsAndEdgeLineSegments( - /* eslint-disable no-shadow */ - indexedProcessTree - /* eslint-enable no-shadow */ -) { - return isometricTaxiLayout(indexedProcessTree); -}); +export const layout = createSelector( + tree, + originID, + function processNodePositionsAndEdgeLineSegments( + /* eslint-disable no-shadow */ + indexedProcessTree, + originID + /* eslint-enable no-shadow */ + ) { + // use the isometric taxi layout as a base + const taxiLayout = isometricTaxiLayoutModel.isometricTaxiLayoutFactory(indexedProcessTree); + + if (!originID) { + // no data has loaded. + return taxiLayout; + } + + // find the origin node + const originNode = indexedProcessTreeModel.processEvent(indexedProcessTree, originID); + + if (!originNode) { + // this should only happen if the `ResolverTree` from the server has an entity ID with no matching lifecycle events. + throw new Error('Origin node not found in ResolverTree'); + } + + // Find the position of the origin, we'll center the map on it intrinsically + const originPosition = isometricTaxiLayoutModel.nodePosition(taxiLayout, originNode); + // adjust the position of everything so that the origin node is at `(0, 0)` + + if (originPosition === undefined) { + // not sure how this could happen. + return taxiLayout; + } + + // Take the origin position, and multipy it by -1, then move the layout by that amount. + // This should center the layout around the origin. + return isometricTaxiLayoutModel.translated(taxiLayout, vector2.scale(originPosition, -1)); + } +); /** * Given a nodeID (aka entity_id) get the indexed process event. diff --git a/x-pack/plugins/security_solution/public/resolver/store/reducer.ts b/x-pack/plugins/security_solution/public/resolver/store/reducer.ts index fc4c4de5819f3..028c28d94a41b 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/reducer.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ import { Reducer, combineReducers } from 'redux'; -import { htmlIdGenerator } from '@elastic/eui'; import { animateProcessIntoView } from './methods'; import { cameraReducer } from './camera/reducer'; import { dataReducer } from './data/reducer'; @@ -12,51 +11,38 @@ import { ResolverAction } from './actions'; import { ResolverState, ResolverUIState } from '../types'; import { uniquePidForProcess } from '../models/process_event'; -/** - * Despite the name "generator", this function is entirely determinant - * (i.e. it will return the same html id given the same prefix 'resolverNode' - * and nodeId) - */ -const resolverNodeIdGenerator = htmlIdGenerator('resolverNode'); - const uiReducer: Reducer = ( - uiState = { - activeDescendantId: null, - selectedDescendantId: null, - processEntityIdOfSelectedDescendant: null, + state = { + ariaActiveDescendant: null, + selectedNode: null, }, action ) => { if (action.type === 'userFocusedOnResolverNode') { - return { - ...uiState, - activeDescendantId: action.payload.nodeId, + const next: ResolverUIState = { + ...state, + ariaActiveDescendant: action.payload, }; + return next; } else if (action.type === 'userSelectedResolverNode') { - return { - ...uiState, - selectedDescendantId: action.payload.nodeId, - processEntityIdOfSelectedDescendant: action.payload.selectedProcessId, + const next: ResolverUIState = { + ...state, + selectedNode: action.payload, }; + return next; } else if ( action.type === 'userBroughtProcessIntoView' || action.type === 'appDetectedNewIdFromQueryParams' ) { - /** - * This action has a process payload (instead of a processId), so we use - * `uniquePidForProcess` and `resolverNodeIdGenerator` to resolve the determinant - * html id of the node being brought into view. - */ - const processEntityId = uniquePidForProcess(action.payload.process); - const processNodeId = resolverNodeIdGenerator(processEntityId); - return { - ...uiState, - activeDescendantId: processNodeId, - selectedDescendantId: processNodeId, - processEntityIdOfSelectedDescendant: processEntityId, + const nodeID = uniquePidForProcess(action.payload.process); + const next: ResolverUIState = { + ...state, + ariaActiveDescendant: nodeID, + selectedNode: nodeID, }; + return next; } else { - return uiState; + return state; } }; diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts index 09293d0b3b683..66d7e04d118ed 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -144,26 +144,15 @@ export const relatedEventInfoByEntityId = composeSelectors( /** * Returns the id of the "current" tree node (fake-focused) */ -export const uiActiveDescendantId = composeSelectors( +export const ariaActiveDescendant = composeSelectors( uiStateSelector, - uiSelectors.activeDescendantId + uiSelectors.ariaActiveDescendant ); /** - * Returns the id of the "selected" tree node (the node that is currently "pressed" and possibly controlling other popups / components) + * Returns the nodeID of the selected node */ -export const uiSelectedDescendantId = composeSelectors( - uiStateSelector, - uiSelectors.selectedDescendantId -); - -/** - * Returns the entity_id of the "selected" tree node's process - */ -export const uiSelectedDescendantProcessId = composeSelectors( - uiStateSelector, - uiSelectors.selectedDescendantProcessId -); +export const selectedNode = composeSelectors(uiStateSelector, uiSelectors.selectedNode); /** * Returns the camera state from within ResolverState @@ -251,6 +240,14 @@ export const ariaLevel: ( dataSelectors.ariaLevel ); +/** + * the node ID of the node representing the databaseDocumentID + */ +export const originID: (state: ResolverState) => string | undefined = composeSelectors( + dataStateSelector, + dataSelectors.originID +); + /** * Takes a nodeID (aka entity_id) and returns the node ID of the node that aria should 'flowto' or null * If the node has a flowto candidate that is currently visible, that will be returned, otherwise null. diff --git a/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts index 494d8884329c6..91a2cbecbc04c 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts @@ -10,32 +10,21 @@ import { ResolverUIState } from '../../types'; /** * id of the "current" tree node (fake-focused) */ -export const activeDescendantId = createSelector( +export const ariaActiveDescendant = createSelector( (uiState: ResolverUIState) => uiState, /* eslint-disable no-shadow */ - ({ activeDescendantId }) => { - return activeDescendantId; + ({ ariaActiveDescendant }) => { + return ariaActiveDescendant; } ); /** * id of the currently "selected" tree node */ -export const selectedDescendantId = createSelector( +export const selectedNode = createSelector( (uiState: ResolverUIState) => uiState, /* eslint-disable no-shadow */ - ({ selectedDescendantId }) => { - return selectedDescendantId; - } -); - -/** - * id of the currently "selected" tree node - */ -export const selectedDescendantProcessId = createSelector( - (uiState: ResolverUIState) => uiState, - /* eslint-disable no-shadow */ - ({ processEntityIdOfSelectedDescendant }: ResolverUIState) => { - return processEntityIdOfSelectedDescendant; + ({ selectedNode }: ResolverUIState) => { + return selectedNode; } ); diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index 0272de0d8fd2a..856ae2d6240e3 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -34,17 +34,13 @@ export interface ResolverState { */ export interface ResolverUIState { /** - * The ID attribute of the resolver's aria-activedescendent. + * The nodeID for the process that is selected (in the aria-activedescendent sense of being selected.) */ - readonly activeDescendantId: string | null; + readonly ariaActiveDescendant: string | null; /** - * The ID attribute of the resolver's currently selected descendant. + * nodeID of the selected node */ - readonly selectedDescendantId: string | null; - /** - * The entity_id of the process for the resolver's currently selected descendant. - */ - readonly processEntityIdOfSelectedDescendant: string | null; + readonly selectedNode: string | null; } /** diff --git a/x-pack/plugins/security_solution/public/resolver/view/assets.tsx b/x-pack/plugins/security_solution/public/resolver/view/assets.tsx index 42f9634238e6a..fc4a9daf17ad1 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/assets.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/assets.tsx @@ -422,7 +422,7 @@ const processTypeToCube: Record = { export const useResolverTheme = (): { colorMap: ColorMap; nodeAssets: NodeStyleMap; - cubeAssetsForNode: (isProcessTerimnated: boolean, isProcessOrigin: boolean) => NodeStyleConfig; + cubeAssetsForNode: (isProcessTerimnated: boolean, isProcessTrigger: boolean) => NodeStyleConfig; } => { const isDarkMode = useUiSetting(DEFAULT_DARK_MODE); const theme = isDarkMode ? euiThemeAmsterdamDark : euiThemeAmsterdamLight; @@ -497,10 +497,14 @@ export const useResolverTheme = (): { }, }; - function cubeAssetsForNode(isProcessTerminated: boolean, isProcessOrigin: boolean) { + function cubeAssetsForNode(isProcessTerminated: boolean, isProcessTrigger: boolean) { if (isProcessTerminated) { - return nodeAssets[processTypeToCube.processTerminated]; - } else if (isProcessOrigin) { + if (isProcessTrigger) { + return nodeAssets.terminatedTriggerCube; + } else { + return nodeAssets[processTypeToCube.processTerminated]; + } + } else if (isProcessTrigger) { return nodeAssets[processTypeToCube.processCausedAlert]; } else { return nodeAssets[processTypeToCube.processRan]; diff --git a/x-pack/plugins/security_solution/public/resolver/view/map.tsx b/x-pack/plugins/security_solution/public/resolver/view/map.tsx index 930e96c3f3e40..69ff9c8e2351b 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/map.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/map.tsx @@ -64,7 +64,7 @@ export const ResolverMap = React.memo(function ({ const { projectionMatrix, ref, onMouseDown } = useCamera(); const isLoading = useSelector(selectors.isLoading); const hasError = useSelector(selectors.hasError); - const activeDescendantId = useSelector(selectors.uiActiveDescendantId); + const activeDescendantId = useSelector(selectors.ariaActiveDescendant); const { colorMap } = useResolverTheme(); return ( @@ -110,7 +110,6 @@ export const ResolverMap = React.memo(function ({ projectionMatrix={projectionMatrix} event={processEvent} isProcessTerminated={terminatedProcesses.has(processEntityId)} - isProcessOrigin={false} timeAtRender={timeAtRender} /> ); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx index efb2d95396ef5..cb0acdc29ceb1 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx @@ -60,10 +60,10 @@ const PanelContent = memo(function PanelContent() { // The "selected" node (and its corresponding event) in the tree control. // It may need to be synchronized with the ID indicated as selected via the `idFromParams` // memo above. When this is the case, it is handled by the layout effect below. - const selectedDescendantProcessId = useSelector(selectors.uiSelectedDescendantProcessId); + const selectedNode = useSelector(selectors.selectedNode); const uiSelectedEvent = useMemo(() => { - return graphableProcesses.find((evt) => event.entityId(evt) === selectedDescendantProcessId); - }, [graphableProcesses, selectedDescendantProcessId]); + return graphableProcesses.find((evt) => event.entityId(evt) === selectedNode); + }, [graphableProcesses, selectedNode]); // Until an event is dispatched during update, the event indicated as selected by params may // be different than the one in state. diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 1f4952f15119d..05f2e0cbfcfa9 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -72,7 +72,6 @@ const UnstyledProcessEventDot = React.memo( event, projectionMatrix, isProcessTerminated, - isProcessOrigin, timeAtRender, }: { /** @@ -95,10 +94,6 @@ const UnstyledProcessEventDot = React.memo( * Whether or not to show the process as terminated. */ isProcessTerminated: boolean; - /** - * Whether or not to show the process as the originating event. - */ - isProcessOrigin: boolean; /** * The time (unix epoch) at render. @@ -117,8 +112,8 @@ const UnstyledProcessEventDot = React.memo( const [xScale] = projectionMatrix; // Node (html id=) IDs - const activeDescendantId = useSelector(selectors.uiActiveDescendantId); - const selectedDescendantId = useSelector(selectors.uiSelectedDescendantId); + const ariaActiveDescendant = useSelector(selectors.ariaActiveDescendant); + const selectedNode = useSelector(selectors.selectedNode); const nodeID = processEventModel.uniquePidForProcess(event); const relatedEventStats = useSelector(selectors.relatedEventsStats)(nodeID); @@ -212,23 +207,26 @@ const UnstyledProcessEventDot = React.memo( isLabelFilled, labelButtonFill, strokeColor, - } = cubeAssetsForNode(isProcessTerminated, isProcessOrigin); + } = cubeAssetsForNode( + isProcessTerminated, + /** + * There is no definition for 'trigger process' yet. return false. + */ false + ); const labelHTMLID = htmlIdGenerator('resolver')(`${nodeID}:label`); - const isAriaCurrent = nodeID === activeDescendantId; - const isAriaSelected = nodeID === selectedDescendantId; + const isAriaCurrent = nodeID === ariaActiveDescendant; + const isAriaSelected = nodeID === selectedNode; const dispatch = useResolverDispatch(); const handleFocus = useCallback(() => { dispatch({ type: 'userFocusedOnResolverNode', - payload: { - nodeId: nodeHTMLID(nodeID), - }, + payload: nodeID, }); - }, [dispatch, nodeHTMLID, nodeID]); + }, [dispatch, nodeID]); const handleRelatedEventRequest = useCallback(() => { dispatch({ @@ -247,13 +245,10 @@ const UnstyledProcessEventDot = React.memo( } dispatch({ type: 'userSelectedResolverNode', - payload: { - nodeId: nodeHTMLID(nodeID), - selectedProcessId: nodeID, - }, + payload: nodeID, }); pushToQueryParams({ crumbId: nodeID, crumbEvent: '' }); - }, [animationTarget, dispatch, pushToQueryParams, nodeID, nodeHTMLID]); + }, [animationTarget, dispatch, pushToQueryParams, nodeID]); /** * Enumerates the stats for related events to display with the node as options, @@ -422,6 +417,7 @@ const UnstyledProcessEventDot = React.memo( buttonFill={colorMap.resolverBackground} menuAction={handleRelatedEventRequest} menuTitle={subMenuAssets.relatedEvents.title} + projectionMatrix={projectionMatrix} optionsWithActions={relatedEventStatusOrOptions} /> )} diff --git a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx index ce126bf695559..2499a451b9c8c 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import React, { ReactNode, useState, useMemo, useCallback } from 'react'; +import React, { ReactNode, useState, useMemo, useCallback, useRef, useLayoutEffect } from 'react'; import { EuiI18nNumber, EuiSelectable, @@ -15,6 +15,7 @@ import { htmlIdGenerator, } from '@elastic/eui'; import styled from 'styled-components'; +import { Matrix3 } from '../types'; /** * i18n-translated titles for submenus and identifiers for display of states: @@ -133,6 +134,7 @@ const NodeSubMenuComponents = React.memo( menuAction, optionsWithActions, className, + projectionMatrix, }: { menuTitle: string; className?: string; @@ -140,9 +142,16 @@ const NodeSubMenuComponents = React.memo( buttonBorderColor: ButtonColor; buttonFill: string; count?: number; + /** + * Receive the projection matrix, so we can see when the camera position changed, so we can force the submenu to reposition itself. + */ + projectionMatrix: Matrix3; } & { optionsWithActions?: ResolverSubmenuOptionList | string | undefined; }) => { + // keep a ref to the popover so we can call its reposition method + const popoverRef = useRef(null); + const [menuIsOpen, setMenuOpen] = useState(false); const handleMenuOpenClick = useCallback( (clickEvent: React.MouseEvent) => { @@ -169,6 +178,28 @@ const NodeSubMenuComponents = React.memo( const isMenuLoading = optionsWithActions === 'waitingForRelatedEventData'; + // The last projection matrix that was used to position the popover + const projectionMatrixAtLastRender = useRef(); + + useLayoutEffect(() => { + if ( + /** + * If there is a popover component reference, + * and this isn't the first render, + * and the projectionMatrix has changed since last render, + * then force the popover to reposition itself. + */ + popoverRef.current && + !projectionMatrixAtLastRender.current && + projectionMatrixAtLastRender.current !== projectionMatrix + ) { + popoverRef.current.positionPopoverFixed(); + } + + // no matter what, keep track of the last project matrix that was used to size the popover + projectionMatrixAtLastRender.current = projectionMatrix; + }, [projectionMatrixAtLastRender, projectionMatrix]); + if (!optionsWithActions) { /** * When called with a `menuAction` @@ -216,6 +247,7 @@ const NodeSubMenuComponents = React.memo( isOpen={menuIsOpen} closePopover={closePopover} repositionOnScroll + ref={popoverRef} > {menuIsOpen && typeof optionsWithActions === 'object' && ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/button/index.tsx index 5896a02b82023..c0a59fd07e348 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/button/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/button/index.tsx @@ -39,6 +39,7 @@ const Container = styled.div` } .${FLYOUT_BUTTON_CLASS_NAME} { + background: ${({ theme }) => rgba(theme.eui.euiPageBackgroundColor, 1)}; border-radius: 4px 4px 0 0; box-shadow: none; height: 46px; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx index f41d318ba9587..3f842bcc2eb68 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx @@ -44,6 +44,8 @@ const StyledResizable = styled(Resizable)` const RESIZABLE_ENABLE = { left: true }; +const RESIZABLE_DISABLED = { left: false }; + const FlyoutPaneComponent: React.FC = ({ children, onClose, @@ -98,10 +100,10 @@ const FlyoutPaneComponent: React.FC = ({ size="l" > diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx index 9f20c7f6c1571..54b30aca44a1f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx @@ -181,7 +181,7 @@ const GraphOverlayComponent = ({ {timelineId === TimelineId.active && timelineType === TimelineType.default && ( - + { '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 3"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 4"}}],"minimum_should_match":1}}]}}]}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 2"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 5"}}],"minimum_should_match":1}}]}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}' ); }); + + describe('resolverIsShowing', () => { + test('it returns true when graphEventId is NOT an empty string', () => { + expect(resolverIsShowing('a valid id')).toBe(true); + }); + + test('it returns false when graphEventId is undefined', () => { + expect(resolverIsShowing(undefined)).toBe(false); + }); + + test('it returns false when graphEventId is an empty string', () => { + expect(resolverIsShowing('')).toBe(false); + }); + }); + + describe('showGlobalFilters', () => { + test('it returns false when `globalFullScreen` is true and `graphEventId` is NOT an empty string, because Resolver IS showing', () => { + expect(showGlobalFilters({ globalFullScreen: true, graphEventId: 'a valid id' })).toBe(false); + }); + + test('it returns true when `globalFullScreen` is true and `graphEventId` is undefined, because Resolver is NOT showing', () => { + expect(showGlobalFilters({ globalFullScreen: true, graphEventId: undefined })).toBe(true); + }); + + test('it returns true when `globalFullScreen` is true and `graphEventId` is an empty string, because Resolver is NOT showing', () => { + expect(showGlobalFilters({ globalFullScreen: true, graphEventId: '' })).toBe(true); + }); + + test('it returns true when `globalFullScreen` is false and `graphEventId` is NOT an empty string, because Resolver IS showing', () => { + expect(showGlobalFilters({ globalFullScreen: false, graphEventId: 'a valid id' })).toBe(true); + }); + + test('it returns true when `globalFullScreen` is false and `graphEventId` is undefined, because Resolver is NOT showing', () => { + expect(showGlobalFilters({ globalFullScreen: false, graphEventId: undefined })).toBe(true); + }); + + test('it returns true when `globalFullScreen` is false and `graphEventId` is an empty string, because Resolver is NOT showing', () => { + expect(showGlobalFilters({ globalFullScreen: false, graphEventId: '' })).toBe(true); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx index b21ea3e4f86e9..84387720b5b11 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx @@ -158,3 +158,14 @@ export const combineQueries = ({ export const STATEFUL_EVENT_CSS_CLASS_NAME = 'event-column-view'; export const DEFAULT_ICON_BUTTON_WIDTH = 24; + +export const resolverIsShowing = (graphEventId: string | undefined): boolean => + graphEventId != null && graphEventId !== ''; + +export const showGlobalFilters = ({ + globalFullScreen, + graphEventId, +}: { + globalFullScreen: boolean; + graphEventId: string | undefined; +}): boolean => (globalFullScreen && resolverIsShowing(graphEventId) ? false : true); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.test.ts deleted file mode 100644 index 00c764d0b912e..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.test.ts +++ /dev/null @@ -1,48 +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 { ExceptionsCache } from './cache'; - -describe('ExceptionsCache tests', () => { - let cache: ExceptionsCache; - const body = Buffer.from('body'); - - beforeEach(() => { - jest.clearAllMocks(); - cache = new ExceptionsCache(3); - }); - - test('it should cache', async () => { - cache.set('test', body); - const cacheResp = cache.get('test'); - expect(cacheResp).toEqual(body); - }); - - test('it should handle cache miss', async () => { - cache.set('test', body); - const cacheResp = cache.get('not test'); - expect(cacheResp).toEqual(undefined); - }); - - test('it should handle cache eviction', async () => { - const a = Buffer.from('a'); - const b = Buffer.from('b'); - const c = Buffer.from('c'); - const d = Buffer.from('d'); - cache.set('1', a); - cache.set('2', b); - cache.set('3', c); - const cacheResp = cache.get('1'); - expect(cacheResp).toEqual(a); - - cache.set('4', d); - const secondResp = cache.get('1'); - expect(secondResp).toEqual(undefined); - expect(cache.get('2')).toEqual(b); - expect(cache.get('3')).toEqual(c); - expect(cache.get('4')).toEqual(d); - }); -}); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.ts deleted file mode 100644 index b9d3bae4e6ef9..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.ts +++ /dev/null @@ -1,37 +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. - */ - -const DEFAULT_MAX_SIZE = 10; - -/** - * FIFO cache implementation for artifact downloads. - */ -export class ExceptionsCache { - private cache: Map; - private queue: string[]; - private maxSize: number; - - constructor(maxSize: number) { - this.cache = new Map(); - this.queue = []; - this.maxSize = maxSize || DEFAULT_MAX_SIZE; - } - - set(id: string, body: Buffer) { - if (this.queue.length + 1 > this.maxSize) { - const entry = this.queue.shift(); - if (entry !== undefined) { - this.cache.delete(entry); - } - } - this.queue.push(id); - this.cache.set(id, body); - } - - get(id: string): Buffer | undefined { - return this.cache.get(id); - } -} diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/index.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/index.ts index ee7d44459aa38..21a9047a04299 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/index.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './cache'; export * from './common'; export * from './lists'; export * from './manifest'; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts index d3d073efa73c1..bb8b4fb3d5ce7 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts @@ -8,7 +8,7 @@ import { ExceptionListClient } from '../../../../../lists/server'; import { listMock } from '../../../../../lists/server/mocks'; import { getFoundExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { EntriesArray, EntryList } from '../../../../../lists/common/schemas/types/entries'; +import { EntriesArray, EntryList } from '../../../../../lists/common/schemas/types'; import { buildArtifact, getFullEndpointExceptionList } from './lists'; import { TranslatedEntry, TranslatedExceptionListItem } from '../../schemas/artifacts'; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts index 68fa2a0511a48..5998a88527f2f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts @@ -9,7 +9,7 @@ import { deflate } from 'zlib'; import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { validate } from '../../../../common/validate'; -import { Entry, EntryNested } from '../../../../../lists/common/schemas/types/entries'; +import { Entry, EntryNested } from '../../../../../lists/common/schemas/types'; import { FoundExceptionListItemSchema } from '../../../../../lists/common/schemas/response/found_exception_list_item_schema'; import { ExceptionListClient } from '../../../../../lists/server'; import { ENDPOINT_LIST_ID } from '../../../../common/shared_imports'; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts index 8c6faee7f7a5d..4454da855569b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { deflateSync, inflateSync } from 'zlib'; +import LRU from 'lru-cache'; import { ILegacyClusterClient, IRouter, @@ -22,7 +23,6 @@ import { httpServerMock, loggingSystemMock, } from 'src/core/server/mocks'; -import { ExceptionsCache } from '../../lib/artifacts/cache'; import { ArtifactConstants } from '../../lib/artifacts'; import { registerDownloadExceptionListRoute } from './download_exception_list'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; @@ -97,7 +97,7 @@ describe('test alerts route', () => { let routeConfig: RouteConfig; let routeHandler: RequestHandler; let endpointAppContextService: EndpointAppContextService; - let cache: ExceptionsCache; + let cache: LRU; let ingestSavedObjectClient: jest.Mocked; beforeEach(() => { @@ -108,7 +108,7 @@ describe('test alerts route', () => { mockClusterClient.asScoped.mockReturnValue(mockScopedClient); routerMock = httpServiceMock.createRouter(); endpointAppContextService = new EndpointAppContextService(); - cache = new ExceptionsCache(5); + cache = new LRU({ max: 10, maxAge: 1000 * 60 * 60 }); const startContract = createMockEndpointAppContextServiceStartContract(); // The authentication with the Fleet Plugin needs a separate scoped SO Client @@ -164,7 +164,7 @@ describe('test alerts route', () => { path.startsWith('/api/endpoint/artifacts/download') )!; - expect(routeConfig.options).toEqual(undefined); + expect(routeConfig.options).toEqual({ tags: ['endpoint:limited-concurrency'] }); await routeHandler( ({ diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts index 218f7c059da48..38e900c4d5015 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts @@ -11,11 +11,13 @@ import { IKibanaResponse, SavedObject, } from 'src/core/server'; +import LRU from 'lru-cache'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { authenticateAgentWithAccessToken } from '../../../../../ingest_manager/server/services/agents/authenticate'; import { validate } from '../../../../common/validate'; +import { LIMITED_CONCURRENCY_ENDPOINT_ROUTE_TAG } from '../../../../common/endpoint/constants'; import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; -import { ArtifactConstants, ExceptionsCache } from '../../lib/artifacts'; +import { ArtifactConstants } from '../../lib/artifacts'; import { DownloadArtifactRequestParamsSchema, downloadArtifactRequestParamsSchema, @@ -32,7 +34,7 @@ const allowlistBaseRoute: string = '/api/endpoint/artifacts'; export function registerDownloadExceptionListRoute( router: IRouter, endpointContext: EndpointAppContext, - cache: ExceptionsCache + cache: LRU ) { router.get( { @@ -43,6 +45,7 @@ export function registerDownloadExceptionListRoute( DownloadArtifactRequestParamsSchema >(downloadArtifactRequestParamsSchema), }, + options: { tags: [LIMITED_CONCURRENCY_ENDPOINT_ROUTE_TAG] }, }, async (context, req, res) => { let scopedSOClient: SavedObjectsClientContract; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/limited_concurrency.ts b/x-pack/plugins/security_solution/server/endpoint/routes/limited_concurrency.ts new file mode 100644 index 0000000000000..767ba31311a77 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/limited_concurrency.ts @@ -0,0 +1,72 @@ +/* + * 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 { + CoreSetup, + KibanaRequest, + LifecycleResponseFactory, + OnPreAuthToolkit, +} from 'kibana/server'; +import { + LIMITED_CONCURRENCY_ENDPOINT_ROUTE_TAG, + LIMITED_CONCURRENCY_ENDPOINT_COUNT, +} from '../../../common/endpoint/constants'; + +class MaxCounter { + constructor(private readonly max: number = 1) {} + private counter = 0; + valueOf() { + return this.counter; + } + increase() { + if (this.counter < this.max) { + this.counter += 1; + } + } + decrease() { + if (this.counter > 0) { + this.counter -= 1; + } + } + lessThanMax() { + return this.counter < this.max; + } +} + +function shouldHandleRequest(request: KibanaRequest) { + const tags = request.route.options.tags; + return tags.includes(LIMITED_CONCURRENCY_ENDPOINT_ROUTE_TAG); +} + +export function registerLimitedConcurrencyRoutes(core: CoreSetup) { + const counter = new MaxCounter(LIMITED_CONCURRENCY_ENDPOINT_COUNT); + core.http.registerOnPreAuth(function preAuthHandler( + request: KibanaRequest, + response: LifecycleResponseFactory, + toolkit: OnPreAuthToolkit + ) { + if (!shouldHandleRequest(request)) { + return toolkit.next(); + } + + if (!counter.lessThanMax()) { + return response.customError({ + body: 'Too Many Requests', + statusCode: 429, + }); + } + + counter.increase(); + + // requests.events.aborted$ has a bug (but has test which explicitly verifies) where it's fired even when the request completes + // https://github.com/elastic/kibana/pull/70495#issuecomment-656288766 + request.events.aborted$.toPromise().then(() => { + counter.decrease(); + }); + + return toolkit.next(); + }); +} diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts index 2ebffa6fb3ad8..592ffb0eae62a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts @@ -9,7 +9,7 @@ import { Logger } from 'src/core/server'; import { PackageConfigServiceInterface } from '../../../../../../ingest_manager/server'; import { createPackageConfigServiceMock } from '../../../../../../ingest_manager/server/mocks'; import { listMock } from '../../../../../../lists/server/mocks'; -import { ExceptionsCache } from '../../../lib/artifacts'; +import LRU from 'lru-cache'; import { getArtifactClientMock } from '../artifact_client.mock'; import { getManifestClientMock } from '../manifest_client.mock'; import { ManifestManager } from './manifest_manager'; @@ -28,11 +28,11 @@ export enum ManifestManagerMockType { export const getManifestManagerMock = (opts?: { mockType?: ManifestManagerMockType; - cache?: ExceptionsCache; + cache?: LRU; packageConfigService?: jest.Mocked; savedObjectsClient?: ReturnType; }): ManifestManager => { - let cache = new ExceptionsCache(5); + let cache = new LRU({ max: 10, maxAge: 1000 * 60 * 60 }); if (opts?.cache !== undefined) { cache = opts.cache; } diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts index ff331f7d017f4..c838f772fb66b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts @@ -7,13 +7,10 @@ import { inflateSync } from 'zlib'; import { savedObjectsClientMock } from 'src/core/server/mocks'; import { createPackageConfigServiceMock } from '../../../../../../ingest_manager/server/mocks'; -import { - ArtifactConstants, - ManifestConstants, - ExceptionsCache, - isCompleteArtifact, -} from '../../../lib/artifacts'; +import { ArtifactConstants, ManifestConstants, isCompleteArtifact } from '../../../lib/artifacts'; + import { getManifestManagerMock } from './manifest_manager.mock'; +import LRU from 'lru-cache'; describe('manifest_manager', () => { describe('ManifestManager sanity checks', () => { @@ -41,7 +38,7 @@ describe('manifest_manager', () => { }); test('ManifestManager populates cache properly', async () => { - const cache = new ExceptionsCache(5); + const cache = new LRU({ max: 10, maxAge: 1000 * 60 * 60 }); const manifestManager = getManifestManagerMock({ cache }); const oldManifest = await manifestManager.getLastComputedManifest( ManifestConstants.SCHEMA_VERSION diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts index 2501f07cb26e0..13ca51e1f2b39 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts @@ -5,6 +5,7 @@ */ import { Logger, SavedObjectsClientContract } from 'src/core/server'; +import LRU from 'lru-cache'; import { PackageConfigServiceInterface } from '../../../../../../ingest_manager/server'; import { ExceptionListClient } from '../../../../../../lists/server'; import { ManifestSchemaVersion } from '../../../../../common/endpoint/schema/common'; @@ -16,7 +17,6 @@ import { Manifest, buildArtifact, getFullEndpointExceptionList, - ExceptionsCache, ManifestDiff, getArtifactId, } from '../../../lib/artifacts'; @@ -33,7 +33,7 @@ export interface ManifestManagerContext { exceptionListClient: ExceptionListClient; packageConfigService: PackageConfigServiceInterface; logger: Logger; - cache: ExceptionsCache; + cache: LRU; } export interface ManifestSnapshotOpts { @@ -51,7 +51,7 @@ export class ManifestManager { protected packageConfigService: PackageConfigServiceInterface; protected savedObjectsClient: SavedObjectsClientContract; protected logger: Logger; - protected cache: ExceptionsCache; + protected cache: LRU; constructor(context: ManifestManagerContext) { this.artifactClient = context.artifactClient; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.test.ts index 744e2b0c06efe..d97dc4ba2cbd2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.test.ts @@ -193,4 +193,39 @@ describe('getThresholdSignalQueryFields', () => { 'event.dataset': 'traefik.access', }); }); + + it('should return proper object for exists filters', () => { + const filters = { + bool: { + should: [ + { + bool: { + should: [ + { + exists: { + field: 'process.name', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + exists: { + field: 'event.type', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + minimum_should_match: 1, + }, + }; + expect(getThresholdSignalQueryFields(filters)).toEqual({}); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts index ef9fbe485b92f..e2f3d16bd6d03 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts @@ -83,7 +83,7 @@ export const getThresholdSignalQueryFields = (filter: unknown) => { return { ...acc, ...item.match_phrase }; } - if (item.bool.should && (item.bool.should[0].match || item.bool.should[0].match_phrase)) { + if (item.bool?.should && (item.bool.should[0].match || item.bool.should[0].match_phrase)) { return { ...acc, ...(item.bool.should[0].match || item.bool.should[0].match_phrase) }; } diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index f121c758bcce8..8fc413236dd2c 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -7,6 +7,7 @@ import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; +import LRU from 'lru-cache'; import { CoreSetup, @@ -34,7 +35,7 @@ import { isAlertExecutor } from './lib/detection_engine/signals/types'; import { signalRulesAlertType } from './lib/detection_engine/signals/signal_rule_alert_type'; import { rulesNotificationAlertType } from './lib/detection_engine/notifications/rules_notification_alert_type'; import { isNotificationAlertExecutor } from './lib/detection_engine/notifications/types'; -import { ManifestTask, ExceptionsCache } from './endpoint/lib/artifacts'; +import { ManifestTask } from './endpoint/lib/artifacts'; import { initSavedObjects, savedObjectTypes } from './saved_objects'; import { AppClientFactory } from './client'; import { createConfig$, ConfigType } from './config'; @@ -48,6 +49,7 @@ import { NOTIFICATIONS_ID, } from '../common/constants'; import { registerEndpointRoutes } from './endpoint/routes/metadata'; +import { registerLimitedConcurrencyRoutes } from './endpoint/routes/limited_concurrency'; import { registerResolverRoutes } from './endpoint/routes/resolver'; import { registerPolicyRoutes } from './endpoint/routes/policy'; import { ArtifactClient, ManifestManager } from './endpoint/services'; @@ -101,14 +103,15 @@ export class Plugin implements IPlugin; constructor(context: PluginInitializerContext) { this.context = context; this.logger = context.logger.get('plugins', APP_ID); this.config$ = createConfig$(context); this.appClientFactory = new AppClientFactory(); - this.exceptionsCache = new ExceptionsCache(5); // TODO + // Cache up to three artifacts with a max retention of 5 mins each + this.exceptionsCache = new LRU({ max: 3, maxAge: 1000 * 60 * 5 }); this.logger.debug('plugin initialized'); } @@ -156,6 +159,7 @@ export class Plugin implements IPlugin { expect(bulkUpdate).toHaveBeenCalledWith([task1, task2]); }); - test('batch updates are executed at most by the next Event Loop tick by default', async () => { - const bulkUpdate: jest.Mocked> = jest.fn((tasks) => { - return Promise.resolve(tasks.map(incrementAttempts)); - }); - - const bufferedUpdate = createBuffer(bulkUpdate); - - const task1 = createTask(); - const task2 = createTask(); - const task3 = createTask(); - const task4 = createTask(); - const task5 = createTask(); - const task6 = createTask(); - - return new Promise((resolve) => { - Promise.all([bufferedUpdate(task1), bufferedUpdate(task2)]).then((_) => { - expect(bulkUpdate).toHaveBeenCalledTimes(1); - expect(bulkUpdate).toHaveBeenCalledWith([task1, task2]); - expect(bulkUpdate).not.toHaveBeenCalledWith([task3, task4]); - }); - - setTimeout(() => { - // on next tick - setTimeout(() => { - // on next tick - expect(bulkUpdate).toHaveBeenCalledTimes(2); - Promise.all([bufferedUpdate(task5), bufferedUpdate(task6)]).then((_) => { - expect(bulkUpdate).toHaveBeenCalledTimes(3); - expect(bulkUpdate).toHaveBeenCalledWith([task5, task6]); - resolve(); - }); - }, 0); - - expect(bulkUpdate).toHaveBeenCalledTimes(1); - Promise.all([bufferedUpdate(task3), bufferedUpdate(task4)]).then((_) => { - expect(bulkUpdate).toHaveBeenCalledTimes(2); - expect(bulkUpdate).toHaveBeenCalledWith([task3, task4]); - }); - }, 0); - }); - }); - test('batch updates can be customised to execute after a certain period', async () => { const bulkUpdate: jest.Mocked> = jest.fn((tasks) => { return Promise.resolve(tasks.map(incrementAttempts)); diff --git a/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.ts b/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.ts index fca7ce02e0cd7..c8e5b837fa36c 100644 --- a/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.ts +++ b/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.ts @@ -93,11 +93,8 @@ export function createBuffer { setTimeout(resolve, ms); diff --git a/x-pack/plugins/transform/public/app/common/pivot_aggs.test.ts b/x-pack/plugins/transform/public/app/common/pivot_aggs.test.ts index 9e748068742f3..48a0951e906e9 100644 --- a/x-pack/plugins/transform/public/app/common/pivot_aggs.test.ts +++ b/x-pack/plugins/transform/public/app/common/pivot_aggs.test.ts @@ -11,10 +11,6 @@ import { } from '../sections/create_transform/components/step_define/common/filter_agg/components'; describe('getAggConfigFromEsAgg', () => { - test('should throw an error for unsupported agg', () => { - expect(() => getAggConfigFromEsAgg({ terms: {} }, 'test')).toThrowError(); - }); - test('should return a common config if the agg does not have a custom config defined', () => { expect(getAggConfigFromEsAgg({ avg: { field: 'region' } }, 'test_1')).toEqual({ agg: 'avg', diff --git a/x-pack/plugins/transform/public/app/common/pivot_aggs.ts b/x-pack/plugins/transform/public/app/common/pivot_aggs.ts index 54dfd9ecda7b1..ec52de4b9da92 100644 --- a/x-pack/plugins/transform/public/app/common/pivot_aggs.ts +++ b/x-pack/plugins/transform/public/app/common/pivot_aggs.ts @@ -110,8 +110,8 @@ export function getAggConfigFromEsAgg( // Find the main aggregation key const agg = aggKeys.find((aggKey) => aggKey !== 'aggs'); - if (!isPivotSupportedAggs(agg)) { - throw new Error(`Aggregation "${agg}" is not supported`); + if (agg === undefined) { + throw new Error(`Aggregation key is required`); } const commonConfig: PivotAggsConfigBase = { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 4175f76ad7ba8..1d8d93e7c961d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4685,14 +4685,14 @@ "xpack.canvas.argFormArgSimpleForm.requiredTooltip": "この引数は必須です。数値を入力してください。", "xpack.canvas.argFormPendingArgValue.loadingMessage": "読み込み中", "xpack.canvas.argFormSimpleFailure.failureTooltip": "この引数のインターフェースが値を解析できなかったため、フォールバックインプットが使用されています", + "xpack.canvas.asset.confirmModalButtonLabel": "削除", + "xpack.canvas.asset.confirmModalDetail": "このアセットを削除してよろしいですか?", + "xpack.canvas.asset.confirmModalTitle": "アセットの削除", "xpack.canvas.asset.copyAssetTooltip": "ID をクリップボードにコピー", "xpack.canvas.asset.createImageTooltip": "画像エレメントを作成", "xpack.canvas.asset.deleteAssetTooltip": "削除", "xpack.canvas.asset.downloadAssetTooltip": "ダウンロード", "xpack.canvas.asset.thumbnailAltText": "アセットのサムネイル", - "xpack.canvas.assetManager.confirmModalButtonLabel": "削除", - "xpack.canvas.assetManager.confirmModalDetail": "このアセットを削除してよろしいですか?", - "xpack.canvas.assetManager.confirmModalTitle": "アセットの削除", "xpack.canvas.assetManager.manageButtonLabel": "アセットの管理", "xpack.canvas.assetModal.emptyAssetsDescription": "アセットをインポートして開始します", "xpack.canvas.assetModal.filePickerPromptText": "画像を選択するかドラッグ &amp; ドロップしてください", @@ -8569,15 +8569,15 @@ "xpack.lens.pie.treemaplabel": "ツリーマップ", "xpack.lens.pie.treemapSuggestionLabel": "ツリーマップとして", "xpack.lens.pie.visualizationName": "パイ", - "xpack.lens.pieChart.alwaysShowLegendLabel": "表示", "xpack.lens.pieChart.categoriesInLegendLabel": "ラベルを非表示", - "xpack.lens.pieChart.defaultLegendLabel": "自動", "xpack.lens.pieChart.fitInsideOnlyLabel": "内部のみ", "xpack.lens.pieChart.hiddenNumbersLabel": "グラフから非表示", - "xpack.lens.pieChart.hideLegendLabel": "非表示", "xpack.lens.pieChart.labelPositionLabel": "ラベル位置", "xpack.lens.pieChart.legendDisplayLabel": "凡例表示", "xpack.lens.pieChart.legendDisplayLegend": "凡例表示", + "xpack.lens.pieChart.legendVisibility.auto": "自動", + "xpack.lens.pieChart.legendVisibility.hide": "非表示", + "xpack.lens.pieChart.legendVisibility.show": "表示", "xpack.lens.pieChart.nestedLegendLabel": "ネストされた凡例", "xpack.lens.pieChart.numberLabels": "ラベル値", "xpack.lens.pieChart.percentDecimalsLabel": "割合の小数点桁数", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 33d60dbd17700..0ea2c9f17e257 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4689,14 +4689,14 @@ "xpack.canvas.argFormArgSimpleForm.requiredTooltip": "此参数为必需,应指定值。", "xpack.canvas.argFormPendingArgValue.loadingMessage": "正在加载", "xpack.canvas.argFormSimpleFailure.failureTooltip": "此参数的接口无法解析该值,因此将使用回退输入", + "xpack.canvas.asset.confirmModalButtonLabel": "删除", + "xpack.canvas.asset.confirmModalDetail": "确定要删除此资产?", + "xpack.canvas.asset.confirmModalTitle": "删除资产", "xpack.canvas.asset.copyAssetTooltip": "将 ID 复制到剪贴板", "xpack.canvas.asset.createImageTooltip": "创建图像元素", "xpack.canvas.asset.deleteAssetTooltip": "删除", "xpack.canvas.asset.downloadAssetTooltip": "下载", "xpack.canvas.asset.thumbnailAltText": "资产缩略图", - "xpack.canvas.assetManager.confirmModalButtonLabel": "删除", - "xpack.canvas.assetManager.confirmModalDetail": "确定要删除此资产?", - "xpack.canvas.assetManager.confirmModalTitle": "删除资产", "xpack.canvas.assetManager.manageButtonLabel": "管理资产", "xpack.canvas.assetModal.emptyAssetsDescription": "导入您的资产以开始", "xpack.canvas.assetModal.filePickerPromptText": "选择或拖放图像", @@ -8574,15 +8574,15 @@ "xpack.lens.pie.treemaplabel": "树状图", "xpack.lens.pie.treemapSuggestionLabel": "为树状图", "xpack.lens.pie.visualizationName": "饼图", - "xpack.lens.pieChart.alwaysShowLegendLabel": "显示", "xpack.lens.pieChart.categoriesInLegendLabel": "隐藏标签", - "xpack.lens.pieChart.defaultLegendLabel": "自动", "xpack.lens.pieChart.fitInsideOnlyLabel": "仅内部", "xpack.lens.pieChart.hiddenNumbersLabel": "在图表中隐藏", - "xpack.lens.pieChart.hideLegendLabel": "隐藏", "xpack.lens.pieChart.labelPositionLabel": "标签位置", "xpack.lens.pieChart.legendDisplayLabel": "图例显示", "xpack.lens.pieChart.legendDisplayLegend": "图例显示", + "xpack.lens.pieChart.legendVisibility.auto": "自动", + "xpack.lens.pieChart.legendVisibility.hide": "隐藏", + "xpack.lens.pieChart.legendVisibility.show": "显示", "xpack.lens.pieChart.nestedLegendLabel": "嵌套图例", "xpack.lens.pieChart.numberLabels": "标签值", "xpack.lens.pieChart.percentDecimalsLabel": "百分比的小数位数", diff --git a/x-pack/plugins/uptime/public/components/overview/filter_group/parse_filter_map.ts b/x-pack/plugins/uptime/public/components/overview/filter_group/parse_filter_map.ts index 08766521799ea..47c86543c1287 100644 --- a/x-pack/plugins/uptime/public/components/overview/filter_group/parse_filter_map.ts +++ b/x-pack/plugins/uptime/public/components/overview/filter_group/parse_filter_map.ts @@ -14,7 +14,7 @@ interface FilterField { * If your code needs to support custom fields, introduce a second parameter to * `parseFiltersMap` to take a list of FilterField objects. */ -const filterWhitelist: FilterField[] = [ +const filterAllowList: FilterField[] = [ { name: 'ports', fieldName: 'url.port' }, { name: 'locations', fieldName: 'observer.geo.name' }, { name: 'tags', fieldName: 'tags' }, @@ -28,7 +28,7 @@ export const parseFiltersMap = (filterMapString: string) => { const filterSlices: { [key: string]: any } = {}; try { const map = new Map(JSON.parse(filterMapString)); - filterWhitelist.forEach(({ name, fieldName }) => { + filterAllowList.forEach(({ name, fieldName }) => { filterSlices[name] = map.get(fieldName) ?? []; }); return filterSlices; diff --git a/x-pack/test/apm_api_integration/basic/fixtures/es_archiver/observability_overview/data.json.gz b/x-pack/test/apm_api_integration/basic/fixtures/es_archiver/observability_overview/data.json.gz new file mode 100644 index 0000000000000..23602666f3b43 Binary files /dev/null and b/x-pack/test/apm_api_integration/basic/fixtures/es_archiver/observability_overview/data.json.gz differ diff --git a/x-pack/test/apm_api_integration/basic/fixtures/es_archiver/observability_overview/mappings.json b/x-pack/test/apm_api_integration/basic/fixtures/es_archiver/observability_overview/mappings.json new file mode 100644 index 0000000000000..e6f40fedaab4c --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/fixtures/es_archiver/observability_overview/mappings.json @@ -0,0 +1,4229 @@ +{ + "type": "index", + "value": { + "aliases": { + }, + "index": "apm-8.0.0-onboarding-2020.06.29", + "mappings": { + "_meta": { + "beat": "apm", + "version": "8.0.0" + }, + "date_detection": false, + "dynamic_templates": [ + { + "labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "container.labels.*" + } + }, + { + "dns.answers": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "dns.answers.*" + } + }, + { + "log.syslog": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "log.syslog.*" + } + }, + { + "network.inner": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "network.inner.*" + } + }, + { + "observer.egress": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "observer.egress.*" + } + }, + { + "observer.ingress": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "observer.ingress.*" + } + }, + { + "fields": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "fields.*" + } + }, + { + "docker.container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "docker.container.labels.*" + } + }, + { + "kubernetes.labels.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.labels.*" + } + }, + { + "kubernetes.annotations.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.annotations.*" + } + }, + { + "labels_string": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "labels_boolean": { + "mapping": { + "type": "boolean" + }, + "match_mapping_type": "boolean", + "path_match": "labels.*" + } + }, + { + "labels_*": { + "mapping": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "path_match": "labels.*" + } + }, + { + "transaction.marks": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "transaction.marks.*" + } + }, + { + "transaction.marks.*.*": { + "mapping": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "path_match": "transaction.marks.*.*" + } + }, + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "dynamic": "false", + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "path": "agent.name", + "type": "alias" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "child": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "client": { + "dynamic": "false", + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "cloud": { + "properties": { + "account": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "availability_zone": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "instance": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "machine": { + "dynamic": "false", + "properties": { + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "project": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "region": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "container": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "tag": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "runtime": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dll": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dns": { + "properties": { + "answers": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "ttl": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "header_flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "op_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "question": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "subdomain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "resolved_ip": { + "type": "ip" + }, + "response_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "docker": { + "properties": { + "container": { + "properties": { + "labels": { + "type": "object" + } + } + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "error": { + "dynamic": "false", + "properties": { + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "culprit": { + "ignore_above": 1024, + "type": "keyword" + }, + "exception": { + "properties": { + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "handled": { + "type": "boolean" + }, + "message": { + "norms": false, + "type": "text" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "grouping_key": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "log": { + "properties": { + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "logger_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "message": { + "norms": false, + "type": "text" + }, + "param_message": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "stack_trace": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "ignore_above": 1024, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "experimental": { + "dynamic": "true", + "type": "object" + }, + "fields": { + "type": "object" + }, + "file": { + "properties": { + "accessed": { + "type": "date" + }, + "attributes": { + "ignore_above": 1024, + "type": "keyword" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "created": { + "type": "date" + }, + "ctime": { + "type": "date" + }, + "device": { + "ignore_above": 1024, + "type": "keyword" + }, + "directory": { + "ignore_above": 1024, + "type": "keyword" + }, + "drive_letter": { + "ignore_above": 1, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "inode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mime_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mtime": { + "type": "date" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "owner": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "size": { + "type": "long" + }, + "target_path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "dynamic": "false", + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "containerized": { + "type": "boolean" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "build": { + "ignore_above": 1024, + "type": "keyword" + }, + "codename": { + "ignore_above": 1024, + "type": "keyword" + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "http": { + "dynamic": "false", + "properties": { + "request": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "headers": { + "enabled": false, + "type": "object" + }, + "method": { + "ignore_above": 1024, + "type": "keyword" + }, + "referrer": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "response": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "finished": { + "type": "boolean" + }, + "headers": { + "enabled": false, + "type": "object" + }, + "status_code": { + "type": "long" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "kubernetes": { + "dynamic": "false", + "properties": { + "annotations": { + "properties": { + "*": { + "type": "object" + } + } + }, + "container": { + "properties": { + "image": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "deployment": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "properties": { + "*": { + "type": "object" + } + } + }, + "namespace": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pod": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "replicaset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "statefulset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "labels": { + "dynamic": "true", + "type": "object" + }, + "log": { + "properties": { + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "logger": { + "ignore_above": 1024, + "type": "keyword" + }, + "origin": { + "properties": { + "file": { + "properties": { + "line": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "function": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "original": { + "ignore_above": 1024, + "type": "keyword" + }, + "syslog": { + "properties": { + "facility": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "priority": { + "type": "long" + }, + "severity": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "network": { + "properties": { + "application": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "community_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "direction": { + "ignore_above": 1024, + "type": "keyword" + }, + "forwarded_ip": { + "type": "ip" + }, + "iana_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "inner": { + "properties": { + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "transport": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "observer": { + "dynamic": "false", + "properties": { + "egress": { + "properties": { + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "zone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingress": { + "properties": { + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "zone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "listening": { + "ignore_above": 1024, + "type": "keyword" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vendor": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + }, + "version_major": { + "type": "byte" + } + } + }, + "organization": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "package": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "build_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "checksum": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "install_scope": { + "ignore_above": 1024, + "type": "keyword" + }, + "installed": { + "type": "date" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "parent": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "process": { + "dynamic": "false", + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "parent": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "processor": { + "properties": { + "event": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "profile": { + "dynamic": "false", + "properties": { + "alloc_objects": { + "properties": { + "count": { + "type": "long" + } + } + }, + "alloc_space": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "cpu": { + "properties": { + "ns": { + "type": "long" + } + } + }, + "duration": { + "type": "long" + }, + "inuse_objects": { + "properties": { + "count": { + "type": "long" + } + } + }, + "inuse_space": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "samples": { + "properties": { + "count": { + "type": "long" + } + } + }, + "stack": { + "dynamic": "false", + "properties": { + "filename": { + "ignore_above": 1024, + "type": "keyword" + }, + "function": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "line": { + "type": "long" + } + } + }, + "top": { + "dynamic": "false", + "properties": { + "filename": { + "ignore_above": 1024, + "type": "keyword" + }, + "function": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "line": { + "type": "long" + } + } + } + } + }, + "registry": { + "properties": { + "data": { + "properties": { + "bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "strings": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hive": { + "ignore_above": 1024, + "type": "keyword" + }, + "key": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "value": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "related": { + "properties": { + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "rule": { + "properties": { + "author": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "ruleset": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "server": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "service": { + "dynamic": "false", + "properties": { + "environment": { + "ignore_above": 1024, + "type": "keyword" + }, + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "framework": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "language": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "source": { + "dynamic": "false", + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "sourcemap": { + "dynamic": "false", + "properties": { + "bundle_filepath": { + "ignore_above": 1024, + "type": "keyword" + }, + "service": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "span": { + "dynamic": "false", + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "db": { + "dynamic": "false", + "properties": { + "link": { + "ignore_above": 1024, + "type": "keyword" + }, + "rows_affected": { + "type": "long" + } + } + }, + "destination": { + "dynamic": "false", + "properties": { + "service": { + "dynamic": "false", + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "resource": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "duration": { + "properties": { + "us": { + "type": "long" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "message": { + "dynamic": "false", + "properties": { + "age": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "queue": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "self_time": { + "properties": { + "count": { + "type": "long" + }, + "sum": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "start": { + "properties": { + "us": { + "type": "long" + } + } + }, + "subtype": { + "ignore_above": 1024, + "type": "keyword" + }, + "sync": { + "type": "boolean" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "system": { + "properties": { + "cpu": { + "properties": { + "total": { + "properties": { + "norm": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + } + } + } + } + }, + "memory": { + "properties": { + "actual": { + "properties": { + "free": { + "type": "long" + } + } + }, + "total": { + "type": "long" + } + } + }, + "process": { + "properties": { + "cpu": { + "properties": { + "total": { + "properties": { + "norm": { + "properties": { + "pct": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + } + } + } + } + }, + "memory": { + "properties": { + "rss": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "size": { + "type": "long" + } + } + } + } + } + } + }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + }, + "threat": { + "properties": { + "framework": { + "ignore_above": 1024, + "type": "keyword" + }, + "tactic": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "technique": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "timeseries": { + "properties": { + "instance": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "timestamp": { + "properties": { + "us": { + "type": "long" + } + } + }, + "tls": { + "properties": { + "cipher": { + "ignore_above": 1024, + "type": "keyword" + }, + "client": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "server_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + }, + "supported_ciphers": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "established": { + "type": "boolean" + }, + "next_protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "resumed": { + "type": "boolean" + }, + "server": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3s": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + }, + "version_protocol": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "trace": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "tracing": { + "properties": { + "trace": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "transaction": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "transaction": { + "dynamic": "false", + "properties": { + "breakdown": { + "properties": { + "count": { + "type": "long" + } + } + }, + "duration": { + "properties": { + "count": { + "type": "long" + }, + "histogram": { + "type": "histogram" + }, + "sum": { + "properties": { + "us": { + "type": "long" + } + } + }, + "us": { + "type": "long" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "marks": { + "dynamic": "true", + "properties": { + "*": { + "properties": { + "*": { + "dynamic": "true", + "type": "object" + } + } + } + } + }, + "message": { + "dynamic": "false", + "properties": { + "age": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "queue": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "result": { + "ignore_above": 1024, + "type": "keyword" + }, + "root": { + "type": "boolean" + }, + "sampled": { + "type": "boolean" + }, + "self_time": { + "properties": { + "count": { + "type": "long" + }, + "sum": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "span_count": { + "properties": { + "dropped": { + "type": "long" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "url": { + "dynamic": "false", + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "fragment": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "password": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "query": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "username": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user": { + "dynamic": "false", + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user_agent": { + "dynamic": "false", + "properties": { + "device": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "view spans": { + "ignore_above": 1024, + "type": "keyword" + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vulnerability": { + "properties": { + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "classification": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "enumeration": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "report_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "scanner": { + "properties": { + "vendor": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "score": { + "properties": { + "base": { + "type": "float" + }, + "environmental": { + "type": "float" + }, + "temporal": { + "type": "float" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "severity": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "codec": "best_compression", + "mapping": { + "total_fields": { + "limit": "2000" + } + }, + "number_of_replicas": "0", + "number_of_shards": "1", + "query": { + "default_field": [ + "message", + "tags", + "agent.ephemeral_id", + "agent.id", + "agent.name", + "agent.type", + "agent.version", + "as.organization.name", + "client.address", + "client.as.organization.name", + "client.domain", + "client.geo.city_name", + "client.geo.continent_name", + "client.geo.country_iso_code", + "client.geo.country_name", + "client.geo.name", + "client.geo.region_iso_code", + "client.geo.region_name", + "client.mac", + "client.registered_domain", + "client.top_level_domain", + "client.user.domain", + "client.user.email", + "client.user.full_name", + "client.user.group.domain", + "client.user.group.id", + "client.user.group.name", + "client.user.hash", + "client.user.id", + "client.user.name", + "cloud.account.id", + "cloud.availability_zone", + "cloud.instance.id", + "cloud.instance.name", + "cloud.machine.type", + "cloud.provider", + "cloud.region", + "container.id", + "container.image.name", + "container.image.tag", + "container.name", + "container.runtime", + "destination.address", + "destination.as.organization.name", + "destination.domain", + "destination.geo.city_name", + "destination.geo.continent_name", + "destination.geo.country_iso_code", + "destination.geo.country_name", + "destination.geo.name", + "destination.geo.region_iso_code", + "destination.geo.region_name", + "destination.mac", + "destination.registered_domain", + "destination.top_level_domain", + "destination.user.domain", + "destination.user.email", + "destination.user.full_name", + "destination.user.group.domain", + "destination.user.group.id", + "destination.user.group.name", + "destination.user.hash", + "destination.user.id", + "destination.user.name", + "dns.answers.class", + "dns.answers.data", + "dns.answers.name", + "dns.answers.type", + "dns.header_flags", + "dns.id", + "dns.op_code", + "dns.question.class", + "dns.question.name", + "dns.question.registered_domain", + "dns.question.subdomain", + "dns.question.top_level_domain", + "dns.question.type", + "dns.response_code", + "dns.type", + "ecs.version", + "error.code", + "error.id", + "error.message", + "error.stack_trace", + "error.type", + "event.action", + "event.category", + "event.code", + "event.dataset", + "event.hash", + "event.id", + "event.kind", + "event.module", + "event.original", + "event.outcome", + "event.provider", + "event.timezone", + "event.type", + "file.device", + "file.directory", + "file.extension", + "file.gid", + "file.group", + "file.hash.md5", + "file.hash.sha1", + "file.hash.sha256", + "file.hash.sha512", + "file.inode", + "file.mode", + "file.name", + "file.owner", + "file.path", + "file.target_path", + "file.type", + "file.uid", + "geo.city_name", + "geo.continent_name", + "geo.country_iso_code", + "geo.country_name", + "geo.name", + "geo.region_iso_code", + "geo.region_name", + "group.domain", + "group.id", + "group.name", + "hash.md5", + "hash.sha1", + "hash.sha256", + "hash.sha512", + "host.architecture", + "host.geo.city_name", + "host.geo.continent_name", + "host.geo.country_iso_code", + "host.geo.country_name", + "host.geo.name", + "host.geo.region_iso_code", + "host.geo.region_name", + "host.hostname", + "host.id", + "host.mac", + "host.name", + "host.os.family", + "host.os.full", + "host.os.kernel", + "host.os.name", + "host.os.platform", + "host.os.version", + "host.type", + "host.user.domain", + "host.user.email", + "host.user.full_name", + "host.user.group.domain", + "host.user.group.id", + "host.user.group.name", + "host.user.hash", + "host.user.id", + "host.user.name", + "http.request.body.content", + "http.request.method", + "http.request.referrer", + "http.response.body.content", + "http.version", + "log.level", + "log.logger", + "log.origin.file.name", + "log.origin.function", + "log.original", + "log.syslog.facility.name", + "log.syslog.severity.name", + "network.application", + "network.community_id", + "network.direction", + "network.iana_number", + "network.name", + "network.protocol", + "network.transport", + "network.type", + "observer.geo.city_name", + "observer.geo.continent_name", + "observer.geo.country_iso_code", + "observer.geo.country_name", + "observer.geo.name", + "observer.geo.region_iso_code", + "observer.geo.region_name", + "observer.hostname", + "observer.mac", + "observer.name", + "observer.os.family", + "observer.os.full", + "observer.os.kernel", + "observer.os.name", + "observer.os.platform", + "observer.os.version", + "observer.product", + "observer.serial_number", + "observer.type", + "observer.vendor", + "observer.version", + "organization.id", + "organization.name", + "os.family", + "os.full", + "os.kernel", + "os.name", + "os.platform", + "os.version", + "package.architecture", + "package.checksum", + "package.description", + "package.install_scope", + "package.license", + "package.name", + "package.path", + "package.version", + "process.args", + "text", + "process.executable", + "process.hash.md5", + "process.hash.sha1", + "process.hash.sha256", + "process.hash.sha512", + "process.name", + "text", + "text", + "text", + "text", + "text", + "process.thread.name", + "process.title", + "process.working_directory", + "server.address", + "server.as.organization.name", + "server.domain", + "server.geo.city_name", + "server.geo.continent_name", + "server.geo.country_iso_code", + "server.geo.country_name", + "server.geo.name", + "server.geo.region_iso_code", + "server.geo.region_name", + "server.mac", + "server.registered_domain", + "server.top_level_domain", + "server.user.domain", + "server.user.email", + "server.user.full_name", + "server.user.group.domain", + "server.user.group.id", + "server.user.group.name", + "server.user.hash", + "server.user.id", + "server.user.name", + "service.ephemeral_id", + "service.id", + "service.name", + "service.node.name", + "service.state", + "service.type", + "service.version", + "source.address", + "source.as.organization.name", + "source.domain", + "source.geo.city_name", + "source.geo.continent_name", + "source.geo.country_iso_code", + "source.geo.country_name", + "source.geo.name", + "source.geo.region_iso_code", + "source.geo.region_name", + "source.mac", + "source.registered_domain", + "source.top_level_domain", + "source.user.domain", + "source.user.email", + "source.user.full_name", + "source.user.group.domain", + "source.user.group.id", + "source.user.group.name", + "source.user.hash", + "source.user.id", + "source.user.name", + "threat.framework", + "threat.tactic.id", + "threat.tactic.name", + "threat.tactic.reference", + "threat.technique.id", + "threat.technique.name", + "threat.technique.reference", + "tracing.trace.id", + "tracing.transaction.id", + "url.domain", + "url.extension", + "url.fragment", + "url.full", + "url.original", + "url.password", + "url.path", + "url.query", + "url.registered_domain", + "url.scheme", + "url.top_level_domain", + "url.username", + "user.domain", + "user.email", + "user.full_name", + "user.group.domain", + "user.group.id", + "user.group.name", + "user.hash", + "user.id", + "user.name", + "user_agent.device.name", + "user_agent.name", + "text", + "user_agent.original", + "user_agent.os.family", + "user_agent.os.full", + "user_agent.os.kernel", + "user_agent.os.name", + "user_agent.os.platform", + "user_agent.os.version", + "user_agent.version", + "text", + "timeseries.instance", + "cloud.project.id", + "cloud.image.id", + "host.os.build", + "host.os.codename", + "kubernetes.pod.name", + "kubernetes.pod.uid", + "kubernetes.namespace", + "kubernetes.node.name", + "kubernetes.replicaset.name", + "kubernetes.deployment.name", + "kubernetes.statefulset.name", + "kubernetes.container.name", + "kubernetes.container.image", + "processor.name", + "processor.event", + "url.scheme", + "url.full", + "url.domain", + "url.path", + "url.query", + "url.fragment", + "http.version", + "http.request.method", + "http.request.referrer", + "service.name", + "service.version", + "service.environment", + "service.node.name", + "service.language.name", + "service.language.version", + "service.runtime.name", + "service.runtime.version", + "service.framework.name", + "service.framework.version", + "transaction.id", + "transaction.type", + "text", + "transaction.name", + "span.type", + "span.subtype", + "trace.id", + "parent.id", + "agent.name", + "agent.version", + "agent.ephemeral_id", + "container.id", + "kubernetes.namespace", + "kubernetes.node.name", + "kubernetes.pod.name", + "kubernetes.pod.uid", + "host.architecture", + "host.hostname", + "host.name", + "host.os.platform", + "process.args", + "process.title", + "observer.listening", + "observer.hostname", + "observer.version", + "observer.type", + "user.name", + "user.id", + "user.email", + "destination.address", + "text", + "user_agent.original", + "user_agent.name", + "user_agent.version", + "user_agent.device.name", + "user_agent.os.platform", + "user_agent.os.name", + "user_agent.os.full", + "user_agent.os.family", + "user_agent.os.version", + "user_agent.os.kernel", + "cloud.account.id", + "cloud.account.name", + "cloud.availability_zone", + "cloud.instance.id", + "cloud.instance.name", + "cloud.machine.type", + "cloud.project.id", + "cloud.project.name", + "cloud.provider", + "cloud.region", + "error.id", + "error.culprit", + "error.grouping_key", + "error.exception.code", + "error.exception.message", + "error.exception.module", + "error.exception.type", + "error.log.level", + "error.log.logger_name", + "error.log.message", + "error.log.param_message", + "profile.top.id", + "profile.top.function", + "profile.top.filename", + "profile.stack.id", + "profile.stack.function", + "profile.stack.filename", + "sourcemap.service.name", + "sourcemap.service.version", + "sourcemap.bundle_filepath", + "view spans", + "child.id", + "span.id", + "span.name", + "span.action", + "span.db.link", + "span.destination.service.type", + "span.destination.service.name", + "span.destination.service.resource", + "span.message.queue.name", + "transaction.result", + "transaction.message.queue.name", + "fields.*" + ] + }, + "refresh_interval": "1ms" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/apm_api_integration/basic/tests/index.ts b/x-pack/test/apm_api_integration/basic/tests/index.ts index 873aa478ad080..b6a32ace5db56 100644 --- a/x-pack/test/apm_api_integration/basic/tests/index.ts +++ b/x-pack/test/apm_api_integration/basic/tests/index.ts @@ -35,6 +35,13 @@ export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderCont loadTestFile(require.resolve('./transaction_groups/top_transaction_groups')); loadTestFile(require.resolve('./transaction_groups/transaction_charts')); loadTestFile(require.resolve('./transaction_groups/error_rate')); + loadTestFile(require.resolve('./transaction_groups/breakdown')); + loadTestFile(require.resolve('./transaction_groups/avg_duration_by_browser')); + }); + + describe('Observability overview', function () { + loadTestFile(require.resolve('./observability_overview/has_data')); + loadTestFile(require.resolve('./observability_overview/observability_overview')); }); }); } diff --git a/x-pack/test/apm_api_integration/basic/tests/observability_overview/has_data.ts b/x-pack/test/apm_api_integration/basic/tests/observability_overview/has_data.ts new file mode 100644 index 0000000000000..127721e8e2112 --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/observability_overview/has_data.ts @@ -0,0 +1,41 @@ +/* + * 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'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('Has data', () => { + describe('when data is not loaded', () => { + it('returns false when there is no data', async () => { + const response = await supertest.get('/api/apm/observability_overview/has_data'); + expect(response.status).to.be(200); + expect(response.body).to.eql(false); + }); + }); + describe('when only onboarding data is loaded', () => { + before(() => esArchiver.load('observability_overview')); + after(() => esArchiver.unload('observability_overview')); + it('returns false when there is only onboarding data', async () => { + const response = await supertest.get('/api/apm/observability_overview/has_data'); + expect(response.status).to.be(200); + expect(response.body).to.eql(false); + }); + }); + describe('when data is loaded', () => { + before(() => esArchiver.load('8.0.0')); + after(() => esArchiver.unload('8.0.0')); + + it('returns true when there is at least one document on transaction, error or metrics indices', async () => { + const response = await supertest.get('/api/apm/observability_overview/has_data'); + expect(response.status).to.be(200); + expect(response.body).to.eql(true); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts b/x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts new file mode 100644 index 0000000000000..bd8b0c6126faa --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts @@ -0,0 +1,47 @@ +/* + * 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'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + // url parameters + const start = encodeURIComponent('2020-06-29T06:00:00.000Z'); + const end = encodeURIComponent('2020-06-29T10:00:00.000Z'); + const bucketSize = '60s'; + + describe('Observability overview', () => { + describe('when data is not loaded', () => { + it('handles the empty state', async () => { + const response = await supertest.get( + `/api/apm/observability_overview?start=${start}&end=${end}&bucketSize=${bucketSize}` + ); + expect(response.status).to.be(200); + expect(response.body).to.eql({ serviceCount: 0, transactionCoordinates: [] }); + }); + }); + describe('when data is loaded', () => { + before(() => esArchiver.load('8.0.0')); + after(() => esArchiver.unload('8.0.0')); + + it('returns the service count and transaction coordinates', async () => { + const response = await supertest.get( + `/api/apm/observability_overview?start=${start}&end=${end}&bucketSize=${bucketSize}` + ); + expect(response.status).to.be(200); + expect(response.body).to.eql({ + serviceCount: 3, + transactionCoordinates: [ + { x: 1593413220000, y: 0.016666666666666666 }, + { x: 1593413280000, y: 1.0458333333333334 }, + ], + }); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/avg_duration_by_browser.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/avg_duration_by_browser.ts new file mode 100644 index 0000000000000..690935ddc7f6a --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/avg_duration_by_browser.ts @@ -0,0 +1,53 @@ +/* + * 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'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import expectedAvgDurationByBrowser from './expectation/avg_duration_by_browser.json'; +import expectedAvgDurationByBrowserWithTransactionName from './expectation/avg_duration_by_browser_transaction_name.json'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const start = encodeURIComponent('2020-06-29T06:45:00.000Z'); + const end = encodeURIComponent('2020-06-29T06:49:00.000Z'); + const transactionName = '/products'; + const uiFilters = encodeURIComponent(JSON.stringify({})); + + describe('Average duration by browser', () => { + describe('when data is not loaded', () => { + it('handles the empty state', async () => { + const response = await supertest.get( + `/api/apm/services/client/transaction_groups/avg_duration_by_browser?start=${start}&end=${end}&uiFilters=${uiFilters}` + ); + expect(response.status).to.be(200); + expect(response.body).to.eql([]); + }); + }); + + describe('when data is loaded', () => { + before(() => esArchiver.load('8.0.0')); + after(() => esArchiver.unload('8.0.0')); + + it('returns the average duration by browser', async () => { + const response = await supertest.get( + `/api/apm/services/client/transaction_groups/avg_duration_by_browser?start=${start}&end=${end}&uiFilters=${uiFilters}` + ); + + expect(response.status).to.be(200); + expect(response.body).to.eql(expectedAvgDurationByBrowser); + }); + it('returns the average duration by browser filtering by transaction name', async () => { + const response = await supertest.get( + `/api/apm/services/client/transaction_groups/avg_duration_by_browser?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionName=${transactionName}` + ); + + expect(response.status).to.be(200); + expect(response.body).to.eql(expectedAvgDurationByBrowserWithTransactionName); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts new file mode 100644 index 0000000000000..5b61112a374c1 --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts @@ -0,0 +1,67 @@ +/* + * 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'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import expectedBreakdown from './expectation/breakdown.json'; +import expectedBreakdownWithTransactionName from './expectation/breakdown_transaction_name.json'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const start = encodeURIComponent('2020-06-29T06:45:00.000Z'); + const end = encodeURIComponent('2020-06-29T06:49:00.000Z'); + const transactionType = 'request'; + const transactionName = 'GET /api'; + const uiFilters = encodeURIComponent(JSON.stringify({})); + + describe('Breakdown', () => { + describe('when data is not loaded', () => { + it('handles the empty state', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-node/transaction_groups/breakdown?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=${transactionType}` + ); + expect(response.status).to.be(200); + expect(response.body).to.eql({ kpis: [], timeseries: [] }); + }); + }); + + describe('when data is loaded', () => { + before(() => esArchiver.load('8.0.0')); + after(() => esArchiver.unload('8.0.0')); + + it('returns the transaction breakdown for a service', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-node/transaction_groups/breakdown?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=${transactionType}` + ); + + expect(response.status).to.be(200); + expect(response.body).to.eql(expectedBreakdown); + }); + it('returns the transaction breakdown for a transaction group', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-node/transaction_groups/breakdown?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=${transactionType}&transactionName=${transactionName}` + ); + + expect(response.status).to.be(200); + expect(response.body).to.eql(expectedBreakdownWithTransactionName); + }); + it('returns the top 4 by percentage and sorts them by name', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-node/transaction_groups/breakdown?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=${transactionType}` + ); + + expect(response.status).to.be(200); + expect(response.body.kpis.map((kpi: { name: string }) => kpi.name)).to.eql([ + 'app', + 'http', + 'postgresql', + 'redis', + ]); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/avg_duration_by_browser.json b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/avg_duration_by_browser.json new file mode 100644 index 0000000000000..cd53af3bf7080 --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/avg_duration_by_browser.json @@ -0,0 +1,735 @@ +[ + { + "title":"HeadlessChrome", + "data":[ + { + "x":1593413100000 + }, + { + "x":1593413101000 + }, + { + "x":1593413102000 + }, + { + "x":1593413103000 + }, + { + "x":1593413104000 + }, + { + "x":1593413105000 + }, + { + "x":1593413106000 + }, + { + "x":1593413107000 + }, + { + "x":1593413108000 + }, + { + "x":1593413109000 + }, + { + "x":1593413110000 + }, + { + "x":1593413111000 + }, + { + "x":1593413112000 + }, + { + "x":1593413113000 + }, + { + "x":1593413114000 + }, + { + "x":1593413115000 + }, + { + "x":1593413116000 + }, + { + "x":1593413117000 + }, + { + "x":1593413118000 + }, + { + "x":1593413119000 + }, + { + "x":1593413120000 + }, + { + "x":1593413121000 + }, + { + "x":1593413122000 + }, + { + "x":1593413123000 + }, + { + "x":1593413124000 + }, + { + "x":1593413125000 + }, + { + "x":1593413126000 + }, + { + "x":1593413127000 + }, + { + "x":1593413128000 + }, + { + "x":1593413129000 + }, + { + "x":1593413130000 + }, + { + "x":1593413131000 + }, + { + "x":1593413132000 + }, + { + "x":1593413133000 + }, + { + "x":1593413134000 + }, + { + "x":1593413135000 + }, + { + "x":1593413136000 + }, + { + "x":1593413137000 + }, + { + "x":1593413138000 + }, + { + "x":1593413139000 + }, + { + "x":1593413140000 + }, + { + "x":1593413141000 + }, + { + "x":1593413142000 + }, + { + "x":1593413143000 + }, + { + "x":1593413144000 + }, + { + "x":1593413145000 + }, + { + "x":1593413146000 + }, + { + "x":1593413147000 + }, + { + "x":1593413148000 + }, + { + "x":1593413149000 + }, + { + "x":1593413150000 + }, + { + "x":1593413151000 + }, + { + "x":1593413152000 + }, + { + "x":1593413153000 + }, + { + "x":1593413154000 + }, + { + "x":1593413155000 + }, + { + "x":1593413156000 + }, + { + "x":1593413157000 + }, + { + "x":1593413158000 + }, + { + "x":1593413159000 + }, + { + "x":1593413160000 + }, + { + "x":1593413161000 + }, + { + "x":1593413162000 + }, + { + "x":1593413163000 + }, + { + "x":1593413164000 + }, + { + "x":1593413165000 + }, + { + "x":1593413166000 + }, + { + "x":1593413167000 + }, + { + "x":1593413168000 + }, + { + "x":1593413169000 + }, + { + "x":1593413170000 + }, + { + "x":1593413171000 + }, + { + "x":1593413172000 + }, + { + "x":1593413173000 + }, + { + "x":1593413174000 + }, + { + "x":1593413175000 + }, + { + "x":1593413176000 + }, + { + "x":1593413177000 + }, + { + "x":1593413178000 + }, + { + "x":1593413179000 + }, + { + "x":1593413180000 + }, + { + "x":1593413181000 + }, + { + "x":1593413182000 + }, + { + "x":1593413183000 + }, + { + "x":1593413184000 + }, + { + "x":1593413185000 + }, + { + "x":1593413186000 + }, + { + "x":1593413187000 + }, + { + "x":1593413188000 + }, + { + "x":1593413189000 + }, + { + "x":1593413190000 + }, + { + "x":1593413191000 + }, + { + "x":1593413192000 + }, + { + "x":1593413193000 + }, + { + "x":1593413194000 + }, + { + "x":1593413195000 + }, + { + "x":1593413196000 + }, + { + "x":1593413197000 + }, + { + "x":1593413198000 + }, + { + "x":1593413199000 + }, + { + "x":1593413200000 + }, + { + "x":1593413201000 + }, + { + "x":1593413202000 + }, + { + "x":1593413203000 + }, + { + "x":1593413204000 + }, + { + "x":1593413205000 + }, + { + "x":1593413206000 + }, + { + "x":1593413207000 + }, + { + "x":1593413208000 + }, + { + "x":1593413209000 + }, + { + "x":1593413210000 + }, + { + "x":1593413211000 + }, + { + "x":1593413212000 + }, + { + "x":1593413213000 + }, + { + "x":1593413214000 + }, + { + "x":1593413215000 + }, + { + "x":1593413216000 + }, + { + "x":1593413217000 + }, + { + "x":1593413218000 + }, + { + "x":1593413219000 + }, + { + "x":1593413220000 + }, + { + "x":1593413221000 + }, + { + "x":1593413222000 + }, + { + "x":1593413223000 + }, + { + "x":1593413224000 + }, + { + "x":1593413225000 + }, + { + "x":1593413226000 + }, + { + "x":1593413227000 + }, + { + "x":1593413228000 + }, + { + "x":1593413229000 + }, + { + "x":1593413230000 + }, + { + "x":1593413231000 + }, + { + "x":1593413232000 + }, + { + "x":1593413233000 + }, + { + "x":1593413234000 + }, + { + "x":1593413235000 + }, + { + "x":1593413236000 + }, + { + "x":1593413237000 + }, + { + "x":1593413238000 + }, + { + "x":1593413239000 + }, + { + "x":1593413240000 + }, + { + "x":1593413241000 + }, + { + "x":1593413242000 + }, + { + "x":1593413243000 + }, + { + "x":1593413244000 + }, + { + "x":1593413245000 + }, + { + "x":1593413246000 + }, + { + "x":1593413247000 + }, + { + "x":1593413248000 + }, + { + "x":1593413249000 + }, + { + "x":1593413250000 + }, + { + "x":1593413251000 + }, + { + "x":1593413252000 + }, + { + "x":1593413253000 + }, + { + "x":1593413254000 + }, + { + "x":1593413255000 + }, + { + "x":1593413256000 + }, + { + "x":1593413257000 + }, + { + "x":1593413258000 + }, + { + "x":1593413259000 + }, + { + "x":1593413260000 + }, + { + "x":1593413261000 + }, + { + "x":1593413262000 + }, + { + "x":1593413263000 + }, + { + "x":1593413264000 + }, + { + "x":1593413265000 + }, + { + "x":1593413266000 + }, + { + "x":1593413267000 + }, + { + "x":1593413268000 + }, + { + "x":1593413269000 + }, + { + "x":1593413270000 + }, + { + "x":1593413271000 + }, + { + "x":1593413272000 + }, + { + "x":1593413273000 + }, + { + "x":1593413274000 + }, + { + "x":1593413275000 + }, + { + "x":1593413276000 + }, + { + "x":1593413277000 + }, + { + "x":1593413278000 + }, + { + "x":1593413279000 + }, + { + "x":1593413280000 + }, + { + "x":1593413281000 + }, + { + "x":1593413282000 + }, + { + "x":1593413283000 + }, + { + "x":1593413284000 + }, + { + "x":1593413285000 + }, + { + "x":1593413286000 + }, + { + "x":1593413287000, + "y":342000 + }, + { + "x":1593413288000 + }, + { + "x":1593413289000 + }, + { + "x":1593413290000 + }, + { + "x":1593413291000 + }, + { + "x":1593413292000 + }, + { + "x":1593413293000 + }, + { + "x":1593413294000 + }, + { + "x":1593413295000 + }, + { + "x":1593413296000 + }, + { + "x":1593413297000 + }, + { + "x":1593413298000, + "y":173000 + }, + { + "x":1593413299000 + }, + { + "x":1593413300000 + }, + { + "x":1593413301000, + "y":109000 + }, + { + "x":1593413302000 + }, + { + "x":1593413303000 + }, + { + "x":1593413304000 + }, + { + "x":1593413305000 + }, + { + "x":1593413306000 + }, + { + "x":1593413307000 + }, + { + "x":1593413308000 + }, + { + "x":1593413309000 + }, + { + "x":1593413310000 + }, + { + "x":1593413311000 + }, + { + "x":1593413312000 + }, + { + "x":1593413313000 + }, + { + "x":1593413314000 + }, + { + "x":1593413315000 + }, + { + "x":1593413316000 + }, + { + "x":1593413317000 + }, + { + "x":1593413318000, + "y":140000 + }, + { + "x":1593413319000 + }, + { + "x":1593413320000 + }, + { + "x":1593413321000 + }, + { + "x":1593413322000 + }, + { + "x":1593413323000 + }, + { + "x":1593413324000 + }, + { + "x":1593413325000 + }, + { + "x":1593413326000 + }, + { + "x":1593413327000 + }, + { + "x":1593413328000, + "y":77000 + }, + { + "x":1593413329000 + }, + { + "x":1593413330000 + }, + { + "x":1593413331000 + }, + { + "x":1593413332000 + }, + { + "x":1593413333000 + }, + { + "x":1593413334000 + }, + { + "x":1593413335000 + }, + { + "x":1593413336000 + }, + { + "x":1593413337000 + }, + { + "x":1593413338000 + }, + { + "x":1593413339000 + }, + { + "x":1593413340000 + } + ] + } +] \ No newline at end of file diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/avg_duration_by_browser_transaction_name.json b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/avg_duration_by_browser_transaction_name.json new file mode 100644 index 0000000000000..107302831d55f --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/avg_duration_by_browser_transaction_name.json @@ -0,0 +1,731 @@ +[ + { + "title":"HeadlessChrome", + "data":[ + { + "x":1593413100000 + }, + { + "x":1593413101000 + }, + { + "x":1593413102000 + }, + { + "x":1593413103000 + }, + { + "x":1593413104000 + }, + { + "x":1593413105000 + }, + { + "x":1593413106000 + }, + { + "x":1593413107000 + }, + { + "x":1593413108000 + }, + { + "x":1593413109000 + }, + { + "x":1593413110000 + }, + { + "x":1593413111000 + }, + { + "x":1593413112000 + }, + { + "x":1593413113000 + }, + { + "x":1593413114000 + }, + { + "x":1593413115000 + }, + { + "x":1593413116000 + }, + { + "x":1593413117000 + }, + { + "x":1593413118000 + }, + { + "x":1593413119000 + }, + { + "x":1593413120000 + }, + { + "x":1593413121000 + }, + { + "x":1593413122000 + }, + { + "x":1593413123000 + }, + { + "x":1593413124000 + }, + { + "x":1593413125000 + }, + { + "x":1593413126000 + }, + { + "x":1593413127000 + }, + { + "x":1593413128000 + }, + { + "x":1593413129000 + }, + { + "x":1593413130000 + }, + { + "x":1593413131000 + }, + { + "x":1593413132000 + }, + { + "x":1593413133000 + }, + { + "x":1593413134000 + }, + { + "x":1593413135000 + }, + { + "x":1593413136000 + }, + { + "x":1593413137000 + }, + { + "x":1593413138000 + }, + { + "x":1593413139000 + }, + { + "x":1593413140000 + }, + { + "x":1593413141000 + }, + { + "x":1593413142000 + }, + { + "x":1593413143000 + }, + { + "x":1593413144000 + }, + { + "x":1593413145000 + }, + { + "x":1593413146000 + }, + { + "x":1593413147000 + }, + { + "x":1593413148000 + }, + { + "x":1593413149000 + }, + { + "x":1593413150000 + }, + { + "x":1593413151000 + }, + { + "x":1593413152000 + }, + { + "x":1593413153000 + }, + { + "x":1593413154000 + }, + { + "x":1593413155000 + }, + { + "x":1593413156000 + }, + { + "x":1593413157000 + }, + { + "x":1593413158000 + }, + { + "x":1593413159000 + }, + { + "x":1593413160000 + }, + { + "x":1593413161000 + }, + { + "x":1593413162000 + }, + { + "x":1593413163000 + }, + { + "x":1593413164000 + }, + { + "x":1593413165000 + }, + { + "x":1593413166000 + }, + { + "x":1593413167000 + }, + { + "x":1593413168000 + }, + { + "x":1593413169000 + }, + { + "x":1593413170000 + }, + { + "x":1593413171000 + }, + { + "x":1593413172000 + }, + { + "x":1593413173000 + }, + { + "x":1593413174000 + }, + { + "x":1593413175000 + }, + { + "x":1593413176000 + }, + { + "x":1593413177000 + }, + { + "x":1593413178000 + }, + { + "x":1593413179000 + }, + { + "x":1593413180000 + }, + { + "x":1593413181000 + }, + { + "x":1593413182000 + }, + { + "x":1593413183000 + }, + { + "x":1593413184000 + }, + { + "x":1593413185000 + }, + { + "x":1593413186000 + }, + { + "x":1593413187000 + }, + { + "x":1593413188000 + }, + { + "x":1593413189000 + }, + { + "x":1593413190000 + }, + { + "x":1593413191000 + }, + { + "x":1593413192000 + }, + { + "x":1593413193000 + }, + { + "x":1593413194000 + }, + { + "x":1593413195000 + }, + { + "x":1593413196000 + }, + { + "x":1593413197000 + }, + { + "x":1593413198000 + }, + { + "x":1593413199000 + }, + { + "x":1593413200000 + }, + { + "x":1593413201000 + }, + { + "x":1593413202000 + }, + { + "x":1593413203000 + }, + { + "x":1593413204000 + }, + { + "x":1593413205000 + }, + { + "x":1593413206000 + }, + { + "x":1593413207000 + }, + { + "x":1593413208000 + }, + { + "x":1593413209000 + }, + { + "x":1593413210000 + }, + { + "x":1593413211000 + }, + { + "x":1593413212000 + }, + { + "x":1593413213000 + }, + { + "x":1593413214000 + }, + { + "x":1593413215000 + }, + { + "x":1593413216000 + }, + { + "x":1593413217000 + }, + { + "x":1593413218000 + }, + { + "x":1593413219000 + }, + { + "x":1593413220000 + }, + { + "x":1593413221000 + }, + { + "x":1593413222000 + }, + { + "x":1593413223000 + }, + { + "x":1593413224000 + }, + { + "x":1593413225000 + }, + { + "x":1593413226000 + }, + { + "x":1593413227000 + }, + { + "x":1593413228000 + }, + { + "x":1593413229000 + }, + { + "x":1593413230000 + }, + { + "x":1593413231000 + }, + { + "x":1593413232000 + }, + { + "x":1593413233000 + }, + { + "x":1593413234000 + }, + { + "x":1593413235000 + }, + { + "x":1593413236000 + }, + { + "x":1593413237000 + }, + { + "x":1593413238000 + }, + { + "x":1593413239000 + }, + { + "x":1593413240000 + }, + { + "x":1593413241000 + }, + { + "x":1593413242000 + }, + { + "x":1593413243000 + }, + { + "x":1593413244000 + }, + { + "x":1593413245000 + }, + { + "x":1593413246000 + }, + { + "x":1593413247000 + }, + { + "x":1593413248000 + }, + { + "x":1593413249000 + }, + { + "x":1593413250000 + }, + { + "x":1593413251000 + }, + { + "x":1593413252000 + }, + { + "x":1593413253000 + }, + { + "x":1593413254000 + }, + { + "x":1593413255000 + }, + { + "x":1593413256000 + }, + { + "x":1593413257000 + }, + { + "x":1593413258000 + }, + { + "x":1593413259000 + }, + { + "x":1593413260000 + }, + { + "x":1593413261000 + }, + { + "x":1593413262000 + }, + { + "x":1593413263000 + }, + { + "x":1593413264000 + }, + { + "x":1593413265000 + }, + { + "x":1593413266000 + }, + { + "x":1593413267000 + }, + { + "x":1593413268000 + }, + { + "x":1593413269000 + }, + { + "x":1593413270000 + }, + { + "x":1593413271000 + }, + { + "x":1593413272000 + }, + { + "x":1593413273000 + }, + { + "x":1593413274000 + }, + { + "x":1593413275000 + }, + { + "x":1593413276000 + }, + { + "x":1593413277000 + }, + { + "x":1593413278000 + }, + { + "x":1593413279000 + }, + { + "x":1593413280000 + }, + { + "x":1593413281000 + }, + { + "x":1593413282000 + }, + { + "x":1593413283000 + }, + { + "x":1593413284000 + }, + { + "x":1593413285000 + }, + { + "x":1593413286000 + }, + { + "x":1593413287000 + }, + { + "x":1593413288000 + }, + { + "x":1593413289000 + }, + { + "x":1593413290000 + }, + { + "x":1593413291000 + }, + { + "x":1593413292000 + }, + { + "x":1593413293000 + }, + { + "x":1593413294000 + }, + { + "x":1593413295000 + }, + { + "x":1593413296000 + }, + { + "x":1593413297000 + }, + { + "x":1593413298000 + }, + { + "x":1593413299000 + }, + { + "x":1593413300000 + }, + { + "x":1593413301000 + }, + { + "x":1593413302000 + }, + { + "x":1593413303000 + }, + { + "x":1593413304000 + }, + { + "x":1593413305000 + }, + { + "x":1593413306000 + }, + { + "x":1593413307000 + }, + { + "x":1593413308000 + }, + { + "x":1593413309000 + }, + { + "x":1593413310000 + }, + { + "x":1593413311000 + }, + { + "x":1593413312000 + }, + { + "x":1593413313000 + }, + { + "x":1593413314000 + }, + { + "x":1593413315000 + }, + { + "x":1593413316000 + }, + { + "x":1593413317000 + }, + { + "x":1593413318000 + }, + { + "x":1593413319000 + }, + { + "x":1593413320000 + }, + { + "x":1593413321000 + }, + { + "x":1593413322000 + }, + { + "x":1593413323000 + }, + { + "x":1593413324000 + }, + { + "x":1593413325000 + }, + { + "x":1593413326000 + }, + { + "x":1593413327000 + }, + { + "x":1593413328000, + "y":77000 + }, + { + "x":1593413329000 + }, + { + "x":1593413330000 + }, + { + "x":1593413331000 + }, + { + "x":1593413332000 + }, + { + "x":1593413333000 + }, + { + "x":1593413334000 + }, + { + "x":1593413335000 + }, + { + "x":1593413336000 + }, + { + "x":1593413337000 + }, + { + "x":1593413338000 + }, + { + "x":1593413339000 + }, + { + "x":1593413340000 + } + ] + } +] \ No newline at end of file diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown.json b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown.json new file mode 100644 index 0000000000000..3b884a9eb7907 --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown.json @@ -0,0 +1,202 @@ +{ + "kpis":[ + { + "name":"app", + "percentage":0.16700861715223636, + "color":"#54b399" + }, + { + "name":"http", + "percentage":0.7702092736971686, + "color":"#6092c0" + }, + { + "name":"postgresql", + "percentage":0.0508822322527698, + "color":"#d36086" + }, + { + "name":"redis", + "percentage":0.011899876897825195, + "color":"#9170b8" + } + ], + "timeseries":[ + { + "title":"app", + "color":"#54b399", + "type":"areaStacked", + "data":[ + { + "x":1593413100000, + "y":null + }, + { + "x":1593413130000, + "y":null + }, + { + "x":1593413160000, + "y":null + }, + { + "x":1593413190000, + "y":null + }, + { + "x":1593413220000, + "y":null + }, + { + "x":1593413250000, + "y":null + }, + { + "x":1593413280000, + "y":null + }, + { + "x":1593413310000, + "y":0.16700861715223636 + }, + { + "x":1593413340000, + "y":null + } + ], + "hideLegend":true + }, + { + "title":"http", + "color":"#6092c0", + "type":"areaStacked", + "data":[ + { + "x":1593413100000, + "y":null + }, + { + "x":1593413130000, + "y":null + }, + { + "x":1593413160000, + "y":null + }, + { + "x":1593413190000, + "y":null + }, + { + "x":1593413220000, + "y":null + }, + { + "x":1593413250000, + "y":null + }, + { + "x":1593413280000, + "y":null + }, + { + "x":1593413310000, + "y":0.7702092736971686 + }, + { + "x":1593413340000, + "y":null + } + ], + "hideLegend":true + }, + { + "title":"postgresql", + "color":"#d36086", + "type":"areaStacked", + "data":[ + { + "x":1593413100000, + "y":null + }, + { + "x":1593413130000, + "y":null + }, + { + "x":1593413160000, + "y":null + }, + { + "x":1593413190000, + "y":null + }, + { + "x":1593413220000, + "y":null + }, + { + "x":1593413250000, + "y":null + }, + { + "x":1593413280000, + "y":null + }, + { + "x":1593413310000, + "y":0.0508822322527698 + }, + { + "x":1593413340000, + "y":null + } + ], + "hideLegend":true + }, + { + "title":"redis", + "color":"#9170b8", + "type":"areaStacked", + "data":[ + { + "x":1593413100000, + "y":null + }, + { + "x":1593413130000, + "y":null + }, + { + "x":1593413160000, + "y":null + }, + { + "x":1593413190000, + "y":null + }, + { + "x":1593413220000, + "y":null + }, + { + "x":1593413250000, + "y":null + }, + { + "x":1593413280000, + "y":null + }, + { + "x":1593413310000, + "y":0.011899876897825195 + }, + { + "x":1593413340000, + "y":null + } + ], + "hideLegend":true + } + ] +} \ No newline at end of file diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown_transaction_name.json b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown_transaction_name.json new file mode 100644 index 0000000000000..b4f8e376d3609 --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown_transaction_name.json @@ -0,0 +1,55 @@ +{ + "kpis":[ + { + "name":"app", + "percentage":1, + "color":"#54b399" + } + ], + "timeseries":[ + { + "title":"app", + "color":"#54b399", + "type":"areaStacked", + "data":[ + { + "x":1593413100000, + "y":null + }, + { + "x":1593413130000, + "y":null + }, + { + "x":1593413160000, + "y":null + }, + { + "x":1593413190000, + "y":null + }, + { + "x":1593413220000, + "y":null + }, + { + "x":1593413250000, + "y":null + }, + { + "x":1593413280000, + "y":null + }, + { + "x":1593413310000, + "y":1 + }, + { + "x":1593413340000, + "y":null + } + ], + "hideLegend":true + } + ] +} \ No newline at end of file