diff --git a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md deleted file mode 100644 index 9cd77ca6e3a368..00000000000000 --- a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-embeddable-server](./kibana-plugin-plugins-embeddable-server.md) > [EmbeddableSetup](./kibana-plugin-plugins-embeddable-server.embeddablesetup.md) > [getAttributeService](./kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md) - -## EmbeddableSetup.getAttributeService property - -Signature: - -```typescript -getAttributeService: any; -``` diff --git a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md index bd024095e80bed..5109a75ad57f06 100644 --- a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md +++ b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md @@ -7,14 +7,13 @@ Signature: ```typescript -export interface EmbeddableSetup +export interface EmbeddableSetup extends PersistableStateService ``` ## Properties | Property | Type | Description | | --- | --- | --- | -| [getAttributeService](./kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md) | any | | | [registerEmbeddableFactory](./kibana-plugin-plugins-embeddable-server.embeddablesetup.registerembeddablefactory.md) | (factory: EmbeddableRegistryDefinition) => void | | | [registerEnhancement](./kibana-plugin-plugins-embeddable-server.embeddablesetup.registerenhancement.md) | (enhancement: EnhancementRegistryDefinition) => void | | diff --git a/src/plugins/dashboard/common/embeddable/embeddable_references.test.ts b/src/plugins/dashboard/common/embeddable/embeddable_references.test.ts new file mode 100644 index 00000000000000..fabc89f8c8233a --- /dev/null +++ b/src/plugins/dashboard/common/embeddable/embeddable_references.test.ts @@ -0,0 +1,87 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + ExtractDeps, + extractPanelsReferences, + InjectDeps, + injectPanelsReferences, +} from './embeddable_references'; +import { createEmbeddablePersistableStateServiceMock } from '../../../embeddable/common/mocks'; +import { SavedDashboardPanel } from '../types'; +import { EmbeddableStateWithType } from '../../../embeddable/common'; + +const embeddablePersistableStateService = createEmbeddablePersistableStateServiceMock(); +const deps: InjectDeps & ExtractDeps = { + embeddablePersistableStateService, +}; + +test('inject/extract panel references', () => { + embeddablePersistableStateService.extract.mockImplementationOnce((state) => { + const { HARDCODED_ID, ...restOfState } = (state as unknown) as Record; + return { + state: restOfState as EmbeddableStateWithType, + references: [{ id: HARDCODED_ID as string, name: 'refName', type: 'type' }], + }; + }); + + embeddablePersistableStateService.inject.mockImplementationOnce((state, references) => { + const ref = references.find((r) => r.name === 'refName'); + return { + ...state, + HARDCODED_ID: ref!.id, + }; + }); + + const savedDashboardPanel: SavedDashboardPanel = { + type: 'search', + embeddableConfig: { + HARDCODED_ID: 'IMPORTANT_HARDCODED_ID', + }, + id: 'savedObjectId', + panelIndex: '123', + gridData: { + x: 0, + y: 0, + h: 15, + w: 15, + i: '123', + }, + version: '7.0.0', + }; + + const [{ panel: extractedPanel, references }] = extractPanelsReferences( + [savedDashboardPanel], + deps + ); + expect(extractedPanel.embeddableConfig).toEqual({}); + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "IMPORTANT_HARDCODED_ID", + "name": "refName", + "type": "type", + }, + ] + `); + + const [injectedPanel] = injectPanelsReferences([extractedPanel], references, deps); + + expect(injectedPanel).toEqual(savedDashboardPanel); +}); diff --git a/src/plugins/dashboard/common/embeddable/embeddable_references.ts b/src/plugins/dashboard/common/embeddable/embeddable_references.ts new file mode 100644 index 00000000000000..dd686203fa3516 --- /dev/null +++ b/src/plugins/dashboard/common/embeddable/embeddable_references.ts @@ -0,0 +1,82 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { omit } from 'lodash'; +import { + convertSavedDashboardPanelToPanelState, + convertPanelStateToSavedDashboardPanel, +} from './embeddable_saved_object_converters'; +import { SavedDashboardPanel } from '../types'; +import { SavedObjectReference } from '../../../../core/types'; +import { EmbeddablePersistableStateService } from '../../../embeddable/common/types'; + +export interface InjectDeps { + embeddablePersistableStateService: EmbeddablePersistableStateService; +} + +export function injectPanelsReferences( + panels: SavedDashboardPanel[], + references: SavedObjectReference[], + deps: InjectDeps +): SavedDashboardPanel[] { + const result: SavedDashboardPanel[] = []; + for (const panel of panels) { + const embeddableState = convertSavedDashboardPanelToPanelState(panel); + embeddableState.explicitInput = omit( + deps.embeddablePersistableStateService.inject( + { ...embeddableState.explicitInput, type: panel.type }, + references + ), + 'type' + ); + result.push(convertPanelStateToSavedDashboardPanel(embeddableState, panel.version)); + } + return result; +} + +export interface ExtractDeps { + embeddablePersistableStateService: EmbeddablePersistableStateService; +} + +export function extractPanelsReferences( + panels: SavedDashboardPanel[], + deps: ExtractDeps +): Array<{ panel: SavedDashboardPanel; references: SavedObjectReference[] }> { + const result: Array<{ panel: SavedDashboardPanel; references: SavedObjectReference[] }> = []; + + for (const panel of panels) { + const embeddable = convertSavedDashboardPanelToPanelState(panel); + const { + state: embeddableInputWithExtractedReferences, + references, + } = deps.embeddablePersistableStateService.extract({ + ...embeddable.explicitInput, + type: embeddable.type, + }); + embeddable.explicitInput = omit(embeddableInputWithExtractedReferences, 'type'); + + const newPanel = convertPanelStateToSavedDashboardPanel(embeddable, panel.version); + result.push({ + panel: newPanel, + references, + }); + } + + return result; +} diff --git a/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.test.ts b/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.test.ts similarity index 82% rename from src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.test.ts rename to src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.test.ts index 926d5f405b384a..bf044a1fa77d10 100644 --- a/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.test.ts +++ b/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.test.ts @@ -21,9 +21,8 @@ import { convertSavedDashboardPanelToPanelState, convertPanelStateToSavedDashboardPanel, } from './embeddable_saved_object_converters'; -import { SavedDashboardPanel } from '../../types'; -import { DashboardPanelState } from '../embeddable'; -import { EmbeddableInput } from '../../../../embeddable/public'; +import { SavedDashboardPanel, DashboardPanelState } from '../types'; +import { EmbeddableInput } from '../../../embeddable/common/types'; test('convertSavedDashboardPanelToPanelState', () => { const savedDashboardPanel: SavedDashboardPanel = { @@ -135,3 +134,24 @@ test('convertPanelStateToSavedDashboardPanel will not add an undefined id when n const converted = convertPanelStateToSavedDashboardPanel(dashboardPanel, '8.0.0'); expect(converted.hasOwnProperty('id')).toBe(false); }); + +test('convertPanelStateToSavedDashboardPanel will not leave title as part of embeddable config', () => { + const dashboardPanel: DashboardPanelState = { + gridData: { + x: 0, + y: 0, + h: 15, + w: 15, + i: '123', + }, + explicitInput: { + id: '123', + title: 'title', + } as EmbeddableInput, + type: 'search', + }; + + const converted = convertPanelStateToSavedDashboardPanel(dashboardPanel, '8.0.0'); + expect(converted.embeddableConfig.hasOwnProperty('title')).toBe(false); + expect(converted.title).toBe('title'); +}); diff --git a/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.ts b/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts similarity index 91% rename from src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.ts rename to src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts index b19ef31ccb9ac0..b71b4f067ae33d 100644 --- a/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.ts +++ b/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts @@ -17,9 +17,8 @@ * under the License. */ import { omit } from 'lodash'; -import { SavedDashboardPanel } from '../../types'; -import { DashboardPanelState } from '../embeddable'; -import { SavedObjectEmbeddableInput } from '../../embeddable_plugin'; +import { DashboardPanelState, SavedDashboardPanel } from '../types'; +import { SavedObjectEmbeddableInput } from '../../../embeddable/common/'; export function convertSavedDashboardPanelToPanelState( savedDashboardPanel: SavedDashboardPanel @@ -49,7 +48,7 @@ export function convertPanelStateToSavedDashboardPanel( type: panelState.type, gridData: panelState.gridData, panelIndex: panelState.explicitInput.id, - embeddableConfig: omit(panelState.explicitInput, ['id', 'savedObjectId']), + embeddableConfig: omit(panelState.explicitInput, ['id', 'savedObjectId', 'title']), ...(customTitle && { title: customTitle }), ...(savedObjectId !== undefined && { id: savedObjectId }), }; diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.test.ts b/src/plugins/dashboard/common/saved_dashboard_references.test.ts similarity index 52% rename from src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.test.ts rename to src/plugins/dashboard/common/saved_dashboard_references.test.ts index 48f15e84c93072..3632c4cca9e937 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.test.ts +++ b/src/plugins/dashboard/common/saved_dashboard_references.test.ts @@ -17,8 +17,18 @@ * under the License. */ -import { extractReferences, injectReferences } from './saved_dashboard_references'; -import { SavedObjectDashboard } from './saved_dashboard'; +import { + extractReferences, + injectReferences, + InjectDeps, + ExtractDeps, +} from './saved_dashboard_references'; +import { createEmbeddablePersistableStateServiceMock } from '../../embeddable/common/mocks'; + +const embeddablePersistableStateServiceMock = createEmbeddablePersistableStateServiceMock(); +const deps: InjectDeps & ExtractDeps = { + embeddablePersistableStateService: embeddablePersistableStateServiceMock, +}; describe('extractReferences', () => { test('extracts references from panelsJSON', () => { @@ -41,28 +51,28 @@ describe('extractReferences', () => { }, references: [], }; - const updatedDoc = extractReferences(doc); + const updatedDoc = extractReferences(doc, deps); expect(updatedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - "panelsJSON": "[{\\"title\\":\\"Title 1\\",\\"panelRefName\\":\\"panel_0\\"},{\\"title\\":\\"Title 2\\",\\"panelRefName\\":\\"panel_1\\"}]", - }, - "references": Array [ - Object { - "id": "1", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel_1", - "type": "visualization", - }, - ], -} -`); + Object { + "attributes": Object { + "foo": true, + "panelsJSON": "[{\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"panelRefName\\":\\"panel_0\\"},{\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"panelRefName\\":\\"panel_1\\"}]", + }, + "references": Array [ + Object { + "id": "1", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel_1", + "type": "visualization", + }, + ], + } + `); }); test('fails when "type" attribute is missing from a panel', () => { @@ -79,7 +89,7 @@ Object { }, references: [], }; - expect(() => extractReferences(doc)).toThrowErrorMatchingInlineSnapshot( + expect(() => extractReferences(doc, deps)).toThrowErrorMatchingInlineSnapshot( `"\\"type\\" attribute is missing from panel \\"0\\""` ); }); @@ -98,21 +108,21 @@ Object { }, references: [], }; - expect(extractReferences(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - "panelsJSON": "[{\\"type\\":\\"visualization\\",\\"title\\":\\"Title 1\\"}]", - }, - "references": Array [], -} -`); + expect(extractReferences(doc, deps)).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "foo": true, + "panelsJSON": "[{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\"}]", + }, + "references": Array [], + } + `); }); }); describe('injectReferences', () => { - test('injects references into context', () => { - const context = { + test('returns injected attributes', () => { + const attributes = { id: '1', title: 'test', panelsJSON: JSON.stringify([ @@ -125,7 +135,7 @@ describe('injectReferences', () => { title: 'Title 2', }, ]), - } as SavedObjectDashboard; + }; const references = [ { name: 'panel_0', @@ -138,49 +148,49 @@ describe('injectReferences', () => { id: '2', }, ]; - injectReferences(context, references); + const newAttributes = injectReferences({ attributes, references }, deps); - expect(context).toMatchInlineSnapshot(` -Object { - "id": "1", - "panelsJSON": "[{\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\",\\"type\\":\\"visualization\\"},{\\"title\\":\\"Title 2\\",\\"id\\":\\"2\\",\\"type\\":\\"visualization\\"}]", - "title": "test", -} -`); + expect(newAttributes).toMatchInlineSnapshot(` + Object { + "id": "1", + "panelsJSON": "[{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"id\\":\\"2\\"}]", + "title": "test", + } + `); }); test('skips when panelsJSON is missing', () => { - const context = { + const attributes = { id: '1', title: 'test', - } as SavedObjectDashboard; - injectReferences(context, []); - expect(context).toMatchInlineSnapshot(` -Object { - "id": "1", - "title": "test", -} -`); + }; + const newAttributes = injectReferences({ attributes, references: [] }, deps); + expect(newAttributes).toMatchInlineSnapshot(` + Object { + "id": "1", + "title": "test", + } + `); }); test('skips when panelsJSON is not an array', () => { - const context = { + const attributes = { id: '1', panelsJSON: '{}', title: 'test', - } as SavedObjectDashboard; - injectReferences(context, []); - expect(context).toMatchInlineSnapshot(` -Object { - "id": "1", - "panelsJSON": "{}", - "title": "test", -} -`); + }; + const newAttributes = injectReferences({ attributes, references: [] }, deps); + expect(newAttributes).toMatchInlineSnapshot(` + Object { + "id": "1", + "panelsJSON": "{}", + "title": "test", + } + `); }); test('skips a panel when panelRefName is missing', () => { - const context = { + const attributes = { id: '1', title: 'test', panelsJSON: JSON.stringify([ @@ -192,7 +202,7 @@ Object { title: 'Title 2', }, ]), - } as SavedObjectDashboard; + }; const references = [ { name: 'panel_0', @@ -200,18 +210,18 @@ Object { id: '1', }, ]; - injectReferences(context, references); - expect(context).toMatchInlineSnapshot(` -Object { - "id": "1", - "panelsJSON": "[{\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\",\\"type\\":\\"visualization\\"},{\\"title\\":\\"Title 2\\"}]", - "title": "test", -} -`); + const newAttributes = injectReferences({ attributes, references }, deps); + expect(newAttributes).toMatchInlineSnapshot(` + Object { + "id": "1", + "panelsJSON": "[{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\"}]", + "title": "test", + } + `); }); test(`fails when it can't find the reference in the array`, () => { - const context = { + const attributes = { id: '1', title: 'test', panelsJSON: JSON.stringify([ @@ -220,9 +230,9 @@ Object { title: 'Title 1', }, ]), - } as SavedObjectDashboard; - expect(() => injectReferences(context, [])).toThrowErrorMatchingInlineSnapshot( - `"Could not find reference \\"panel_0\\""` - ); + }; + expect(() => + injectReferences({ attributes, references: [] }, deps) + ).toThrowErrorMatchingInlineSnapshot(`"Could not find reference \\"panel_0\\""`); }); }); diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.ts b/src/plugins/dashboard/common/saved_dashboard_references.ts similarity index 55% rename from src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.ts rename to src/plugins/dashboard/common/saved_dashboard_references.ts index 3df9e64887725a..0726d301b34ac4 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.ts +++ b/src/plugins/dashboard/common/saved_dashboard_references.ts @@ -17,18 +17,47 @@ * under the License. */ -import { SavedObjectAttributes, SavedObjectReference } from 'kibana/public'; -import { SavedObjectDashboard } from './saved_dashboard'; +import { SavedObjectAttributes, SavedObjectReference } from '../../../core/types'; +import { + extractPanelsReferences, + injectPanelsReferences, +} from './embeddable/embeddable_references'; +import { SavedDashboardPanel730ToLatest } from './types'; +import { EmbeddablePersistableStateService } from '../../embeddable/common/types'; -export function extractReferences({ - attributes, - references = [], -}: { +export interface ExtractDeps { + embeddablePersistableStateService: EmbeddablePersistableStateService; +} + +export interface SavedObjectAttributesAndReferences { attributes: SavedObjectAttributes; references: SavedObjectReference[]; -}) { +} + +export function extractReferences( + { attributes, references = [] }: SavedObjectAttributesAndReferences, + deps: ExtractDeps +): SavedObjectAttributesAndReferences { + if (typeof attributes.panelsJSON !== 'string') { + return { attributes, references }; + } const panelReferences: SavedObjectReference[] = []; - const panels: Array> = JSON.parse(String(attributes.panelsJSON)); + let panels: Array> = JSON.parse(String(attributes.panelsJSON)); + + const extractedReferencesResult = extractPanelsReferences( + (panels as unknown) as SavedDashboardPanel730ToLatest[], + deps + ); + + panels = (extractedReferencesResult.map((res) => res.panel) as unknown) as Array< + Record + >; + extractedReferencesResult.forEach((res) => { + panelReferences.push(...res.references); + }); + + // TODO: This extraction should be done by EmbeddablePersistableStateService + // https://github.com/elastic/kibana/issues/82830 panels.forEach((panel, i) => { if (!panel.type) { throw new Error(`"type" attribute is missing from panel "${i}"`); @@ -46,6 +75,7 @@ export function extractReferences({ delete panel.type; delete panel.id; }); + return { references: [...references, ...panelReferences], attributes: { @@ -55,21 +85,28 @@ export function extractReferences({ }; } +export interface InjectDeps { + embeddablePersistableStateService: EmbeddablePersistableStateService; +} + export function injectReferences( - savedObject: SavedObjectDashboard, - references: SavedObjectReference[] -) { + { attributes, references = [] }: SavedObjectAttributesAndReferences, + deps: InjectDeps +): SavedObjectAttributes { // Skip if panelsJSON is missing otherwise this will cause saved object import to fail when // importing objects without panelsJSON. At development time of this, there is no guarantee each saved // object has panelsJSON in all previous versions of kibana. - if (typeof savedObject.panelsJSON !== 'string') { - return; + if (typeof attributes.panelsJSON !== 'string') { + return attributes; } - const panels = JSON.parse(savedObject.panelsJSON); + let panels = JSON.parse(attributes.panelsJSON); // Same here, prevent failing saved object import if ever panels aren't an array. if (!Array.isArray(panels)) { - return; + return attributes; } + + // TODO: This injection should be done by EmbeddablePersistableStateService + // https://github.com/elastic/kibana/issues/82830 panels.forEach((panel) => { if (!panel.panelRefName) { return; @@ -84,5 +121,11 @@ export function injectReferences( panel.type = reference.type; delete panel.panelRefName; }); - savedObject.panelsJSON = JSON.stringify(panels); + + panels = injectPanelsReferences(panels, references, deps); + + return { + ...attributes, + panelsJSON: JSON.stringify(panels), + }; } diff --git a/src/plugins/dashboard/common/types.ts b/src/plugins/dashboard/common/types.ts index 7cc82a91739767..ae214764052dca 100644 --- a/src/plugins/dashboard/common/types.ts +++ b/src/plugins/dashboard/common/types.ts @@ -17,6 +17,8 @@ * under the License. */ +import { EmbeddableInput, PanelState } from '../../../../src/plugins/embeddable/common/types'; +import { SavedObjectEmbeddableInput } from '../../../../src/plugins/embeddable/common/lib/saved_object_embeddable'; import { RawSavedDashboardPanelTo60, RawSavedDashboardPanel610, @@ -26,6 +28,21 @@ import { RawSavedDashboardPanel730ToLatest, } from './bwc/types'; +import { GridData } from './embeddable/types'; +export type PanelId = string; +export type SavedObjectId = string; + +export interface DashboardPanelState< + TEmbeddableInput extends EmbeddableInput | SavedObjectEmbeddableInput = SavedObjectEmbeddableInput +> extends PanelState { + readonly gridData: GridData; +} + +/** + * This should always represent the latest dashboard panel shape, after all possible migrations. + */ +export type SavedDashboardPanel = SavedDashboardPanel730ToLatest; + export type SavedDashboardPanel640To720 = Pick< RawSavedDashboardPanel640To720, Exclude diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index feae110c271fc6..c99e4e4e069878 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -81,7 +81,6 @@ import { getTopNavConfig } from './top_nav/get_top_nav_config'; import { TopNavIds } from './top_nav/top_nav_ids'; import { getDashboardTitle } from './dashboard_strings'; import { DashboardAppScope } from './dashboard_app'; -import { convertSavedDashboardPanelToPanelState } from './lib/embeddable_saved_object_converters'; import { RenderDeps } from './application'; import { IKbnUrlStateStorage, @@ -97,6 +96,7 @@ import { subscribeWithScope, } from '../../../kibana_legacy/public'; import { migrateLegacyQuery } from './lib/migrate_legacy_query'; +import { convertSavedDashboardPanelToPanelState } from '../../common/embeddable/embeddable_saved_object_converters'; export interface DashboardAppControllerDependencies extends RenderDeps { $scope: DashboardAppScope; diff --git a/src/plugins/dashboard/public/application/dashboard_state_manager.ts b/src/plugins/dashboard/public/application/dashboard_state_manager.ts index 38479b1384477c..6ef109ff60e421 100644 --- a/src/plugins/dashboard/public/application/dashboard_state_manager.ts +++ b/src/plugins/dashboard/public/application/dashboard_state_manager.ts @@ -30,7 +30,6 @@ import { migrateLegacyQuery } from './lib/migrate_legacy_query'; import { ViewMode } from '../embeddable_plugin'; import { getAppStateDefaults, migrateAppState, getDashboardIdFromUrl } from './lib'; -import { convertPanelStateToSavedDashboardPanel } from './lib/embeddable_saved_object_converters'; import { FilterUtils } from './lib/filter_utils'; import { DashboardAppState, @@ -48,6 +47,7 @@ import { } from '../../../kibana_utils/public'; import { SavedObjectDashboard } from '../saved_dashboards'; import { DashboardContainer } from './embeddable'; +import { convertPanelStateToSavedDashboardPanel } from '../../common/embeddable/embeddable_saved_object_converters'; /** * Dashboard state manager handles connecting angular and redux state between the angular and react portions of the diff --git a/src/plugins/dashboard/public/application/embeddable/types.ts b/src/plugins/dashboard/public/application/embeddable/types.ts index 66cdd22ed6bd43..efeb68c8a885ab 100644 --- a/src/plugins/dashboard/public/application/embeddable/types.ts +++ b/src/plugins/dashboard/public/application/embeddable/types.ts @@ -16,14 +16,4 @@ * specific language governing permissions and limitations * under the License. */ -import { SavedObjectEmbeddableInput } from 'src/plugins/embeddable/public'; -import { GridData } from '../../../common'; -import { PanelState, EmbeddableInput } from '../../embeddable_plugin'; -export type PanelId = string; -export type SavedObjectId = string; - -export interface DashboardPanelState< - TEmbeddableInput extends EmbeddableInput | SavedObjectEmbeddableInput = SavedObjectEmbeddableInput -> extends PanelState { - readonly gridData: GridData; -} +export * from '../../../common/types'; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 53b892475704f9..24bf736cfa2740 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -450,6 +450,7 @@ export class DashboardPlugin const savedDashboardLoader = createSavedDashboardLoader({ savedObjectsClient: core.savedObjects.client, savedObjects: plugins.savedObjects, + embeddableStart: plugins.embeddable, }); const dashboardContainerFactory = plugins.embeddable.getEmbeddableFactory( DASHBOARD_CONTAINER_TYPE diff --git a/src/plugins/dashboard/public/saved_dashboards/index.ts b/src/plugins/dashboard/public/saved_dashboards/index.ts index 9b7745bd884f71..9adaf0dc3ba15f 100644 --- a/src/plugins/dashboard/public/saved_dashboards/index.ts +++ b/src/plugins/dashboard/public/saved_dashboards/index.ts @@ -16,6 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -export * from './saved_dashboard_references'; +export * from '../../common/saved_dashboard_references'; export * from './saved_dashboard'; export * from './saved_dashboards'; diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts b/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts index bfc52ec33c35c7..e3bfe346fbc07b 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts +++ b/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts @@ -17,10 +17,12 @@ * under the License. */ import { SavedObject, SavedObjectsStart } from '../../../../plugins/saved_objects/public'; -import { extractReferences, injectReferences } from './saved_dashboard_references'; import { Filter, ISearchSource, Query, RefreshInterval } from '../../../../plugins/data/public'; import { createDashboardEditUrl } from '../dashboard_constants'; +import { EmbeddableStart } from '../../../embeddable/public'; +import { SavedObjectAttributes, SavedObjectReference } from '../../../../core/types'; +import { extractReferences, injectReferences } from '../../common/saved_dashboard_references'; export interface SavedObjectDashboard extends SavedObject { id?: string; @@ -41,7 +43,8 @@ export interface SavedObjectDashboard extends SavedObject { // Used only by the savedDashboards service, usually no reason to change this export function createSavedDashboardClass( - savedObjectStart: SavedObjectsStart + savedObjectStart: SavedObjectsStart, + embeddableStart: EmbeddableStart ): new (id: string) => SavedObjectDashboard { class SavedDashboard extends savedObjectStart.SavedObjectClass { // save these objects with the 'dashboard' type @@ -77,8 +80,19 @@ export function createSavedDashboardClass( type: SavedDashboard.type, mapping: SavedDashboard.mapping, searchSource: SavedDashboard.searchSource, - extractReferences, - injectReferences, + extractReferences: (opts: { + attributes: SavedObjectAttributes; + references: SavedObjectReference[]; + }) => extractReferences(opts, { embeddablePersistableStateService: embeddableStart }), + injectReferences: (so: SavedObjectDashboard, references: SavedObjectReference[]) => { + const newAttributes = injectReferences( + { attributes: so._serialize().attributes, references }, + { + embeddablePersistableStateService: embeddableStart, + } + ); + Object.assign(so, newAttributes); + }, // if this is null/undefined then the SavedObject will be assigned the defaults id, diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts b/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts index 750fec4d4d1f91..7193a77fd0ec9c 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts +++ b/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts @@ -20,16 +20,22 @@ import { SavedObjectsClientContract } from 'kibana/public'; import { SavedObjectLoader, SavedObjectsStart } from '../../../../plugins/saved_objects/public'; import { createSavedDashboardClass } from './saved_dashboard'; +import { EmbeddableStart } from '../../../embeddable/public'; interface Services { savedObjectsClient: SavedObjectsClientContract; savedObjects: SavedObjectsStart; + embeddableStart: EmbeddableStart; } /** * @param services */ -export function createSavedDashboardLoader({ savedObjects, savedObjectsClient }: Services) { - const SavedDashboard = createSavedDashboardClass(savedObjects); +export function createSavedDashboardLoader({ + savedObjects, + savedObjectsClient, + embeddableStart, +}: Services) { + const SavedDashboard = createSavedDashboardClass(savedObjects, embeddableStart); return new SavedObjectLoader(SavedDashboard, savedObjectsClient); } diff --git a/src/plugins/dashboard/public/types.ts b/src/plugins/dashboard/public/types.ts index 1af739c34b76a4..8f6fe7fce5cfe5 100644 --- a/src/plugins/dashboard/public/types.ts +++ b/src/plugins/dashboard/public/types.ts @@ -19,9 +19,12 @@ import { Query, Filter } from 'src/plugins/data/public'; import { SavedObject as SavedObjectType, SavedObjectAttributes } from 'src/core/public'; -import { SavedDashboardPanel730ToLatest } from '../common'; + import { ViewMode } from './embeddable_plugin'; +import { SavedDashboardPanel } from '../common/types'; +export { SavedDashboardPanel }; + export interface DashboardCapabilities { showWriteControls: boolean; createNew: boolean; @@ -71,11 +74,6 @@ export interface Field { export type NavAction = (anchorElement?: any) => void; -/** - * This should always represent the latest dashboard panel shape, after all possible migrations. - */ -export type SavedDashboardPanel = SavedDashboardPanel730ToLatest; - export interface DashboardAppState { panels: SavedDashboardPanel[]; fullScreenMode: boolean; diff --git a/src/plugins/dashboard/server/plugin.ts b/src/plugins/dashboard/server/plugin.ts index ba7bdeeda01335..6a4c297f258819 100644 --- a/src/plugins/dashboard/server/plugin.ts +++ b/src/plugins/dashboard/server/plugin.ts @@ -25,22 +25,34 @@ import { Logger, } from '../../../core/server'; -import { dashboardSavedObjectType } from './saved_objects'; +import { createDashboardSavedObjectType } from './saved_objects'; import { capabilitiesProvider } from './capabilities_provider'; import { DashboardPluginSetup, DashboardPluginStart } from './types'; +import { EmbeddableSetup } from '../../embeddable/server'; -export class DashboardPlugin implements Plugin { +interface SetupDeps { + embeddable: EmbeddableSetup; +} + +export class DashboardPlugin + implements Plugin { private readonly logger: Logger; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); } - public setup(core: CoreSetup) { + public setup(core: CoreSetup, plugins: SetupDeps) { this.logger.debug('dashboard: Setup'); - core.savedObjects.registerType(dashboardSavedObjectType); + core.savedObjects.registerType( + createDashboardSavedObjectType({ + migrationDeps: { + embeddable: plugins.embeddable, + }, + }) + ); core.capabilities.registerProvider(capabilitiesProvider); return {}; diff --git a/src/plugins/dashboard/server/saved_objects/dashboard.ts b/src/plugins/dashboard/server/saved_objects/dashboard.ts index a85f67f5ba56ad..7d3e48ce1ae8b0 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard.ts @@ -18,9 +18,16 @@ */ import { SavedObjectsType } from 'kibana/server'; -import { dashboardSavedObjectTypeMigrations } from './dashboard_migrations'; +import { + createDashboardSavedObjectTypeMigrations, + DashboardSavedObjectTypeMigrationsDeps, +} from './dashboard_migrations'; -export const dashboardSavedObjectType: SavedObjectsType = { +export const createDashboardSavedObjectType = ({ + migrationDeps, +}: { + migrationDeps: DashboardSavedObjectTypeMigrationsDeps; +}): SavedObjectsType => ({ name: 'dashboard', hidden: false, namespaceType: 'single', @@ -65,5 +72,5 @@ export const dashboardSavedObjectType: SavedObjectsType = { version: { type: 'integer' }, }, }, - migrations: dashboardSavedObjectTypeMigrations, -}; + migrations: createDashboardSavedObjectTypeMigrations(migrationDeps), +}); diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts index 22ed18f75c652a..50f12d21d4db93 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts @@ -19,7 +19,14 @@ import { SavedObjectUnsanitizedDoc } from 'kibana/server'; import { savedObjectsServiceMock } from '../../../../core/server/mocks'; -import { dashboardSavedObjectTypeMigrations as migrations } from './dashboard_migrations'; +import { createEmbeddableSetupMock } from '../../../embeddable/server/mocks'; +import { createDashboardSavedObjectTypeMigrations } from './dashboard_migrations'; +import { DashboardDoc730ToLatest } from '../../common'; + +const embeddableSetupMock = createEmbeddableSetupMock(); +const migrations = createDashboardSavedObjectTypeMigrations({ + embeddable: embeddableSetupMock, +}); const contextMock = savedObjectsServiceMock.createMigrationContext(); @@ -448,4 +455,50 @@ Object { `); }); }); + + describe('7.11.0 - embeddable persistable state extraction', () => { + const migration = migrations['7.11.0']; + const doc: DashboardDoc730ToLatest = { + attributes: { + description: '', + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"query":{"language":"kuery","query":""},"filter":[{"query":{"match_phrase":{"machine.os.keyword":"osx"}},"$state":{"store":"appState"},"meta":{"type":"phrase","key":"machine.os.keyword","params":{"query":"osx"},"disabled":false,"negate":false,"alias":null,"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index"}}]}', + }, + optionsJSON: '{"useMargins":true,"hidePanelTitles":false}', + panelsJSON: + '[{"version":"7.9.3","gridData":{"x":0,"y":0,"w":24,"h":15,"i":"82fa0882-9f9e-476a-bbb9-03555e5ced91"},"panelIndex":"82fa0882-9f9e-476a-bbb9-03555e5ced91","embeddableConfig":{"enhancements":{"dynamicActions":{"events":[]}}},"panelRefName":"panel_0"}]', + timeRestore: false, + title: 'Dashboard A', + version: 1, + }, + id: '376e6260-1f5e-11eb-91aa-7b6d5f8a61d6', + references: [ + { + id: '90943e30-9a47-11e8-b64d-95841ca0b247', + name: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', + type: 'index-pattern', + }, + { id: '14e2e710-4258-11e8-b3aa-73fdaf54bfc9', name: 'panel_0', type: 'visualization' }, + ], + type: 'dashboard', + }; + + test('should migrate 7.3.0 doc without embeddable state to extract', () => { + const newDoc = migration(doc, contextMock); + expect(newDoc).toEqual(doc); + }); + + test('should migrate 7.3.0 doc and extract embeddable state', () => { + embeddableSetupMock.extract.mockImplementationOnce((state) => ({ + state: { ...state, __extracted: true }, + references: [{ id: '__new', name: '__newRefName', type: '__newType' }], + })); + + const newDoc = migration(doc, contextMock); + expect(newDoc).not.toEqual(doc); + expect(newDoc.references).toHaveLength(doc.references.length + 1); + expect(JSON.parse(newDoc.attributes.panelsJSON)[0].embeddableConfig.__extracted).toBe(true); + }); + }); }); diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts index ac91c5a92048a6..177440c5ea5d11 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts @@ -18,11 +18,12 @@ */ import { get, flow } from 'lodash'; - -import { SavedObjectMigrationFn } from 'kibana/server'; +import { SavedObjectAttributes, SavedObjectMigrationFn } from 'kibana/server'; import { migrations730 } from './migrations_730'; import { migrateMatchAllQuery } from './migrate_match_all_query'; -import { DashboardDoc700To720 } from '../../common'; +import { DashboardDoc700To720, DashboardDoc730ToLatest } from '../../common'; +import { EmbeddableSetup } from '../../../embeddable/server'; +import { injectReferences, extractReferences } from '../../common/saved_dashboard_references'; function migrateIndexPattern(doc: DashboardDoc700To720) { const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); @@ -100,7 +101,57 @@ const migrations700: SavedObjectMigrationFn = (doc): DashboardDoc700To return doc as DashboardDoc700To720; }; -export const dashboardSavedObjectTypeMigrations = { +/** + * In 7.8.0 we introduced dashboard drilldowns which are stored inside dashboard saved object as part of embeddable state + * In 7.11.0 we created an embeddable references/migrations system that allows to properly extract embeddable persistable state + * https://github.com/elastic/kibana/issues/71409 + * The idea of this migration is to inject all the embeddable panel references and then run the extraction again. + * As the result of the extraction: + * 1. In addition to regular `panel_` we will get new references which are extracted by `embeddablePersistableStateService` (dashboard drilldown references) + * 2. `panel_` references will be regenerated + * All other references like index-patterns are forwarded non touched + * @param deps + */ +function createExtractPanelReferencesMigration( + deps: DashboardSavedObjectTypeMigrationsDeps +): SavedObjectMigrationFn { + return (doc) => { + const references = doc.references ?? []; + + /** + * Remembering this because dashboard's extractReferences won't return those + * All other references like `panel_` will be overwritten + */ + const oldNonPanelReferences = references.filter((ref) => !ref.name.startsWith('panel_')); + + const injectedAttributes = injectReferences( + { + attributes: (doc.attributes as unknown) as SavedObjectAttributes, + references, + }, + { embeddablePersistableStateService: deps.embeddable } + ); + + const { attributes, references: newPanelReferences } = extractReferences( + { attributes: injectedAttributes, references: [] }, + { embeddablePersistableStateService: deps.embeddable } + ); + + return { + ...doc, + references: [...oldNonPanelReferences, ...newPanelReferences], + attributes, + }; + }; +} + +export interface DashboardSavedObjectTypeMigrationsDeps { + embeddable: EmbeddableSetup; +} + +export const createDashboardSavedObjectTypeMigrations = ( + deps: DashboardSavedObjectTypeMigrationsDeps +) => ({ /** * We need to have this migration twice, once with a version prior to 7.0.0 once with a version * after it. The reason for that is, that this migration has been introduced once 7.0.0 was already @@ -115,4 +166,5 @@ export const dashboardSavedObjectTypeMigrations = { '7.0.0': flow(migrations700), '7.3.0': flow(migrations730), '7.9.3': flow(migrateMatchAllQuery), -}; + '7.11.0': flow(createExtractPanelReferencesMigration(deps)), +}); diff --git a/src/plugins/dashboard/server/saved_objects/index.ts b/src/plugins/dashboard/server/saved_objects/index.ts index ca97b9d2a6b70f..ea4808de968480 100644 --- a/src/plugins/dashboard/server/saved_objects/index.ts +++ b/src/plugins/dashboard/server/saved_objects/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { dashboardSavedObjectType } from './dashboard'; +export { createDashboardSavedObjectType } from './dashboard'; diff --git a/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts b/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts index a58df547fa5224..37a8881ab520b7 100644 --- a/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts +++ b/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts @@ -18,12 +18,16 @@ */ import { savedObjectsServiceMock } from '../../../../core/server/mocks'; -import { dashboardSavedObjectTypeMigrations as migrations } from './dashboard_migrations'; +import { createDashboardSavedObjectTypeMigrations } from './dashboard_migrations'; import { migrations730 } from './migrations_730'; import { DashboardDoc700To720, DashboardDoc730ToLatest, DashboardDocPre700 } from '../../common'; import { RawSavedDashboardPanel730ToLatest } from '../../common'; +import { createEmbeddableSetupMock } from '../../../embeddable/server/mocks'; const mockContext = savedObjectsServiceMock.createMigrationContext(); +const migrations = createDashboardSavedObjectTypeMigrations({ + embeddable: createEmbeddableSetupMock(), +}); test('dashboard migration 7.3.0 migrates filters to query on search source', () => { const doc: DashboardDoc700To720 = { diff --git a/src/plugins/embeddable/common/index.ts b/src/plugins/embeddable/common/index.ts new file mode 100644 index 00000000000000..a4cbfb11b36f85 --- /dev/null +++ b/src/plugins/embeddable/common/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './types'; +export * from './lib'; diff --git a/src/plugins/embeddable/common/lib/index.ts b/src/plugins/embeddable/common/lib/index.ts index e180ca9489df05..1ac6834365cd13 100644 --- a/src/plugins/embeddable/common/lib/index.ts +++ b/src/plugins/embeddable/common/lib/index.ts @@ -22,3 +22,4 @@ export * from './inject'; export * from './migrate'; export * from './migrate_base_input'; export * from './telemetry'; +export * from './saved_object_embeddable'; diff --git a/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts b/src/plugins/embeddable/common/lib/saved_object_embeddable.ts similarity index 96% rename from src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts rename to src/plugins/embeddable/common/lib/saved_object_embeddable.ts index 5f093c55e94e4f..f2dc9ed1ae3956 100644 --- a/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts +++ b/src/plugins/embeddable/common/lib/saved_object_embeddable.ts @@ -17,7 +17,7 @@ * under the License. */ -import { EmbeddableInput } from '..'; +import { EmbeddableInput } from '../types'; export interface SavedObjectEmbeddableInput extends EmbeddableInput { savedObjectId: string; diff --git a/src/plugins/embeddable/common/mocks.ts b/src/plugins/embeddable/common/mocks.ts new file mode 100644 index 00000000000000..a9ac144d1f2762 --- /dev/null +++ b/src/plugins/embeddable/common/mocks.ts @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EmbeddablePersistableStateService } from './types'; + +export const createEmbeddablePersistableStateServiceMock = (): jest.Mocked< + EmbeddablePersistableStateService +> => { + return { + inject: jest.fn((state, references) => state), + extract: jest.fn((state) => ({ state, references: [] })), + migrate: jest.fn((state, version) => state), + telemetry: jest.fn((state, collector) => ({})), + }; +}; diff --git a/src/plugins/embeddable/common/types.ts b/src/plugins/embeddable/common/types.ts index 7e024eda9b7937..8965446cc85fa7 100644 --- a/src/plugins/embeddable/common/types.ts +++ b/src/plugins/embeddable/common/types.ts @@ -17,7 +17,7 @@ * under the License. */ -import { SerializableState } from '../../kibana_utils/common'; +import { PersistableStateService, SerializableState } from '../../kibana_utils/common'; import { Query, TimeRange } from '../../data/common/query'; import { Filter } from '../../data/common/es_query/filters'; @@ -74,8 +74,21 @@ export type EmbeddableInput = { searchSessionId?: string; }; +export interface PanelState { + // The type of embeddable in this panel. Will be used to find the factory in which to + // load the embeddable. + type: string; + + // Stores input for this embeddable that is specific to this embeddable. Other parts of embeddable input + // will be derived from the container's input. **Any state in here will override any state derived from + // the container.** + explicitInput: Partial & { id: string }; +} + export type EmbeddableStateWithType = EmbeddableInput & { type: string }; +export type EmbeddablePersistableStateService = PersistableStateService; + export interface CommonEmbeddableStartContract { getEmbeddableFactory: (embeddableFactoryId: string) => any; getEnhancement: (enhancementId: string) => any; diff --git a/src/plugins/embeddable/public/lib/containers/container.ts b/src/plugins/embeddable/public/lib/containers/container.ts index 4dede8bf5d752f..a5c5133dbc7028 100644 --- a/src/plugins/embeddable/public/lib/containers/container.ts +++ b/src/plugins/embeddable/public/lib/containers/container.ts @@ -31,7 +31,7 @@ import { import { IContainer, ContainerInput, ContainerOutput, PanelState } from './i_container'; import { PanelNotFoundError, EmbeddableFactoryNotFoundError } from '../errors'; import { EmbeddableStart } from '../../plugin'; -import { isSavedObjectEmbeddableInput } from '../embeddables/saved_object_embeddable'; +import { isSavedObjectEmbeddableInput } from '../../../common/lib/saved_object_embeddable'; const getKeys = (o: T): Array => Object.keys(o) as Array; diff --git a/src/plugins/embeddable/public/lib/containers/i_container.ts b/src/plugins/embeddable/public/lib/containers/i_container.ts index db219fa8b73142..270caec2f3f843 100644 --- a/src/plugins/embeddable/public/lib/containers/i_container.ts +++ b/src/plugins/embeddable/public/lib/containers/i_container.ts @@ -24,17 +24,9 @@ import { ErrorEmbeddable, IEmbeddable, } from '../embeddables'; +import { PanelState } from '../../../common/types'; -export interface PanelState { - // The type of embeddable in this panel. Will be used to find the factory in which to - // load the embeddable. - type: string; - - // Stores input for this embeddable that is specific to this embeddable. Other parts of embeddable input - // will be derived from the container's input. **Any state in here will override any state derived from - // the container.** - explicitInput: Partial & { id: string }; -} +export { PanelState }; export interface ContainerOutput extends EmbeddableOutput { embeddableLoaded: { [key: string]: boolean }; diff --git a/src/plugins/embeddable/public/lib/embeddables/index.ts b/src/plugins/embeddable/public/lib/embeddables/index.ts index 5bab5ac27f3cc9..2f6de1be60c9c4 100644 --- a/src/plugins/embeddable/public/lib/embeddables/index.ts +++ b/src/plugins/embeddable/public/lib/embeddables/index.ts @@ -24,5 +24,5 @@ export * from './default_embeddable_factory_provider'; export { ErrorEmbeddable, isErrorEmbeddable } from './error_embeddable'; export { withEmbeddableSubscription } from './with_subscription'; export { EmbeddableRoot } from './embeddable_root'; -export * from './saved_object_embeddable'; +export * from '../../../common/lib/saved_object_embeddable'; export { EmbeddableRenderer, EmbeddableRendererProps } from './embeddable_renderer'; diff --git a/src/plugins/embeddable/server/mocks.ts b/src/plugins/embeddable/server/mocks.ts new file mode 100644 index 00000000000000..28bb9542ab7cb0 --- /dev/null +++ b/src/plugins/embeddable/server/mocks.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createEmbeddablePersistableStateServiceMock } from '../common/mocks'; +import { EmbeddableSetup, EmbeddableStart } from './plugin'; + +export const createEmbeddableSetupMock = (): jest.Mocked => ({ + ...createEmbeddablePersistableStateServiceMock(), + registerEmbeddableFactory: jest.fn(), + registerEnhancement: jest.fn(), +}); + +export const createEmbeddableStartMock = (): jest.Mocked => + createEmbeddablePersistableStateServiceMock(); diff --git a/src/plugins/embeddable/server/plugin.ts b/src/plugins/embeddable/server/plugin.ts index 6e9186e2864910..d99675f950ad0b 100644 --- a/src/plugins/embeddable/server/plugin.ts +++ b/src/plugins/embeddable/server/plugin.ts @@ -32,23 +32,32 @@ import { getMigrateFunction, getTelemetryFunction, } from '../common/lib'; -import { SerializableState } from '../../kibana_utils/common'; +import { PersistableStateService, SerializableState } from '../../kibana_utils/common'; import { EmbeddableStateWithType } from '../common/types'; -export interface EmbeddableSetup { - getAttributeService: any; +export interface EmbeddableSetup extends PersistableStateService { registerEmbeddableFactory: (factory: EmbeddableRegistryDefinition) => void; registerEnhancement: (enhancement: EnhancementRegistryDefinition) => void; } -export class EmbeddableServerPlugin implements Plugin { +export type EmbeddableStart = PersistableStateService; + +export class EmbeddableServerPlugin implements Plugin { private readonly embeddableFactories: EmbeddableFactoryRegistry = new Map(); private readonly enhancements: EnhancementsRegistry = new Map(); public setup(core: CoreSetup) { + const commonContract = { + getEmbeddableFactory: this.getEmbeddableFactory, + getEnhancement: this.getEnhancement, + }; return { registerEmbeddableFactory: this.registerEmbeddableFactory, registerEnhancement: this.registerEnhancement, + telemetry: getTelemetryFunction(commonContract), + extract: getExtractFunction(commonContract), + inject: getInjectFunction(commonContract), + migrate: getMigrateFunction(commonContract), }; } diff --git a/src/plugins/embeddable/server/server.api.md b/src/plugins/embeddable/server/server.api.md index 87f7d76cffaa85..d3921ab11457c8 100644 --- a/src/plugins/embeddable/server/server.api.md +++ b/src/plugins/embeddable/server/server.api.md @@ -18,12 +18,11 @@ export interface EmbeddableRegistryDefinition

{ // (undocumented) registerEmbeddableFactory: (factory: EmbeddableRegistryDefinition) => void; // (undocumented) diff --git a/x-pack/plugins/apm/common/runtime_types/to_number_rt/index.ts b/x-pack/plugins/apm/common/runtime_types/to_number_rt/index.ts new file mode 100644 index 00000000000000..0fe8181c114052 --- /dev/null +++ b/x-pack/plugins/apm/common/runtime_types/to_number_rt/index.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 * as t from 'io-ts'; + +export const toNumberRt = new t.Type( + 'ToNumber', + t.any.is, + (input, context) => { + const number = Number(input); + return !isNaN(number) ? t.success(number) : t.failure(input, context); + }, + t.identity +); diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/ServiceListMetric.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/ServiceListMetric.tsx index c94c94d4a0b72c..716fed7775f7b4 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/ServiceListMetric.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/ServiceListMetric.tsx @@ -3,15 +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 { EuiFlexItem } from '@elastic/eui'; -import { EuiFlexGroup } from '@elastic/eui'; - import React from 'react'; -import { useTheme } from '../../../../hooks/useTheme'; import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { getEmptySeries } from '../../../shared/charts/CustomPlot/getEmptySeries'; -import { SparkPlot } from '../../../shared/charts/SparkPlot'; +import { SparkPlotWithValueLabel } from '../../../shared/charts/spark_plot/spark_plot_with_value_label'; export function ServiceListMetric({ color, @@ -22,28 +16,17 @@ export function ServiceListMetric({ series?: Array<{ x: number; y: number | null }>; valueLabel: React.ReactNode; }) { - const theme = useTheme(); - const { urlParams: { start, end }, } = useUrlParams(); - const colorValue = theme.eui[color]; - return ( - - - - - - {valueLabel} - - + ); } diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index 016ee3daf6b510..ee77157fe4eb36 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -12,9 +12,10 @@ import { useTrackPageview } from '../../../../../observability/public'; import { isRumAgentName } from '../../../../common/agent_name'; import { ChartsSyncContextProvider } from '../../../context/charts_sync_context'; import { TransactionErrorRateChart } from '../../shared/charts/transaction_error_rate_chart'; -import { ErrorOverviewLink } from '../../shared/Links/apm/ErrorOverviewLink'; import { ServiceMapLink } from '../../shared/Links/apm/ServiceMapLink'; import { TransactionOverviewLink } from '../../shared/Links/apm/TransactionOverviewLink'; +import { ServiceOverviewErrorsTable } from './service_overview_errors_table'; +import { TableLinkFlexItem } from './table_link_flex_item'; const rowHeight = 310; const latencyChartRowHeight = 230; @@ -27,12 +28,6 @@ const LatencyChartRow = styled(EuiFlexItem)` height: ${latencyChartRowHeight}px; `; -const TableLinkFlexItem = styled(EuiFlexItem)` - & > a { - text-align: right; - } -`; - interface ServiceOverviewProps { agentName?: string; serviceName: string; @@ -130,30 +125,7 @@ export function ServiceOverview({ )} - - - -

- {i18n.translate( - 'xpack.apm.serviceOverview.errorsTableTitle', - { - defaultMessage: 'Errors', - } - )} -

- - - - - {i18n.translate( - 'xpack.apm.serviceOverview.errorsTableLinkText', - { - defaultMessage: 'View errors', - } - )} - - - + diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/fetch_wrapper.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/fetch_wrapper.tsx new file mode 100644 index 00000000000000..4c8d368811a0c1 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/fetch_wrapper.tsx @@ -0,0 +1,30 @@ +/* + * 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 { FETCH_STATUS } from '../../../../hooks/useFetcher'; +import { ErrorStatePrompt } from '../../../shared/ErrorStatePrompt'; +import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; + +export function FetchWrapper({ + hasData, + status, + children, +}: { + hasData: boolean; + status: FETCH_STATUS; + children: React.ReactNode; +}) { + if (status === FETCH_STATUS.FAILURE) { + return ; + } + + if (!hasData && status !== FETCH_STATUS.SUCCESS) { + return ; + } + + return <>{children}; +} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx new file mode 100644 index 00000000000000..a5a002cf3aca49 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx @@ -0,0 +1,266 @@ +/* + * 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, { useState } from 'react'; +import { EuiTitle } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EuiBasicTable } from '@elastic/eui'; +import { EuiBasicTableColumn } from '@elastic/eui'; +import styled from 'styled-components'; +import { EuiToolTip } from '@elastic/eui'; +import { asInteger } from '../../../../../common/utils/formatters'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/useFetcher'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { ErrorOverviewLink } from '../../../shared/Links/apm/ErrorOverviewLink'; +import { TableLinkFlexItem } from '../table_link_flex_item'; +import { SparkPlotWithValueLabel } from '../../../shared/charts/spark_plot/spark_plot_with_value_label'; +import { callApmApi } from '../../../../services/rest/createCallApmApi'; +import { TimestampTooltip } from '../../../shared/TimestampTooltip'; +import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; +import { px, truncate, unit } from '../../../../style/variables'; +import { FetchWrapper } from './fetch_wrapper'; + +interface Props { + serviceName: string; +} + +interface ErrorGroupItem { + name: string; + last_seen: number; + group_id: string; + occurrences: { + value: number; + timeseries: Array<{ x: number; y: number }> | null; + }; +} + +type SortDirection = 'asc' | 'desc'; +type SortField = 'name' | 'last_seen' | 'occurrences'; + +const PAGE_SIZE = 5; +const DEFAULT_SORT = { + direction: 'desc' as const, + field: 'occurrences' as const, +}; + +const ErrorDetailLinkWrapper = styled.div` + width: 100%; + .euiToolTipAnchor { + width: 100% !important; + } +`; + +const StyledErrorDetailLink = styled(ErrorDetailLink)` + display: block; + ${truncate('100%')} +`; + +export function ServiceOverviewErrorsTable({ serviceName }: Props) { + const { + urlParams: { start, end }, + uiFilters, + } = useUrlParams(); + + const [tableOptions, setTableOptions] = useState<{ + pageIndex: number; + sort: { + direction: SortDirection; + field: SortField; + }; + }>({ + pageIndex: 0, + sort: DEFAULT_SORT, + }); + + const columns: Array> = [ + { + field: 'name', + name: i18n.translate('xpack.apm.serviceOverview.errorsTableColumnName', { + defaultMessage: 'Name', + }), + render: (_, { name, group_id: errorGroupId }) => { + return ( + + + + {name} + + + + ); + }, + }, + { + field: 'last_seen', + name: i18n.translate( + 'xpack.apm.serviceOverview.errorsTableColumnLastSeen', + { + defaultMessage: 'Last seen', + } + ), + render: (_, { last_seen: lastSeen }) => { + return ; + }, + width: px(unit * 8), + }, + { + field: 'occurrences', + name: i18n.translate( + 'xpack.apm.serviceOverview.errorsTableColumnOccurrences', + { + defaultMessage: 'Occurrences', + } + ), + width: px(unit * 12), + render: (_, { occurrences }) => { + return ( + + ); + }, + }, + ]; + + const { + data = { + totalItemCount: 0, + items: [], + tableOptions: { + pageIndex: 0, + sort: DEFAULT_SORT, + }, + }, + status, + } = useFetcher(() => { + if (!start || !end) { + return; + } + + return callApmApi({ + pathname: '/api/apm/services/{serviceName}/error_groups', + params: { + path: { serviceName }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + size: PAGE_SIZE, + numBuckets: 20, + pageIndex: tableOptions.pageIndex, + sortField: tableOptions.sort.field, + sortDirection: tableOptions.sort.direction, + }, + }, + }).then((response) => { + return { + items: response.error_groups, + totalItemCount: response.total_error_groups, + tableOptions: { + pageIndex: tableOptions.pageIndex, + sort: { + field: tableOptions.sort.field, + direction: tableOptions.sort.direction, + }, + }, + }; + }); + }, [ + start, + end, + serviceName, + uiFilters, + tableOptions.pageIndex, + tableOptions.sort.field, + tableOptions.sort.direction, + ]); + + const { + items, + totalItemCount, + tableOptions: { pageIndex, sort }, + } = data; + + return ( + + + + + +

+ {i18n.translate('xpack.apm.serviceOverview.errorsTableTitle', { + defaultMessage: 'Errors', + })} +

+
+
+ + + {i18n.translate('xpack.apm.serviceOverview.errorsTableLinkText', { + defaultMessage: 'View errors', + })} + + +
+
+ + + { + setTableOptions({ + pageIndex: newTableOptions.page?.index ?? 0, + sort: newTableOptions.sort + ? { + field: newTableOptions.sort.field as SortField, + direction: newTableOptions.sort.direction, + } + : DEFAULT_SORT, + }); + }} + sorting={{ + enableAllColumns: true, + sort: { + direction: sort.direction, + field: sort.field, + }, + }} + /> + + +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/table_link_flex_item.tsx b/x-pack/plugins/apm/public/components/app/service_overview/table_link_flex_item.tsx new file mode 100644 index 00000000000000..35df003af380d4 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/table_link_flex_item.tsx @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; + +export const TableLinkFlexItem = styled(EuiFlexItem)` + & > a { + text-align: right; + } +`; diff --git a/x-pack/plugins/apm/public/components/shared/charts/SparkPlot/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/charts/SparkPlot/index.tsx rename to x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx diff --git a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx new file mode 100644 index 00000000000000..e2bb42fddb33bb --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; + +import React from 'react'; +import { useTheme } from '../../../../../hooks/useTheme'; +import { getEmptySeries } from '../../CustomPlot/getEmptySeries'; +import { SparkPlot } from '../'; + +type Color = + | 'euiColorVis0' + | 'euiColorVis1' + | 'euiColorVis2' + | 'euiColorVis3' + | 'euiColorVis4' + | 'euiColorVis5' + | 'euiColorVis6' + | 'euiColorVis7' + | 'euiColorVis8' + | 'euiColorVis9'; + +export function SparkPlotWithValueLabel({ + start, + end, + color, + series, + valueLabel, +}: { + start: number; + end: number; + color: Color; + series?: Array<{ x: number; y: number | null }>; + valueLabel: React.ReactNode; +}) { + const theme = useTheme(); + + const colorValue = theme.eui[color]; + + return ( + + + + + + {valueLabel} + + + ); +} diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts index d734a1395fc5e4..97c03924538c80 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts @@ -16,6 +16,7 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { getErrorGroupsProjection } from '../../projections/errors'; import { mergeProjection } from '../../projections/util/merge_projection'; +import { getErrorName } from '../helpers/get_error_name'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; export type ErrorGroupListAPIResponse = PromiseReturnType< @@ -93,8 +94,7 @@ export async function getErrorGroups({ // this is an exception rather than the rule so the ES type does not account for this. const hits = (resp.aggregations?.error_groups.buckets || []).map((bucket) => { const source = bucket.sample.hits.hits[0]._source; - const message = - source.error.log?.message || source.error.exception?.[0]?.message; + const message = getErrorName(source); return { message, diff --git a/x-pack/plugins/apm/server/lib/helpers/get_error_name.ts b/x-pack/plugins/apm/server/lib/helpers/get_error_name.ts new file mode 100644 index 00000000000000..dbc69592a4f8e8 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/get_error_name.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { APMError } from '../../../typings/es_schemas/ui/apm_error'; + +export function getErrorName({ error }: APMError) { + return error.log?.message || error.exception?.[0]?.message; +} diff --git a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts new file mode 100644 index 00000000000000..99d978116180b7 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts @@ -0,0 +1,177 @@ +/* + * 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 { ValuesType } from 'utility-types'; +import { orderBy } from 'lodash'; +import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; +import { PromiseReturnType } from '../../../../../observability/typings/common'; +import { rangeFilter } from '../../../../common/utils/range_filter'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { + ERROR_EXC_MESSAGE, + ERROR_GROUP_ID, + ERROR_LOG_MESSAGE, + SERVICE_NAME, +} from '../../../../common/elasticsearch_fieldnames'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { getErrorName } from '../../helpers/get_error_name'; + +export type ServiceErrorGroupItem = ValuesType< + PromiseReturnType +>; + +export async function getServiceErrorGroups({ + serviceName, + setup, + size, + numBuckets, + pageIndex, + sortDirection, + sortField, +}: { + serviceName: string; + setup: Setup & SetupTimeRange; + size: number; + pageIndex: number; + numBuckets: number; + sortDirection: 'asc' | 'desc'; + sortField: 'name' | 'last_seen' | 'occurrences'; +}) { + const { apmEventClient, start, end, esFilter } = setup; + + const { intervalString } = getBucketSize(start, end, numBuckets); + + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.error], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { range: rangeFilter(start, end) }, + ...esFilter, + ], + }, + }, + aggs: { + error_groups: { + terms: { + field: ERROR_GROUP_ID, + size: 500, + order: { + _count: 'desc', + }, + }, + aggs: { + sample: { + top_hits: { + size: 1, + _source: [ERROR_LOG_MESSAGE, ERROR_EXC_MESSAGE, '@timestamp'], + sort: { + '@timestamp': 'desc', + }, + }, + }, + }, + }, + }, + }, + }); + + const errorGroups = + response.aggregations?.error_groups.buckets.map((bucket) => ({ + group_id: bucket.key as string, + name: + getErrorName(bucket.sample.hits.hits[0]._source) ?? NOT_AVAILABLE_LABEL, + last_seen: new Date( + bucket.sample.hits.hits[0]?._source['@timestamp'] + ).getTime(), + occurrences: { + value: bucket.doc_count, + }, + })) ?? []; + + // Sort error groups first, and only get timeseries for data in view. + // This is to limit the possibility of creating too many buckets. + + const sortedAndSlicedErrorGroups = orderBy( + errorGroups, + (group) => { + if (sortField === 'occurrences') { + return group.occurrences.value; + } + return group[sortField]; + }, + [sortDirection] + ).slice(pageIndex * size, pageIndex * size + size); + + const sortedErrorGroupIds = sortedAndSlicedErrorGroups.map( + (group) => group.group_id + ); + + const timeseriesResponse = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.error], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { terms: { [ERROR_GROUP_ID]: sortedErrorGroupIds } }, + { term: { [SERVICE_NAME]: serviceName } }, + { range: rangeFilter(start, end) }, + ...esFilter, + ], + }, + }, + aggs: { + error_groups: { + terms: { + field: ERROR_GROUP_ID, + size, + }, + aggs: { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { + min: start, + max: end, + }, + }, + }, + }, + }, + }, + }, + }); + + return { + total_error_groups: errorGroups.length, + is_aggregation_accurate: + (response.aggregations?.error_groups.sum_other_doc_count ?? 0) === 0, + error_groups: sortedAndSlicedErrorGroups.map((errorGroup) => ({ + ...errorGroup, + occurrences: { + ...errorGroup.occurrences, + timeseries: + timeseriesResponse.aggregations?.error_groups.buckets + .find((bucket) => bucket.key === errorGroup.group_id) + ?.timeseries.buckets.map((dateBucket) => ({ + x: dateBucket.key, + y: dateBucket.doc_count, + })) ?? null, + }, + })), + }; +} diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index 2fbe404a70d829..34551c35ee2342 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -21,6 +21,7 @@ import { serviceNodeMetadataRoute, serviceAnnotationsRoute, serviceAnnotationsCreateRoute, + serviceErrorGroupsRoute, } from './services'; import { agentConfigurationRoute, @@ -115,6 +116,7 @@ const createApmApi = () => { .add(serviceNodeMetadataRoute) .add(serviceAnnotationsRoute) .add(serviceAnnotationsCreateRoute) + .add(serviceErrorGroupsRoute) // Agent configuration .add(getSingleAgentConfigurationRoute) diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 590b6c49d71bf8..ada1674d4555db 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -17,6 +17,8 @@ import { uiFiltersRt, rangeRt } from './default_api_types'; import { getServiceAnnotations } from '../lib/services/annotations'; import { dateAsStringRt } from '../../common/runtime_types/date_as_string_rt'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; +import { getServiceErrorGroups } from '../lib/services/get_service_error_groups'; +import { toNumberRt } from '../../common/runtime_types/to_number_rt'; export const servicesRoute = createRoute(() => ({ path: '/api/apm/services', @@ -195,3 +197,45 @@ export const serviceAnnotationsCreateRoute = createRoute(() => ({ }); }, })); + +export const serviceErrorGroupsRoute = createRoute(() => ({ + path: '/api/apm/services/{serviceName}/error_groups', + params: { + path: t.type({ + serviceName: t.string, + }), + query: t.intersection([ + rangeRt, + uiFiltersRt, + t.type({ + size: toNumberRt, + numBuckets: toNumberRt, + pageIndex: toNumberRt, + sortDirection: t.union([t.literal('asc'), t.literal('desc')]), + sortField: t.union([ + t.literal('last_seen'), + t.literal('occurrences'), + t.literal('name'), + ]), + }), + ]), + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + + const { + path: { serviceName }, + query: { size, numBuckets, pageIndex, sortDirection, sortField }, + } = context.params; + + return getServiceErrorGroups({ + serviceName, + setup, + size, + numBuckets, + pageIndex, + sortDirection, + sortField, + }); + }, +})); diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/constants.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/constants.ts new file mode 100644 index 00000000000000..922ec36619a4b9 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/constants.ts @@ -0,0 +1,14 @@ +/* + * 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. + */ + +/** + * NOTE: DO NOT CHANGE THIS STRING WITHOUT CAREFUL CONSIDERATOIN, BECAUSE IT IS + * STORED IN SAVED OBJECTS. + * + * Also temporary dashboard drilldown migration code inside embeddable plugin relies on it + * x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts + */ +export const EMBEDDABLE_TO_DASHBOARD_DRILLDOWN = 'DASHBOARD_TO_DASHBOARD_DRILLDOWN'; diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.test.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.test.ts new file mode 100644 index 00000000000000..dd890b2463226b --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.test.ts @@ -0,0 +1,48 @@ +/* + * 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 { createExtract, createInject } from './dashboard_drilldown_persistable_state'; +import { SerializedEvent } from '../../../../ui_actions_enhanced/common'; + +const drilldownId = 'test_id'; +const extract = createExtract({ drilldownId }); +const inject = createInject({ drilldownId }); + +const state: SerializedEvent = { + eventId: 'event_id', + triggers: [], + action: { + factoryId: drilldownId, + name: 'name', + config: { + dashboardId: 'dashboardId_1', + }, + }, +}; + +test('should extract and injected dashboard reference', () => { + const { state: extractedState, references } = extract(state); + expect(extractedState).not.toEqual(state); + expect(extractedState.action.config.dashboardId).toBeUndefined(); + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "dashboardId_1", + "name": "drilldown:test_id:event_id:dashboardId", + "type": "dashboard", + }, + ] + `); + + let injectedState = inject(extractedState, references); + expect(injectedState).toEqual(state); + + references[0].id = 'dashboardId_2'; + + injectedState = inject(extractedState, references); + expect(injectedState).not.toEqual(extractedState); + expect(injectedState.action.config.dashboardId).toBe('dashboardId_2'); +}); diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.ts new file mode 100644 index 00000000000000..bd972723c649b2 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.ts @@ -0,0 +1,75 @@ +/* + * 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 { SavedObjectReference } from '../../../../../../src/core/types'; +import { PersistableStateService } from '../../../../../../src/plugins/kibana_utils/common'; +import { SerializedAction, SerializedEvent } from '../../../../ui_actions_enhanced/common'; +import { DrilldownConfig } from './types'; + +type DashboardDrilldownPersistableState = PersistableStateService; + +const generateRefName = (state: SerializedEvent, id: string) => + `drilldown:${id}:${state.eventId}:dashboardId`; + +const injectDashboardId = (state: SerializedEvent, dashboardId: string): SerializedEvent => { + return { + ...state, + action: { + ...state.action, + config: { + ...state.action.config, + dashboardId, + }, + }, + }; +}; + +export const createInject = ({ + drilldownId, +}: { + drilldownId: string; +}): DashboardDrilldownPersistableState['inject'] => { + return (state: SerializedEvent, references: SavedObjectReference[]) => { + const action = state.action as SerializedAction; + const refName = generateRefName(state, drilldownId); + const ref = references.find((r) => r.name === refName); + if (!ref) return state; + if (ref.id && ref.id === action.config.dashboardId) return state; + return injectDashboardId(state, ref.id); + }; +}; + +export const createExtract = ({ + drilldownId, +}: { + drilldownId: string; +}): DashboardDrilldownPersistableState['extract'] => { + return (state: SerializedEvent) => { + const action = state.action as SerializedAction; + const references: SavedObjectReference[] = action.config.dashboardId + ? [ + { + name: generateRefName(state, drilldownId), + type: 'dashboard', + id: action.config.dashboardId, + }, + ] + : []; + + const { dashboardId, ...restOfConfig } = action.config; + + return { + state: { + ...state, + action: ({ + ...state.action, + config: restOfConfig, + } as unknown) as SerializedAction, + }, + references, + }; + }; +}; diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/index.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/index.ts new file mode 100644 index 00000000000000..f6a757ad7a1801 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { createExtract, createInject } from './dashboard_drilldown_persistable_state'; +export { EMBEDDABLE_TO_DASHBOARD_DRILLDOWN } from './constants'; +export { DrilldownConfig } from './types'; diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/types.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/types.ts new file mode 100644 index 00000000000000..3be2a9739837ed --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/types.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. + */ + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type DrilldownConfig = { + dashboardId?: string; + useCurrentFilters: boolean; + useCurrentDateRange: boolean; +}; diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/index.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/index.ts new file mode 100644 index 00000000000000..76c9abbd4bfbe3 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './dashboard_drilldown'; diff --git a/x-pack/plugins/dashboard_enhanced/common/index.ts b/x-pack/plugins/dashboard_enhanced/common/index.ts new file mode 100644 index 00000000000000..8cc3e129065311 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './drilldowns'; diff --git a/x-pack/plugins/dashboard_enhanced/kibana.json b/x-pack/plugins/dashboard_enhanced/kibana.json index f79a69c9f4aba5..b24c0b6983f407 100644 --- a/x-pack/plugins/dashboard_enhanced/kibana.json +++ b/x-pack/plugins/dashboard_enhanced/kibana.json @@ -1,7 +1,7 @@ { "id": "dashboardEnhanced", "version": "kibana", - "server": false, + "server": true, "ui": true, "requiredPlugins": ["data", "uiActionsEnhanced", "embeddable", "dashboard", "share"], "configPath": ["xpack", "dashboardEnhanced"], diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx index b098d66619814e..451254efd96485 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx @@ -9,19 +9,19 @@ import { DataPublicPluginStart } from 'src/plugins/data/public'; import { DashboardStart } from 'src/plugins/dashboard/public'; import { reactToUiComponent } from '../../../../../../../src/plugins/kibana_react/public'; import { - TriggerId, TriggerContextMapping, + TriggerId, } from '../../../../../../../src/plugins/ui_actions/public'; import { CollectConfigContainer } from './components'; import { - UiActionsEnhancedDrilldownDefinition as Drilldown, - UiActionsEnhancedBaseActionFactoryContext as BaseActionFactoryContext, AdvancedUiActionsStart, + UiActionsEnhancedBaseActionFactoryContext as BaseActionFactoryContext, + UiActionsEnhancedDrilldownDefinition as Drilldown, } from '../../../../../ui_actions_enhanced/public'; import { txtGoToDashboard } from './i18n'; import { - StartServicesGetter, CollectConfigProps, + StartServicesGetter, } from '../../../../../../../src/plugins/kibana_utils/public'; import { KibanaURL } from '../../../../../../../src/plugins/share/public'; import { Config } from './types'; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/types.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/types.ts index 330a501a78d391..7f5137812ee324 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/types.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/types.ts @@ -6,12 +6,8 @@ import { UiActionsEnhancedBaseActionFactoryContext } from '../../../../../ui_actions_enhanced/public'; import { APPLY_FILTER_TRIGGER } from '../../../../../../../src/plugins/ui_actions/public'; +import { DrilldownConfig } from '../../../../common'; -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -export type Config = { - dashboardId?: string; - useCurrentFilters: boolean; - useCurrentDateRange: boolean; -}; +export type Config = DrilldownConfig; export type FactoryContext = UiActionsEnhancedBaseActionFactoryContext; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx index f6de2ba931c58d..5bfb175ea0d00c 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx @@ -65,6 +65,12 @@ test('getHref is defined', () => { expect(drilldown.getHref).toBeDefined(); }); +test('inject/extract are defined', () => { + const drilldown = new EmbeddableToDashboardDrilldown({} as any); + expect(drilldown.extract).toBeDefined(); + expect(drilldown.inject).toBeDefined(); +}); + describe('.execute() & getHref', () => { /** * A convenience test setup helper diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx index 25bc93ad38b368..921c2aed00624a 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx @@ -22,6 +22,7 @@ import { } from '../abstract_dashboard_drilldown'; import { KibanaURL } from '../../../../../../../src/plugins/share/public'; import { EMBEDDABLE_TO_DASHBOARD_DRILLDOWN } from './constants'; +import { createExtract, createInject } from '../../../../common'; type Trigger = typeof APPLY_FILTER_TRIGGER; type Context = TriggerContextMapping[Trigger]; @@ -80,4 +81,8 @@ export class EmbeddableToDashboardDrilldown extends AbstractDashboardDrilldown { + constructor(protected readonly context: PluginInitializerContext) {} + + public setup(core: CoreSetup, plugins: SetupDependencies): SetupContract { + plugins.uiActionsEnhanced.registerActionFactory({ + id: EMBEDDABLE_TO_DASHBOARD_DRILLDOWN, + inject: createInject({ drilldownId: EMBEDDABLE_TO_DASHBOARD_DRILLDOWN }), + extract: createExtract({ drilldownId: EMBEDDABLE_TO_DASHBOARD_DRILLDOWN }), + }); + + return {}; + } + + public start(core: CoreStart, plugins: StartDependencies): StartContract { + return {}; + } + + public stop() {} +} diff --git a/x-pack/plugins/enterprise_search/common/version.ts b/x-pack/plugins/enterprise_search/common/version.ts index e29ad8a9f866b9..c23b05f7cdb3d8 100644 --- a/x-pack/plugins/enterprise_search/common/version.ts +++ b/x-pack/plugins/enterprise_search/common/version.ts @@ -8,4 +8,4 @@ import { SemVer } from 'semver'; import pkg from '../../../../package.json'; export const CURRENT_VERSION = new SemVer(pkg.version as string); -export const CURRENT_MAJOR_VERSION = CURRENT_VERSION.major; +export const CURRENT_MAJOR_VERSION = `${CURRENT_VERSION.major}.${CURRENT_VERSION.minor}`; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts index ea4906ec08946b..2b96e3322cd555 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import { DOCS_PREFIX } from '../../routes'; export const CREDENTIALS_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.credentials.title', @@ -100,4 +101,4 @@ export const TOKEN_TYPE_INFO = [ export const FLYOUT_ARIA_LABEL_ID = 'credentialsFlyoutTitle'; -export const DOCS_HREF = 'https://www.elastic.co/guide/en/app-search/current/authentication.html'; +export const DOCS_HREF = `${DOCS_PREFIX}/authentication.html`; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx index 23572074b3c692..3297f0df4d7bd2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx @@ -10,6 +10,8 @@ import { i18n } from '@kbn/i18n'; import { EuiLink, EuiSpacer, EuiSwitch, EuiText, EuiTextColor, EuiTitle } from '@elastic/eui'; import { useActions, useValues } from 'kea'; +import { DOCS_PREFIX } from '../../../routes'; + import { LogRetentionLogic } from './log_retention_logic'; import { AnalyticsLogRetentionMessage, ApiLogRetentionMessage } from './messaging'; import { LogRetentionOptions } from './types'; @@ -41,10 +43,7 @@ export const LogRetentionPanel: React.FC = () => { {i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.description', { defaultMessage: 'Manage the default write settings for API Logs and Analytics.', })}{' '} - + {i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.learnMore', { defaultMessage: 'Learn more about retention settings.', })} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx index 60d7f6951a478c..b3faa73dfaed6a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx @@ -13,14 +13,15 @@ import { APP_SEARCH_PLUGIN } from '../../../../../common/constants'; import { SetupGuide as SetupGuideLayout } from '../../../shared/setup_guide'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; +import { DOCS_PREFIX } from '../../routes'; import GettingStarted from './assets/getting_started.png'; export const SetupGuide: React.FC = () => ( = {}; const MANIFEST_NAME = 'manifest.yml'; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts index c5c9e8ac2c01ba..b6988f64843d09 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts @@ -11,8 +11,7 @@ import { ElasticsearchAssetType, InstallablePackage, } from '../../../../types'; -import { ArchiveEntry } from '../../registry'; -import { getAsset, getPathParts } from '../../archive'; +import { ArchiveEntry, getAsset, getPathParts } from '../../archive'; import { CallESAsCurrentUser } from '../../../../types'; import { saveInstalledEsRefs } from '../../packages/install'; import { getInstallationObject } from '../../packages'; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/assets.ts b/x-pack/plugins/fleet/server/services/epm/packages/assets.ts index 50d8f2f4d2fb22..80e1cbba6484b6 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/assets.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/assets.ts @@ -6,7 +6,7 @@ import { InstallablePackage } from '../../../types'; import * as Registry from '../registry'; -import { getArchiveFilelist, getAsset } from '../archive'; +import { ArchiveEntry, getArchiveFilelist, getAsset } from '../archive'; // paths from RegistryPackage are routes to the assets on EPR // e.g. `/package/nginx/1.2.0/data_stream/access/fields/fields.yml` @@ -51,14 +51,14 @@ export async function getAssetsData( packageInfo: InstallablePackage, filter = (path: string): boolean => true, datasetName?: string -): Promise { +): Promise { // TODO: Needs to be called to fill the cache but should not be required await Registry.ensureCachedArchiveInfo(packageInfo.name, packageInfo.version, 'registry'); // Gather all asset data const assets = getAssets(packageInfo, filter, datasetName); - const entries: Registry.ArchiveEntry[] = assets.map((path) => { + const entries: ArchiveEntry[] = assets.map((path) => { const buffer = getAsset(path); return { path, buffer }; diff --git a/x-pack/plugins/fleet/server/services/epm/registry/index.test.ts b/x-pack/plugins/fleet/server/services/epm/registry/index.test.ts index 1208ffdaefe4af..aea28b5d56ab9d 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/index.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/index.test.ts @@ -5,9 +5,8 @@ */ import { AssetParts } from '../../../types'; -import { getPathParts } from '../archive'; -import { getBufferExtractor, splitPkgKey } from './index'; -import { untarBuffer, unzipBuffer } from './extract'; +import { getBufferExtractor, getPathParts, untarBuffer, unzipBuffer } from '../archive'; +import { splitPkgKey } from './index'; const testPaths = [ { diff --git a/x-pack/plugins/fleet/server/services/epm/registry/index.ts b/x-pack/plugins/fleet/server/services/epm/registry/index.ts index c35e91bdf580bd..aef1bb75619cdf 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/index.ts @@ -24,13 +24,11 @@ import { unpackArchiveToCache, } from '../archive'; import { fetchUrl, getResponse, getResponseStream } from './requests'; -import { streamToBuffer } from './streams'; +import { streamToBuffer } from '../streams'; import { getRegistryUrl } from './registry_url'; import { appContextService } from '../..'; import { PackageNotFoundError, PackageCacheError } from '../../../errors'; -export { ArchiveEntry, getBufferExtractor } from './extract'; - export interface SearchParams { category?: CategoryId; experimental?: boolean; diff --git a/x-pack/plugins/fleet/server/services/epm/registry/requests.ts b/x-pack/plugins/fleet/server/services/epm/registry/requests.ts index 2b9c3495657903..c8d158c8afaaaa 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/requests.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/requests.ts @@ -6,7 +6,7 @@ import fetch, { FetchError, Response, RequestInit } from 'node-fetch'; import pRetry from 'p-retry'; -import { streamToString } from './streams'; +import { streamToString } from '../streams'; import { appContextService } from '../../app_context'; import { RegistryError, RegistryConnectionError, RegistryResponseError } from '../../../errors'; import { getProxyAgent, getRegistryProxyUrl } from './proxy'; diff --git a/x-pack/plugins/fleet/server/services/epm/registry/streams.ts b/x-pack/plugins/fleet/server/services/epm/streams.ts similarity index 100% rename from x-pack/plugins/fleet/server/services/epm/registry/streams.ts rename to x-pack/plugins/fleet/server/services/epm/streams.ts diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/format_column.ts b/x-pack/plugins/lens/public/indexpattern_datasource/format_column.ts index 3666528f431667..1f337298a03adb 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/format_column.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/format_column.ts @@ -116,9 +116,18 @@ export const formatColumn: ExpressionFunctionDefinition< }); } if (parentFormatParams) { - const innerParams = (col.meta.params?.params as Record) ?? {}; + // if original format is already a nested one, we are just replacing the wrapper params + // otherwise wrapping it inside parentFormatId/parentFormatParams + const isNested = isNestedFormat(col.meta.params); + const innerParams = isNested + ? col.meta.params?.params + : { id: col.meta.params?.id, params: col.meta.params?.params }; + + const formatId = isNested ? col.meta.params?.id : parentFormatId; + return withParams(col, { ...col.meta.params, + id: formatId, params: { ...innerParams, ...parentFormatParams, @@ -132,6 +141,11 @@ export const formatColumn: ExpressionFunctionDefinition< }, }; +function isNestedFormat(params: DatatableColumn['meta']['params']) { + // if there is a nested params object with an id, it's a nested format + return !!params?.params?.id; +} + function withParams(col: DatatableColumn, params: Record) { return { ...col, meta: { ...col.meta, params } }; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts index 92280b0fb6ce6f..793f3387e707d3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts @@ -38,8 +38,10 @@ export class IndexPatternDatasource { renameColumns, formatColumn, getTimeScaleFunction, + getSuffixFormatter, } = await import('../async_services'); return core.getStartServices().then(([coreStart, { data }]) => { + data.fieldFormats.register([getSuffixFormatter(data.fieldFormats.deserialize)]); expressions.registerFunction(getTimeScaleFunction(data)); expressions.registerFunction(renameColumns); expressions.registerFunction(formatColumn); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index e37c31559cd0ca..94f240058d6189 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -78,6 +78,7 @@ export function columnToOperation(column: IndexPatternColumn, uniqueLabel?: stri export * from './rename_columns'; export * from './format_column'; export * from './time_scale'; +export * from './suffix_formatter'; export function getIndexPatternDatasource({ core, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.test.ts new file mode 100644 index 00000000000000..ef1739e4424fa3 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.test.ts @@ -0,0 +1,28 @@ +/* + * 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 { FormatFactory } from '../types'; +import { getSuffixFormatter } from './suffix_formatter'; + +describe('suffix formatter', () => { + it('should call nested formatter and apply suffix', () => { + const convertMock = jest.fn((x) => x); + const formatFactory = jest.fn(() => ({ convert: convertMock })); + const SuffixFormatter = getSuffixFormatter((formatFactory as unknown) as FormatFactory); + const nestedParams = { abc: 123 }; + const formatterInstance = new SuffixFormatter({ + unit: 'h', + id: 'nestedFormatter', + params: nestedParams, + }); + + const result = formatterInstance.convert(12345); + + expect(result).toEqual('12345/h'); + expect(convertMock).toHaveBeenCalledWith(12345); + expect(formatFactory).toHaveBeenCalledWith({ id: 'nestedFormatter', params: nestedParams }); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.ts b/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.ts new file mode 100644 index 00000000000000..5594976738efee --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { FieldFormat, KBN_FIELD_TYPES } from '../../../../../src/plugins/data/public'; +import { FormatFactory } from '../types'; +import { TimeScaleUnit } from './time_scale'; + +const unitSuffixes: Record = { + s: i18n.translate('xpack.lens.fieldFormats.suffix.s', { defaultMessage: '/h' }), + m: i18n.translate('xpack.lens.fieldFormats.suffix.m', { defaultMessage: '/m' }), + h: i18n.translate('xpack.lens.fieldFormats.suffix.h', { defaultMessage: '/h' }), + d: i18n.translate('xpack.lens.fieldFormats.suffix.d', { defaultMessage: '/d' }), +}; + +export function getSuffixFormatter(formatFactory: FormatFactory) { + return class SuffixFormatter extends FieldFormat { + static id = 'suffix'; + static title = i18n.translate('xpack.lens.fieldFormats.suffix.title', { + defaultMessage: 'Suffix', + }); + static fieldType = KBN_FIELD_TYPES.NUMBER; + allowsNumericalAggregations = true; + + getParamDefaults() { + return { + unit: undefined, + nestedParams: {}, + }; + } + + textConvert = (val: unknown) => { + const unit = this.param('unit') as TimeScaleUnit | undefined; + const suffix = unit ? unitSuffixes[unit] : undefined; + const nestedFormatter = this.param('id'); + const nestedParams = this.param('params'); + + const formattedValue = formatFactory({ id: nestedFormatter, params: nestedParams }).convert( + val + ); + + if (suffix) { + return `${formattedValue}${suffix}`; + } + return formattedValue; + }; + }; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts b/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts index 7a4e8f6bc0638a..06ff8058b1d09e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts @@ -11,7 +11,7 @@ import { DataPublicPluginStart } from 'src/plugins/data/public'; import { search } from '../../../../../src/plugins/data/public'; import { buildResultColumns } from '../../../../../src/plugins/expressions/common'; -type TimeScaleUnit = 's' | 'm' | 'h' | 'd'; +export type TimeScaleUnit = 's' | 'm' | 'h' | 'd'; export interface TimeScaleArgs { dateColumnId: string; diff --git a/x-pack/plugins/reporting/common/constants.ts b/x-pack/plugins/reporting/common/constants.ts index 07a239494da239..16e40bab65a46e 100644 --- a/x-pack/plugins/reporting/common/constants.ts +++ b/x-pack/plugins/reporting/common/constants.ts @@ -11,13 +11,6 @@ export const BROWSER_TYPE = 'chromium'; export const JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY = 'xpack.reporting.jobCompletionNotifications'; -export const API_BASE_URL = '/api/reporting'; // "Generation URL" from share menu -export const API_BASE_URL_V1 = '/api/reporting/v1'; // -export const API_BASE_GENERATE_V1 = `${API_BASE_URL_V1}/generate`; -export const API_LIST_URL = '/api/reporting/jobs'; -export const API_GENERATE_IMMEDIATE = `${API_BASE_URL_V1}/generate/immediate/csv/saved-object`; -export const API_DIAGNOSE_URL = `${API_BASE_URL}/diagnose`; - export const CONTENT_TYPE_CSV = 'text/csv'; export const CSV_REPORTING_ACTION = 'downloadCsvReport'; export const CSV_BOM_CHARS = '\ufeff'; @@ -57,15 +50,49 @@ export const UI_SETTINGS_CUSTOM_PDF_LOGO = 'xpackReporting:customPdfLogo'; export const UI_SETTINGS_CSV_SEPARATOR = 'csv:separator'; export const UI_SETTINGS_CSV_QUOTE_VALUES = 'csv:quoteValues'; +export const LAYOUT_TYPES = { + PRESERVE_LAYOUT: 'preserve_layout', + PRINT: 'print', +}; + +// Export Type Definitions +export const CSV_REPORT_TYPE = 'CSV'; +export const PDF_REPORT_TYPE = 'printablePdf'; +export const PNG_REPORT_TYPE = 'PNG'; + export const PDF_JOB_TYPE = 'printable_pdf'; export const PNG_JOB_TYPE = 'PNG'; export const CSV_JOB_TYPE = 'csv'; export const CSV_FROM_SAVEDOBJECT_JOB_TYPE = 'csv_from_savedobject'; export const USES_HEADLESS_JOB_TYPES = [PDF_JOB_TYPE, PNG_JOB_TYPE]; +// Licenses export const LICENSE_TYPE_TRIAL = 'trial'; export const LICENSE_TYPE_BASIC = 'basic'; export const LICENSE_TYPE_STANDARD = 'standard'; export const LICENSE_TYPE_GOLD = 'gold'; export const LICENSE_TYPE_PLATINUM = 'platinum'; export const LICENSE_TYPE_ENTERPRISE = 'enterprise'; + +// Routes +export const API_BASE_URL = '/api/reporting'; // "Generation URL" from share menu +export const API_BASE_GENERATE = `${API_BASE_URL}/generate`; +export const API_LIST_URL = `${API_BASE_URL}/jobs`; +export const API_DIAGNOSE_URL = `${API_BASE_URL}/diagnose`; + +// hacky endpoint +export const API_BASE_URL_V1 = '/api/reporting/v1'; // +export const API_GENERATE_IMMEDIATE = `${API_BASE_URL_V1}/generate/immediate/csv/saved-object`; + +// Management UI route +export const REPORTING_MANAGEMENT_HOME = '/app/management/insightsAndAlerting/reporting'; + +// Statuses +export enum JOB_STATUSES { + PENDING = 'pending', + PROCESSING = 'processing', + COMPLETED = 'completed', + FAILED = 'failed', + CANCELLED = 'cancelled', + WARNINGS = 'completed_with_warnings', +} diff --git a/x-pack/plugins/reporting/common/index.ts b/x-pack/plugins/reporting/common/index.ts index cda8934fc8bf62..0be6ab66827746 100644 --- a/x-pack/plugins/reporting/common/index.ts +++ b/x-pack/plugins/reporting/common/index.ts @@ -4,5 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { LayoutSelectorDictionary } from './types'; + export { CancellationToken } from './cancellation_token'; export { Poller } from './poller'; + +export const getDefaultLayoutSelectors = (): LayoutSelectorDictionary => ({ + screenshot: '[data-shared-items-container]', + renderComplete: '[data-shared-item]', + itemsCountAttribute: 'data-shared-items-count', + timefilterDurationAttribute: 'data-shared-timefilter-duration', +}); diff --git a/x-pack/plugins/reporting/common/poller.ts b/x-pack/plugins/reporting/common/poller.ts index 2127a876f4a271..017dbac13e29b5 100644 --- a/x-pack/plugins/reporting/common/poller.ts +++ b/x-pack/plugins/reporting/common/poller.ts @@ -5,7 +5,16 @@ */ import _ from 'lodash'; -import { PollerOptions } from './types'; + +interface PollerOptions { + functionToPoll: () => Promise; + pollFrequencyInMillis: number; + trailing?: boolean; + continuePollingOnError?: boolean; + pollFrequencyErrorMultiplier?: number; + successFunction?: (...args: any) => any; + errorFunction?: (error: Error) => any; +} // @TODO Maybe move to observables someday export class Poller { diff --git a/x-pack/plugins/reporting/common/types.ts b/x-pack/plugins/reporting/common/types.ts index 24c126bfe0571f..abd0bee7fb6ea2 100644 --- a/x-pack/plugins/reporting/common/types.ts +++ b/x-pack/plugins/reporting/common/types.ts @@ -4,15 +4,94 @@ * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export { ReportingConfigType } from '../server/config'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutParams } from '../server/lib/layouts'; -export { LayoutParams }; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export { ReportDocument, ReportSource } from '../server/lib/store/report'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export { BaseParams } from '../server/types'; +export interface PageSizeParams { + pageMarginTop: number; + pageMarginBottom: number; + pageMarginWidth: number; + tableBorderWidth: number; + headingHeight: number; + subheadingHeight: number; +} + +export interface LayoutSelectorDictionary { + screenshot: string; + renderComplete: string; + itemsCountAttribute: string; + timefilterDurationAttribute: string; +} + +export interface PdfImageSize { + width: number; + height?: number; +} + +export interface Size { + width: number; + height: number; +} + +export interface LayoutParams { + id: string; + dimensions?: Size; + selectors?: LayoutSelectorDictionary; +} + +export interface ReportDocumentHead { + _id: string; + _index: string; + _seq_no: unknown; + _primary_term: unknown; +} + +export interface TaskRunResult { + content_type: string | null; + content: string | null; + csv_contains_formulas?: boolean; + size: number; + max_size_reached?: boolean; + warnings?: string[]; +} + +export interface ReportSource { + jobtype: string; + kibana_name: string; + kibana_id: string; + created_by: string | false; + payload: { + headers: string; // encrypted headers + browserTimezone?: string; // may use timezone from advanced settings + objectType: string; + title: string; + layout?: LayoutParams; + }; + meta: { objectType: string; layout?: string }; + browser_type: string; + max_attempts: number; + timeout: number; + + status: JobStatus; + attempts: number; + output: TaskRunResult | null; + started_at?: string; + completed_at?: string; + created_at: string; + priority?: number; + process_expiration?: string; +} + +/* + * The document created by Reporting to store in the .reporting index + */ +export interface ReportDocument extends ReportDocumentHead { + _source: ReportSource; +} + +export interface BaseParams { + browserTimezone?: string; // browserTimezone is optional: it is not in old POST URLs that were generated prior to being added to this interface + layout?: LayoutParams; + objectType: string; + title: string; +} export type JobId = string; export type JobStatus = @@ -59,18 +138,28 @@ export interface ReportApiJSON { status: string; } -export interface PollerOptions { - functionToPoll: () => Promise; - pollFrequencyInMillis: number; - trailing?: boolean; - continuePollingOnError?: boolean; - pollFrequencyErrorMultiplier?: number; - successFunction?: (...args: any) => any; - errorFunction?: (error: Error) => any; -} - export interface LicenseCheckResults { enableLinks: boolean; showLinks: boolean; message: string; } + +export interface JobSummary { + id: JobId; + status: JobStatus; + title: string; + jobtype: string; + maxSizeReached?: boolean; + csvContainsFormulas?: boolean; +} + +export interface JobSummarySet { + completed: JobSummary[]; + failed: JobSummary[]; +} + +type DownloadLink = string; +export type DownloadReportFn = (jobId: JobId) => DownloadLink; + +type ManagementLink = string; +export type ManagementLinkFn = () => ManagementLink; diff --git a/x-pack/plugins/reporting/constants.ts b/x-pack/plugins/reporting/constants.ts deleted file mode 100644 index 772c52dde4a152..00000000000000 --- a/x-pack/plugins/reporting/constants.ts +++ /dev/null @@ -1,39 +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. - */ - -export const JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY = - 'xpack.reporting.jobCompletionNotifications'; - -// Routes -export const API_BASE_URL = '/api/reporting'; -export const API_LIST_URL = `${API_BASE_URL}/jobs`; -export const API_BASE_GENERATE = `${API_BASE_URL}/generate`; -export const API_GENERATE_IMMEDIATE = `${API_BASE_URL}/v1/generate/immediate/csv/saved-object`; -export const REPORTING_MANAGEMENT_HOME = '/app/management/insightsAndAlerting/reporting'; - -// Statuses -export const JOB_STATUS_FAILED = 'failed'; -export const JOB_STATUS_COMPLETED = 'completed'; -export const JOB_STATUS_WARNINGS = 'completed_with_warnings'; - -export enum JobStatuses { - PENDING = 'pending', - PROCESSING = 'processing', - COMPLETED = 'completed', - FAILED = 'failed', - CANCELLED = 'cancelled', - WARNINGS = 'completed_with_warnings', -} - -// Types -export const PDF_JOB_TYPE = 'printable_pdf'; -export const PNG_JOB_TYPE = 'PNG'; -export const CSV_JOB_TYPE = 'csv'; -export const CSV_FROM_SAVEDOBJECT_JOB_TYPE = 'csv_from_savedobject'; -export const USES_HEADLESS_JOB_TYPES = [PDF_JOB_TYPE, PNG_JOB_TYPE]; - -// Actions -export const CSV_REPORTING_ACTION = 'downloadCsvReport'; diff --git a/x-pack/plugins/reporting/public/components/buttons/report_download_button.tsx b/x-pack/plugins/reporting/public/components/buttons/report_download_button.tsx index 6c13264ebcb1fc..4bd86d15949e85 100644 --- a/x-pack/plugins/reporting/public/components/buttons/report_download_button.tsx +++ b/x-pack/plugins/reporting/public/components/buttons/report_download_button.tsx @@ -6,7 +6,7 @@ import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import React, { FunctionComponent } from 'react'; -import { JobStatuses } from '../../../constants'; +import { JOB_STATUSES } from '../../../common/constants'; import { Job as ListingJob, Props as ListingProps } from '../report_listing'; type Props = { record: ListingJob } & ListingProps; @@ -14,7 +14,7 @@ type Props = { record: ListingJob } & ListingProps; export const ReportDownloadButton: FunctionComponent = (props: Props) => { const { record, apiClient, intl } = props; - if (record.status !== JobStatuses.COMPLETED && record.status !== JobStatuses.WARNINGS) { + if (record.status !== JOB_STATUSES.COMPLETED && record.status !== JOB_STATUSES.WARNINGS) { return null; } diff --git a/x-pack/plugins/reporting/public/components/buttons/report_error_button.tsx b/x-pack/plugins/reporting/public/components/buttons/report_error_button.tsx index 4eee86cd79ce72..2864802f843f45 100644 --- a/x-pack/plugins/reporting/public/components/buttons/report_error_button.tsx +++ b/x-pack/plugins/reporting/public/components/buttons/report_error_button.tsx @@ -7,7 +7,7 @@ import { EuiButtonIcon, EuiCallOut, EuiPopover } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { Component } from 'react'; -import { JobStatuses } from '../../../constants'; +import { JOB_STATUSES } from '../../../common/constants'; import { JobContent, ReportingAPIClient } from '../../lib/reporting_api_client'; import { Job as ListingJob } from '../report_listing'; @@ -43,7 +43,7 @@ class ReportErrorButtonUi extends Component { public render() { const { record, intl } = this.props; - if (record.status !== JobStatuses.FAILED) { + if (record.status !== JOB_STATUSES.FAILED) { return null; } diff --git a/x-pack/plugins/reporting/public/components/buttons/report_info_button.tsx b/x-pack/plugins/reporting/public/components/buttons/report_info_button.tsx index 068cb7d44b0a16..0e249f156f5874 100644 --- a/x-pack/plugins/reporting/public/components/buttons/report_info_button.tsx +++ b/x-pack/plugins/reporting/public/components/buttons/report_info_button.tsx @@ -17,8 +17,8 @@ import { } from '@elastic/eui'; import { get } from 'lodash'; import React, { Component, Fragment } from 'react'; +import { USES_HEADLESS_JOB_TYPES } from '../../../common/constants'; import { ReportApiJSON } from '../../../common/types'; -import { USES_HEADLESS_JOB_TYPES } from '../../../constants'; import { ReportingAPIClient } from '../../lib/reporting_api_client'; interface Props { diff --git a/x-pack/plugins/reporting/public/components/index.ts b/x-pack/plugins/reporting/public/components/index.ts index 354ef189704ad0..370e90c8d2d08f 100644 --- a/x-pack/plugins/reporting/public/components/index.ts +++ b/x-pack/plugins/reporting/public/components/index.ts @@ -9,3 +9,4 @@ export { getFailureToast } from './job_failure'; export { getWarningFormulasToast } from './job_warning_formulas'; export { getWarningMaxSizeToast } from './job_warning_max_size'; export { getGeneralErrorToast } from './general_error'; +export { ScreenCapturePanelContent } from './screen_capture_panel_content'; diff --git a/x-pack/plugins/reporting/public/components/job_download_button.tsx b/x-pack/plugins/reporting/public/components/job_download_button.tsx index 8cf3ce8644add1..7dff2cafa047b2 100644 --- a/x-pack/plugins/reporting/public/components/job_download_button.tsx +++ b/x-pack/plugins/reporting/public/components/job_download_button.tsx @@ -7,8 +7,7 @@ import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { JobSummary } from '../'; -import { JobId } from '../../common/types'; +import { JobId, JobSummary } from '../../common/types'; interface Props { getUrl: (jobId: JobId) => string; diff --git a/x-pack/plugins/reporting/public/components/job_failure.tsx b/x-pack/plugins/reporting/public/components/job_failure.tsx index 8d8f32f692343d..e9c3a448cfe412 100644 --- a/x-pack/plugins/reporting/public/components/job_failure.tsx +++ b/x-pack/plugins/reporting/public/components/job_failure.tsx @@ -9,8 +9,8 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment } from 'react'; import { ToastInput } from 'src/core/public'; -import { JobSummary, ManagementLinkFn } from '../'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; +import { JobSummary, ManagementLinkFn } from '../../common/types'; export const getFailureToast = ( errorText: string, diff --git a/x-pack/plugins/reporting/public/components/job_success.tsx b/x-pack/plugins/reporting/public/components/job_success.tsx index 05cf2c4c5784a6..f03914b2be2f2d 100644 --- a/x-pack/plugins/reporting/public/components/job_success.tsx +++ b/x-pack/plugins/reporting/public/components/job_success.tsx @@ -7,9 +7,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment } from 'react'; import { ToastInput } from 'src/core/public'; -import { JobSummary } from '../'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { JobId } from '../../common/types'; +import { JobId, JobSummary } from '../../common/types'; import { DownloadButton } from './job_download_button'; import { ReportLink } from './report_link'; diff --git a/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx b/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx index 8cccc94e98dcda..338c718a060c1e 100644 --- a/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx +++ b/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx @@ -7,9 +7,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment } from 'react'; import { ToastInput } from 'src/core/public'; -import { JobSummary } from '../'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { JobId } from '../../common/types'; +import { JobId, JobSummary } from '../../common/types'; import { DownloadButton } from './job_download_button'; import { ReportLink } from './report_link'; diff --git a/x-pack/plugins/reporting/public/components/job_warning_max_size.tsx b/x-pack/plugins/reporting/public/components/job_warning_max_size.tsx index c350eef0e5a547..cab743e2006df0 100644 --- a/x-pack/plugins/reporting/public/components/job_warning_max_size.tsx +++ b/x-pack/plugins/reporting/public/components/job_warning_max_size.tsx @@ -7,9 +7,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment } from 'react'; import { ToastInput } from 'src/core/public'; -import { JobSummary } from '../'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { JobId } from '../../common/types'; +import { JobId, JobSummary } from '../../common/types'; import { DownloadButton } from './job_download_button'; import { ReportLink } from './report_link'; diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx index a512b1305b8e03..ac6d03a407c289 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -22,9 +22,9 @@ import { Component, default as React, Fragment } from 'react'; import { Subscription } from 'rxjs'; import { ApplicationStart, ToastsSetup } from 'src/core/public'; import { ILicense, LicensingPluginSetup } from '../../../licensing/public'; +import { JOB_STATUSES as JobStatuses } from '../../common/constants'; import { Poller } from '../../common/poller'; import { durationToNumber } from '../../common/schema_utils'; -import { JobStatuses } from '../../constants'; import { checkLicense } from '../lib/license_check'; import { JobQueueEntry, ReportingAPIClient } from '../lib/reporting_api_client'; import { ClientConfigType } from '../plugin'; diff --git a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx index 18895f9e623eb9..7f48b5d9101baa 100644 --- a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx +++ b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx @@ -10,6 +10,7 @@ import React, { Component, ReactElement } from 'react'; import { ToastsSetup } from 'src/core/public'; import url from 'url'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; +import { CSV_REPORT_TYPE, PDF_REPORT_TYPE, PNG_REPORT_TYPE } from '../../common/constants'; import { BaseParams } from '../../common/types'; import { ReportingAPIClient } from '../lib/reporting_api_client'; @@ -165,12 +166,12 @@ class ReportingPanelContentUi extends Component { private prettyPrintReportingType = () => { switch (this.props.reportType) { - case 'printablePdf': + case PDF_REPORT_TYPE: return 'PDF'; case 'csv': - return 'CSV'; + return CSV_REPORT_TYPE; case 'png': - return 'PNG'; + return PNG_REPORT_TYPE; default: return this.props.reportType; } diff --git a/x-pack/plugins/reporting/public/index.ts b/x-pack/plugins/reporting/public/index.ts index 251fd14ee4d57c..f15a5ca4817571 100644 --- a/x-pack/plugins/reporting/public/index.ts +++ b/x-pack/plugins/reporting/public/index.ts @@ -5,33 +5,21 @@ */ import { PluginInitializerContext } from 'src/core/public'; -import { ReportingPublicPlugin } from './plugin'; +import { ScreenCapturePanelContent } from './components/screen_capture_panel_content'; import * as jobCompletionNotifications from './lib/job_completion_notifications'; -import { JobId, JobStatus } from '../common/types'; +import { ReportingAPIClient } from './lib/reporting_api_client'; +import { ReportingPublicPlugin } from './plugin'; -export function plugin(initializerContext: PluginInitializerContext) { - return new ReportingPublicPlugin(initializerContext); +export interface ReportingSetup { + components: { + ScreenCapturePanel: typeof ScreenCapturePanelContent; + }; } -export { ReportingPublicPlugin as Plugin }; -export { jobCompletionNotifications }; +export type ReportingStart = ReportingSetup; -export interface JobSummary { - id: JobId; - status: JobStatus; - title: string; - jobtype: string; - maxSizeReached?: boolean; - csvContainsFormulas?: boolean; -} +export { ReportingAPIClient, ReportingPublicPlugin as Plugin, jobCompletionNotifications }; -export interface JobSummarySet { - completed: JobSummary[]; - failed: JobSummary[]; +export function plugin(initializerContext: PluginInitializerContext) { + return new ReportingPublicPlugin(initializerContext); } - -type DownloadLink = string; -export type DownloadReportFn = (jobId: JobId) => DownloadLink; - -type ManagementLink = string; -export type ManagementLinkFn = () => ManagementLink; diff --git a/x-pack/plugins/reporting/public/lib/job_completion_notifications.ts b/x-pack/plugins/reporting/public/lib/job_completion_notifications.ts index 06694361b757d5..39a7c9f84b8e5b 100644 --- a/x-pack/plugins/reporting/public/lib/job_completion_notifications.ts +++ b/x-pack/plugins/reporting/public/lib/job_completion_notifications.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../../constants'; +import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../../common/constants'; type JobId = string; diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client.ts index 2853caaaaa1b51..71b57d0c0124e3 100644 --- a/x-pack/plugins/reporting/public/lib/reporting_api_client.ts +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client.ts @@ -7,14 +7,20 @@ import { stringify } from 'query-string'; import rison from 'rison-node'; import { HttpSetup } from 'src/core/public'; -import { DownloadReportFn, ManagementLinkFn } from '../'; -import { JobId, ReportApiJSON, ReportDocument, ReportSource } from '../../common/types'; import { API_BASE_GENERATE, API_BASE_URL, API_LIST_URL, REPORTING_MANAGEMENT_HOME, -} from '../../constants'; +} from '../../common/constants'; +import { + DownloadReportFn, + JobId, + ManagementLinkFn, + ReportApiJSON, + ReportDocument, + ReportSource, +} from '../../common/types'; import { add } from './job_completion_notifications'; export interface JobQueueEntry { diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts index f91517e4397f93..31d324bd77159b 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts @@ -6,8 +6,7 @@ import sinon, { stub } from 'sinon'; import { NotificationsStart } from 'src/core/public'; -import { JobSummary } from '../'; -import { ReportDocument } from '../../common/types'; +import { JobSummary, ReportDocument } from '../../common/types'; import { ReportingAPIClient } from './reporting_api_client'; import { ReportingNotifierStreamHandler } from './stream_handler'; diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.ts b/x-pack/plugins/reporting/public/lib/stream_handler.ts index d97c0a7a2d11ef..4b2305b60c413c 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.ts @@ -8,14 +8,8 @@ import { i18n } from '@kbn/i18n'; import * as Rx from 'rxjs'; import { catchError, map } from 'rxjs/operators'; import { NotificationsSetup } from 'src/core/public'; -import { JobSummarySet, JobSummary } from '../'; -import { JobId, ReportDocument } from '../../common/types'; -import { - JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, - JOB_STATUS_COMPLETED, - JOB_STATUS_FAILED, - JOB_STATUS_WARNINGS, -} from '../../constants'; +import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JOB_STATUSES } from '../../common/constants'; +import { JobId, JobSummary, JobSummarySet, ReportDocument } from '../../common/types'; import { getFailureToast, getGeneralErrorToast, @@ -107,9 +101,9 @@ export class ReportingNotifierStreamHandler { _source: { status: jobStatus }, } = job; if (storedJobs.includes(jobId)) { - if (jobStatus === JOB_STATUS_COMPLETED || jobStatus === JOB_STATUS_WARNINGS) { + if (jobStatus === JOB_STATUSES.COMPLETED || jobStatus === JOB_STATUSES.WARNINGS) { completedJobs.push(getReportStatus(job)); - } else if (jobStatus === JOB_STATUS_FAILED) { + } else if (jobStatus === JOB_STATUSES.FAILED) { failedJobs.push(getReportStatus(job)); } else { pending.push(jobId); diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index 1e3f7e34bebdb1..9a4832b114e40d 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -9,20 +9,18 @@ import _ from 'lodash'; import moment from 'moment-timezone'; import { CoreSetup } from 'src/core/public'; import { - UiActionsActionDefinition as ActionDefinition, + ISearchEmbeddable, + SEARCH_EMBEDDABLE_TYPE, +} from '../../../../../src/plugins/discover/public'; +import { IEmbeddable, ViewMode } from '../../../../../src/plugins/embeddable/public'; +import { IncompatibleActionError, + UiActionsActionDefinition as ActionDefinition, } from '../../../../../src/plugins/ui_actions/public'; import { LicensingPluginSetup } from '../../../licensing/public'; +import { API_GENERATE_IMMEDIATE, CSV_REPORTING_ACTION } from '../../common/constants'; import { checkLicense } from '../lib/license_check'; -import { ViewMode, IEmbeddable } from '../../../../../src/plugins/embeddable/public'; -import { - ISearchEmbeddable, - SEARCH_EMBEDDABLE_TYPE, -} from '../../../../../src/plugins/discover/public'; - -import { API_GENERATE_IMMEDIATE, CSV_REPORTING_ACTION } from '../../constants'; - function isSavedSearchEmbeddable( embeddable: IEmbeddable | ISearchEmbeddable ): embeddable is ISearchEmbeddable { diff --git a/x-pack/plugins/reporting/public/plugin.ts b/x-pack/plugins/reporting/public/plugin.ts index 33f4fd4abf72cb..52362b4c68734f 100644 --- a/x-pack/plugins/reporting/public/plugin.ts +++ b/x-pack/plugins/reporting/public/plugin.ts @@ -24,11 +24,14 @@ import { import { ManagementSetup, ManagementStart } from '../../../../src/plugins/management/public'; import { SharePluginSetup, SharePluginStart } from '../../../../src/plugins/share/public'; import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/public'; +import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../common/constants'; import { durationToNumber } from '../common/schema_utils'; -import { JobId, ReportingConfigType } from '../common/types'; -import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../constants'; -import { JobSummarySet } from './'; -import { getGeneralErrorToast } from './components'; +import { JobId, JobSummarySet } from '../common/types'; +import { ReportingSetup, ReportingStart } from './'; +import { + getGeneralErrorToast, + ScreenCapturePanelContent as ScreenCapturePanel, +} from './components'; import { ReportingAPIClient } from './lib/reporting_api_client'; import { ReportingNotifierStreamHandler as StreamHandler } from './lib/stream_handler'; import { GetCsvReportPanelAction } from './panel_actions/get_csv_panel_action'; @@ -36,7 +39,12 @@ import { csvReportingProvider } from './share_context_menu/register_csv_reportin import { reportingPDFPNGProvider } from './share_context_menu/register_pdf_png_reporting'; export interface ClientConfigType { - poll: ReportingConfigType['poll']; + poll: { + jobsRefresh: { + interval: number; + intervalErrorMultiplier: number; + }; + }; } function getStored(): JobId[] { @@ -75,8 +83,13 @@ export interface ReportingPublicPluginStartDendencies { export class ReportingPublicPlugin implements - Plugin { - private config: ClientConfigType; + Plugin< + ReportingSetup, + ReportingStart, + ReportingPublicPluginSetupDendencies, + ReportingPublicPluginStartDendencies + > { + private readonly contract: ReportingStart = { components: { ScreenCapturePanel } }; private readonly stop$ = new Rx.ReplaySubject(1); private readonly title = i18n.translate('xpack.reporting.management.reportingTitle', { defaultMessage: 'Reporting', @@ -84,6 +97,7 @@ export class ReportingPublicPlugin private readonly breadcrumbText = i18n.translate('xpack.reporting.breadcrumb', { defaultMessage: 'Reporting', }); + private config: ClientConfigType; constructor(initializerContext: PluginInitializerContext) { this.config = initializerContext.config.get(); @@ -149,6 +163,8 @@ export class ReportingPublicPlugin uiSettings, }) ); + + return this.contract; } public start(core: CoreStart) { @@ -166,6 +182,8 @@ export class ReportingPublicPlugin catchError((err) => handleError(notifications, err)) ) .subscribe(); + + return this.contract; } public stop() { diff --git a/x-pack/plugins/reporting/server/export_types/csv/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts index 5b98a198b7d1a6..43243d265e926d 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/create_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CSV_JOB_TYPE } from '../../../constants'; +import { CSV_JOB_TYPE } from '../../../common/constants'; import { cryptoFactory } from '../../lib'; import { CreateJobFn, CreateJobFnFactory } from '../../types'; import { IndexPatternSavedObject, JobParamsCSV, TaskPayloadCSV } from './types'; diff --git a/x-pack/plugins/reporting/server/export_types/csv/index.ts b/x-pack/plugins/reporting/server/export_types/csv/index.ts index e66cfef18c6e26..f7b7ff5709fe69 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/index.ts @@ -5,6 +5,7 @@ */ import { + CSV_JOB_TYPE as jobType, LICENSE_TYPE_BASIC, LICENSE_TYPE_ENTERPRISE, LICENSE_TYPE_GOLD, @@ -12,7 +13,6 @@ import { LICENSE_TYPE_STANDARD, LICENSE_TYPE_TRIAL, } from '../../../common/constants'; -import { CSV_JOB_TYPE as jobType } from '../../../constants'; import { CreateJobFn, ExportTypeDefinition, RunTaskFn } from '../../types'; import { createJobFnFactory } from './create_job'; import { runTaskFnFactory } from './execute_job'; diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts index abe9fbf3e39506..2c163aeb57a644 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts @@ -5,6 +5,7 @@ */ import { + CSV_FROM_SAVEDOBJECT_JOB_TYPE, LICENSE_TYPE_BASIC, LICENSE_TYPE_ENTERPRISE, LICENSE_TYPE_GOLD, @@ -12,7 +13,6 @@ import { LICENSE_TYPE_STANDARD, LICENSE_TYPE_TRIAL, } from '../../../common/constants'; -import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../constants'; import { ExportTypeDefinition } from '../../types'; import { createJobFnFactory, ImmediateCreateJobFn } from './create_job'; import { ImmediateExecuteFn, runTaskFnFactory } from './execute_job'; diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/metadata.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/metadata.ts index a0fd8a29fdcc45..fda360103a1153 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/metadata.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/metadata.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../constants'; +import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants'; export const metadata = { id: CSV_FROM_SAVEDOBJECT_JOB_TYPE, 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 b1fcdbe05fd67f..010b6f431db7ed 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 @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PNG_JOB_TYPE } from '../../../../constants'; +import { PNG_JOB_TYPE } from '../../../../common/constants'; import { cryptoFactory } from '../../../lib'; import { CreateJobFn, CreateJobFnFactory } from '../../../types'; import { validateUrls } from '../../common'; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts index dcd33a0fc8d530..a529cb864b6f73 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PDF_JOB_TYPE } from '../../../../constants'; +import { PDF_JOB_TYPE } from '../../../../common/constants'; import { cryptoFactory } from '../../../lib'; import { CreateJobFn, CreateJobFnFactory } from '../../../types'; import { validateUrls } from '../../common'; diff --git a/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts index e69b8d61dec0d2..c90f67b81317e8 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { LAYOUT_TYPES } from '../../../common/constants'; import { CaptureConfig } from '../../types'; -import { LayoutInstance, LayoutParams, LayoutTypes } from './'; +import { LayoutInstance, LayoutParams } from './'; import { PreserveLayout } from './preserve_layout'; import { PrintLayout } from './print_layout'; @@ -13,7 +14,7 @@ export function createLayout( captureConfig: CaptureConfig, layoutParams?: LayoutParams ): LayoutInstance { - if (layoutParams && layoutParams.dimensions && layoutParams.id === LayoutTypes.PRESERVE_LAYOUT) { + if (layoutParams && layoutParams.dimensions && layoutParams.id === LAYOUT_TYPES.PRESERVE_LAYOUT) { return new PreserveLayout(layoutParams.dimensions); } diff --git a/x-pack/plugins/reporting/server/lib/layouts/index.ts b/x-pack/plugins/reporting/server/lib/layouts/index.ts index c091339a605823..8bfe79aeb8a216 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/index.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/index.ts @@ -4,59 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HeadlessChromiumDriver } from '../../browsers'; import { LevelLogger } from '../'; +import { LayoutSelectorDictionary, Size } from '../../../common/types'; +import { HeadlessChromiumDriver } from '../../browsers'; import { Layout } from './layout'; +export { + LayoutParams, + LayoutSelectorDictionary, + PageSizeParams, + PdfImageSize, + Size, +} from '../../../common/types'; export { createLayout } from './create_layout'; export { Layout } from './layout'; export { PreserveLayout } from './preserve_layout'; export { PrintLayout } from './print_layout'; -export const LayoutTypes = { - PRESERVE_LAYOUT: 'preserve_layout', - PRINT: 'print', -}; - -export const getDefaultLayoutSelectors = (): LayoutSelectorDictionary => ({ - screenshot: '[data-shared-items-container]', - renderComplete: '[data-shared-item]', - itemsCountAttribute: 'data-shared-items-count', - timefilterDurationAttribute: 'data-shared-timefilter-duration', -}); - -export interface PageSizeParams { - pageMarginTop: number; - pageMarginBottom: number; - pageMarginWidth: number; - tableBorderWidth: number; - headingHeight: number; - subheadingHeight: number; -} - -export interface LayoutSelectorDictionary { - screenshot: string; - renderComplete: string; - itemsCountAttribute: string; - timefilterDurationAttribute: string; -} - -export interface PdfImageSize { - width: number; - height?: number; -} - -export interface Size { - width: number; - height: number; -} - -export interface LayoutParams { - id: string; - dimensions?: Size; - selectors?: LayoutSelectorDictionary; -} - interface LayoutSelectors { // Fields that are not part of Layout: the instances // independently implement these fields on their own diff --git a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts index faddaae64ce5dd..549e898d8a13ea 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts @@ -6,15 +6,10 @@ import path from 'path'; import { CustomPageSize } from 'pdfmake/interfaces'; -import { - getDefaultLayoutSelectors, - Layout, - LayoutInstance, - LayoutSelectorDictionary, - LayoutTypes, - PageSizeParams, - Size, -} from './'; +import { getDefaultLayoutSelectors } from '../../../common'; +import { LAYOUT_TYPES } from '../../../common/constants'; +import { LayoutSelectorDictionary, PageSizeParams, Size } from '../../../common/types'; +import { Layout, LayoutInstance } from './'; // We use a zoom of two to bump up the resolution of the screenshot a bit. const ZOOM: number = 2; @@ -28,7 +23,7 @@ export class PreserveLayout extends Layout implements LayoutInstance { private readonly scaledWidth: number; constructor(size: Size, layoutSelectors?: LayoutSelectorDictionary) { - super(LayoutTypes.PRESERVE_LAYOUT); + super(LAYOUT_TYPES.PRESERVE_LAYOUT); this.height = size.height; this.width = size.width; this.scaledHeight = size.height * ZOOM; diff --git a/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts index e979cdeeb71fec..8db1fa7ff63478 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts @@ -8,16 +8,12 @@ import path from 'path'; import { PageOrientation, PredefinedPageSize } from 'pdfmake/interfaces'; import { EvaluateFn, SerializableOrJSHandle } from 'puppeteer'; import { LevelLogger } from '../'; +import { getDefaultLayoutSelectors } from '../../../common'; +import { LAYOUT_TYPES } from '../../../common/constants'; +import { LayoutSelectorDictionary, Size } from '../../../common/types'; import { HeadlessChromiumDriver } from '../../browsers'; import { CaptureConfig } from '../../types'; -import { - getDefaultLayoutSelectors, - LayoutInstance, - LayoutSelectorDictionary, - LayoutTypes, - Size, -} from './'; -import { Layout } from './layout'; +import { Layout, LayoutInstance } from './'; export class PrintLayout extends Layout implements LayoutInstance { public readonly selectors: LayoutSelectorDictionary = { @@ -28,7 +24,7 @@ export class PrintLayout extends Layout implements LayoutInstance { private captureConfig: CaptureConfig; constructor(captureConfig: CaptureConfig) { - super(LayoutTypes.PRINT); + super(LAYOUT_TYPES.PRINT); this.captureConfig = captureConfig; } diff --git a/x-pack/plugins/reporting/server/lib/store/index.ts b/x-pack/plugins/reporting/server/lib/store/index.ts index a48f2661203236..17f0fb5bf03893 100644 --- a/x-pack/plugins/reporting/server/lib/store/index.ts +++ b/x-pack/plugins/reporting/server/lib/store/index.ts @@ -4,5 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export { Report, ReportDocument } from './report'; +export { ReportDocument } from '../../../common/types'; +export { Report } from './report'; export { ReportingStore } from './store'; diff --git a/x-pack/plugins/reporting/server/lib/store/report.ts b/x-pack/plugins/reporting/server/lib/store/report.ts index d82b90f4025ed2..2e4473ef8f2ea6 100644 --- a/x-pack/plugins/reporting/server/lib/store/report.ts +++ b/x-pack/plugins/reporting/server/lib/store/report.ts @@ -7,51 +7,8 @@ import moment from 'moment'; // @ts-ignore no module definition import Puid from 'puid'; -import { JobStatus, ReportApiJSON } from '../../../common/types'; -import { JobStatuses } from '../../../constants'; -import { LayoutParams } from '../layouts'; -import { TaskRunResult } from '../tasks'; - -interface ReportDocumentHead { - _id: string; - _index: string; - _seq_no: unknown; - _primary_term: unknown; -} - -/* - * The document created by Reporting to store in the .reporting index - */ -export interface ReportDocument extends ReportDocumentHead { - _source: ReportSource; -} - -export interface ReportSource { - jobtype: string; - kibana_name: string; - kibana_id: string; - created_by: string | false; - payload: { - headers: string; // encrypted headers - browserTimezone?: string; // may use timezone from advanced settings - objectType: string; - title: string; - layout?: LayoutParams; - }; - meta: { objectType: string; layout?: string }; - browser_type: string; - max_attempts: number; - timeout: number; - - status: JobStatus; - attempts: number; - output: TaskRunResult | null; - started_at?: string; - completed_at?: string; - created_at: string; - priority?: number; - process_expiration?: string; -} +import { JOB_STATUSES } from '../../../common/constants'; +import { ReportApiJSON, ReportDocumentHead, ReportSource } from '../../../common/types'; const puid = new Puid(); @@ -107,7 +64,7 @@ export class Report implements Partial { this.browser_type = opts.browser_type; this.priority = opts.priority; - this.status = opts.status || JobStatuses.PENDING; + this.status = opts.status || JOB_STATUSES.PENDING; this.output = opts.output || null; } @@ -175,3 +132,5 @@ export class Report implements Partial { }; } } + +export { ReportApiJSON, ReportSource }; diff --git a/x-pack/plugins/reporting/server/lib/tasks/index.ts b/x-pack/plugins/reporting/server/lib/tasks/index.ts index 0dd9945985bfb5..c866c81c9793c0 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/index.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/index.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ReportSource, TaskRunResult } from '../../../common/types'; import { BasePayload } from '../../types'; -import { ReportSource } from '../store/report'; /* * The document created by Reporting to store as task parameters for Task @@ -22,11 +22,4 @@ export interface ReportTaskParams { meta: ReportSource['meta']; } -export interface TaskRunResult { - content_type: string | null; - content: string | null; - csv_contains_formulas?: boolean; - size: number; - max_size_reached?: boolean; - warnings?: string[]; -} +export { TaskRunResult }; diff --git a/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts b/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts index 400fbb16f54dcb..6ac5875acd34c6 100644 --- a/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts +++ b/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts @@ -7,7 +7,6 @@ import { schema } from '@kbn/config-schema'; import { KibanaRequest } from 'src/core/server'; import { ReportingCore } from '../'; -import { API_BASE_GENERATE_V1 } from '../../common/constants'; import { createJobFnFactory } from '../export_types/csv_from_savedobject/create_job'; import { runTaskFnFactory } from '../export_types/csv_from_savedobject/execute_job'; import { @@ -20,6 +19,9 @@ import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routi import { getJobParamsFromRequest } from './lib/get_job_params_from_request'; import { HandlerErrorFunction } from './types'; +const API_BASE_URL_V1 = '/api/reporting/v1'; +const API_BASE_GENERATE_V1 = `${API_BASE_URL_V1}/generate`; + export type CsvFromSavedObjectRequest = KibanaRequest< JobParamsPanelCsv, unknown, diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts index c9dbbda9fd68de..12a3ac5c762c74 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createLayout, LayoutInstance, LayoutTypes } from '../lib/layouts'; +import { LAYOUT_TYPES } from '../../common/constants'; +import { createLayout, LayoutInstance } from '../lib/layouts'; import { CaptureConfig } from '../types'; export const createMockLayoutInstance = (captureConfig: CaptureConfig) => { const mockLayout = createLayout(captureConfig, { - id: LayoutTypes.PRESERVE_LAYOUT, + id: LAYOUT_TYPES.PRESERVE_LAYOUT, dimensions: { height: 100, width: 100 }, }) as LayoutInstance; mockLayout.selectors = { diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts index eb046a3eab0752..8cd26df032f641 100644 --- a/x-pack/plugins/reporting/server/types.ts +++ b/x-pack/plugins/reporting/server/types.ts @@ -8,15 +8,15 @@ import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { DataPluginStart } from 'src/plugins/data/server/plugin'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { SpacesPluginSetup } from '../../spaces/server'; -import { CancellationToken } from '../../../plugins/reporting/common'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { AuthenticatedUser, SecurityPluginSetup } from '../../security/server'; +import { SpacesPluginSetup } from '../../spaces/server'; +import { CancellationToken } from '../common'; +import { BaseParams } from '../common/types'; import { ReportingConfigType } from './config'; import { ReportingCore } from './core'; import { LevelLogger } from './lib'; -import { LayoutParams } from './lib/layouts'; import { ReportTaskParams, TaskRunResult } from './lib/tasks'; /* @@ -47,12 +47,7 @@ export type ReportingUser = { username: AuthenticatedUser['username'] } | false; export type CaptureConfig = ReportingConfigType['capture']; export type ScrollConfig = ReportingConfigType['csv']['scroll']; -export interface BaseParams { - browserTimezone?: string; // browserTimezone is optional: it is not in old POST URLs that were generated prior to being added to this interface - layout?: LayoutParams; - objectType: string; - title: string; -} +export { BaseParams }; // base params decorated with encrypted headers that come into runJob functions export interface BasePayload extends BaseParams { diff --git a/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts b/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts index 3250e048edad21..890def5b63d4a7 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts @@ -33,6 +33,9 @@ export const factory = (): PolicyConfig => { logging: { file: 'info', }, + antivirus_registration: { + enabled: false, + }, }, mac: { events: { diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index 1d64578a6a7f11..673d04c856935f 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -8,6 +8,7 @@ import { ApplicationStart } from 'kibana/public'; import { NewPackagePolicy, PackagePolicy } from '../../../../fleet/common'; import { ManifestSchema } from '../schema/manifest'; +export * from './os'; export * from './trusted_apps'; /** @@ -880,6 +881,9 @@ export interface PolicyConfig { enabled: boolean; }; }; + antivirus_registration: { + enabled: boolean; + }; }; mac: { advanced?: {}; @@ -919,7 +923,10 @@ export interface UIPolicyConfig { /** * Windows-specific policy configuration that is supported via the UI */ - windows: Pick; + windows: Pick< + PolicyConfig['windows'], + 'events' | 'malware' | 'popup' | 'antivirus_registration' | 'advanced' + >; /** * Mac-specific policy configuration that is supported via the UI */ diff --git a/x-pack/plugins/security_solution/common/endpoint/types/os.ts b/x-pack/plugins/security_solution/common/endpoint/types/os.ts new file mode 100644 index 00000000000000..b9afbd63ecd54a --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/types/os.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export type Linux = 'linux'; +export type MacOS = 'macos'; +export type Windows = 'windows'; +export type OperatingSystem = Linux | MacOS | Windows; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts index 3568136dd0e7b9..79d66443bc8f13 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts @@ -11,6 +11,7 @@ import { GetTrustedAppsRequestSchema, PostTrustedAppCreateRequestSchema, } from '../schema/trusted_apps'; +import { Linux, MacOS, Windows } from './os'; /** API request params for deleting Trusted App entry */ export type DeleteTrustedAppsRequestParams = TypeOf; @@ -51,11 +52,11 @@ export type NewTrustedApp = { description?: string; } & ( | { - os: 'linux' | 'macos'; + os: Linux | MacOS; entries: MacosLinuxConditionEntry[]; } | { - os: 'windows'; + os: Windows; entries: WindowsConditionEntry[]; } ); diff --git a/x-pack/plugins/security_solution/public/management/common/translations.ts b/x-pack/plugins/security_solution/public/management/common/translations.ts index d24eb1bd315fa7..415658c1fd6afd 100644 --- a/x-pack/plugins/security_solution/public/management/common/translations.ts +++ b/x-pack/plugins/security_solution/public/management/common/translations.ts @@ -6,6 +6,8 @@ import { i18n } from '@kbn/i18n'; +import { OperatingSystem } from '../../../common/endpoint/types'; + export const ENDPOINTS_TAB = i18n.translate('xpack.securitySolution.endpointsTab', { defaultMessage: 'Endpoints', }); @@ -21,3 +23,15 @@ export const TRUSTED_APPS_TAB = i18n.translate('xpack.securitySolution.trustedAp export const BETA_BADGE_LABEL = i18n.translate('xpack.securitySolution.administration.list.beta', { defaultMessage: 'Beta', }); + +export const OS_TITLES: Readonly<{ [K in OperatingSystem]: string }> = { + windows: i18n.translate('xpack.securitySolution.administration.os.windows', { + defaultMessage: 'Windows', + }), + macos: i18n.translate('xpack.securitySolution.administration.os.macos', { + defaultMessage: 'Mac', + }), + linux: i18n.translate('xpack.securitySolution.administration.os.linux', { + defaultMessage: 'Linux', + }), +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action.ts index f5a219bce4a6bd..bda408cd00e75e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action.ts @@ -31,6 +31,13 @@ interface UserChangedPolicyConfig { }; } +interface UserChangedAntivirusRegistration { + type: 'userChangedAntivirusRegistration'; + payload: { + enabled: boolean; + }; +} + interface ServerReturnedPolicyDetailsAgentSummaryData { type: 'serverReturnedPolicyDetailsAgentSummaryData'; payload: { @@ -62,4 +69,5 @@ export type PolicyDetailsAction = | ServerReturnedPolicyDetailsUpdateFailure | ServerReturnedUpdatedPolicyDetailsData | ServerFailedToReturnPolicyDetailsData - | UserChangedPolicyConfig; + | UserChangedPolicyConfig + | UserChangedAntivirusRegistration; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts index 89ba05547f4479..69c2afbd019607 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts @@ -245,6 +245,9 @@ describe('policy details: ', () => { }, }, logging: { file: 'info' }, + antivirus_registration: { + enabled: false, + }, }, mac: { events: { process: true, file: true, network: true }, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts index 43a6ad2c585b43..bcdc7ba2089c64 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts @@ -4,11 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ import { fullPolicy, isOnPolicyDetailsPage } from './selectors'; -import { Immutable, PolicyConfig, UIPolicyConfig } from '../../../../../../common/endpoint/types'; +import { + Immutable, + PolicyConfig, + UIPolicyConfig, + PolicyData, +} from '../../../../../../common/endpoint/types'; import { ImmutableReducer } from '../../../../../common/store'; import { AppAction } from '../../../../../common/store/actions'; import { PolicyDetailsState } from '../../types'; +const updatePolicyConfigInPolicyData = ( + policyData: Immutable, + policyConfig: Immutable +) => ({ + ...policyData, + inputs: policyData.inputs.map((input) => ({ + ...input, + config: input.config && { + ...input.config, + policy: { + ...input.config.policy, + value: policyConfig, + }, + }, + })), +}); + /** * Return a fresh copy of initial state, since we mutate state in the reducer. */ @@ -126,5 +148,26 @@ export const policyDetailsReducer: ImmutableReducer UIPolicyConfig = createSel events: windows.events, malware: windows.malware, popup: windows.popup, + antivirus_registration: windows.antivirus_registration, }, mac: { advanced: mac.advanced, @@ -122,6 +123,10 @@ export const policyConfig: (s: PolicyDetailsState) => UIPolicyConfig = createSel } ); +export const isAntivirusRegistrationEnabled = createSelector(policyConfig, (uiPolicyConfig) => { + return uiPolicyConfig.windows.antivirus_registration.enabled; +}); + /** Returns the total number of possible windows eventing configurations */ export const totalWindowsEvents = (state: PolicyDetailsState): number => { const config = policyConfig(state); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts index 152caff3714b0a..3926ad2220e35d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts @@ -76,68 +76,6 @@ export interface PolicyListUrlSearchParams { page_size: number; } -/** - * Endpoint Policy configuration - */ -export interface PolicyConfig { - windows: { - events: { - dll_and_driver_load: boolean; - dns: boolean; - file: boolean; - network: boolean; - process: boolean; - registry: boolean; - security: boolean; - }; - malware: MalwareFields; - logging: { - stdout: string; - file: string; - }; - advanced: PolicyConfigAdvancedOptions; - }; - mac: { - events: { - file: boolean; - process: boolean; - network: boolean; - }; - malware: MalwareFields; - logging: { - stdout: string; - file: string; - }; - advanced: PolicyConfigAdvancedOptions; - }; - linux: { - events: { - file: boolean; - process: boolean; - network: boolean; - }; - logging: { - stdout: string; - file: string; - }; - advanced: PolicyConfigAdvancedOptions; - }; -} - -interface PolicyConfigAdvancedOptions { - elasticsearch: { - indices: { - control: string; - event: string; - logging: string; - }; - kernel: { - connect: boolean; - process: boolean; - }; - }; -} - export enum OS { windows = 'windows', mac = 'mac', diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx new file mode 100644 index 00000000000000..4f288af393b7c0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { storiesOf, addDecorator } from '@storybook/react'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { EuiCheckbox, EuiSpacer, EuiSwitch, EuiText } from '@elastic/eui'; + +import { ConfigForm } from '.'; + +addDecorator((storyFn) => ( + ({ eui: euiLightVars, darkMode: false })}>{storyFn()} +)); + +storiesOf('PolicyDetails/ConfigForm', module) + .add('One OS', () => { + return ( + + {'Some content'} + + ); + }) + .add('Multiple OSs', () => { + return ( + + {'Some content'} + + ); + }) + .add('Complex content', () => { + return ( + + + {'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore ' + + 'et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut ' + + 'aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum ' + + 'dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia ' + + 'deserunt mollit anim id est laborum.'} + + + {}} /> + + {}} /> + {}} /> + {}} /> + + ); + }) + .add('Right corner content', () => { + const toggle = {}} />; + + return ( + + {'Some content'} + + ); + }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx new file mode 100644 index 00000000000000..30c35de9b907f3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx @@ -0,0 +1,84 @@ +/* + * 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, ReactNode, memo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiHorizontalRule, + EuiText, + EuiShowFor, + EuiPanel, +} from '@elastic/eui'; + +import { OperatingSystem } from '../../../../../../../common/endpoint/types'; +import { OS_TITLES } from '../../../../../common/translations'; + +const TITLES = { + type: i18n.translate('xpack.securitySolution.endpoint.policyDetailType', { + defaultMessage: 'Type', + }), + os: i18n.translate('xpack.securitySolution.endpoint.policyDetailOS', { + defaultMessage: 'Operating System', + }), +}; + +interface ConfigFormProps { + /** + * A subtitle for this component. + **/ + type: string; + /** + * Types of supported operating systems. + */ + supportedOss: OperatingSystem[]; + dataTestSubj?: string; + /** React Node to be put on the right corner of the card */ + rightCorner?: ReactNode; +} + +export const ConfigFormHeading: FC = memo(({ children }) => ( + +
{children}
+
+)); + +ConfigFormHeading.displayName = 'ConfigFormHeading'; + +export const ConfigForm: FC = memo( + ({ type, supportedOss, dataTestSubj, rightCorner, children }) => ( + + + + {TITLES.type} + {type} + + + {TITLES.os} + {supportedOss.map((os) => OS_TITLES[os]).join(', ')} + + + + + {rightCorner} + + + + + {rightCorner} + + + + + + {children} + + ) +); + +ConfigForm.displayName = 'ConfigForm'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx index 8fc5de48f36db2..9c11bc6f5a4d13 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx @@ -36,6 +36,7 @@ import { AgentsSummary } from './agents_summary'; import { VerticalDivider } from './vertical_divider'; import { WindowsEvents, MacEvents, LinuxEvents } from './policy_forms/events'; import { MalwareProtections } from './policy_forms/protections/malware'; +import { AntivirusRegistrationForm } from './policy_forms/antivirus_registration'; import { useToasts } from '../../../../common/lib/kibana'; import { AppAction } from '../../../../common/store/actions'; import { SpyRoute } from '../../../../common/utils/route/spy_routes'; @@ -251,6 +252,8 @@ export const PolicyDetails = React.memo(() => { + + diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/antivirus_registration/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/antivirus_registration/index.tsx new file mode 100644 index 00000000000000..8d1ac29c8ce1e7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/antivirus_registration/index.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo, useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { i18n } from '@kbn/i18n'; +import { EuiSpacer, EuiSwitch, EuiText } from '@elastic/eui'; + +import { isAntivirusRegistrationEnabled } from '../../../store/policy_details/selectors'; +import { usePolicyDetailsSelector } from '../../policy_hooks'; +import { ConfigForm } from '../../components/config_form'; + +export const AntivirusRegistrationForm = memo(() => { + const antivirusRegistrationEnabled = usePolicyDetailsSelector(isAntivirusRegistrationEnabled); + const dispatch = useDispatch(); + + const handleSwitchChange = useCallback( + (event) => + dispatch({ + type: 'userChangedAntivirusRegistration', + payload: { + enabled: event.target.checked, + }, + }), + [dispatch] + ); + + return ( + + + {i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.explanation', + { + defaultMessage: 'Switch the toggle to on to register Elastic anti-virus', + } + )} + + + + + ); +}); + +AntivirusRegistrationForm.displayName = 'AntivirusRegistrationForm'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/config_form.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/config_form.tsx deleted file mode 100644 index 8e3c4138efb36e..00000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/config_form.tsx +++ /dev/null @@ -1,89 +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, { useMemo } from 'react'; -import { - EuiCard, - EuiFlexGroup, - EuiFlexItem, - EuiTitle, - EuiHorizontalRule, - EuiText, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import styled from 'styled-components'; - -const PolicyDetailCard = styled.div` - .policyDetailTitleOS { - flex-grow: 2; - } - .policyDetailTitleFlexItem { - margin: 0; - } -`; -export const ConfigForm: React.FC<{ - /** - * A subtitle for this component. - **/ - type: string; - /** - * Types of supported operating systems. - */ - supportedOss: React.ReactNode; - children: React.ReactNode; - dataTestSubj: string; - /** React Node to be put on the right corner of the card */ - rightCorner: React.ReactNode; -}> = React.memo(({ type, supportedOss, children, dataTestSubj, rightCorner }) => { - const typeTitle = useMemo(() => { - return ( - - - - -
- -
-
-
- - {type} - -
- - - -
- -
-
-
- - {supportedOss} - -
- {rightCorner} -
- ); - }, [rightCorner, supportedOss, type]); - - return ( - - - - {children} - - - ); -}); - -ConfigForm.displayName = 'ConfigForm'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx index 66126adb7a4e1d..b43f93f1a1e2be 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx @@ -6,15 +6,19 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; +import { EuiText, EuiSpacer } from '@elastic/eui'; import { EventsCheckbox } from './checkbox'; import { OS } from '../../../types'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { selectedLinuxEvents, totalLinuxEvents } from '../../../store/policy_details/selectors'; -import { ConfigForm } from '../config_form'; +import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; import { getIn, setIn } from '../../../models/policy_details_config'; import { UIPolicyConfig } from '../../../../../../../common/endpoint/types'; +import { + COLLECTIONS_ENABLED_MESSAGE, + EVENTS_FORM_TYPE_LABEL, + EVENTS_HEADING, +} from './translations'; export const LinuxEvents = React.memo(() => { const selected = usePolicyDetailsSelector(selectedLinuxEvents); @@ -59,14 +63,7 @@ export const LinuxEvents = React.memo(() => { ]; return ( <> - -
- -
-
+ {EVENTS_HEADING} {items.map((item, index) => { return ( @@ -85,28 +82,16 @@ export const LinuxEvents = React.memo(() => { ); }, []); - const collectionsEnabled = useMemo(() => { - return ( - - - - ); - }, [selected, total]); - return ( + {COLLECTIONS_ENABLED_MESSAGE(selected, total)} + + } > {checkboxes} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx index dc70fc0ba0f4fd..fbbe50fbec1b03 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx @@ -6,15 +6,19 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; +import { EuiText, EuiSpacer } from '@elastic/eui'; import { EventsCheckbox } from './checkbox'; import { OS } from '../../../types'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { selectedMacEvents, totalMacEvents } from '../../../store/policy_details/selectors'; -import { ConfigForm } from '../config_form'; +import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; import { getIn, setIn } from '../../../models/policy_details_config'; import { UIPolicyConfig } from '../../../../../../../common/endpoint/types'; +import { + COLLECTIONS_ENABLED_MESSAGE, + EVENTS_FORM_TYPE_LABEL, + EVENTS_HEADING, +} from './translations'; export const MacEvents = React.memo(() => { const selected = usePolicyDetailsSelector(selectedMacEvents); @@ -59,14 +63,7 @@ export const MacEvents = React.memo(() => { ]; return ( <> - -
- -
-
+ {EVENTS_HEADING} {items.map((item, index) => { return ( @@ -85,28 +82,16 @@ export const MacEvents = React.memo(() => { ); }, []); - const collectionsEnabled = useMemo(() => { - return ( - - - - ); - }, [selected, total]); - return ( + {COLLECTIONS_ENABLED_MESSAGE(selected, total)} + + } > {checkboxes} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/translations.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/translations.ts new file mode 100644 index 00000000000000..3b48b7969a8ced --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/translations.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const EVENTS_HEADING = i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.eventingEvents', + { + defaultMessage: 'Events', + } +); + +export const EVENTS_FORM_TYPE_LABEL = i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.eventCollection', + { + defaultMessage: 'Event Collection', + } +); + +export const COLLECTIONS_ENABLED_MESSAGE = (selected: number, total: number) => { + return i18n.translate('xpack.securitySolution.endpoint.policy.details.eventCollectionsEnabled', { + defaultMessage: '{selected} / {total} event collections enabled', + values: { selected, total }, + }); +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx index 5acdf67922a3a6..f7b1a8e901ed2c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx @@ -6,15 +6,19 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; +import { EuiText, EuiSpacer } from '@elastic/eui'; import { EventsCheckbox } from './checkbox'; import { OS } from '../../../types'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { selectedWindowsEvents, totalWindowsEvents } from '../../../store/policy_details/selectors'; -import { ConfigForm } from '../config_form'; +import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; import { setIn, getIn } from '../../../models/policy_details_config'; import { UIPolicyConfig, Immutable } from '../../../../../../../common/endpoint/types'; +import { + COLLECTIONS_ENABLED_MESSAGE, + EVENTS_FORM_TYPE_LABEL, + EVENTS_HEADING, +} from './translations'; export const WindowsEvents = React.memo(() => { const selected = usePolicyDetailsSelector(selectedWindowsEvents); @@ -99,14 +103,7 @@ export const WindowsEvents = React.memo(() => { ]; return ( <> - -
- -
-
+ {EVENTS_HEADING} {items.map((item, index) => { return ( @@ -125,28 +122,16 @@ export const WindowsEvents = React.memo(() => { ); }, []); - const collectionsEnabled = useMemo(() => { - return ( - - - - ); - }, [selected, total]); - return ( + {COLLECTIONS_ENABLED_MESSAGE(selected, total)} + + } > {checkboxes} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx index b61dee52697370..7259b2ec19ee2a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx @@ -7,10 +7,11 @@ import React, { useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiRadio, EuiSwitch, - EuiTitle, EuiText, EuiSpacer, EuiTextArea, @@ -18,15 +19,13 @@ import { EuiCallOut, EuiCheckbox, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import { cloneDeep } from 'lodash'; import { APP_ID } from '../../../../../../../common/constants'; import { SecurityPageName } from '../../../../../../app/types'; import { Immutable, ProtectionModes } from '../../../../../../../common/endpoint/types'; import { OS, MalwareProtectionOSes } from '../../../types'; -import { ConfigForm } from '../config_form'; +import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; import { policyConfig } from '../../../store/policy_details/selectors'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { LinkToApp } from '../../../../../../common/components/endpoint/link_to_app'; @@ -200,14 +199,12 @@ export const MalwareProtections = React.memo(() => { const radioButtons = useMemo(() => { return ( <> - -
- -
-
+ + + {radios.map((radio) => { @@ -221,14 +218,12 @@ export const MalwareProtections = React.memo(() => { })} - -
- -
-
+ + + { type={i18n.translate('xpack.securitySolution.endpoint.policy.details.malware', { defaultMessage: 'Malware', })} - supportedOss={i18n.translate('xpack.securitySolution.endpoint.policy.details.windowsAndMac', { - defaultMessage: 'Windows, Mac', - })} + supportedOss={['windows', 'macos']} dataTestSubj="malwareProtectionsForm" rightCorner={protectionSwitch} > diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx index 211fc9ec3371e1..4bac9164e1d621 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx @@ -118,7 +118,7 @@ describe('When showing the Trusted App Create Form', () => { '.euiSuperSelect__listbox button.euiSuperSelect__item' ) ).map((button) => button.textContent); - expect(options).toEqual(['Mac OS', 'Windows', 'Linux']); + expect(options).toEqual(['Mac', 'Windows', 'Linux']); }); it('should show Description as optional', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/__snapshots__/index.test.tsx.snap index a94e6287a4f58b..a47558257420c5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/__snapshots__/index.test.tsx.snap @@ -17,7 +17,7 @@ exports[`trusted_app_card TrustedAppCard should render correctly 1`] = ` value={ } /> @@ -112,7 +112,7 @@ exports[`trusted_app_card TrustedAppCard should trim long texts 1`] = ` value={ } /> diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap index c82b9cac8ab1fc..6d45059099f8d3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap @@ -412,7 +412,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiToolTipAnchor" > - Mac OS + Mac @@ -1168,7 +1168,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiToolTipAnchor" > - Mac OS + Mac @@ -1924,7 +1924,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiToolTipAnchor" > - Mac OS + Mac @@ -3222,7 +3222,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiToolTipAnchor" > - Mac OS + Mac @@ -3978,7 +3978,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiToolTipAnchor" > - Mac OS + Mac @@ -4734,7 +4734,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiToolTipAnchor" > - Mac OS + Mac @@ -5990,7 +5990,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiToolTipAnchor" > - Mac OS + Mac @@ -6746,7 +6746,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiToolTipAnchor" > - Mac OS + Mac @@ -7502,7 +7502,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiToolTipAnchor" > - Mac OS + Mac diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap index 2797c433b8236c..d0459871d48811 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap @@ -1061,7 +1061,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -1448,7 +1448,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -1835,7 +1835,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -2222,7 +2222,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -2609,7 +2609,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -2996,7 +2996,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -3383,7 +3383,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -4001,7 +4001,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -4388,7 +4388,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -4775,7 +4775,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -5162,7 +5162,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -5549,7 +5549,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -5936,7 +5936,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -6323,7 +6323,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -7099,7 +7099,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -7486,7 +7486,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -7873,7 +7873,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -8260,7 +8260,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -8647,7 +8647,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -9034,7 +9034,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -9421,7 +9421,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -10039,7 +10039,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -10426,7 +10426,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -10813,7 +10813,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -11200,7 +11200,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -11587,7 +11587,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -11974,7 +11974,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -12361,7 +12361,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts index b2f62c2f1da4eb..4c2b3f0e59ccb4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts @@ -11,23 +11,13 @@ import { WindowsConditionEntry, } from '../../../../../common/endpoint/types'; +export { OS_TITLES } from '../../../common/translations'; + export const ABOUT_TRUSTED_APPS = i18n.translate('xpack.securitySolution.trustedapps.aboutInfo', { defaultMessage: 'Add a trusted application to improve performance or alleviate conflicts with other applications running on your hosts. Trusted applications will be applied to hosts running Endpoint Security.', }); -export const OS_TITLES: Readonly<{ [K in TrustedApp['os']]: string }> = { - windows: i18n.translate('xpack.securitySolution.trustedapps.os.windows', { - defaultMessage: 'Windows', - }), - macos: i18n.translate('xpack.securitySolution.trustedapps.os.macos', { - defaultMessage: 'Mac OS', - }), - linux: i18n.translate('xpack.securitySolution.trustedapps.os.linux', { - defaultMessage: 'Linux', - }), -}; - type Entry = MacosLinuxConditionEntry | WindowsConditionEntry; export const CONDITION_FIELD_TITLE: { [K in Entry['field']]: string } = { diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts index d1c5256c81c63d..c2e62b6e1898b7 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts @@ -46,9 +46,13 @@ describe('Workload Statistics Aggregator', () => { aggregations: { taskType: { buckets: [], + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, }, schedule: { buckets: [], + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, }, idleTasks: { doc_count: 0, @@ -158,6 +162,8 @@ describe('Workload Statistics Aggregator', () => { }, aggregations: { schedule: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, buckets: [ { key: '3600s', @@ -174,11 +180,15 @@ describe('Workload Statistics Aggregator', () => { ], }, taskType: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, buckets: [ { key: 'actions_telemetry', doc_count: 2, status: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, buckets: [ { key: 'idle', @@ -191,6 +201,8 @@ describe('Workload Statistics Aggregator', () => { key: 'alerting_telemetry', doc_count: 1, status: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, buckets: [ { key: 'idle', @@ -203,6 +215,8 @@ describe('Workload Statistics Aggregator', () => { key: 'session_cleanup', doc_count: 1, status: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, buckets: [ { key: 'idle', @@ -608,6 +622,7 @@ describe('padBuckets', () => { key: 1601668047000, doc_count: 1, interval: { + doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [], }, @@ -617,6 +632,7 @@ describe('padBuckets', () => { key: 1601668050000, doc_count: 1, interval: { + doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [], }, @@ -626,6 +642,7 @@ describe('padBuckets', () => { key: 1601668053000, doc_count: 0, interval: { + doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [], }, @@ -635,6 +652,7 @@ describe('padBuckets', () => { key: 1601668056000, doc_count: 0, interval: { + doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [], }, @@ -644,6 +662,7 @@ describe('padBuckets', () => { key: 1601668059000, doc_count: 0, interval: { + doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [], }, @@ -653,6 +672,7 @@ describe('padBuckets', () => { key: 1601668062000, doc_count: 1, interval: { + doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [], }, @@ -678,13 +698,13 @@ describe('padBuckets', () => { key_as_string: '2020-10-02T20:40:09.000Z', key: 1601671209000, doc_count: 1, - interval: { buckets: [] }, + interval: { buckets: [], sum_other_doc_count: 0, doc_count_error_upper_bound: 0 }, }, { key_as_string: '2020-10-02T20:40:12.000Z', key: 1601671212000, doc_count: 1, - interval: { buckets: [] }, + interval: { buckets: [], sum_other_doc_count: 0, doc_count_error_upper_bound: 0 }, }, ], }, @@ -707,13 +727,13 @@ describe('padBuckets', () => { key_as_string: '2020-10-02T20:40:09.000Z', key: 1601671209000, doc_count: 1, - interval: { buckets: [] }, + interval: { buckets: [], sum_other_doc_count: 0, doc_count_error_upper_bound: 0 }, }, { key_as_string: '2020-10-02T20:40:12.000Z', key: 1601671212000, doc_count: 1, - interval: { buckets: [] }, + interval: { buckets: [], sum_other_doc_count: 0, doc_count_error_upper_bound: 0 }, }, ], }, @@ -796,7 +816,7 @@ function mockHistogram( key_as_string: key.toISOString(), key: key.getTime(), doc_count: count, - interval: { buckets: [] }, + interval: { buckets: [], doc_count_error_upper_bound: 0, sum_other_doc_count: 0 }, }); } return histogramBuckets; @@ -806,6 +826,8 @@ function mockHistogram( key: number; doc_count: number; interval: { + doc_count_error_upper_bound: number; + sum_other_doc_count: number; buckets: Array<{ key: string; doc_count: number; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1892174550a1f6..238b3dccc698a4 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -17498,8 +17498,6 @@ "xpack.securitySolution.endpoint.policy.details.detectionRulesMessage": "{detectionRulesLink}を表示します。事前構築済みルールは、[検出ルール]ページで「Elastic」というタグが付けられています。", "xpack.securitySolution.endpoint.policy.details.eventCollection": "イベント収集", "xpack.securitySolution.endpoint.policy.details.eventCollectionsEnabled": "{selected} / {total}件のイベント収集が有効です", - "xpack.securitySolution.endpoint.policy.details.linux": "Linux", - "xpack.securitySolution.endpoint.policy.details.mac": "Mac", "xpack.securitySolution.endpoint.policy.details.malware": "マルウェア", "xpack.securitySolution.endpoint.policy.details.malwareProtectionsEnabled": "マルウェア保護{mode, select, true {有効} false {無効}}", "xpack.securitySolution.endpoint.policy.details.prevent": "防御", @@ -17514,8 +17512,6 @@ "xpack.securitySolution.endpoint.policy.details.updateErrorTitle": "失敗しました。", "xpack.securitySolution.endpoint.policy.details.updateSuccessMessage": "統合{name}が更新されました。", "xpack.securitySolution.endpoint.policy.details.updateSuccessTitle": "成功!", - "xpack.securitySolution.endpoint.policy.details.windows": "Windows", - "xpack.securitySolution.endpoint.policy.details.windowsAndMac": "Windows、Mac", "xpack.securitySolution.endpoint.policyDetailOS": "オペレーティングシステム", "xpack.securitySolution.endpoint.policyDetails.agentsSummary.errorTitle": "エラー", "xpack.securitySolution.endpoint.policyDetails.agentsSummary.offlineTitle": "オフライン", @@ -18466,9 +18462,6 @@ "xpack.securitySolution.trustedapps.logicalConditionBuilder.group.andOperator": "AND", "xpack.securitySolution.trustedapps.logicalConditionBuilder.noEntries": "条件が定義されていません", "xpack.securitySolution.trustedapps.noResults": "項目が見つかりません", - "xpack.securitySolution.trustedapps.os.linux": "Linux", - "xpack.securitySolution.trustedapps.os.macos": "Mac OS", - "xpack.securitySolution.trustedapps.os.windows": "Windows", "xpack.securitySolution.trustedapps.trustedapp.createdAt": "作成日", "xpack.securitySolution.trustedapps.trustedapp.createdBy": "作成者", "xpack.securitySolution.trustedapps.trustedapp.description": "説明", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 65b7f4dfe9f279..48654a5ec5ff43 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -17516,8 +17516,6 @@ "xpack.securitySolution.endpoint.policy.details.detectionRulesMessage": "请查看{detectionRulesLink}。在“检测规则”页面上,预置规则标记有“Elastic”。", "xpack.securitySolution.endpoint.policy.details.eventCollection": "事件收集", "xpack.securitySolution.endpoint.policy.details.eventCollectionsEnabled": "{selected} / {total} 事件收集已启用", - "xpack.securitySolution.endpoint.policy.details.linux": "Linux", - "xpack.securitySolution.endpoint.policy.details.mac": "Mac", "xpack.securitySolution.endpoint.policy.details.malware": "恶意软件", "xpack.securitySolution.endpoint.policy.details.malwareProtectionsEnabled": "恶意软件防护{mode, select, true {已启用} false {已禁用}}", "xpack.securitySolution.endpoint.policy.details.prevent": "防御", @@ -17533,8 +17531,6 @@ "xpack.securitySolution.endpoint.policy.details.updateErrorTitle": "失败!", "xpack.securitySolution.endpoint.policy.details.updateSuccessMessage": "集成 {name} 已更新。", "xpack.securitySolution.endpoint.policy.details.updateSuccessTitle": "成功!", - "xpack.securitySolution.endpoint.policy.details.windows": "Windows", - "xpack.securitySolution.endpoint.policy.details.windowsAndMac": "Windows、Mac", "xpack.securitySolution.endpoint.policyDetailOS": "操作系统", "xpack.securitySolution.endpoint.policyDetails.agentsSummary.errorTitle": "错误", "xpack.securitySolution.endpoint.policyDetails.agentsSummary.offlineTitle": "脱机", @@ -18485,9 +18481,6 @@ "xpack.securitySolution.trustedapps.logicalConditionBuilder.group.andOperator": "AND", "xpack.securitySolution.trustedapps.logicalConditionBuilder.noEntries": "未定义条件", "xpack.securitySolution.trustedapps.noResults": "找不到项目", - "xpack.securitySolution.trustedapps.os.linux": "Linux", - "xpack.securitySolution.trustedapps.os.macos": "Mac OS", - "xpack.securitySolution.trustedapps.os.windows": "Windows", "xpack.securitySolution.trustedapps.trustedapp.createdAt": "创建日期", "xpack.securitySolution.trustedapps.trustedapp.createdBy": "创建者", "xpack.securitySolution.trustedapps.trustedapp.description": "描述", diff --git a/x-pack/plugins/ui_actions_enhanced/common/index.ts b/x-pack/plugins/ui_actions_enhanced/common/index.ts new file mode 100644 index 00000000000000..9f4141dbcae7df --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/common/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './types'; diff --git a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts index b3664362009149..ade78c31211ab2 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts +++ b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts @@ -7,11 +7,11 @@ import { EnhancementRegistryDefinition } from '../../../../src/plugins/embeddable/server'; import { SavedObjectReference } from '../../../../src/core/types'; import { DynamicActionsState, SerializedEvent } from './types'; -import { AdvancedUiActionsPublicPlugin } from './plugin'; +import { AdvancedUiActionsServerPlugin } from './plugin'; import { SerializableState } from '../../../../src/plugins/kibana_utils/common'; export const dynamicActionEnhancement = ( - uiActionsEnhanced: AdvancedUiActionsPublicPlugin + uiActionsEnhanced: AdvancedUiActionsServerPlugin ): EnhancementRegistryDefinition => { return { id: 'dynamicActions', diff --git a/x-pack/plugins/ui_actions_enhanced/server/index.ts b/x-pack/plugins/ui_actions_enhanced/server/index.ts index 5419c4135796df..e1363be35e2e9a 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/index.ts +++ b/x-pack/plugins/ui_actions_enhanced/server/index.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AdvancedUiActionsPublicPlugin } from './plugin'; +import { AdvancedUiActionsServerPlugin } from './plugin'; export function plugin() { - return new AdvancedUiActionsPublicPlugin(); + return new AdvancedUiActionsServerPlugin(); } -export { AdvancedUiActionsPublicPlugin as Plugin }; +export { AdvancedUiActionsServerPlugin as Plugin }; export { SetupContract as AdvancedUiActionsSetup, StartContract as AdvancedUiActionsStart, diff --git a/x-pack/plugins/ui_actions_enhanced/server/plugin.ts b/x-pack/plugins/ui_actions_enhanced/server/plugin.ts index d6d18848be4de4..718304018730de 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/plugin.ts +++ b/x-pack/plugins/ui_actions_enhanced/server/plugin.ts @@ -16,7 +16,7 @@ import { } from './types'; export interface SetupContract { - registerActionFactory: any; + registerActionFactory: (definition: ActionFactoryDefinition) => void; } export type StartContract = void; @@ -25,7 +25,7 @@ interface SetupDependencies { embeddable: EmbeddableSetup; // Embeddable are needed because they register basic triggers/actions. } -export class AdvancedUiActionsPublicPlugin +export class AdvancedUiActionsServerPlugin implements Plugin { protected readonly actionFactories: ActionFactoryRegistry = new Map(); 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 df3e60d79aca57..39dd721c7067e9 100644 --- a/x-pack/test/apm_api_integration/basic/tests/index.ts +++ b/x-pack/test/apm_api_integration/basic/tests/index.ts @@ -25,6 +25,10 @@ export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderCont loadTestFile(require.resolve('./services/transaction_types')); }); + describe('Service overview', function () { + loadTestFile(require.resolve('./service_overview/error_groups')); + }); + describe('Settings', function () { loadTestFile(require.resolve('./settings/custom_link')); loadTestFile(require.resolve('./settings/agent_configuration')); diff --git a/x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts b/x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts new file mode 100644 index 00000000000000..b699a30d40418c --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts @@ -0,0 +1,220 @@ +/* + * 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 qs from 'querystring'; +import { pick, uniqBy } from 'lodash'; +import { expectSnapshot } from '../../../common/match_snapshot'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import archives from '../../../common/archives_metadata'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const archiveName = 'apm_8.0.0'; + const { start, end } = archives[archiveName]; + + describe('Service overview error groups', () => { + describe('when data is not loaded', () => { + it('handles the empty state', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'occurrences', + })}` + ); + + expect(response.status).to.be(200); + expect(response.body).to.eql({ + total_error_groups: 0, + error_groups: [], + is_aggregation_accurate: true, + }); + }); + }); + + describe('when data is loaded', () => { + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); + + it('returns the correct data', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'occurrences', + })}` + ); + + expect(response.status).to.be(200); + + expectSnapshot(response.body.total_error_groups).toMatchInline(`5`); + + expectSnapshot(response.body.error_groups.map((group: any) => group.name)).toMatchInline(` + Array [ + "Could not write JSON: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getCost(); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getCost() (through reference chain: co.elastic.apm.opbeans.repositories.Stats[\\"numbers\\"]->com.sun.proxy.$Proxy133[\\"cost\\"])", + "java.io.IOException: Connection reset by peer", + "Connection reset by peer", + "Could not write JSON: Unable to find co.elastic.apm.opbeans.model.Customer with id 6617; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Unable to find co.elastic.apm.opbeans.model.Customer with id 6617 (through reference chain: co.elastic.apm.opbeans.model.Customer_$$_jvst369_3[\\"email\\"])", + "Request method 'POST' not supported", + ] + `); + + expectSnapshot(response.body.error_groups.map((group: any) => group.occurrences.value)) + .toMatchInline(` + Array [ + 8, + 2, + 1, + 1, + 1, + ] + `); + + const firstItem = response.body.error_groups[0]; + + expectSnapshot(pick(firstItem, 'group_id', 'last_seen', 'name', 'occurrences.value')) + .toMatchInline(` + Object { + "group_id": "051f95eabf120ebe2f8b0399fe3e54c5", + "last_seen": 1601391561523, + "name": "Could not write JSON: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getCost(); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getCost() (through reference chain: co.elastic.apm.opbeans.repositories.Stats[\\"numbers\\"]->com.sun.proxy.$Proxy133[\\"cost\\"])", + "occurrences": Object { + "value": 8, + }, + } + `); + + expectSnapshot( + firstItem.occurrences.timeseries.filter(({ y }: any) => y > 0).length + ).toMatchInline(`7`); + }); + + it('sorts items in the correct order', async () => { + const descendingResponse = await supertest.get( + `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'occurrences', + })}` + ); + + expect(descendingResponse.status).to.be(200); + + const descendingOccurrences = descendingResponse.body.error_groups.map( + (item: any) => item.occurrences.value + ); + + expect(descendingOccurrences).to.eql(descendingOccurrences.concat().sort().reverse()); + + const ascendingResponse = await supertest.get( + `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'occurrences', + })}` + ); + + const ascendingOccurrences = ascendingResponse.body.error_groups.map( + (item: any) => item.occurrences.value + ); + + expect(ascendingOccurrences).to.eql(ascendingOccurrences.concat().sort().reverse()); + }); + + it('sorts items by the correct field', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'last_seen', + })}` + ); + + expect(response.status).to.be(200); + + const dates = response.body.error_groups.map((group: any) => group.last_seen); + + expect(dates).to.eql(dates.concat().sort().reverse()); + }); + + it('paginates through the items', async () => { + const size = 1; + + const firstPage = await supertest.get( + `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ + start, + end, + uiFilters: '{}', + size, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'occurrences', + })}` + ); + + expect(firstPage.status).to.eql(200); + + const totalItems = firstPage.body.total_error_groups; + + const pages = Math.floor(totalItems / size); + + const items = await new Array(pages) + .fill(undefined) + .reduce(async (prevItemsPromise, _, pageIndex) => { + const prevItems = await prevItemsPromise; + + const thisPage = await supertest.get( + `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ + start, + end, + uiFilters: '{}', + size, + numBuckets: 20, + pageIndex, + sortDirection: 'desc', + sortField: 'occurrences', + })}` + ); + + return prevItems.concat(thisPage.body.error_groups); + }, Promise.resolve([])); + + expect(items.length).to.eql(totalItems); + + expect(uniqBy(items, 'group_id').length).to.eql(totalItems); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts index 43b88915b69d90..9326f7e240e3e4 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts +++ b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts @@ -14,7 +14,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardPanelActions = getService('dashboardPanelActions'); const dashboardDrilldownPanelActions = getService('dashboardDrilldownPanelActions'); const dashboardDrilldownsManage = getService('dashboardDrilldownsManage'); - const PageObjects = getPageObjects(['dashboard', 'common', 'header', 'timePicker']); + const PageObjects = getPageObjects([ + 'dashboard', + 'common', + 'header', + 'timePicker', + 'settings', + 'copySavedObjectsToSpace', + ]); const pieChart = getService('pieChart'); const log = getService('log'); const browser = getService('browser'); @@ -22,120 +29,188 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const filterBar = getService('filterBar'); const security = getService('security'); + const spaces = getService('spaces'); describe('Dashboard to dashboard drilldown', function () { - before(async () => { - log.debug('Dashboard Drilldowns:initTests'); - await security.testUser.setRoles(['test_logstash_reader', 'global_dashboard_all']); - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.preserveCrossAppState(); - }); - - after(async () => { - await security.testUser.restoreDefaults(); - }); - - it('should create dashboard to dashboard drilldown, use it, and then delete it', async () => { - await PageObjects.dashboard.gotoDashboardEditMode( - dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME - ); - - // create drilldown - await dashboardPanelActions.openContextMenu(); - await dashboardDrilldownPanelActions.expectExistsCreateDrilldownAction(); - await dashboardDrilldownPanelActions.clickCreateDrilldown(); - await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutOpen(); - await dashboardDrilldownsManage.fillInDashboardToDashboardDrilldownWizard({ - drilldownName: DRILLDOWN_TO_AREA_CHART_NAME, - destinationDashboardTitle: dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME, + describe('Create & use drilldowns', () => { + before(async () => { + log.debug('Dashboard Drilldowns:initTests'); + await security.testUser.setRoles(['test_logstash_reader', 'global_dashboard_all']); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); }); - await dashboardDrilldownsManage.saveChanges(); - await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutClose(); - - // check that drilldown notification badge is shown - expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(1); - - // save dashboard, navigate to view mode - await PageObjects.dashboard.saveDashboard( - dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME, - { - saveAsNew: false, - waitDialogIsClosed: true, - } - ); - - // trigger drilldown action by clicking on a pie and picking drilldown action by it's name - await pieChart.clickOnPieSlice('40,000'); - await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); - - const href = await dashboardDrilldownPanelActions.getActionHrefByText( - DRILLDOWN_TO_AREA_CHART_NAME - ); - expect(typeof href).to.be('string'); // checking that action has a href - const dashboardIdFromHref = PageObjects.dashboard.getDashboardIdFromUrl(href); - - await navigateWithinDashboard(async () => { - await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_AREA_CHART_NAME); - }); - // checking that href is at least pointing to the same dashboard that we are navigated to by regular click - expect(dashboardIdFromHref).to.be(await PageObjects.dashboard.getDashboardIdFromCurrentUrl()); - - // check that we drilled-down with filter from pie chart - expect(await filterBar.getFilterCount()).to.be(1); - - const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - // brush area chart and drilldown back to pie chat dashboard - await brushAreaChart(); - await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); - - await navigateWithinDashboard(async () => { - await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + after(async () => { + await security.testUser.restoreDefaults(); }); - // because filters are preserved during navigation, we expect that only one slice is displayed (filter is still applied) - expect(await filterBar.getFilterCount()).to.be(1); - await pieChart.expectPieSliceCount(1); - - // check that new time range duration was applied - const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); - - // delete drilldown - await PageObjects.dashboard.switchToEditMode(); - await dashboardPanelActions.openContextMenu(); - await dashboardDrilldownPanelActions.expectExistsManageDrilldownsAction(); - await dashboardDrilldownPanelActions.clickManageDrilldowns(); - await dashboardDrilldownsManage.expectsManageDrilldownsFlyoutOpen(); - - await dashboardDrilldownsManage.deleteDrilldownsByTitles([DRILLDOWN_TO_AREA_CHART_NAME]); - await dashboardDrilldownsManage.closeFlyout(); + it('should create dashboard to dashboard drilldown, use it, and then delete it', async () => { + await PageObjects.dashboard.gotoDashboardEditMode( + dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME + ); + + // create drilldown + await dashboardPanelActions.openContextMenu(); + await dashboardDrilldownPanelActions.expectExistsCreateDrilldownAction(); + await dashboardDrilldownPanelActions.clickCreateDrilldown(); + await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutOpen(); + await dashboardDrilldownsManage.fillInDashboardToDashboardDrilldownWizard({ + drilldownName: DRILLDOWN_TO_AREA_CHART_NAME, + destinationDashboardTitle: dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME, + }); + await dashboardDrilldownsManage.saveChanges(); + await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutClose(); + + // check that drilldown notification badge is shown + expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(1); + + // save dashboard, navigate to view mode + await PageObjects.dashboard.saveDashboard( + dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME, + { + saveAsNew: false, + waitDialogIsClosed: true, + } + ); + + // trigger drilldown action by clicking on a pie and picking drilldown action by it's name + await pieChart.clickOnPieSlice('40,000'); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + + const href = await dashboardDrilldownPanelActions.getActionHrefByText( + DRILLDOWN_TO_AREA_CHART_NAME + ); + expect(typeof href).to.be('string'); // checking that action has a href + const dashboardIdFromHref = PageObjects.dashboard.getDashboardIdFromUrl(href); + + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_AREA_CHART_NAME); + }); + // checking that href is at least pointing to the same dashboard that we are navigated to by regular click + expect(dashboardIdFromHref).to.be( + await PageObjects.dashboard.getDashboardIdFromCurrentUrl() + ); + + // check that we drilled-down with filter from pie chart + expect(await filterBar.getFilterCount()).to.be(1); + + const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + + // brush area chart and drilldown back to pie chat dashboard + await brushAreaChart(); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + }); + + // because filters are preserved during navigation, we expect that only one slice is displayed (filter is still applied) + expect(await filterBar.getFilterCount()).to.be(1); + await pieChart.expectPieSliceCount(1); + + // check that new time range duration was applied + const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); + + // delete drilldown + await PageObjects.dashboard.switchToEditMode(); + await dashboardPanelActions.openContextMenu(); + await dashboardDrilldownPanelActions.expectExistsManageDrilldownsAction(); + await dashboardDrilldownPanelActions.clickManageDrilldowns(); + await dashboardDrilldownsManage.expectsManageDrilldownsFlyoutOpen(); + + await dashboardDrilldownsManage.deleteDrilldownsByTitles([DRILLDOWN_TO_AREA_CHART_NAME]); + await dashboardDrilldownsManage.closeFlyout(); + + // check that drilldown notification badge is shown + expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(0); + }); - // check that drilldown notification badge is shown - expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(0); + it('browser back/forward navigation works after drilldown navigation', async () => { + await PageObjects.dashboard.loadSavedDashboard( + dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME + ); + const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + await brushAreaChart(); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + }); + // check that new time range duration was applied + const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); + + await navigateWithinDashboard(async () => { + await browser.goBack(); + }); + + expect(await PageObjects.timePicker.getTimeDurationInHours()).to.be( + originalTimeRangeDurationHours + ); + }); }); - it('browser back/forward navigation works after drilldown navigation', async () => { - await PageObjects.dashboard.loadSavedDashboard( - dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME - ); - const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - await brushAreaChart(); - await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); - await navigateWithinDashboard(async () => { - await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + describe('Copy to space', () => { + const destinationSpaceId = 'custom_space'; + before(async () => { + await spaces.create({ + id: destinationSpaceId, + name: 'custom_space', + disabledFeatures: [], + }); + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaSavedObjects(); }); - // check that new time range duration was applied - const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); - await navigateWithinDashboard(async () => { - await browser.goBack(); + after(async () => { + await spaces.delete(destinationSpaceId); }); - expect(await PageObjects.timePicker.getTimeDurationInHours()).to.be( - originalTimeRangeDurationHours - ); + it('Dashboards linked by a drilldown are both copied to a space', async () => { + await PageObjects.copySavedObjectsToSpace.openCopyToSpaceFlyoutForObject( + dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME + ); + await PageObjects.copySavedObjectsToSpace.setupForm({ + destinationSpaceId, + }); + await PageObjects.copySavedObjectsToSpace.startCopy(); + + // Wait for successful copy + await testSubjects.waitForDeleted(`cts-summary-indicator-loading-${destinationSpaceId}`); + await testSubjects.existOrFail(`cts-summary-indicator-success-${destinationSpaceId}`); + + const summaryCounts = await PageObjects.copySavedObjectsToSpace.getSummaryCounts(); + + expect(summaryCounts).to.eql({ + success: 5, // 2 dashboards (linked by a drilldown) + 2 visualizations + 1 index pattern + pending: 0, + skipped: 0, + errors: 0, + }); + + await PageObjects.copySavedObjectsToSpace.finishCopy(); + + // Actually use copied dashboards in a new space: + + await PageObjects.common.navigateToApp('dashboard', { + basePath: `/s/${destinationSpaceId}`, + }); + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.loadSavedDashboard( + dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME + ); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); + + // brush area chart and drilldown back to pie chat dashboard + await brushAreaChart(); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + }); + await pieChart.expectPieSliceCount(10); + }); }); }); diff --git a/x-pack/test/functional/es_archives/reporting/hugedata/data.json.gz b/x-pack/test/functional/es_archives/reporting/hugedata/data.json.gz index c524379640df7e..c616730ff35b64 100644 Binary files a/x-pack/test/functional/es_archives/reporting/hugedata/data.json.gz and b/x-pack/test/functional/es_archives/reporting/hugedata/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json b/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json index 3434e1f80a7cee..552142d3b190ae 100644 --- a/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json +++ b/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json @@ -126,7 +126,7 @@ "title": "Dashboard Foo", "hits": 0, "description": "", - "panelsJSON": "[{}]", + "panelsJSON": "[]", "optionsJSON": "{}", "version": 1, "timeRestore": false, @@ -156,7 +156,7 @@ "title": "Dashboard Bar", "hits": 0, "description": "", - "panelsJSON": "[{}]", + "panelsJSON": "[]", "optionsJSON": "{}", "version": 1, "timeRestore": false, diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard.json index ef08d693242104..7f416c26cc9aae 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard.json +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard.json @@ -11,6 +11,12 @@ "title": "[Logs Sample] Overview ECS", "version": 1 }, + "references": [ + { "id": "sample_visualization", "name": "panel_0", "type": "visualization" }, + { "id": "sample_search", "name": "panel_1", "type": "search" }, + { "id": "sample_search", "name": "panel_2", "type": "search" }, + { "id": "sample_visualization", "name": "panel_3", "type": "visualization" } + ], "id": "sample_dashboard", "type": "dashboard" -} \ No newline at end of file +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard2.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard2.json index 7ea63c5d444ba9..c99506fec3cf54 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard2.json +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard2.json @@ -11,6 +11,12 @@ "title": "[Logs Sample2] Overview ECS", "version": 1 }, + "references": [ + { "id": "sample_visualization", "name": "panel_0", "type": "visualization" }, + { "id": "sample_search", "name": "panel_1", "type": "search" }, + { "id": "sample_search", "name": "panel_2", "type": "search" }, + { "id": "sample_visualization", "name": "panel_3", "type": "visualization" } + ], "id": "sample_dashboard2", "type": "dashboard" -} \ No newline at end of file +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/dashboard/sample_dashboard.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/dashboard/sample_dashboard.json index ef08d693242104..4513c07f277867 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/dashboard/sample_dashboard.json +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/dashboard/sample_dashboard.json @@ -11,6 +11,12 @@ "title": "[Logs Sample] Overview ECS", "version": 1 }, + "references": [ + { "id": "sample_visualization", "name": "panel_0", "type": "visualization" }, + { "id": "sample_search2", "name": "panel_1", "type": "search" }, + { "id": "sample_search2", "name": "panel_2", "type": "search" }, + { "id": "sample_visualization", "name": "panel_3", "type": "visualization" } + ], "id": "sample_dashboard", "type": "dashboard" -} \ No newline at end of file +} diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index 15c26a1b9374d6..f032416d2e7bb7 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -221,6 +221,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { message: 'Elastic Security { action } { filename }', }, }, + antivirus_registration: { + enabled: false, + }, }, }, streams: [], diff --git a/x-pack/test/security_solution_endpoint/config.ts b/x-pack/test/security_solution_endpoint/config.ts index 9499c235a5f0d5..f3cb4a5812a5cb 100644 --- a/x-pack/test/security_solution_endpoint/config.ts +++ b/x-pack/test/security_solution_endpoint/config.ts @@ -43,5 +43,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...getRegistryUrlAsArray(), ], }, + layout: { + fixedHeaderHeight: 200, + }, }; } diff --git a/x-pack/typings/elasticsearch/aggregations.d.ts b/x-pack/typings/elasticsearch/aggregations.d.ts index 29c78e93831751..bc9ed447c8717f 100644 --- a/x-pack/typings/elasticsearch/aggregations.d.ts +++ b/x-pack/typings/elasticsearch/aggregations.d.ts @@ -204,6 +204,8 @@ type SubAggregationResponseOf< interface AggregationResponsePart { terms: { + doc_count_error_upper_bound: number; + sum_other_doc_count: number; buckets: Array< { doc_count: number;