[ML] Explain Log Rate Spikes: sample groups table view (#140464)

* wip: create groups table

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

* show significant terms data in groups expanded row

* update mock data to reflect new data format

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

* sort group fields alphabetically

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

* replace mock data with api data

* fix functional test and remove commented code

* update types

* update functional tests

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Melissa Alvarez 2022-09-15 07:52:26 -06:00 committed by GitHub
parent 8b15dd87b3
commit af7337597c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 755 additions and 5 deletions

View file

@ -8,7 +8,14 @@
import React, { useEffect, useMemo, useState, FC } from 'react';
import { isEqual } from 'lodash';
import { EuiCallOut, EuiEmptyPrompt, EuiSpacer, EuiText } from '@elastic/eui';
import {
EuiCallOut,
EuiEmptyPrompt,
EuiFormRow,
EuiSpacer,
EuiSwitch,
EuiText,
} from '@elastic/eui';
import type { DataView } from '@kbn/data-views-plugin/public';
import { ProgressControls } from '@kbn/aiops-components';
@ -23,8 +30,16 @@ import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import { initialState, streamReducer } from '../../../common/api/stream_reducer';
import type { ApiExplainLogRateSpikes } from '../../../common/api';
import { SpikeAnalysisGroupsTable } from '../spike_analysis_table';
import { SpikeAnalysisTable } from '../spike_analysis_table';
const groupResultsMessage = i18n.translate(
'xpack.aiops.spikeAnalysisTable.groupedSwitchLabel.groupResults',
{
defaultMessage: 'Group results',
}
);
/**
* ExplainLogRateSpikes props require a data view.
*/
@ -59,6 +74,11 @@ export const ExplainLogRateSpikesAnalysis: FC<ExplainLogRateSpikesAnalysisProps>
const [currentAnalysisWindowParameters, setCurrentAnalysisWindowParameters] = useState<
WindowParameters | undefined
>();
const [groupResults, setGroupResults] = useState<boolean>(true);
const onSwitchToggle = (e: { target: { checked: React.SetStateAction<boolean> } }) => {
setGroupResults(e.target.checked);
};
const {
cancel,
@ -75,6 +95,7 @@ export const ExplainLogRateSpikesAnalysis: FC<ExplainLogRateSpikesAnalysisProps>
// TODO Handle data view without time fields.
timeFieldName: dataView.timeFieldName ?? '',
index: dataView.title,
grouping: true,
...windowParameters,
},
{ reducer: streamReducer, initialState }
@ -102,6 +123,34 @@ export const ExplainLogRateSpikesAnalysis: FC<ExplainLogRateSpikesAnalysisProps>
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const groupTableItems = useMemo(() => {
const tableItems = data.changePointsGroups.map(({ group, docCount }, index) => {
const sortedGroup = group.sort((a, b) =>
a.fieldName > b.fieldName ? 1 : b.fieldName > a.fieldName ? -1 : 0
);
const dedupedGroup: Record<string, any> = {};
const repeatedValues: Record<string, any> = {};
sortedGroup.forEach((pair) => {
const { fieldName, fieldValue } = pair;
if (pair.duplicate === false) {
dedupedGroup[fieldName] = fieldValue;
} else {
repeatedValues[fieldName] = fieldValue;
}
});
return {
id: index,
docCount,
group: dedupedGroup,
repeatedValues,
};
});
return tableItems;
}, [data.changePointsGroups]);
const shouldRerunAnalysis = useMemo(
() =>
currentAnalysisWindowParameters !== undefined &&
@ -110,6 +159,11 @@ export const ExplainLogRateSpikesAnalysis: FC<ExplainLogRateSpikesAnalysisProps>
);
const showSpikeAnalysisTable = data?.changePoints.length > 0;
const groupItemCount = groupTableItems.reduce((p, c) => {
return p + Object.keys(c.group).length;
}, 0);
const foundGroups =
groupTableItems.length === 0 || (groupTableItems.length > 0 && groupItemCount > 0);
return (
<div data-test-subj="aiopsExplainLogRateSpikesAnalysis">
@ -121,6 +175,17 @@ export const ExplainLogRateSpikesAnalysis: FC<ExplainLogRateSpikesAnalysisProps>
onCancel={cancel}
shouldRerunAnalysis={shouldRerunAnalysis}
/>
{showSpikeAnalysisTable && foundGroups && (
<EuiFormRow display="columnCompressedSwitch" label={groupResultsMessage}>
<EuiSwitch
showLabel={false}
label={''}
checked={groupResults}
onChange={onSwitchToggle}
compressed
/>
</EuiFormRow>
)}
<EuiSpacer size="xs" />
{!isRunning && !showSpikeAnalysisTable && (
<EuiEmptyPrompt
@ -171,7 +236,18 @@ export const ExplainLogRateSpikesAnalysis: FC<ExplainLogRateSpikesAnalysisProps>
<EuiSpacer size="xs" />
</>
)}
{showSpikeAnalysisTable && (
{showSpikeAnalysisTable && groupResults && foundGroups ? (
<SpikeAnalysisGroupsTable
changePoints={data.changePoints}
groupTableItems={groupTableItems}
loading={isRunning}
onPinnedChangePoint={onPinnedChangePoint}
onSelectedChangePoint={onSelectedChangePoint}
selectedChangePoint={selectedChangePoint}
dataViewId={dataView.id}
/>
) : null}
{showSpikeAnalysisTable && (!groupResults || !foundGroups) ? (
<SpikeAnalysisTable
changePoints={data.changePoints}
loading={isRunning}
@ -180,7 +256,7 @@ export const ExplainLogRateSpikesAnalysis: FC<ExplainLogRateSpikesAnalysisProps>
selectedChangePoint={selectedChangePoint}
dataViewId={dataView.id}
/>
)}
) : null}
</div>
);
};

