From 90b6e4dbe01219d5af51a93943338043bfdd773a Mon Sep 17 00:00:00 2001 From: amyjtechwriter <61687663+amyjtechwriter@users.noreply.github.com> Date: Mon, 21 Aug 2023 14:24:48 +0100 Subject: [PATCH 01/10] [DOCS] Removes 8.9.1 coming tag. (#164302) Removes `coming` tag from the 8.9.1 release notes. --- docs/CHANGELOG.asciidoc | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index 5da373c8d9a51..898597eabce5d 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -49,8 +49,6 @@ Review important information about the {kib} 8.x releases. [[release-notes-8.9.1]] == {kib} 8.9.1 -coming::[8.9.1] - Review the following information about the {kib} 8.9.1 release. [float] From 011ae97061b9f5b5d751fa4a00f19a355c3bb68b Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Mon, 21 Aug 2023 09:51:07 -0400 Subject: [PATCH 02/10] [Dashboard] Remove clone by reference (#164108) Removes all clone by reference functionality on the Dashboard. This means that we no longer have any `savedObjectsClient` usage in the browser side. --- .../clone_panel_action.test.tsx | 171 ++++++------------ .../dashboard_actions/clone_panel_action.tsx | 158 +++++----------- .../public/dashboard_actions/index.ts | 2 +- .../embeddable/api/index.ts | 2 +- .../embeddable/api/panel_management.ts | 54 ------ .../embeddable/dashboard_container.tsx | 2 - .../public/placeholder_embeddable/index.ts | 11 -- .../placeholder_embeddable.tsx | 52 ------ .../placeholder_embeddable_factory.ts | 41 ----- .../public/placeholder_embeddable/readme.md | 13 -- src/plugins/dashboard/public/plugin.tsx | 4 - .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 14 files changed, 105 insertions(+), 408 deletions(-) delete mode 100644 src/plugins/dashboard/public/placeholder_embeddable/index.ts delete mode 100644 src/plugins/dashboard/public/placeholder_embeddable/placeholder_embeddable.tsx delete mode 100644 src/plugins/dashboard/public/placeholder_embeddable/placeholder_embeddable_factory.ts delete mode 100644 src/plugins/dashboard/public/placeholder_embeddable/readme.md diff --git a/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.test.tsx b/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.test.tsx index 6c1e8bc6d680e..5ec0ac57c574b 100644 --- a/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.test.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.test.tsx @@ -16,17 +16,21 @@ import { import { CoreStart } from '@kbn/core/public'; import { coreMock } from '@kbn/core/public/mocks'; import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks'; -import { ErrorEmbeddable, IContainer, isErrorEmbeddable } from '@kbn/embeddable-plugin/public'; +import { + ErrorEmbeddable, + IContainer, + isErrorEmbeddable, + ReferenceOrValueEmbeddable, +} from '@kbn/embeddable-plugin/public'; -import { DashboardPanelState } from '../../common'; import { ClonePanelAction } from './clone_panel_action'; import { pluginServices } from '../services/plugin_services'; import { buildMockDashboard, getSampleDashboardPanel } from '../mocks'; import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container'; let container: DashboardContainer; -let byRefOrValEmbeddable: ContactCardEmbeddable; let genericEmbeddable: ContactCardEmbeddable; +let byRefOrValEmbeddable: ContactCardEmbeddable & ReferenceOrValueEmbeddable; let coreStart: CoreStart; beforeEach(async () => { coreStart = coreMock.createStart(); @@ -58,20 +62,22 @@ beforeEach(async () => { >(CONTACT_CARD_EMBEDDABLE, { firstName: 'RefOrValEmbeddable', }); - const genericContactCardEmbeddable = await container.addNewEmbeddable< + + const nonRefOrValueContactCard = await container.addNewEmbeddable< ContactCardEmbeddableInput, ContactCardEmbeddableOutput, ContactCardEmbeddable >(CONTACT_CARD_EMBEDDABLE, { - firstName: 'NotRefOrValEmbeddable', + firstName: 'Not a refOrValEmbeddable', }); if ( isErrorEmbeddable(refOrValContactCardEmbeddable) || - isErrorEmbeddable(genericContactCardEmbeddable) + isErrorEmbeddable(nonRefOrValueContactCard) ) { throw new Error('Failed to create embeddables'); } else { + genericEmbeddable = nonRefOrValueContactCard; byRefOrValEmbeddable = embeddablePluginMock.mockRefOrValEmbeddable< ContactCardEmbeddable, ContactCardEmbeddableInput @@ -80,14 +86,14 @@ beforeEach(async () => { savedObjectId: 'testSavedObjectId', id: refOrValContactCardEmbeddable.id, }, - mockedByValueInput: { firstName: 'Kibanana', id: refOrValContactCardEmbeddable.id }, + mockedByValueInput: { firstName: 'RefOrValEmbeddable', id: refOrValContactCardEmbeddable.id }, }); - genericEmbeddable = genericContactCardEmbeddable; + jest.spyOn(byRefOrValEmbeddable, 'getInputAsValueType'); } }); test('Clone is incompatible with Error Embeddables', async () => { - const action = new ClonePanelAction(coreStart.savedObjects); + const action = new ClonePanelAction(); const errorEmbeddable = new ErrorEmbeddable('Wow what an awful error', { id: ' 404' }, container); expect(await action.isCompatible({ embeddable: errorEmbeddable })).toBe(false); }); @@ -96,134 +102,65 @@ test('Clone adds a new embeddable', async () => { const dashboard = byRefOrValEmbeddable.getRoot() as IContainer; const originalPanelCount = Object.keys(dashboard.getInput().panels).length; const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels)); - const action = new ClonePanelAction(coreStart.savedObjects); + const action = new ClonePanelAction(); await action.execute({ embeddable: byRefOrValEmbeddable }); + expect(Object.keys(container.getInput().panels).length).toEqual(originalPanelCount + 1); const newPanelId = Object.keys(container.getInput().panels).find( (key) => !originalPanelKeySet.has(key) ); expect(newPanelId).toBeDefined(); const newPanel = container.getInput().panels[newPanelId!]; - expect(newPanel.type).toEqual('placeholder'); - // let the placeholder load - await dashboard.untilEmbeddableLoaded(newPanelId!); - await new Promise((r) => process.nextTick(r)); // Allow the current loop of the event loop to run to completion - // now wait for the full embeddable to replace it - const loadedPanel = await dashboard.untilEmbeddableLoaded(newPanelId!); - expect(loadedPanel.type).toEqual(byRefOrValEmbeddable.type); + expect(newPanel.type).toEqual(byRefOrValEmbeddable.type); }); test('Clones a RefOrVal embeddable by value', async () => { const dashboard = byRefOrValEmbeddable.getRoot() as IContainer; - const panel = dashboard.getInput().panels[byRefOrValEmbeddable.id] as DashboardPanelState; - const action = new ClonePanelAction(coreStart.savedObjects); - // @ts-ignore - const newPanel = await action.cloneEmbeddable(panel, byRefOrValEmbeddable); - expect(coreStart.savedObjects.client.get).toHaveBeenCalledTimes(0); - expect(coreStart.savedObjects.client.find).toHaveBeenCalledTimes(0); - expect(coreStart.savedObjects.client.create).toHaveBeenCalledTimes(0); - expect(newPanel.type).toEqual(byRefOrValEmbeddable.type); -}); + const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels)); + const action = new ClonePanelAction(); + await action.execute({ embeddable: byRefOrValEmbeddable }); + const newPanelId = Object.keys(container.getInput().panels).find( + (key) => !originalPanelKeySet.has(key) + ); -test('Clones a non-RefOrVal embeddable by value if the panel does not have a savedObjectId', async () => { - const dashboard = genericEmbeddable.getRoot() as IContainer; - const panel = dashboard.getInput().panels[genericEmbeddable.id] as DashboardPanelState; - const action = new ClonePanelAction(coreStart.savedObjects); - // @ts-ignore - const newPanelWithoutId = await action.cloneEmbeddable(panel, genericEmbeddable); - expect(coreStart.savedObjects.client.get).toHaveBeenCalledTimes(0); - expect(coreStart.savedObjects.client.find).toHaveBeenCalledTimes(0); - expect(coreStart.savedObjects.client.create).toHaveBeenCalledTimes(0); - expect(newPanelWithoutId.type).toEqual(genericEmbeddable.type); -}); + const originalFirstName = ( + container.getInput().panels[byRefOrValEmbeddable.id].explicitInput as ContactCardEmbeddableInput + ).firstName; -test('Clones a non-RefOrVal embeddable by reference if the panel has a savedObjectId', async () => { - const dashboard = genericEmbeddable.getRoot() as IContainer; - const panel = dashboard.getInput().panels[genericEmbeddable.id] as DashboardPanelState; - panel.explicitInput.savedObjectId = 'holySavedObjectBatman'; - const action = new ClonePanelAction(coreStart.savedObjects); - // @ts-ignore - const newPanel = await action.cloneEmbeddable(panel, genericEmbeddable); - expect(coreStart.savedObjects.client.get).toHaveBeenCalledTimes(1); - expect(coreStart.savedObjects.client.find).toHaveBeenCalledTimes(1); - expect(coreStart.savedObjects.client.create).toHaveBeenCalledTimes(1); - expect(newPanel.type).toEqual(genericEmbeddable.type); + const newFirstName = ( + container.getInput().panels[newPanelId!].explicitInput as ContactCardEmbeddableInput + ).firstName; + + expect(byRefOrValEmbeddable.getInputAsValueType).toHaveBeenCalled(); + + expect(originalFirstName).toEqual(newFirstName); + expect(container.getInput().panels[newPanelId!].type).toEqual(byRefOrValEmbeddable.type); }); -test('Gets a unique title from the saved objects library', async () => { +test('Clones a non RefOrVal embeddable by value', async () => { const dashboard = genericEmbeddable.getRoot() as IContainer; - const panel = dashboard.getInput().panels[genericEmbeddable.id] as DashboardPanelState; - panel.explicitInput.savedObjectId = 'holySavedObjectBatman'; - coreStart.savedObjects.client.find = jest.fn().mockImplementation(({ search }) => { - if (search === '"testFirstClone"') { - return { - savedObjects: [ - { - attributes: { title: 'testFirstClone' }, - get: jest.fn().mockReturnValue('testFirstClone'), - }, - ], - total: 1, - }; - } else if (search === '"testBeforePageLimit"') { - return { - savedObjects: [ - { - attributes: { title: 'testBeforePageLimit (copy 9)' }, - get: jest.fn().mockReturnValue('testBeforePageLimit (copy 9)'), - }, - ], - total: 10, - }; - } else if (search === '"testMaxLogic"') { - return { - savedObjects: [ - { - attributes: { title: 'testMaxLogic (copy 10000)' }, - get: jest.fn().mockReturnValue('testMaxLogic (copy 10000)'), - }, - ], - total: 2, - }; - } else if (search === '"testAfterPageLimit"') { - return { total: 11 }; - } - }); - - const action = new ClonePanelAction(coreStart.savedObjects); - // @ts-ignore - expect(await action.getCloneTitle(genericEmbeddable, 'testFirstClone')).toEqual( - 'testFirstClone (copy)' - ); - // @ts-ignore - expect(await action.getCloneTitle(genericEmbeddable, 'testBeforePageLimit')).toEqual( - 'testBeforePageLimit (copy 10)' - ); - // @ts-ignore - expect(await action.getCloneTitle(genericEmbeddable, 'testBeforePageLimit (copy 9)')).toEqual( - 'testBeforePageLimit (copy 10)' - ); - // @ts-ignore - expect(await action.getCloneTitle(genericEmbeddable, 'testMaxLogic')).toEqual( - 'testMaxLogic (copy 10001)' - ); - // @ts-ignore - expect(await action.getCloneTitle(genericEmbeddable, 'testAfterPageLimit')).toEqual( - 'testAfterPageLimit (copy 11)' - ); - // @ts-ignore - expect(await action.getCloneTitle(genericEmbeddable, 'testAfterPageLimit (copy 10)')).toEqual( - 'testAfterPageLimit (copy 11)' - ); - // @ts-ignore - expect(await action.getCloneTitle(genericEmbeddable, 'testAfterPageLimit (copy 10000)')).toEqual( - 'testAfterPageLimit (copy 11)' + const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels)); + const action = new ClonePanelAction(); + await action.execute({ embeddable: genericEmbeddable }); + const newPanelId = Object.keys(container.getInput().panels).find( + (key) => !originalPanelKeySet.has(key) ); + + const originalFirstName = ( + container.getInput().panels[genericEmbeddable.id].explicitInput as ContactCardEmbeddableInput + ).firstName; + + const newFirstName = ( + container.getInput().panels[newPanelId!].explicitInput as ContactCardEmbeddableInput + ).firstName; + + expect(originalFirstName).toEqual(newFirstName); + expect(container.getInput().panels[newPanelId!].type).toEqual(genericEmbeddable.type); }); test('Gets a unique title from the dashboard', async () => { - const dashboard = genericEmbeddable.getRoot() as DashboardContainer; - const action = new ClonePanelAction(coreStart.savedObjects); + const dashboard = byRefOrValEmbeddable.getRoot() as DashboardContainer; + const action = new ClonePanelAction(); // @ts-ignore expect(await action.getCloneTitle(byRefOrValEmbeddable, '')).toEqual(''); diff --git a/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx b/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx index cd65f3b9cff45..e028d8f387312 100644 --- a/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx @@ -6,11 +6,8 @@ * Side Public License, v 1. */ -import _ from 'lodash'; import { v4 as uuidv4 } from 'uuid'; - -// TODO Remove this usage of the SavedObjectsStart contract. -import { SavedObjectsStart } from '@kbn/core/public'; +import { filter, map, max } from 'lodash'; import { ViewMode, @@ -18,21 +15,17 @@ import { IEmbeddable, PanelNotFoundError, EmbeddableInput, - SavedObjectEmbeddableInput, isErrorEmbeddable, isReferenceOrValueEmbeddable, } from '@kbn/embeddable-plugin/public'; import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; -import type { SavedObject } from '@kbn/saved-objects-plugin/public'; -import { - placePanelBeside, - IPanelPlacementBesideArgs, -} from '../dashboard_container/component/panel/dashboard_panel_placement'; import { type DashboardPanelState } from '../../common'; import { pluginServices } from '../services/plugin_services'; +import { createPanelState } from '../dashboard_container/component/panel'; import { dashboardClonePanelActionStrings } from './_dashboard_actions_strings'; import { DASHBOARD_CONTAINER_TYPE, type DashboardContainer } from '../dashboard_container'; +import { placePanelBeside } from '../dashboard_container/component/panel/dashboard_panel_placement'; export const ACTION_CLONE_PANEL = 'clonePanel'; @@ -47,7 +40,7 @@ export class ClonePanelAction implements Action { private toastsService; - constructor(private savedObjects: SavedObjectsStart) { + constructor() { ({ notifications: { toasts: this.toastsService }, } = pluginServices.getServices()); @@ -89,8 +82,37 @@ export class ClonePanelAction implements Action { throw new PanelNotFoundError(); } - dashboard.showPlaceholderUntil( - this.cloneEmbeddable(panelToClone, embeddable), + const clonedPanelState: PanelState = await (async () => { + const newTitle = await this.getCloneTitle(embeddable, embeddable.getTitle() || ''); + const id = uuidv4(); + if (isReferenceOrValueEmbeddable(embeddable)) { + return { + type: embeddable.type, + explicitInput: { + ...(await embeddable.getInputAsValueType()), + hidePanelTitles: panelToClone.explicitInput.hidePanelTitles, + title: newTitle, + id, + }, + }; + } + return { + type: embeddable.type, + explicitInput: { + ...panelToClone.explicitInput, + title: newTitle, + id, + }, + }; + })(); + this.toastsService.addSuccess({ + title: dashboardClonePanelActionStrings.getSuccessMessage(), + 'data-test-subj': 'addObjectToContainerSuccess', + }); + + const { otherPanels, newPanel } = createPanelState( + clonedPanelState, + dashboard.getInput().panels, placePanelBeside, { width: panelToClone.gridData.w, @@ -98,8 +120,15 @@ export class ClonePanelAction implements Action { currentPanels: dashboard.getInput().panels, placeBesideId: panelToClone.explicitInput.id, scrollToPanel: true, - } as IPanelPlacementBesideArgs + } ); + + dashboard.updateInput({ + panels: { + ...otherPanels, + [newPanel.explicitInput.id]: newPanel, + }, + }); } private async getCloneTitle(embeddable: IEmbeddable, rawTitle: string) { @@ -109,109 +138,20 @@ export class ClonePanelAction implements Action { const cloneRegex = new RegExp(`\\(${clonedTag}\\)`, 'g'); const cloneNumberRegex = new RegExp(`\\(${clonedTag} [0-9]+\\)`, 'g'); const baseTitle = rawTitle.replace(cloneNumberRegex, '').replace(cloneRegex, '').trim(); - let similarTitles: string[]; - if ( - isReferenceOrValueEmbeddable(embeddable) || - !_.has(embeddable.getExplicitInput(), 'savedObjectId') - ) { - const dashboard: DashboardContainer = embeddable.getRoot() as DashboardContainer; - similarTitles = _.filter(await dashboard.getPanelTitles(), (title: string) => { - return title.startsWith(baseTitle); - }); - } else { - const perPage = 10; - const similarSavedObjects = await this.savedObjects.client.find({ - type: embeddable.type, - perPage, - fields: ['title'], - searchFields: ['title'], - search: `"${baseTitle}"`, - }); - if (similarSavedObjects.total <= perPage) { - similarTitles = similarSavedObjects.savedObjects.map((savedObject) => { - return savedObject.get('title'); - }); - } else { - similarTitles = [baseTitle + ` (${clonedTag} ${similarSavedObjects.total - 1})`]; - } - } + const dashboard: DashboardContainer = embeddable.getRoot() as DashboardContainer; + const similarTitles = filter(await dashboard.getPanelTitles(), (title: string) => { + return title.startsWith(baseTitle); + }); - const cloneNumbers = _.map(similarTitles, (title: string) => { + const cloneNumbers = map(similarTitles, (title: string) => { if (title.match(cloneRegex)) return 0; const cloneTag = title.match(cloneNumberRegex); return cloneTag ? parseInt(cloneTag[0].replace(/[^0-9.]/g, ''), 10) : -1; }); - const similarBaseTitlesCount = _.max(cloneNumbers) || 0; + const similarBaseTitlesCount = max(cloneNumbers) || 0; return similarBaseTitlesCount < 0 ? baseTitle + ` (${clonedTag})` : baseTitle + ` (${clonedTag} ${similarBaseTitlesCount + 1})`; } - - private async addCloneToLibrary( - embeddable: IEmbeddable, - objectIdToClone: string - ): Promise { - // TODO: Remove this entire functionality. See https://github.com/elastic/kibana/issues/158632 for more info. - const savedObjectToClone = await this.savedObjects.client.get( - embeddable.type, - objectIdToClone - ); - - // Clone the saved object - const newTitle = await this.getCloneTitle(embeddable, savedObjectToClone.attributes.title); - const clonedSavedObject = await this.savedObjects.client.create( - embeddable.type, - { - ..._.cloneDeep(savedObjectToClone.attributes), - title: newTitle, - }, - { references: _.cloneDeep(savedObjectToClone.references) } - ); - return clonedSavedObject.id; - } - - private async cloneEmbeddable( - panelToClone: DashboardPanelState, - embeddable: IEmbeddable - ): Promise> { - let panelState: PanelState; - if (isReferenceOrValueEmbeddable(embeddable)) { - const newTitle = await this.getCloneTitle(embeddable, embeddable.getTitle() || ''); - panelState = { - type: embeddable.type, - explicitInput: { - ...(await embeddable.getInputAsValueType()), - id: uuidv4(), - title: newTitle, - hidePanelTitles: panelToClone.explicitInput.hidePanelTitles, - }, - version: panelToClone.version, - }; - } else { - panelState = { - type: embeddable.type, - explicitInput: { - ...panelToClone.explicitInput, - id: uuidv4(), - }, - version: panelToClone.version, - }; - - // TODO Remove the entire `addCloneToLibrary` section from here. - if (panelToClone.explicitInput.savedObjectId) { - const clonedSavedObjectId = await this.addCloneToLibrary( - embeddable, - panelToClone.explicitInput.savedObjectId - ); - (panelState.explicitInput as SavedObjectEmbeddableInput).savedObjectId = - clonedSavedObjectId; - } - } - this.toastsService.addSuccess({ - title: dashboardClonePanelActionStrings.getSuccessMessage(), - 'data-test-subj': 'addObjectToContainerSuccess', - }); - return panelState; - } } diff --git a/src/plugins/dashboard/public/dashboard_actions/index.ts b/src/plugins/dashboard/public/dashboard_actions/index.ts index 8376ee1e65171..4f0daff8c3390 100644 --- a/src/plugins/dashboard/public/dashboard_actions/index.ts +++ b/src/plugins/dashboard/public/dashboard_actions/index.ts @@ -34,7 +34,7 @@ export const buildAllDashboardActions = async ({ }: BuildAllDashboardActionsProps) => { const { uiActions, share, presentationUtil, savedObjectsTaggingOss, contentManagement } = plugins; - const clonePanelAction = new ClonePanelAction(core.savedObjects); + const clonePanelAction = new ClonePanelAction(); uiActions.registerAction(clonePanelAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, clonePanelAction.id); diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/api/index.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/api/index.ts index 2a2d20cc5da14..a97d038d89d95 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/api/index.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/api/index.ts @@ -9,4 +9,4 @@ export { showSettings } from './show_settings'; export { addFromLibrary } from './add_panel_from_library'; export { runSaveAs, runQuickSave, runClone } from './run_save_functions'; -export { addOrUpdateEmbeddable, replacePanel, showPlaceholderUntil } from './panel_management'; +export { addOrUpdateEmbeddable, replacePanel } from './panel_management'; diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/api/panel_management.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/api/panel_management.ts index 7b02001a93c6c..848600a2767d6 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/api/panel_management.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/api/panel_management.ts @@ -14,14 +14,8 @@ import { } from '@kbn/embeddable-plugin/public'; import { v4 as uuidv4 } from 'uuid'; -import { - IPanelPlacementArgs, - PanelPlacementMethod, -} from '../../component/panel/dashboard_panel_placement'; import { DashboardPanelState } from '../../../../common'; -import { createPanelState } from '../../component/panel'; import { DashboardContainer } from '../dashboard_container'; -import { PLACEHOLDER_EMBEDDABLE } from '../../../placeholder_embeddable'; export async function addOrUpdateEmbeddable< EEI extends EmbeddableInput = EmbeddableInput, @@ -89,51 +83,3 @@ export async function replacePanel( await this.updateInput({ panels }); return panelId; } - -export function showPlaceholderUntil( - this: DashboardContainer, - newStateComplete: Promise>, - placementMethod?: PanelPlacementMethod, - placementArgs?: TPlacementMethodArgs -): void { - const originalPanelState = { - type: PLACEHOLDER_EMBEDDABLE, - explicitInput: { - id: uuidv4(), - disabledActions: [ - 'ACTION_CUSTOMIZE_PANEL', - 'CUSTOM_TIME_RANGE', - 'clonePanel', - 'replacePanel', - 'togglePanel', - ], - }, - } as PanelState; - - const { otherPanels, newPanel: placeholderPanelState } = createPanelState( - originalPanelState, - this.input.panels, - placementMethod, - placementArgs - ); - - this.updateInput({ - panels: { - ...otherPanels, - [placeholderPanelState.explicitInput.id]: placeholderPanelState, - }, - }); - - // wait until the placeholder is ready, then replace it with new panel - // this is useful as sometimes panels can load faster than the placeholder one (i.e. by value embeddables) - this.untilEmbeddableLoaded(originalPanelState.explicitInput.id) - .then(() => newStateComplete) - .then(async (newPanelState: Partial) => { - const panelId = await this.replacePanel(placeholderPanelState, newPanelState); - - if (placementArgs?.scrollToPanel) { - this.setScrollToPanelId(panelId); - this.setHighlightPanelId(panelId); - } - }); -} diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx index 636634d23099f..df0e728a16d1b 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx @@ -41,7 +41,6 @@ import { runQuickSave, replacePanel, addFromLibrary, - showPlaceholderUntil, addOrUpdateEmbeddable, } from './api'; @@ -312,7 +311,6 @@ export class DashboardContainer extends Container -
- -
- , - node - ); - } - - public reload() {} -} diff --git a/src/plugins/dashboard/public/placeholder_embeddable/placeholder_embeddable_factory.ts b/src/plugins/dashboard/public/placeholder_embeddable/placeholder_embeddable_factory.ts deleted file mode 100644 index 26cdddbf17d85..0000000000000 --- a/src/plugins/dashboard/public/placeholder_embeddable/placeholder_embeddable_factory.ts +++ /dev/null @@ -1,41 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; - -import { - EmbeddableFactoryDefinition, - EmbeddableInput, - IContainer, -} from '@kbn/embeddable-plugin/public'; -import { PLACEHOLDER_EMBEDDABLE } from '.'; - -export class PlaceholderEmbeddableFactory implements EmbeddableFactoryDefinition { - public readonly type = PLACEHOLDER_EMBEDDABLE; - - constructor() {} - - public async isEditable() { - return false; - } - - public canCreateNew() { - return false; - } - - public async create(initialInput: EmbeddableInput, parent?: IContainer) { - const { PlaceholderEmbeddable } = await import('./placeholder_embeddable'); - return new PlaceholderEmbeddable(initialInput, parent); - } - - public getDisplayName() { - return i18n.translate('dashboard.placeholder.factory.displayName', { - defaultMessage: 'placeholder', - }); - } -} diff --git a/src/plugins/dashboard/public/placeholder_embeddable/readme.md b/src/plugins/dashboard/public/placeholder_embeddable/readme.md deleted file mode 100644 index 5bdb0569c50f6..0000000000000 --- a/src/plugins/dashboard/public/placeholder_embeddable/readme.md +++ /dev/null @@ -1,13 +0,0 @@ -## What is this for? - -This Placeholder Embeddable is shown when a BY REFERENCE panel (a panel which is linked to a saved object) is cloned using the Dashboard Panel Clone action. - -## Why was it made? - -This was important for the first iteration of the clone feature so that something could be shown while the saved object was being duplicated, but later iterations of that feature automatically unlink panels on clone. By Value panels don't need a placeholder because they load much faster. - -## Can I delete it? - -Currently, the only embeddable type that cannot be loaded by value is the Discover Saved Search Embeddable. Without a placeholder embeddable, the dashboard wouldn't reflow at all until after the saved object clone operation is complete. - -The placeholder embeddable should be removed as soon as the Discover Saved Search Embeddable can be saved By Value. diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index b42a13f858965..a28dbe9c45ae7 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -67,7 +67,6 @@ import { SEARCH_SESSION_ID, } from './dashboard_constants'; import { DashboardMountContextProps } from './dashboard_app/types'; -import { PlaceholderEmbeddableFactory } from './placeholder_embeddable'; import type { FindDashboardsService } from './services/dashboard_content_management/types'; import { CONTENT_ID, LATEST_VERSION } from '../common/content_management'; @@ -220,9 +219,6 @@ export class DashboardPlugin dashboardContainerFactory.type, dashboardContainerFactory ); - - const placeholderFactory = new PlaceholderEmbeddableFactory(); - embeddable.registerEmbeddableFactory(placeholderFactory.type, placeholderFactory); }); this.stopUrlTracking = () => { diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index b54778a7ae73a..22ee2086a1a33 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -1225,7 +1225,6 @@ "dashboard.panel.title.clonedTag": "copier", "dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage": "Impossible de migrer les données du panneau pour une rétro-compatibilité \"6.1.0\". Le panneau ne contient pas les champs de colonne et/ou de ligne attendus.", "dashboard.panel.unlinkFromLibrary": "Dissocier de la bibliothèque", - "dashboard.placeholder.factory.displayName": "paramètre fictif", "dashboard.resetChangesConfirmModal.confirmButtonLabel": "Réinitialiser le tableau de bord", "dashboard.resetChangesConfirmModal.resetChangesDescription": "Ce tableau de bord va revenir à son dernier état d'enregistrement. Vous risquez de perdre les modifications apportées aux filtres et aux requêtes.", "dashboard.resetChangesConfirmModal.resetChangesTitle": "Réinitialiser le tableau de bord ?", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a2a5719e9fa21..84c71c20e176d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1239,7 +1239,6 @@ "dashboard.panel.title.clonedTag": "コピー", "dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage": "「6.1.0」のダッシュボードの互換性のため、パネルデータを移行できませんでした。パネルには想定された列または行フィールドがありません", "dashboard.panel.unlinkFromLibrary": "ライブラリからのリンクを解除", - "dashboard.placeholder.factory.displayName": "プレースホルダー", "dashboard.resetChangesConfirmModal.confirmButtonLabel": "ダッシュボードをリセット", "dashboard.resetChangesConfirmModal.resetChangesDescription": "このダッシュボードは最後に保存された状態に戻ります。 フィルターとクエリの変更が失われる場合があります。", "dashboard.resetChangesConfirmModal.resetChangesTitle": "ダッシュボードをリセットしますか?", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index b65369d942c92..c15e2f639c597 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1239,7 +1239,6 @@ "dashboard.panel.title.clonedTag": "副本", "dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage": "无法迁移用于“6.1.0”向后兼容的面板数据,面板不包含所需的列和/或行字段", "dashboard.panel.unlinkFromLibrary": "取消与库的链接", - "dashboard.placeholder.factory.displayName": "占位符", "dashboard.resetChangesConfirmModal.confirmButtonLabel": "重置仪表板", "dashboard.resetChangesConfirmModal.resetChangesDescription": "此仪表板将返回到其最后保存的状态。 您可能会丢失对筛选和查询的更改。", "dashboard.resetChangesConfirmModal.resetChangesTitle": "重置仪表板?", From 88bd71c0773d158ed1e6312075633ed85abc575e Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Mon, 21 Aug 2023 10:00:53 -0400 Subject: [PATCH 03/10] [Security Solution] File paths for Blocklist Windows and Mac should be case insensitive (#164200) ## Summary This fixes a bug where Windows and Mac Blocklist file path entries should be passed as case insensitive. This is because Mac and Windows are caseless for most use cases. Bug ticket: https://github.com/elastic/kibana/issues/158581 Here is how it will be displayed in the UI: image Here are the breakdown of the artifacts after the fix: Linux: ``` ------------------------------------------------------------------- Policy: Protect Manifest: 1.0.6 | v1 Artifact: endpoint-blocklist-linux-v1 Relative URL: /api/fleet/artifacts/endpoint-blocklist-linux-v1/f33e6890aeced00861c26a08121dd42d2d29ba08abfeb3c065d0447e32e18640 Encoded SHA256: a907835be40af89b8b7aa23a6efc66c01ceaa5a19622edd378139319f3ca5fa0 Decoded SHA256: f33e6890aeced00861c26a08121dd42d2d29ba08abfeb3c065d0447e32e18640 ------------------------------------------------------------------- { "entries": [ { "type": "simple", "entries": [ { "field": "file.path", "operator": "included", "type": "exact_cased_any", "value": [ "/opt/bin/bin.exe" ] } ] } ] } ``` Mac: ``` ------------------------------------------------------------------- Policy: Protect Manifest: 1.0.6 | v1 Artifact: endpoint-blocklist-macos-v1 Relative URL: /api/fleet/artifacts/endpoint-blocklist-macos-v1/b28e7978da4314ebc2c94770e0638fc4b2270f9dc17a11d6d32b8634b1fbec0f Encoded SHA256: 4f3e80d688f5cae4bf6a88b0704e37909f9fa4f47fe8325b7b154cddd46a2db9 Decoded SHA256: b28e7978da4314ebc2c94770e0638fc4b2270f9dc17a11d6d32b8634b1fbec0f ------------------------------------------------------------------- { "entries": [ { "type": "simple", "entries": [ { "field": "file.path", "operator": "included", "type": "exact_caseless_any", "value": [ "/opt/exe.exe" ] } ] } ``` Windows: ``` ------------------------------------------------------------------- Policy: Protect Manifest: 1.0.6 | v1 Artifact: endpoint-blocklist-windows-v1 Relative URL: /api/fleet/artifacts/endpoint-blocklist-windows-v1/2a6fcc67c696ad4e29d91f8b685bff46977198cd34b9a61e8003d55b78dff6ac Encoded SHA256: c6e045fce97651336eeb400f0123541475b940e3aa38ce721f299585683da288 Decoded SHA256: 2a6fcc67c696ad4e29d91f8b685bff46977198cd34b9a61e8003d55b78dff6ac ------------------------------------------------------------------- { "entries": [ { "type": "simple", "entries": [ { "field": "file.path", "operator": "included", "type": "exact_caseless_any", "value": [ "C:\\path\\path.exe" ] } ] } ] } ``` ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../src/path_validations/index.ts | 6 +++- .../cypress/fixtures/artifacts_page.ts | 4 +-- .../pages/blocklist/translations.ts | 12 +++++++ .../view/components/blocklist_form.test.tsx | 32 ++++++++++++++++++ .../view/components/blocklist_form.tsx | 33 ++++++++++++++----- .../validators/blocklist_validator.ts | 6 +++- .../apps/integrations/mocks.ts | 4 +-- 7 files changed, 83 insertions(+), 14 deletions(-) diff --git a/packages/kbn-securitysolution-utils/src/path_validations/index.ts b/packages/kbn-securitysolution-utils/src/path_validations/index.ts index ba17757589613..ac7c17426e723 100644 --- a/packages/kbn-securitysolution-utils/src/path_validations/index.ts +++ b/packages/kbn-securitysolution-utils/src/path_validations/index.ts @@ -33,7 +33,11 @@ export type TrustedAppConditionEntryField = | 'process.hash.*' | 'process.executable.caseless' | 'process.Ext.code_signature'; -export type BlocklistConditionEntryField = 'file.hash.*' | 'file.path' | 'file.Ext.code_signature'; +export type BlocklistConditionEntryField = + | 'file.hash.*' + | 'file.path' + | 'file.Ext.code_signature' + | 'file.path.caseless'; export type AllConditionEntryFields = | TrustedAppConditionEntryField | BlocklistConditionEntryField diff --git a/x-pack/plugins/security_solution/public/management/cypress/fixtures/artifacts_page.ts b/x-pack/plugins/security_solution/public/management/cypress/fixtures/artifacts_page.ts index 77490b84d0773..e6c3c941482a9 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/fixtures/artifacts_page.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/fixtures/artifacts_page.ts @@ -358,7 +358,7 @@ export const getArtifactsListTestsData = (): ArtifactsFixtureType[] => [ }, { type: 'click', - selector: 'blocklist-form-file.path', + selector: 'blocklist-form-file.path.caseless', }, { type: 'click', @@ -379,7 +379,7 @@ export const getArtifactsListTestsData = (): ArtifactsFixtureType[] => [ { selector: 'blocklistPage-card-criteriaConditions', value: - 'OSIS WindowsAND file.pathis one of\nc:\\randomFolder\\randomFile.exe\nc:\\randomFolder\\randomFile2.exe', + 'OSIS WindowsAND file.path.caselessis one of\nc:\\randomFolder\\randomFile.exe\nc:\\randomFolder\\randomFile2.exe', }, { selector: 'blocklistPage-card-header-title', diff --git a/x-pack/plugins/security_solution/public/management/pages/blocklist/translations.ts b/x-pack/plugins/security_solution/public/management/pages/blocklist/translations.ts index c084c9443ba5d..60e87de41bf21 100644 --- a/x-pack/plugins/security_solution/public/management/pages/blocklist/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/blocklist/translations.ts @@ -76,6 +76,12 @@ export const CONDITION_FIELD_TITLE: { [K in BlocklistConditionEntryField]: strin 'file.path': i18n.translate('xpack.securitySolution.blocklist.entry.field.path', { defaultMessage: 'Path', }), + 'file.path.caseless': i18n.translate( + 'xpack.securitySolution.blocklist.entry.field.path.caseless', + { + defaultMessage: 'Path', + } + ), 'file.Ext.code_signature': i18n.translate( 'xpack.securitySolution.blocklist.entry.field.signature', { defaultMessage: 'Signature' } @@ -89,6 +95,12 @@ export const CONDITION_FIELD_DESCRIPTION: { [K in BlocklistConditionEntryField]: 'file.path': i18n.translate('xpack.securitySolution.blocklist.entry.field.description.path', { defaultMessage: 'The full path of the application', }), + 'file.path.caseless': i18n.translate( + 'xpack.securitySolution.blocklist.entry.field.description.path.caseless', + { + defaultMessage: 'The full path of the application (case insenstive)', + } + ), 'file.Ext.code_signature': i18n.translate( 'xpack.securitySolution.blocklist.entry.field.description.signature', { defaultMessage: 'The signer of the application' } diff --git a/x-pack/plugins/security_solution/public/management/pages/blocklist/view/components/blocklist_form.test.tsx b/x-pack/plugins/security_solution/public/management/pages/blocklist/view/components/blocklist_form.test.tsx index 5ca42ce213448..e07dc6f2d081b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/blocklist/view/components/blocklist_form.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/blocklist/view/components/blocklist_form.test.tsx @@ -225,6 +225,38 @@ describe('blocklist form', () => { userEvent.click(screen.getByRole('option', { name: /path/i })); const expected = createOnChangeArgs({ item: createItem({ + entries: [createEntry('file.path.caseless', [])], + }), + }); + expect(onChangeSpy).toHaveBeenCalledWith(expected); + }); + + it('should correctly create `file.path.caseless` when Mac OS is selected', async () => { + render(createProps({ item: createItem({ os_types: [OperatingSystem.MAC] }) })); + expect(screen.getByTestId('blocklist-form-os-select').textContent).toEqual('Mac'); + + userEvent.click(screen.getByTestId('blocklist-form-field-select')); + await waitForEuiPopoverOpen(); + userEvent.click(screen.getByRole('option', { name: /path/i })); + const expected = createOnChangeArgs({ + item: createItem({ + os_types: [OperatingSystem.MAC], + entries: [createEntry('file.path.caseless', [])], + }), + }); + expect(onChangeSpy).toHaveBeenCalledWith(expected); + }); + + it('should correctly create `file.path` when Linux is selected', async () => { + render(createProps({ item: createItem({ os_types: [OperatingSystem.LINUX] }) })); + expect(screen.getByTestId('blocklist-form-os-select').textContent).toEqual('Linux'); + + userEvent.click(screen.getByTestId('blocklist-form-field-select')); + await waitForEuiPopoverOpen(); + userEvent.click(screen.getByRole('option', { name: /path/i })); + const expected = createOnChangeArgs({ + item: createItem({ + os_types: [OperatingSystem.LINUX], entries: [createEntry('file.path', [])], }), }); diff --git a/x-pack/plugins/security_solution/public/management/pages/blocklist/view/components/blocklist_form.tsx b/x-pack/plugins/security_solution/public/management/pages/blocklist/view/components/blocklist_form.tsx index 612bfdb1b2761..f819a55690563 100644 --- a/x-pack/plugins/security_solution/public/management/pages/blocklist/view/components/blocklist_form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/blocklist/view/components/blocklist_form.tsx @@ -178,14 +178,31 @@ export const BlockListForm = memo( ); const fieldOptions: Array> = useMemo(() => { - const selectableFields: Array> = ( - ['file.hash.*', 'file.path'] as BlocklistConditionEntryField[] - ).map((field) => ({ - value: field, - inputDisplay: CONDITION_FIELD_TITLE[field], - dropdownDisplay: getDropdownDisplay(field), - 'data-test-subj': getTestId(field), - })); + const selectableFields: Array> = []; + + selectableFields.push({ + value: 'file.hash.*', + inputDisplay: CONDITION_FIELD_TITLE['file.hash.*'], + dropdownDisplay: getDropdownDisplay('file.hash.*'), + 'data-test-subj': getTestId('file.hash.*'), + }); + + if (selectedOs === OperatingSystem.LINUX) { + selectableFields.push({ + value: 'file.path', + inputDisplay: CONDITION_FIELD_TITLE['file.path'], + dropdownDisplay: getDropdownDisplay('file.path'), + 'data-test-subj': getTestId('file.path'), + }); + } else { + selectableFields.push({ + value: 'file.path.caseless', + inputDisplay: CONDITION_FIELD_TITLE['file.path.caseless'], + dropdownDisplay: getDropdownDisplay('file.path.caseless'), + 'data-test-subj': getTestId('file.path.caseless'), + }); + } + if (selectedOs === OperatingSystem.WINDOWS) { selectableFields.push({ value: 'file.Ext.code_signature', diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/blocklist_validator.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/blocklist_validator.ts index 0a7c29bb67c2b..3841c6f0e9b67 100644 --- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/blocklist_validator.ts +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/blocklist_validator.ts @@ -21,12 +21,16 @@ import { isValidHash } from '../../../../common/endpoint/service/artifacts/valid import { EndpointArtifactExceptionValidationError } from './errors'; const allowedHashes: Readonly = ['file.hash.md5', 'file.hash.sha1', 'file.hash.sha256']; +const allowedFilePaths: Readonly = ['file.path', 'file.path.caseless']; const FileHashField = schema.oneOf( allowedHashes.map((hash) => schema.literal(hash)) as [Type] ); -const FilePath = schema.literal('file.path'); +const FilePath = schema.oneOf( + allowedFilePaths.map((path) => schema.literal(path)) as [Type] +); + const FileCodeSigner = schema.literal('file.Ext.code_signature'); const ConditionEntryTypeSchema = schema.literal('match_any'); diff --git a/x-pack/test/security_solution_endpoint/apps/integrations/mocks.ts b/x-pack/test/security_solution_endpoint/apps/integrations/mocks.ts index d8cdbf5a6f6b6..1d5ab43c28a32 100644 --- a/x-pack/test/security_solution_endpoint/apps/integrations/mocks.ts +++ b/x-pack/test/security_solution_endpoint/apps/integrations/mocks.ts @@ -423,7 +423,7 @@ export const getArtifactsListTestsData = () => [ { selector: 'blocklistPage-card-criteriaConditions', value: - 'OSIS Windows\nAND file.pathIS ONE OF\nc:\\randomFolder\\randomFile.exe\nc:\\randomFolder\\randomFile2.exe', + 'OSIS Windows\nAND file.path.caselessIS ONE OF\nc:\\randomFolder\\randomFile.exe\nc:\\randomFolder\\randomFile2.exe', }, { selector: 'blocklistPage-card-header-title', @@ -499,7 +499,7 @@ export const getArtifactsListTestsData = () => [ { field: 'file.path', operator: 'included', - type: 'exact_cased_any', + type: 'exact_caseless_any', value: ['c:\\randomFolder\\randomFile.exe', ' c:\\randomFolder\\randomFile2.exe'], }, ], From 75d351052dbc5f5a35985a2bf58e83c459a35c9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Mon, 21 Aug 2023 16:15:30 +0200 Subject: [PATCH 04/10] It adds 8.10 into the .backportrc config file (#164259) ## Summary It adds 8.10 into the .backportrc config file Co-authored-by: Tiago Costa --- .backportrc.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.backportrc.json b/.backportrc.json index f6328c251d7e2..e4b7db154d711 100644 --- a/.backportrc.json +++ b/.backportrc.json @@ -3,6 +3,7 @@ "repoName": "kibana", "targetBranchChoices": [ "main", + "8.10", "8.9", "8.8", "8.7", @@ -46,7 +47,7 @@ "backport" ], "branchLabelMapping": { - "^v8.10.0$": "main", + "^v8.11.0$": "main", "^v(\\d+).(\\d+).\\d+$": "$1.$2" }, "autoMerge": true, From d34c845955e2cb1cd6bae630ac3d0d551544f916 Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Mon, 21 Aug 2023 16:31:21 +0200 Subject: [PATCH 05/10] [Security Solution] Fix value lists tests flakiness (#164253) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Fixes:** https://github.com/elastic/kibana/issues/164056 ## Summary This PR fixes [value_lists.cy.ts](https://github.com/elastic/kibana/blob/main/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/value_lists/value_lists.cy.ts) tests flakiness. ## The flakiness reason Value list items are processed in a bulk via bulk creation and `refresh=wait_for` is [used](https://github.com/elastic/kibana/blob/main/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts#L87). The problem it returns sometimes earlier than data is available. [Bulk API docs](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html#bulk-refresh) say the following > Only the shards that receive the bulk request will be affected by refresh. Imagine a _bulk?refresh=wait_for request with three documents in it that happen to be routed to different shards in an index with five shards. The request will only wait for those three shards to refresh. The other two shards that make up the index do not participate in the _bulk request at all. While (it seems) only one shard is used in tests but it still cause issues (approx. 1 test per 50 fails) so adding explicit index refresh helps to get rid of flakiness. ## Flaky test runner [value_lists.cy.ts (150 runs)](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/2924) 🟢 --- .../value_lists/value_lists.cy.ts | 126 +++++++++++------- .../cypress/tasks/lists.ts | 35 +---- 2 files changed, 84 insertions(+), 77 deletions(-) diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/value_lists/value_lists.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/value_lists/value_lists.cy.ts index 60b0b02d79726..697ccadbdbc7f 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/value_lists/value_lists.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/value_lists/value_lists.cy.ts @@ -17,33 +17,35 @@ import { selectValueListsFile, uploadValueList, selectValueListType, - deleteAllValueListsFromUI, closeValueListsModal, importValueList, deleteValueListsFile, exportValueList, waitForListsIndex, + deleteValueLists, } from '../../../tasks/lists'; import { VALUE_LISTS_TABLE, VALUE_LISTS_ROW, VALUE_LISTS_MODAL_ACTIVATOR, } from '../../../screens/lists'; +import { refreshIndex } from '../../../tasks/api_calls/elasticsearch'; + +const TEXT_LIST_FILE_NAME = 'value_list.txt'; +const IPS_LIST_FILE_NAME = 'ip_list.txt'; +const CIDRS_LIST_FILE_NAME = 'cidr_list.txt'; describe('value lists', () => { describe('management modal', { tags: [tag.ESS, tag.SERVERLESS] }, () => { beforeEach(() => { login(); + deleteValueLists([TEXT_LIST_FILE_NAME, IPS_LIST_FILE_NAME, CIDRS_LIST_FILE_NAME]); createListsIndex(); visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); waitForListsIndex(); waitForValueListsModalToBeLoaded(); }); - afterEach(() => { - deleteAllValueListsFromUI(); - }); - it('can open and close the modal', () => { openValueListsModal(); closeValueListsModal(); @@ -55,57 +57,53 @@ describe('value lists', () => { }); it('creates a "keyword" list from an uploaded file', () => { - const listName = 'value_list.txt'; selectValueListType('keyword'); - selectValueListsFile(listName); + selectValueListsFile(TEXT_LIST_FILE_NAME); uploadValueList(); cy.get(VALUE_LISTS_TABLE) .find(VALUE_LISTS_ROW) .should(($row) => { - expect($row.text()).to.contain(listName); + expect($row.text()).to.contain(TEXT_LIST_FILE_NAME); expect($row.text()).to.contain('Keywords'); }); }); it('creates a "text" list from an uploaded file', () => { - const listName = 'value_list.txt'; selectValueListType('text'); - selectValueListsFile(listName); + selectValueListsFile(TEXT_LIST_FILE_NAME); uploadValueList(); cy.get(VALUE_LISTS_TABLE) .find(VALUE_LISTS_ROW) .should(($row) => { - expect($row.text()).to.contain(listName); + expect($row.text()).to.contain(TEXT_LIST_FILE_NAME); expect($row.text()).to.contain('Text'); }); }); it('creates a "ip" list from an uploaded file', () => { - const listName = 'ip_list.txt'; selectValueListType('ip'); - selectValueListsFile(listName); + selectValueListsFile(IPS_LIST_FILE_NAME); uploadValueList(); cy.get(VALUE_LISTS_TABLE) .find(VALUE_LISTS_ROW) .should(($row) => { - expect($row.text()).to.contain(listName); + expect($row.text()).to.contain(IPS_LIST_FILE_NAME); expect($row.text()).to.contain('IP addresses'); }); }); it('creates a "ip_range" list from an uploaded file', () => { - const listName = 'cidr_list.txt'; selectValueListType('ip_range'); - selectValueListsFile(listName); + selectValueListsFile(CIDRS_LIST_FILE_NAME); uploadValueList(); cy.get(VALUE_LISTS_TABLE) .find(VALUE_LISTS_ROW) .should(($row) => { - expect($row.text()).to.contain(listName); + expect($row.text()).to.contain(CIDRS_LIST_FILE_NAME); expect($row.text()).to.contain('IP ranges'); }); }); @@ -113,63 +111,68 @@ describe('value lists', () => { describe('delete list types', () => { it('deletes a "keyword" list from an uploaded file', () => { - const listName = 'value_list.txt'; - importValueList(listName, 'keyword'); + importValueList(TEXT_LIST_FILE_NAME, 'keyword'); openValueListsModal(); - deleteValueListsFile(listName); + deleteValueListsFile(TEXT_LIST_FILE_NAME); cy.get(VALUE_LISTS_TABLE) .find(VALUE_LISTS_ROW) .should(($row) => { - expect($row.text()).not.to.contain(listName); + expect($row.text()).not.to.contain(TEXT_LIST_FILE_NAME); }); }); it('deletes a "text" list from an uploaded file', () => { - const listName = 'value_list.txt'; - importValueList(listName, 'text'); + importValueList(TEXT_LIST_FILE_NAME, 'text'); openValueListsModal(); - deleteValueListsFile(listName); + deleteValueListsFile(TEXT_LIST_FILE_NAME); cy.get(VALUE_LISTS_TABLE) .find(VALUE_LISTS_ROW) .should(($row) => { - expect($row.text()).not.to.contain(listName); + expect($row.text()).not.to.contain(TEXT_LIST_FILE_NAME); }); }); it('deletes a "ip" from an uploaded file', () => { - const listName = 'ip_list.txt'; - importValueList(listName, 'ip'); + importValueList(IPS_LIST_FILE_NAME, 'ip'); openValueListsModal(); - deleteValueListsFile(listName); + deleteValueListsFile(IPS_LIST_FILE_NAME); cy.get(VALUE_LISTS_TABLE) .find(VALUE_LISTS_ROW) .should(($row) => { - expect($row.text()).not.to.contain(listName); + expect($row.text()).not.to.contain(IPS_LIST_FILE_NAME); }); }); it('deletes a "ip_range" from an uploaded file', () => { - const listName = 'cidr_list.txt'; - importValueList(listName, 'ip_range', ['192.168.100.0']); + importValueList(CIDRS_LIST_FILE_NAME, 'ip_range', ['192.168.100.0']); openValueListsModal(); - deleteValueListsFile(listName); + deleteValueListsFile(CIDRS_LIST_FILE_NAME); cy.get(VALUE_LISTS_TABLE) .find(VALUE_LISTS_ROW) .should(($row) => { - expect($row.text()).not.to.contain(listName); + expect($row.text()).not.to.contain(CIDRS_LIST_FILE_NAME); }); }); }); describe('export list types', () => { it('exports a "keyword" list from an uploaded file', () => { - const listName = 'value_list.txt'; - cy.intercept('POST', `/api/lists/items/_export?list_id=${listName}`).as('exportList'); - importValueList('value_list.txt', 'keyword'); + cy.intercept('POST', `/api/lists/items/_export?list_id=${TEXT_LIST_FILE_NAME}`).as( + 'exportList' + ); + importValueList(TEXT_LIST_FILE_NAME, 'keyword'); + + // Importing value lists includes bulk creation of list items with refresh=wait_for + // While it should wait for data update and return after that it's not always a case with bulk operations. + // Sometimes list items are empty making this test flaky. + // To fix it refresh used list items index (for the default space) + refreshIndex('.items-default'); + openValueListsModal(); exportValueList(); + cy.wait('@exportList').then(({ response }) => { - cy.fixture(listName).then((list: string) => { + cy.fixture(TEXT_LIST_FILE_NAME).then((list: string) => { const [lineOne, lineTwo] = list.split('\n'); expect(response?.body).to.contain(lineOne); expect(response?.body).to.contain(lineTwo); @@ -178,13 +181,22 @@ describe('value lists', () => { }); it('exports a "text" list from an uploaded file', () => { - const listName = 'value_list.txt'; - cy.intercept('POST', `/api/lists/items/_export?list_id=${listName}`).as('exportList'); - importValueList(listName, 'text'); + cy.intercept('POST', `/api/lists/items/_export?list_id=${TEXT_LIST_FILE_NAME}`).as( + 'exportList' + ); + importValueList(TEXT_LIST_FILE_NAME, 'text'); + + // Importing value lists includes bulk creation of list items with refresh=wait_for + // While it should wait for data update and return after that it's not always a case with bulk operations. + // Sometimes list items are empty making this test flaky. + // To fix it refresh used list items index (for the default space) + refreshIndex('.items-default'); + openValueListsModal(); exportValueList(); + cy.wait('@exportList').then(({ response }) => { - cy.fixture(listName).then((list: string) => { + cy.fixture(TEXT_LIST_FILE_NAME).then((list: string) => { const [lineOne, lineTwo] = list.split('\n'); expect(response?.body).to.contain(lineOne); expect(response?.body).to.contain(lineTwo); @@ -193,13 +205,21 @@ describe('value lists', () => { }); it('exports a "ip" list from an uploaded file', () => { - const listName = 'ip_list.txt'; - cy.intercept('POST', `/api/lists/items/_export?list_id=${listName}`).as('exportList'); - importValueList(listName, 'ip'); + cy.intercept('POST', `/api/lists/items/_export?list_id=${IPS_LIST_FILE_NAME}`).as( + 'exportList' + ); + importValueList(IPS_LIST_FILE_NAME, 'ip'); + + // Importing value lists includes bulk creation of list items with refresh=wait_for + // While it should wait for data update and return after that it's not always a case with bulk operations. + // Sometimes list items are empty making this test flaky. + // To fix it refresh used list items index (for the default space) + refreshIndex('.items-default'); + openValueListsModal(); exportValueList(); cy.wait('@exportList').then(({ response }) => { - cy.fixture(listName).then((list: string) => { + cy.fixture(IPS_LIST_FILE_NAME).then((list: string) => { const [lineOne, lineTwo] = list.split('\n'); expect(response?.body).to.contain(lineOne); expect(response?.body).to.contain(lineTwo); @@ -208,13 +228,21 @@ describe('value lists', () => { }); it('exports a "ip_range" list from an uploaded file', () => { - const listName = 'cidr_list.txt'; - cy.intercept('POST', `/api/lists/items/_export?list_id=${listName}`).as('exportList'); - importValueList(listName, 'ip_range', ['192.168.100.0']); + cy.intercept('POST', `/api/lists/items/_export?list_id=${CIDRS_LIST_FILE_NAME}`).as( + 'exportList' + ); + importValueList(CIDRS_LIST_FILE_NAME, 'ip_range', ['192.168.100.0']); + + // Importing value lists includes bulk creation of list items with refresh=wait_for + // While it should wait for data update and return after that it's not always a case with bulk operations. + // Sometimes list items are empty making this test flaky. + // To fix it refresh used list items index (for the default space) + refreshIndex('.items-default'); + openValueListsModal(); exportValueList(); cy.wait('@exportList').then(({ response }) => { - cy.fixture(listName).then((list: string) => { + cy.fixture(CIDRS_LIST_FILE_NAME).then((list: string) => { const [lineOne] = list.split('\n'); expect(response?.body).to.contain(lineOne); }); diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/lists.ts b/x-pack/test/security_solution_cypress/cypress/tasks/lists.ts index d160f4c2deb42..e9dd3d882b429 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/lists.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/lists.ts @@ -10,7 +10,6 @@ import { VALUE_LIST_CLOSE_BUTTON, VALUE_LIST_DELETE_BUTTON, VALUE_LIST_EXPORT_BUTTON, - VALUE_LIST_FILES, VALUE_LIST_FILE_PICKER, VALUE_LIST_FILE_UPLOAD_BUTTON, VALUE_LIST_TYPE_SELECTOR, @@ -73,9 +72,14 @@ export const exportValueList = (): Cypress.Chainable> => { /** * Given an array of value lists this will delete them all using Cypress Request and the lists REST API + * + * If a list doesn't exist it ignores the error. + * * Ref: https://www.elastic.co/guide/en/security/current/lists-api-delete-container.html */ -const deleteValueLists = (lists: string[]): Array>> => { +export const deleteValueLists = ( + lists: string[] +): Array>> => { return lists.map((list) => deleteValueList(list)); }; @@ -88,6 +92,7 @@ const deleteValueList = (list: string): Cypress.Chainable { return cy.fixture(file).then((data) => uploadListItemData(file, type, data)); }; - -/** - * If you are on the value lists from the UI, this will loop over all the HTML elements - * that have action-delete-value-list-${list_name} and delete all of those value lists - * using Cypress Request and the lists REST API. - * If the UI does not contain any value based lists this will not fail. If the UI does - * contain value based lists but the backend does not return a success on DELETE then this - * will cause errors. - * Ref: https://www.elastic.co/guide/en/security/current/lists-api-delete-container.html - */ -export const deleteAllValueListsFromUI = (): Array< - Cypress.Chainable> -> => { - const lists = Cypress.$(VALUE_LIST_FILES) - .toArray() - .reduce((accum, $el) => { - const attribute = $el.getAttribute('data-test-subj'); - if (attribute != null) { - const list = attribute.substr('data-test-subj-value-list'.length); - return [...accum, list]; - } else { - return accum; - } - }, []); - return deleteValueLists(lists); -}; From 79a0a5e94089121cbce294e0ca899084c63bae0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Mon, 21 Aug 2023 16:33:07 +0200 Subject: [PATCH 06/10] [DOCS] Improves change point detection documentation (#164277) Co-authored-by: Dima Arnautov --- docs/user/ml/index.asciidoc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/user/ml/index.asciidoc b/docs/user/ml/index.asciidoc index e7a60df8d289e..59f7345923870 100644 --- a/docs/user/ml/index.asciidoc +++ b/docs/user/ml/index.asciidoc @@ -223,3 +223,11 @@ point type selector to filter the results by specific types of change points. [role="screenshot"] image::user/ml/images/ml-change-point-detection-selected.png[Selected change points] + + +You can attach change point charts to a dashboard or a case by using the context +menu. If the split field is selected, you can either select specific charts +(partitions) or set the maximum number of top change points to plot. It's +possible to preserve the applied time range or use the time bound from the page +date picker. You can also add or edit change point charts directly from the +**Dashboard** app. \ No newline at end of file From 5f310f773abcd850e739a712082fb188378bb014 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 21 Aug 2023 16:48:08 +0200 Subject: [PATCH 07/10] Unskip X-Pack Saved Object Tagging Functional Tests (#164273) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary close https://github.com/elastic/kibana/issues/88639 10 🟢 runs https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/2926 40 🟢 runs https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/2928 --- .../functional/tests/visualize_integration.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts b/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts index 515c79ae5156a..a7a03a58ba0cf 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts @@ -61,8 +61,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); }; - // Failing: See https://github.com/elastic/kibana/issues/88639 - describe.skip('visualize integration', () => { + describe('visualize integration', () => { before(async () => { // clean up any left-over visualizations and tags from tests that didn't clean up after themselves await kibanaServer.savedObjects.clean({ types: ['tag', 'visualization'] }); From 98a135cc7aba034592e77a3c0a0c9caed6c0615a Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 21 Aug 2023 11:08:47 -0400 Subject: [PATCH 08/10] skip failing test suite (#164318) --- .../group4/telemetry/task_based/detection_rules.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/task_based/detection_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/task_based/detection_rules.ts index 686596b25e008..789d653f5f3aa 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/task_based/detection_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/task_based/detection_rules.ts @@ -34,7 +34,8 @@ export default ({ getService }: FtrProviderContext) => { const log = getService('log'); const retry = getService('retry'); - describe('Detection rule task telemetry', async () => { + // Failing: See https://github.com/elastic/kibana/issues/164318 + describe.skip('Detection rule task telemetry', async () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/security_solution/telemetry'); }); From c328d2da218f4da46c73a0e5823e434385c179ca Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Mon, 21 Aug 2023 08:10:35 -0700 Subject: [PATCH 09/10] [Reporting] Adjust export type conditionals in server startup (#164232) ## Summary This PR cleans up how config settings are used as conditionals in the Reporting plugin startup phase. The existing code is correct, but it's heavily aligned to certain business requirements that might not be understood by a reader. The change in the PR uses simpler conditionals that are separated from internal business decisions. The result should be clearer readability of the code. --- .../common/constants/report_types.ts | 2 +- .../reporting_panel_content.tsx | 4 +- x-pack/plugins/reporting/server/core.ts | 21 ++-- .../plugins/reporting/server/plugin.test.ts | 97 +++++++++++-------- .../create_mock_reportingplugin.ts | 1 + 5 files changed, 76 insertions(+), 49 deletions(-) diff --git a/x-pack/plugins/reporting/common/constants/report_types.ts b/x-pack/plugins/reporting/common/constants/report_types.ts index 4e9b945aadb72..b66e00de7407f 100644 --- a/x-pack/plugins/reporting/common/constants/report_types.ts +++ b/x-pack/plugins/reporting/common/constants/report_types.ts @@ -6,7 +6,7 @@ */ // Export Type Definitions -export const CSV_REPORT_TYPE = 'CSV'; +export const CSV_REPORT_TYPE = 'csv_searchsource'; export const CSV_REPORT_TYPE_V2 = 'csv_v2'; export const PDF_REPORT_TYPE = 'printablePdf'; diff --git a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/reporting_panel_content.tsx b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/reporting_panel_content.tsx index 2252694fcaa97..58358d33f5b1f 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/reporting_panel_content.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/reporting_panel_content.tsx @@ -251,8 +251,8 @@ class ReportingPanelContentUi extends Component { case PDF_REPORT_TYPE: case PDF_REPORT_TYPE_V2: return 'PDF'; - case 'csv_searchsource': - return CSV_REPORT_TYPE; + case CSV_REPORT_TYPE: + return 'csv'; case 'png': case PNG_REPORT_TYPE_V2: return PNG_REPORT_TYPE; diff --git a/x-pack/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts index d81f7b8ab9808..453940f3cc914 100644 --- a/x-pack/plugins/reporting/server/core.ts +++ b/x-pack/plugins/reporting/server/core.ts @@ -226,22 +226,27 @@ export class ReportingCore { * only CSV export types should be registered in the export types registry for serverless */ private getExportTypes(): ExportType[] { + const { csv, pdf, png } = this.config.export_types; const exportTypes = []; - if (!this.config.export_types.pdf.enabled || !this.config.export_types.png.enabled) { - exportTypes.push( - new CsvSearchSourceExportType(this.core, this.config, this.logger, this.context) - ); - exportTypes.push(new CsvV2ExportType(this.core, this.config, this.logger, this.context)); - } else { + + if (csv.enabled) { + // NOTE: CsvSearchSourceExportType should be deprecated and replaced with V2 in the UI: https://github.com/elastic/kibana/issues/151190 exportTypes.push( new CsvSearchSourceExportType(this.core, this.config, this.logger, this.context) ); exportTypes.push(new CsvV2ExportType(this.core, this.config, this.logger, this.context)); + } + + if (pdf.enabled) { + // NOTE: PdfV1ExportType is deprecated and tagged for removal: https://github.com/elastic/kibana/issues/154601 + exportTypes.push(new PdfV1ExportType(this.core, this.config, this.logger, this.context)); exportTypes.push(new PdfExportType(this.core, this.config, this.logger, this.context)); + } + + if (png.enabled) { exportTypes.push(new PngExportType(this.core, this.config, this.logger, this.context)); - // deprecated export types for tests - exportTypes.push(new PdfV1ExportType(this.core, this.config, this.logger, this.context)); } + return exportTypes; } diff --git a/x-pack/plugins/reporting/server/plugin.test.ts b/x-pack/plugins/reporting/server/plugin.test.ts index 176b648b9d02a..3e57e3bf3322b 100644 --- a/x-pack/plugins/reporting/server/plugin.test.ts +++ b/x-pack/plugins/reporting/server/plugin.test.ts @@ -7,8 +7,15 @@ import type { CoreSetup, CoreStart, Logger } from '@kbn/core/server'; import { coreMock, loggingSystemMock } from '@kbn/core/server/mocks'; -import { PDF_REPORT_TYPE_V2, PNG_REPORT_TYPE_V2 } from '../common/constants/report_types'; +import { + CSV_REPORT_TYPE, + CSV_REPORT_TYPE_V2, + PDF_REPORT_TYPE, + PDF_REPORT_TYPE_V2, + PNG_REPORT_TYPE_V2, +} from '../common/constants'; import type { ReportingCore, ReportingInternalStart } from './core'; +import { ExportTypesRegistry } from './lib/export_types_registry'; import { ReportingPlugin } from './plugin'; import { createMockConfigSchema, @@ -30,6 +37,8 @@ describe('Reporting Plugin', () => { let plugin: ReportingPlugin; beforeEach(async () => { + jest.clearAllMocks(); + configSchema = createMockConfigSchema(); initContext = coreMock.createPluginInitializerContext(configSchema); coreSetup = coreMock.createSetup(configSchema); @@ -81,50 +90,62 @@ describe('Reporting Plugin', () => { `); expect(logger.error).toHaveBeenCalledTimes(2); }); - describe('config and export types registry validation', () => { - it('expect image reporting to be in registry by default', async () => { - // wait for the setup phase background work - plugin.setup(coreSetup, pluginSetup); - await new Promise(setImmediate); - - // create a way for an error to happen - const reportingCore = (plugin as unknown as { reportingCore: ReportingCore }).reportingCore; - - // wait for the startup phase background work - plugin.start(coreStart, pluginStart); - await new Promise(setImmediate); - expect(reportingCore.getExportTypesRegistry().getById(PDF_REPORT_TYPE_V2)).toHaveProperty( - 'id', - PDF_REPORT_TYPE_V2 - ); - expect(reportingCore.getExportTypesRegistry().getById(PNG_REPORT_TYPE_V2)).toHaveProperty( - 'id', - PNG_REPORT_TYPE_V2 - ); + + describe('config and export types registration', () => { + jest.mock('./lib/export_types_registry'); + ExportTypesRegistry.prototype.getAll = jest.fn(() => []); // code breaks if getAll returns undefined + let registerSpy: jest.SpyInstance; + + beforeEach(async () => { + registerSpy = jest.spyOn(ExportTypesRegistry.prototype, 'register'); + pluginSetup = createMockPluginSetup({}) as unknown as ReportingSetupDeps; + pluginStart = await createMockPluginStart(coreStart, configSchema); + plugin = new ReportingPlugin(initContext); + }); + + it('expect all report types to be in registry', async () => { + // check the spy function + expect(registerSpy).toHaveBeenCalledTimes(5); + expect(registerSpy).toHaveBeenCalledWith(expect.objectContaining({ id: CSV_REPORT_TYPE })); + expect(registerSpy).toHaveBeenCalledWith(expect.objectContaining({ id: CSV_REPORT_TYPE_V2 })); + expect(registerSpy).toHaveBeenCalledWith(expect.objectContaining({ id: PDF_REPORT_TYPE })); + expect(registerSpy).toHaveBeenCalledWith(expect.objectContaining({ id: PDF_REPORT_TYPE_V2 })); + expect(registerSpy).toHaveBeenCalledWith(expect.objectContaining({ id: PNG_REPORT_TYPE_V2 })); }); - it('expect pdf to not be in registry if config does not enable it', async () => { - configSchema = { ...createMockConfigSchema(), export_types: { pdf: { enabled: false } } }; + + it('expect image report types not to be in registry if disabled', async () => { + jest.clearAllMocks(); + + configSchema = createMockConfigSchema({ + export_types: { + csv: { enabled: true }, + pdf: { enabled: false }, + png: { enabled: false }, + }, + }); + initContext = coreMock.createPluginInitializerContext(configSchema); coreSetup = coreMock.createSetup(configSchema); coreStart = coreMock.createStart(); pluginSetup = createMockPluginSetup({}) as unknown as ReportingSetupDeps; pluginStart = await createMockPluginStart(coreStart, configSchema); - plugin = new ReportingPlugin(initContext); - // wait for the setup phase background work - plugin.setup(coreSetup, pluginSetup); - await new Promise(setImmediate); - - // create a way for an error to happen - const reportingCore = (plugin as unknown as { reportingCore: ReportingCore }).reportingCore; - - // wait for the startup phase background work - plugin.start(coreStart, pluginStart); - await new Promise(setImmediate); - const checkPdf = () => reportingCore.getExportTypesRegistry().getById(PDF_REPORT_TYPE_V2); - const checkPng = () => reportingCore.getExportTypesRegistry().getById(PNG_REPORT_TYPE_V2); - expect(checkPdf).toThrowError(`Unknown id ${PDF_REPORT_TYPE_V2}`); - expect(checkPng).toThrowError(`Unknown id ${PNG_REPORT_TYPE_V2}`); + + // check the spy function was called with CSV + expect(registerSpy).toHaveBeenCalledTimes(2); + expect(registerSpy).toHaveBeenCalledWith(expect.objectContaining({ id: CSV_REPORT_TYPE })); + expect(registerSpy).toHaveBeenCalledWith(expect.objectContaining({ id: CSV_REPORT_TYPE_V2 })); + + // check the spy function was NOT called with anything else + expect(registerSpy).not.toHaveBeenCalledWith( + expect.objectContaining({ id: PDF_REPORT_TYPE }) + ); + expect(registerSpy).not.toHaveBeenCalledWith( + expect.objectContaining({ id: PDF_REPORT_TYPE_V2 }) + ); + expect(registerSpy).not.toHaveBeenCalledWith( + expect.objectContaining({ id: PNG_REPORT_TYPE_V2 }) + ); }); }); }); diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts index 4fbacb3a8b994..38a7bb8399abb 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts @@ -115,6 +115,7 @@ export const createMockConfigSchema = ( pdf: { enabled: true }, png: { enabled: true }, csv: { enabled: true }, + ...overrides.export_types, }, } as ReportingConfigType; }; From c1d2834f1aa675f2682793d83ac9ad07c3854990 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Mon, 21 Aug 2023 09:29:08 -0700 Subject: [PATCH 10/10] [OAS] Remove redundant connector properties (#163987) --- .../api-generated/connectors/connector-apis-passthru.asciidoc | 4 ++-- x-pack/plugins/actions/docs/openapi/bundled.json | 4 ---- x-pack/plugins/actions/docs/openapi/bundled.yaml | 4 ---- .../schemas/update_connector_request_slack_api.yaml | 2 -- .../schemas/update_connector_request_slack_webhook.yaml | 2 -- 5 files changed, 2 insertions(+), 14 deletions(-) diff --git a/docs/api-generated/connectors/connector-apis-passthru.asciidoc b/docs/api-generated/connectors/connector-apis-passthru.asciidoc index a31c0c5fa963f..64bf78a2b66ed 100644 --- a/docs/api-generated/connectors/connector-apis-passthru.asciidoc +++ b/docs/api-generated/connectors/connector-apis-passthru.asciidoc @@ -2475,7 +2475,7 @@ Any modifications made to this file will be overwritten.
name
String The display name for the connector.
-
secrets
+
secrets
@@ -2483,7 +2483,7 @@ Any modifications made to this file will be overwritten.
name
String The display name for the connector.
-
secrets
+
secrets
diff --git a/x-pack/plugins/actions/docs/openapi/bundled.json b/x-pack/plugins/actions/docs/openapi/bundled.json index 27606af5b7d20..59ca423963caf 100644 --- a/x-pack/plugins/actions/docs/openapi/bundled.json +++ b/x-pack/plugins/actions/docs/openapi/bundled.json @@ -3720,8 +3720,6 @@ "description": "The display name for the connector." }, "secrets": { - "type": "object", - "description": "The secrets object containing the necessary fields for authentication.", "$ref": "#/components/schemas/secrets_properties_slack_api" } } @@ -3739,8 +3737,6 @@ "description": "The display name for the connector." }, "secrets": { - "type": "object", - "description": "The secrets object containing the necessary fields for authentication.", "$ref": "#/components/schemas/secrets_properties_slack_webhook" } } diff --git a/x-pack/plugins/actions/docs/openapi/bundled.yaml b/x-pack/plugins/actions/docs/openapi/bundled.yaml index a784d60473b09..b090dba90a87a 100644 --- a/x-pack/plugins/actions/docs/openapi/bundled.yaml +++ b/x-pack/plugins/actions/docs/openapi/bundled.yaml @@ -2549,8 +2549,6 @@ components: type: string description: The display name for the connector. secrets: - type: object - description: The secrets object containing the necessary fields for authentication. $ref: '#/components/schemas/secrets_properties_slack_api' update_connector_request_slack_webhook: title: Update Slack connector request @@ -2563,8 +2561,6 @@ components: type: string description: The display name for the connector. secrets: - type: object - description: The secrets object containing the necessary fields for authentication. $ref: '#/components/schemas/secrets_properties_slack_webhook' update_connector_request_swimlane: title: Update Swimlane connector request diff --git a/x-pack/plugins/actions/docs/openapi/components/schemas/update_connector_request_slack_api.yaml b/x-pack/plugins/actions/docs/openapi/components/schemas/update_connector_request_slack_api.yaml index e85a0436a035b..1a0c99c1847a0 100644 --- a/x-pack/plugins/actions/docs/openapi/components/schemas/update_connector_request_slack_api.yaml +++ b/x-pack/plugins/actions/docs/openapi/components/schemas/update_connector_request_slack_api.yaml @@ -8,6 +8,4 @@ properties: type: string description: The display name for the connector. secrets: - type: object - description: The secrets object containing the necessary fields for authentication. $ref: 'secrets_properties_slack_api.yaml' diff --git a/x-pack/plugins/actions/docs/openapi/components/schemas/update_connector_request_slack_webhook.yaml b/x-pack/plugins/actions/docs/openapi/components/schemas/update_connector_request_slack_webhook.yaml index 4ed93f519f3c2..67b0f9bb310ad 100644 --- a/x-pack/plugins/actions/docs/openapi/components/schemas/update_connector_request_slack_webhook.yaml +++ b/x-pack/plugins/actions/docs/openapi/components/schemas/update_connector_request_slack_webhook.yaml @@ -8,6 +8,4 @@ properties: type: string description: The display name for the connector. secrets: - type: object - description: The secrets object containing the necessary fields for authentication. $ref: 'secrets_properties_slack_webhook.yaml'