mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
8b15dd87b3
commit
af7337597c
9 changed files with 755 additions and 5 deletions
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,3 +6,4 @@
|
|||
*/
|
||||
|
||||
export { SpikeAnalysisTable } from './spike_analysis_table';
|
||||
export { SpikeAnalysisGroupsTable } from './spike_analysis_table_groups';
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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}`,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ export const farequoteDataViewTestData: TestData = {
|
|||
brushTargetTimestamp: 1455033600000,
|
||||
expected: {
|
||||
totalDocCountFormatted: '86,274',
|
||||
analysisGroupsTable: [{ group: 'airline: AAL', docCount: '297' }],
|
||||
analysisTable: [
|
||||
{
|
||||
fieldName: 'airline',
|
||||
|
|
|
@ -13,6 +13,7 @@ export interface TestData {
|
|||
brushTargetTimestamp: number;
|
||||
expected: {
|
||||
totalDocCountFormatted: string;
|
||||
analysisGroupsTable: Array<{ group: string; docCount: string }>;
|
||||
analysisTable: Array<{
|
||||
fieldName: string;
|
||||
fieldValue: string;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
})();
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue