mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[ML] Explain Log Rate Spikes: Adds table action to copy filter to clipboard (#154311)
- Adds an action to the analysis results filter to copy a KUERY filter for each row to the clipboard. - Consolidates duplicate code for the Discover action into a custom hook.
This commit is contained in:
parent
b1882495f0
commit
eb7db09da8
12 changed files with 348 additions and 172 deletions
|
@ -16,6 +16,7 @@ export type {
|
|||
NumericChartData,
|
||||
NumericHistogramField,
|
||||
} from './src/fetch_histograms_for_fields';
|
||||
export { isSignificantTerm } from './src/type_guards';
|
||||
export type {
|
||||
AggCardinality,
|
||||
SignificantTerm,
|
||||
|
|
29
x-pack/packages/ml/agg_utils/src/type_guards.test.ts
Normal file
29
x-pack/packages/ml/agg_utils/src/type_guards.test.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { isSignificantTerm } from './type_guards';
|
||||
|
||||
describe('isSignificantTerm', () => {
|
||||
it('identifies significant terms', () => {
|
||||
expect(isSignificantTerm({})).toBeFalsy();
|
||||
expect(isSignificantTerm({ fieldName: 'response_code' })).toBeFalsy();
|
||||
expect(isSignificantTerm({ fieldValue: '500' })).toBeFalsy();
|
||||
expect(
|
||||
isSignificantTerm({
|
||||
fieldName: 'response_code',
|
||||
fieldValue: '500',
|
||||
doc_count: 1819,
|
||||
bg_count: 553,
|
||||
total_doc_count: 4671,
|
||||
total_bg_count: 1975,
|
||||
score: 26.546201745993947,
|
||||
pValue: 2.9589053032077285e-12,
|
||||
normalizedScore: 0.7814127409489161,
|
||||
})
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
32
x-pack/packages/ml/agg_utils/src/type_guards.ts
Normal file
32
x-pack/packages/ml/agg_utils/src/type_guards.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { isPopulatedObject } from '@kbn/ml-is-populated-object';
|
||||
|
||||
import type { SignificantTerm } from './types';
|
||||
|
||||
/**
|
||||
* Type guard for a significant term.
|
||||
* Note this is used as a custom type within Explain Log Rate Spikes
|
||||
* for a p-value based variant, not a generic significant terms
|
||||
* aggregation type.
|
||||
* @param arg The unknown type to be evaluated
|
||||
* @returns whether arg is of type SignificantTerm
|
||||
*/
|
||||
export function isSignificantTerm(arg: unknown): arg is SignificantTerm {
|
||||
return isPopulatedObject(arg, [
|
||||
'fieldName',
|
||||
'fieldValue',
|
||||
'doc_count',
|
||||
'bg_count',
|
||||
'total_doc_count',
|
||||
'total_bg_count',
|
||||
'score',
|
||||
'pValue',
|
||||
'normalizedScore',
|
||||
]);
|
||||
}
|
|
@ -58,6 +58,9 @@ export interface HistogramField {
|
|||
|
||||
/**
|
||||
* Significant term meta data for a field/value pair.
|
||||
* Note this is used as a custom type within Explain Log Rate Spikes
|
||||
* for a p-value based variant, not a generic significant terms
|
||||
* aggregation type.
|
||||
*/
|
||||
export interface SignificantTerm extends FieldValuePair {
|
||||
doc_count: number;
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { finalSignificantTermGroups } from '../../../common/__mocks__/artificial_logs/final_significant_term_groups';
|
||||
import { significantTerms } from '../../../common/__mocks__/artificial_logs/significant_terms';
|
||||
|
||||
import { getGroupTableItems } from './get_group_table_items';
|
||||
import { getTableItemAsKQL } from './get_table_item_as_kql';
|
||||
|
||||
describe('getTableItemAsKQL', () => {
|
||||
it('returns a KQL syntax for a significant term', () => {
|
||||
expect(getTableItemAsKQL(significantTerms[0])).toBe('user:Peter');
|
||||
expect(getTableItemAsKQL(significantTerms[1])).toBe('response_code:500');
|
||||
expect(getTableItemAsKQL(significantTerms[2])).toBe('url:home.php');
|
||||
expect(getTableItemAsKQL(significantTerms[3])).toBe('url:login.php');
|
||||
});
|
||||
it('returns a KQL syntax for a group of significant terms', () => {
|
||||
const groupTableItems = getGroupTableItems(finalSignificantTermGroups);
|
||||
expect(getTableItemAsKQL(groupTableItems[0])).toBe('user:Peter AND url:login.php');
|
||||
expect(getTableItemAsKQL(groupTableItems[1])).toBe('response_code:500 AND url:home.php');
|
||||
expect(getTableItemAsKQL(groupTableItems[2])).toBe('url:login.php AND response_code:500');
|
||||
expect(getTableItemAsKQL(groupTableItems[3])).toBe('user:Peter AND url:home.php');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { escapeKuery } from '@kbn/es-query';
|
||||
import { isSignificantTerm, type SignificantTerm } from '@kbn/ml-agg-utils';
|
||||
|
||||
import type { GroupTableItem } from './types';
|
||||
|
||||
export const getTableItemAsKQL = (tableItem: GroupTableItem | SignificantTerm) => {
|
||||
if (isSignificantTerm(tableItem)) {
|
||||
return `${escapeKuery(tableItem.fieldName)}:${escapeKuery(String(tableItem.fieldValue))}`;
|
||||
}
|
||||
|
||||
return [
|
||||
...tableItem.groupItemsSortedByUniqueness.map(
|
||||
({ fieldName, fieldValue }) => `${escapeKuery(fieldName)}:${escapeKuery(String(fieldValue))}`
|
||||
),
|
||||
].join(' AND ');
|
||||
};
|
|
@ -21,17 +21,16 @@ import {
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { escapeKuery } from '@kbn/es-query';
|
||||
import type { SignificantTerm } 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';
|
||||
import { useSpikeAnalysisTableRowContext } from './spike_analysis_table_row_provider';
|
||||
import { useCopyToClipboardAction } from './use_copy_to_clipboard_action';
|
||||
import { useViewInDiscoverAction } from './use_view_in_discover_action';
|
||||
|
||||
const NARROW_COLUMN_WIDTH = '120px';
|
||||
const ACTIONS_COLUMN_WIDTH = '60px';
|
||||
|
@ -41,12 +40,6 @@ 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 SpikeAnalysisTableProps {
|
||||
significantTerms: SignificantTerm[];
|
||||
|
@ -76,63 +69,8 @@ export const SpikeAnalysisTable: FC<SpikeAnalysisTableProps> = ({
|
|||
const [sortField, setSortField] = useState<keyof SignificantTerm>(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 (significantTerm: SignificantTerm) => {
|
||||
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(significantTerm.fieldName)}:${escapeKuery(
|
||||
String(significantTerm.fieldValue)
|
||||
)}`,
|
||||
},
|
||||
});
|
||||
|
||||
return url;
|
||||
}
|
||||
};
|
||||
const copyToClipBoardAction = useCopyToClipboardAction();
|
||||
const viewInDiscoverAction = useViewInDiscoverAction(dataViewId);
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<SignificantTerm>> = [
|
||||
{
|
||||
|
@ -263,24 +201,7 @@ export const SpikeAnalysisTable: FC<SpikeAnalysisTableProps> = ({
|
|||
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 (significantTerm) => {
|
||||
const openInDiscoverUrl = await generateDiscoverUrl(significantTerm);
|
||||
if (typeof openInDiscoverUrl === 'string') {
|
||||
await application.navigateToUrl(openInDiscoverUrl);
|
||||
}
|
||||
},
|
||||
enabled: () => discoverUrlError === undefined,
|
||||
},
|
||||
],
|
||||
actions: [viewInDiscoverAction, copyToClipBoardAction],
|
||||
width: ACTIONS_COLUMN_WIDTH,
|
||||
valign: 'top',
|
||||
},
|
||||
|
|
|
@ -26,19 +26,17 @@ import {
|
|||
} from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { escapeKuery } from '@kbn/es-query';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { SignificantTerm } from '@kbn/ml-agg-utils';
|
||||
|
||||
import { SEARCH_QUERY_LANGUAGE } from '../../application/utils/search_utils';
|
||||
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
|
||||
|
||||
import { MiniHistogram } from '../mini_histogram';
|
||||
|
||||
import { getFailedTransactionsCorrelationImpactLabel } from './get_failed_transactions_correlation_impact_label';
|
||||
import { SpikeAnalysisTable } from './spike_analysis_table';
|
||||
import { useSpikeAnalysisTableRowContext } from './spike_analysis_table_row_provider';
|
||||
import type { GroupTableItem } from './types';
|
||||
import { useCopyToClipboardAction } from './use_copy_to_clipboard_action';
|
||||
import { useViewInDiscoverAction } from './use_view_in_discover_action';
|
||||
|
||||
const NARROW_COLUMN_WIDTH = '120px';
|
||||
const EXPAND_COLUMN_WIDTH = '40px';
|
||||
|
@ -49,12 +47,6 @@ const MAX_GROUP_BADGES = 5;
|
|||
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 SpikeAnalysisTableProps {
|
||||
significantTerms: SignificantTerm[];
|
||||
|
@ -117,66 +109,8 @@ export const SpikeAnalysisGroupsTable: FC<SpikeAnalysisTableProps> = ({
|
|||
setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues);
|
||||
};
|
||||
|
||||
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 (groupTableItem: GroupTableItem) => {
|
||||
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: [
|
||||
...groupTableItem.groupItemsSortedByUniqueness.map(
|
||||
({ fieldName, fieldValue }) =>
|
||||
`${escapeKuery(fieldName)}:${escapeKuery(String(fieldValue))}`
|
||||
),
|
||||
].join(' AND '),
|
||||
},
|
||||
});
|
||||
|
||||
return url;
|
||||
}
|
||||
};
|
||||
const copyToClipBoardAction = useCopyToClipboardAction();
|
||||
const viewInDiscoverAction = useViewInDiscoverAction(dataViewId);
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<GroupTableItem>> = [
|
||||
{
|
||||
|
@ -411,24 +345,7 @@ export const SpikeAnalysisGroupsTable: FC<SpikeAnalysisTableProps> = ({
|
|||
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 (tableItem) => {
|
||||
const openInDiscoverUrl = await generateDiscoverUrl(tableItem);
|
||||
if (typeof openInDiscoverUrl === 'string') {
|
||||
await application.navigateToUrl(openInDiscoverUrl);
|
||||
}
|
||||
},
|
||||
enabled: () => discoverUrlError === undefined,
|
||||
},
|
||||
],
|
||||
actions: [viewInDiscoverAction, copyToClipBoardAction],
|
||||
width: ACTIONS_COLUMN_WIDTH,
|
||||
valign: 'top',
|
||||
},
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { EuiTableActionsColumnType } from '@elastic/eui';
|
||||
|
||||
import type { SignificantTerm, SignificantTermGroupItem } from '@kbn/ml-agg-utils';
|
||||
|
||||
export type GroupTableItemGroup = Pick<
|
||||
|
@ -20,3 +22,7 @@ export interface GroupTableItem {
|
|||
groupItemsSortedByUniqueness: GroupTableItemGroup[];
|
||||
histogram: SignificantTerm['histogram'];
|
||||
}
|
||||
|
||||
export type TableItemAction = EuiTableActionsColumnType<
|
||||
SignificantTerm | GroupTableItem
|
||||
>['actions'][number];
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 { ReactElement } from 'react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render, act } from '@testing-library/react';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import type { SignificantTerm } from '@kbn/ml-agg-utils';
|
||||
|
||||
import { finalSignificantTermGroups } from '../../../common/__mocks__/artificial_logs/final_significant_term_groups';
|
||||
import { significantTerms } from '../../../common/__mocks__/artificial_logs/significant_terms';
|
||||
|
||||
import { getGroupTableItems } from './get_group_table_items';
|
||||
import { useCopyToClipboardAction } from './use_copy_to_clipboard_action';
|
||||
import type { GroupTableItem } from './types';
|
||||
|
||||
interface Action {
|
||||
render: (tableItem: SignificantTerm | GroupTableItem) => ReactElement;
|
||||
}
|
||||
|
||||
const execCommandMock = (global.document.execCommand = jest.fn());
|
||||
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
describe('useCopyToClipboardAction', () => {
|
||||
it('renders the action for a single significant term', async () => {
|
||||
execCommandMock.mockImplementationOnce(() => true);
|
||||
const { result } = renderHook(() => useCopyToClipboardAction());
|
||||
const { getByLabelText } = render((result.current as Action).render(significantTerms[0]));
|
||||
|
||||
const button = getByLabelText('Copy field/value pair as KQL syntax to clipboard');
|
||||
|
||||
expect(button).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.click(button);
|
||||
});
|
||||
|
||||
// EUI implements copy-to-clipboard with deprecated `document.execCommand`.
|
||||
// We can assert that is has been triggered, but the combo with jsdom doesn't
|
||||
// give us a way to assert the actual value that has been copied to the clipboard.
|
||||
expect(execCommandMock).toHaveBeenCalledWith('copy');
|
||||
expect(warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders the action for a group of items', async () => {
|
||||
execCommandMock.mockImplementationOnce(() => true);
|
||||
const groupTableItems = getGroupTableItems(finalSignificantTermGroups);
|
||||
const { result } = renderHook(() => useCopyToClipboardAction());
|
||||
const { getByLabelText } = render((result.current as Action).render(groupTableItems[0]));
|
||||
|
||||
const button = getByLabelText('Copy group items as KQL syntax to clipboard');
|
||||
|
||||
expect(button).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.click(button);
|
||||
});
|
||||
|
||||
// EUI implements copy-to-clipboard with deprecated `document.execCommand`.
|
||||
// We can assert that is has been triggered, but the combo with jsdom doesn't
|
||||
// give us a way to assert the actual value that has been copied to the clipboard.
|
||||
expect(execCommandMock).toHaveBeenCalledWith('copy');
|
||||
expect(warn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
|
||||
import { EuiCopy, EuiToolTip, EuiButtonIcon } from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { isSignificantTerm, type SignificantTerm } from '@kbn/ml-agg-utils';
|
||||
|
||||
import { getTableItemAsKQL } from './get_table_item_as_kql';
|
||||
import type { GroupTableItem, TableItemAction } from './types';
|
||||
|
||||
const copyToClipboardSignificantTermMessage = i18n.translate(
|
||||
'xpack.aiops.spikeAnalysisTable.linksMenu.copyToClipboardSignificantTermMessage',
|
||||
{
|
||||
defaultMessage: 'Copy field/value pair as KQL syntax to clipboard',
|
||||
}
|
||||
);
|
||||
|
||||
const copyToClipboardGroupMessage = i18n.translate(
|
||||
'xpack.aiops.spikeAnalysisTable.linksMenu.copyToClipboardGroupMessage',
|
||||
{
|
||||
defaultMessage: 'Copy group items as KQL syntax to clipboard',
|
||||
}
|
||||
);
|
||||
|
||||
export const useCopyToClipboardAction = (): TableItemAction => ({
|
||||
render: (tableItem: SignificantTerm | GroupTableItem) => {
|
||||
const message = isSignificantTerm(tableItem)
|
||||
? copyToClipboardSignificantTermMessage
|
||||
: copyToClipboardGroupMessage;
|
||||
return (
|
||||
<EuiToolTip content={message}>
|
||||
<EuiCopy textToCopy={getTableItemAsKQL(tableItem)}>
|
||||
{(copy) => <EuiButtonIcon iconType="copyClipboard" onClick={copy} aria-label={message} />}
|
||||
</EuiCopy>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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, { useMemo } from 'react';
|
||||
|
||||
import { EuiIcon, EuiToolTip } from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { SignificantTerm } from '@kbn/ml-agg-utils';
|
||||
|
||||
import { SEARCH_QUERY_LANGUAGE } from '../../application/utils/search_utils';
|
||||
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
|
||||
|
||||
import { getTableItemAsKQL } from './get_table_item_as_kql';
|
||||
import type { GroupTableItem, TableItemAction } from './types';
|
||||
|
||||
const viewInDiscoverMessage = i18n.translate(
|
||||
'xpack.aiops.spikeAnalysisTable.linksMenu.viewInDiscover',
|
||||
{
|
||||
defaultMessage: 'View in Discover',
|
||||
}
|
||||
);
|
||||
|
||||
export const useViewInDiscoverAction = (dataViewId?: string): TableItemAction => {
|
||||
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 (groupTableItem: GroupTableItem | SignificantTerm) => {
|
||||
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: getTableItemAsKQL(groupTableItem),
|
||||
},
|
||||
});
|
||||
|
||||
return url;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
name: () => (
|
||||
<EuiToolTip content={discoverUrlError ? discoverUrlError : viewInDiscoverMessage}>
|
||||
<EuiIcon type="discoverApp" />
|
||||
</EuiToolTip>
|
||||
),
|
||||
description: viewInDiscoverMessage,
|
||||
type: 'button',
|
||||
onClick: async (tableItem) => {
|
||||
const openInDiscoverUrl = await generateDiscoverUrl(tableItem);
|
||||
if (typeof openInDiscoverUrl === 'string') {
|
||||
await application.navigateToUrl(openInDiscoverUrl);
|
||||
}
|
||||
},
|
||||
enabled: () => discoverUrlError === undefined,
|
||||
};
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue