mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
# Backport This will backport the following commits from `main` to `8.8`: - [[Infrastructure UI] Hosts view handle invalid KQL error (#156989)](https://github.com/elastic/kibana/pull/156989) <!--- Backport version: 8.9.7 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Carlos Crespo","email":"crespocarlos@users.noreply.github.com"},"sourceCommit":{"committedDate":"2023-05-11T16:59:23Z","message":"[Infrastructure UI] Hosts view handle invalid KQL error (#156989)\n\ncloses [#987](https://github.com/elastic/obs-infraobs-team/issues/987)\r\n\r\n## Summary\r\n\r\nThis PR changes the hosts view to graciously handle exceptions caused by\r\ninvalid KQL submissions\r\n\r\n<img width=\"1451\" alt=\"image\"\r\nsrc=\"5bafc987
-9a14-4b03-9038-53179f7b6735\">\r\n\r\n\r\nBesides, it changes the way it was handling a fatal error that can\r\nhappen if something wrong happens while creating an ad-hoc data-view -\r\nthis is highly unlike to happen, but we're standardizing how we display\r\nerrors on the hosts view\r\n\r\n_Previously:_\r\n<img width=\"1439\" alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/2767137/236833673-c994512f-cb73-441b-9783-506bab67ff4b.png\">\r\n\r\n_Now:_\r\n<img width=\"1439\" alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/2767137/236862216-fada9f50-5d27-45b9-a6f3-8ac497a3e048.png\">\r\n\r\n### How to test\r\n- Go to hosts view\r\n- Type invalid KQL expressions on the search bar\r\n\r\n### For reviewer\r\n\r\nIf the page is loaded with a querystring containing an invalid `query`\r\n(e.g:\r\n`_a=(dateRange:(from:now-15m,to:now),filters:!(),limit:20,panelFilters:!(),query:(language:kuery,query:%27%7D%27)`),\r\nthe Control components will show an error. However, they can't recover\r\nfrom fatal errors. So even after the user corrects the mistake in the\r\nquery, the controls will remain in the error state.\r\n\r\nA ticket has been opened to address this problem:\r\nhttps://github.com/elastic/kibana/issues/156430\r\n\r\n---------\r\n\r\nCo-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>","sha":"8baff25966d45c1b351e96a5cc524372b4dbdb29","branchLabelMapping":{"^v8.9.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Feature:Metrics UI","Team:Infra Monitoring UI","release_note:skip","backport:prev-minor","Feature:ObsHosts","v8.9.0"],"number":156989,"url":"https://github.com/elastic/kibana/pull/156989","mergeCommit":{"message":"[Infrastructure UI] Hosts view handle invalid KQL error (#156989)\n\ncloses [#987](https://github.com/elastic/obs-infraobs-team/issues/987)\r\n\r\n## Summary\r\n\r\nThis PR changes the hosts view to graciously handle exceptions caused by\r\ninvalid KQL submissions\r\n\r\n<img width=\"1451\" alt=\"image\"\r\nsrc=\"5bafc987
-9a14-4b03-9038-53179f7b6735\">\r\n\r\n\r\nBesides, it changes the way it was handling a fatal error that can\r\nhappen if something wrong happens while creating an ad-hoc data-view -\r\nthis is highly unlike to happen, but we're standardizing how we display\r\nerrors on the hosts view\r\n\r\n_Previously:_\r\n<img width=\"1439\" alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/2767137/236833673-c994512f-cb73-441b-9783-506bab67ff4b.png\">\r\n\r\n_Now:_\r\n<img width=\"1439\" alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/2767137/236862216-fada9f50-5d27-45b9-a6f3-8ac497a3e048.png\">\r\n\r\n### How to test\r\n- Go to hosts view\r\n- Type invalid KQL expressions on the search bar\r\n\r\n### For reviewer\r\n\r\nIf the page is loaded with a querystring containing an invalid `query`\r\n(e.g:\r\n`_a=(dateRange:(from:now-15m,to:now),filters:!(),limit:20,panelFilters:!(),query:(language:kuery,query:%27%7D%27)`),\r\nthe Control components will show an error. However, they can't recover\r\nfrom fatal errors. So even after the user corrects the mistake in the\r\nquery, the controls will remain in the error state.\r\n\r\nA ticket has been opened to address this problem:\r\nhttps://github.com/elastic/kibana/issues/156430\r\n\r\n---------\r\n\r\nCo-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>","sha":"8baff25966d45c1b351e96a5cc524372b4dbdb29"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v8.9.0","labelRegex":"^v8.9.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/156989","number":156989,"mergeCommit":{"message":"[Infrastructure UI] Hosts view handle invalid KQL error (#156989)\n\ncloses [#987](https://github.com/elastic/obs-infraobs-team/issues/987)\r\n\r\n## Summary\r\n\r\nThis PR changes the hosts view to graciously handle exceptions caused by\r\ninvalid KQL submissions\r\n\r\n<img width=\"1451\" alt=\"image\"\r\nsrc=\"5bafc987
-9a14-4b03-9038-53179f7b6735\">\r\n\r\n\r\nBesides, it changes the way it was handling a fatal error that can\r\nhappen if something wrong happens while creating an ad-hoc data-view -\r\nthis is highly unlike to happen, but we're standardizing how we display\r\nerrors on the hosts view\r\n\r\n_Previously:_\r\n<img width=\"1439\" alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/2767137/236833673-c994512f-cb73-441b-9783-506bab67ff4b.png\">\r\n\r\n_Now:_\r\n<img width=\"1439\" alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/2767137/236862216-fada9f50-5d27-45b9-a6f3-8ac497a3e048.png\">\r\n\r\n### How to test\r\n- Go to hosts view\r\n- Type invalid KQL expressions on the search bar\r\n\r\n### For reviewer\r\n\r\nIf the page is loaded with a querystring containing an invalid `query`\r\n(e.g:\r\n`_a=(dateRange:(from:now-15m,to:now),filters:!(),limit:20,panelFilters:!(),query:(language:kuery,query:%27%7D%27)`),\r\nthe Control components will show an error. However, they can't recover\r\nfrom fatal errors. So even after the user corrects the mistake in the\r\nquery, the controls will remain in the error state.\r\n\r\nA ticket has been opened to address this problem:\r\nhttps://github.com/elastic/kibana/issues/156430\r\n\r\n---------\r\n\r\nCo-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>","sha":"8baff25966d45c1b351e96a5cc524372b4dbdb29"}}]}] BACKPORT--> Co-authored-by: Carlos Crespo <crespocarlos@users.noreply.github.com>
This commit is contained in:
parent
9ac1170e09
commit
6593d2318f
13 changed files with 308 additions and 120 deletions
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButton,
|
||||
EuiEmptyPrompt,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiCodeBlock,
|
||||
} from '@elastic/eui';
|
||||
import { KQLSyntaxError } from '@kbn/es-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana';
|
||||
|
||||
interface Props {
|
||||
error: Error;
|
||||
titleOverride?: string;
|
||||
messageOverride?: string;
|
||||
hasDetailsModal?: boolean;
|
||||
hasTryAgainButton?: boolean;
|
||||
onTryAgainClick?: () => void;
|
||||
}
|
||||
|
||||
export const ErrorCallout = ({
|
||||
error,
|
||||
titleOverride,
|
||||
messageOverride,
|
||||
hasDetailsModal = false,
|
||||
hasTryAgainButton = false,
|
||||
onTryAgainClick,
|
||||
}: Props) => {
|
||||
const {
|
||||
services: { notifications },
|
||||
} = useKibanaContextForPlugin();
|
||||
|
||||
const errorContent = getErrorContent(error);
|
||||
const title = titleOverride ? titleOverride : errorContent.title;
|
||||
|
||||
const openDetails = () => {
|
||||
notifications.showErrorDialog({ title, error });
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="error"
|
||||
color="danger"
|
||||
title={<h2>{title}</h2>}
|
||||
data-test-subj="hostsViewErrorCallout"
|
||||
body={
|
||||
<>
|
||||
{messageOverride ? <p>{messageOverride}</p> : errorContent.body}
|
||||
<EuiFlexGroup justifyContent="center">
|
||||
{hasDetailsModal && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="hostsViewErrorDetailsButton"
|
||||
onClick={openDetails}
|
||||
color="danger"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.hostsViewPage.error.detailsButton"
|
||||
defaultMessage="Error details"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{hasTryAgainButton && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="hostsViewTryAgainButton"
|
||||
onClick={onTryAgainClick}
|
||||
iconType="refresh"
|
||||
color="danger"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.hostsViewPage.error.tryAgainButton"
|
||||
defaultMessage="Try again"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const getErrorContent = (error: Error): { title: string; body: JSX.Element } => {
|
||||
if (error instanceof KQLSyntaxError) {
|
||||
return {
|
||||
title: i18n.translate('xpack.infra.hostsViewPage.error.kqlErrorTitle', {
|
||||
defaultMessage: 'Invalid KQL expression',
|
||||
}),
|
||||
body: (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.hostsViewPage.error.kqlErrorMessage"
|
||||
defaultMessage="We can't show any results because we couldn't apply your filter."
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiCodeBlock transparentBackground paddingSize="s">
|
||||
{error.message}
|
||||
</EuiCodeBlock>
|
||||
</>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: i18n.translate('xpack.infra.hostsViewPage.error.unknownErrorTitle', {
|
||||
defaultMessage: 'An error occurred',
|
||||
}),
|
||||
body: <>{error.message}</>,
|
||||
};
|
||||
};
|
|
@ -5,24 +5,20 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { InfraLoadingPanel } from '../../../../components/loading';
|
||||
import { useMetricsDataViewContext } from '../hooks/use_data_view';
|
||||
import { UnifiedSearchBar } from './search_bar/unified_search_bar';
|
||||
import { HostsTable } from './hosts_table';
|
||||
import { KPIGrid } from './kpis/kpi_grid';
|
||||
import { Tabs } from './tabs/tabs';
|
||||
import { AlertsQueryProvider } from '../hooks/use_alerts_query';
|
||||
import { HostsViewProvider } from '../hooks/use_hosts_view';
|
||||
import { HostsTableProvider } from '../hooks/use_hosts_table';
|
||||
import { HostsContent } from './hosts_content';
|
||||
import { ErrorCallout } from './error_callout';
|
||||
|
||||
export const HostContainer = () => {
|
||||
const { dataView, loading, hasError } = useMetricsDataViewContext();
|
||||
const { dataView, loading, error, metricAlias, loadDataView } = useMetricsDataViewContext();
|
||||
|
||||
const isLoading = loading || !dataView;
|
||||
if (isLoading && !hasError) {
|
||||
if (isLoading && !error) {
|
||||
return (
|
||||
<InfraLoadingPanel
|
||||
height="100%"
|
||||
|
@ -34,27 +30,25 @@ export const HostContainer = () => {
|
|||
);
|
||||
}
|
||||
|
||||
return hasError ? null : (
|
||||
return error ? (
|
||||
<ErrorCallout
|
||||
error={error}
|
||||
titleOverride={i18n.translate('xpack.infra.hostsViewPage.errorOnCreateOrLoadDataviewTitle', {
|
||||
defaultMessage: 'Error creating Data View',
|
||||
})}
|
||||
messageOverride={i18n.translate('xpack.infra.hostsViewPage.errorOnCreateOrLoadDataview', {
|
||||
defaultMessage:
|
||||
'There was an error trying to create a Data View: {metricAlias}. Try reloading the page.',
|
||||
values: { metricAlias },
|
||||
})}
|
||||
onTryAgainClick={loadDataView}
|
||||
hasTryAgainButton
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<UnifiedSearchBar />
|
||||
<EuiSpacer />
|
||||
<HostsViewProvider>
|
||||
<HostsTableProvider>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<KPIGrid />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<HostsTable />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<AlertsQueryProvider>
|
||||
<Tabs />
|
||||
</AlertsQueryProvider>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</HostsTableProvider>
|
||||
</HostsViewProvider>
|
||||
<HostsContent />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
|
||||
import { HostsTable } from './hosts_table';
|
||||
import { KPIGrid } from './kpis/kpi_grid';
|
||||
import { Tabs } from './tabs/tabs';
|
||||
import { AlertsQueryProvider } from '../hooks/use_alerts_query';
|
||||
import { HostsViewProvider } from '../hooks/use_hosts_view';
|
||||
import { HostsTableProvider } from '../hooks/use_hosts_table';
|
||||
import { ErrorCallout } from './error_callout';
|
||||
import { useUnifiedSearchContext } from '../hooks/use_unified_search';
|
||||
|
||||
export const HostsContent = () => {
|
||||
const { error } = useUnifiedSearchContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
{error ? (
|
||||
<ErrorCallout error={error} hasDetailsModal />
|
||||
) : (
|
||||
<HostsViewProvider>
|
||||
<HostsTableProvider>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<KPIGrid />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<HostsTable />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<AlertsQueryProvider>
|
||||
<Tabs />
|
||||
</AlertsQueryProvider>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</HostsTableProvider>
|
||||
</HostsViewProvider>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
EuiButtonGroup,
|
||||
EuiButtonGroupOptionProps,
|
||||
|
@ -26,6 +26,11 @@ interface Props {
|
|||
}
|
||||
|
||||
export const LimitOptions = ({ limit, onChange }: Props) => {
|
||||
const [idSelected, setIdSelected] = useState(limit as number);
|
||||
const onSelected = (value: number) => {
|
||||
setIdSelected(value);
|
||||
onChange(value);
|
||||
};
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
|
@ -63,9 +68,9 @@ export const LimitOptions = ({ limit, onChange }: Props) => {
|
|||
legend={i18n.translate('xpack.infra.hostsViewPage.tabs.alerts.alertStatusFilter.legend', {
|
||||
defaultMessage: 'Filter by',
|
||||
})}
|
||||
idSelected={buildId(limit)}
|
||||
idSelected={buildId(idSelected)}
|
||||
options={options}
|
||||
onChange={(_, value: number) => onChange(value)}
|
||||
onChange={(_, value: number) => onSelected(value)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -6,7 +6,13 @@
|
|||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { compareFilters, COMPARE_ALL_OPTIONS, type Filter } from '@kbn/es-query';
|
||||
import {
|
||||
compareFilters,
|
||||
COMPARE_ALL_OPTIONS,
|
||||
type Query,
|
||||
type TimeRange,
|
||||
type Filter,
|
||||
} from '@kbn/es-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiFlexGrid,
|
||||
|
@ -21,7 +27,6 @@ import { useKibanaContextForPlugin } from '../../../../../hooks/use_kibana';
|
|||
import { useUnifiedSearchContext } from '../../hooks/use_unified_search';
|
||||
import { ControlsContent } from './controls_content';
|
||||
import { useMetricsDataViewContext } from '../../hooks/use_data_view';
|
||||
import { HostsSearchPayload } from '../../hooks/use_unified_search_url_state';
|
||||
import { LimitOptions } from './limit_options';
|
||||
import { HostLimitOptions } from '../../types';
|
||||
|
||||
|
@ -44,7 +49,7 @@ export const UnifiedSearchBar = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleRefresh = (payload: HostsSearchPayload, isUpdate?: boolean) => {
|
||||
const handleRefresh = (payload: { query?: Query; dateRange: TimeRange }, isUpdate?: boolean) => {
|
||||
// This makes sure `onQueryChange` is only called when the submit button is clicked
|
||||
if (isUpdate === false) {
|
||||
onSubmit(payload);
|
||||
|
|
|
@ -8,24 +8,20 @@
|
|||
import { useDataView } from './use_data_view';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { type KibanaReactContextValue, useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { coreMock, notificationServiceMock } from '@kbn/core/public/mocks';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import type { DataView, DataViewsServicePublic } from '@kbn/data-views-plugin/public';
|
||||
import type { InfraClientStartDeps } from '../../../../types';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
|
||||
jest.mock('@kbn/i18n');
|
||||
jest.mock('@kbn/kibana-react-plugin/public');
|
||||
|
||||
let dataViewMock: jest.Mocked<DataViewsServicePublic>;
|
||||
const useKibanaMock = useKibana as jest.MockedFunction<typeof useKibana>;
|
||||
const notificationMock = notificationServiceMock.createStartContract();
|
||||
const prop = { metricAlias: 'test' };
|
||||
|
||||
const mockUseKibana = () => {
|
||||
useKibanaMock.mockReturnValue({
|
||||
services: {
|
||||
...coreMock.createStart(),
|
||||
notifications: notificationMock,
|
||||
dataViews: dataViewMock,
|
||||
} as Partial<CoreStart> & Partial<InfraClientStartDeps>,
|
||||
} as unknown as KibanaReactContextValue<Partial<CoreStart> & Partial<InfraClientStartDeps>>);
|
||||
|
@ -43,34 +39,21 @@ const mockDataView = {
|
|||
describe('useDataView hook', () => {
|
||||
beforeEach(() => {
|
||||
dataViewMock = {
|
||||
create: jest.fn(),
|
||||
find: jest.fn(),
|
||||
create: jest.fn().mockImplementation(() => Promise.resolve(mockDataView)),
|
||||
} as Partial<DataViewsServicePublic> as jest.Mocked<DataViewsServicePublic>;
|
||||
|
||||
mockUseKibana();
|
||||
});
|
||||
|
||||
it('should create a new ad-hoc data view', async () => {
|
||||
dataViewMock.create.mockReturnValue(Promise.resolve(mockDataView));
|
||||
const { result, waitForNextUpdate } = renderHook(() => useDataView(prop));
|
||||
const { result, waitForNextUpdate } = renderHook(() => useDataView({ metricAlias: 'test' }));
|
||||
|
||||
await waitForNextUpdate();
|
||||
expect(result.current.loading).toEqual(false);
|
||||
expect(result.current.hasError).toEqual(false);
|
||||
expect(result.current.error).toBeUndefined();
|
||||
expect(result.current.dataView).toEqual(mockDataView);
|
||||
});
|
||||
|
||||
it('should display a toast when it fails to load the data view', async () => {
|
||||
dataViewMock.create.mockReturnValue(Promise.reject());
|
||||
const { result, waitForNextUpdate } = renderHook(() => useDataView(prop));
|
||||
|
||||
await waitForNextUpdate();
|
||||
expect(result.current.loading).toEqual(false);
|
||||
expect(result.current.hasError).toEqual(true);
|
||||
expect(result.current.dataView).toBeUndefined();
|
||||
expect(notificationMock.toasts.addDanger).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should create a dataview with unique id for metricAlias metrics', async () => {
|
||||
const { waitForNextUpdate } = renderHook(() => useDataView({ metricAlias: 'metrics' }));
|
||||
|
||||
|
|
|
@ -5,14 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { v5 as uuidv5 } from 'uuid';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import createContainer from 'constate';
|
||||
import type { DataView, DataViewSpec } from '@kbn/data-views-plugin/public';
|
||||
import { useTrackedPromise } from '../../../../utils/use_tracked_promise';
|
||||
import type { InfraClientStartDeps } from '../../../../types';
|
||||
import useAsyncRetry from 'react-use/lib/useAsyncRetry';
|
||||
import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana';
|
||||
import { DATA_VIEW_PREFIX, TIMESTAMP_FIELD } from '../constants';
|
||||
|
||||
export const generateDataViewId = (indexPattern: string) => {
|
||||
|
@ -22,60 +18,25 @@ export const generateDataViewId = (indexPattern: string) => {
|
|||
|
||||
export const useDataView = ({ metricAlias }: { metricAlias: string }) => {
|
||||
const {
|
||||
services: { dataViews, notifications },
|
||||
} = useKibana<InfraClientStartDeps>();
|
||||
services: { dataViews },
|
||||
} = useKibanaContextForPlugin();
|
||||
|
||||
const [dataView, setDataView] = useState<DataView>();
|
||||
const [hasError, setHasError] = useState<boolean>(false);
|
||||
|
||||
const [createAdhocDataViewRequest, createAdhocDataView] = useTrackedPromise(
|
||||
{
|
||||
createPromise: (config: DataViewSpec): Promise<DataView> => {
|
||||
return dataViews.create(config);
|
||||
},
|
||||
onResolve: (response: DataView) => {
|
||||
setDataView(response);
|
||||
setHasError(false);
|
||||
},
|
||||
onReject: () => {
|
||||
setHasError(true);
|
||||
},
|
||||
cancelPreviousOn: 'creation',
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const loading = useMemo(
|
||||
() =>
|
||||
createAdhocDataViewRequest.state === 'pending' ||
|
||||
createAdhocDataViewRequest.state === 'uninitialized',
|
||||
[createAdhocDataViewRequest.state]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
createAdhocDataView({
|
||||
const state = useAsyncRetry(() => {
|
||||
return dataViews.create({
|
||||
id: generateDataViewId(metricAlias),
|
||||
title: metricAlias,
|
||||
timeFieldName: TIMESTAMP_FIELD,
|
||||
});
|
||||
}, [createAdhocDataView, metricAlias]);
|
||||
}, [metricAlias]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasError && notifications) {
|
||||
notifications.toasts.addDanger(
|
||||
i18n.translate('xpack.infra.hostsViewPage.errorOnCreateOrLoadDataview', {
|
||||
defaultMessage: 'There was an error trying to create a Data View: {metricAlias}',
|
||||
values: { metricAlias },
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [hasError, notifications, metricAlias]);
|
||||
const { value, loading, error, retry } = state;
|
||||
|
||||
return {
|
||||
metricAlias,
|
||||
dataView,
|
||||
dataView: value,
|
||||
loading,
|
||||
hasError,
|
||||
loadDataView: retry,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -5,13 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import createContainer from 'constate';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import DateMath from '@kbn/datemath';
|
||||
import { buildEsQuery, type Query } from '@kbn/es-query';
|
||||
import { buildEsQuery, fromKueryExpression, type Query } from '@kbn/es-query';
|
||||
import { map, skip, startWith } from 'rxjs/operators';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import useEffectOnce from 'react-use/lib/useEffectOnce';
|
||||
import { useKibanaQuerySettings } from '../../../../utils/use_kibana_query_settings';
|
||||
import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana';
|
||||
import { telemetryTimeRangeFormatter } from '../../../../../common/formatters/telemetry_time_range';
|
||||
import { useMetricsDataViewContext } from './use_data_view';
|
||||
|
@ -48,9 +49,12 @@ const getDefaultTimestamps = () => {
|
|||
};
|
||||
|
||||
export const useUnifiedSearch = () => {
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [searchCriteria, setSearch] = useHostsUrlState();
|
||||
const { dataView } = useMetricsDataViewContext();
|
||||
const { services } = useKibanaContextForPlugin();
|
||||
const kibanaQuerySettings = useKibanaQuerySettings();
|
||||
|
||||
const {
|
||||
data: {
|
||||
query: {
|
||||
|
@ -62,7 +66,37 @@ export const useUnifiedSearch = () => {
|
|||
telemetry,
|
||||
} = services;
|
||||
|
||||
const onSubmit = (params?: HostsSearchPayload) => setSearch(params ?? {});
|
||||
const validateQuery = useCallback(
|
||||
(query: Query) => {
|
||||
fromKueryExpression(query.query, kibanaQuerySettings);
|
||||
},
|
||||
[kibanaQuerySettings]
|
||||
);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(params?: HostsSearchPayload) => {
|
||||
try {
|
||||
setError(null);
|
||||
/*
|
||||
/ Validates the Search Bar input values before persisting them in the state.
|
||||
/ Since the search can be triggered by components that are unaware of the Unified Search state (e.g Controls and Host Limit),
|
||||
/ this will always validates the query bar value, regardless of whether it's been sent in the current event or not.
|
||||
*/
|
||||
validateQuery(params?.query ?? (queryStringService.getQuery() as Query));
|
||||
setSearch(params ?? {});
|
||||
} catch (err) {
|
||||
/*
|
||||
/ Persists in the state the params so they can be used in case the query bar is fixed by the user.
|
||||
/ This is needed because the Unified Search observables are unnaware of the other componets in the search bar.
|
||||
/ Invalid query isn't persisted because it breaks the Control component
|
||||
*/
|
||||
const { query, ...validParams } = params ?? {};
|
||||
setSearch(validParams ?? {});
|
||||
setError(err);
|
||||
}
|
||||
},
|
||||
[queryStringService, setSearch, validateQuery]
|
||||
);
|
||||
|
||||
const getParsedDateRange = useCallback(() => {
|
||||
const defaults = getDefaultTimestamps();
|
||||
|
@ -84,21 +118,38 @@ export const useUnifiedSearch = () => {
|
|||
}, [getParsedDateRange]);
|
||||
|
||||
const buildQuery = useCallback(() => {
|
||||
return buildEsQuery(dataView, searchCriteria.query, [
|
||||
...searchCriteria.filters,
|
||||
...searchCriteria.panelFilters,
|
||||
]);
|
||||
}, [dataView, searchCriteria.query, searchCriteria.filters, searchCriteria.panelFilters]);
|
||||
return buildEsQuery(
|
||||
dataView,
|
||||
searchCriteria.query,
|
||||
[...searchCriteria.filters, ...searchCriteria.panelFilters],
|
||||
kibanaQuerySettings
|
||||
);
|
||||
}, [
|
||||
dataView,
|
||||
searchCriteria.query,
|
||||
searchCriteria.filters,
|
||||
searchCriteria.panelFilters,
|
||||
kibanaQuerySettings,
|
||||
]);
|
||||
|
||||
useEffectOnce(() => {
|
||||
// Sync filtersService from state
|
||||
// Sync filtersService from the URL state
|
||||
if (!deepEqual(filterManagerService.getFilters(), searchCriteria.filters)) {
|
||||
filterManagerService.setFilters(searchCriteria.filters);
|
||||
}
|
||||
// Sync queryService from state
|
||||
// Sync queryService from the URL state
|
||||
if (!deepEqual(queryStringService.getQuery(), searchCriteria.query)) {
|
||||
queryStringService.setQuery(searchCriteria.query);
|
||||
}
|
||||
|
||||
try {
|
||||
// Validates the "query" object from the URL state
|
||||
if (searchCriteria.query) {
|
||||
validateQuery(searchCriteria.query);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -117,12 +168,12 @@ export const useUnifiedSearch = () => {
|
|||
query: query$,
|
||||
})
|
||||
.pipe(skip(1))
|
||||
.subscribe(setSearch);
|
||||
.subscribe(onSubmit);
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [filterManagerService, setSearch, queryStringService, timeFilterService.timefilter]);
|
||||
}, [filterManagerService, onSubmit, queryStringService, timeFilterService.timefilter]);
|
||||
|
||||
// Track telemetry event on query/filter/date changes
|
||||
useEffect(() => {
|
||||
|
@ -133,6 +184,7 @@ export const useUnifiedSearch = () => {
|
|||
}, [getDateRangeAsTimestamp, searchCriteria, telemetry]);
|
||||
|
||||
return {
|
||||
error,
|
||||
buildQuery,
|
||||
onSubmit,
|
||||
getParsedDateRange,
|
||||
|
|
|
@ -17129,6 +17129,10 @@
|
|||
"xpack.infra.hostsPage.tellUsWhatYouThinkLink": "Dites-nous ce que vous pensez !",
|
||||
"xpack.infra.hostsViewPage.experimentalBadgeDescription": "Cette fonctionnalité est en version d'évaluation technique et pourra être modifiée ou retirée complètement dans une future version. Elastic s'efforcera au maximum de corriger tout problème, mais les fonctionnalités en version d'évaluation technique ne sont pas soumises aux accords de niveau de service d'assistance des fonctionnalités officielles en disponibilité générale.",
|
||||
"xpack.infra.hostsViewPage.experimentalBadgeLabel": "Version d'évaluation technique",
|
||||
"xpack.infra.hostsViewPage.error.kqlErrorTitle": "Expression KQL non valide",
|
||||
"xpack.infra.hostsViewPage.error.detailsButton": "Afficher les détails",
|
||||
"xpack.infra.hostsViewPage.error.tryAgainButton": "Réessayer",
|
||||
"xpack.infra.hostsViewPage.error.unknownErrorTitle": "Une erreur s'est produite",
|
||||
"xpack.infra.hostsViewPage.landing.calloutReachOutToYourKibanaAdministrator": "Votre rôle d'utilisateur ne dispose pas des privilèges suffisants pour activer cette fonctionnalité - veuillez \n contacter votre administrateur Kibana et lui demander de visiter cette page pour activer la fonctionnalité.",
|
||||
"xpack.infra.hostsViewPage.landing.enableHostsView": "Activer la vue des hôtes",
|
||||
"xpack.infra.hostsViewPage.landing.introMessage": "Présentation de notre nouvelle fonctionnalité \"Hôtes\", maintenant disponible dans la version d'évaluation technique !\n À l'aide de ce puissant outil, vous pouvez facilement afficher et analyser vos hôtes et identifier tout\n problème afin de le corriger rapidement. Obtenez une vue détaillée des indicateurs pour vos hôtes, regardez\n ceux qui déclenchent le plus d'alertes et filtrez les hôtes que vous souhaitez analyser\n à l'aide de tout filtre KQL et de répartitions simples telles que le fournisseur cloud et le système d'exploitation.",
|
||||
|
@ -37742,4 +37746,4 @@
|
|||
"xpack.painlessLab.title": "Painless Lab",
|
||||
"xpack.painlessLab.walkthroughButtonLabel": "Présentation"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17128,6 +17128,10 @@
|
|||
"xpack.infra.hostsPage.tellUsWhatYouThinkLink": "ご意見をお聞かせください。",
|
||||
"xpack.infra.hostsViewPage.experimentalBadgeDescription": "この機能はテクニカルプレビュー中であり、将来のリリースでは変更されたり完全に削除されたりする場合があります。Elasticは最善の努力を講じてすべての問題の修正に努めますが、テクニカルプレビュー中の機能には正式なGA機能のサポートSLAが適用されません。",
|
||||
"xpack.infra.hostsViewPage.experimentalBadgeLabel": "テクニカルプレビュー",
|
||||
"xpack.infra.hostsViewPage.error.kqlErrorTitle": "無効なKQL式",
|
||||
"xpack.infra.hostsViewPage.error.detailsButton": "詳細を表示",
|
||||
"xpack.infra.hostsViewPage.error.tryAgainButton": "再試行",
|
||||
"xpack.infra.hostsViewPage.error.unknownErrorTitle": "エラーが発生しました",
|
||||
"xpack.infra.hostsViewPage.landing.calloutReachOutToYourKibanaAdministrator": "ユーザーロールには、この機能を有効にするための十分な権限がありません。 \n この機能を有効にするために、Kibana管理者に連絡して、このページにアクセスするように依頼してください。",
|
||||
"xpack.infra.hostsViewPage.landing.enableHostsView": "ホストビューを有効化",
|
||||
"xpack.infra.hostsViewPage.landing.introMessage": "新機能「ホスト」のテクニカルプレビューを開始しました。\n この強力なツールを使えば、ホストを簡単に表示、分析し、あらゆる問題を特定して、\n 迅速に対処できます。ホストのメトリックの詳細ビューを表示します。\n 最も多くのアラートを発生させているホストを確認し、\n KQLフィルターと、クラウドプロバイダーやオペレーティングシステムなどの簡単な内訳を使用して、分析したいホストをフィルタリングします。",
|
||||
|
@ -37710,4 +37714,4 @@
|
|||
"xpack.painlessLab.title": "Painless Lab",
|
||||
"xpack.painlessLab.walkthroughButtonLabel": "実地検証"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17130,6 +17130,10 @@
|
|||
"xpack.infra.hostsPage.tellUsWhatYouThinkLink": "告诉我们您的看法!",
|
||||
"xpack.infra.hostsViewPage.experimentalBadgeDescription": "此功能处于技术预览状态,在未来版本中可能会更改或完全移除。Elastic 将尽最大努力来修复任何问题,但处于技术预览状态的功能不受正式 GA 功能支持 SLA 的约束。",
|
||||
"xpack.infra.hostsViewPage.experimentalBadgeLabel": "技术预览",
|
||||
"xpack.infra.hostsViewPage.error.kqlErrorTitle": "KQL 表达式无效",
|
||||
"xpack.infra.hostsViewPage.error.detailsButton": "查看详情",
|
||||
"xpack.infra.hostsViewPage.error.tryAgainButton": "重试",
|
||||
"xpack.infra.hostsViewPage.error.unknownErrorTitle": "发生错误",
|
||||
"xpack.infra.hostsViewPage.landing.calloutReachOutToYourKibanaAdministrator": "您的用户角色权限不足,无法启用此功能 - 请 \n 联系您的 Kibana 管理员,要求他们访问此页面以启用该功能。",
|
||||
"xpack.infra.hostsViewPage.landing.enableHostsView": "启用主机视图",
|
||||
"xpack.infra.hostsViewPage.landing.introMessage": "介绍目前在技术预览中可用的新“主机”功能!\n 使用这个强大的工具,您可以轻松查看并分析主机,并确定任何\n 问题以便快速予以解决。获取您主机的指标的详细视图,\n 查看哪些主机触发了最多告警,并使用任何 KQL 筛选\n 以及云提供商和操作系统等细目筛选您要分析的主机。",
|
||||
|
@ -37738,4 +37742,4 @@
|
|||
"xpack.painlessLab.title": "Painless 实验室",
|
||||
"xpack.painlessLab.walkthroughButtonLabel": "指导"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -544,6 +544,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
expect(cells.length).to.be(ALL_ALERTS * COLUMNS);
|
||||
});
|
||||
});
|
||||
|
||||
it('should show an error message when an invalid KQL is submitted', async () => {
|
||||
await pageObjects.infraHostsView.submitQuery('cloud.provider="gcp" A');
|
||||
await testSubjects.existOrFail('hostsViewErrorCallout');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pagination and Sorting', () => {
|
||||
|
|
|
@ -111,7 +111,7 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) {
|
|||
return testSubjects.find('hostsView-metricChart');
|
||||
},
|
||||
|
||||
// MetricsTtab
|
||||
// Metrics Tab
|
||||
async getMetricsTab() {
|
||||
return testSubjects.find('hostsView-tabs-metrics');
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue