137988 browser fields UI (#140516)

* first commit

* first commit

* get auth index and try field caps

* use esClient

* [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix'

* wait for promise to finish

* logs for debugging

* format field capabilities

* add simplier browserFields mapper

* update response and remove width

* update response

* refactor

* types and refactor

* update api response

* fix column ids

* add columns toggle and reset

* sort visible columns id on toggle on

* merging info

* call api

* load info on browser field loaded

* remove logs

* add useColumns hook

* remove browser fields dependency

* update fn name

* update types

* update imported type package

* update mock object

* error message for no o11y alert indices

* add endpoint integration test

* activate commented tests

* add unit test

* comment uncommented tests

* fix tests

* review by Xavier

* [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix'

* remove unnecessary api calls

* update types

* update param names + right type

* update types

* update id and index to be same type as rest

* update timelines id and index format

* add schema update on load

* add functional test

* fix tests

* reactivate skipped test

* update row action types to work with new api

* rollback basic fields as array update o11y render cell fn

* update cell render fn to handle strings too

* update column recovery on update

* recover previous deleted column stats

* add browser fields error handling

* add toast on error and avoid calling field api when in siem

* remove spread operator

* add toast mock

* update render cell cb when value is an object

* remove not needed prop

* fix browser field types

* fix reset fields action

* add missing hook dependency

* [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix'

* fix browser field modal types

* update browser field types

* update render cell

* update export type

* fix default columns

* remove description column in browser field modal

* fix populate default fields on reset

* delete description field in triggers_actions_ui

* avoid to refetch the data because all the data is already there

* remove description tests

* insert new column in first pos + minor fixes

* update onToggleColumn callback to avoid innecesary calls

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Xavier Mouligneau <xavier.mouligneau@elastic.co>
This commit is contained in:
Julian Gernun 2022-09-20 16:31:25 +02:00 committed by GitHub
parent 0af26e2ab9
commit 0f7cfd16f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 571 additions and 136 deletions

View file

@ -142,7 +142,12 @@ export class TriggersActionsUiExamplePlugin
useInternalFlyout,
getRenderCellValue: () => (props: any) => {
const value = props.data.find((d: any) => d.field === props.columnId)?.value ?? [];
return <>{value.length ? value.join() : '--'}</>;
if (Array.isArray(value)) {
return <>{value.length ? value.join() : '--'}</>;
}
return <>{value}</>;
},
sort,
};

View file

@ -427,6 +427,11 @@ describe('CaseViewPage', () => {
}),
},
},
notifications: {
toasts: {
addDanger: () => {},
},
},
},
}),
}));

View file

@ -16,6 +16,7 @@ import {
TIMESTAMP,
} from '@kbn/rule-data-utils';
import type { CellValueElementProps, TimelineNonEcsData } from '@kbn/timelines-plugin/common';
import { isEmpty } from 'lodash';
import { AlertStatusIndicator } from '../../../../components/shared/alert_status_indicator';
import { TimestampTooltip } from '../../../../components/shared/timestamp_tooltip';
import { asDuration } from '../../../../../common/utils/formatters';
@ -38,6 +39,20 @@ export const getMappedNonEcsValue = ({
return undefined;
};
const getRenderValue = (mappedNonEcsValue: any) => {
// can be updated when working on https://github.com/elastic/kibana/issues/140819
const value = Array.isArray(mappedNonEcsValue) ? mappedNonEcsValue.join() : mappedNonEcsValue;
if (!isEmpty(value)) {
if (typeof value === 'object') {
return JSON.stringify(value);
}
return value;
}
return '—';
};
/**
* This implementation of `EuiDataGrid`'s `renderCellValue`
* accepts `EuiDataGridCellValueElementProps`, plus `data`
@ -53,10 +68,12 @@ export const getRenderCellValue = ({
}) => {
return ({ columnId, data }: CellValueElementProps) => {
if (!data) return null;
const value = getMappedNonEcsValue({
const mappedNonEcsValue = getMappedNonEcsValue({
data,
fieldName: columnId,
})?.reduce((x) => x[0]);
});
const value = getRenderValue(mappedNonEcsValue);
switch (columnId) {
case ALERT_STATUS:

View file

@ -11,3 +11,4 @@ export type {
RuleRegistrySearchRequestPagination,
} from './search_strategy';
export { BASE_RAC_ALERTS_API_PATH } from './constants';
export type { BrowserFields, BrowserField } from './types';

View file

@ -9,6 +9,9 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import * as t from 'io-ts';
import type { IFieldSubType } from '@kbn/es-query';
import type { RuntimeField } from '@kbn/data-views-plugin/common';
// note: these schemas are not exhaustive. See the `Sort` type of `@elastic/elasticsearch` if you need to enhance it.
const fieldSchema = t.string;
export const sortOrderSchema = t.union([t.literal('asc'), t.literal('desc'), t.literal('_doc')]);
@ -302,3 +305,21 @@ export interface ClusterPutComponentTemplateBody {
mappings: estypes.MappingTypeMapping;
};
}
export interface BrowserField {
aggregatable: boolean;
category: string;
description?: string | null;
example?: string | number | null;
fields: Readonly<Record<string, Partial<BrowserField>>>;
format?: string;
indexes: string[];
name: string;
searchable: boolean;
type: string;
subType?: IFieldSubType;
readFromDocValues: boolean;
runtimeField?: RuntimeField;
}
export type BrowserFields = Record<string, Partial<BrowserField>>;

View file

@ -31,6 +31,7 @@ import {
import { Logger, ElasticsearchClient, EcsEventOutcome } from '@kbn/core/server';
import { AuditLogger } from '@kbn/security-plugin/server';
import { IndexPatternsFetcher } from '@kbn/data-plugin/server';
import { BrowserFields } from '../../common';
import { alertAuditEvent, operationAlertAuditActionMap } from './audit_events';
import {
ALERT_WORKFLOW_STATUS,
@ -42,7 +43,6 @@ import { ParsedTechnicalFields } from '../../common/parse_technical_fields';
import { Dataset, IRuleDataService } from '../rule_data_plugin_service';
import { getAuthzFilter, getSpacesFilter } from '../lib';
import { fieldDescriptorToBrowserFieldMapper } from './browser_fields';
import { BrowserFields } from '../types';
// TODO: Fix typings https://github.com/elastic/kibana/issues/101776
type NonNullableProps<Obj extends {}, Props extends keyof Obj> = Omit<Obj, Props> & {

View file

@ -6,7 +6,7 @@
*/
import { FieldDescriptor } from '@kbn/data-views-plugin/server';
import { BrowserField, BrowserFields } from '../../types';
import { BrowserFields, BrowserField } from '../../../common';
const getFieldCategory = (fieldCapability: FieldDescriptor) => {
const name = fieldCapability.name.split('.');
@ -21,7 +21,7 @@ const getFieldCategory = (fieldCapability: FieldDescriptor) => {
const browserFieldFactory = (
fieldCapability: FieldDescriptor,
category: string
): { [fieldName in string]: BrowserField } => {
): Readonly<Record<string, Partial<BrowserField>>> => {
return {
[fieldCapability.name]: {
...fieldCapability,

View file

@ -9,6 +9,7 @@ import { IRouter } from '@kbn/core/server';
import { transformError } from '@kbn/securitysolution-es-utils';
import * as t from 'io-ts';
import { BrowserFields } from '../../common';
import { RacRequestHandlerContext } from '../types';
import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants';
import { buildRouteValidation } from './utils/route_validation';
@ -50,7 +51,7 @@ export const getBrowserFieldsByFeatureId = (router: IRouter<RacRequestHandlerCon
});
}
const browserFields = await alertsClient.getBrowserFields({
const browserFields: BrowserFields = await alertsClient.getBrowserFields({
indices: o11yIndices,
metaFields: ['_id', '_index'],
allowNoIndex: true,

View file

@ -13,7 +13,6 @@ import {
RuleTypeState,
} from '@kbn/alerting-plugin/common';
import { RuleExecutorOptions, RuleExecutorServices, RuleType } from '@kbn/alerting-plugin/server';
import { FieldSpec } from '@kbn/data-plugin/common';
import { AlertsClient } from './alert_data_client/alerts_client';
type SimpleAlertType<
@ -72,11 +71,3 @@ export interface RacApiRequestHandlerContext {
export type RacRequestHandlerContext = CustomRequestHandlerContext<{
rac: RacApiRequestHandlerContext;
}>;
export type BrowserField = FieldSpec & {
category: string;
};
export type BrowserFields = {
[category in string]: { fields: { [fieldName in string]: BrowserField } };
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { BrowserField } from '@kbn/triggers-actions-ui-plugin/public/application/sections/field_browser/types';
import { BrowserField } from '@kbn/rule-registry-plugin/common';
import { VFC } from 'react';
import { useKibana } from '../../../../hooks/use_kibana';

View file

@ -8,7 +8,7 @@
import React from 'react';
import { useMemo } from 'react';
import { EuiDataGridColumn, EuiText } from '@elastic/eui';
import { BrowserField } from '@kbn/triggers-actions-ui-plugin/public/application/sections/field_browser/types';
import { BrowserField } from '@kbn/rule-registry-plugin/common';
import { IndicatorsFieldBrowser } from '../../indicators_field_browser';
export const useToolbarOptions = ({

View file

@ -16,8 +16,8 @@ import {
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { TimelinesUIStart } from '@kbn/timelines-plugin/public';
import type { TriggersAndActionsUIPublicPluginStart as TriggersActionsStart } from '@kbn/triggers-actions-ui-plugin/public';
import { BrowserField } from '@kbn/triggers-actions-ui-plugin/public/application/sections/field_browser/types';
import { DataViewBase } from '@kbn/es-query';
import { BrowserField } from '@kbn/rule-registry-plugin/common';
import { Store } from 'redux';
import { DataProvider } from '@kbn/timelines-plugin/common';

View file

@ -92,6 +92,11 @@ describe('AlertsTable', () => {
visibleColumns: columns.map((c) => c.id),
'data-test-subj': 'testTable',
updatedAt: Date.now(),
onToggleColumn: () => {},
onResetColumns: () => {},
onColumnsChange: () => {},
onChangeVisibleColumns: () => {},
browserFields: {},
};
const AlertsTableWithLocale: React.FunctionComponent<AlertsTableProps> = (props) => (

View file

@ -44,7 +44,6 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = (props: AlertsTab
alerts,
alertsCount,
isLoading,
onColumnsChange,
onPageChange,
onSortChange,
sort: sortingFields,
@ -66,18 +65,6 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = (props: AlertsTab
useBulkActionsConfig: props.alertsTableConfiguration.useBulkActions,
});
const toolbarVisibility = useCallback(() => {
const { rowSelection } = bulkActionsState;
return getToolbarVisibility({
bulkActions,
alertsCount,
rowSelection,
alerts: alertsData.alerts,
updatedAt: props.updatedAt,
isLoading,
});
}, [bulkActionsState, bulkActions, alertsCount, alertsData.alerts, props.updatedAt, isLoading])();
const {
pagination,
onChangePageSize,
@ -91,7 +78,14 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = (props: AlertsTab
pageSize: props.pageSize,
});
const [visibleColumns, setVisibleColumns] = useState(props.visibleColumns);
const {
visibleColumns,
onToggleColumn,
onResetColumns,
updatedAt,
browserFields,
onChangeVisibleColumns,
} = props;
// TODO when every solution is using this table, we will be able to simplify it by just passing the alert index
const handleFlyoutAlert = useCallback(
@ -104,16 +98,32 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = (props: AlertsTab
[alerts, setFlyoutAlertIndex]
);
const onChangeVisibleColumns = useCallback(
(newColumns: string[]) => {
setVisibleColumns(newColumns);
onColumnsChange(
props.columns.sort((a, b) => newColumns.indexOf(a.id) - newColumns.indexOf(b.id)),
newColumns
);
},
[onColumnsChange, props.columns]
);
const toolbarVisibility = useCallback(() => {
const { rowSelection } = bulkActionsState;
return getToolbarVisibility({
bulkActions,
alertsCount,
rowSelection,
alerts: alertsData.alerts,
updatedAt,
isLoading,
columnIds: visibleColumns,
onToggleColumn,
onResetColumns,
browserFields,
});
}, [
bulkActionsState,
bulkActions,
alertsCount,
alertsData.alerts,
updatedAt,
browserFields,
isLoading,
visibleColumns,
onToggleColumn,
onResetColumns,
])();
const leadingControlColumns = useMemo(() => {
const isActionButtonsColumnActive =
@ -203,7 +213,10 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = (props: AlertsTab
columnId: string;
}) => {
const value = data.find((d) => d.field === columnId)?.value ?? [];
return <>{value.length ? value.join() : '--'}</>;
if (Array.isArray(value)) {
return <>{value.length ? value.join() : '--'}</>;
}
return <>{value}</>;
};
const renderCellValue = useCallback(

View file

@ -21,10 +21,12 @@ import { PLUGIN_ID } from '../../../common/constants';
import { TypeRegistry } from '../../type_registry';
import AlertsTableState, { AlertsTableStateProps } from './alerts_table_state';
import { useFetchAlerts } from './hooks/use_fetch_alerts';
import { useFetchBrowserFieldCapabilities } from './hooks/use_fetch_browser_fields_capabilities';
import { DefaultSort } from './hooks';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
jest.mock('./hooks/use_fetch_alerts');
jest.mock('./hooks/use_fetch_browser_fields_capabilities');
jest.mock('@kbn/kibana-utils-plugin/public');
jest.mock('@kbn/kibana-react-plugin/public', () => ({
useKibana: () => ({
@ -55,6 +57,11 @@ jest.mock('@kbn/kibana-react-plugin/public', () => ({
}),
},
},
notifications: {
toasts: {
addDanger: () => {},
},
},
},
}),
}));
@ -137,6 +144,9 @@ hookUseFetchAlerts.mockImplementation(() => [
},
]);
const hookUseFetchBrowserFieldCapabilities = useFetchBrowserFieldCapabilities as jest.Mock;
hookUseFetchBrowserFieldCapabilities.mockImplementation(() => [false, {}]);
const AlertsTableWithLocale: React.FunctionComponent<AlertsTableStateProps> = (props) => (
<IntlProvider locale="en">
<AlertsTableState {...props} />

View file

@ -35,6 +35,7 @@ import { ALERTS_TABLE_CONF_ERROR_MESSAGE, ALERTS_TABLE_CONF_ERROR_TITLE } from '
import { TypeRegistry } from '../../type_registry';
import { bulkActionsReducer } from './bulk_actions/reducer';
import { useGetUserCasesPermissions } from './hooks/use_get_user_cases_permissions';
import { useColumns } from './hooks/use_columns';
const DefaultPagination = {
pageSize: 10,
@ -59,7 +60,7 @@ export interface AlertsTableStateProps {
showExpandToDetails: boolean;
}
interface AlertsTableStorage {
export interface AlertsTableStorage {
columns: EuiDataGridColumn[];
visibleColumns?: string[];
sort: SortCombinations[];
@ -93,6 +94,7 @@ const AlertsTableWithBulkActionsContextComponent: React.FunctionComponent<{
);
const AlertsTableWithBulkActionsContext = React.memo(AlertsTableWithBulkActionsContextComponent);
const EMPTY_FIELDS = [{ field: '*', include_unmapped: true }];
const AlertsTableState = ({
alertsTableConfigurationRegistry,
@ -106,6 +108,7 @@ const AlertsTableState = ({
showExpandToDetails,
}: AlertsTableStateProps) => {
const { cases } = useKibana<{ cases: CaseUi }>().services;
const hasAlertsTableConfiguration =
alertsTableConfigurationRegistry?.has(configurationId) ?? false;
const alertsTableConfiguration = hasAlertsTableConfiguration
@ -143,7 +146,23 @@ const AlertsTableState = ({
...DefaultPagination,
pageSize: pageSize ?? DefaultPagination.pageSize,
});
const [columns, setColumns] = useState<EuiDataGridColumn[]>(storageAlertsTable.current.columns);
const {
columns,
onColumnsChange,
browserFields,
isBrowserFieldDataLoading,
onToggleColumn,
onResetColumns,
visibleColumns,
onChangeVisibleColumns,
} = useColumns({
featureIds,
storageAlertsTable,
storage,
id,
defaultColumns: (alertsTableConfiguration && alertsTableConfiguration.columns) ?? [],
});
const [
isLoading,
@ -156,7 +175,7 @@ const AlertsTableState = ({
updatedAt,
},
] = useFetchAlerts({
fields: columns.map((col) => ({ field: col.id, include_unmapped: true })),
fields: EMPTY_FIELDS,
featureIds,
query,
pagination,
@ -194,18 +213,6 @@ const AlertsTableState = ({
},
[id]
);
const onColumnsChange = useCallback(
(newColumns: EuiDataGridColumn[], visibleColumns: string[]) => {
setColumns(newColumns);
storageAlertsTable.current = {
...storageAlertsTable.current,
columns: newColumns,
visibleColumns,
};
storage.current.set(id, storageAlertsTable.current);
},
[id, storage]
);
const useFetchAlertsData = useCallback(() => {
return {
@ -215,7 +222,6 @@ const AlertsTableState = ({
isInitializing,
isLoading,
getInspectQuery,
onColumnsChange,
onPageChange,
onSortChange,
refresh,
@ -228,7 +234,6 @@ const AlertsTableState = ({
getInspectQuery,
isInitializing,
isLoading,
onColumnsChange,
onPageChange,
onSortChange,
pagination.pageIndex,
@ -252,9 +257,14 @@ const AlertsTableState = ({
showExpandToDetails,
trailingControlColumns: [],
useFetchAlertsData,
visibleColumns: storageAlertsTable.current.visibleColumns ?? [],
visibleColumns,
'data-test-subj': 'internalAlertsState',
updatedAt,
browserFields,
onToggleColumn,
onResetColumns,
onColumnsChange,
onChangeVisibleColumns,
}),
[
alertsTableConfiguration,
@ -264,7 +274,13 @@ const AlertsTableState = ({
id,
showExpandToDetails,
useFetchAlertsData,
visibleColumns,
updatedAt,
browserFields,
onToggleColumn,
onResetColumns,
onColumnsChange,
onChangeVisibleColumns,
]
);
@ -281,7 +297,7 @@ const AlertsTableState = ({
return hasAlertsTableConfiguration ? (
<>
{!isLoading && alertsCount === 0 && <EmptyState />}
{isLoading && (
{(isLoading || isBrowserFieldDataLoading) && (
<EuiProgress size="xs" color="accent" data-test-subj="internalAlertsPageLoading" />
)}
{alertsCount !== 0 && CasesContext && cases && (

View file

@ -96,6 +96,11 @@ describe('AlertsTable.BulkActions', () => {
visibleColumns: columns.map((c) => c.id),
'data-test-subj': 'testTable',
updatedAt: Date.now(),
onToggleColumn: () => {},
onResetColumns: () => {},
onColumnsChange: () => {},
onChangeVisibleColumns: () => {},
browserFields: {},
};
const tablePropsWithBulkActions = {

View file

@ -13,3 +13,10 @@ export const ERROR_FETCH_ALERTS = i18n.translate(
defaultMessage: `An error has occurred on alerts search`,
}
);
export const ERROR_FETCH_BROWSER_FIELDS = i18n.translate(
'xpack.triggersActionsUI.components.alertTable.useFetchBrowserFieldsCapabilities.errorMessageText',
{
defaultMessage: 'An error has occurred loading browser fields',
}
);

View file

@ -0,0 +1,235 @@
/*
* 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 { EuiDataGridColumn } from '@elastic/eui';
import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
import { AlertConsumers } from '@kbn/rule-data-utils';
import { BrowserField, BrowserFields } from '@kbn/rule-registry-plugin/common';
import { useCallback, useEffect, useState } from 'react';
import { AlertsTableStorage } from '../alerts_table_state';
import { useFetchBrowserFieldCapabilities } from './use_fetch_browser_fields_capabilities';
interface UseColumnsArgs {
featureIds: AlertConsumers[];
storageAlertsTable: React.MutableRefObject<AlertsTableStorage>;
storage: React.MutableRefObject<IStorageWrapper>;
id: string;
defaultColumns: EuiDataGridColumn[];
}
const fieldTypeToDataGridColumnTypeMapper = (fieldType: string | undefined) => {
if (fieldType === 'date') return 'datetime';
if (fieldType === 'number') return 'numeric';
if (fieldType === 'object') return 'json';
return fieldType;
};
/**
* EUI Data Grid expects the columns to have a property 'schema' defined for proper sorting
* this schema as its own types as can be check out in the docs so we add it here manually
* https://eui.elastic.co/#/tabular-content/data-grid-schema-columns
*/
const euiColumnFactory = (
column: EuiDataGridColumn,
browserFields: BrowserFields
): EuiDataGridColumn => {
const browserFieldsProps = getBrowserFieldProps(column.id, browserFields);
return {
...column,
schema: fieldTypeToDataGridColumnTypeMapper(browserFieldsProps.type),
};
};
/**
* Searches in browser fields object for a specific field
*/
const getBrowserFieldProps = (
columnId: string,
browserFields: BrowserFields
): Partial<BrowserField> => {
for (const [, categoryDescriptor] of Object.entries(browserFields)) {
if (!categoryDescriptor.fields) {
continue;
}
for (const [fieldName, fieldDescriptor] of Object.entries(categoryDescriptor.fields)) {
if (fieldName === columnId) {
return fieldDescriptor;
}
}
}
return { type: 'string' };
};
/**
* @param columns Columns to be considered in the alerts table
* @param browserFields constant object with all field capabilities
* @returns columns but with the info needed by the data grid to work as expected, e.g sorting
*/
const populateColumns = (
columns: EuiDataGridColumn[],
browserFields: BrowserFields
): EuiDataGridColumn[] => {
return columns.map((column: EuiDataGridColumn) => {
return euiColumnFactory(column, browserFields);
});
};
const getColumnByColumnId = (columns: EuiDataGridColumn[], columnId: string) => {
return columns.find(({ id }: { id: string }) => id === columnId);
};
const persist = ({
id,
storageAlertsTable,
columns,
visibleColumns,
storage,
}: {
id: string;
storageAlertsTable: React.MutableRefObject<AlertsTableStorage>;
storage: React.MutableRefObject<IStorageWrapper>;
columns: EuiDataGridColumn[];
visibleColumns: string[];
}) => {
storageAlertsTable.current = {
...storageAlertsTable.current,
columns,
visibleColumns,
};
storage.current.set(id, storageAlertsTable.current);
};
export const useColumns = ({
featureIds,
storageAlertsTable,
storage,
id,
defaultColumns,
}: UseColumnsArgs) => {
const [isBrowserFieldDataLoading, browserFields] = useFetchBrowserFieldCapabilities({
featureIds,
});
const [columns, setColumns] = useState<EuiDataGridColumn[]>(storageAlertsTable.current.columns);
const [isColumnsPopulated, setColumnsPopulated] = useState<boolean>(false);
const [visibleColumns, setVisibleColumns] = useState(
storageAlertsTable.current.visibleColumns ?? []
);
useEffect(() => {
if (isBrowserFieldDataLoading !== false || isColumnsPopulated) return;
const populatedColumns = populateColumns(columns, browserFields);
setColumnsPopulated(true);
setColumns(populatedColumns);
}, [browserFields, columns, isBrowserFieldDataLoading, isColumnsPopulated]);
const onColumnsChange = useCallback(
(newColumns: EuiDataGridColumn[], newVisibleColumns: string[]) => {
setColumns(newColumns);
persist({
id,
storage,
storageAlertsTable,
columns: newColumns,
visibleColumns: newVisibleColumns,
});
},
[id, storage, storageAlertsTable]
);
const onChangeVisibleColumns = useCallback(
(newColumns: string[]) => {
setVisibleColumns(newColumns);
onColumnsChange(
columns.sort((a, b) => newColumns.indexOf(a.id) - newColumns.indexOf(b.id)),
newColumns
);
},
[onColumnsChange, columns]
);
const onToggleColumn = useCallback(
(columnId: string): void => {
const visibleIndex = visibleColumns.indexOf(columnId);
const defaultIndex = defaultColumns.findIndex(
(column: EuiDataGridColumn) => column.id === columnId
);
const isVisible = visibleIndex >= 0;
const isInDefaultConfig = defaultIndex >= 0;
let newColumnIds: string[] = [];
// if the column is shown, remove it
if (isVisible) {
newColumnIds = [
...visibleColumns.slice(0, visibleIndex),
...visibleColumns.slice(visibleIndex + 1),
];
}
// if the column isn't shown but it's part of the default config
// insert into the same position as in the default config
if (!isVisible && isInDefaultConfig) {
newColumnIds = [
...visibleColumns.slice(0, defaultIndex),
columnId,
...visibleColumns.slice(defaultIndex),
];
}
// if the column isn't shown and it's not part of the default config
// push it into the second position. Behaviour copied by t_grid, security
// does this to insert right after the timestamp column
if (!isVisible && !isInDefaultConfig) {
newColumnIds = [visibleColumns[0], columnId, ...visibleColumns.slice(1)];
}
const newColumns = newColumnIds.map((_columnId) => {
const column = getColumnByColumnId(defaultColumns, _columnId);
return euiColumnFactory(column ? column : { id: _columnId }, browserFields);
});
setVisibleColumns(newColumnIds);
setColumns(newColumns);
persist({
id,
storage,
storageAlertsTable,
columns: newColumns,
visibleColumns: newColumnIds,
});
},
[browserFields, defaultColumns, id, storage, storageAlertsTable, visibleColumns]
);
const onResetColumns = useCallback(() => {
const newVisibleColumns = defaultColumns.map((column) => column.id);
const populatedDefaultColumns = populateColumns(defaultColumns, browserFields);
setVisibleColumns(newVisibleColumns);
setColumns(populatedDefaultColumns);
persist({
id,
storage,
storageAlertsTable,
columns: populatedDefaultColumns,
visibleColumns: newVisibleColumns,
});
}, [browserFields, defaultColumns, id, storage, storageAlertsTable]);
return {
columns,
isBrowserFieldDataLoading,
browserFields,
visibleColumns,
onColumnsChange,
onToggleColumn,
onResetColumns,
onChangeVisibleColumns,
};
};

View file

@ -0,0 +1,73 @@
/*
* 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 type { ValidFeatureId } from '@kbn/rule-data-utils';
import type { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common/search_strategy';
import { BASE_RAC_ALERTS_API_PATH, BrowserFields } from '@kbn/rule-registry-plugin/common';
import { useCallback, useEffect, useState } from 'react';
import { useKibana } from '../../../../common/lib/kibana';
import { ERROR_FETCH_BROWSER_FIELDS } from './translations';
export interface FetchAlertsArgs {
featureIds: ValidFeatureId[];
}
export interface FetchAlertResp {
alerts: EcsFieldsResponse[];
}
export type UseFetchAlerts = ({ featureIds }: FetchAlertsArgs) => [boolean, FetchAlertResp];
const INVALID_FEATURE_ID = 'siem';
export const useFetchBrowserFieldCapabilities = ({
featureIds,
}: FetchAlertsArgs): [boolean | undefined, BrowserFields] => {
const {
http,
notifications: { toasts },
} = useKibana().services;
const [isLoading, setIsLoading] = useState<boolean | undefined>(undefined);
const [browserFields, setBrowserFields] = useState<BrowserFields>(() => ({}));
const getBrowserFieldInfo = useCallback(async () => {
if (!http) return Promise.resolve({});
try {
return await http.get<BrowserFields>(`${BASE_RAC_ALERTS_API_PATH}/browser_fields`, {
query: { featureIds },
});
} catch (e) {
toasts.addDanger(ERROR_FETCH_BROWSER_FIELDS);
return {};
}
}, [featureIds, http, toasts]);
useEffect(() => {
if (featureIds.includes(INVALID_FEATURE_ID)) {
setIsLoading(false);
}
}, [featureIds]);
useEffect(() => {
if (isLoading !== undefined) return;
setIsLoading(true);
const callApi = async () => {
const browserFieldsInfo = await getBrowserFieldInfo();
setBrowserFields(browserFieldsInfo);
setIsLoading(false);
};
callApi();
}, [getBrowserFieldInfo, isLoading]);
return [isLoading, browserFields];
};

View file

@ -5,5 +5,4 @@
* 2.0.
*/
export { getToolbarVisibility } from './toolbar_visibility';
export { AlertsCount } from './components/alerts_count/alerts_count';
export * from './toolbar_visibility';

View file

@ -8,22 +8,47 @@
import { EuiDataGridToolBarVisibilityOptions } from '@elastic/eui';
import { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common/search_strategy';
import React, { lazy, Suspense } from 'react';
import { BrowserFields } from '@kbn/rule-registry-plugin/common';
import { AlertsCount } from './components/alerts_count/alerts_count';
import { BulkActionsConfig } from '../../../../types';
import { LastUpdatedAt } from './components/last_updated_at';
import { FieldBrowser } from '../../field_browser';
const BulkActionsToolbar = lazy(() => import('../bulk_actions/components/toolbar'));
const getDefaultVisibility = ({
alertsCount,
updatedAt,
columnIds,
onToggleColumn,
onResetColumns,
browserFields,
}: {
alertsCount: number;
updatedAt: number;
}) => {
columnIds: string[];
onToggleColumn: (columnId: string) => void;
onResetColumns: () => void;
browserFields: BrowserFields;
}): EuiDataGridToolBarVisibilityOptions => {
const hasBrowserFields = Object.keys(browserFields).length > 0;
const additionalControls = {
right: <LastUpdatedAt updatedAt={updatedAt} />,
left: { append: <AlertsCount count={alertsCount} /> },
left: {
append: (
<>
<AlertsCount count={alertsCount} />
{hasBrowserFields ? (
<FieldBrowser
columnIds={columnIds}
browserFields={browserFields}
onResetColumns={onResetColumns}
onToggleColumn={onToggleColumn}
/>
) : undefined}
</>
),
},
};
return {
@ -40,6 +65,10 @@ export const getToolbarVisibility = ({
alerts,
isLoading,
updatedAt,
columnIds,
onToggleColumn,
onResetColumns,
browserFields,
}: {
bulkActions: BulkActionsConfig[];
alertsCount: number;
@ -47,18 +76,30 @@ export const getToolbarVisibility = ({
alerts: EcsFieldsResponse[];
isLoading: boolean;
updatedAt: number;
columnIds: string[];
onToggleColumn: (columnId: string) => void;
onResetColumns: () => void;
browserFields: any;
}): EuiDataGridToolBarVisibilityOptions => {
const selectedRowsCount = rowSelection.size;
const defaultVisibility = getDefaultVisibility({ alertsCount, updatedAt });
const defaultVisibility = getDefaultVisibility({
alertsCount,
updatedAt,
columnIds,
onToggleColumn,
onResetColumns,
browserFields,
});
const isBulkActionsActive =
selectedRowsCount === 0 || selectedRowsCount === undefined || bulkActions.length === 0;
if (selectedRowsCount === 0 || selectedRowsCount === undefined || bulkActions.length === 0)
return defaultVisibility;
if (isBulkActionsActive) return defaultVisibility;
const options = {
showColumnSelector: false,
showSortSelector: false,
additionalControls: {
...defaultVisibility.additionalControls,
right: <LastUpdatedAt updatedAt={updatedAt} />,
left: {
append: (
<>

View file

@ -17,7 +17,7 @@ import {
EuiSelectable,
FilterChecked,
} from '@elastic/eui';
import type { BrowserFields } from '../../types';
import { BrowserFields } from '@kbn/rule-registry-plugin/common';
import * as i18n from '../../translations';
import { getFieldCount, isEscape } from '../../helpers';
import { styles } from './categories_selector.styles';

View file

@ -127,12 +127,6 @@ describe('field_items', () => {
sortable: true,
width: '225px',
},
{
field: 'description',
name: 'Description',
sortable: true,
width: '400px',
},
{
field: 'category',
name: 'Category',
@ -188,12 +182,10 @@ describe('field_items', () => {
);
expect(getAllByText('Name').at(0)).toBeInTheDocument();
expect(getAllByText('Description').at(0)).toBeInTheDocument();
expect(getAllByText('Category').at(0)).toBeInTheDocument();
expect(getByTestId(`field-${timestampFieldId}-checkbox`)).toBeInTheDocument();
expect(getByTestId(`field-${timestampFieldId}-name`)).toBeInTheDocument();
expect(getByTestId(`field-${timestampFieldId}-description`)).toBeInTheDocument();
expect(getByTestId(`field-${timestampFieldId}-category`)).toBeInTheDocument();
});

View file

@ -11,20 +11,15 @@ import {
EuiToolTip,
EuiFlexGroup,
EuiFlexItem,
EuiScreenReaderOnly,
EuiBadge,
EuiBasicTableColumn,
EuiTableActionsColumnType,
} from '@elastic/eui';
import { uniqBy } from 'lodash/fp';
import { getEmptyValue, getExampleText, getIconFromType } from '../../helpers';
import type {
BrowserFields,
BrowserFieldItem,
FieldTableColumns,
GetFieldTableColumns,
} from '../../types';
import { BrowserFields } from '@kbn/rule-registry-plugin/common';
import { getIconFromType } from '../../helpers';
import type { BrowserFieldItem, FieldTableColumns, GetFieldTableColumns } from '../../types';
import { FieldName } from '../field_name';
import * as i18n from '../../translations';
import { styles } from './field_items.style';
@ -93,26 +88,6 @@ const getDefaultFieldTableColumns = (highlight: string): FieldTableColumns => [
sortable: true,
width: '225px',
},
{
field: 'description',
name: i18n.DESCRIPTION,
render: (description: string, { name, example }) => (
<EuiToolTip content={description}>
<>
<EuiScreenReaderOnly data-test-subj="descriptionForScreenReaderOnly">
<p>{i18n.DESCRIPTION_FOR_FIELD(name)}</p>
</EuiScreenReaderOnly>
<span css={styles.truncatable}>
<span css={styles.description} data-test-subj={`field-${name}-description`}>
{`${description ?? getEmptyValue()} ${getExampleText(example)}`}
</span>
</span>
</>
</EuiToolTip>
),
sortable: true,
width: '400px',
},
{
field: 'category',
name: i18n.CATEGORY,

View file

@ -6,9 +6,10 @@
*/
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { EuiInMemoryTable, Pagination, Direction, useEuiTheme } from '@elastic/eui';
import { BrowserFields } from '@kbn/rule-registry-plugin/common';
import { getFieldColumns, getFieldItems, isActionsColumn } from '../field_items';
import { CATEGORY_TABLE_CLASS_NAME, TABLE_HEIGHT } from '../../helpers';
import type { BrowserFields, FieldBrowserProps, GetFieldTableColumns } from '../../types';
import type { FieldBrowserProps, GetFieldTableColumns } from '../../types';
import { FieldTableHeader } from './field_table_header';
import { styles } from './field_table.styles';

View file

@ -8,7 +8,8 @@ import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui';
import { debounce } from 'lodash';
import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react';
import type { FieldBrowserProps, BrowserFields } from './types';
import { BrowserFields } from '@kbn/rule-registry-plugin/common';
import type { FieldBrowserProps } from './types';
import { FieldBrowserModal } from './field_browser_modal';
import { filterBrowserFieldsByFieldName, filterSelectedBrowserFields } from './helpers';
import * as i18n from './translations';

View file

@ -18,7 +18,8 @@ import {
} from '@elastic/eui';
import React, { useCallback } from 'react';
import type { FieldBrowserProps, BrowserFields } from './types';
import { BrowserFields } from '@kbn/rule-registry-plugin/common';
import type { FieldBrowserProps } from './types';
import { Search } from './components/search';
import { CLOSE_BUTTON_CLASS_NAME, FIELD_BROWSER_WIDTH, RESET_FIELDS_CLASS_NAME } from './helpers';

View file

@ -13,7 +13,7 @@ import {
filterBrowserFieldsByFieldName,
filterSelectedBrowserFields,
} from './helpers';
import type { BrowserFields } from './types';
import { BrowserFields } from '@kbn/rule-registry-plugin/common';
describe('helpers', () => {
describe('categoryHasFields', () => {

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import { BrowserField, BrowserFields } from '@kbn/rule-registry-plugin/common';
import { isEmpty } from 'lodash/fp';
import { BrowserField, BrowserFields } from './types';
export const FIELD_BROWSER_WIDTH = 925;
export const TABLE_HEIGHT = 260;

View file

@ -6,7 +6,7 @@
*/
import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { BrowserFields } from './types';
import { BrowserFields } from '@kbn/rule-registry-plugin/common';
const DEFAULT_INDEX_PATTERN = [
'apm-*-transaction*',

View file

@ -6,26 +6,8 @@
*/
import type { EuiBasicTableColumn } from '@elastic/eui';
import type { IFieldSubType } from '@kbn/es-query';
import type { RuntimeField } from '@kbn/data-views-plugin/common';
import { BrowserFields } from '@kbn/rule-registry-plugin/common';
export interface BrowserField {
aggregatable: boolean;
category: string;
description: string | null;
example: string | number | null;
fields: Readonly<Record<string, Partial<BrowserField>>>;
format: string;
indexes: string[];
name: string;
searchable: boolean;
type: string;
subType?: IFieldSubType;
readFromDocValues: boolean;
runtimeField?: RuntimeField;
}
export type BrowserFields = Readonly<Record<string, Partial<BrowserField>>>;
/**
* An item rendered in the table
*/

View file

@ -410,7 +410,6 @@ export interface FetchAlertData {
isInitializing: boolean;
isLoading: boolean;
getInspectQuery: () => { request: {}; response: {} };
onColumnsChange: (columns: EuiDataGridColumn[], visibleColumns: string[]) => void;
onPageChange: (pagination: RuleRegistrySearchRequestPagination) => void;
onSortChange: (sort: EuiDataGridSorting['columns']) => void;
refresh: () => void;
@ -434,6 +433,11 @@ export interface AlertsTableProps {
visibleColumns: string[];
'data-test-subj': string;
updatedAt: number;
browserFields: any;
onToggleColumn: (columnId: string) => void;
onResetColumns: () => void;
onColumnsChange: (columns: EuiDataGridColumn[], visibleColumns: string[]) => void;
onChangeVisibleColumns: (newColumns: string[]) => void;
}
// TODO We need to create generic type between our plugin, right now we have different one because of the old alerts table

View file

@ -46,6 +46,32 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
);
});
it('should let the user choose between fields', async () => {
await waitAndClickByTestId('show-field-browser');
await waitAndClickByTestId('categories-filter-button');
await waitAndClickByTestId('categories-selector-option-name-base');
await find.clickByCssSelector('#_id');
await waitAndClickByTestId('close');
const headers = await find.allByCssSelector('.euiDataGridHeaderCell');
expect(headers.length).to.be(6);
});
it('should take into account the column type when sorting', async () => {
const sortElQuery =
'[data-test-subj="dataGridHeaderCellActionGroup-kibana.alert.duration.us"] > li:nth-child(2)';
await waitAndClickByTestId('dataGridHeaderCell-kibana.alert.duration.us');
await retry.try(async () => {
const exists = await find.byCssSelector(sortElQuery);
if (!exists) throw new Error('Still loading...');
});
const sortItem = await find.byCssSelector(sortElQuery);
expect(await sortItem.getVisibleText()).to.be('Sort Low-High');
});
it('should sort properly', async () => {
await find.clickDisplayedByCssSelector(
'[data-test-subj="dataGridHeaderCell-event.action"] .euiDataGridHeaderCell__button'
@ -161,4 +187,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
return rows;
}
});
const waitAndClickByTestId = async (testId: string) => {
retry.try(async () => {
const exists = await testSubjects.exists(testId);
if (!exists) throw new Error('Still loading...');
});
return find.clickDisplayedByCssSelector(`[data-test-subj="${testId}"]`);
};
}

View file

@ -49,8 +49,7 @@ export default ({ getService, getPageObject }: FtrProviderContext) => {
expect(durationColumnExists).to.be(false);
});
// TODO Enable this test after fixing: https://github.com/elastic/kibana/issues/137988
it.skip('remembers sorting changes', async () => {
it('remembers sorting changes', async () => {
const timestampColumnButton = await testSubjects.find(
'dataGridHeaderCellActionButton-@timestamp'
);