diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2c5a51ddb81ec..c2450338f3e45 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -225,7 +225,6 @@ packages/core/security/core-security-server @elastic/kibana-core packages/core/security/core-security-server-internal @elastic/kibana-core packages/core/security/core-security-server-mocks @elastic/kibana-core packages/core/status/core-status-common @elastic/kibana-core -packages/core/status/core-status-common-internal @elastic/kibana-core packages/core/status/core-status-server @elastic/kibana-core packages/core/status/core-status-server-internal @elastic/kibana-core packages/core/status/core-status-server-mocks @elastic/kibana-core @@ -1283,6 +1282,9 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai /x-pack/test_serverless/**/test_suites/observability/infra/ @elastic/obs-ux-infra_services-team # Elastic Stack Monitoring +/x-pack/test/monitoring_api_integration @elastic/stack-monitoring +/x-pack/test/functional/page_objects/monitoring_page.ts @elastic/stack-monitoring +/x-pack/test/functional/es_archives/monitoring @elastic/stack-monitoring /x-pack/test/functional/services/monitoring @elastic/stack-monitoring /x-pack/test/functional/apps/monitoring @elastic/stack-monitoring /x-pack/test/api_integration/apis/monitoring @elastic/stack-monitoring @@ -1350,6 +1352,12 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai /src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview @elastic/obs-ux-logs-team /x-pack/test/api_integration/apis/logs_shared @elastic/obs-ux-logs-team +# Observability-ui +/x-pack/test_serverless/api_integration/test_suites/observability/index.ts @elastic/observability-ui +/x-pack/test/functional_solution_sidenav/tests/observability_sidenav.ts @elastic/observability-ui +/x-pack/test/functional/page_objects/observability_page.ts @elastic/observability-ui +/x-pack/test_serverless/**/test_suites/observability/config.ts @elastic/observability-ui + # Observability onboarding tour /x-pack/plugins/observability_solution/observability_shared/public/components/tour @elastic/appex-sharedux /x-pack/test/functional/apps/infra/tour.ts @elastic/appex-sharedux diff --git a/examples/content_management_examples/public/examples/finder/finder_app.tsx b/examples/content_management_examples/public/examples/finder/finder_app.tsx index 99ec949fac7d1..b8aaa6fe5f34b 100644 --- a/examples/content_management_examples/public/examples/finder/finder_app.tsx +++ b/examples/content_management_examples/public/examples/finder/finder_app.tsx @@ -23,6 +23,7 @@ export const FinderApp = (props: { ({ defaultValue: false, }, }); + +export const localStorageMock = (): IStorage => { + let store: Record = {}; + + return { + getItem: (key: string) => { + return store[key] || null; + }, + setItem: (key: string, value: unknown) => { + store[key] = value; + }, + clear() { + store = {}; + }, + removeItem(key: string) { + delete store[key]; + }, + }; +}; diff --git a/packages/content-management/table_list_view_table/src/table_list_view.test.tsx b/packages/content-management/table_list_view_table/src/table_list_view.test.tsx index 38229399f2ec8..aebaca335db5f 100644 --- a/packages/content-management/table_list_view_table/src/table_list_view.test.tsx +++ b/packages/content-management/table_list_view_table/src/table_list_view.test.tsx @@ -18,7 +18,7 @@ import type { LocationDescriptor, History } from 'history'; import type { UserContentCommonSchema } from '@kbn/content-management-table-list-view-common'; import { WithServices } from './__jest__'; -import { getTagList } from './mocks'; +import { getTagList, localStorageMock } from './mocks'; import { TableListViewTable, type TableListViewTableProps } from './table_list_view_table'; import { getActions } from './table_list_view.test.helpers'; import type { Services } from './services'; @@ -335,6 +335,12 @@ describe('TableListView', () => { const totalItems = 30; const updatedAt = new Date().toISOString(); + beforeEach(() => { + Object.defineProperty(window, 'localStorage', { + value: localStorageMock(), + }); + }); + const hits: UserContentCommonSchema[] = [...Array(totalItems)].map((_, i) => ({ id: `item${i}`, type: 'dashboard', @@ -429,6 +435,54 @@ describe('TableListView', () => { expect(firstRowTitle).toBe('Item 20'); expect(lastRowTitle).toBe('Item 29'); }); + + test('should persist the number of rows in the table', async () => { + let testBed: TestBed; + + const tableId = 'myTable'; + + await act(async () => { + testBed = await setup({ + initialPageSize, + findItems: jest.fn().mockResolvedValue({ total: hits.length, hits: [...hits] }), + id: tableId, + }); + }); + + { + const { component, table, find } = testBed!; + component.update(); + + const { tableCellsValues } = table.getMetaData('itemsInMemTable'); + expect(tableCellsValues.length).toBe(20); // 20 by default + + let storageValue = localStorage.getItem(`tablePersist:${tableId}`); + expect(storageValue).toBe(null); + + find('tablePaginationPopoverButton').simulate('click'); + find('tablePagination-10-rows').simulate('click'); + + storageValue = localStorage.getItem(`tablePersist:${tableId}`); + expect(storageValue).not.toBe(null); + expect(JSON.parse(storageValue!).pageSize).toBe(10); + } + + // Mount a second table and verify that is shows only 10 rows + { + await act(async () => { + testBed = await setup({ + initialPageSize, + findItems: jest.fn().mockResolvedValue({ total: hits.length, hits: [...hits] }), + id: tableId, + }); + }); + + const { component, table } = testBed!; + component.update(); + const { tableCellsValues } = table.getMetaData('itemsInMemTable'); + expect(tableCellsValues.length).toBe(10); // 10 items this time + } + }); }); describe('column sorting', () => { diff --git a/packages/content-management/table_list_view_table/src/table_list_view_table.tsx b/packages/content-management/table_list_view_table/src/table_list_view_table.tsx index 1fe5123d54151..c7653c668f0df 100644 --- a/packages/content-management/table_list_view_table/src/table_list_view_table.tsx +++ b/packages/content-management/table_list_view_table/src/table_list_view_table.tsx @@ -43,6 +43,7 @@ import { ContentInsightsProvider, useContentInsightsServices, } from '@kbn/content-management-content-insights-public'; +import { useEuiTablePersist } from '@kbn/shared-ux-table-persist'; import { Table, @@ -443,7 +444,7 @@ function TableListViewTableComp({ hasUpdatedAtMetadata, hasCreatedByMetadata, hasRecentlyAccessedMetadata, - pagination, + pagination: _pagination, tableSort, tableFilter, } = state; @@ -903,7 +904,7 @@ function TableListViewTableComp({ [updateTableSortFilterAndPagination] ); - const onTableChange = useCallback( + const customOnTableChange = useCallback( (criteria: CriteriaWithPagination) => { const data: { sort?: State['tableSort']; @@ -1038,6 +1039,20 @@ function TableListViewTableComp({ ); }, [entityName, fetchError]); + const { pageSize, onTableChange } = useEuiTablePersist({ + tableId: listingId, + initialPageSize, + customOnTableChange, + pageSizeOptions: uniq([10, 20, 50, initialPageSize]).sort(), + }); + + const pagination = useMemo(() => { + return { + ..._pagination, + pageSize, + }; + }, [_pagination, pageSize]); + // ------------ // Effects // ------------ diff --git a/packages/content-management/table_list_view_table/tsconfig.json b/packages/content-management/table_list_view_table/tsconfig.json index a5530ee717e49..90a96953570fb 100644 --- a/packages/content-management/table_list_view_table/tsconfig.json +++ b/packages/content-management/table_list_view_table/tsconfig.json @@ -37,7 +37,9 @@ "@kbn/content-management-user-profiles", "@kbn/recently-accessed", "@kbn/content-management-content-insights-public", - "@kbn/content-management-favorites-public" + "@kbn/content-management-favorites-public", + "@kbn/kibana-utils-plugin", + "@kbn/shared-ux-table-persist" ], "exclude": [ "target/**/*" diff --git a/packages/core/apps/core-apps-browser-internal/src/status/components/status_table.test.tsx b/packages/core/apps/core-apps-browser-internal/src/status/components/status_table.test.tsx index 38d69311d741e..b9949a6decf44 100644 --- a/packages/core/apps/core-apps-browser-internal/src/status/components/status_table.test.tsx +++ b/packages/core/apps/core-apps-browser-internal/src/status/components/status_table.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import type { StatusInfoServiceStatus as ServiceStatus } from '@kbn/core-status-common-internal'; +import type { StatusInfoServiceStatus as ServiceStatus } from '@kbn/core-status-common'; import { StatusTable } from './status_table'; const state = { diff --git a/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.test.tsx b/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.test.tsx index 62e48467ae51f..6180860df780d 100644 --- a/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.test.tsx +++ b/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { mountWithIntl, findTestSubject } from '@kbn/test-jest-helpers'; -import type { ServerVersion } from '@kbn/core-status-common-internal'; +import type { ServerVersion } from '@kbn/core-status-common'; import { VersionHeader } from './version_header'; const buildServerVersion = (parts: Partial = {}): ServerVersion => ({ diff --git a/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.tsx b/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.tsx index 0dc64a3cb7db0..15c1f9d07a273 100644 --- a/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.tsx +++ b/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.tsx @@ -10,7 +10,7 @@ import React, { FC } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { ServerVersion } from '@kbn/core-status-common-internal'; +import type { ServerVersion } from '@kbn/core-status-common'; interface VersionHeaderProps { version: ServerVersion; diff --git a/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.test.ts b/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.test.ts index a63c5011dcaf8..c37db930de789 100644 --- a/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.test.ts +++ b/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.test.ts @@ -8,7 +8,7 @@ */ import { httpServiceMock } from '@kbn/core-http-browser-mocks'; -import type { StatusResponse } from '@kbn/core-status-common-internal'; +import type { StatusResponse } from '@kbn/core-status-common'; import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks'; import { mocked } from '@kbn/core-metrics-collectors-server-mocks'; import { loadStatus } from './load_status'; diff --git a/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts b/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts index f89e2196d2122..e8519030c3fdf 100644 --- a/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts +++ b/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts @@ -11,11 +11,11 @@ import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; import type { HttpSetup } from '@kbn/core-http-browser'; import type { NotificationsSetup } from '@kbn/core-notifications-browser'; -import type { ServiceStatusLevelId } from '@kbn/core-status-common'; import type { + ServiceStatusLevelId, StatusResponse, StatusInfoServiceStatus as ServiceStatus, -} from '@kbn/core-status-common-internal'; +} from '@kbn/core-status-common'; import type { DataType } from './format_number'; interface MetricMeta { diff --git a/packages/core/apps/core-apps-browser-internal/src/status/lib/status_level.test.ts b/packages/core/apps/core-apps-browser-internal/src/status/lib/status_level.test.ts index 3d393bd8e4719..290845c4bdd08 100644 --- a/packages/core/apps/core-apps-browser-internal/src/status/lib/status_level.test.ts +++ b/packages/core/apps/core-apps-browser-internal/src/status/lib/status_level.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { StatusInfoServiceStatus as ServiceStatus } from '@kbn/core-status-common-internal'; +import type { StatusInfoServiceStatus as ServiceStatus } from '@kbn/core-status-common'; import { getLevelSortValue, groupByLevel, getHighestStatus } from './status_level'; import { FormattedStatus, StatusState } from './load_status'; diff --git a/packages/core/apps/core-apps-browser-internal/tsconfig.json b/packages/core/apps/core-apps-browser-internal/tsconfig.json index a18bb3421a1f4..9902b12732760 100644 --- a/packages/core/apps/core-apps-browser-internal/tsconfig.json +++ b/packages/core/apps/core-apps-browser-internal/tsconfig.json @@ -24,7 +24,6 @@ "@kbn/core-application-browser", "@kbn/core-application-browser-internal", "@kbn/core-mount-utils-browser-internal", - "@kbn/core-status-common-internal", "@kbn/core-http-browser-internal", "@kbn/core-application-browser-mocks", "@kbn/core-notifications-browser-mocks", diff --git a/packages/core/status/core-status-common-internal/README.md b/packages/core/status/core-status-common-internal/README.md deleted file mode 100644 index f4e4af7fd3b3a..0000000000000 --- a/packages/core/status/core-status-common-internal/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @kbn/core-status-common-internal - -This package contains the common internal types for Core's `status` domain. diff --git a/packages/core/status/core-status-common-internal/index.ts b/packages/core/status/core-status-common-internal/index.ts deleted file mode 100644 index f6a7a29056145..0000000000000 --- a/packages/core/status/core-status-common-internal/index.ts +++ /dev/null @@ -1,17 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export type { - StatusInfoCoreStatus, - StatusInfoServiceStatus, - StatusInfo, - StatusResponse, - ServerVersion, - ServerMetrics, -} from './src'; diff --git a/packages/core/status/core-status-common-internal/jest.config.js b/packages/core/status/core-status-common-internal/jest.config.js deleted file mode 100644 index bc848cd656199..0000000000000 --- a/packages/core/status/core-status-common-internal/jest.config.js +++ /dev/null @@ -1,14 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../../../..', - roots: ['/packages/core/status/core-status-common-internal'], -}; diff --git a/packages/core/status/core-status-common-internal/kibana.jsonc b/packages/core/status/core-status-common-internal/kibana.jsonc deleted file mode 100644 index 20ce17ae3cefa..0000000000000 --- a/packages/core/status/core-status-common-internal/kibana.jsonc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "type": "shared-common", - "id": "@kbn/core-status-common-internal", - "owner": "@elastic/kibana-core" -} diff --git a/packages/core/status/core-status-common-internal/package.json b/packages/core/status/core-status-common-internal/package.json deleted file mode 100644 index d2c456b6dc96a..0000000000000 --- a/packages/core/status/core-status-common-internal/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@kbn/core-status-common-internal", - "private": true, - "version": "1.0.0", - "author": "Kibana Core", - "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0" -} \ No newline at end of file diff --git a/packages/core/status/core-status-common-internal/src/index.ts b/packages/core/status/core-status-common-internal/src/index.ts deleted file mode 100644 index 60c51dcf47632..0000000000000 --- a/packages/core/status/core-status-common-internal/src/index.ts +++ /dev/null @@ -1,17 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export type { - StatusInfo, - StatusInfoCoreStatus, - StatusInfoServiceStatus, - StatusResponse, - ServerVersion, - ServerMetrics, -} from './status'; diff --git a/packages/core/status/core-status-common-internal/tsconfig.json b/packages/core/status/core-status-common-internal/tsconfig.json deleted file mode 100644 index 7d31fa090eb0f..0000000000000 --- a/packages/core/status/core-status-common-internal/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "extends": "../../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "target/types", - "types": [ - "jest", - "node" - ] - }, - "include": [ - "**/*.ts", - "**/*.tsx", - ], - "kbn_references": [ - "@kbn/core-status-common", - "@kbn/core-metrics-server", - "@kbn/config" - ], - "exclude": [ - "target/**/*", - ] -} diff --git a/packages/core/status/core-status-common/index.ts b/packages/core/status/core-status-common/index.ts index 50eb85608522e..1aae83558016a 100644 --- a/packages/core/status/core-status-common/index.ts +++ b/packages/core/status/core-status-common/index.ts @@ -7,5 +7,14 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { ServiceStatusLevels } from './src'; -export type { ServiceStatus, ServiceStatusLevel, ServiceStatusLevelId, CoreStatus } from './src'; +export { ServiceStatusLevels } from './src/service_status'; +export type { CoreStatus } from './src/core_status'; +export type { ServiceStatus, ServiceStatusLevel, ServiceStatusLevelId } from './src/service_status'; +export type { + StatusInfo, + StatusInfoCoreStatus, + StatusInfoServiceStatus, + StatusResponse, + ServerVersion, + ServerMetrics, +} from './src/status'; diff --git a/packages/core/status/core-status-common/jest.config.js b/packages/core/status/core-status-common/jest.config.js index bc848cd656199..48ce844bb7d3f 100644 --- a/packages/core/status/core-status-common/jest.config.js +++ b/packages/core/status/core-status-common/jest.config.js @@ -10,5 +10,5 @@ module.exports = { preset: '@kbn/test', rootDir: '../../../..', - roots: ['/packages/core/status/core-status-common-internal'], + roots: ['/packages/core/status/core-status-common'], }; diff --git a/packages/core/status/core-status-common/src/index.ts b/packages/core/status/core-status-common/src/index.ts deleted file mode 100644 index 7cfcc7dbf79a8..0000000000000 --- a/packages/core/status/core-status-common/src/index.ts +++ /dev/null @@ -1,12 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export { ServiceStatusLevels } from './service_status'; -export type { ServiceStatus, ServiceStatusLevel, ServiceStatusLevelId } from './service_status'; -export type { CoreStatus } from './core_status'; diff --git a/packages/core/status/core-status-common-internal/src/status.ts b/packages/core/status/core-status-common/src/status.ts similarity index 92% rename from packages/core/status/core-status-common-internal/src/status.ts rename to packages/core/status/core-status-common/src/status.ts index 370d2c9ac6e5d..7c981c56ceeb3 100644 --- a/packages/core/status/core-status-common-internal/src/status.ts +++ b/packages/core/status/core-status-common/src/status.ts @@ -8,8 +8,9 @@ */ import type { BuildFlavor } from '@kbn/config'; -import type { ServiceStatusLevelId, ServiceStatus, CoreStatus } from '@kbn/core-status-common'; import type { OpsMetrics } from '@kbn/core-metrics-server'; +import type { ServiceStatusLevelId, ServiceStatus } from './service_status'; +import type { CoreStatus } from './core_status'; export interface StatusInfoServiceStatus extends Omit { level: ServiceStatusLevelId; diff --git a/packages/core/status/core-status-common/tsconfig.json b/packages/core/status/core-status-common/tsconfig.json index a63f70f93043d..3b61a574a06bb 100644 --- a/packages/core/status/core-status-common/tsconfig.json +++ b/packages/core/status/core-status-common/tsconfig.json @@ -12,7 +12,9 @@ "**/*.tsx", ], "kbn_references": [ - "@kbn/std" + "@kbn/std", + "@kbn/config", + "@kbn/core-metrics-server" ], "exclude": [ "target/**/*", diff --git a/packages/core/status/core-status-server-internal/src/routes/status.ts b/packages/core/status/core-status-server-internal/src/routes/status.ts index 87e0e6e745a92..bafda87c2b08d 100644 --- a/packages/core/status/core-status-server-internal/src/routes/status.ts +++ b/packages/core/status/core-status-server-internal/src/routes/status.ts @@ -15,7 +15,7 @@ import type { IRouter } from '@kbn/core-http-server'; import type { MetricsServiceSetup } from '@kbn/core-metrics-server'; import type { CoreIncrementUsageCounter } from '@kbn/core-usage-data-server'; import { type ServiceStatus, type CoreStatus, ServiceStatusLevels } from '@kbn/core-status-common'; -import { StatusResponse } from '@kbn/core-status-common-internal'; +import type { StatusResponse } from '@kbn/core-status-common'; import { calculateLegacyStatus, type LegacyStatusInfo } from '../legacy_status'; import { statusResponse, type RedactedStatusHttpBody } from './status_response_schemas'; diff --git a/packages/core/status/core-status-server-internal/src/routes/status_response_schemas.ts b/packages/core/status/core-status-server-internal/src/routes/status_response_schemas.ts index a2dcbcf7d21b6..68cebab4392e0 100644 --- a/packages/core/status/core-status-server-internal/src/routes/status_response_schemas.ts +++ b/packages/core/status/core-status-server-internal/src/routes/status_response_schemas.ts @@ -9,15 +9,15 @@ import { schema, type Type, type TypeOf } from '@kbn/config-schema'; import type { BuildFlavor } from '@kbn/config'; -import type { ServiceStatusLevelId, ServiceStatus } from '@kbn/core-status-common'; - import type { + ServiceStatusLevelId, + ServiceStatus, StatusResponse, StatusInfoCoreStatus, ServerMetrics, StatusInfo, ServerVersion, -} from '@kbn/core-status-common-internal'; +} from '@kbn/core-status-common'; const serviceStatusLevelId: () => Type = () => schema.oneOf( diff --git a/packages/core/status/core-status-server-internal/tsconfig.json b/packages/core/status/core-status-server-internal/tsconfig.json index bda646809e414..5ca46556cac33 100644 --- a/packages/core/status/core-status-server-internal/tsconfig.json +++ b/packages/core/status/core-status-server-internal/tsconfig.json @@ -29,7 +29,6 @@ "@kbn/core-saved-objects-server-internal", "@kbn/core-status-server", "@kbn/core-status-common", - "@kbn/core-status-common-internal", "@kbn/core-usage-data-base-server-internal", "@kbn/core-base-server-mocks", "@kbn/core-environment-server-mocks", diff --git a/packages/kbn-alerts-ui-shared/src/alert_fields_table/index.tsx b/packages/kbn-alerts-ui-shared/src/alert_fields_table/index.tsx index 3f3940e98bf4a..3da86b5f848f7 100644 --- a/packages/kbn-alerts-ui-shared/src/alert_fields_table/index.tsx +++ b/packages/kbn-alerts-ui-shared/src/alert_fields_table/index.tsx @@ -13,12 +13,13 @@ import { EuiTabbedContent, EuiTabbedContentProps, useEuiOverflowScroll, + EuiBasicTableColumn, } from '@elastic/eui'; import { css } from '@emotion/react'; -import React, { memo, useCallback, useMemo, useState } from 'react'; +import React, { memo, useMemo } from 'react'; import { Alert } from '@kbn/alerting-types'; import { euiThemeVars } from '@kbn/ui-theme'; -import { EuiBasicTableColumn } from '@elastic/eui/src/components/basic_table/basic_table'; +import { useEuiTablePersist } from '@kbn/shared-ux-table-persist'; export const search = { box: { @@ -66,28 +67,6 @@ export const ScrollableFlyoutTabbedContent = (props: EuiTabbedContentProps) => ( const COUNT_PER_PAGE_OPTIONS = [25, 50, 100]; -const useFieldBrowserPagination = () => { - const [pagination, setPagination] = useState<{ pageIndex: number }>({ - pageIndex: 0, - }); - - const onTableChange = useCallback(({ page: { index } }: { page: { index: number } }) => { - setPagination({ pageIndex: index }); - }, []); - const paginationTableProp = useMemo( - () => ({ - ...pagination, - pageSizeOptions: COUNT_PER_PAGE_OPTIONS, - }), - [pagination] - ); - - return { - onTableChange, - paginationTableProp, - }; -}; - type AlertField = Exclude< { [K in keyof Alert]: { key: K; value: Alert[K] }; @@ -111,7 +90,11 @@ export interface AlertFieldsTableProps { * A paginated, filterable table to show alert object fields */ export const AlertFieldsTable = memo(({ alert, fields }: AlertFieldsTableProps) => { - const { onTableChange, paginationTableProp } = useFieldBrowserPagination(); + const { pageSize, sorting, onTableChange } = useEuiTablePersist({ + tableId: 'obltAlertFields', + initialPageSize: 25, + }); + const items = useMemo(() => { let _items = Object.entries(alert).map( ([key, value]) => @@ -131,7 +114,11 @@ export const AlertFieldsTable = memo(({ alert, fields }: AlertFieldsTableProps) itemId="key" columns={columns} onTableChange={onTableChange} - pagination={paginationTableProp} + pagination={{ + pageSize, + pageSizeOptions: COUNT_PER_PAGE_OPTIONS, + }} + sorting={sorting} search={search} css={css` & .euiTableRow { diff --git a/packages/kbn-alerts-ui-shared/tsconfig.json b/packages/kbn-alerts-ui-shared/tsconfig.json index 0da17dfe3d1ac..317f80dd209f3 100644 --- a/packages/kbn-alerts-ui-shared/tsconfig.json +++ b/packages/kbn-alerts-ui-shared/tsconfig.json @@ -49,6 +49,7 @@ "@kbn/core-ui-settings-browser", "@kbn/core-http-browser-mocks", "@kbn/core-notifications-browser-mocks", - "@kbn/kibana-react-plugin" + "@kbn/kibana-react-plugin", + "@kbn/shared-ux-table-persist" ] } diff --git a/packages/kbn-esql-editor/src/editor_footer/query_history.tsx b/packages/kbn-esql-editor/src/editor_footer/query_history.tsx index 864306737e9ca..7316a5b49ddea 100644 --- a/packages/kbn-esql-editor/src/editor_footer/query_history.tsx +++ b/packages/kbn-esql-editor/src/editor_footer/query_history.tsx @@ -17,7 +17,6 @@ import { EuiInMemoryTable, EuiBasicTableColumn, EuiButtonEmpty, - Criteria, EuiButtonIcon, CustomItemAction, EuiCopy, @@ -25,6 +24,7 @@ import { euiScrollBarStyles, } from '@elastic/eui'; import { css, Interpolation, Theme } from '@emotion/react'; +import { useEuiTablePersist } from '@kbn/shared-ux-table-persist'; import { type QueryHistoryItem, getHistoryItems } from '../history_local_storage'; import { getReducedSpaceStyling, swapArrayElements } from './query_history_helpers'; @@ -212,8 +212,16 @@ export function QueryHistory({ }) { const theme = useEuiTheme(); const scrollBarStyles = euiScrollBarStyles(theme); - const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); - const historyItems: QueryHistoryItem[] = getHistoryItems(sortDirection); + + const { sorting, onTableChange } = useEuiTablePersist({ + tableId: 'esqlQueryHistory', + initialSort: { + field: 'timeRan', + direction: 'desc', + }, + }); + + const historyItems: QueryHistoryItem[] = getHistoryItems(sorting.sort.direction); const actions: Array> = useMemo(() => { return [ @@ -276,19 +284,6 @@ export function QueryHistory({ return getTableColumns(containerWidth, isOnReducedSpaceLayout, actions); }, [actions, containerWidth, isOnReducedSpaceLayout]); - const onTableChange = ({ page, sort }: Criteria) => { - if (sort) { - const { direction } = sort; - setSortDirection(direction); - } - }; - - const sorting = { - sort: { - field: 'timeRan', - direction: sortDirection, - }, - }; const { euiTheme } = theme; const extraStyling = isOnReducedSpaceLayout ? getReducedSpaceStyling() : ''; diff --git a/packages/kbn-esql-editor/tsconfig.json b/packages/kbn-esql-editor/tsconfig.json index c26b971e5231c..075c5ff9ab457 100644 --- a/packages/kbn-esql-editor/tsconfig.json +++ b/packages/kbn-esql-editor/tsconfig.json @@ -28,6 +28,7 @@ "@kbn/fields-metadata-plugin", "@kbn/esql-validation-autocomplete", "@kbn/esql-utils", + "@kbn/shared-ux-table-persist", ], "exclude": [ "target/**/*", diff --git a/packages/kbn-event-annotation-components/components/event_annotation_group_saved_object_finder.tsx b/packages/kbn-event-annotation-components/components/event_annotation_group_saved_object_finder.tsx index 2f9c7afcd2190..38a701abdd81c 100644 --- a/packages/kbn-event-annotation-components/components/event_annotation_group_saved_object_finder.tsx +++ b/packages/kbn-event-annotation-components/components/event_annotation_group_saved_object_finder.tsx @@ -100,6 +100,7 @@ export const EventAnnotationGroupSavedObjectFinder = ({ ) : ( { onChoose({ id, type, fullName, savedObject }); diff --git a/packages/kbn-manifest/index.ts b/packages/kbn-manifest/index.ts index 5fc4727a1a72d..ce890742ea61f 100644 --- a/packages/kbn-manifest/index.ts +++ b/packages/kbn-manifest/index.ts @@ -37,7 +37,7 @@ export const runKbnManifestCli = () => { --list all List all the manifests --package [packageId] Select a package to update. --plugin [pluginId] Select a plugin to update. - --set [property] [value] Set the desired "[property]": "[value]" + --set [property]=[value] Set the desired "[property]": "[value]" --unset [property] Removes the desired "[property]: value" from the manifest `, }, diff --git a/packages/kbn-securitysolution-utils/src/path_validations/index.ts b/packages/kbn-securitysolution-utils/src/path_validations/index.ts index 0609129349b60..1f1eaf0b01423 100644 --- a/packages/kbn-securitysolution-utils/src/path_validations/index.ts +++ b/packages/kbn-securitysolution-utils/src/path_validations/index.ts @@ -21,19 +21,22 @@ export enum ConditionEntryField { HASH = 'process.hash.*', PATH = 'process.executable.caseless', SIGNER = 'process.Ext.code_signature', + SIGNER_MAC = 'process.code_signature', } export enum EntryFieldType { HASH = '.hash.', EXECUTABLE = '.executable.caseless', PATH = '.path', - SIGNER = '.Ext.code_signature', + SIGNER = '.code_signature', } export type TrustedAppConditionEntryField = | 'process.hash.*' | 'process.executable.caseless' - | 'process.Ext.code_signature'; + | 'process.Ext.code_signature' + | 'process.code_signature'; + export type BlocklistConditionEntryField = | 'file.hash.*' | 'file.path' diff --git a/packages/shared-ux/table_persist/index.ts b/packages/shared-ux/table_persist/index.ts index da2596dac14ab..c46f6a8e8a00a 100644 --- a/packages/shared-ux/table_persist/index.ts +++ b/packages/shared-ux/table_persist/index.ts @@ -7,4 +7,5 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { useEuiTablePersist, DEFAULT_PAGE_SIZE_OPTIONS } from './src'; +export { useEuiTablePersist, DEFAULT_PAGE_SIZE_OPTIONS, withEuiTablePersist } from './src'; +export type { EuiTablePersistInjectedProps, EuiTablePersistPropsGetter, HOCProps } from './src'; diff --git a/packages/shared-ux/table_persist/src/index.ts b/packages/shared-ux/table_persist/src/index.ts index 20416b5cec902..33ac8b18d1d34 100644 --- a/packages/shared-ux/table_persist/src/index.ts +++ b/packages/shared-ux/table_persist/src/index.ts @@ -9,3 +9,9 @@ export { useEuiTablePersist } from './use_table_persist'; export { DEFAULT_PAGE_SIZE_OPTIONS } from './constants'; +export { withEuiTablePersist } from './table_persist_hoc'; +export type { + EuiTablePersistInjectedProps, + EuiTablePersistPropsGetter, + HOCProps, +} from './table_persist_hoc'; diff --git a/packages/shared-ux/table_persist/src/table_persist_hoc.test.tsx b/packages/shared-ux/table_persist/src/table_persist_hoc.test.tsx new file mode 100644 index 0000000000000..bed84111379bb --- /dev/null +++ b/packages/shared-ux/table_persist/src/table_persist_hoc.test.tsx @@ -0,0 +1,113 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React, { PureComponent } from 'react'; +import { render, screen } from '@testing-library/react'; + +import { withEuiTablePersist, type EuiTablePersistInjectedProps } from './table_persist_hoc'; + +const mockUseEuiTablePersist = jest.fn().mockReturnValue({ + pageSize: 'mockPageSize', + sorting: 'mockSorting', + onTableChange: 'mockOnTableChange', +}); + +jest.mock('./use_table_persist', () => { + const original = jest.requireActual('./use_table_persist'); + + return { + ...original, + useEuiTablePersist: (...args: unknown[]) => mockUseEuiTablePersist(...args), + }; +}); + +class TestComponent extends PureComponent> { + constructor(props: EuiTablePersistInjectedProps) { + super(props); + } + + render() { + return
{JSON.stringify(this.props.euiTablePersist)}
; + } +} + +describe('withEuiTablePersist', () => { + it('should call useEuiTablePersist and return its values', () => { + const customOnTableChange = jest.fn(); + const pageSizeOptions = [5, 10, 25, 50]; + + const WrappedComponent = withEuiTablePersist(TestComponent, { + tableId: 'testTableId', + initialPageSize: 10, + initialSort: { field: 'testField', direction: 'asc' }, + customOnTableChange, + pageSizeOptions, + }); + + render(); + + expect(mockUseEuiTablePersist).toHaveBeenCalledWith({ + tableId: 'testTableId', + customOnTableChange, + initialPageSize: 10, + initialSort: { field: 'testField', direction: 'asc' }, + pageSizeOptions, + }); + + expect(screen.getByTestId('value').textContent).toBe( + JSON.stringify({ + pageSize: 'mockPageSize', + sorting: 'mockSorting', + onTableChange: 'mockOnTableChange', + }) + ); + }); + + it('should allow override through props', () => { + const customOnTableChangeDefault = jest.fn(); + const customOnTableChangeProp = jest.fn(); + const pageSizeOptions = [5, 10, 25, 50]; + + const WrappedComponent = withEuiTablePersist(TestComponent, { + tableId: 'testTableId', + initialPageSize: 10, + initialSort: { field: 'testField', direction: 'asc' }, + customOnTableChange: customOnTableChangeDefault, + pageSizeOptions, + }); + + render( + + ); + + expect(mockUseEuiTablePersist).toHaveBeenCalledWith({ + tableId: 'testTableIdChanged', + customOnTableChange: customOnTableChangeProp, + initialPageSize: 20, + initialSort: { field: 'testFieldChanged', direction: 'desc' }, + pageSizeOptions: [5], + }); + + expect(screen.getByTestId('value').textContent).toBe( + JSON.stringify({ + pageSize: 'mockPageSize', + sorting: 'mockSorting', + onTableChange: 'mockOnTableChange', + }) + ); + }); +}); diff --git a/packages/shared-ux/table_persist/src/table_persist_hoc.tsx b/packages/shared-ux/table_persist/src/table_persist_hoc.tsx new file mode 100644 index 0000000000000..313cf1a1a21a2 --- /dev/null +++ b/packages/shared-ux/table_persist/src/table_persist_hoc.tsx @@ -0,0 +1,74 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import React from 'react'; +import { type CriteriaWithPagination } from '@elastic/eui'; +import { EuiTablePersistProps, useEuiTablePersist } from './use_table_persist'; +import { PropertySort } from './types'; + +export interface EuiTablePersistInjectedProps { + euiTablePersist: { + /** The EuiInMemoryTable onTableChange prop */ + onTableChange: (change: CriteriaWithPagination) => void; + /** The EuiInMemoryTable sorting prop */ + sorting: { sort: PropertySort } | true; + /** The EuiInMemoryTable pagination.pageSize value */ + pageSize: number; + }; +} + +export type EuiTablePersistPropsGetter = ( + props: Omit> +) => EuiTablePersistProps; + +export type HOCProps = P & { + /** Custom value for the EuiTablePersist HOC */ + euiTablePersistProps?: Partial>; +}; + +export function withEuiTablePersist( + WrappedComponent: React.ComponentClass>, + euiTablePersistDefault: + | (EuiTablePersistProps & { get?: undefined }) + | { + get: EuiTablePersistPropsGetter; + } +) { + const HOC: React.FC>>> = ( + props + ) => { + const getterOverride = euiTablePersistDefault.get ? euiTablePersistDefault.get(props) : {}; + + const mergedProps = { + ...euiTablePersistDefault, + ...props.euiTablePersistProps, + ...getterOverride, // Getter override other props + }; + + const { tableId, customOnTableChange, initialSort, initialPageSize, pageSizeOptions } = + mergedProps; + + if (!tableId) { + throw new Error('tableId is required'); + } + + const euiTablePersist = useEuiTablePersist({ + tableId, + customOnTableChange, + initialSort, + initialPageSize, + pageSizeOptions, + }); + + const { euiTablePersistProps, ...rest } = props; + + return ; + }; + + return HOC; +} diff --git a/packages/shared-ux/table_persist/src/use_table_persist.test.ts b/packages/shared-ux/table_persist/src/use_table_persist.test.ts index 235777aa5d294..51fbd93f7a214 100644 --- a/packages/shared-ux/table_persist/src/use_table_persist.test.ts +++ b/packages/shared-ux/table_persist/src/use_table_persist.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { Criteria } from '@elastic/eui'; +import { CriteriaWithPagination } from '@elastic/eui'; import { renderHook, act } from '@testing-library/react-hooks'; import { useEuiTablePersist } from './use_table_persist'; import { createStorage } from './storage'; // Mock this if it's external @@ -58,7 +58,7 @@ describe('useEuiTablePersist', () => { }; act(() => { - result.current.onTableChange(nextCriteria as Criteria); + result.current.onTableChange(nextCriteria as CriteriaWithPagination); }); expect(result.current.pageSize).toBe(100); @@ -85,7 +85,7 @@ describe('useEuiTablePersist', () => { }; act(() => { - result.current.onTableChange(nextCriteria as Criteria); + result.current.onTableChange(nextCriteria as CriteriaWithPagination); }); expect(customOnTableChange).toHaveBeenCalledWith(nextCriteria); @@ -98,7 +98,7 @@ describe('useEuiTablePersist', () => { const { result } = renderHook(() => useEuiTablePersist({ tableId: 'testTable' })); act(() => { - result.current.onTableChange({}); // Empty change + result.current.onTableChange({} as CriteriaWithPagination); // Empty change }); expect(result.current.pageSize).toBe(25); @@ -118,7 +118,7 @@ describe('useEuiTablePersist', () => { }; act(() => { - result.current.onTableChange(nextCriteria as Criteria); + result.current.onTableChange(nextCriteria as CriteriaWithPagination); }); expect(result.current.pageSize).toBe(100); diff --git a/packages/shared-ux/table_persist/src/use_table_persist.ts b/packages/shared-ux/table_persist/src/use_table_persist.ts index 9c3b7a75788b0..bf91f66beb292 100644 --- a/packages/shared-ux/table_persist/src/use_table_persist.ts +++ b/packages/shared-ux/table_persist/src/use_table_persist.ts @@ -8,7 +8,7 @@ */ import { useState, useCallback } from 'react'; -import { Criteria } from '@elastic/eui'; +import type { CriteriaWithPagination } from '@elastic/eui'; import { DEFAULT_INITIAL_PAGE_SIZE, DEFAULT_PAGE_SIZE_OPTIONS } from './constants'; import { createStorage } from './storage'; import { validatePersistData } from './validate_persist_data'; @@ -18,7 +18,7 @@ export interface EuiTablePersistProps { /** A unique id that will be included in the local storage variable for this table. */ tableId: string; /** (Optional) Specifies a custom onTableChange handler. */ - customOnTableChange?: (change: Criteria) => void; + customOnTableChange?: (change: CriteriaWithPagination) => void; /** (Optional) Specifies a custom initial table sorting. */ initialSort?: PropertySort; /** (Optional) Specifies a custom initial page size for the table. Defaults to 50. */ @@ -33,13 +33,37 @@ export interface EuiTablePersistProps { * Returns the persisting page size and sort and the onTableChange handler that should be passed * as props to an Eui table component. */ -export const useEuiTablePersist = ({ +export function useEuiTablePersist( + props: EuiTablePersistProps & { initialSort: PropertySort } +): { + sorting: { sort: PropertySort }; + pageSize: number; + onTableChange: (nextValues: CriteriaWithPagination) => void; +}; + +export function useEuiTablePersist( + props: EuiTablePersistProps & { initialSort?: undefined } +): { + sorting: true; + pageSize: number; + onTableChange: (nextValues: CriteriaWithPagination) => void; +}; + +export function useEuiTablePersist( + props: EuiTablePersistProps +): { + sorting: true | { sort: PropertySort }; + pageSize: number; + onTableChange: (nextValues: CriteriaWithPagination) => void; +}; + +export function useEuiTablePersist({ tableId, customOnTableChange, initialSort, initialPageSize, pageSizeOptions, -}: EuiTablePersistProps) => { +}: EuiTablePersistProps) { const storage = createStorage(); const storedPersistData = storage.get(tableId, undefined); @@ -55,7 +79,7 @@ export const useEuiTablePersist = ({ const sorting = sort ? { sort } : true; // If sort is undefined, return true to allow sorting const onTableChange = useCallback( - (nextValues: Criteria) => { + (nextValues: CriteriaWithPagination) => { if (customOnTableChange) { customOnTableChange(nextValues); } @@ -92,4 +116,4 @@ export const useEuiTablePersist = ({ ); return { pageSize, sorting, onTableChange }; -}; +} diff --git a/src/plugins/data/public/search/session/sessions_mgmt/components/table/table.tsx b/src/plugins/data/public/search/session/sessions_mgmt/components/table/table.tsx index 211fcdcd50235..3e41263c8750f 100644 --- a/src/plugins/data/public/search/session/sessions_mgmt/components/table/table.tsx +++ b/src/plugins/data/public/search/session/sessions_mgmt/components/table/table.tsx @@ -13,6 +13,7 @@ import { CoreStart } from '@kbn/core/public'; import moment from 'moment'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; +import { useEuiTablePersist } from '@kbn/shared-ux-table-persist'; import { TableText } from '..'; import { SEARCH_SESSIONS_TABLE_ID } from '../../../../../../common'; import { SearchSessionsMgmtAPI } from '../../lib/api'; @@ -45,7 +46,6 @@ export function SearchSessionsMgmtTable({ const [tableData, setTableData] = useState([]); const [isLoading, setIsLoading] = useState(false); const [debouncedIsLoading, setDebouncedIsLoading] = useState(false); - const [pagination, setPagination] = useState({ pageIndex: 0 }); const showLatestResultsHandler = useRef(); const refreshTimeoutRef = useRef(null); const refreshInterval = useMemo( @@ -53,6 +53,14 @@ export function SearchSessionsMgmtTable({ [config.management.refreshInterval] ); + const { pageSize, sorting, onTableChange } = useEuiTablePersist({ + tableId: 'searchSessionsMgmt', + initialSort: { + field: 'created', + direction: 'desc', + }, + }); + // Debounce rendering the state of the Refresh button useDebounce( () => { @@ -148,12 +156,12 @@ export function SearchSessionsMgmtTable({ searchUsageCollector )} items={tableData} - pagination={pagination} - search={search} - sorting={{ sort: { field: 'created', direction: 'desc' } }} - onTableChange={({ page: { index } }) => { - setPagination({ pageIndex: index }); + pagination={{ + pageSize, }} + search={search} + sorting={sorting} + onTableChange={onTableChange} tableLayout="auto" /> ); diff --git a/src/plugins/data/public/utils/table_inspector_view/components/data_table.tsx b/src/plugins/data/public/utils/table_inspector_view/components/data_table.tsx index ebb97fd210f85..10d95bcc46906 100644 --- a/src/plugins/data/public/utils/table_inspector_view/components/data_table.tsx +++ b/src/plugins/data/public/utils/table_inspector_view/components/data_table.tsx @@ -22,11 +22,17 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { IUiSettingsClient } from '@kbn/core/public'; -import { Datatable, DatatableColumn } from '@kbn/expressions-plugin/public'; +import { Datatable, DatatableColumn, DatatableRow } from '@kbn/expressions-plugin/public'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; +import { + withEuiTablePersist, + type EuiTablePersistInjectedProps, +} from '@kbn/shared-ux-table-persist/src'; import { DataViewRow, DataViewColumn } from '../types'; +const PAGE_SIZE_OPTIONS = [10, 20, 50]; + interface DataTableFormatState { columns: DataViewColumn[]; rows: DataViewRow[]; @@ -49,7 +55,10 @@ interface RenderCellArguments { isFilterable: boolean; } -export class DataTableFormat extends Component { +class DataTableFormatClass extends Component< + DataTableFormatProps & EuiTablePersistInjectedProps, + DataTableFormatState +> { static propTypes = { data: PropTypes.object.isRequired, uiSettings: PropTypes.object.isRequired, @@ -169,7 +178,7 @@ export class DataTableFormat extends Component row[dataColumn.id] === value) || 0; - return DataTableFormat.renderCell({ + return DataTableFormatClass.renderCell({ table: data, columnIndex: index, rowIndex, @@ -186,9 +195,10 @@ export class DataTableFormat extends Component {}, + sorting: { sort: { direction: 'asc' as const, field: 'name' as const } }, + }, +}; + const renderTable = ( { editField } = { editField: () => {}, @@ -87,6 +100,7 @@ const renderTable = ( ) => shallow( { +const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; + +class TableClass extends PureComponent< + IndexedFieldProps & EuiTablePersistInjectedProps +> { renderBooleanTemplate(value: string, arialLabel: string) { return value ? : ; } @@ -403,11 +411,17 @@ export class Table extends PureComponent { } render() { - const { items, editField, deleteField, indexPattern } = this.props; + const { + items, + editField, + deleteField, + indexPattern, + euiTablePersist: { pageSize, sorting, onTableChange }, + } = this.props; const pagination = { - initialPageSize: 10, - pageSizeOptions: [5, 10, 25, 50], + pageSize, + pageSizeOptions: PAGE_SIZE_OPTIONS, }; const columns: Array> = [ @@ -508,8 +522,18 @@ export class Table extends PureComponent { items={items} columns={columns} pagination={pagination} - sorting={{ sort: { field: 'displayName', direction: 'asc' } }} + sorting={sorting} + onTableChange={onTableChange} /> ); } } + +export const TableWithoutPersist = TableClass; // For testing purposes + +export const Table = withEuiTablePersist(TableClass, { + tableId: 'dataViewsIndexedFields', + pageSizeOptions: PAGE_SIZE_OPTIONS, + initialSort: { field: 'displayName', direction: 'asc' }, + initialPageSize: 10, +}); diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/relationships_table/relationships_table.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/relationships_table/relationships_table.tsx index 06991b2081639..5fb2adf8697ab 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/relationships_table/relationships_table.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/relationships_table/relationships_table.tsx @@ -18,6 +18,7 @@ import { import { CoreStart } from '@kbn/core/public'; import { get } from 'lodash'; import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; +import { useEuiTablePersist } from '@kbn/shared-ux-table-persist'; import { SavedObjectRelation, @@ -139,12 +140,20 @@ export const RelationshipsTable = ({ ] as SearchFilterConfig[], }; + const { pageSize, onTableChange } = useEuiTablePersist({ + tableId: 'dataViewMgmtRelationships', + initialPageSize: 10, + }); + return ( items={relationships} columns={columns} - pagination={true} + pagination={{ + pageSize, + }} + onTableChange={onTableChange} search={search} rowProps={() => ({ 'data-test-subj': `relationshipsTableRow`, diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/__snapshots__/table.test.tsx.snap b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/__snapshots__/table.test.tsx.snap index 5f8e34d0776ec..f3fee53256c67 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/__snapshots__/table.test.tsx.snap +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/__snapshots__/table.test.tsx.snap @@ -75,9 +75,10 @@ exports[`Table should render normally 1`] = ` }, ] } + onTableChange={[Function]} pagination={ Object { - "initialPageSize": 10, + "pageSize": 10, "pageSizeOptions": Array [ 5, 10, @@ -87,7 +88,14 @@ exports[`Table should render normally 1`] = ` } } searchFormat="eql" - sorting={true} + sorting={ + Object { + "sort": Object { + "direction": "asc", + "field": "name", + }, + } + } tableLayout="fixed" /> `; diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.test.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.test.tsx index 9ef16a1cb1531..29b51160e4730 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.test.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { Table } from '.'; +import { TableWithoutPersist as Table } from './table'; import { ScriptedFieldItem } from '../../types'; import { DataView } from '@kbn/data-views-plugin/public'; @@ -21,6 +21,14 @@ const items: ScriptedFieldItem[] = [ { name: '2', lang: 'painless', script: '', isUserEditable: false }, ]; +const baseProps = { + euiTablePersist: { + pageSize: 10, + onTableChange: () => {}, + sorting: { sort: { direction: 'asc' as const, field: 'name' as const } }, + }, +}; + describe('Table', () => { let indexPattern: DataView; @@ -37,8 +45,9 @@ describe('Table', () => { }); test('should render normally', () => { - const component = shallow
( + const component = shallow(
{}} @@ -52,6 +61,7 @@ describe('Table', () => { test('should render the format', () => { const component = shallow(
{}} @@ -68,6 +78,7 @@ describe('Table', () => { const component = shallow(
{ const component = shallow(
{}} @@ -100,6 +112,7 @@ describe('Table', () => { test('should not allow edit or deletion for user with only read access', () => { const component = shallow(
{}} diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.tsx index f1561ed99f8fd..834e6792768c6 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.tsx @@ -13,8 +13,14 @@ import { i18n } from '@kbn/i18n'; import { EuiInMemoryTable, EuiBasicTableColumn } from '@elastic/eui'; import { DataView } from '@kbn/data-views-plugin/public'; +import { + withEuiTablePersist, + type EuiTablePersistInjectedProps, +} from '@kbn/shared-ux-table-persist'; import { ScriptedFieldItem } from '../../types'; +const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; + interface TableProps { indexPattern: DataView; items: ScriptedFieldItem[]; @@ -22,7 +28,9 @@ interface TableProps { deleteField: (field: ScriptedFieldItem) => void; } -export class Table extends PureComponent { +class TableClass extends PureComponent< + TableProps & EuiTablePersistInjectedProps +> { renderFormatCell = (value: string) => { const { indexPattern } = this.props; const title = get(indexPattern, ['fieldFormatMap', value, 'type', 'title'], ''); @@ -31,7 +39,12 @@ export class Table extends PureComponent { }; render() { - const { items, editField, deleteField } = this.props; + const { + items, + editField, + deleteField, + euiTablePersist: { pageSize, sorting, onTableChange }, + } = this.props; const columns: Array> = [ { @@ -132,12 +145,26 @@ export class Table extends PureComponent { ]; const pagination = { - initialPageSize: 10, - pageSizeOptions: [5, 10, 25, 50], + pageSize, + pageSizeOptions: PAGE_SIZE_OPTIONS, }; return ( - + ); } } + +export const TableWithoutPersist = TableClass; // For testing purposes + +export const Table = withEuiTablePersist(TableClass, { + tableId: 'dataViewsScriptedFields', + pageSizeOptions: PAGE_SIZE_OPTIONS, + initialPageSize: 10, +}); diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/__snapshots__/table.test.tsx.snap b/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/__snapshots__/table.test.tsx.snap index 7ddd6d34fb089..9469bacfc8a7d 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/__snapshots__/table.test.tsx.snap +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/__snapshots__/table.test.tsx.snap @@ -79,9 +79,10 @@ exports[`Table should render normally 1`] = ` ] } loading={true} + onTableChange={[Function]} pagination={ Object { - "initialPageSize": 10, + "pageSize": 10, "pageSizeOptions": Array [ 5, 10, @@ -91,7 +92,14 @@ exports[`Table should render normally 1`] = ` } } searchFormat="eql" - sorting={true} + sorting={ + Object { + "sort": Object { + "direction": "asc", + "field": "clientId", + }, + } + } tableLayout="fixed" /> `; diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx index db766ca9b3e54..695002440754f 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx @@ -10,7 +10,7 @@ import React, { ReactElement } from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; -import { Table, TableProps, TableState } from './table'; +import { TableWithoutPersist as Table } from './table'; import { EuiTableFieldDataColumnType, keys } from '@elastic/eui'; import { DataView } from '@kbn/data-views-plugin/public'; import { SourceFiltersTableFilter } from '../../types'; @@ -20,10 +20,15 @@ const items: SourceFiltersTableFilter[] = [{ value: 'tim*', clientId: '' }]; const getIndexPatternMock = (mockedFields: any = {}) => ({ ...mockedFields } as DataView); -const getTableColumnRender = ( - component: ShallowWrapper, - index: number = 0 -) => { +const baseProps = { + euiTablePersist: { + pageSize: 10, + onTableChange: () => {}, + sorting: { sort: { direction: 'asc' as const, field: 'clientId' as const } }, + }, +}; + +const getTableColumnRender = (component: ShallowWrapper, index: number = 0) => { const columns = component.prop>>('columns'); return { @@ -35,6 +40,7 @@ describe('Table', () => { test('should render normally', () => { const component = shallow(
{}} @@ -48,8 +54,9 @@ describe('Table', () => { }); test('should render filter matches', () => { - const component = shallow
( + const component = shallow(
[{ name: 'time' }, { name: 'value' }], })} @@ -70,11 +77,12 @@ describe('Table', () => { describe('editing', () => { const saveFilter = jest.fn(); const clientId = '1'; - let component: ShallowWrapper; + let component: ShallowWrapper; beforeEach(() => { - component = shallow
( + component = shallow(
{}} @@ -125,6 +133,7 @@ describe('Table', () => { test('should update the matches dynamically as input value is changed', () => { const localComponent = shallow(
[{ name: 'time' }, { name: 'value' }], })} @@ -191,6 +200,7 @@ describe('Table', () => { const component = shallow(
{ const component = shallow(
{}} @@ -251,6 +262,7 @@ describe('Table', () => { const component = shallow(
{}} diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx index 21de13871d03d..d43c72991c136 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx @@ -21,6 +21,11 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { DataView } from '@kbn/data-views-plugin/public'; +import { + withEuiTablePersist, + type EuiTablePersistInjectedProps, +} from '@kbn/shared-ux-table-persist'; + import { SourceFiltersTableFilter } from '../../types'; const filterHeader = i18n.translate( @@ -69,6 +74,8 @@ const cancelAria = i18n.translate( } ); +const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; + export interface TableProps { indexPattern: DataView; items: SourceFiltersTableFilter[]; @@ -83,8 +90,11 @@ export interface TableState { editingFilterValue: string; } -export class Table extends Component { - constructor(props: TableProps) { +class TableClass extends Component< + TableProps & EuiTablePersistInjectedProps, + TableState +> { + constructor(props: TableProps & EuiTablePersistInjectedProps) { super(props); this.state = { editingFilterId: '', @@ -227,11 +237,15 @@ export class Table extends Component { } render() { - const { items, isSaving } = this.props; + const { + items, + isSaving, + euiTablePersist: { pageSize, sorting, onTableChange }, + } = this.props; const columns = this.getColumns(); const pagination = { - initialPageSize: 10, - pageSizeOptions: [5, 10, 25, 50], + pageSize, + pageSizeOptions: PAGE_SIZE_OPTIONS, }; return ( @@ -240,8 +254,17 @@ export class Table extends Component { items={items} columns={columns} pagination={pagination} - sorting={true} + sorting={sorting} + onTableChange={onTableChange} /> ); } } + +export const TableWithoutPersist = TableClass; // For testing purposes + +export const Table = withEuiTablePersist(TableClass, { + tableId: 'dataViewsSourceFilters', + pageSizeOptions: PAGE_SIZE_OPTIONS, + initialPageSize: 10, +}); diff --git a/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx index 4512cb520c574..daabfe3fe6a9a 100644 --- a/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx +++ b/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx @@ -30,6 +30,8 @@ import { NoDataViewsPromptComponent, useOnTryESQL } from '@kbn/shared-ux-prompt- import type { SpacesContextProps } from '@kbn/spaces-plugin/public'; import { DataViewType } from '@kbn/data-views-plugin/public'; import { RollupDeprecationTooltip } from '@kbn/rollup'; +import { useEuiTablePersist } from '@kbn/shared-ux-table-persist'; + import type { IndexPatternManagmentContext } from '../../types'; import { getListBreadcrumbs } from '../breadcrumbs'; import { type RemoveDataViewProps, removeDataView } from '../edit_index_pattern'; @@ -42,10 +44,7 @@ import { deleteModalMsg } from './delete_modal_msg'; import { NoData } from './no_data'; import { SpacesList } from './spaces_list'; -const pagination = { - initialPageSize: 10, - pageSizeOptions: [5, 10, 25, 50], -}; +const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; const sorting = { sort: { @@ -123,6 +122,12 @@ export const IndexPatternTable = ({ }; const onTryESQL = useOnTryESQL(useOnTryESQLParams); + const { pageSize, onTableChange } = useEuiTablePersist({ + tableId: 'dataViewsIndexPattern', + initialPageSize: 10, + pageSizeOptions: PAGE_SIZE_OPTIONS, + }); + const handleOnChange = ({ queryText, error }: { queryText: string; error: unknown }) => { if (!error) { setQuery(queryText); @@ -361,8 +366,12 @@ export const IndexPatternTable = ({ itemId="id" items={indexPatterns} columns={columns} - pagination={pagination} + pagination={{ + pageSize, + pageSizeOptions: PAGE_SIZE_OPTIONS, + }} sorting={sorting} + onTableChange={onTableChange} search={search} selection={dataViews.getCanSaveSync() ? selection : undefined} /> diff --git a/src/plugins/data_view_management/tsconfig.json b/src/plugins/data_view_management/tsconfig.json index 9857dd44829fa..879d2dab84da9 100644 --- a/src/plugins/data_view_management/tsconfig.json +++ b/src/plugins/data_view_management/tsconfig.json @@ -46,6 +46,7 @@ "@kbn/react-kibana-mount", "@kbn/rollup", "@kbn/share-plugin", + "@kbn/shared-ux-table-persist", ], "exclude": [ "target/**/*", diff --git a/src/plugins/discover/public/application/main/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap b/src/plugins/discover/public/application/main/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap index 1851856a8739e..b1b399d1bd736 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap +++ b/src/plugins/discover/public/application/main/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap @@ -22,6 +22,7 @@ exports[`OpenSearchPanel render 1`] = ` ) : ( { diff --git a/src/plugins/interactive_setup/public/progress_indicator.tsx b/src/plugins/interactive_setup/public/progress_indicator.tsx index 6bb87a792e809..be094f9ef7e8d 100644 --- a/src/plugins/interactive_setup/public/progress_indicator.tsx +++ b/src/plugins/interactive_setup/public/progress_indicator.tsx @@ -15,7 +15,7 @@ import useAsyncFn from 'react-use/lib/useAsyncFn'; import useTimeoutFn from 'react-use/lib/useTimeoutFn'; import type { IHttpFetchError } from '@kbn/core-http-browser'; -import type { StatusResponse } from '@kbn/core-status-common-internal'; +import type { StatusResponse } from '@kbn/core-status-common'; import { i18n } from '@kbn/i18n'; import { useKibana } from './use_kibana'; diff --git a/src/plugins/interactive_setup/tsconfig.json b/src/plugins/interactive_setup/tsconfig.json index 51fff541980cc..048143fd464e0 100644 --- a/src/plugins/interactive_setup/tsconfig.json +++ b/src/plugins/interactive_setup/tsconfig.json @@ -14,7 +14,7 @@ "@kbn/i18n", "@kbn/ui-theme", "@kbn/core-http-browser", - "@kbn/core-status-common-internal", + "@kbn/core-status-common", "@kbn/safer-lodash-set", "@kbn/test-jest-helpers", "@kbn/config-schema", diff --git a/src/plugins/saved_objects_finder/public/finder/index.tsx b/src/plugins/saved_objects_finder/public/finder/index.tsx index 28a79391dd0a6..cd985f5235920 100644 --- a/src/plugins/saved_objects_finder/public/finder/index.tsx +++ b/src/plugins/saved_objects_finder/public/finder/index.tsx @@ -12,10 +12,11 @@ import React from 'react'; import { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public'; import type { ContentClient } from '@kbn/content-management-plugin/public'; import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; -import type { SavedObjectFinderProps } from './saved_object_finder'; +import type { HOCProps } from '@kbn/shared-ux-table-persist'; +import type { SavedObjectFinderItem, SavedObjectFinderProps } from './saved_object_finder'; const LazySavedObjectFinder = React.lazy(() => import('./saved_object_finder')); -const SavedObjectFinder = (props: SavedObjectFinderProps) => ( +const SavedObjectFinder = (props: HOCProps) => ( @@ -32,7 +33,7 @@ export const getSavedObjectFinder = ( uiSettings: IUiSettingsClient, savedObjectsTagging?: SavedObjectsTaggingApi ) => { - return (props: SavedObjectFinderProps) => ( + return (props: HOCProps) => ( ); }; diff --git a/src/plugins/saved_objects_finder/public/finder/saved_object_finder.test.tsx b/src/plugins/saved_objects_finder/public/finder/saved_object_finder.test.tsx index d6cce936200d4..ace6f6a9d3661 100644 --- a/src/plugins/saved_objects_finder/public/finder/saved_object_finder.test.tsx +++ b/src/plugins/saved_objects_finder/public/finder/saved_object_finder.test.tsx @@ -28,7 +28,10 @@ import { IconType } from '@elastic/eui'; import { mount, shallow } from 'enzyme'; import React from 'react'; import * as sinon from 'sinon'; -import { SavedObjectFinderUi as SavedObjectFinder } from './saved_object_finder'; +import { + SavedObjectFinderWithoutPersist as SavedObjectFinder, + SavedObjectFinderUi, +} from './saved_object_finder'; import { contentManagementMock } from '@kbn/content-management-plugin/public/mocks'; import { findTestSubject } from '@kbn/test-jest-helpers'; import { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public'; @@ -72,6 +75,15 @@ describe('SavedObjectsFinder', () => { }, ]; + const baseProps = { + id: 'foo', + euiTablePersist: { + pageSize: 10, + onTableChange: () => {}, + sorting: { sort: { direction: 'asc' as const, field: 'title' as const } }, + }, + }; + const contentManagement = contentManagementMock.createStartContract(); const contentClient = contentManagement.client; beforeEach(() => { @@ -109,6 +121,7 @@ describe('SavedObjectsFinder', () => { const wrapper = shallow( { const wrapper = shallow( @@ -157,6 +171,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( { const wrapper = shallow( { const button = Hello; const wrapper = shallow( { const wrapper = mount( @@ -251,6 +269,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( @@ -279,6 +298,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( @@ -299,6 +319,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( @@ -322,6 +343,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( @@ -346,6 +368,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( @@ -375,6 +398,7 @@ describe('SavedObjectsFinder', () => { const wrapper = shallow( { const wrapper = mount( @@ -430,6 +455,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( @@ -453,6 +479,7 @@ describe('SavedObjectsFinder', () => { const wrapper = shallow( { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( { const noItemsMessage = ; const wrapper = mount( { const wrapper = mount( { const wrapper = mount( { ); const wrapper = mount( - ); - wrapper.instance().componentDidMount!(); await nextTick(); wrapper.update(); expect(wrapper.find(EuiInMemoryTable).find('tbody tr')).toHaveLength(15); @@ -774,6 +818,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( @@ -840,6 +887,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( { const wrapper = mount( @@ -884,6 +933,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( { const wrapper = mount( @@ -933,6 +984,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( @@ -954,6 +1006,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( @@ -978,6 +1031,7 @@ describe('SavedObjectsFinder', () => { render( (item.id === doc3.id ? tooltipText : undefined)} @@ -990,7 +1044,7 @@ describe('SavedObjectsFinder', () => { const tooltip = screen.queryByText(tooltipText); if (show) { - expect(tooltip).toBeInTheDocument(); + expect(tooltip)?.toBeInTheDocument(); } else { expect(tooltip).toBeNull(); } diff --git a/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx b/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx index e9f51a808b335..9ea3472e59d3c 100644 --- a/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx +++ b/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx @@ -25,15 +25,21 @@ import { EuiToolTip, EuiIconTip, IconType, - PropertySort, Query, SearchFilterConfig, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public'; +import { + withEuiTablePersist, + type EuiTablePersistInjectedProps, +} from '@kbn/shared-ux-table-persist/src'; + import { FinderAttributes, SavedObjectCommon, LISTING_LIMIT_SETTING } from '../../common'; +const PAGE_SIZE_OPTIONS = [5, 10, 15, 25]; + export interface SavedObjectMetaData { type: string; name: string; @@ -45,7 +51,7 @@ export interface SavedObjectMetaData; @@ -55,7 +61,6 @@ interface SavedObjectFinderState { items: SavedObjectFinderItem[]; query: Query; isFetchingItems: boolean; - sort?: PropertySort; } interface SavedObjectFinderServices { @@ -65,6 +70,7 @@ interface SavedObjectFinderServices { } interface BaseSavedObjectFinder { + id: string; services: SavedObjectFinderServices; onChoose?: ( id: SavedObjectCommon['id'], @@ -93,8 +99,8 @@ interface SavedObjectFinderInitialPageSize extends BaseSavedObjectFinder { export type SavedObjectFinderProps = SavedObjectFinderFixedPage | SavedObjectFinderInitialPageSize; -export class SavedObjectFinderUi extends React.Component< - SavedObjectFinderProps, +class SavedObjectFinderUiClass extends React.Component< + SavedObjectFinderProps & EuiTablePersistInjectedProps, SavedObjectFinderState > { public static propTypes = { @@ -174,7 +180,7 @@ export class SavedObjectFinderUi extends React.Component< } }, 300); - constructor(props: SavedObjectFinderProps) { + constructor(props: SavedObjectFinderProps & EuiTablePersistInjectedProps) { super(props); this.state = { @@ -211,7 +217,11 @@ export class SavedObjectFinderUi extends React.Component< }; public render() { - const { onChoose, savedObjectMetaData } = this.props; + const { + onChoose, + savedObjectMetaData, + euiTablePersist: { pageSize, sorting, onTableChange }, + } = this.props; const taggingApi = this.props.services.savedObjectsTagging; const originalTagColumn = taggingApi?.ui.getTableColumnDefinition(); const tagColumn: EuiTableFieldDataColumnType | undefined = originalTagColumn @@ -320,16 +330,11 @@ export class SavedObjectFinderUi extends React.Component< ...(tagColumn ? [tagColumn] : []), ]; const pagination = { - initialPageSize: this.props.initialPageSize || this.props.fixedPageSize || 10, - pageSizeOptions: [5, 10, 15, 25], + initialPageSize: !!this.props.fixedPageSize ? this.props.fixedPageSize : pageSize ?? 10, + pageSize: !!this.props.fixedPageSize ? undefined : pageSize, + pageSizeOptions: PAGE_SIZE_OPTIONS, showPerPageOptions: !this.props.fixedPageSize, }; - const sorting = { - sort: this.state.sort ?? { - field: this.state.query?.text ? '' : 'title', - direction: 'asc', - }, - }; const typeFilter: SearchFilterConfig = { type: 'field_value_selection', field: 'type', @@ -382,10 +387,8 @@ export class SavedObjectFinderUi extends React.Component< message={this.props.noItemsMessage} search={search} pagination={pagination} - sorting={sorting} - onTableChange={({ sort }) => { - this.setState({ sort }); - }} + sorting={!!this.state.query?.text ? undefined : sorting} + onTableChange={onTableChange} /> @@ -393,6 +396,16 @@ export class SavedObjectFinderUi extends React.Component< } } +export const SavedObjectFinderUi = withEuiTablePersist(SavedObjectFinderUiClass, { + get: (props) => ({ + tableId: `soFinder-${props.id}`, + pageSizeOptions: PAGE_SIZE_OPTIONS, + initialPageSize: props.initialPageSize ?? props.fixedPageSize ?? 10, + }), +}); + +export const SavedObjectFinderWithoutPersist = SavedObjectFinderUiClass; // For testing + // Needed for React.lazy // eslint-disable-next-line import/no-default-export export default SavedObjectFinderUi; diff --git a/src/plugins/saved_objects_finder/tsconfig.json b/src/plugins/saved_objects_finder/tsconfig.json index cecc9fbdadb61..e32d4f34e68bc 100644 --- a/src/plugins/saved_objects_finder/tsconfig.json +++ b/src/plugins/saved_objects_finder/tsconfig.json @@ -15,6 +15,7 @@ "@kbn/content-management-plugin", "@kbn/content-management-utils", "@kbn/core-ui-settings-browser", + "@kbn/shared-ux-table-persist", ], "exclude": [ "target/**/*", diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap index af21429a9f7bb..b82a989d32851 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap @@ -102,7 +102,6 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = ` onTableChange={[Function]} pagination={ Object { - "pageIndex": 0, "pageSize": 5, "pageSizeOptions": Array [ 5, @@ -251,10 +250,6 @@ exports[`Flyout conflicts should allow conflict resolution 2`] = ` "newIndexPatternId": "2", }, ], - "unmatchedReferencesTablePagination": Object { - "pageIndex": 0, - "pageSize": 5, - }, }, }, ], diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap index f4a552f0a2fa2..124f4b4f2e285 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap @@ -110,7 +110,12 @@ exports[`Relationships should render dashboards normally 1`] = ` }, ] } - pagination={true} + onTableChange={[Function]} + pagination={ + Object { + "pageSize": 10, + } + } rowProps={[Function]} search={ Object { @@ -310,7 +315,12 @@ exports[`Relationships should render index patterns normally 1`] = ` }, ] } - pagination={true} + onTableChange={[Function]} + pagination={ + Object { + "pageSize": 10, + } + } rowProps={[Function]} search={ Object { @@ -501,7 +511,12 @@ exports[`Relationships should render invalid relations 1`] = ` ] } items={Array []} - pagination={true} + onTableChange={[Function]} + pagination={ + Object { + "pageSize": 10, + } + } rowProps={[Function]} search={ Object { @@ -652,7 +667,12 @@ exports[`Relationships should render searches normally 1`] = ` }, ] } - pagination={true} + onTableChange={[Function]} + pagination={ + Object { + "pageSize": 10, + } + } rowProps={[Function]} search={ Object { @@ -813,7 +833,12 @@ exports[`Relationships should render visualizations normally 1`] = ` }, ] } - pagination={true} + onTableChange={[Function]} + pagination={ + Object { + "pageSize": 10, + } + } rowProps={[Function]} search={ Object { diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx index 17a25fe3d98b7..4c94812d7de69 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx @@ -12,7 +12,7 @@ import { importFileMock, resolveImportErrorsMock } from './flyout.test.mocks'; import React from 'react'; import { shallowWithI18nProvider } from '@kbn/test-jest-helpers'; import { coreMock, httpServiceMock } from '@kbn/core/public/mocks'; -import { Flyout, FlyoutProps, FlyoutState } from './flyout'; +import { FlyoutClass as Flyout, FlyoutProps, FlyoutState } from './flyout'; import { ShallowWrapper } from 'enzyme'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; @@ -21,15 +21,21 @@ const mockFile = { path: '/home/foo.ndjson', } as unknown as File; +const baseProps = { + euiTablePersist: { + pageSize: 5, + onTableChange: () => {}, + sorting: { sort: { direction: 'asc' as const, field: 'foo' as const } }, + }, +}; + describe('Flyout', () => { let defaultProps: FlyoutProps; const shallowRender = (props: FlyoutProps) => { - return shallowWithI18nProvider() as unknown as ShallowWrapper< - FlyoutProps, - FlyoutState, - Flyout - >; + return shallowWithI18nProvider( + + ) as unknown as ShallowWrapper; }; beforeEach(() => { diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx index 7b50cec41fa27..4e29f34dedb73 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx @@ -36,6 +36,10 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { HttpStart, IBasePath } from '@kbn/core/public'; import { ISearchStart } from '@kbn/data-plugin/public'; import type { DataViewsContract, DataView } from '@kbn/data-views-plugin/public'; +import { + withEuiTablePersist, + type EuiTablePersistInjectedProps, +} from '@kbn/shared-ux-table-persist'; import type { SavedObjectManagementTypeInfo } from '../../../../common/types'; import { importFile, @@ -50,6 +54,7 @@ import { ImportSummary } from './import_summary'; const CREATE_NEW_COPIES_DEFAULT = false; const OVERWRITE_ALL_DEFAULT = true; +const PAGE_SIZE_OPTIONS = [5, 10, 25]; export interface FlyoutProps { close: () => void; @@ -65,7 +70,6 @@ export interface FlyoutProps { export interface FlyoutState { unmatchedReferences?: ProcessedImportResponse['unmatchedReferences']; - unmatchedReferencesTablePagination: { pageIndex: number; pageSize: number }; failedImports?: ProcessedImportResponse['failedImports']; successfulImports?: ProcessedImportResponse['successfulImports']; conflictingRecord?: ConflictingRecord; @@ -95,16 +99,15 @@ const getErrorMessage = (e: any) => { }); }; -export class Flyout extends Component { - constructor(props: FlyoutProps) { +export class FlyoutClass extends Component< + FlyoutProps & EuiTablePersistInjectedProps, + FlyoutState +> { + constructor(props: FlyoutProps & EuiTablePersistInjectedProps) { super(props); this.state = { unmatchedReferences: undefined, - unmatchedReferencesTablePagination: { - pageIndex: 0, - pageSize: 5, - }, conflictingRecord: undefined, error: undefined, file: undefined, @@ -275,7 +278,10 @@ export class Flyout extends Component { }; renderUnmatchedReferences() { - const { unmatchedReferences, unmatchedReferencesTablePagination: tablePagination } = this.state; + const { unmatchedReferences } = this.state; + const { + euiTablePersist: { pageSize, onTableChange }, + } = this.props; if (!unmatchedReferences) { return null; @@ -367,8 +373,8 @@ export class Flyout extends Component { ]; const pagination = { - ...tablePagination, - pageSizeOptions: [5, 10, 25], + pageSize, + pageSizeOptions: PAGE_SIZE_OPTIONS, }; return ( @@ -376,16 +382,7 @@ export class Flyout extends Component { items={unmatchedReferences as any[]} columns={columns} pagination={pagination} - onTableChange={({ page }) => { - if (page) { - this.setState({ - unmatchedReferencesTablePagination: { - pageSize: page.size, - pageIndex: page.index, - }, - }); - } - }} + onTableChange={onTableChange} /> ); } @@ -657,3 +654,9 @@ export class Flyout extends Component { ); } } + +export const Flyout = withEuiTablePersist(FlyoutClass, { + tableId: 'savedObjectsMgmtUnmatchedReferences', + pageSizeOptions: PAGE_SIZE_OPTIONS, + initialPageSize: 5, +}); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx index e98c9e7b54223..e963b626552ca 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx @@ -11,7 +11,7 @@ import React from 'react'; import { shallowWithI18nProvider } from '@kbn/test-jest-helpers'; import { httpServiceMock } from '@kbn/core/public/mocks'; import type { SavedObjectManagementTypeInfo } from '../../../../common/types'; -import { Relationships, RelationshipsProps } from './relationships'; +import { RelationshipsClass as Relationships, RelationshipsProps } from './relationships'; jest.mock('../../../lib/fetch_export_by_type_and_search', () => ({ fetchExportByTypeAndSearch: jest.fn(), @@ -21,6 +21,14 @@ jest.mock('../../../lib/fetch_export_objects', () => ({ fetchExportObjects: jest.fn(), })); +const baseProps = { + euiTablePersist: { + pageSize: 10, + onTableChange: () => {}, + sorting: { sort: { direction: 'asc' as const, field: 'id' as const } }, + }, +}; + const allowedTypes: SavedObjectManagementTypeInfo[] = [ { name: 'index-pattern', @@ -86,7 +94,7 @@ describe('Relationships', () => { close: jest.fn(), }; - const component = shallowWithI18nProvider(); + const component = shallowWithI18nProvider(); // Make sure we are showing loading expect(component.find('EuiLoadingElastic').length).toBe(1); @@ -155,7 +163,7 @@ describe('Relationships', () => { close: jest.fn(), }; - const component = shallowWithI18nProvider(); + const component = shallowWithI18nProvider(); // Make sure we are showing loading expect(component.find('EuiLoadingElastic').length).toBe(1); @@ -223,7 +231,7 @@ describe('Relationships', () => { close: jest.fn(), }; - const component = shallowWithI18nProvider(); + const component = shallowWithI18nProvider(); // Make sure we are showing loading expect(component.find('EuiLoadingElastic').length).toBe(1); @@ -292,7 +300,7 @@ describe('Relationships', () => { showPlainSpinner: true, }; - const component = shallowWithI18nProvider(); + const component = shallowWithI18nProvider(); // Make sure we are showing loading expect(component.find('EuiLoadingSpinner').length).toBe(1); @@ -332,7 +340,7 @@ describe('Relationships', () => { close: jest.fn(), }; - const component = shallowWithI18nProvider(); + const component = shallowWithI18nProvider(); // Ensure all promises resolve await new Promise((resolve) => process.nextTick(resolve)); @@ -378,7 +386,7 @@ describe('Relationships', () => { close: jest.fn(), }; - const component = shallowWithI18nProvider(); + const component = shallowWithI18nProvider(); // Ensure all promises resolve await new Promise((resolve) => process.nextTick(resolve)); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx index 36cb9da9ad436..0d9c71ceae2ff 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx @@ -27,6 +27,10 @@ import { SearchFilterConfig } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { IBasePath } from '@kbn/core/public'; +import { + withEuiTablePersist, + type EuiTablePersistInjectedProps, +} from '@kbn/shared-ux-table-persist'; import type { SavedObjectManagementTypeInfo } from '../../../../common/types'; import { getDefaultTitle, getSavedObjectLabel } from '../../../lib'; import type { v1 } from '../../../../common'; @@ -83,8 +87,11 @@ const relationshipColumn = { }, }; -export class Relationships extends Component { - constructor(props: RelationshipsProps) { +export class RelationshipsClass extends Component< + RelationshipsProps & EuiTablePersistInjectedProps, + RelationshipsState +> { + constructor(props: RelationshipsProps & EuiTablePersistInjectedProps) { super(props); this.state = { @@ -218,7 +225,14 @@ export class Relationships extends Component ({ 'data-test-subj': `relationshipsTableRow`, @@ -420,3 +435,8 @@ export class Relationships extends Component { { it('loads the data from the URI', async () => { await PageObjects.common.navigateToApp('console', { - hash: '#/console?load_from=data:text/plain,BYUwNmD2Q', + hash: '#/console/shell?load_from=data:text/plain,BYUwNmD2Q', }); await retry.try(async () => { @@ -44,7 +44,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('with invalid data', () => { it('shows a toast error', async () => { await PageObjects.common.navigateToApp('console', { - hash: '#/console?load_from=data:text/plain,BYUwNmD2', + hash: '#/console/shell?load_from=data:text/plain,BYUwNmD2', }); await retry.try(async () => { diff --git a/test/functional/apps/visualize/replaced_vislib_chart_types/_area_chart.ts b/test/functional/apps/visualize/replaced_vislib_chart_types/_area_chart.ts index 4d558b1b7c147..73cdc3acf918a 100644 --- a/test/functional/apps/visualize/replaced_vislib_chart_types/_area_chart.ts +++ b/test/functional/apps/visualize/replaced_vislib_chart_types/_area_chart.ts @@ -179,6 +179,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await visEditor.setInterval('Second'); await visEditor.clickGo(); await inspector.open(); + await inspector.setTablePageSize(20); await inspector.expectTableData(expectedTableData); await inspector.close(); }); @@ -211,6 +212,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await visEditor.toggleScaleMetrics(); await visEditor.clickGo(); await inspector.open(); + await inspector.setTablePageSize(20); await inspector.expectTableData(expectedTableData); await inspector.close(); }); @@ -245,6 +247,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await visEditor.selectAggregateWith('average'); await visEditor.clickGo(); await inspector.open(); + await inspector.setTablePageSize(20); await inspector.expectTableData(expectedTableData); await inspector.close(); }); diff --git a/test/functional/services/inspector.ts b/test/functional/services/inspector.ts index b6fa928d61e47..3cfecae420107 100644 --- a/test/functional/services/inspector.ts +++ b/test/functional/services/inspector.ts @@ -97,8 +97,7 @@ export class InspectorService extends FtrService { * @param size rows count */ public async setTablePageSize(size: number): Promise { - const panel = await this.testSubjects.find('inspectorPanel'); - await this.find.clickByButtonText('Rows per page: 20', panel); + await this.testSubjects.click('tablePaginationPopoverButton'); // The buttons for setting table page size are in a popover element. This popover // element appears as if it's part of the inspectorPanel but it's really attached // to the body element by a portal. diff --git a/tsconfig.base.json b/tsconfig.base.json index 223b2d5a58ca2..f6aaa2ee0ac7f 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -606,8 +606,6 @@ "@kbn/core-security-server-mocks/*": ["packages/core/security/core-security-server-mocks/*"], "@kbn/core-status-common": ["packages/core/status/core-status-common"], "@kbn/core-status-common/*": ["packages/core/status/core-status-common/*"], - "@kbn/core-status-common-internal": ["packages/core/status/core-status-common-internal"], - "@kbn/core-status-common-internal/*": ["packages/core/status/core-status-common-internal/*"], "@kbn/core-status-server": ["packages/core/status/core-status-server"], "@kbn/core-status-server/*": ["packages/core/status/core-status-server/*"], "@kbn/core-status-server-internal": ["packages/core/status/core-status-server-internal"], diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx index fbb7b971bfcb4..543367fda127a 100644 --- a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx +++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx @@ -135,6 +135,7 @@ export const AddEmbeddableFlyout: FC = ({ { onIndexPatternSelected(indexPattern as IndexPatternSavedObject); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx index 5aa0ccc46a5cd..ff173c47a5320 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx @@ -125,6 +125,7 @@ export const SourceSelection: FC = () => { )} { = ({ onClose }) => { = ({ = { nodejs: 'nodejs', 'opentelemetry/nodejs': 'opentelemetry_nodejs', + 'opentelemetry/nodejs/elastic': 'opentelemetry_nodejs', java: 'java', 'opentelemetry/java': 'opentelemetry_java', 'opentelemetry/java/opentelemetry-java-instrumentation': 'opentelemetry_java', diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/metrics/static_dashboard/dashboards/opentelemetry_nodejs.json b/x-pack/plugins/observability_solution/apm/public/components/app/metrics/static_dashboard/dashboards/opentelemetry_nodejs.json index b9552e182893b..2eeb4d48b11d1 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/metrics/static_dashboard/dashboards/opentelemetry_nodejs.json +++ b/x-pack/plugins/observability_solution/apm/public/components/app/metrics/static_dashboard/dashboards/opentelemetry_nodejs.json @@ -1 +1 @@ -{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"},"optionsJSON":"{\"useMargins\":true,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"hidePanelTitles\":false}","panelsJSON":"[{\"version\":\"8.10.2\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"f3de253a-8c79-46d0-acb2-05eef8d056be\"},\"panelIndex\":\"f3de253a-8c79-46d0-acb2-05eef8d056be\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"type\":\"index-pattern\",\"id\":\"metrics-*\",\"name\":\"indexpattern-datasource-layer-ed2c0b0b-d8c5-415d-aec6-7681d578d3db\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"bottom\",\"shouldTruncate\":true},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"ed2c0b0b-d8c5-415d-aec6-7681d578d3db\",\"accessors\":[\"8a334ede-47b6-4eae-aeea-1e2bcc472f94\",\"3b9cf7ee-613d-4c9c-8a1b-5fca7fdc5dc7\",\"282f5b98-13c4-41dd-bbf4-5a6f8928a857\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"xAccessor\":\"719ac5fd-a2f0-4bf3-8dbc-c00357e96228\"}],\"yTitle\":\"Usage [bytes]\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"ed2c0b0b-d8c5-415d-aec6-7681d578d3db\":{\"columns\":{\"719ac5fd-a2f0-4bf3-8dbc-c00357e96228\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true,\"dropPartials\":false}},\"8a334ede-47b6-4eae-aeea-1e2bcc472f94\":{\"label\":\"Free\",\"dataType\":\"number\",\"operationType\":\"average\",\"sourceField\":\"system.memory.usage\",\"isBucketed\":false,\"scale\":\"ratio\",\"filter\":{\"query\":\"labels.state: \\\"free\\\" \",\"language\":\"kuery\"},\"params\":{\"emptyAsNull\":true,\"format\":{\"id\":\"bytes\",\"params\":{\"decimals\":2}}},\"customLabel\":true},\"3b9cf7ee-613d-4c9c-8a1b-5fca7fdc5dc7\":{\"label\":\"Used\",\"dataType\":\"number\",\"operationType\":\"average\",\"sourceField\":\"system.memory.usage\",\"isBucketed\":false,\"scale\":\"ratio\",\"filter\":{\"query\":\"labels.state: \\\"used\\\" \",\"language\":\"kuery\"},\"params\":{\"emptyAsNull\":true,\"format\":{\"id\":\"bytes\",\"params\":{\"decimals\":2}}},\"customLabel\":true},\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X0\":{\"label\":\"Part of average(system.memory.usage, kql='labels.state: \\\"used\\\"') + average(system.memory.usage, kql='labels.state: \\\"free\\\"')\",\"dataType\":\"number\",\"operationType\":\"average\",\"sourceField\":\"system.memory.usage\",\"isBucketed\":false,\"scale\":\"ratio\",\"filter\":{\"query\":\"labels.state: \\\"used\\\"\",\"language\":\"kuery\"},\"params\":{\"emptyAsNull\":false},\"customLabel\":true},\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X1\":{\"label\":\"Part of average(system.memory.usage, kql='labels.state: \\\"used\\\"') + average(system.memory.usage, kql='labels.state: \\\"free\\\"')\",\"dataType\":\"number\",\"operationType\":\"average\",\"sourceField\":\"system.memory.usage\",\"isBucketed\":false,\"scale\":\"ratio\",\"filter\":{\"query\":\"labels.state: \\\"free\\\"\",\"language\":\"kuery\"},\"params\":{\"emptyAsNull\":false},\"customLabel\":true},\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X2\":{\"label\":\"Part of average(system.memory.usage, kql='labels.state: \\\"used\\\"') + average(system.memory.usage, kql='labels.state: \\\"free\\\"')\",\"dataType\":\"number\",\"operationType\":\"math\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"tinymathAst\":{\"type\":\"function\",\"name\":\"add\",\"args\":[\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X0\",\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X1\"],\"location\":{\"min\":0,\"max\":115},\"text\":\"average(system.memory.usage, kql='labels.state: \\\"used\\\"') + average(system.memory.usage, kql='labels.state: \\\"free\\\"')\"}},\"references\":[\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X0\",\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X1\"],\"customLabel\":true},\"282f5b98-13c4-41dd-bbf4-5a6f8928a857\":{\"label\":\"Total\",\"dataType\":\"number\",\"operationType\":\"formula\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"formula\":\"average(system.memory.usage, kql='labels.state: \\\"used\\\"') + average(system.memory.usage, kql='labels.state: \\\"free\\\"')\",\"isFormulaBroken\":false,\"format\":{\"id\":\"bytes\",\"params\":{\"decimals\":2}}},\"references\":[\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X2\"],\"customLabel\":true}},\"columnOrder\":[\"719ac5fd-a2f0-4bf3-8dbc-c00357e96228\",\"8a334ede-47b6-4eae-aeea-1e2bcc472f94\",\"3b9cf7ee-613d-4c9c-8a1b-5fca7fdc5dc7\",\"282f5b98-13c4-41dd-bbf4-5a6f8928a857\",\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X0\",\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X1\",\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X2\"],\"incompleteColumns\":{},\"sampling\":1}}},\"indexpattern\":{\"layers\":{}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}},\"hidePanelTitles\":false,\"enhancements\":{}},\"title\":\"System Memory Usage\"},{\"version\":\"8.10.2\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"9dcf1114-6984-4238-8229-cb9e802e0bdb\"},\"panelIndex\":\"9dcf1114-6984-4238-8229-cb9e802e0bdb\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"type\":\"index-pattern\",\"id\":\"apm_static_index_pattern_id\",\"name\":\"indexpattern-datasource-layer-1633fa19-f9f3-4149-90c3-bffd1ba4e6c4\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"bottom\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":false,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar_stacked\",\"layers\":[{\"layerId\":\"1633fa19-f9f3-4149-90c3-bffd1ba4e6c4\",\"accessors\":[\"74908758-6e28-43d5-929a-b4070c9026a1\",\"a49dd439-fc3c-4ebb-a2ca-46c7992cc29f\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"xAccessor\":\"24a38611-9513-4e32-bb79-2a723c11a513\"}],\"yTitle\":\"Utilization [%]\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"1633fa19-f9f3-4149-90c3-bffd1ba4e6c4\":{\"columns\":{\"24a38611-9513-4e32-bb79-2a723c11a513\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true,\"dropPartials\":false}},\"74908758-6e28-43d5-929a-b4070c9026a1\":{\"label\":\"Average\",\"dataType\":\"number\",\"operationType\":\"average\",\"sourceField\":\"system.memory.utilization\",\"isBucketed\":false,\"scale\":\"ratio\",\"filter\":{\"query\":\"labels.state: \\\"used\\\" \",\"language\":\"kuery\"},\"params\":{\"emptyAsNull\":true,\"format\":{\"id\":\"percent\",\"params\":{\"decimals\":2}}},\"customLabel\":true},\"a49dd439-fc3c-4ebb-a2ca-46c7992cc29f\":{\"label\":\"Max\",\"dataType\":\"number\",\"operationType\":\"max\",\"sourceField\":\"system.memory.utilization\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"emptyAsNull\":true,\"format\":{\"id\":\"percent\",\"params\":{\"decimals\":2}}},\"customLabel\":true,\"filter\":{\"query\":\"labels.state: \\\"used\\\" \",\"language\":\"kuery\"}}},\"columnOrder\":[\"24a38611-9513-4e32-bb79-2a723c11a513\",\"74908758-6e28-43d5-929a-b4070c9026a1\",\"a49dd439-fc3c-4ebb-a2ca-46c7992cc29f\"],\"incompleteColumns\":{},\"sampling\":1}}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}},\"hidePanelTitles\":false,\"enhancements\":{}},\"title\":\"System Memory Utilization\"}]","timeRestore":false,"title":"OpenTelemetry Memory Metrics","version":1},"coreMigrationVersion":"8.8.0","created_at":"2023-11-06T12:28:32.691Z","id":"fef77323-303f-4e39-a81f-553c268d16ec","managed":false,"references":[{"id":"metrics-*","name":"f3de253a-8c79-46d0-acb2-05eef8d056be:indexpattern-datasource-layer-ed2c0b0b-d8c5-415d-aec6-7681d578d3db","type":"index-pattern"},{"id":"apm_static_index_pattern_id","name":"9dcf1114-6984-4238-8229-cb9e802e0bdb:indexpattern-datasource-layer-1633fa19-f9f3-4149-90c3-bffd1ba4e6c4","type":"index-pattern"}],"type":"dashboard","typeMigrationVersion":"8.9.0","updated_at":"2023-11-06T12:28:32.691Z","version":"WzM4OSwyXQ=="} \ No newline at end of file +{"attributes":{"controlGroupInput":{"chainingSystem":"HIERARCHICAL","controlStyle":"oneLine","ignoreParentSettingsJSON":"{\"ignoreFilters\":false,\"ignoreQuery\":false,\"ignoreTimerange\":false,\"ignoreValidations\":false}","panelsJSON":"{\"c27c120b-4674-4ad7-9972-cd10bd9c1a34\":{\"grow\":true,\"order\":0,\"type\":\"optionsListControl\",\"width\":\"medium\",\"explicitInput\":{\"id\":\"c27c120b-4674-4ad7-9972-cd10bd9c1a34\",\"dataViewId\":\"metrics-*\",\"fieldName\":\"service.node.name\",\"title\":\"Node\",\"searchTechnique\":\"prefix\",\"selectedOptions\":[],\"sort\":{\"by\":\"_count\",\"direction\":\"desc\"},\"exclude\":true}}}","showApplySelections":false},"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"},"optionsJSON":"{\"useMargins\":true,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"hidePanelTitles\":false}","panelsJSON":"[{\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"48508429-15ea-4628-aa6f-23ca15aa1f55\"},\"panelIndex\":\"48508429-15ea-4628-aa6f-23ca15aa1f55\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"type\":\"index-pattern\",\"id\":\"metrics-*\",\"name\":\"indexpattern-datasource-layer-969de0e0-6fd8-493b-b789-9edd46b5c67c\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"bottom\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"969de0e0-6fd8-493b-b789-9edd46b5c67c\",\"accessors\":[\"0c1e94e4-2029-4558-8963-ceeb3169486a\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"colorMapping\":{\"assignments\":[],\"specialAssignments\":[{\"rule\":{\"type\":\"other\"},\"color\":{\"type\":\"loop\"},\"touched\":false}],\"paletteId\":\"eui_amsterdam_color_blind\",\"colorMode\":{\"type\":\"categorical\"}},\"xAccessor\":\"82a0c81c-a712-46b2-a6c1-c178c3c1c848\",\"splitAccessor\":\"bcb37e21-93fb-4913-a749-75bb1b594002\"}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"969de0e0-6fd8-493b-b789-9edd46b5c67c\":{\"columns\":{\"82a0c81c-a712-46b2-a6c1-c178c3c1c848\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true,\"dropPartials\":false}},\"0c1e94e4-2029-4558-8963-ceeb3169486a\":{\"label\":\"Usage[bytes]\",\"dataType\":\"number\",\"operationType\":\"last_value\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"process.memory.usage\",\"filter\":{\"query\":\"\\\"process.memory.usage\\\": *\",\"language\":\"kuery\"},\"params\":{\"sortField\":\"@timestamp\",\"format\":{\"id\":\"bytes\",\"params\":{\"decimals\":2}}},\"customLabel\":true},\"bcb37e21-93fb-4913-a749-75bb1b594002\":{\"label\":\"Top 5 values of service.node.name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"service.node.name\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"column\",\"columnId\":\"0c1e94e4-2029-4558-8963-ceeb3169486a\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false}}},\"columnOrder\":[\"bcb37e21-93fb-4913-a749-75bb1b594002\",\"82a0c81c-a712-46b2-a6c1-c178c3c1c848\",\"0c1e94e4-2029-4558-8963-ceeb3169486a\"],\"incompleteColumns\":{},\"sampling\":1,\"indexPatternId\":\"metrics-*\"}},\"currentIndexPatternId\":\"metrics-*\"},\"indexpattern\":{\"layers\":{}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}},\"enhancements\":{}},\"title\":\"Process Memory Usage\"},{\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":15,\"w\":24,\"h\":15,\"i\":\"9110e9e1-ef3e-4c17-93cf-ba5770a3c2b0\"},\"panelIndex\":\"9110e9e1-ef3e-4c17-93cf-ba5770a3c2b0\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"type\":\"index-pattern\",\"id\":\"metrics-*\",\"name\":\"indexpattern-datasource-layer-ecea9aed-08bf-47a0-8050-cb8b168bfc05\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"bottom\",\"showSingleSeries\":true},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"ecea9aed-08bf-47a0-8050-cb8b168bfc05\",\"accessors\":[\"9c53bcf0-b852-4b71-a1d4-ad21c324f478\",\"003ad900-7138-4989-9b8c-a7290d7243d1\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"xAccessor\":\"ed17ee1c-af87-455d-96d9-ca21614b5eed\",\"splitAccessor\":\"a137d8d5-8283-45ac-b145-e364dcec6697\",\"collapseFn\":\"\",\"palette\":{\"type\":\"palette\",\"name\":\"default\"}}],\"yTitle\":\"Delay [sec]\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"ecea9aed-08bf-47a0-8050-cb8b168bfc05\":{\"columns\":{\"a137d8d5-8283-45ac-b145-e364dcec6697\":{\"label\":\"Top 5 values of service.node.name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"service.node.name\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"column\",\"columnId\":\"9c53bcf0-b852-4b71-a1d4-ad21c324f478\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false}},\"ed17ee1c-af87-455d-96d9-ca21614b5eed\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true,\"dropPartials\":false}},\"9c53bcf0-b852-4b71-a1d4-ad21c324f478\":{\"label\":\"p90\",\"dataType\":\"number\",\"operationType\":\"last_value\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"nodejs.eventloop.delay.p90\",\"filter\":{\"query\":\"\\\"nodejs.eventloop.delay.p90\\\": *\",\"language\":\"kuery\"},\"params\":{\"sortField\":\"@timestamp\"},\"customLabel\":true},\"003ad900-7138-4989-9b8c-a7290d7243d1\":{\"label\":\"p50\",\"dataType\":\"number\",\"operationType\":\"last_value\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"nodejs.eventloop.delay.p50\",\"filter\":{\"query\":\"\\\"nodejs.eventloop.delay.p50\\\": *\",\"language\":\"kuery\"},\"params\":{\"sortField\":\"@timestamp\"},\"customLabel\":true}},\"columnOrder\":[\"a137d8d5-8283-45ac-b145-e364dcec6697\",\"ed17ee1c-af87-455d-96d9-ca21614b5eed\",\"9c53bcf0-b852-4b71-a1d4-ad21c324f478\",\"003ad900-7138-4989-9b8c-a7290d7243d1\"],\"incompleteColumns\":{},\"sampling\":1}}},\"indexpattern\":{\"layers\":{}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}},\"enhancements\":{}},\"title\":\"Event Loop Delay\"},{\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"3e4d0b5c-0d67-421a-8ce2-b7f4914481c2\"},\"panelIndex\":\"3e4d0b5c-0d67-421a-8ce2-b7f4914481c2\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"type\":\"index-pattern\",\"id\":\"metrics-*\",\"name\":\"indexpattern-datasource-layer-0b0f2810-b098-4db1-a3a9-607e0361b2f1\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"bottom\",\"showSingleSeries\":true},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"0b0f2810-b098-4db1-a3a9-607e0361b2f1\",\"accessors\":[\"64d972ab-b5b0-4c18-884b-a245d99fdc39\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"xAccessor\":\"b9505654-28b7-4121-883b-6745d4660cd3\",\"splitAccessor\":\"dac92d9d-c37d-432e-9bc9-7f82d97a5a8d\",\"collapseFn\":\"\",\"palette\":{\"type\":\"palette\",\"name\":\"default\"}}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"0b0f2810-b098-4db1-a3a9-607e0361b2f1\":{\"columns\":{\"b9505654-28b7-4121-883b-6745d4660cd3\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true,\"dropPartials\":false}},\"64d972ab-b5b0-4c18-884b-a245d99fdc39\":{\"label\":\"Utilization[%]\",\"dataType\":\"number\",\"operationType\":\"last_value\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"nodejs.eventloop.utilization\",\"filter\":{\"query\":\"\\\"nodejs.eventloop.utilization\\\": *\",\"language\":\"kuery\"},\"params\":{\"sortField\":\"@timestamp\",\"format\":{\"id\":\"percent\",\"params\":{\"decimals\":2}}},\"customLabel\":true},\"dac92d9d-c37d-432e-9bc9-7f82d97a5a8d\":{\"label\":\"Top 5 values of service.node.name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"service.node.name\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"column\",\"columnId\":\"64d972ab-b5b0-4c18-884b-a245d99fdc39\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false}}},\"columnOrder\":[\"dac92d9d-c37d-432e-9bc9-7f82d97a5a8d\",\"b9505654-28b7-4121-883b-6745d4660cd3\",\"64d972ab-b5b0-4c18-884b-a245d99fdc39\"],\"incompleteColumns\":{},\"sampling\":1}}},\"indexpattern\":{\"layers\":{}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}},\"enhancements\":{}},\"title\":\"Event Loop Utilization\"},{\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"d0e1efe6-23b1-4e61-8602-6df59ac0609b\"},\"panelIndex\":\"d0e1efe6-23b1-4e61-8602-6df59ac0609b\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"type\":\"index-pattern\",\"id\":\"metrics-*\",\"name\":\"indexpattern-datasource-layer-176f3949-5c04-44e2-a200-810c8fe28183\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"bottom\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"yTitle\":\"Utilization[%]\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"176f3949-5c04-44e2-a200-810c8fe28183\",\"accessors\":[\"d5901d17-43c6-44ff-acd3-02824a9aed37\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"colorMapping\":{\"assignments\":[],\"specialAssignments\":[{\"rule\":{\"type\":\"other\"},\"color\":{\"type\":\"loop\"},\"touched\":false}],\"paletteId\":\"eui_amsterdam_color_blind\",\"colorMode\":{\"type\":\"categorical\"}},\"xAccessor\":\"bfb6cb06-0dcd-4ef8-a49e-993012519d03\",\"splitAccessor\":\"f33b7f72-9b78-4fc3-8c09-4b321b794f31\"}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"176f3949-5c04-44e2-a200-810c8fe28183\":{\"columns\":{\"bfb6cb06-0dcd-4ef8-a49e-993012519d03\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true,\"dropPartials\":false}},\"d5901d17-43c6-44ff-acd3-02824a9aed37\":{\"label\":\"Last value of process.cpu.utilization\",\"dataType\":\"number\",\"operationType\":\"last_value\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"process.cpu.utilization\",\"filter\":{\"query\":\"\\\"process.cpu.utilization\\\": *\",\"language\":\"kuery\"},\"params\":{\"sortField\":\"@timestamp\",\"format\":{\"id\":\"percent\",\"params\":{\"decimals\":2}}}},\"f33b7f72-9b78-4fc3-8c09-4b321b794f31\":{\"label\":\"Top 5 values of service.node.name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"service.node.name\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"column\",\"columnId\":\"d5901d17-43c6-44ff-acd3-02824a9aed37\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false}}},\"columnOrder\":[\"f33b7f72-9b78-4fc3-8c09-4b321b794f31\",\"bfb6cb06-0dcd-4ef8-a49e-993012519d03\",\"d5901d17-43c6-44ff-acd3-02824a9aed37\"],\"incompleteColumns\":{},\"sampling\":1}}},\"indexpattern\":{\"layers\":{}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}},\"enhancements\":{}},\"title\":\"Process CPU Utilization\"}]","timeRestore":false,"title":"OTEL nodejs runtime metrics","version":2},"coreMigrationVersion":"8.8.0","created_at":"2024-10-15T10:37:56.030Z","id":"4fdfa1d4-6dfa-45fa-8f21-37b9f93d30d2","managed":false,"references":[{"id":"metrics-*","name":"48508429-15ea-4628-aa6f-23ca15aa1f55:indexpattern-datasource-layer-969de0e0-6fd8-493b-b789-9edd46b5c67c","type":"index-pattern"},{"id":"metrics-*","name":"9110e9e1-ef3e-4c17-93cf-ba5770a3c2b0:indexpattern-datasource-layer-ecea9aed-08bf-47a0-8050-cb8b168bfc05","type":"index-pattern"},{"id":"metrics-*","name":"3e4d0b5c-0d67-421a-8ce2-b7f4914481c2:indexpattern-datasource-layer-0b0f2810-b098-4db1-a3a9-607e0361b2f1","type":"index-pattern"},{"id":"metrics-*","name":"d0e1efe6-23b1-4e61-8602-6df59ac0609b:indexpattern-datasource-layer-176f3949-5c04-44e2-a200-810c8fe28183","type":"index-pattern"},{"id":"metrics-*","name":"controlGroup_c27c120b-4674-4ad7-9972-cd10bd9c1a34:optionsListDataView","type":"index-pattern"}],"type":"dashboard","typeMigrationVersion":"10.2.0","updated_at":"2024-11-07T18:37:51.713Z","updated_by":"u_elastic_found","version":"WzE1MzcsMTRd"} \ No newline at end of file diff --git a/x-pack/plugins/osquery/server/routes/live_query/create_live_query_route.ts b/x-pack/plugins/osquery/server/routes/live_query/create_live_query_route.ts index 6372a48c89b6e..31edbd59f5400 100644 --- a/x-pack/plugins/osquery/server/routes/live_query/create_live_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/live_query/create_live_query_route.ts @@ -31,6 +31,13 @@ export const createLiveQueryRoute = (router: IRouter, osqueryContext: OsqueryApp .addVersion( { version: API_VERSIONS.public.v1, + security: { + authz: { + enabled: false, + reason: + 'We do the check for 2 different scenarios below (const isInvalid): writeLiveQueries and runSavedQueries with saved_query_id, or pack_id', + }, + }, validate: { request: { body: buildRouteValidation< diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/exceptions_list_item_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/exceptions_list_item_generator.ts index cb332f8dea55a..b7293a3cef16c 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/exceptions_list_item_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/exceptions_list_item_generator.ts @@ -9,13 +9,9 @@ import type { ExceptionListItemSchema, CreateExceptionListItemSchema, UpdateExceptionListItemSchema, + EntriesArray, } from '@kbn/securitysolution-io-ts-list-types'; -import { - ENDPOINT_EVENT_FILTERS_LIST_ID, - ENDPOINT_TRUSTED_APPS_LIST_ID, - ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, - ENDPOINT_BLOCKLISTS_LIST_ID, -} from '@kbn/securitysolution-list-constants'; +import { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants'; import { ConditionEntryField } from '@kbn/securitysolution-utils'; import { BaseDataGenerator } from './base_data_generator'; import { BY_POLICY_ARTIFACT_TAG_PREFIX, GLOBAL_ARTIFACT_TAG } from '../service/artifacts/constants'; @@ -150,7 +146,7 @@ export class ExceptionsListItemGenerator extends BaseDataGenerator = {}): ExceptionListItemSchema { return this.generate({ name: `Trusted app (${this.randomString(5)})`, - list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, + list_id: ENDPOINT_ARTIFACT_LISTS.trustedApps.id, ...overrides, }); } @@ -173,10 +169,33 @@ export class ExceptionsListItemGenerator extends BaseDataGenerator = {}): ExceptionListItemSchema { return this.generate({ name: `Event filter (${this.randomString(5)})`, - list_id: ENDPOINT_EVENT_FILTERS_LIST_ID, + list_id: ENDPOINT_ARTIFACT_LISTS.eventFilters.id, entries: [ { field: 'process.pe.company', @@ -224,7 +243,7 @@ export class ExceptionsListItemGenerator extends BaseDataGenerator { @@ -105,14 +105,15 @@ describe('When invoking Trusted Apps Schema', () => { value: 'c:/programs files/Anti-Virus', ...(data || {}), }); - const createNewTrustedApp = (data?: T): NewTrustedApp => ({ - name: 'Some Anti-Virus App', - description: 'this one is ok', - os: OperatingSystem.WINDOWS, - effectScope: { type: 'global' }, - entries: [createConditionEntry()], - ...(data || {}), - }); + const createNewTrustedApp = (data?: T): NewTrustedApp => + ({ + name: 'Some Anti-Virus App', + description: 'this one is ok', + os: OperatingSystem.WINDOWS, + effectScope: { type: 'global' }, + entries: [createConditionEntry()], + ...(data || {}), + } as NewTrustedApp); const body = PostTrustedAppCreateRequestSchema.body; it('should not error on a valid message', () => { @@ -389,14 +390,15 @@ describe('When invoking Trusted Apps Schema', () => { value: 'c:/programs files/Anti-Virus', ...(data || {}), }); - const createNewTrustedApp = (data?: T): NewTrustedApp => ({ - name: 'Some Anti-Virus App', - description: 'this one is ok', - os: OperatingSystem.WINDOWS, - effectScope: { type: 'global' }, - entries: [createConditionEntry()], - ...(data || {}), - }); + const createNewTrustedApp = (data?: T): NewTrustedApp => + ({ + name: 'Some Anti-Virus App', + description: 'this one is ok', + os: OperatingSystem.WINDOWS, + effectScope: { type: 'global' }, + entries: [createConditionEntry()], + ...(data || {}), + } as NewTrustedApp); const updateParams = (data?: T): PutTrustedAppsRequestParams => ({ id: 'validId', diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts index 4b0a1ee2157de..e31e65195ec95 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts @@ -80,6 +80,15 @@ const LinuxEntrySchema = schema.object({ const MacEntrySchema = schema.object({ ...CommonEntrySchema, + field: schema.oneOf([ + schema.literal(ConditionEntryField.HASH), + schema.literal(ConditionEntryField.PATH), + schema.literal(ConditionEntryField.SIGNER_MAC), + ]), + value: schema.string({ + validate: (field: string) => + field.length > 0 ? undefined : `invalidField.${ConditionEntryField.SIGNER_MAC}`, + }), }); const entriesSchemaOptions = { diff --git a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts index dd97ad02dcb96..4fa99c015a7c7 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts @@ -36,23 +36,32 @@ export interface TrustedAppConditionEntry; export type WindowsConditionEntry = TrustedAppConditionEntry< ConditionEntryField.HASH | ConditionEntryField.PATH | ConditionEntryField.SIGNER >; -export interface MacosLinuxConditionEntries { - os: OperatingSystem.LINUX | OperatingSystem.MAC; - entries: MacosLinuxConditionEntry[]; +export type MacosConditionEntry = TrustedAppConditionEntry< + ConditionEntryField.HASH | ConditionEntryField.PATH | ConditionEntryField.SIGNER_MAC +>; + +interface LinuxConditionEntries { + os: OperatingSystem.LINUX; + entries: LinuxConditionEntry[]; } -export interface WindowsConditionEntries { +interface WindowsConditionEntries { os: OperatingSystem.WINDOWS; entries: WindowsConditionEntry[]; } +interface MacosConditionEntries { + os: OperatingSystem.MAC; + entries: MacosConditionEntry[]; +} + export interface GlobalEffectScope { type: 'global'; } @@ -70,7 +79,7 @@ export type NewTrustedApp = { name: string; description?: string; effectScope: EffectScope; -} & (MacosLinuxConditionEntries | WindowsConditionEntries); +} & (LinuxConditionEntries | WindowsConditionEntries | MacosConditionEntries); /** A trusted app entry */ export type TrustedApp = NewTrustedApp & { diff --git a/x-pack/plugins/security_solution/common/endpoint/utils/kibana_status.ts b/x-pack/plugins/security_solution/common/endpoint/utils/kibana_status.ts index 78147439eedd9..f1fcac6e758c0 100644 --- a/x-pack/plugins/security_solution/common/endpoint/utils/kibana_status.ts +++ b/x-pack/plugins/security_solution/common/endpoint/utils/kibana_status.ts @@ -7,7 +7,7 @@ import { KbnClient } from '@kbn/test'; import type { Client } from '@elastic/elasticsearch'; -import type { StatusResponse } from '@kbn/core-status-common-internal'; +import type { StatusResponse } from '@kbn/core-status-common'; import { catchAxiosErrorFormatAndThrow } from '../format_axios_error'; export const fetchKibanaStatus = async (kbnClient: KbnClient): Promise => { @@ -15,11 +15,11 @@ export const fetchKibanaStatus = async (kbnClient: KbnClient): Promise({ method: 'GET', path: '/api/status', }) - .then((response) => response.data as StatusResponse) + .then(({ data }) => data) .catch(catchAxiosErrorFormatAndThrow); }; /** diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/trusted_apps.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/trusted_apps.cy.ts new file mode 100644 index 0000000000000..aef24ed4ce045 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/trusted_apps.cy.ts @@ -0,0 +1,231 @@ +/* + * 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 { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants'; +import { + createArtifactList, + createPerPolicyArtifact, + removeExceptionsList, + trustedAppsFormSelectors, +} from '../../tasks/artifacts'; +import type { IndexedFleetEndpointPolicyResponse } from '../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy'; +import { createAgentPolicyTask, getEndpointIntegrationVersion } from '../../tasks/fleet'; +import { login } from '../../tasks/login'; + +const { + openTrustedApps, + selectOs, + openFieldSelector, + expectedFieldOptions, + selectField, + fillOutValueField, + fillOutTrustedAppsFlyout, + submitForm, + validateSuccessPopup, + validateRenderedCondition, + clickAndConditionButton, + validateRenderedConditions, + deleteTrustedAppItem, + removeSingleCondition, + expectAllFieldOptionsRendered, + expectFieldOptionsNotRendered, +} = trustedAppsFormSelectors; + +describe( + 'Trusted Apps', + { + tags: ['@ess', '@serverless', '@skipInServerlessMKI'], // @skipInServerlessMKI until kibana is rebuilt after merge + }, + () => { + let indexedPolicy: IndexedFleetEndpointPolicyResponse; + + before(() => { + getEndpointIntegrationVersion().then((version) => { + createAgentPolicyTask(version).then((data) => { + indexedPolicy = data; + }); + }); + }); + + beforeEach(() => { + login(); + }); + + after(() => { + if (indexedPolicy) { + cy.task('deleteIndexedFleetEndpointPolicies', indexedPolicy); + } + }); + + const createArtifactBodyRequest = (multiCondition = false) => ({ + list_id: ENDPOINT_ARTIFACT_LISTS.trustedApps.id, + entries: [ + { + entries: [ + { + field: 'trusted', + operator: 'included', + type: 'match', + value: 'true', + }, + { + field: 'subject_name', + value: 'TestSignature', + type: 'match', + operator: 'included', + }, + ], + field: 'process.code_signature', + type: 'nested', + }, + ...(multiCondition + ? [ + { + field: 'process.hash.sha1', + value: '323769d194406183912bb903e7fe738221543348', + type: 'match', + operator: 'included', + }, + { + field: 'process.executable.caseless', + value: '/dev/null', + type: 'match', + operator: 'included', + }, + ] + : []), + ], + os_types: ['macos'], + }); + + describe('Renders Trusted Apps form fields', () => { + it('Correctly renders all blocklist fields for different OSs', () => { + openTrustedApps({ create: true }); + selectOs('windows'); + expectFieldOptionsNotRendered(); + openFieldSelector(); + expectAllFieldOptionsRendered(); + + selectOs('macos'); + expectFieldOptionsNotRendered(); + openFieldSelector(); + expectAllFieldOptionsRendered(); + + selectOs('linux'); + expectFieldOptionsNotRendered(); + openFieldSelector(); + expectedFieldOptions(['Path', 'Hash']); + }); + }); + + describe('Handles CRUD with signature field', () => { + afterEach(() => { + removeExceptionsList(ENDPOINT_ARTIFACT_LISTS.trustedApps.id); + }); + + it('Correctly creates a trusted app with a single signature field on Mac', () => { + const expectedCondition = /AND\s*process\.code_signature\s*IS\s*TestSignature/; + + openTrustedApps({ create: true }); + fillOutTrustedAppsFlyout(); + selectOs('macos'); + openFieldSelector(); + selectField(); + fillOutValueField('TestSignature'); + submitForm(); + validateSuccessPopup('create'); + validateRenderedCondition(expectedCondition); + }); + + describe('Correctly updates and deletes Mac os trusted app with single signature field', () => { + let itemId: string; + + beforeEach(() => { + createArtifactList(ENDPOINT_ARTIFACT_LISTS.trustedApps.id); + createPerPolicyArtifact('Test TrustedApp', createArtifactBodyRequest()).then( + (response) => { + itemId = response.body.item_id; + } + ); + }); + + it('Updates Mac os single signature field trusted app item', () => { + const expectedCondition = /AND\s*process\.code_signature\s*IS\s*TestSignatureNext/; + openTrustedApps({ itemId }); + fillOutValueField('Next'); + submitForm(); + validateSuccessPopup('update'); + validateRenderedCondition(expectedCondition); + }); + + it('Deletes a blocklist item', () => { + openTrustedApps(); + deleteTrustedAppItem(); + validateSuccessPopup('delete'); + }); + }); + + it('Correctly creates a trusted app with a multiple conditions on Mac', () => { + const expectedCondition = + /\s*OSIS\s*Mac\s*AND\s*process\.code_signature\s*IS\s*TestSignature\s*AND\s*process\.hash\.\*\s*IS\s*323769d194406183912bb903e7fe738221543348\s*AND\s*process\.executable\.caselessIS\s*\/dev\/null\s*/; + + openTrustedApps({ create: true }); + fillOutTrustedAppsFlyout(); + selectOs('macos'); + // Set signature field + openFieldSelector(); + selectField(); + fillOutValueField('TestSignature'); + // Add another condition + clickAndConditionButton(); + // Set hash field + openFieldSelector(1, 1); + selectField('Hash', 1, 1); + fillOutValueField('323769d194406183912bb903e7fe738221543348', 1, 1); + // Add another condition + clickAndConditionButton(); + // Set path field + openFieldSelector(1, 2); + selectField('Path', 1, 2); + fillOutValueField('/dev/null', 1, 2); + + submitForm(); + validateSuccessPopup('create'); + validateRenderedConditions(expectedCondition); + }); + + describe('Correctly updates and deletes Mac os trusted app with multiple conditions', () => { + let itemId: string; + + beforeEach(() => { + createArtifactList(ENDPOINT_ARTIFACT_LISTS.trustedApps.id); + createPerPolicyArtifact('Test TrustedApp', createArtifactBodyRequest(true)).then( + (response) => { + itemId = response.body.item_id; + } + ); + }); + + it('Updates Mac os multiple condition trusted app item', () => { + const expectedCondition = + /\s*AND\s*process\.code_signature\s*IS\s*TestSignature\s*AND\s*process\.executable\.caselessIS\s*\/dev\/null\s*/; + openTrustedApps({ itemId }); + removeSingleCondition(1, 1); + submitForm(); + validateSuccessPopup('update'); + validateRenderedCondition(expectedCondition); + }); + + it('Deletes a blocklist item', () => { + openTrustedApps(); + deleteTrustedAppItem(); + validateSuccessPopup('delete'); + }); + }); + }); + } +); diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/artifacts.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/artifacts.ts index fdffa0bd03381..034ea11d87a83 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/artifacts.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/artifacts.ts @@ -18,7 +18,7 @@ import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL, } from '@kbn/securitysolution-list-constants'; -import { APP_BLOCKLIST_PATH } from '../../../../common/constants'; +import { APP_BLOCKLIST_PATH, APP_TRUSTED_APPS_PATH } from '../../../../common/constants'; import { loadPage, request } from './common'; export const removeAllArtifacts = () => { @@ -108,6 +108,128 @@ export const yieldFirstPolicyID = (): Cypress.Chainable => return body.items[0].id; }); +export const trustedAppsFormSelectors = { + selectOs: (os: 'windows' | 'macos' | 'linux') => { + cy.getByTestSubj('trustedApps-form-osSelectField').click(); + cy.get(`button[role="option"][id="${os}"]`).click(); + }, + + openFieldSelector: (group = 1, entry = 0) => { + cy.getByTestSubj( + `trustedApps-form-conditionsBuilder-group${group}-entry${entry}-field` + ).click(); + }, + + selectField: (field: 'Signature' | 'Hash' | 'Path' = 'Signature', group = 1, entry = 0) => { + cy.getByTestSubj( + `trustedApps-form-conditionsBuilder-group${group}-entry${entry}-field-type-${field}` + ).click(); + }, + + fillOutValueField: (value: string, group = 1, entry = 0) => { + cy.getByTestSubj(`trustedApps-form-conditionsBuilder-group${group}-entry${entry}-value`).type( + value + ); + }, + + clickAndConditionButton: () => { + cy.getByTestSubj('trustedApps-form-conditionsBuilder-group1-AndButton').click(); + }, + + submitForm: () => { + cy.getByTestSubj('trustedAppsListPage-flyout-submitButton').click(); + }, + + fillOutTrustedAppsFlyout: () => { + cy.getByTestSubj('trustedApps-form-nameTextField').type('Test TrustedApp'); + cy.getByTestSubj('trustedApps-form-descriptionField').type('Test Description'); + }, + + expectedFieldOptions: (fields = ['Path', 'Hash', 'Signature']) => { + if (fields.length) { + fields.forEach((field) => { + cy.getByTestSubj( + `trustedApps-form-conditionsBuilder-group1-entry0-field-type-${field}` + ).contains(field); + }); + } else { + const fields2 = ['Path', 'Hash', 'Signature']; + fields2.forEach((field) => { + cy.getByTestSubj( + `trustedApps-form-conditionsBuilder-group1-entry0-field-type-${field}` + ).should('not.exist'); + }); + } + }, + + expectAllFieldOptionsRendered: () => { + trustedAppsFormSelectors.expectedFieldOptions(); + }, + + expectFieldOptionsNotRendered: () => { + trustedAppsFormSelectors.expectedFieldOptions([]); + }, + + openTrustedApps: ({ create, itemId }: { create?: boolean; itemId?: string } = {}) => { + if (!create && !itemId) { + loadPage(APP_TRUSTED_APPS_PATH); + } else if (create) { + loadPage(`${APP_TRUSTED_APPS_PATH}?show=create`); + } else if (itemId) { + loadPage(`${APP_TRUSTED_APPS_PATH}?itemId=${itemId}&show=edit`); + } + }, + + validateSuccessPopup: (type: 'create' | 'update' | 'delete') => { + let expectedTitle = ''; + switch (type) { + case 'create': + expectedTitle = '"Test TrustedApp" has been added to your trusted applications.'; + break; + case 'update': + expectedTitle = '"Test TrustedApp" has been updated'; + break; + case 'delete': + expectedTitle = '"Test TrustedApp" has been removed from trusted applications.'; + break; + } + cy.getByTestSubj('euiToastHeader__title').contains(expectedTitle); + }, + + validateRenderedCondition: (expectedCondition: RegExp) => { + cy.getByTestSubj('trustedAppsListPage-card') + .first() + .within(() => { + cy.getByTestSubj('trustedAppsListPage-card-criteriaConditions-os') + .invoke('text') + .should('match', /OS\s*IS\s*Mac/); + cy.getByTestSubj('trustedAppsListPage-card-criteriaConditions-condition') + .invoke('text') + .should('match', expectedCondition); + }); + }, + validateRenderedConditions: (expectedConditions: RegExp) => { + cy.getByTestSubj('trustedAppsListPage-card-criteriaConditions') + .invoke('text') + .should('match', expectedConditions); + }, + removeSingleCondition: (group = 1, entry = 0) => { + cy.getByTestSubj( + `trustedApps-form-conditionsBuilder-group${group}-entry${entry}-remove` + ).click(); + }, + deleteTrustedAppItem: () => { + cy.getByTestSubj('trustedAppsListPage-card') + .first() + .within(() => { + cy.getByTestSubj('trustedAppsListPage-card-header-actions-button').click(); + }); + + cy.getByTestSubj('trustedAppsListPage-card-cardDeleteAction').click(); + cy.getByTestSubj('trustedAppsListPage-deleteModal-submitButton').click(); + }, +}; + export const blocklistFormSelectors = { expectSingleOperator: (field: 'Path' | 'Signature' | 'Hash') => { cy.getByTestSubj('blocklist-form-field-select').contains(field); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts index 082435817d43d..f6dff90b48227 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts @@ -8,18 +8,14 @@ import { ConditionEntryField } from '@kbn/securitysolution-utils'; import type { TrustedAppConditionEntry, - MacosLinuxConditionEntry, - WindowsConditionEntry, + LinuxConditionEntry, } from '../../../../../common/endpoint/types'; -export const isWindowsTrustedAppCondition = ( +export const isSignerFieldExcluded = ( condition: TrustedAppConditionEntry -): condition is WindowsConditionEntry => { - return condition.field === ConditionEntryField.SIGNER || true; -}; - -export const isMacosLinuxTrustedAppCondition = ( - condition: TrustedAppConditionEntry -): condition is MacosLinuxConditionEntry => { - return condition.field !== ConditionEntryField.SIGNER; +): condition is LinuxConditionEntry => { + return ( + condition.field !== ConditionEntryField.SIGNER && + condition.field !== ConditionEntryField.SIGNER_MAC + ); }; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.test.tsx index 6ba6a0b91d4eb..100e10e16cb99 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.test.tsx @@ -152,13 +152,13 @@ describe('Condition entry input', () => { expect(superSelectProps.options.length).toBe(2); }); - it('should be able to select two options when MAC OS', () => { + it('should be able to select three options when MAC OS', () => { const element = mount(getElement('testCheckSignatureOption', { os: OperatingSystem.MAC })); const superSelectProps = element .find('[data-test-subj="testCheckSignatureOption-field"]') .first() .props() as EuiSuperSelectProps; - expect(superSelectProps.options.length).toBe(2); + expect(superSelectProps.options.length).toBe(3); }); it('should have operator value selected when field is HASH', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.tsx index 3ce26c70d3186..b55c86b939395 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.tsx @@ -143,6 +143,18 @@ export const ConditionEntryInput = memo( }, ] : []), + ...(os === OperatingSystem.MAC + ? [ + { + dropdownDisplay: getDropdownDisplay(ConditionEntryField.SIGNER_MAC), + inputDisplay: CONDITION_FIELD_TITLE[ConditionEntryField.SIGNER_MAC], + value: ConditionEntryField.SIGNER_MAC, + 'data-test-subj': getTestId( + `field-type-${CONDITION_FIELD_TITLE[ConditionEntryField.SIGNER_MAC]}` + ), + }, + ] + : []), ]; }, [getTestId, os]); @@ -224,7 +236,7 @@ export const ConditionEntryInput = memo( - {/* Unicode `nbsp` is used below so that Remove button is property displayed */} + {/* Unicode `nbsp` is used below so that Remove button is properly displayed */} ( entries: [] as ArtifactFormComponentProps['item']['entries'], }; - if (os !== OperatingSystem.WINDOWS) { - const macOsLinuxConditionEntry = item.entries.filter((entry) => - isMacosLinuxTrustedAppCondition(entry as TrustedAppConditionEntry) - ); - nextItem.entries.push(...macOsLinuxConditionEntry); - if (item.entries.length === 0) { - nextItem.entries.push(defaultConditionEntry()); - } - } else { - nextItem.entries.push(...item.entries); + switch (os) { + case OperatingSystem.LINUX: + nextItem.entries = item.entries.filter((entry) => + isSignerFieldExcluded(entry as TrustedAppConditionEntry) + ); + if (item.entries.length === 0) { + nextItem.entries.push(defaultConditionEntry()); + } + break; + case OperatingSystem.MAC: + nextItem.entries = item.entries.map((entry) => + entry.field === ConditionEntryField.SIGNER + ? { ...entry, field: ConditionEntryField.SIGNER_MAC } + : entry + ); + if (item.entries.length === 0) { + nextItem.entries.push(defaultConditionEntry()); + } + break; + case OperatingSystem.WINDOWS: + nextItem.entries = item.entries.map((entry) => + entry.field === ConditionEntryField.SIGNER_MAC + ? { ...entry, field: ConditionEntryField.SIGNER } + : entry + ); + if (item.entries.length === 0) { + nextItem.entries.push(defaultConditionEntry()); + } + break; + default: + nextItem.entries.push(...item.entries); + break; } processChanged(nextItem); @@ -429,17 +448,15 @@ export const TrustedAppsForm = memo( entries: [], }; const os = ((item.os_types ?? [])[0] as OperatingSystem) ?? OperatingSystem.WINDOWS; - if (os === OperatingSystem.WINDOWS) { - nextItem.entries = [...item.entries, defaultConditionEntry()].filter((entry) => - isWindowsTrustedAppCondition(entry as TrustedAppConditionEntry) - ); - } else { + if (os === OperatingSystem.LINUX) { nextItem.entries = [ ...item.entries.filter((entry) => - isMacosLinuxTrustedAppCondition(entry as TrustedAppConditionEntry) + isSignerFieldExcluded(entry as TrustedAppConditionEntry) ), defaultConditionEntry(), ]; + } else { + nextItem.entries = [...item.entries, defaultConditionEntry()]; } processChanged(nextItem); setHasFormChanged(true); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts index c1e544be537ef..0208898617c49 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts @@ -8,9 +8,10 @@ import { i18n } from '@kbn/i18n'; import { ConditionEntryField } from '@kbn/securitysolution-utils'; import type { - MacosLinuxConditionEntry, + LinuxConditionEntry, WindowsConditionEntry, OperatorFieldIds, + MacosConditionEntry, } from '../../../../../common/endpoint/types'; export const NAME_LABEL = i18n.translate('xpack.securitySolution.trustedApps.name.label', { @@ -68,6 +69,10 @@ export const CONDITION_FIELD_TITLE: { [K in ConditionEntryField]: string } = { 'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.signature', { defaultMessage: 'Signature' } ), + [ConditionEntryField.SIGNER_MAC]: i18n.translate( + 'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.signatureMac', + { defaultMessage: 'Signature' } + ), }; export const CONDITION_FIELD_DESCRIPTION: { [K in ConditionEntryField]: string } = { @@ -83,6 +88,10 @@ export const CONDITION_FIELD_DESCRIPTION: { [K in ConditionEntryField]: string } 'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.description.signature', { defaultMessage: 'The signer of the application' } ), + [ConditionEntryField.SIGNER_MAC]: i18n.translate( + 'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.description.signatureMac', + { defaultMessage: 'The signer of the application' } + ), }; export const OPERATOR_TITLES: { [K in OperatorFieldIds]: string } = { @@ -95,7 +104,10 @@ export const OPERATOR_TITLES: { [K in OperatorFieldIds]: string } = { }; export const ENTRY_PROPERTY_TITLES: Readonly<{ - [K in keyof Omit]: string; + [K in keyof Omit< + LinuxConditionEntry | WindowsConditionEntry | MacosConditionEntry, + 'type' + >]: string; }> = { field: i18n.translate('xpack.securitySolution.trustedapps.trustedapp.entry.field', { defaultMessage: 'Field', diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts index 955b11f198a74..950fd37bce2de 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts @@ -83,6 +83,11 @@ export function registerEndpointRoutes( .addVersion( { version: '2023-10-31', + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, validate: { request: GetMetadataRequestSchema, }, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/index.ts index cbbd53555767e..7b60f4fba8be8 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/index.ts @@ -29,6 +29,11 @@ export function registerPolicyRoutes( .addVersion( { version: '2023-10-31', + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, validate: { request: GetPolicyResponseSchema, }, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts index d8cb4db4b0a65..0edd0e90d4907 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts @@ -35,6 +35,11 @@ export const registerResolverRoutes = ( router.post( { path: '/api/endpoint/resolver/tree', + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, validate: validateTree, options: { authRequired: true }, }, @@ -44,6 +49,11 @@ export const registerResolverRoutes = ( router.post( { path: '/api/endpoint/resolver/events', + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, validate: validateEvents, options: { authRequired: true }, }, @@ -56,6 +66,11 @@ export const registerResolverRoutes = ( router.get( { path: '/api/endpoint/resolver/entity', + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, validate: validateEntities, options: { authRequired: true }, }, diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts index 38dd3442f3b4f..3b8a77a964006 100644 --- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants'; +import { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants'; import type { TypeOf } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; @@ -30,7 +30,8 @@ const ProcessHashField = schema.oneOf([ schema.literal('process.hash.sha256'), ]); const ProcessExecutablePath = schema.literal('process.executable.caseless'); -const ProcessCodeSigner = schema.literal('process.Ext.code_signature'); +const ProcessWindowsCodeSigner = schema.literal('process.Ext.code_signature'); +const ProcessMacCodeSigner = schema.literal('process.code_signature'); const ConditionEntryTypeSchema = schema.conditional( schema.siblingRef('field'), @@ -43,7 +44,8 @@ const ConditionEntryOperatorSchema = schema.literal('included'); type ConditionEntryFieldAllowedType = | TypeOf | TypeOf - | TypeOf; + | TypeOf + | TypeOf; type TrustedAppConditionEntry< T extends ConditionEntryFieldAllowedType = ConditionEntryFieldAllowedType @@ -54,7 +56,8 @@ type TrustedAppConditionEntry< operator: 'included'; value: string; } - | TypeOf; + | TypeOf + | TypeOf; /* * A generic Entry schema to be used for a specific entry schema depending on the OS @@ -85,11 +88,10 @@ const CommonEntrySchema = { ), }; -// Windows Signer entries use a Nested field that checks to ensure +// Windows/MacOS Signer entries use a Nested field that checks to ensure // that the certificate is trusted -const WindowsSignerEntrySchema = schema.object({ +const SignerEntrySchema = { type: schema.literal('nested'), - field: ProcessCodeSigner, entries: schema.arrayOf( schema.oneOf([ schema.object({ @@ -107,21 +109,35 @@ const WindowsSignerEntrySchema = schema.object({ ]), { minSize: 2, maxSize: 2 } ), +}; + +const SignerWindowsEntrySchema = schema.object({ + ...SignerEntrySchema, + field: ProcessWindowsCodeSigner, +}); + +const SignerMacEntrySchema = schema.object({ + ...SignerEntrySchema, + field: ProcessMacCodeSigner, }); const WindowsEntrySchema = schema.oneOf([ - WindowsSignerEntrySchema, + SignerWindowsEntrySchema, schema.object({ ...CommonEntrySchema, field: schema.oneOf([ProcessHashField, ProcessExecutablePath]), }), ]); -const LinuxEntrySchema = schema.object({ - ...CommonEntrySchema, -}); +const MacEntrySchema = schema.oneOf([ + SignerMacEntrySchema, + schema.object({ + ...CommonEntrySchema, + field: schema.oneOf([ProcessHashField, ProcessExecutablePath]), + }), +]); -const MacEntrySchema = schema.object({ +const LinuxEntrySchema = schema.object({ ...CommonEntrySchema, }); @@ -172,7 +188,7 @@ const TrustedAppDataSchema = schema.object( export class TrustedAppValidator extends BaseValidator { static isTrustedApp(item: { listId: string }): boolean { - return item.listId === ENDPOINT_TRUSTED_APPS_LIST_ID; + return item.listId === ENDPOINT_ARTIFACT_LISTS.trustedApps.id; } protected async validateHasWritePrivilege(): Promise { diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index c0b84c2b70a84..cbd992ea78d8b 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -126,7 +126,7 @@ "@kbn/dev-cli-errors", "@kbn/dev-utils", "@kbn/tooling-log", - "@kbn/core-status-common-internal", + "@kbn/core-status-common", "@kbn/repo-info", "@kbn/storybook", "@kbn/controls-plugin", diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/search_selection/search_selection.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/search_selection/search_selection.tsx index 7c0b03f7b9856..7ebb8702669b9 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/search_selection/search_selection.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/search_selection/search_selection.tsx @@ -48,6 +48,7 @@ export const SearchSelection: FC = ({ { + const body = trustedAppApiCall.getBody(); + + body.os_types = ['windows']; + body.entries = exceptionsGenerator.generateTrustedAppSignerEntry('mac'); + + await endpointPolicyManagerSupertest[trustedAppApiCall.method](trustedAppApiCall.path) + .set('kbn-xsrf', 'true') + .send(body) + .expect(400); + }); + + it(`should error on [${trustedAppApiCall.method} if Windows signer field is used for Mac entry`, async () => { + const body = trustedAppApiCall.getBody(); + + body.os_types = ['macos']; + body.entries = exceptionsGenerator.generateTrustedAppSignerEntry(); + + await endpointPolicyManagerSupertest[trustedAppApiCall.method](trustedAppApiCall.path) + .set('kbn-xsrf', 'true') + .send(body) + .expect(400); + }); + + it('should not error if signer is set for a windows os entry item', async () => { + const body = trustedAppApiCalls[0].getBody(); + + body.os_types = ['windows']; + body.entries = exceptionsGenerator.generateTrustedAppSignerEntry(); + + await endpointPolicyManagerSupertest[trustedAppApiCalls[0].method]( + trustedAppApiCalls[0].path + ) + .set('kbn-xsrf', 'true') + .send(body) + .expect(200); + }); + + it('should not error if signer is set for a mac os entry item', async () => { + const body = trustedAppApiCalls[0].getBody(); + + body.os_types = ['macos']; + body.entries = exceptionsGenerator.generateTrustedAppSignerEntry('mac'); + + await endpointPolicyManagerSupertest[trustedAppApiCalls[0].method]( + trustedAppApiCalls[0].path + ) + .set('kbn-xsrf', 'true') + .send(body) + .expect(200); + }); + it(`should error on [${trustedAppApiCall.method}] if more than one OS is set`, async () => { const body = trustedAppApiCall.getBody(); diff --git a/yarn.lock b/yarn.lock index b69c7755172e4..c2add93693fe7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4458,10 +4458,6 @@ version "0.0.0" uid "" -"@kbn/core-status-common-internal@link:packages/core/status/core-status-common-internal": - version "0.0.0" - uid "" - "@kbn/core-status-common@link:packages/core/status/core-status-common": version "0.0.0" uid ""