View file

@ -6,3 +6,4 @@
*/
export { SpikeAnalysisTable } from './spike_analysis_table';
export { SpikeAnalysisGroupsTable } from './spike_analysis_table_groups';

View file

@ -0,0 +1,357 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { FC, useCallback, useMemo, useState } from 'react';
import { sortBy } from 'lodash';
import {
EuiBadge,
EuiBasicTable,
EuiBasicTableColumn,
EuiIcon,
EuiTableSortingType,
EuiToolTip,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { escapeKuery } from '@kbn/es-query';
import type { ChangePoint } from '@kbn/ml-agg-utils';
import { SEARCH_QUERY_LANGUAGE } from '../../application/utils/search_utils';
import { useEuiTheme } from '../../hooks/use_eui_theme';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import { MiniHistogram } from '../mini_histogram';
import { getFailedTransactionsCorrelationImpactLabel } from './get_failed_transactions_correlation_impact_label';
const NARROW_COLUMN_WIDTH = '120px';
const ACTIONS_COLUMN_WIDTH = '60px';
const NOT_AVAILABLE = '--';
const PAGINATION_SIZE_OPTIONS = [5, 10, 20, 50];
const DEFAULT_SORT_FIELD = 'pValue';
const DEFAULT_SORT_DIRECTION = 'asc';
const viewInDiscoverMessage = i18n.translate(
'xpack.aiops.spikeAnalysisTable.linksMenu.viewInDiscover',
{
defaultMessage: 'View in Discover',
}
);
interface SpikeAnalysisTableExpandedRowProps {
changePoints: ChangePoint[];
dataViewId?: string;
loading: boolean;
onPinnedChangePoint?: (changePoint: ChangePoint | null) => void;
onSelectedChangePoint?: (changePoint: ChangePoint | null) => void;
selectedChangePoint?: ChangePoint;
}
export const SpikeAnalysisTableExpandedRow: FC<SpikeAnalysisTableExpandedRowProps> = ({
changePoints,
dataViewId,
loading,
onPinnedChangePoint,
onSelectedChangePoint,
selectedChangePoint,
}) => {
const euiTheme = useEuiTheme();
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);
const [sortField, setSortField] = useState<keyof ChangePoint>(DEFAULT_SORT_FIELD);
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>(DEFAULT_SORT_DIRECTION);
const { application, share, data } = useAiopsAppContext();
const discoverLocator = useMemo(
() => share.url.locators.get('DISCOVER_APP_LOCATOR'),
[share.url.locators]
);
const discoverUrlError = useMemo(() => {
if (!application.capabilities.discover?.show) {
const discoverNotEnabled = i18n.translate(
'xpack.aiops.spikeAnalysisTable.discoverNotEnabledErrorMessage',
{
defaultMessage: 'Discover is not enabled',
}
);
return discoverNotEnabled;
}
if (!discoverLocator) {
const discoverLocatorMissing = i18n.translate(
'xpack.aiops.spikeAnalysisTable.discoverLocatorMissingErrorMessage',
{
defaultMessage: 'No locator for Discover detected',
}
);
return discoverLocatorMissing;
}
if (!dataViewId) {
const autoGeneratedDiscoverLinkError = i18n.translate(
'xpack.aiops.spikeAnalysisTable.autoGeneratedDiscoverLinkErrorMessage',
{
defaultMessage: 'Unable to link to Discover; no data view exists for this index',
}
);
return autoGeneratedDiscoverLinkError;
}
}, [application.capabilities.discover?.show, dataViewId, discoverLocator]);
const generateDiscoverUrl = async (changePoint: ChangePoint) => {
if (discoverLocator !== undefined) {
const url = await discoverLocator.getRedirectUrl({
indexPatternId: dataViewId,
timeRange: data.query.timefilter.timefilter.getTime(),
filters: data.query.filterManager.getFilters(),
query: {
language: SEARCH_QUERY_LANGUAGE.KUERY,
query: `${escapeKuery(changePoint.fieldName)}:${escapeKuery(
String(changePoint.fieldValue)
)}`,
},
});
return url;
}
};
const columns: Array<EuiBasicTableColumn<ChangePoint>> = [
{
'data-test-subj': 'aiopsSpikeAnalysisTableColumnFieldName',
field: 'fieldName',
name: i18n.translate(
'xpack.aiops.correlations.failedTransactions.correlationsTable.fieldNameLabel',
{ defaultMessage: 'Field name' }
),
sortable: true,
},
{
'data-test-subj': 'aiopsSpikeAnalysisTableColumnFieldValue',
field: 'fieldValue',
name: i18n.translate(
'xpack.aiops.correlations.failedTransactions.correlationsTable.fieldValueLabel',
{ defaultMessage: 'Field value' }
),
render: (_, { fieldValue }) => String(fieldValue).slice(0, 50),
sortable: true,
},
{
'data-test-subj': 'aiopsSpikeAnalysisTableColumnLogRate',
width: NARROW_COLUMN_WIDTH,
field: 'pValue',
name: (
<EuiToolTip
position="top"
content={i18n.translate(
'xpack.aiops.correlations.failedTransactions.correlationsTable.logRateColumnTooltip',
{
defaultMessage:
'A visual representation of the impact of the field on the message rate difference',
}
)}
>
<>
<FormattedMessage
id="xpack.aiops.correlations.failedTransactions.correlationsTable.logRateLabel"
defaultMessage="Log rate"
/>
<EuiIcon size="s" color="subdued" type="questionInCircle" className="eui-alignTop" />
</>
</EuiToolTip>
),
render: (_, { histogram, fieldName, fieldValue }) => {
if (!histogram) return NOT_AVAILABLE;
return (
<MiniHistogram
chartData={histogram}
isLoading={loading && histogram === undefined}
label={`${fieldName}:${fieldValue}`}
/>
);
},
sortable: false,
},
{
'data-test-subj': 'aiopsSpikeAnalysisTableColumnPValue',
width: NARROW_COLUMN_WIDTH,
field: 'pValue',
name: (
<EuiToolTip
position="top"
content={i18n.translate(
'xpack.aiops.correlations.failedTransactions.correlationsTable.pValueColumnTooltip',
{
defaultMessage:
'The significance of changes in the frequency of values; lower values indicate greater change',
}
)}
>
<>
<FormattedMessage
id="xpack.aiops.correlations.failedTransactions.correlationsTable.pValueLabel"
defaultMessage="p-value"
/>
<EuiIcon size="s" color="subdued" type="questionInCircle" className="eui-alignTop" />
</>
</EuiToolTip>
),
render: (pValue: number) => pValue?.toPrecision(3) ?? NOT_AVAILABLE,
sortable: true,
},
{
'data-test-subj': 'aiopsSpikeAnalysisTableColumnImpact',
width: NARROW_COLUMN_WIDTH,
field: 'pValue',
name: (
<EuiToolTip
position="top"
content={i18n.translate(
'xpack.aiops.correlations.failedTransactions.correlationsTable.impactLabelColumnTooltip',
{
defaultMessage: 'The level of impact of the field on the message rate difference',
}
)}
>
<>
<FormattedMessage
id="xpack.aiops.correlations.failedTransactions.correlationsTable.impactLabel"
defaultMessage="Impact"
/>
<EuiIcon size="s" color="subdued" type="questionInCircle" className="eui-alignTop" />
</>
</EuiToolTip>
),
render: (_, { pValue }) => {
if (!pValue) return NOT_AVAILABLE;
const label = getFailedTransactionsCorrelationImpactLabel(pValue);
return label ? <EuiBadge color={label.color}>{label.impact}</EuiBadge> : null;
},
sortable: true,
},
{
'data-test-subj': 'aiOpsSpikeAnalysisTableColumnAction',
name: i18n.translate('xpack.aiops.spikeAnalysisTable.actionsColumnName', {
defaultMessage: 'Actions',
}),
actions: [
{
name: () => (
<EuiToolTip content={discoverUrlError ? discoverUrlError : viewInDiscoverMessage}>
<EuiIcon type="discoverApp" />
</EuiToolTip>
),
description: viewInDiscoverMessage,
type: 'button',
onClick: async (changePoint) => {
const openInDiscoverUrl = await generateDiscoverUrl(changePoint);
if (typeof openInDiscoverUrl === 'string') {
await application.navigateToUrl(openInDiscoverUrl);
}
},
enabled: () => discoverUrlError === undefined,
},
],
width: ACTIONS_COLUMN_WIDTH,
},
];
const onChange = useCallback((tableSettings) => {
const { index, size } = tableSettings.page;
const { field, direction } = tableSettings.sort;
setPageIndex(index);
setPageSize(size);
setSortField(field);
setSortDirection(direction);
}, []);
const { pagination, pageOfItems, sorting } = useMemo(() => {
const pageStart = pageIndex * pageSize;
const itemCount = changePoints?.length ?? 0;
let items: ChangePoint[] = changePoints ?? [];
items = sortBy(changePoints, (item) => {
if (item && typeof item[sortField] === 'string') {
// @ts-ignore Object is possibly null or undefined
return item[sortField].toLowerCase();
}
return item[sortField];
});
items = sortDirection === 'asc' ? items : items.reverse();
return {
pageOfItems: items.slice(pageStart, pageStart + pageSize),
pagination: {
pageIndex,
pageSize,
totalItemCount: itemCount,
pageSizeOptions: PAGINATION_SIZE_OPTIONS,
},
sorting: {
sort: {
field: sortField,
direction: sortDirection,
},
},
};
}, [pageIndex, pageSize, sortField, sortDirection, changePoints]);
// Don't pass on the `loading` state to the table itself because
// it disables hovering events. Because the mini histograms take a while
// to load, hovering would not update the main chart. Instead,
// the loading state is shown by the progress bar on the outer component level.
// The outer component also will display a prompt when no data was returned
// running the analysis and will hide this table.
return (
<EuiBasicTable
data-test-subj="aiopsSpikeAnalysisTable"
compressed
columns={columns}
items={pageOfItems}
onChange={onChange}
pagination={pagination}
loading={false}
sorting={sorting as EuiTableSortingType<ChangePoint>}
rowProps={(changePoint) => {
return {
'data-test-subj': `aiopsSpikeAnalysisTableRow row-${changePoint.fieldName}-${changePoint.fieldValue}`,
onClick: () => {
if (onPinnedChangePoint) {
onPinnedChangePoint(changePoint);
}
},
onMouseEnter: () => {
if (onSelectedChangePoint) {
onSelectedChangePoint(changePoint);
}
},
onMouseLeave: () => {
if (onSelectedChangePoint) {
onSelectedChangePoint(null);
}
},
style:
selectedChangePoint &&
selectedChangePoint.fieldValue === changePoint.fieldValue &&
selectedChangePoint.fieldName === changePoint.fieldName
? {
backgroundColor: euiTheme.euiColorLightestShade,
}
: null,
};
}}
/>
);
};

