diff --git a/.buildkite/pipelines/quality-gates/pipeline.tests-qa.yaml b/.buildkite/pipelines/quality-gates/pipeline.tests-qa.yaml index ede4f047c7a11..c72d3e078c879 100644 --- a/.buildkite/pipelines/quality-gates/pipeline.tests-qa.yaml +++ b/.buildkite/pipelines/quality-gates/pipeline.tests-qa.yaml @@ -5,7 +5,7 @@ steps: - label: ":kibana: Kibana Serverless Tests for ${ENVIRONMENT}" trigger: appex-qa-serverless-kibana-ftr-tests # https://buildkite.com/elastic/appex-qa-serverless-kibana-ftr-tests - soft_fail: true # Remove this before release or when tests stabilize + soft_fail: true # Remove when tests stabilize build: env: ENVIRONMENT: ${ENVIRONMENT} @@ -16,7 +16,6 @@ steps: # TODO: Uncomment this code when the integration is ready. # - label: ":pipeline::female-detective::seedling: Trigger Security Solution quality gate script" # trigger: security-serverless-quality-gate # https://buildkite.com/elastic/security-serverless-quality-gate - # soft_fail: true # Remove this when tests are fixed # build: # env: # ENVIRONMENT: ${ENVIRONMENT} diff --git a/.buildkite/pipelines/quality-gates/pipeline.tests-staging.yaml b/.buildkite/pipelines/quality-gates/pipeline.tests-staging.yaml index 2630b555aa7a0..6b450aad7553e 100644 --- a/.buildkite/pipelines/quality-gates/pipeline.tests-staging.yaml +++ b/.buildkite/pipelines/quality-gates/pipeline.tests-staging.yaml @@ -13,7 +13,7 @@ steps: - label: ":kibana: Kibana Serverless Tests for ${ENVIRONMENT}" trigger: appex-qa-serverless-kibana-ftr-tests # https://buildkite.com/elastic/appex-qa-serverless-kibana-ftr-tests - soft_fail: true # Remove this before release or when tests stabilize + soft_fail: true # Remove when tests stabilize build: env: ENVIRONMENT: ${ENVIRONMENT} diff --git a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/get_cluster_info.ts b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/get_cluster_info.ts index a11365feef739..3d1f6e539936f 100644 --- a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/get_cluster_info.ts +++ b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/get_cluster_info.ts @@ -15,7 +15,7 @@ export interface ClusterInfo { cluster_name: string; cluster_uuid: string; cluster_version: string; - cluster_build_flavor: string; + cluster_build_flavor?: string; } /** diff --git a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/register_analytics_context_provider.ts b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/register_analytics_context_provider.ts index be909f5ba21e5..35845519d1e16 100644 --- a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/register_analytics_context_provider.ts +++ b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/register_analytics_context_provider.ts @@ -27,7 +27,10 @@ export function registerAnalyticsContextProvider( cluster_name: { type: 'keyword', _meta: { description: 'The Cluster Name' } }, cluster_uuid: { type: 'keyword', _meta: { description: 'The Cluster UUID' } }, cluster_version: { type: 'keyword', _meta: { description: 'The Cluster version' } }, - cluster_build_flavor: { type: 'keyword', _meta: { description: 'The Cluster build flavor' } }, + cluster_build_flavor: { + type: 'keyword', + _meta: { description: 'The Cluster build flavor', optional: true }, + }, }, }); } diff --git a/packages/core/http/core-http-router-server-internal/src/patch_requests.ts b/packages/core/http/core-http-router-server-internal/src/patch_requests.ts new file mode 100644 index 0000000000000..685c815109a01 --- /dev/null +++ b/packages/core/http/core-http-router-server-internal/src/patch_requests.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// @ts-expect-error not in the definition file +import HapiRequest from '@hapi/hapi/lib/request.js'; +import { IncomingMessage } from 'http'; +import { inspect } from 'util'; + +export const patchRequest = () => { + // HAPI request + HapiRequest.prototype.toString = function () { + return `[HAPI.Request method="${this.method}" url="${this.url}"]`; + }; + + HapiRequest.prototype.toJSON = function () { + return { + method: this.method, + url: String(this.url), + }; + }; + + HapiRequest.prototype[inspect.custom] = function () { + return this.toJSON(); + }; + + // http.IncomingMessage + const IncomingMessageProto = IncomingMessage.prototype; + + IncomingMessageProto.toString = function () { + return `[http.IncomingMessage method="${this.method}" url="${this.url}" complete="${this.complete}" aborted="${this.aborted}"]`; + }; + + // @ts-expect-error missing definition + IncomingMessageProto.toJSON = function () { + return { + method: this.method, + url: this.url, + complete: this.complete, + aborted: this.aborted, + }; + }; + + // @ts-expect-error missing definition + IncomingMessageProto[inspect.custom] = function () { + // @ts-expect-error missing definition + return this.toJSON(); + }; +}; diff --git a/packages/core/http/core-http-router-server-internal/src/request.ts b/packages/core/http/core-http-router-server-internal/src/request.ts index 66efdbd61587c..307950c1fe05d 100644 --- a/packages/core/http/core-http-router-server-internal/src/request.ts +++ b/packages/core/http/core-http-router-server-internal/src/request.ts @@ -8,6 +8,7 @@ import { URL } from 'url'; import { v4 as uuidv4 } from 'uuid'; +import { inspect } from 'util'; import type { Request, RouteOptions } from '@hapi/hapi'; import { fromEvent, NEVER } from 'rxjs'; import { shareReplay, first, filter } from 'rxjs/operators'; @@ -36,6 +37,10 @@ import { import { RouteValidator } from './validator'; import { isSafeMethod } from './route'; import { KibanaSocket } from './socket'; +import { patchRequest } from './patch_requests'; + +// patching at module load +patchRequest(); const requestSymbol = Symbol('request'); @@ -174,6 +179,29 @@ export class CoreKibanaRequest< }; } + toString() { + return `[CoreKibanaRequest id="${this.id}" method="${this.route.method}" url="${this.url}" fake="${this.isFakeRequest}" system="${this.isSystemRequest}" api="${this.isInternalApiRequest}"]`; + } + + toJSON() { + return { + id: this.id, + uuid: this.uuid, + url: `${this.url}`, + isFakeRequest: this.isFakeRequest, + isSystemRequest: this.isSystemRequest, + isInternalApiRequest: this.isInternalApiRequest, + auth: { + isAuthenticated: this.auth.isAuthenticated, + }, + route: this.route, + }; + } + + [inspect.custom]() { + return this.toJSON(); + } + private getEvents(request: RawRequest): KibanaRequestEvents { if (isFakeRawRequest(request)) { return { diff --git a/packages/shared-ux/link/redirect_app/impl/src/redirect_app_links.styles.ts b/packages/shared-ux/link/redirect_app/impl/src/redirect_app_links.styles.ts index 7f53924dff531..6ce56cb4aa67f 100644 --- a/packages/shared-ux/link/redirect_app/impl/src/redirect_app_links.styles.ts +++ b/packages/shared-ux/link/redirect_app/impl/src/redirect_app_links.styles.ts @@ -10,8 +10,8 @@ import { css } from '@emotion/react'; export const redirectAppLinksStyles = css({ display: 'inherit', + height: 'inherit', + width: 'inherit', flex: '1', flexFlow: 'column nowrap', - height: '100%', - width: '100%', }); diff --git a/src/core/server/integration_tests/http/request_representation.ts b/src/core/server/integration_tests/http/request_representation.ts new file mode 100644 index 0000000000000..28a871ea8f849 --- /dev/null +++ b/src/core/server/integration_tests/http/request_representation.ts @@ -0,0 +1,248 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'), +})); + +import supertest from 'supertest'; +import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; +import { executionContextServiceMock } from '@kbn/core-execution-context-server-mocks'; +import { contextServiceMock } from '@kbn/core-http-context-server-mocks'; +import type { HttpService } from '@kbn/core-http-server-internal'; +import { ensureRawRequest } from '@kbn/core-http-router-server-internal'; +import { createHttpServer } from '@kbn/core-http-server-mocks'; +import { inspect } from 'util'; + +let server: HttpService; + +let logger: ReturnType; +const contextSetup = contextServiceMock.createSetupContract(); + +const setupDeps = { + context: contextSetup, + executionContext: executionContextServiceMock.createInternalSetupContract(), +}; + +beforeEach(async () => { + logger = loggingSystemMock.create(); + + server = createHttpServer({ logger }); + await server.preboot({ context: contextServiceMock.createPrebootContract() }); +}); + +afterEach(async () => { + await server.stop(); +}); + +const replacePorts = (input: string): string => input.replace(/[:][0-9]+[/]/g, ':XXXX/'); + +describe('request logging', () => { + describe('KibanaRequest', () => { + it('has expected string representation', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + router.get( + { path: '/', validate: false, options: { authRequired: true } }, + (context, req, res) => { + return res.ok({ body: { req: String(req) } }); + } + ); + await server.start(); + + const response = await supertest(innerServer.listener).get('/').expect(200); + expect(replacePorts(response.body.req)).toEqual( + `[CoreKibanaRequest id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" method="get" url="http://127.0.0.1:XXXX/" fake="false" system="false" api="false"]` + ); + }); + + it('has expected JSON representation', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + router.get( + { path: '/', validate: false, options: { authRequired: true } }, + (context, req, res) => { + return res.ok({ body: { req: JSON.stringify(req) } }); + } + ); + await server.start(); + + const response = await supertest(innerServer.listener).get('/').expect(200); + + expect(JSON.parse(replacePorts(response.body.req))).toEqual({ + id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', + url: 'http://127.0.0.1:XXXX/', + isFakeRequest: false, + isInternalApiRequest: false, + isSystemRequest: false, + auth: { + isAuthenticated: false, + }, + route: { + method: 'get', + path: '/', + options: expect.any(Object), + }, + uuid: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', + }); + }); + + it('has expected inspect representation', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + router.get( + { path: '/', validate: false, options: { authRequired: true } }, + (context, req, res) => { + return res.ok({ body: { req: inspect(req) } }); + } + ); + await server.start(); + + const response = await supertest(innerServer.listener).get('/').expect(200); + expect(replacePorts(response.body.req)).toMatchInlineSnapshot(` + "{ + id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', + uuid: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', + url: 'http://127.0.0.1:XXXX/', + isFakeRequest: false, + isSystemRequest: false, + isInternalApiRequest: false, + auth: { isAuthenticated: false }, + route: { + path: '/', + method: 'get', + options: { + authRequired: true, + xsrfRequired: false, + access: 'internal', + tags: [], + timeout: [Object], + body: undefined + } + } + }" + `); + }); + }); + + describe('HAPI request', () => { + it('has expected string representation', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + router.get( + { path: '/', validate: false, options: { authRequired: true } }, + (context, req, res) => { + const rawRequest = ensureRawRequest(req); + return res.ok({ body: { req: String(rawRequest) } }); + } + ); + await server.start(); + + const response = await supertest(innerServer.listener).get('/').expect(200); + expect(replacePorts(response.body.req)).toEqual( + `[HAPI.Request method="get" url="http://127.0.0.1:XXXX/"]` + ); + }); + + it('has expected JSON representation', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + router.get( + { path: '/', validate: false, options: { authRequired: true } }, + (context, req, res) => { + const rawRequest = ensureRawRequest(req); + return res.ok({ body: { req: JSON.stringify(rawRequest) } }); + } + ); + await server.start(); + + const response = await supertest(innerServer.listener).get('/').expect(200); + expect(JSON.parse(replacePorts(response.body.req))).toEqual({ + method: 'get', + url: 'http://127.0.0.1:XXXX/', + }); + }); + + it('has expected inspect representation', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + router.get( + { path: '/', validate: false, options: { authRequired: true } }, + (context, req, res) => { + const rawRequest = ensureRawRequest(req); + return res.ok({ body: { req: inspect(rawRequest) } }); + } + ); + await server.start(); + + const response = await supertest(innerServer.listener).get('/').expect(200); + expect(replacePorts(response.body.req)).toMatchInlineSnapshot( + `"{ method: 'get', url: 'http://127.0.0.1:XXXX/' }"` + ); + }); + }); + + describe('http.IncomingMessage', () => { + it('has expected string representation', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + router.get( + { path: '/', validate: false, options: { authRequired: true } }, + (context, req, res) => { + const rawRawRequest = ensureRawRequest(req).raw.req; + return res.ok({ body: { req: String(rawRawRequest) } }); + } + ); + await server.start(); + + const response = await supertest(innerServer.listener).get('/').expect(200); + expect(replacePorts(response.body.req)).toEqual( + `[http.IncomingMessage method="GET" url="/" complete="true" aborted="false"]` + ); + }); + + it('has expected JSON representation', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + router.get( + { path: '/', validate: false, options: { authRequired: true } }, + (context, req, res) => { + const rawRawRequest = ensureRawRequest(req).raw.req; + return res.ok({ body: { req: JSON.stringify(rawRawRequest) } }); + } + ); + await server.start(); + + const response = await supertest(innerServer.listener).get('/').expect(200); + expect(JSON.parse(replacePorts(response.body.req))).toEqual({ + aborted: false, + complete: true, + method: 'GET', + url: '/', + }); + }); + + it('has expected inspect representation', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + router.get( + { path: '/', validate: false, options: { authRequired: true } }, + (context, req, res) => { + const rawRawRequest = ensureRawRequest(req).raw.req; + return res.ok({ body: { req: inspect(rawRawRequest) } }); + } + ); + await server.start(); + + const response = await supertest(innerServer.listener).get('/').expect(200); + expect(replacePorts(response.body.req)).toMatchInlineSnapshot( + `"{ method: 'GET', url: '/', complete: true, aborted: false }"` + ); + }); + }); +}); diff --git a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx index 3e72aaad9d179..e3e4059ad3cf5 100644 --- a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx +++ b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx @@ -20,6 +20,9 @@ export interface BreakdownFieldSelectorProps { onBreakdownFieldChange?: (breakdownField: DataViewField | undefined) => void; } +const TRUNCATION_PROPS = { truncation: 'middle' as const }; +const SINGLE_SELECTION = { asPlainText: true }; + export const BreakdownFieldSelector = ({ dataView, breakdown, @@ -56,6 +59,9 @@ export const BreakdownFieldSelector = ({ const breakdownCss = css` width: 100%; max-width: ${euiTheme.base * 22}px; + &:focus-within { + max-width: ${euiTheme.base * 30}px; + } `; return ( @@ -75,10 +81,11 @@ export const BreakdownFieldSelector = ({ aria-label={i18n.translate('unifiedHistogram.breakdownFieldSelectorAriaLabel', { defaultMessage: 'Break down by', })} - singleSelection={{ asPlainText: true }} + singleSelection={SINGLE_SELECTION} options={fieldOptions} selectedOptions={selectedFields} onChange={onFieldChange} + truncationProps={TRUNCATION_PROPS} compressed fullWidth={true} onFocus={disableFieldPopover} diff --git a/src/plugins/unified_search/public/query_string_input/query_string_input.tsx b/src/plugins/unified_search/public/query_string_input/query_string_input.tsx index 386b5c4aa0bd0..597d5788df60b 100644 --- a/src/plugins/unified_search/public/query_string_input/query_string_input.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_string_input.tsx @@ -29,6 +29,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { compact, debounce, isEmpty, isEqual, isFunction, partition } from 'lodash'; import { CoreStart, DocLinksStart, Toast } from '@kbn/core/public'; import type { Query } from '@kbn/es-query'; +import { euiThemeVars } from '@kbn/ui-theme'; import { DataPublicPluginStart, getQueryLog } from '@kbn/data-plugin/public'; import { type DataView, DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { PersistedLog } from '@kbn/data-plugin/public'; @@ -801,7 +802,11 @@ export default class QueryStringInputUI extends PureComponent
` display: flex; align-items: center; } + div .euiDataGridRowCell__content { + width: 100%; + } div .euiDataGridRowCell--lastColumn .euiDataGridRowCell__content { flex-grow: 0; - width: 100%; } div .siemEventsTable__trSupplement--summary { display: block; @@ -269,6 +271,7 @@ export const AlertsTableComponent: FC = ({ showColumnSelector: !isEventRenderedView, showSortSelector: !isEventRenderedView, }, + dynamicRowHeight: isEventRenderedView, }), [ triggersActionsUi.alertsTableConfigurationRegistry, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.test.tsx index 56bb1ed8e844e..5b2a15b57d3b8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.test.tsx @@ -707,5 +707,19 @@ describe('AlertsTable', () => { expect(await screen.findByTestId('cases-components-tooltip')).toBeInTheDocument(); }); }); + + describe('dynamic row height mode', () => { + it('should render a non-virtualized grid body when the dynamicRowHeight option is on', async () => { + const { container } = render(); + + expect(container.querySelector('.euiDataGrid__customRenderBody')).toBeTruthy(); + }); + + it('should render a virtualized grid body when the dynamicRowHeight option is off', async () => { + const { container } = render(); + + expect(container.querySelector('.euiDataGrid__virtualized')).toBeTruthy(); + }); + }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx index 3ba0f17e6188e..2c98853b3c4e6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx @@ -6,7 +6,16 @@ */ import { ALERT_UUID } from '@kbn/rule-data-utils'; -import React, { useState, Suspense, lazy, useCallback, useMemo, useEffect, useRef } from 'react'; +import React, { + useState, + Suspense, + lazy, + useCallback, + useMemo, + useEffect, + useRef, + memo, +} from 'react'; import { EuiDataGrid, EuiDataGridCellValueElementProps, @@ -14,8 +23,11 @@ import { EuiSkeletonText, EuiDataGridRefProps, EuiFlexGroup, + EuiDataGridProps, } from '@elastic/eui'; import { useQueryClient } from '@tanstack/react-query'; +import styled from '@emotion/styled'; +import { RuleRegistrySearchRequestPagination } from '@kbn/rule-registry-plugin/common'; import { useSorting, usePagination, useBulkActions, useActionsColumn } from './hooks'; import { AlertsTableProps, FetchAlertData } from '../../../types'; import { ALERTS_TABLE_CONTROL_COLUMNS_ACTIONS_LABEL } from './translations'; @@ -59,6 +71,62 @@ const isSystemCell = (columnId: string): columnId is SystemCellId => { return systemCells.includes(columnId as SystemCellId); }; +const Row = styled.div` + display: flex; + min-width: fit-content; +`; + +type CustomGridBodyProps = Pick< + Parameters>['0'], + 'Cell' | 'visibleColumns' +> & { + alertsData: FetchAlertData['oldAlertsData']; + isLoading: boolean; + pagination: RuleRegistrySearchRequestPagination; + actualGridStyle: EuiDataGridStyle; + stripes?: boolean; +}; + +const CustomGridBody = memo( + ({ + alertsData, + isLoading, + pagination, + actualGridStyle, + visibleColumns, + Cell, + stripes, + }: CustomGridBodyProps) => { + return ( + <> + {alertsData + .concat(isLoading ? Array.from({ length: pagination.pageSize - alertsData.length }) : []) + .map((_row, rowIndex) => ( + + {visibleColumns.map((_col, colIndex) => ( + + ))} + + ))} + + ); + } +); + const AlertsTable: React.FunctionComponent = (props: AlertsTableProps) => { const dataGridRef = useRef(null); const [activeRowClasses, setActiveRowClasses] = useState< @@ -78,6 +146,7 @@ const AlertsTable: React.FunctionComponent = (props: AlertsTab refresh: alertsRefresh, getInspectQuery, } = alertsData; + const queryClient = useQueryClient(); const { data: cases, isLoading: isLoadingCases } = props.cases; const { data: maintenanceWindows, isLoading: isLoadingMaintenanceWindows } = @@ -284,14 +353,6 @@ const AlertsTable: React.FunctionComponent = (props: AlertsTab if (shouldHighlightRowCheck) { mappedRowClasses = alerts.reduce>( (rowClasses, alert, index) => { - if (props.gridStyle?.stripes && index % 2 !== 0) { - // manually add stripes if props.gridStyle.stripes is true because presence of rowClasses - // overrides the props.gridStyle.stripes option. And rowClasses will always be there. - // Adding strips only on even rows. It will be replace by alertsTableHighlightedRow if - // shouldHighlightRow is correct - rowClasses[index + pagination.pageIndex * pagination.pageSize] = - 'euiDataGridRow--striped'; - } if (shouldHighlightRowCheck(alert)) { rowClasses[index + pagination.pageIndex * pagination.pageSize] = 'alertsTableHighlightedRow'; @@ -303,13 +364,7 @@ const AlertsTable: React.FunctionComponent = (props: AlertsTab ); } return mappedRowClasses; - }, [ - props.shouldHighlightRow, - alerts, - pagination.pageIndex, - pagination.pageSize, - props.gridStyle, - ]); + }, [props.shouldHighlightRow, alerts, pagination.pageIndex, pagination.pageSize]); const handleFlyoutClose = useCallback(() => setFlyoutAlertIndex(-1), [setFlyoutAlertIndex]); @@ -335,7 +390,6 @@ const AlertsTable: React.FunctionComponent = (props: AlertsTab Object.entries(alert ?? {}).forEach(([key, value]) => { data.push({ field: key, value: value as string[] }); }); - if (isSystemCell(_props.columnId)) { return ( = (props: AlertsTab }, [ alerts, - ecsAlertsData, cases, - maintenanceWindows, + ecsAlertsData, isLoading, isLoadingCases, isLoadingMaintenanceWindows, + maintenanceWindows, pagination.pageIndex, pagination.pageSize, renderCellValue, @@ -374,6 +428,16 @@ const AlertsTable: React.FunctionComponent = (props: AlertsTab ] ); + const dataGridPagination = useMemo( + () => ({ + ...pagination, + pageSizeOptions: props.pageSizeOptions, + onChangeItemsPerPage: onChangePageSize, + onChangePage: onChangePageIndex, + }), + [onChangePageIndex, onChangePageSize, pagination, props.pageSizeOptions] + ); + const { getCellActions, visibleCellActions, disabledCellActions } = props.alertsTableConfiguration ?.useCellActions ? props.alertsTableConfiguration?.useCellActions({ @@ -441,6 +505,23 @@ const AlertsTable: React.FunctionComponent = (props: AlertsTab return mergedGridStyle; }, [activeRowClasses, highlightedRowClasses, props.gridStyle]); + const renderCustomGridBody = useCallback< + Exclude + >( + ({ visibleColumns: _visibleColumns, Cell }) => ( + + ), + [actualGridStyle, oldAlertsData, pagination, isLoading, props.gridStyle?.stripes] + ); + return (
@@ -471,15 +552,11 @@ const AlertsTable: React.FunctionComponent = (props: AlertsTab gridStyle={actualGridStyle} sorting={{ columns: sortingColumns, onSort }} toolbarVisibility={toolbarVisibility} - pagination={{ - ...pagination, - pageSizeOptions: props.pageSizeOptions, - onChangeItemsPerPage: onChangePageSize, - onChangePage: onChangePageIndex, - }} + pagination={dataGridPagination} rowHeightsOptions={props.rowHeightsOptions} onColumnResize={onColumnResize} ref={dataGridRef} + renderCustomGridBody={props.dynamicRowHeight ? renderCustomGridBody : undefined} /> )}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.tsx index b74c8efe45f8c..bc47845fb9d9e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.tsx @@ -74,6 +74,10 @@ export type AlertsTableStateProps = { * Allows to consumers of the table to decide to highlight a row based on the current alert. */ shouldHighlightRow?: (alert: Alert) => boolean; + /** + * Enable when rows may have variable heights (disables virtualization) + */ + dynamicRowHeight?: boolean; } & Partial; export interface AlertsTableStorage { @@ -158,6 +162,7 @@ const AlertsTableStateWithQueryProvider = ({ showAlertStatusWithFlapping, toolbarVisibility, shouldHighlightRow, + dynamicRowHeight, }: AlertsTableStateProps) => { const { cases: casesService } = useKibana<{ cases?: CasesService }>().services; @@ -404,6 +409,7 @@ const AlertsTableStateWithQueryProvider = ({ showInspectButton, toolbarVisibility, shouldHighlightRow, + dynamicRowHeight, featureIds, }), [ @@ -431,6 +437,7 @@ const AlertsTableStateWithQueryProvider = ({ showInspectButton, toolbarVisibility, shouldHighlightRow, + dynamicRowHeight, featureIds, ] ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_actions_column.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_actions_column.ts index 679fe79b0b036..33184e738a185 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_actions_column.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_actions_column.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { useContext } from 'react'; +import { useCallback, useContext } from 'react'; import { UseActionsColumnRegistry, BulkActionsVerbs } from '../../../../types'; import { BulkActionsContext } from '../bulk_actions/context'; @@ -30,15 +30,17 @@ export const useActionsColumn = ({ options }: UseActionsColumnProps) => { // we save the rowIndex when creating the function to be used by the clients // so they don't have to manage it - const getSetIsActionLoadingCallback = + const getSetIsActionLoadingCallback = useCallback( (rowIndex: number) => - (isLoading: boolean = true) => { - updateBulkActionsState({ - action: BulkActionsVerbs.updateRowLoadingState, - rowIndex, - isLoading, - }); - }; + (isLoading: boolean = true) => { + updateBulkActionsState({ + action: BulkActionsVerbs.updateRowLoadingState, + rowIndex, + isLoading, + }); + }, + [updateBulkActionsState] + ); return { renderCustomActionsRow, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.ts index 90a1962d0d1f6..dfe5bbde8d98e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.ts @@ -242,9 +242,9 @@ export function useBulkActions({ const [bulkActionsState, updateBulkActionsState] = useContext(BulkActionsContext); const configBulkActionPanels = useBulkActionsConfig(query); - const clearSelection = () => { + const clearSelection = useCallback(() => { updateBulkActionsState({ action: BulkActionsVerbs.clear }); - }; + }, [updateBulkActionsState]); const setIsBulkActionsLoading = (isLoading: boolean = true) => { updateBulkActionsState({ action: BulkActionsVerbs.updateAllLoadingState, isLoading }); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 9c9f08bc77b98..fbcfbc0d38297 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -566,6 +566,10 @@ export type AlertsTableProps = { * Allows to consumers of the table to decide to highlight a row based on the current alert. */ shouldHighlightRow?: (alert: Alert) => boolean; + /** + * Enable when rows may have variable heights (disables virtualization) + */ + dynamicRowHeight?: boolean; featureIds?: ValidFeatureId[]; } & Partial>;