From 885e4bf13d728556efeabb7c8d2e8cc10911d9b5 Mon Sep 17 00:00:00 2001 From: Juan Pablo Djeredjian Date: Thu, 6 Jul 2023 15:16:42 +0200 Subject: [PATCH 1/9] [Security Solution] Hide Rule Updates tab when no updates are available (#161196) ## Summary Fixes: https://github.com/elastic/kibana/issues/161195 - Hide Rule Updates tab when no rules are available for update. ### Checklist Delete any items that are not applicable to this PR. - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../prebuilt_rules_notifications.cy.ts | 5 ++-- .../rules_table/rules_table_toolbar.tsx | 29 ++++++++++++------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/prebuilt_rules_notifications.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/prebuilt_rules_notifications.cy.ts index 782298088244f..e5216705ab7d1 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/prebuilt_rules_notifications.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/prebuilt_rules_notifications.cy.ts @@ -48,9 +48,10 @@ describe('Detection rules, Prebuilt Rules Installation and Update workflow', () waitForRulesTableToBeLoaded(); /* Assert that there are no installation or update notifications */ - /* Add Elastic Rules button and Rule Upgrade tabs should not contain a number badge */ + /* Add Elastic Rules button should not contain a number badge */ + /* and Rule Upgrade tab should not be displayed */ cy.get(ADD_ELASTIC_RULES_BTN).should('have.text', 'Add Elastic rules'); - cy.get(RULES_UPDATES_TAB).should('have.text', 'Rule Updates'); + cy.get(RULES_UPDATES_TAB).should('not.exist'); }); }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx index 17f9d7e979b8e..ea4a43b46db7a 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx @@ -26,6 +26,22 @@ export const RulesTableToolbar = React.memo(() => { (ruleManagementFilters?.rules_summary.prebuilt_installed_count ?? 0); const updateTotal = prebuiltRulesStatus?.num_prebuilt_rules_to_upgrade ?? 0; + const ruleUpdateTab = useMemo( + () => ({ + [AllRulesTabs.updates]: { + id: AllRulesTabs.updates, + name: i18n.RULE_UPDATES_TAB, + disabled: false, + href: `/rules/${AllRulesTabs.updates}`, + isBeta: updateTotal > 0, + betaOptions: { + text: `${updateTotal}`, + }, + }, + }), + [updateTotal] + ); + const ruleTabs = useMemo( () => ({ [AllRulesTabs.management]: { @@ -48,18 +64,9 @@ export const RulesTableToolbar = React.memo(() => { text: `${installedTotal}`, }, }, - [AllRulesTabs.updates]: { - id: AllRulesTabs.updates, - name: i18n.RULE_UPDATES_TAB, - disabled: false, - href: `/rules/${AllRulesTabs.updates}`, - isBeta: updateTotal > 0, - betaOptions: { - text: `${updateTotal}`, - }, - }, + ...(updateTotal > 0 ? ruleUpdateTab : {}), }), - [installedTotal, updateTotal] + [installedTotal, ruleUpdateTab, updateTotal] ); return ; From c843c971939bd42cbce916a7d90ffd54f85c8959 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Thu, 6 Jul 2023 08:29:01 -0500 Subject: [PATCH 2/9] [data views] REST endpoint for swapping saved object references (#157665) ## Summary Managing large number of saved objects can be cumbersome. This api endpoint allows the management of references without clicking through a lot of different UIs. For example - This swaps all data view id `abcd-efg` references to `xyz-123` ``` POST /api/data_views/swap_references { "from_id" : "abcd-efg", "to_id" : "xyz-123", "preview" : false, // optional, necessary to save changes "delete" : true // optional, removes data view which is no longer referenced } returns { preview: false, result: [{ id: "123", type: "visualization" }], deleteSuccess: true } ``` Additional params - ``` from_type: string - specify the saved object type. Default is `index-pattern` for data view for_id: string | string[] - limit the affected saved objects to one or more by id for_type: string - limit the affected saved objects by type ``` Closes https://github.com/elastic/kibana/issues/153806 --- src/plugins/data_views/server/constants.ts | 5 + src/plugins/data_views/server/index.ts | 1 + .../server/rest_api_routes/public/index.ts | 2 + .../rest_api_routes/public/swap_references.ts | 177 ++++++++++++++++++ test/api_integration/apis/data_views/index.ts | 1 + .../apis/data_views/swap_references/errors.ts | 40 ++++ .../apis/data_views/swap_references/index.ts | 16 ++ .../apis/data_views/swap_references/main.ts | 174 +++++++++++++++++ 8 files changed, 416 insertions(+) create mode 100644 src/plugins/data_views/server/rest_api_routes/public/swap_references.ts create mode 100644 test/api_integration/apis/data_views/swap_references/errors.ts create mode 100644 test/api_integration/apis/data_views/swap_references/index.ts create mode 100644 test/api_integration/apis/data_views/swap_references/main.ts diff --git a/src/plugins/data_views/server/constants.ts b/src/plugins/data_views/server/constants.ts index b1b1116317dc1..040a321978e17 100644 --- a/src/plugins/data_views/server/constants.ts +++ b/src/plugins/data_views/server/constants.ts @@ -64,6 +64,11 @@ export const SPECIFIC_SCRIPTED_FIELD_PATH = `${SCRIPTED_FIELD_PATH}/{name}`; */ export const SPECIFIC_SCRIPTED_FIELD_PATH_LEGACY = `${SCRIPTED_FIELD_PATH_LEGACY}/{name}`; +/** + * Path to swap references + */ +export const DATA_VIEW_SWAP_REFERENCES_PATH = `${SERVICE_PATH}/swap_references`; + /** * name of service in path form */ diff --git a/src/plugins/data_views/server/index.ts b/src/plugins/data_views/server/index.ts index d7860e0bed473..40b9030e7d180 100644 --- a/src/plugins/data_views/server/index.ts +++ b/src/plugins/data_views/server/index.ts @@ -52,6 +52,7 @@ export { SPECIFIC_SCRIPTED_FIELD_PATH_LEGACY, SERVICE_KEY, SERVICE_KEY_LEGACY, + DATA_VIEW_SWAP_REFERENCES_PATH, } from './constants'; export type { SERVICE_KEY_TYPE } from './constants'; diff --git a/src/plugins/data_views/server/rest_api_routes/public/index.ts b/src/plugins/data_views/server/rest_api_routes/public/index.ts index 812cda62ac1ef..f4f64841e1de9 100644 --- a/src/plugins/data_views/server/rest_api_routes/public/index.ts +++ b/src/plugins/data_views/server/rest_api_routes/public/index.ts @@ -17,6 +17,7 @@ import * as getRoutes from './get_data_view'; import * as getAllRoutes from './get_data_views'; import * as hasRoutes from './has_user_data_view'; import * as updateRoutes from './update_data_view'; +import { swapReferencesRoute } from './swap_references'; const routes = [ fieldRoutes.registerUpdateFieldsRoute, @@ -45,6 +46,7 @@ const routes = [ updateRoutes.registerUpdateDataViewRoute, updateRoutes.registerUpdateDataViewRouteLegacy, ...Object.values(scriptedRoutes), + swapReferencesRoute, ]; export { routes }; diff --git a/src/plugins/data_views/server/rest_api_routes/public/swap_references.ts b/src/plugins/data_views/server/rest_api_routes/public/swap_references.ts new file mode 100644 index 0000000000000..e8296394857d8 --- /dev/null +++ b/src/plugins/data_views/server/rest_api_routes/public/swap_references.ts @@ -0,0 +1,177 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { UsageCounter } from '@kbn/usage-collection-plugin/server'; +import { schema } from '@kbn/config-schema'; +import { IRouter, StartServicesAccessor, SavedObjectsFindOptions } from '@kbn/core/server'; +import { DataViewsService } from '../../../common'; +import { handleErrors } from './util/handle_errors'; +import type { + DataViewsServerPluginStartDependencies, + DataViewsServerPluginStart, +} from '../../types'; +import { DATA_VIEW_SWAP_REFERENCES_PATH, INITIAL_REST_VERSION } from '../../constants'; +import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../../common/constants'; + +interface GetDataViewArgs { + dataViewsService: DataViewsService; + usageCollection?: UsageCounter; + counterName: string; + id: string; +} + +interface SwapRefResponse { + result: Array<{ id: string; type: string }>; + preview: boolean; + deleteSuccess?: boolean; +} + +export const swapReference = async ({ + dataViewsService, + usageCollection, + counterName, + id, +}: GetDataViewArgs) => { + usageCollection?.incrementCounter({ counterName }); + return dataViewsService.get(id); +}; + +const idSchema = schema.string(); + +export const swapReferencesRoute = ( + router: IRouter, + getStartServices: StartServicesAccessor< + DataViewsServerPluginStartDependencies, + DataViewsServerPluginStart + >, + usageCollection?: UsageCounter +) => { + router.versioned.post({ path: DATA_VIEW_SWAP_REFERENCES_PATH, access: 'public' }).addVersion( + { + version: INITIAL_REST_VERSION, + validate: { + request: { + body: schema.object({ + from_id: idSchema, + from_type: schema.maybe(schema.string()), + to_id: idSchema, + for_id: schema.maybe(schema.oneOf([idSchema, schema.arrayOf(idSchema)])), + for_type: schema.maybe(schema.string()), + preview: schema.maybe(schema.boolean()), + delete: schema.maybe(schema.boolean()), + }), + }, + response: { + 200: { + body: schema.object({ + result: schema.arrayOf(schema.object({ id: idSchema, type: schema.string() })), + preview: schema.boolean(), + deleteSuccess: schema.maybe(schema.boolean()), + }), + }, + }, + }, + }, + router.handleLegacyErrors( + handleErrors(async (ctx, req, res) => { + const savedObjectsClient = (await ctx.core).savedObjects.client; + const [core] = await getStartServices(); + const types = core.savedObjects.getTypeRegistry().getAllTypes(); + const type = req.body.from_type || DATA_VIEW_SAVED_OBJECT_TYPE; + const preview = req.body.preview !== undefined ? req.body.preview : true; + const searchId = + !Array.isArray(req.body.for_id) && req.body.for_id !== undefined + ? [req.body.for_id] + : req.body.for_id; + + usageCollection?.incrementCounter({ counterName: 'swap_references' }); + + // verify 'to' object actually exists + try { + await savedObjectsClient.get(type, req.body.to_id); + } catch (e) { + throw new Error(`Could not find object with type ${type} and id ${req.body.to_id}`); + } + + // assemble search params + const findParams: SavedObjectsFindOptions = { + type: types.map((t) => t.name), + hasReference: { type, id: req.body.from_id }, + }; + + if (req.body.for_type) { + findParams.type = [req.body.for_type]; + } + + const { saved_objects: savedObjects } = await savedObjectsClient.find(findParams); + + const filteredSavedObjects = searchId + ? savedObjects.filter((so) => searchId?.includes(so.id)) + : savedObjects; + + // create summary of affected objects + const resultSummary = filteredSavedObjects.map((savedObject) => ({ + id: savedObject.id, + type: savedObject.type, + })); + + const body: SwapRefResponse = { + result: resultSummary, + preview, + }; + + // bail if preview + if (preview) { + return res.ok({ + headers: { + 'content-type': 'application/json', + }, + body, + }); + } + + // iterate over list and update references + for (const savedObject of filteredSavedObjects) { + const updatedRefs = savedObject.references.map((ref) => { + if (ref.type === type && ref.id === req.body.from_id) { + return { ...ref, id: req.body.to_id }; + } else { + return ref; + } + }); + + await savedObjectsClient.update( + savedObject.type, + savedObject.id, + {}, + { + references: updatedRefs, + } + ); + } + + if (req.body.delete) { + const verifyNoMoreRefs = await savedObjectsClient.find(findParams); + if (verifyNoMoreRefs.total > 0) { + body.deleteSuccess = false; + } else { + await savedObjectsClient.delete(type, req.body.from_id); + body.deleteSuccess = true; + } + } + + return res.ok({ + headers: { + 'content-type': 'application/json', + }, + body, + }); + }) + ) + ); +}; diff --git a/test/api_integration/apis/data_views/index.ts b/test/api_integration/apis/data_views/index.ts index 61d9958f9a336..328c7c1162d1b 100644 --- a/test/api_integration/apis/data_views/index.ts +++ b/test/api_integration/apis/data_views/index.ts @@ -20,5 +20,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./integration')); loadTestFile(require.resolve('./deprecations')); loadTestFile(require.resolve('./has_user_index_pattern')); + loadTestFile(require.resolve('./swap_references')); }); } diff --git a/test/api_integration/apis/data_views/swap_references/errors.ts b/test/api_integration/apis/data_views/swap_references/errors.ts new file mode 100644 index 0000000000000..101eb8fb5ba28 --- /dev/null +++ b/test/api_integration/apis/data_views/swap_references/errors.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 expect from '@kbn/expect'; +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import { INITIAL_REST_VERSION } from '@kbn/data-views-plugin/server/constants'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { dataViewConfig } from '../constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('errors', () => { + it('returns 404 error on non-existing index_pattern', async () => { + const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; + const response = await supertest + .get(`${dataViewConfig.path}/${id}`) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION); + + expect(response.status).to.be(404); + }); + + it('returns error when ID is too long', async () => { + const id = `xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx`; + const response = await supertest + .get(`${dataViewConfig.path}/${id}`) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION); + + expect(response.status).to.be(400); + expect(response.body.message).to.be( + '[request params.id]: value has length [1759] but it must have a maximum length of [1000].' + ); + }); + }); +} diff --git a/test/api_integration/apis/data_views/swap_references/index.ts b/test/api_integration/apis/data_views/swap_references/index.ts new file mode 100644 index 0000000000000..b5818278aaf22 --- /dev/null +++ b/test/api_integration/apis/data_views/swap_references/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('swap_references', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/test/api_integration/apis/data_views/swap_references/main.ts b/test/api_integration/apis/data_views/swap_references/main.ts new file mode 100644 index 0000000000000..93247f090a9da --- /dev/null +++ b/test/api_integration/apis/data_views/swap_references/main.ts @@ -0,0 +1,174 @@ +/* + * 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 expect from '@kbn/expect'; +import { + DATA_VIEW_SWAP_REFERENCES_PATH, + SPECIFIC_DATA_VIEW_PATH, + DATA_VIEW_PATH, +} from '@kbn/data-views-plugin/server'; +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import { INITIAL_REST_VERSION } from '@kbn/data-views-plugin/server/constants'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const title = 'logs-*'; + const prevDataViewId = '91200a00-9efd-11e7-acb3-3dab96693fab'; + let dataViewId = ''; + + describe('main', () => { + const kibanaServer = getService('kibanaServer'); + before(async () => { + const result = await supertest + .post(DATA_VIEW_PATH) + .send({ data_view: { title } }) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION); + dataViewId = result.body.data_view.id; + }); + after(async () => { + await supertest + .delete(SPECIFIC_DATA_VIEW_PATH.replace('{id}', dataViewId)) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION); + }); + beforeEach(async () => { + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); + }); + afterEach(async () => { + await kibanaServer.importExport.unload( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); + }); + + it('can preview', async () => { + const res = await supertest + .post(DATA_VIEW_SWAP_REFERENCES_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + .send({ + from_id: prevDataViewId, + to_id: dataViewId, + }); + expect(res).to.have.property('status', 200); + }); + + it('can preview specifying type', async () => { + const res = await supertest + .post(DATA_VIEW_SWAP_REFERENCES_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + .send({ + from_id: prevDataViewId, + from_type: 'index-pattern', + to_id: dataViewId, + }); + expect(res).to.have.property('status', 200); + }); + + it('can save changes', async () => { + const res = await supertest + .post(DATA_VIEW_SWAP_REFERENCES_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + .send({ + from_id: prevDataViewId, + to_id: dataViewId, + preview: false, + }); + expect(res).to.have.property('status', 200); + expect(res.body.result.length).to.equal(1); + expect(res.body.preview).to.equal(false); + expect(res.body.result[0].id).to.equal('dd7caf20-9efd-11e7-acb3-3dab96693fab'); + expect(res.body.result[0].type).to.equal('visualization'); + }); + + it('can save changes and remove old saved object', async () => { + const res = await supertest + .post(DATA_VIEW_SWAP_REFERENCES_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + .send({ + from_id: prevDataViewId, + to_id: dataViewId, + preview: false, + delete: true, + }); + expect(res).to.have.property('status', 200); + expect(res.body.result.length).to.equal(1); + + const res2 = await supertest + .get(SPECIFIC_DATA_VIEW_PATH.replace('{id}', prevDataViewId)) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION); + + expect(res2).to.have.property('statusCode', 404); + }); + + describe('limit affected saved objects', () => { + beforeEach(async () => { + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/management/saved_objects/relationships.json' + ); + }); + afterEach(async () => { + await kibanaServer.importExport.unload( + 'test/api_integration/fixtures/kbn_archiver/management/saved_objects/relationships.json' + ); + }); + + it('can limit by id', async () => { + // confirm this will find two items + const res = await supertest + .post(DATA_VIEW_SWAP_REFERENCES_PATH) + .send({ + from_id: '8963ca30-3224-11e8-a572-ffca06da1357', + to_id: '91200a00-9efd-11e7-acb3-3dab96693fab', + }) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION); + expect(res).to.have.property('status', 200); + expect(res.body.result.length).to.equal(2); + + // limit to one item + const res2 = await supertest + .post(DATA_VIEW_SWAP_REFERENCES_PATH) + .send({ + from_id: '8963ca30-3224-11e8-a572-ffca06da1357', + to_id: '91200a00-9efd-11e7-acb3-3dab96693fab', + for_id: ['960372e0-3224-11e8-a572-ffca06da1357'], + preview: false, + }) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION); + expect(res2).to.have.property('status', 200); + expect(res2.body.result.length).to.equal(1); + }); + + it('can limit by type', async () => { + // confirm this will find two items + const res = await supertest + .post(DATA_VIEW_SWAP_REFERENCES_PATH) + .send({ + from_id: '8963ca30-3224-11e8-a572-ffca06da1357', + to_id: '91200a00-9efd-11e7-acb3-3dab96693fab', + }) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION); + expect(res).to.have.property('status', 200); + expect(res.body.result.length).to.equal(2); + + // limit to one item + const res2 = await supertest + .post(DATA_VIEW_SWAP_REFERENCES_PATH) + .send({ + from_id: '8963ca30-3224-11e8-a572-ffca06da1357', + to_id: '91200a00-9efd-11e7-acb3-3dab96693fab', + for_type: 'search', + preview: false, + }) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION); + expect(res2).to.have.property('status', 200); + expect(res2.body.result.length).to.equal(1); + }); + }); + }); +} From 7b86444a4cc21046c5d8978be6014bbb8f36f2d0 Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Thu, 6 Jul 2023 07:59:01 -0600 Subject: [PATCH 3/9] [SLO] Allow null values for maxBurnRateThreshold (#161268) ## Summary This PR fixes #161101 by changing the server side validation to allow a null value for `maxBurnRateThreshold`. This value is only used on the client side to display the `X hours until error budget exhaustion.` message. This value is also used in the client validation but due to the architecture of how client side validation works, we have to add this to the params. --- .../observability/server/lib/rules/slo_burn_rate/register.ts | 2 +- .../observability/server/lib/rules/slo_burn_rate/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/register.ts b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/register.ts index 18fd1fbc1bc5b..146e4682c105d 100644 --- a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/register.ts +++ b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/register.ts @@ -34,7 +34,7 @@ const durationSchema = schema.object({ const windowSchema = schema.object({ id: schema.string(), burnRateThreshold: schema.number(), - maxBurnRateThreshold: schema.number(), + maxBurnRateThreshold: schema.nullable(schema.number()), longWindow: durationSchema, shortWindow: durationSchema, actionGroup: schema.string(), diff --git a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/types.ts b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/types.ts index e903ced7c35fa..8e8811e727b55 100644 --- a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/types.ts +++ b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/types.ts @@ -25,7 +25,7 @@ export enum AlertStates { export interface WindowSchema { id: string; burnRateThreshold: number; - maxBurnRateThreshold: number; + maxBurnRateThreshold: number | null; longWindow: { value: number; unit: string }; shortWindow: { value: number; unit: string }; actionGroup: string; From 099835fad5e048d6f2bc90a3f9b5accc6bf5a5bb Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Thu, 6 Jul 2023 08:00:09 -0600 Subject: [PATCH 4/9] [SLO] Support filters for good/total custom metrics (#161308) ## Summary This PR adds support for applying a KQL filter to the good/total metrics. image --- .../kbn-slo-schema/src/schema/indicators.ts | 15 ++- .../indicator_properties_custom_metric.yaml | 8 ++ .../custom_metric/custom_metric_type_form.tsx | 8 ++ .../custom_metric/metric_indicator.tsx | 94 +++++++++------ .../__snapshots__/metric_custom.test.ts.snap | 110 ++++++++++++++---- .../metric_custom.test.ts | 52 +++++++++ .../slo/transform_generators/metric_custom.ts | 21 ++-- 7 files changed, 239 insertions(+), 69 deletions(-) diff --git a/x-pack/packages/kbn-slo-schema/src/schema/indicators.ts b/x-pack/packages/kbn-slo-schema/src/schema/indicators.ts index a63cd845242b8..8b46a1ed88b37 100644 --- a/x-pack/packages/kbn-slo-schema/src/schema/indicators.ts +++ b/x-pack/packages/kbn-slo-schema/src/schema/indicators.ts @@ -60,11 +60,16 @@ const metricCustomValidAggregations = t.keyof({ }); const metricCustomMetricDef = t.type({ metrics: t.array( - t.type({ - name: t.string, - aggregation: metricCustomValidAggregations, - field: t.string, - }) + t.intersection([ + t.type({ + name: t.string, + aggregation: metricCustomValidAggregations, + field: t.string, + }), + t.partial({ + filter: t.string, + }), + ]) ), equation: t.string, }); diff --git a/x-pack/plugins/observability/docs/openapi/slo/components/schemas/indicator_properties_custom_metric.yaml b/x-pack/plugins/observability/docs/openapi/slo/components/schemas/indicator_properties_custom_metric.yaml index 8e53cb3cc094d..6746bf400e8d0 100644 --- a/x-pack/plugins/observability/docs/openapi/slo/components/schemas/indicator_properties_custom_metric.yaml +++ b/x-pack/plugins/observability/docs/openapi/slo/components/schemas/indicator_properties_custom_metric.yaml @@ -60,6 +60,10 @@ properties: description: The field of the metric. type: string example: processor.processed + filter: + description: The filter to apply to the metric. + type: string + example: processor.outcome: "success" equation: description: The equation to calculate the "good" metric. type: string @@ -96,6 +100,10 @@ properties: description: The field of the metric. type: string example: processor.processed + filter: + description: The filter to apply to the metric. + type: string + example: processor.outcome: * equation: description: The equation to calculate the "total" metric. type: string diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/custom_metric/custom_metric_type_form.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/custom_metric/custom_metric_type_form.tsx index 9f223b36419f3..b774c3bc4e8c1 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/custom_metric/custom_metric_type_form.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/custom_metric/custom_metric_type_form.tsx @@ -148,6 +148,10 @@ export function CustomMetricIndicatorTypeForm() { 'xpack.observability.slo.sloEdit.sliType.customMetric.goodMetricLabel', { defaultMessage: 'Good metric' } )} + filterLabel={i18n.translate( + 'xpack.observability.slo.sloEdit.sliType.customMetric.goodFilterLabel', + { defaultMessage: 'Good filter' } + )} metricTooltip={ {fields?.map((metric, index) => ( - - {metricLabel} {metric.name} {metricTooltip} - - } - key={metric.id} - > - - - - - + + + + + + {metricLabel} {metric.name} {metricTooltip} + + } + > )} /> - - - - - - + + + + + } + /> + + + + + ))} diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/metric_custom.test.ts.snap b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/metric_custom.test.ts.snap index 5c401d8bdfbbd..14afc32122d6f 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/metric_custom.test.ts.snap +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/metric_custom.test.ts.snap @@ -4,7 +4,21 @@ exports[`Metric Custom Transform Generator aggregates using the denominator equa Object { "bucket_script": Object { "buckets_path": Object { - "A": "_total_A", + "A": "_total_A>sum", + }, + "script": Object { + "lang": "painless", + "source": "params.A / 100", + }, + }, +} +`; + +exports[`Metric Custom Transform Generator aggregates using the denominator equation with filter 1`] = ` +Object { + "bucket_script": Object { + "buckets_path": Object { + "A": "_total_A>sum", }, "script": Object { "lang": "painless", @@ -18,7 +32,21 @@ exports[`Metric Custom Transform Generator aggregates using the numerator equati Object { "bucket_script": Object { "buckets_path": Object { - "A": "_good_A", + "A": "_good_A>sum", + }, + "script": Object { + "lang": "painless", + "source": "params.A * 100", + }, + }, +} +`; + +exports[`Metric Custom Transform Generator aggregates using the numerator equation with filter 1`] = ` +Object { + "bucket_script": Object { + "buckets_path": Object { + "A": "_good_A>sum", }, "script": Object { "lang": "painless", @@ -59,24 +87,45 @@ Object { "pivot": Object { "aggregations": Object { "_good_A": Object { - "sum": Object { - "field": "total", + "aggs": Object { + "sum": Object { + "sum": Object { + "field": "total", + }, + }, + }, + "filter": Object { + "match_all": Object {}, }, }, "_good_B": Object { - "sum": Object { - "field": "processed", + "aggs": Object { + "sum": Object { + "sum": Object { + "field": "processed", + }, + }, + }, + "filter": Object { + "match_all": Object {}, }, }, "_total_A": Object { - "sum": Object { - "field": "total", + "aggs": Object { + "sum": Object { + "sum": Object { + "field": "total", + }, + }, + }, + "filter": Object { + "match_all": Object {}, }, }, "slo.denominator": Object { "bucket_script": Object { "buckets_path": Object { - "A": "_total_A", + "A": "_total_A>sum", }, "script": Object { "lang": "painless", @@ -96,8 +145,8 @@ Object { "slo.numerator": Object { "bucket_script": Object { "buckets_path": Object { - "A": "_good_A", - "B": "_good_B", + "A": "_good_A>sum", + "B": "_good_B>sum", }, "script": Object { "lang": "painless", @@ -183,24 +232,45 @@ Object { "pivot": Object { "aggregations": Object { "_good_A": Object { - "sum": Object { - "field": "total", + "aggs": Object { + "sum": Object { + "sum": Object { + "field": "total", + }, + }, + }, + "filter": Object { + "match_all": Object {}, }, }, "_good_B": Object { - "sum": Object { - "field": "processed", + "aggs": Object { + "sum": Object { + "sum": Object { + "field": "processed", + }, + }, + }, + "filter": Object { + "match_all": Object {}, }, }, "_total_A": Object { - "sum": Object { - "field": "total", + "aggs": Object { + "sum": Object { + "sum": Object { + "field": "total", + }, + }, + }, + "filter": Object { + "match_all": Object {}, }, }, "slo.denominator": Object { "bucket_script": Object { "buckets_path": Object { - "A": "_total_A", + "A": "_total_A>sum", }, "script": Object { "lang": "painless", @@ -211,8 +281,8 @@ Object { "slo.numerator": Object { "bucket_script": Object { "buckets_path": Object { - "A": "_good_A", - "B": "_good_B", + "A": "_good_A>sum", + "B": "_good_B>sum", }, "script": Object { "lang": "painless", diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/metric_custom.test.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/metric_custom.test.ts index b202fb9d5ab2f..beea8164b1c99 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/metric_custom.test.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/metric_custom.test.ts @@ -27,6 +27,17 @@ describe('Metric Custom Transform Generator', () => { }); expect(() => generator.getTransformParams(anSLO)).toThrow(/Invalid equation/); }); + it('throws when the good filter is invalid', () => { + const anSLO = createSLO({ + indicator: createMetricCustomIndicator({ + good: { + metrics: [{ name: 'A', aggregation: 'sum', field: 'good', filter: 'foo:' }], + equation: 'A', + }, + }), + }); + expect(() => generator.getTransformParams(anSLO)).toThrow(/Invalid KQL: foo:/); + }); it('throws when the total equation is invalid', () => { const anSLO = createSLO({ indicator: createMetricCustomIndicator({ @@ -38,6 +49,17 @@ describe('Metric Custom Transform Generator', () => { }); expect(() => generator.getTransformParams(anSLO)).toThrow(/Invalid equation/); }); + it('throws when the total filter is invalid', () => { + const anSLO = createSLO({ + indicator: createMetricCustomIndicator({ + total: { + metrics: [{ name: 'A', aggregation: 'sum', field: 'total', filter: 'foo:' }], + equation: 'A', + }, + }), + }); + expect(() => generator.getTransformParams(anSLO)).toThrow(/Invalid KQL: foo:/); + }); it('throws when the query_filter is invalid', () => { const anSLO = createSLO({ indicator: createMetricCustomIndicator({ filter: '{ kql.query: invalid' }), @@ -120,6 +142,22 @@ describe('Metric Custom Transform Generator', () => { expect(transform.pivot!.aggregations!['slo.numerator']).toMatchSnapshot(); }); + it('aggregates using the numerator equation with filter', async () => { + const anSLO = createSLO({ + indicator: createMetricCustomIndicator({ + good: { + metrics: [ + { name: 'A', aggregation: 'sum', field: 'good', filter: 'outcome: "success" ' }, + ], + equation: 'A * 100', + }, + }), + }); + const transform = generator.getTransformParams(anSLO); + + expect(transform.pivot!.aggregations!['slo.numerator']).toMatchSnapshot(); + }); + it('aggregates using the denominator equation', async () => { const anSLO = createSLO({ indicator: createMetricCustomIndicator({ @@ -133,4 +171,18 @@ describe('Metric Custom Transform Generator', () => { expect(transform.pivot!.aggregations!['slo.denominator']).toMatchSnapshot(); }); + + it('aggregates using the denominator equation with filter', async () => { + const anSLO = createSLO({ + indicator: createMetricCustomIndicator({ + total: { + metrics: [{ name: 'A', aggregation: 'sum', field: 'total', filter: 'outcome: *' }], + equation: 'A / 100', + }, + }), + }); + const transform = generator.getTransformParams(anSLO); + + expect(transform.pivot!.aggregations!['slo.denominator']).toMatchSnapshot(); + }); }); diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/metric_custom.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/metric_custom.ts index 8ec4ca7468384..3dc7f0044d776 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/metric_custom.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/metric_custom.ts @@ -62,15 +62,22 @@ export class MetricCustomTransformGenerator extends TransformGenerator { } private buildMetricAggregations(type: 'good' | 'total', metricDef: MetricCustomMetricDef) { - return metricDef.metrics.reduce( - (acc, metric) => ({ + return metricDef.metrics.reduce((acc, metric) => { + const filter = metric.filter + ? getElastichsearchQueryOrThrow(metric.filter) + : { match_all: {} }; + return { ...acc, [`_${type}_${metric.name}`]: { - [metric.aggregation]: { field: metric.field }, + filter, + aggs: { + sum: { + [metric.aggregation]: { field: metric.field }, + }, + }, }, - }), - {} - ); + }; + }, {}); } private convertEquationToPainless(bucketsPath: Record, equation: string) { @@ -82,7 +89,7 @@ export class MetricCustomTransformGenerator extends TransformGenerator { private buildMetricEquation(type: 'good' | 'total', metricDef: MetricCustomMetricDef) { const bucketsPath = metricDef.metrics.reduce( - (acc, metric) => ({ ...acc, [metric.name]: `_${type}_${metric.name}` }), + (acc, metric) => ({ ...acc, [metric.name]: `_${type}_${metric.name}>sum` }), {} ); return { From fe0779e522549742ed66c84708bb02c74d4b1307 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Thu, 6 Jul 2023 10:06:14 -0400 Subject: [PATCH 5/9] [Fleet] Support id when creating a package policy through API (#161306) --- .../server/routes/package_policy/handlers.ts | 5 ++- .../apis/package_policy/create.ts | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts index 9edfad74b7c5b..4738342cbb54f 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts @@ -220,7 +220,7 @@ export const createPackagePolicyHandler: FleetRequestHandler< const soClient = fleetContext.internalSoClient; const esClient = coreContext.elasticsearch.client.asInternalUser; const user = appContextService.getSecurity()?.authc.getCurrentUser(request) || undefined; - const { force, package: pkg, ...newPolicy } = request.body; + const { force, id, package: pkg, ...newPolicy } = request.body; const authorizationHeader = HTTPAuthorizationHeader.parseFromRequest(request, user?.username); if ('output_id' in newPolicy) { @@ -252,13 +252,12 @@ export const createPackagePolicyHandler: FleetRequestHandler< } // Create package policy - const packagePolicy = await fleetContext.packagePolicyService.asCurrentUser.create( soClient, esClient, newPackagePolicy, { - user, + id, force, spaceId, authorizationHeader, diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/create.ts b/x-pack/test/fleet_api_integration/apis/package_policy/create.ts index e8e19377e97cb..d65980b3c5bde 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/create.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/create.ts @@ -539,6 +539,37 @@ export default function (providerContext: FtrProviderContext) { }); describe('Simplified package policy', () => { + it('should support providing an id', async () => { + const id = `test-id-${Date.now()}`; + + await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + id, + name: `create-simplified-package-policy-required-variables-${Date.now()}`, + description: '', + namespace: 'default', + policy_id: agentPolicyId, + inputs: { + 'with_required_variables-test_input': { + streams: { + 'with_required_variables.log': { + vars: { test_var_required: 'I am required' }, + }, + }, + }, + }, + package: { + name: 'with_required_variables', + version: '0.1.0', + }, + }) + .expect(200); + + await await supertest.get(`/api/fleet/package_policies/${id}`).expect(200); + }); + it('should work with valid values', async () => { await supertest .post(`/api/fleet/package_policies`) From 0c03f1010ea5cf7ebac6bc99bc1753a001c3259e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Thu, 6 Jul 2023 11:44:30 -0300 Subject: [PATCH 6/9] [Profiling] fixing user privileges (#161269) This PR adds the `.profiling-*` to the profiling-reader role. --- .../server/lib/setup/security_role.ts | 9 +++- .../plugins/profiling/server/routes/setup.ts | 52 +++++++++++-------- 2 files changed, 38 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/profiling/server/lib/setup/security_role.ts b/x-pack/plugins/profiling/server/lib/setup/security_role.ts index 62a4d2d6d284f..1827bf73e5d2a 100644 --- a/x-pack/plugins/profiling/server/lib/setup/security_role.ts +++ b/x-pack/plugins/profiling/server/lib/setup/security_role.ts @@ -9,15 +9,17 @@ import { ProfilingSetupOptions } from './types'; import { PartialSetupState } from '../../../common/setup'; const PROFILING_READER_ROLE_NAME = 'profiling-reader'; +const METADATA_VERSION = 1; export async function validateSecurityRole({ client, }: ProfilingSetupOptions): Promise { const esClient = client.getEsClient(); const roles = await esClient.security.getRole(); + const profilingRole = roles[PROFILING_READER_ROLE_NAME]; return { permissions: { - configured: PROFILING_READER_ROLE_NAME in roles, + configured: !!profilingRole && profilingRole.metadata.version === METADATA_VERSION, }, }; } @@ -28,10 +30,13 @@ export async function setSecurityRole({ client }: ProfilingSetupOptions) { name: PROFILING_READER_ROLE_NAME, indices: [ { - names: ['profiling-*'], + names: ['profiling-*', '.profiling-*'], privileges: ['read', 'view_index_metadata'], }, ], cluster: ['monitor'], + metadata: { + version: METADATA_VERSION, + }, }); } diff --git a/x-pack/plugins/profiling/server/routes/setup.ts b/x-pack/plugins/profiling/server/routes/setup.ts index 6f728eb2da73e..c23f3516109e7 100644 --- a/x-pack/plugins/profiling/server/routes/setup.ts +++ b/x-pack/plugins/profiling/server/routes/setup.ts @@ -58,6 +58,11 @@ export function registerSetupRoute({ request, useDefaultAuth: true, }); + const clientWithProfilingAuth = createProfilingEsClient({ + esClient, + request, + useDefaultAuth: false, + }); const setupOptions: ProfilingSetupOptions = { client: clientWithDefaultAuth, @@ -84,7 +89,10 @@ export function registerSetupRoute({ }); } - state.data.available = await hasProfilingData(setupOptions); + state.data.available = await hasProfilingData({ + ...setupOptions, + client: clientWithProfilingAuth, + }); if (state.data.available) { return response.ok({ body: { @@ -163,31 +171,33 @@ export function registerSetupRoute({ }); } - const verifyFunctions = [ - isApmPackageInstalled, - validateApmPolicy, - validateCollectorPackagePolicy, - validateMaximumBuckets, - validateResourceManagement, - validateSecurityRole, - validateSymbolizerPackagePolicy, - ]; - const partialStates = await Promise.all(verifyFunctions.map((fn) => fn(setupOptions))); + const partialStates = await Promise.all( + [ + isApmPackageInstalled, + validateApmPolicy, + validateCollectorPackagePolicy, + validateMaximumBuckets, + validateResourceManagement, + validateSecurityRole, + validateSymbolizerPackagePolicy, + ].map((fn) => fn(setupOptions)) + ); const mergedState = mergePartialSetupStates(state, partialStates); - if (areResourcesSetup(mergedState)) { + const executeFunctions = [ + ...(mergedState.packages.installed ? [] : [installLatestApmPackage]), + ...(mergedState.policies.apm.installed ? [] : [updateApmPolicy]), + ...(mergedState.policies.collector.installed ? [] : [createCollectorPackagePolicy]), + ...(mergedState.policies.symbolizer.installed ? [] : [createSymbolizerPackagePolicy]), + ...(mergedState.resource_management.enabled ? [] : [enableResourceManagement]), + ...(mergedState.permissions.configured ? [] : [setSecurityRole]), + ...(mergedState.settings.configured ? [] : [setMaximumBuckets]), + ]; + + if (!executeFunctions.length) { return response.ok(); } - const executeFunctions = [ - installLatestApmPackage, - updateApmPolicy, - createCollectorPackagePolicy, - createSymbolizerPackagePolicy, - enableResourceManagement, - setSecurityRole, - setMaximumBuckets, - ]; await Promise.all(executeFunctions.map((fn) => fn(setupOptions))); // We return a status code of 202 instead of 200 because enabling From 9e5d6b3e0fa731a37374c091cd7284d1c41f116e Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Thu, 6 Jul 2023 10:46:55 -0400 Subject: [PATCH 7/9] [Fleet] Adjust background File cleanup task to also process `to-host` indexes (#161138) ## Summary - Updates the `fleet:check-deleted-files-task` to include the indexes that store files for delivery to the Host (currently used only by Endpoint integration) --- .../common/services/file_storage.test.ts | 17 ---- .../fleet/common/services/file_storage.ts | 38 --------- .../fleet/server/services/files/index.test.ts | 7 +- .../fleet/server/services/files/index.ts | 33 ++++---- .../fleet/server/services/files/utils.test.ts | 66 ++++++++++++++++ .../fleet/server/services/files/utils.ts | 77 +++++++++++++++++++ .../tasks/check_deleted_files_task.test.ts | 35 ++++++++- 7 files changed, 198 insertions(+), 75 deletions(-) create mode 100644 x-pack/plugins/fleet/server/services/files/utils.test.ts create mode 100644 x-pack/plugins/fleet/server/services/files/utils.ts diff --git a/x-pack/plugins/fleet/common/services/file_storage.test.ts b/x-pack/plugins/fleet/common/services/file_storage.test.ts index dbf5da61dba1d..4e4292a57808a 100644 --- a/x-pack/plugins/fleet/common/services/file_storage.test.ts +++ b/x-pack/plugins/fleet/common/services/file_storage.test.ts @@ -5,12 +5,8 @@ * 2.0. */ -import { FILE_STORAGE_METADATA_INDEX_PATTERN } from '../constants'; - import { getFileDataIndexName, getFileMetadataIndexName } from '..'; -import { getIntegrationNameFromIndexName } from './file_storage'; - describe('File Storage services', () => { describe('File Index Names', () => { it('should generate file metadata index name for files received from host', () => { @@ -29,17 +25,4 @@ describe('File Storage services', () => { expect(getFileDataIndexName('foo', true)).toEqual('.fleet-fileds-tohost-data-foo'); }); }); - - describe('getIntegrationNameFromIndexName()', () => { - it.each([ - ['regular index names', '.fleet-fileds-fromhost-meta-agent'], - ['datastream index names', '.ds-.fleet-fileds-fromhost-data-agent-2023.06.30-00001'], - ])('should handle %s', (_, index) => { - expect(getIntegrationNameFromIndexName(index, FILE_STORAGE_METADATA_INDEX_PATTERN)).toEqual( - 'agent' - ); - }); - - it.todo('should error if index pattern does not include `*`'); - }); }); diff --git a/x-pack/plugins/fleet/common/services/file_storage.ts b/x-pack/plugins/fleet/common/services/file_storage.ts index af909a22aa946..7bbc9dd2485f4 100644 --- a/x-pack/plugins/fleet/common/services/file_storage.ts +++ b/x-pack/plugins/fleet/common/services/file_storage.ts @@ -55,41 +55,3 @@ export const getFileDataIndexName = ( `Unable to define integration file data index. No '*' in index pattern: ${dataIndex}` ); }; - -/** - * Returns back the integration name for a given File Data (chunks) index name. - * - * @example - * // Given a File data index pattern of `.fleet-fileds-fromhost-data-*`: - * - * getIntegrationNameFromFileDataIndexName('.fleet-fileds-fromhost-data-agent'); - * // return 'agent' - * - * getIntegrationNameFromFileDataIndexName('.ds-.fleet-fileds-fromhost-data-agent'); - * // return 'agent' - * - * getIntegrationNameFromFileDataIndexName('.ds-.fleet-fileds-fromhost-data-agent-2023.06.30-00001'); - * // return 'agent' - */ -export const getIntegrationNameFromFileDataIndexName = (indexName: string): string => { - return getIntegrationNameFromIndexName(indexName, FILE_STORAGE_DATA_INDEX_PATTERN); -}; - -export const getIntegrationNameFromIndexName = ( - indexName: string, - indexPattern: string -): string => { - const integrationNameIndexPosition = indexPattern.split('-').indexOf('*'); - - if (integrationNameIndexPosition === -1) { - throw new Error(`Unable to parse index name. No '*' in index pattern: ${indexPattern}`); - } - - const indexPieces = indexName.replace(/^\.ds-/, '').split('-'); - - if (indexPieces[integrationNameIndexPosition]) { - return indexPieces[integrationNameIndexPosition]; - } - - throw new Error(`Index name ${indexName} does not seem to be a File storage index`); -}; diff --git a/x-pack/plugins/fleet/server/services/files/index.test.ts b/x-pack/plugins/fleet/server/services/files/index.test.ts index f37ff95d1ea96..551cded5d66b2 100644 --- a/x-pack/plugins/fleet/server/services/files/index.test.ts +++ b/x-pack/plugins/fleet/server/services/files/index.test.ts @@ -11,6 +11,8 @@ import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; import { FILE_STORAGE_DATA_INDEX_PATTERN, FILE_STORAGE_METADATA_INDEX_PATTERN, + FILE_STORAGE_TO_HOST_DATA_INDEX_PATTERN, + FILE_STORAGE_TO_HOST_METADATA_INDEX_PATTERN, } from '../../../common/constants/file_storage'; import { getFileDataIndexName, getFileMetadataIndexName } from '../../../common/services'; @@ -67,7 +69,7 @@ describe('files service', () => { expect(esClientMock.search).toBeCalledWith( { - index: FILE_STORAGE_METADATA_INDEX_PATTERN, + index: [FILE_STORAGE_METADATA_INDEX_PATTERN, FILE_STORAGE_TO_HOST_METADATA_INDEX_PATTERN], body: { size: ES_SEARCH_LIMIT, query: { @@ -130,7 +132,8 @@ describe('files service', () => { expect(esClientMock.search).toBeCalledWith( { - index: FILE_STORAGE_DATA_INDEX_PATTERN, + ignore_unavailable: true, + index: [FILE_STORAGE_DATA_INDEX_PATTERN, FILE_STORAGE_TO_HOST_DATA_INDEX_PATTERN], body: { size: ES_SEARCH_LIMIT, query: { diff --git a/x-pack/plugins/fleet/server/services/files/index.ts b/x-pack/plugins/fleet/server/services/files/index.ts index 8d6cbdb9fd5a4..48303c3611fc7 100644 --- a/x-pack/plugins/fleet/server/services/files/index.ts +++ b/x-pack/plugins/fleet/server/services/files/index.ts @@ -12,18 +12,19 @@ import type { FileStatus } from '@kbn/files-plugin/common/types'; import { FILE_STORAGE_DATA_INDEX_PATTERN, FILE_STORAGE_METADATA_INDEX_PATTERN, + FILE_STORAGE_TO_HOST_DATA_INDEX_PATTERN, + FILE_STORAGE_TO_HOST_METADATA_INDEX_PATTERN, } from '../../../common/constants'; -import { - getFileMetadataIndexName, - getIntegrationNameFromFileDataIndexName, - getIntegrationNameFromIndexName, -} from '../../../common/services'; +import { getFileMetadataIndexName } from '../../../common/services'; import { ES_SEARCH_LIMIT } from '../../../common/constants'; +import { parseFileStorageIndex } from './utils'; + /** - * Gets files with given status + * Gets files with given status from the files metadata index. Includes both files + * `tohost` and files `fromhost` * * @param esClient * @param abortController @@ -37,7 +38,7 @@ export async function getFilesByStatus( const result = await esClient .search( { - index: FILE_STORAGE_METADATA_INDEX_PATTERN, + index: [FILE_STORAGE_METADATA_INDEX_PATTERN, FILE_STORAGE_TO_HOST_METADATA_INDEX_PATTERN], body: { size: ES_SEARCH_LIMIT, query: { @@ -79,20 +80,18 @@ export async function fileIdsWithoutChunksByIndex( const noChunkFileIdsByIndex = files.reduce((acc, file) => { allFileIds.add(file._id); - const integration = getIntegrationNameFromIndexName( - file._index, - FILE_STORAGE_METADATA_INDEX_PATTERN - ); - const metadataIndex = getFileMetadataIndexName(integration); + const { index: metadataIndex } = parseFileStorageIndex(file._index); const fileIds = acc[metadataIndex]; + acc[metadataIndex] = fileIds ? fileIds.add(file._id) : new Set([file._id]); + return acc; }, {} as FileIdsByIndex); const chunks = await esClient .search<{ bid: string }>( { - index: FILE_STORAGE_DATA_INDEX_PATTERN, + index: [FILE_STORAGE_DATA_INDEX_PATTERN, FILE_STORAGE_TO_HOST_DATA_INDEX_PATTERN], body: { size: ES_SEARCH_LIMIT, query: { @@ -113,6 +112,7 @@ export async function fileIdsWithoutChunksByIndex( }, _source: ['bid'], }, + ignore_unavailable: true, }, { signal: abortController.signal } ) @@ -123,9 +123,12 @@ export async function fileIdsWithoutChunksByIndex( chunks.hits.hits.forEach((hit) => { const fileId = hit._source?.bid; + if (!fileId) return; - const integration = getIntegrationNameFromFileDataIndexName(hit._index); - const metadataIndex = getFileMetadataIndexName(integration); + + const { integration, direction } = parseFileStorageIndex(hit._index); + const metadataIndex = getFileMetadataIndexName(integration, direction === 'to-host'); + if (noChunkFileIdsByIndex[metadataIndex]?.delete(fileId)) { allFileIds.delete(fileId); } diff --git a/x-pack/plugins/fleet/server/services/files/utils.test.ts b/x-pack/plugins/fleet/server/services/files/utils.test.ts new file mode 100644 index 0000000000000..876f2f9143f8f --- /dev/null +++ b/x-pack/plugins/fleet/server/services/files/utils.test.ts @@ -0,0 +1,66 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getFileDataIndexName, getFileMetadataIndexName } from '../../../common'; + +import { parseFileStorageIndex } from './utils'; + +describe('Files service utils', () => { + describe('parseFileStorageIndex()', () => { + it.each([ + [ + 'tohost meta', + '.ds-.fleet-fileds-tohost-meta-endpoint-2023.07.03-000001', + { + index: getFileMetadataIndexName('endpoint', true), + integration: 'endpoint', + direction: 'to-host', + type: 'meta', + }, + ], + [ + 'tohost data', + '.ds-.fleet-fileds-tohost-data-agent-2023.07.03-000001', + { + index: getFileDataIndexName('agent', true), + integration: 'agent', + direction: 'to-host', + type: 'data', + }, + ], + [ + 'fromhost meta', + '.ds-.fleet-fileds-fromhost-meta-agent-2023.07.03-000001', + { + index: getFileMetadataIndexName('agent'), + integration: 'agent', + direction: 'from-host', + type: 'meta', + }, + ], + [ + 'fromhost data', + '.ds-.fleet-fileds-fromhost-data-endpoint-2023.07.03-000001', + { + index: getFileDataIndexName('endpoint'), + integration: 'endpoint', + direction: 'from-host', + type: 'data', + }, + ], + ])('should parse index %s', (_, index, result) => { + expect(parseFileStorageIndex(index)).toEqual(result); + }); + + it('should error if index does not match a known pattern', () => { + expect(() => parseFileStorageIndex('foo')).toThrow( + 'Unable to parse index [foo]. Does not match a known index pattern: [.fleet-fileds-fromhost-meta-* | ' + + '.fleet-fileds-fromhost-data-* | .fleet-fileds-tohost-meta-* | .fleet-fileds-tohost-data-*]' + ); + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/files/utils.ts b/x-pack/plugins/fleet/server/services/files/utils.ts new file mode 100644 index 0000000000000..281f14cd7e30b --- /dev/null +++ b/x-pack/plugins/fleet/server/services/files/utils.ts @@ -0,0 +1,77 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getFileDataIndexName, getFileMetadataIndexName } from '../../../common/services'; + +import { + FILE_STORAGE_DATA_INDEX_PATTERN, + FILE_STORAGE_METADATA_INDEX_PATTERN, + FILE_STORAGE_TO_HOST_DATA_INDEX_PATTERN, + FILE_STORAGE_TO_HOST_METADATA_INDEX_PATTERN, +} from '../../../common/constants'; + +interface ParsedFileStorageIndex { + index: string; + integration: string; + type: 'meta' | 'data'; + direction: 'to-host' | 'from-host'; +} + +/** + * Given a document index (from either a file's metadata doc or a file's chunk doc), utility will + * parse it and return information about that index + * @param index + */ +export const parseFileStorageIndex = (index: string): ParsedFileStorageIndex => { + const response: ParsedFileStorageIndex = { + index: '', + integration: '', + type: 'meta', + direction: 'from-host', + }; + + const fileStorageIndexPatterns = [ + FILE_STORAGE_METADATA_INDEX_PATTERN, + FILE_STORAGE_DATA_INDEX_PATTERN, + + FILE_STORAGE_TO_HOST_METADATA_INDEX_PATTERN, + FILE_STORAGE_TO_HOST_DATA_INDEX_PATTERN, + ]; + + for (const indexPattern of fileStorageIndexPatterns) { + const indexPrefix = indexPattern.substring(0, indexPattern.indexOf('*')); + + if (index.includes(indexPrefix)) { + const isDeliveryToHost = index.includes('-tohost-'); + const isDataIndex = index.includes('host-data-'); + const integrationPosition = indexPattern.split('-').indexOf('*'); + const integration = index + .replace(/^\.ds-/, '') + .split('-') + .at(integrationPosition); + + if (!integration) { + throw new Error(`Index name ${index} does not seem to be a File storage index`); + } + + response.direction = isDeliveryToHost ? 'to-host' : 'from-host'; + response.type = isDataIndex ? 'data' : 'meta'; + response.integration = integration; + response.index = isDataIndex + ? getFileDataIndexName(response.integration, isDeliveryToHost) + : getFileMetadataIndexName(response.integration, isDeliveryToHost); + + return response; + } + } + + throw new Error( + `Unable to parse index [${index}]. Does not match a known index pattern: [${fileStorageIndexPatterns.join( + ' | ' + )}]` + ); +}; diff --git a/x-pack/plugins/fleet/server/tasks/check_deleted_files_task.test.ts b/x-pack/plugins/fleet/server/tasks/check_deleted_files_task.test.ts index 353b52acc8c13..909a78f74ae34 100644 --- a/x-pack/plugins/fleet/server/tasks/check_deleted_files_task.test.ts +++ b/x-pack/plugins/fleet/server/tasks/check_deleted_files_task.test.ts @@ -21,8 +21,8 @@ import { appContextService } from '../services'; import { CheckDeletedFilesTask, TYPE, VERSION } from './check_deleted_files_task'; -const MOCK_FILE_METADATA_INDEX = getFileMetadataIndexName('mock'); -const MOCK_FILE_DATA_INDEX = getFileDataIndexName('mock'); +const MOCK_FILE_METADATA_INDEX = '.ds-' + getFileMetadataIndexName('mock'); +const MOCK_FILE_DATA_INDEX = '.ds-' + getFileDataIndexName('mock'); const MOCK_TASK_INSTANCE = { id: `${TYPE}:${VERSION}`, @@ -100,6 +100,35 @@ describe('check deleted files task', () => { return taskRunner.run(); }; + it('should search both metadata indexes', async () => { + esClient.search.mockResolvedValue({ + took: 5, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + total: { + value: 0, + relation: 'eq', + }, + hits: [], + }, + }); + + await runTask(); + + expect(esClient.search).toHaveBeenCalledWith( + expect.objectContaining({ + index: ['.fleet-fileds-fromhost-meta-*', '.fleet-fileds-tohost-meta-*'], + }), + expect.anything() + ); + }); + it('should attempt to update deleted files', async () => { // mock getReadyFiles search esClient.search @@ -162,7 +191,7 @@ describe('check deleted files task', () => { expect(esClient.updateByQuery).toHaveBeenCalledWith( { - index: MOCK_FILE_METADATA_INDEX, + index: MOCK_FILE_METADATA_INDEX.replace('.ds-', ''), query: { ids: { values: ['metadata-testid2'], From 8afb9b086cb9a21601f1f0a4a0106aaebedb3b69 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 6 Jul 2023 07:49:49 -0700 Subject: [PATCH 8/9] [DOCS] Remove deprecated action variables from rule APIs (#161216) --- docs/api/alerting/create_rule.asciidoc | 4 ++-- docs/api/alerting/get_rules.asciidoc | 2 +- docs/api/alerting/legacy/create.asciidoc | 4 ++-- .../alerting/docs/openapi/bundled.json | 12 +++++----- .../alerting/docs/openapi/bundled.yaml | 24 +++++++++---------- .../examples/create_rule_request.yaml | 2 +- .../examples/create_rule_response.yaml | 2 +- .../examples/find_rules_response.yaml | 2 +- .../examples/get_rule_response.yaml | 2 +- .../examples/update_rule_request.yaml | 2 +- .../examples/update_rule_response.yaml | 2 +- 11 files changed, 29 insertions(+), 29 deletions(-) diff --git a/docs/api/alerting/create_rule.asciidoc b/docs/api/alerting/create_rule.asciidoc index 590c33e895ea7..c5e5bec9d061f 100644 --- a/docs/api/alerting/create_rule.asciidoc +++ b/docs/api/alerting/create_rule.asciidoc @@ -173,7 +173,7 @@ POST api/alerting/rule "group":"threshold met", "params":{ "level":"info", - "message":"alert '{{alertName}}' is active for group '{{context.group}}':\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}}\n- Timestamp: {{context.date}}" + "message":"Rule '{{rule.name}}' is active for group '{{context.group}}':\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}}\n- Timestamp: {{context.date}}" } } ], @@ -231,7 +231,7 @@ The API returns the following: "id": "dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2", "params": { "level": "info", - "message": "alert {{alertName}} is active for group {{context.group}}:\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}}\n- Timestamp: {{context.date}}" + "message": "Rule {{rule.name}} is active for group {{context.group}}:\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}}\n- Timestamp: {{context.date}}" }, "connector_type_id": ".server-log" } diff --git a/docs/api/alerting/get_rules.asciidoc b/docs/api/alerting/get_rules.asciidoc index fd291617e08d3..60c879116948a 100644 --- a/docs/api/alerting/get_rules.asciidoc +++ b/docs/api/alerting/get_rules.asciidoc @@ -103,7 +103,7 @@ The API returns the following: "id":"1007a0c0-7a6e-11ed-89d5-abec321c0def", "params":{ "level":"info", - "message":"alert {{alertName}} is active for group {{context.group}}:\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}}\n- Timestamp: {{context.date}}" + "message":"Rule {{rule.name}} is active for group {{context.group}}:\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}}\n- Timestamp: {{context.date}}" }, "connector_type_id":".server-log" }], diff --git a/docs/api/alerting/legacy/create.asciidoc b/docs/api/alerting/legacy/create.asciidoc index 8363569541356..18594b0b67f4b 100644 --- a/docs/api/alerting/legacy/create.asciidoc +++ b/docs/api/alerting/legacy/create.asciidoc @@ -127,7 +127,7 @@ $ curl -X POST api/alerts/alert -H 'kbn-xsrf: true' -H 'Content-Type: applicati "group":"threshold met", "params":{ "level":"info", - "message":"alert '{{alertName}}' is active for group '{{context.group}}':\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}}\n- Timestamp: {{context.date}}" + "message":"Rule '{{rule.name}}' is active for group '{{context.group}}':\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}}\n- Timestamp: {{context.date}}" } } ], @@ -175,7 +175,7 @@ The API returns the following: "group": "threshold met", "params": { "level": "info", - "message": "alert {{alertName}} is active for group {{context.group}}:\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}}\n- Timestamp: {{context.date}}" + "message": "Rule {{rule.name}} is active for group {{context.group}}:\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}}\n- Timestamp: {{context.date}}" }, "id": "dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2" } diff --git a/x-pack/plugins/alerting/docs/openapi/bundled.json b/x-pack/plugins/alerting/docs/openapi/bundled.json index 528661a3e3110..d97cc6d892a61 100644 --- a/x-pack/plugins/alerting/docs/openapi/bundled.json +++ b/x-pack/plugins/alerting/docs/openapi/bundled.json @@ -6397,7 +6397,7 @@ "group": "threshold met", "params": { "level": "info", - "message": "alert '{{alertName}}' is active for group '{{context.group}}':\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}}\n- Timestamp: {{context.date}}" + "message": "Rule '{{rule.name}}' is active for group '{{context.group}}':\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}}\n- Timestamp: {{context.date}}" } } ], @@ -6445,7 +6445,7 @@ }, "params": { "level": "info", - "message": "alert {{alertName}} is active for group {{context.group} :\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}}\n- Timestamp: {{context.date}}" + "message": "Rule {{rule.name}} is active for group {{context.group} :\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}}\n- Timestamp: {{context.date}}" } } ], @@ -6550,7 +6550,7 @@ "uuid": "1c7a1280-f28c-4e06-96b2-e4e5f05d1d61", "params": { "level": "info", - "message": "alert {{alertName}} is active for group {{context.group}}:\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}}\n- Timestamp: {{context.date}", + "message": "Rule {{rule.name}} is active for group {{context.group}}:\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}}\n- Timestamp: {{context.date}", "connector_type_id": ".server-log" }, "connector_type_id": ".server-log", @@ -6589,7 +6589,7 @@ "id": "96b668d0-a1b6-11ed-afdf-d39a49596974", "params": { "level": "info", - "message": "alert {{alertName}} is active for group {{context.group}}:\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}}\n- Timestamp: {{context.date}}" + "message": "Rule {{rule.name}} is active for group {{context.group}}:\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}}\n- Timestamp: {{context.date}}" } } ], @@ -6668,7 +6668,7 @@ "group": "threshold met", "params": { "level": "info", - "message": "alert {{alertName}} is active for group {{context.group}}:\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}}\n- Timestamp: {{context.date}" + "message": "Rule {{rule.name}} is active for group {{context.group}}:\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}}\n- Timestamp: {{context.date}" }, "id": "96b668d0-a1b6-11ed-afdf-d39a49596974", "uuid": "07aef2a0-9eed-4ef9-94ec-39ba58eb609d", @@ -6753,7 +6753,7 @@ "uuid": "1c7a1280-f28c-4e06-96b2-e4e5f05d1d61", "params": { "level": "info", - "message": "alert {{alertName}} is active for group {{context.group}}:\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}}\n- Timestamp: {{context.date}}", + "message": "Rule {{rule.name}} is active for group {{context.group}}:\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}}\n- Timestamp: {{context.date}}", "connector_type_id": ".server-log" }, "frequency": { diff --git a/x-pack/plugins/alerting/docs/openapi/bundled.yaml b/x-pack/plugins/alerting/docs/openapi/bundled.yaml index 2c2fb5714df39..a6c019625937b 100644 --- a/x-pack/plugins/alerting/docs/openapi/bundled.yaml +++ b/x-pack/plugins/alerting/docs/openapi/bundled.yaml @@ -4360,10 +4360,10 @@ components: params: level: info message: |- - alert '{{alertName}}' is active for group '{{context.group}}': + Rule '{{rule.name}}' is active for group '{{context.group}}': - Value: {{context.value}} - - Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}} + - Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}} - Timestamp: {{context.date}} consumer: alerts name: my rule @@ -4401,10 +4401,10 @@ components: params: level: info message: |- - alert {{alertName}} is active for group {{context.group} : + Rule {{rule.name}} is active for group {{context.group} : - Value: {{context.value}} - - Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}} + - Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}} - Timestamp: {{context.date}} api_key_created_by_user: false api_key_owner: elastic @@ -4493,10 +4493,10 @@ components: params: level: info message: |- - alert {{alertName}} is active for group {{context.group}}: + Rule {{rule.name}} is active for group {{context.group}}: - Value: {{context.value}} - - Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}} + - Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}} - Timestamp: {{context.date} connector_type_id: .server-log connector_type_id: .server-log @@ -4527,10 +4527,10 @@ components: params: level: info message: |- - alert {{alertName}} is active for group {{context.group}}: + Rule {{rule.name}} is active for group {{context.group}}: - Value: {{context.value}} - - Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}} + - Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}} - Timestamp: {{context.date}} params: aggField: sheet.version @@ -4596,10 +4596,10 @@ components: params: level: info message: |- - alert {{alertName}} is active for group {{context.group}}: + Rule {{rule.name}} is active for group {{context.group}}: - Value: {{context.value}} - - Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}} + - Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}} - Timestamp: {{context.date} id: 96b668d0-a1b6-11ed-afdf-d39a49596974 uuid: 07aef2a0-9eed-4ef9-94ec-39ba58eb609d @@ -4670,10 +4670,10 @@ components: params: level: info message: |- - alert {{alertName}} is active for group {{context.group}}: + Rule {{rule.name}} is active for group {{context.group}}: - Value: {{context.value}} - - Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}} + - Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}} - Timestamp: {{context.date}} connector_type_id: .server-log frequency: diff --git a/x-pack/plugins/alerting/docs/openapi/components/examples/create_rule_request.yaml b/x-pack/plugins/alerting/docs/openapi/components/examples/create_rule_request.yaml index 1b09d410667d5..801d298c5f922 100644 --- a/x-pack/plugins/alerting/docs/openapi/components/examples/create_rule_request.yaml +++ b/x-pack/plugins/alerting/docs/openapi/components/examples/create_rule_request.yaml @@ -8,7 +8,7 @@ value: group: threshold met params: level: info - message: "alert '{{alertName}}' is active for group '{{context.group}}':\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}}\n- Timestamp: {{context.date}}" + message: "Rule '{{rule.name}}' is active for group '{{context.group}}':\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}}\n- Timestamp: {{context.date}}" consumer: alerts name: my rule params: diff --git a/x-pack/plugins/alerting/docs/openapi/components/examples/create_rule_response.yaml b/x-pack/plugins/alerting/docs/openapi/components/examples/create_rule_response.yaml index c29389cada936..cf755e08d3bfd 100644 --- a/x-pack/plugins/alerting/docs/openapi/components/examples/create_rule_response.yaml +++ b/x-pack/plugins/alerting/docs/openapi/components/examples/create_rule_response.yaml @@ -11,7 +11,7 @@ value: throttle: null params: level: info - message: "alert {{alertName}} is active for group {{context.group} :\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}}\n- Timestamp: {{context.date}}" + message: "Rule {{rule.name}} is active for group {{context.group} :\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}}\n- Timestamp: {{context.date}}" api_key_created_by_user: false api_key_owner: elastic consumer: alerts diff --git a/x-pack/plugins/alerting/docs/openapi/components/examples/find_rules_response.yaml b/x-pack/plugins/alerting/docs/openapi/components/examples/find_rules_response.yaml index a4f82165ed7a0..b6af1580d8e40 100644 --- a/x-pack/plugins/alerting/docs/openapi/components/examples/find_rules_response.yaml +++ b/x-pack/plugins/alerting/docs/openapi/components/examples/find_rules_response.yaml @@ -47,7 +47,7 @@ value: uuid: 1c7a1280-f28c-4e06-96b2-e4e5f05d1d61 params: level: info - message: "alert {{alertName}} is active for group {{context.group}}:\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}}\n- Timestamp: {{context.date}}" + message: "Rule {{rule.name}} is active for group {{context.group}}:\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}}\n- Timestamp: {{context.date}}" connector_type_id: .server-log frequency: summary: false diff --git a/x-pack/plugins/alerting/docs/openapi/components/examples/get_rule_response.yaml b/x-pack/plugins/alerting/docs/openapi/components/examples/get_rule_response.yaml index cdd0185115fba..98d2a4462ead5 100644 --- a/x-pack/plugins/alerting/docs/openapi/components/examples/get_rule_response.yaml +++ b/x-pack/plugins/alerting/docs/openapi/components/examples/get_rule_response.yaml @@ -44,7 +44,7 @@ value: uuid: 1c7a1280-f28c-4e06-96b2-e4e5f05d1d61 params: level: info - message: "alert {{alertName}} is active for group {{context.group}}:\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}}\n- Timestamp: {{context.date}" + message: "Rule {{rule.name}} is active for group {{context.group}}:\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}}\n- Timestamp: {{context.date}" connector_type_id: .server-log connector_type_id: .server-log frequency: diff --git a/x-pack/plugins/alerting/docs/openapi/components/examples/update_rule_request.yaml b/x-pack/plugins/alerting/docs/openapi/components/examples/update_rule_request.yaml index 20ee916284ed0..80462cc4d4db3 100644 --- a/x-pack/plugins/alerting/docs/openapi/components/examples/update_rule_request.yaml +++ b/x-pack/plugins/alerting/docs/openapi/components/examples/update_rule_request.yaml @@ -8,7 +8,7 @@ value: id: 96b668d0-a1b6-11ed-afdf-d39a49596974 params: level: info - message: "alert {{alertName}} is active for group {{context.group}}:\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}}\n- Timestamp: {{context.date}}" + message: "Rule {{rule.name}} is active for group {{context.group}}:\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}}\n- Timestamp: {{context.date}}" params: aggField: sheet.version aggType: avg diff --git a/x-pack/plugins/alerting/docs/openapi/components/examples/update_rule_response.yaml b/x-pack/plugins/alerting/docs/openapi/components/examples/update_rule_response.yaml index 2d0f56a04b6b7..17a23811ec2e4 100644 --- a/x-pack/plugins/alerting/docs/openapi/components/examples/update_rule_response.yaml +++ b/x-pack/plugins/alerting/docs/openapi/components/examples/update_rule_response.yaml @@ -39,7 +39,7 @@ value: - group: threshold met params: level: info - message: "alert {{alertName}} is active for group {{context.group}}:\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}}\n- Timestamp: {{context.date}" + message: "Rule {{rule.name}} is active for group {{context.group}}:\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}}\n- Timestamp: {{context.date}" id: 96b668d0-a1b6-11ed-afdf-d39a49596974 uuid: 07aef2a0-9eed-4ef9-94ec-39ba58eb609d connector_type_id: .server-log From b641a22438ceb0a615fbf3ea15d44fd44ca8b748 Mon Sep 17 00:00:00 2001 From: jennypavlova Date: Thu, 6 Jul 2023 17:10:38 +0200 Subject: [PATCH 9/9] [Infrastructure UI] Asset Details: Add pins to the metadata table (#161074) Closes #155190 ## Summary This PR adds the possibility to pin different rows inside the metadata table in asset details embeddable. The pins are persisted in the local storage and should be available after refreshing/reopening the host flyout. The order and sorting are explained in [this comment](https://github.com/elastic/kibana/issues/155190#issuecomment-1523335704), so basically we keep the original sorting order of the table (`host`, `cloud`, `agent`) also for the pins. ## Testing - Go to hosts view and open a single host flyout (metadata tab) - Try to add / remove pins - Check if the pins are persisted after a page refresh https://github.com/elastic/kibana/assets/14139027/62873e7e-b5f0-444c-94ff-5e19f2f46f58 --- .../tabs/metadata/add_pin_to_row.tsx | 75 +++++++++++++++++++ .../asset_details/tabs/metadata/table.tsx | 52 ++++++++++--- .../asset_details/tabs/metadata/utils.ts | 21 +++++- .../test/functional/apps/infra/hosts_view.ts | 20 ++++- .../page_objects/infra_hosts_view.ts | 12 +++ 5 files changed, 166 insertions(+), 14 deletions(-) create mode 100644 x-pack/plugins/infra/public/components/asset_details/tabs/metadata/add_pin_to_row.tsx diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/add_pin_to_row.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/add_pin_to_row.tsx new file mode 100644 index 0000000000000..a1e7c3f106497 --- /dev/null +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/add_pin_to_row.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Dispatch } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import type { Field } from './utils'; + +interface AddMetadataPinToRowProps { + fieldName: Field['name']; + pinnedItems: Array; + onPinned: Dispatch | undefined>>; +} + +const PIN_FIELD = i18n.translate('xpack.infra.metadataEmbeddable.pinField', { + defaultMessage: 'Pin Field', +}); + +export const AddMetadataPinToRow = ({ + fieldName, + pinnedItems, + onPinned, +}: AddMetadataPinToRowProps) => { + const handleAddPin = () => { + onPinned([...pinnedItems, fieldName]); + }; + + const handleRemovePin = () => { + if (pinnedItems && pinnedItems.includes(fieldName)) { + onPinned((pinnedItems ?? []).filter((pinName: string) => fieldName !== pinName)); + } + }; + + if (pinnedItems?.includes(fieldName)) { + return ( + + + + + + ); + } + + return ( + + + + + + ); +}; diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/table.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/table.tsx index b918e50781778..544af40a414a3 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/table.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/table.tsx @@ -10,6 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink, + EuiIcon, EuiInMemoryTable, EuiSearchBarProps, type HorizontalAlignment, @@ -20,15 +21,13 @@ import { FormattedMessage } from '@kbn/i18n-react'; import useToggle from 'react-use/lib/useToggle'; import { debounce } from 'lodash'; import { Query } from '@elastic/eui'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; import { AddMetadataFilterButton } from './add_metadata_filter_button'; - -interface Row { - name: string; - value: string | string[] | undefined; -} +import { type Field, getRowsWithPins } from './utils'; +import { AddMetadataPinToRow } from './add_pin_to_row'; export interface Props { - rows: Row[]; + rows: Field[]; loading: boolean; showActionsColumn?: boolean; search?: string; @@ -65,12 +64,43 @@ const LOADING = i18n.translate('xpack.infra.metadataEmbeddable.loading', { defaultMessage: 'Loading...', }); +const LOCAL_STORAGE_PINNED_METADATA_ROWS = 'hostsView:pinnedMetadataRows'; + export const Table = ({ loading, rows, onSearchChange, search, showActionsColumn }: Props) => { const [searchError, setSearchError] = useState(null); const [metadataSearch, setMetadataSearch] = useState(search); + const [fieldsWithPins, setFieldsWithPins] = useState(rows); + + const [pinnedItems, setPinnedItems] = useLocalStorage>( + LOCAL_STORAGE_PINNED_METADATA_ROWS, + [] + ); + + useMemo(() => { + if (pinnedItems) { + setFieldsWithPins(getRowsWithPins(rows, pinnedItems) ?? rows); + } + }, [rows, pinnedItems]); const defaultColumns = useMemo( () => [ + { + field: 'value', + name: , + align: 'center' as HorizontalAlignment, + width: '5%', + sortable: false, + showOnHover: true, + render: (_name: string, item: Field) => { + return ( + + ); + }, + }, { field: 'name', name: FIELD_LABEL, @@ -81,12 +111,12 @@ export const Table = ({ loading, rows, onSearchChange, search, showActionsColumn { field: 'value', name: VALUE_LABEL, - width: '55%', + width: '50%', sortable: false, - render: (_name: string, item: Row) => , + render: (_name: string, item: Field) => , }, ], - [] + [pinnedItems, setPinnedItems] ); const debouncedSearchOnChange = useMemo( @@ -134,7 +164,7 @@ export const Table = ({ loading, rows, onSearchChange, search, showActionsColumn sortable: false, showOnHover: true, align: 'center' as HorizontalAlignment, - render: (_name: string, item: Row) => { + render: (_name: string, item: Field) => { return ; }, }, @@ -149,7 +179,7 @@ export const Table = ({ loading, rows, onSearchChange, search, showActionsColumn tableLayout={'fixed'} responsive={false} columns={columns} - items={rows} + items={fieldsWithPins} rowProps={{ className: 'euiTableRow-hasActions' }} search={searchBar} loading={loading} diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/utils.ts b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/utils.ts index f277eb1c337e5..e52242c74d4f6 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/utils.ts +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/utils.ts @@ -7,6 +7,11 @@ import type { InfraMetadata } from '../../../../../common/http_api'; +export interface Field { + name: string; + value: string | string[] | undefined; +} + export const getAllFields = (metadata: InfraMetadata | null) => { if (!metadata?.info) return []; return prune([ @@ -105,5 +110,17 @@ export const getAllFields = (metadata: InfraMetadata | null) => { ]); }; -const prune = (fields: Array<{ name: string; value: string | string[] | undefined }>) => - fields.filter((f) => !!f.value); +const prune = (fields: Field[]) => fields.filter((f) => !!f.value); + +export const getRowsWithPins = (rows: Field[], pinnedItems: Array) => { + if (pinnedItems.length > 0) { + const { pinned, other } = rows.reduce( + (acc, row) => { + (pinnedItems.includes(row.name) ? acc.pinned : acc.other).push(row); + return acc; + }, + { pinned: [] as Field[], other: [] as Field[] } + ); + return [...pinned, ...other]; + } +}; diff --git a/x-pack/test/functional/apps/infra/hosts_view.ts b/x-pack/test/functional/apps/infra/hosts_view.ts index 23dc3a18b70e0..2427b0617637e 100644 --- a/x-pack/test/functional/apps/infra/hosts_view.ts +++ b/x-pack/test/functional/apps/infra/hosts_view.ts @@ -267,10 +267,23 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); describe('Metadata Tab', () => { - it('should render metadata tab, add and remove filter', async () => { + it('should render metadata tab, pin/unpin row, add and remove filter', async () => { const metadataTab = await pageObjects.infraHostsView.getMetadataTabName(); expect(metadataTab).to.contain('Metadata'); + // Add Pin + await pageObjects.infraHostsView.clickAddMetadataPin(); + expect(await pageObjects.infraHostsView.getRemovePinExist()).to.be(true); + + // Persist pin after refresh + await browser.refresh(); + await pageObjects.infraHome.waitForLoading(); + expect(await pageObjects.infraHostsView.getRemovePinExist()).to.be(true); + + // Remove Pin + await pageObjects.infraHostsView.clickRemoveMetadataPin(); + expect(await pageObjects.infraHostsView.getRemovePinExist()).to.be(false); + await pageObjects.infraHostsView.clickAddMetadataFilter(); await pageObjects.header.waitUntilLoadingHasFinished(); @@ -289,6 +302,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); + it('should render metadata tab, pin and unpin table row', async () => { + const metadataTab = await pageObjects.infraHostsView.getMetadataTabName(); + expect(metadataTab).to.contain('Metadata'); + }); + describe('Processes Tab', () => { it('should render processes tab and with Total Value summary', async () => { await pageObjects.infraHostsView.clickProcessesFlyoutTab(); diff --git a/x-pack/test/functional/page_objects/infra_hosts_view.ts b/x-pack/test/functional/page_objects/infra_hosts_view.ts index ff0aefa548369..244967174ebf6 100644 --- a/x-pack/test/functional/page_objects/infra_hosts_view.ts +++ b/x-pack/test/functional/page_objects/infra_hosts_view.ts @@ -60,6 +60,14 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) { return testSubjects.click('hostsView-flyout-apm-services-link'); }, + async clickAddMetadataPin() { + return testSubjects.click('infraMetadataEmbeddableAddPin'); + }, + + async clickRemoveMetadataPin() { + return testSubjects.click('infraMetadataEmbeddableRemovePin'); + }, + async clickAddMetadataFilter() { return testSubjects.click('hostsView-flyout-metadata-add-filter'); }, @@ -190,6 +198,10 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) { return tabTitle.getVisibleText(); }, + async getRemovePinExist() { + return testSubjects.exists('infraMetadataEmbeddableRemovePin'); + }, + async getAppliedFilter() { const filter = await testSubjects.find( "filter-badge-'host.architecture: arm64' filter filter-enabled filter-key-host.architecture filter-value-arm64 filter-unpinned filter-id-0"