View file

@ -0,0 +1,243 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { FC, useCallback, useMemo, useState } from 'react';
import { sortBy } from 'lodash';
import {
EuiBadge,
EuiBasicTable,
EuiBasicTableColumn,
EuiButtonIcon,
EuiScreenReaderOnly,
EuiSpacer,
EuiTableSortingType,
RIGHT_ALIGNMENT,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { ChangePoint } from '@kbn/ml-agg-utils';
import { useEuiTheme } from '../../hooks/use_eui_theme';
import { SpikeAnalysisTableExpandedRow } from './spike_analysis_table_expanded_row';
const PAGINATION_SIZE_OPTIONS = [5, 10, 20, 50];
const DEFAULT_SORT_FIELD = 'docCount';
const DEFAULT_SORT_DIRECTION = 'desc';
interface GroupTableItem {
id: number;
docCount: number;
group: Record<string, any>;
repeatedValues: Record<string, any>;
}
interface SpikeAnalysisTableProps {
changePoints: ChangePoint[];
groupTableItems: GroupTableItem[];
dataViewId?: string;
loading: boolean;
onPinnedChangePoint?: (changePoint: ChangePoint | null) => void;
onSelectedChangePoint?: (changePoint: ChangePoint | null) => void;
selectedChangePoint?: ChangePoint;
}
export const SpikeAnalysisGroupsTable: FC<SpikeAnalysisTableProps> = ({
changePoints,
groupTableItems,
dataViewId,
loading,
onPinnedChangePoint,
onSelectedChangePoint,
selectedChangePoint,
}) => {
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);
const [sortField, setSortField] = useState<keyof GroupTableItem>(DEFAULT_SORT_FIELD);
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>(DEFAULT_SORT_DIRECTION);
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<Record<string, JSX.Element>>(
{}
);
const euiTheme = useEuiTheme();
const toggleDetails = (item: GroupTableItem) => {
const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap };
if (itemIdToExpandedRowMapValues[item.id]) {
delete itemIdToExpandedRowMapValues[item.id];
} else {
const { group, repeatedValues } = item;
const expandedTableItems = [];
const fullGroup = { ...group, ...repeatedValues };
for (const fieldName in fullGroup) {
if (fullGroup.hasOwnProperty(fieldName)) {
const fieldValue = fullGroup[fieldName];
expandedTableItems.push({
fieldName: `${fieldName}`,
fieldValue: `${fullGroup[fieldName]}`,
...(changePoints.find(
(changePoint) =>
(changePoint.fieldName === fieldName ||
changePoint.fieldName === `${fieldName}.keyword`) &&
(changePoint.fieldValue === fieldValue ||
changePoint.fieldValue === `${fieldValue}.keyword`)
) ?? {}),
});
}
}
itemIdToExpandedRowMapValues[item.id] = (
<SpikeAnalysisTableExpandedRow
changePoints={expandedTableItems as ChangePoint[]}
loading={loading}
onPinnedChangePoint={onPinnedChangePoint}
onSelectedChangePoint={onSelectedChangePoint}
selectedChangePoint={selectedChangePoint}
dataViewId={dataViewId}
/>
);
}
setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues);
};
const columns: Array<EuiBasicTableColumn<GroupTableItem>> = [
{
align: RIGHT_ALIGNMENT,
width: '40px',
isExpander: true,
name: (
<EuiScreenReaderOnly>
<span>Expand rows</span>
</EuiScreenReaderOnly>
),
render: (item: GroupTableItem) => (
<EuiButtonIcon
data-test-subj={'aiopsSpikeAnalysisGroupsTableRowExpansionButton'}
onClick={() => toggleDetails(item)}
aria-label={itemIdToExpandedRowMap[item.id] ? 'Collapse' : 'Expand'}
iconType={itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'}
/>
),
},
{
'data-test-subj': 'aiopsSpikeAnalysisGroupsTableColumnGroup',
field: 'group',
name: i18n.translate(
'xpack.aiops.correlations.failedTransactions.correlationsTable.groupLabel',
{ defaultMessage: 'Group' }
),
render: (_, { group, repeatedValues }) => {
const valuesBadges = [];
for (const fieldName in group) {
if (group.hasOwnProperty(fieldName)) {
valuesBadges.push(
<>
<EuiBadge
key={`${fieldName}-id`}
data-test-subj="aiopsSpikeAnalysisTableColumnGroupBadge"
color="hollow"
>
<span>{`${fieldName}: `}</span>
<span
style={{ color: euiTheme.euiCodeBlockStringColor }}
>{`${group[fieldName]}`}</span>
</EuiBadge>
<EuiSpacer size="xs" />
</>
);
}
}
if (Object.keys(repeatedValues).length > 0) {
valuesBadges.push(
<>
<EuiBadge
key={`$more-id`}
data-test-subj="aiopsSpikeAnalysisGroupsTableColumnGroupBadge"
color="hollow"
>
+{Object.keys(repeatedValues).length} more
</EuiBadge>
<EuiSpacer size="xs" />
</>
);
}
return valuesBadges;
},
sortable: false,
textOnly: true,
},
{
'data-test-subj': 'aiopsSpikeAnalysisGroupsTableColumnDocCount',
field: 'docCount',
name: i18n.translate('xpack.aiops.correlations.correlationsTable.docCountLabel', {
defaultMessage: 'Doc count',
}),
sortable: true,
width: '20%',
},
];
const onChange = useCallback((tableSettings) => {
const { index, size } = tableSettings.page;
const { field, direction } = tableSettings.sort;
setPageIndex(index);
setPageSize(size);
setSortField(field);
setSortDirection(direction);
}, []);
const { pagination, pageOfItems, sorting } = useMemo(() => {
const pageStart = pageIndex * pageSize;
const itemCount = groupTableItems?.length ?? 0;
let items = groupTableItems ?? [];
items = sortBy(groupTableItems, (item) => {
if (item && typeof item[sortField] === 'string') {
// @ts-ignore Object is possibly null or undefined
return item[sortField].toLowerCase();
}
return item[sortField];
});
items = sortDirection === 'asc' ? items : items.reverse();
return {
pageOfItems: items.slice(pageStart, pageStart + pageSize),
pagination: {
pageIndex,
pageSize,
totalItemCount: itemCount,
pageSizeOptions: PAGINATION_SIZE_OPTIONS,
},
sorting: {
sort: {
field: sortField,
direction: sortDirection,
},
},
};
}, [pageIndex, pageSize, sortField, sortDirection, groupTableItems]);
return (
<EuiBasicTable
data-test-subj="aiopsSpikeAnalysisGroupsTable"
compressed
columns={columns}
items={pageOfItems}
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
onChange={onChange}
pagination={pagination}
loading={false}
sorting={sorting as EuiTableSortingType<GroupTableItem>}
rowProps={(group) => {
return {
'data-test-subj': `aiopsSpikeAnalysisGroupsTableRow row-${group.id}`,
};
}}
/>
);
};

View file

@ -114,10 +114,18 @@ export default function ({ getPageObject, getService }: FtrProviderContext) {
await aiops.explainLogRateSpikes.clickRerunAnalysisButton(true);
await aiops.explainLogRateSpikes.assertProgressTitle('Progress: 100% — Done.');
await aiops.explainLogRateSpikesAnalysisTable.assertSpikeAnalysisTableExists();
await aiops.explainLogRateSpikesAnalysisGroupsTable.assertSpikeAnalysisTableExists();
const analysisGroupsTable =
await aiops.explainLogRateSpikesAnalysisGroupsTable.parseAnalysisTable();
expect(analysisGroupsTable).to.be.eql(testData.expected.analysisGroupsTable);
await ml.testExecution.logTestStep('expand table row');
await aiops.explainLogRateSpikesAnalysisGroupsTable.assertExpandRowButtonExists();
await aiops.explainLogRateSpikesAnalysisGroupsTable.expandRow();
const analysisTable = await aiops.explainLogRateSpikesAnalysisTable.parseAnalysisTable();
expect(analysisTable).to.be.eql(testData.expected.analysisTable);
});
}

View file

@ -14,6 +14,7 @@ export const farequoteDataViewTestData: TestData = {
brushTargetTimestamp: 1455033600000,
expected: {
totalDocCountFormatted: '86,274',
analysisGroupsTable: [{ group: 'airline: AAL', docCount: '297' }],
analysisTable: [
{
fieldName: 'airline',

View file

@ -13,6 +13,7 @@ export interface TestData {
brushTargetTimestamp: number;
expected: {
totalDocCountFormatted: string;
analysisGroupsTable: Array<{ group: string; docCount: string }>;
analysisTable: Array<{
fieldName: string;
fieldValue: string;

View file

@ -0,0 +1,59 @@
/*
* 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 { FtrProviderContext } from '../../ftr_provider_context';
export function ExplainLogRateSpikesAnalysisGroupsTableProvider({
getService,
}: FtrProviderContext) {
const testSubjects = getService('testSubjects');
return new (class AnalysisTable {
public async assertSpikeAnalysisTableExists() {
await testSubjects.existOrFail(`aiopsSpikeAnalysisGroupsTable`);
}
public async assertExpandRowButtonExists() {
await testSubjects.existOrFail('aiopsSpikeAnalysisGroupsTableRowExpansionButton');
}
public async expandRow() {
await testSubjects.click('aiopsSpikeAnalysisGroupsTableRowExpansionButton');
await testSubjects.existOrFail('aiopsSpikeAnalysisTable');
}
public async parseAnalysisTable() {
const table = await testSubjects.find('~aiopsSpikeAnalysisGroupsTable');
const $ = await table.parseDomContent();
const rows = [];
for (const tr of $.findTestSubjects('~aiopsSpikeAnalysisGroupsTableRow').toArray()) {
const $tr = $(tr);
const rowObject: {
group: any;
docCount: string;
} = {
group: $tr
.findTestSubject('aiopsSpikeAnalysisGroupsTableColumnGroup')
.find('.euiTableCellContent')
.text()
.trim(),
docCount: $tr
.findTestSubject('aiopsSpikeAnalysisGroupsTableColumnDocCount')
.find('.euiTableCellContent')
.text()
.trim(),
};
rows.push(rowObject);
}
return rows;
}
})();
}

View file

@ -9,13 +9,17 @@ import type { FtrProviderContext } from '../../ftr_provider_context';
import { ExplainLogRateSpikesProvider } from './explain_log_rate_spikes';
import { ExplainLogRateSpikesAnalysisTableProvider } from './explain_log_rate_spikes_analysis_table';
import { ExplainLogRateSpikesAnalysisGroupsTableProvider } from './explain_log_rate_spikes_analysis_groups_table';
export function AiopsProvider(context: FtrProviderContext) {
const explainLogRateSpikes = ExplainLogRateSpikesProvider(context);
const explainLogRateSpikesAnalysisTable = ExplainLogRateSpikesAnalysisTableProvider(context);
const explainLogRateSpikesAnalysisGroupsTable =
ExplainLogRateSpikesAnalysisGroupsTableProvider(context);
return {
explainLogRateSpikes,
explainLogRateSpikesAnalysisTable,
explainLogRateSpikesAnalysisGroupsTable,
};
}