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, 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] 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_properties_slack_api
+
secrets
secrets_properties_slack_api
@@ -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/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 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/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/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' 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; }; 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/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": "重置仪表板?", 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'); }); 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'] }); 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); -}; 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'], }, ],