diff --git a/x-pack/plugins/infra/public/hooks/use_http_request.tsx b/x-pack/plugins/infra/public/hooks/use_http_request.tsx index 041cd1ea0f66e..78ef969923992 100644 --- a/x-pack/plugins/infra/public/hooks/use_http_request.tsx +++ b/x-pack/plugins/infra/public/hooks/use_http_request.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { HttpHandler } from '@kbn/core/public'; import { ToastInput } from '@kbn/core/public'; @@ -83,7 +83,7 @@ export function useHTTPRequest( }; }, [abortable]); - const [request, makeRequest] = useTrackedPromise( + const [request, makeRequest, resetRequestState] = useTrackedPromise( { cancelPreviousOn: 'resolution', createPromise: () => { @@ -117,17 +117,13 @@ export function useHTTPRequest( [pathname, body, method, fetch, toast, onError] ); - const loading = useMemo(() => { - if (request.state === 'resolved' && response === null) { - return true; - } - return request.state === 'pending'; - }, [request.state, response]); + const loading = request.state === 'uninitialized' || request.state === 'pending'; return { response, error, loading, makeRequest, + resetRequestState, }; } diff --git a/x-pack/plugins/infra/public/observability_logs/log_stream_position_state/src/url_state_storage_service.ts b/x-pack/plugins/infra/public/observability_logs/log_stream_position_state/src/url_state_storage_service.ts index 219a2f0105e07..db81474594c48 100644 --- a/x-pack/plugins/infra/public/observability_logs/log_stream_position_state/src/url_state_storage_service.ts +++ b/x-pack/plugins/infra/public/observability_logs/log_stream_position_state/src/url_state_storage_service.ts @@ -37,7 +37,8 @@ export const updateContextInUrl = positionStateKey, positionStateInUrlRT.encode({ position: context.latestPosition ? pickTimeKey(context.latestPosition) : null, - }) + }), + { replace: true } ); }; diff --git a/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/url_state_storage_service.ts b/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/url_state_storage_service.ts index 3a8d08635dca7..5a6d20f4c9c12 100644 --- a/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/url_state_storage_service.ts +++ b/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/url_state_storage_service.ts @@ -88,7 +88,8 @@ export const updateContextInUrl = filters: context.filters, timeRange: context.timeRange, refreshInterval: context.refreshInterval, - }) + }), + { replace: true } ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/index.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/index.ts new file mode 100644 index 0000000000000..5b7cf282d6afd --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export * from './logs_tab_content'; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_link_to_stream.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_link_to_stream.tsx new file mode 100644 index 0000000000000..618d511b93cac --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_link_to_stream.tsx @@ -0,0 +1,60 @@ +/* + * 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 { stringify } from 'querystring'; +import { encode } from '@kbn/rison'; +import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useKibanaContextForPlugin } from '../../../../../../hooks/use_kibana'; + +interface LogsLinkToStreamProps { + startTimestamp: number; + endTimestamp: number; + query: string; +} + +export const LogsLinkToStream = ({ + startTimestamp, + endTimestamp, + query, +}: LogsLinkToStreamProps) => { + const { services } = useKibanaContextForPlugin(); + const { http } = services; + + const queryString = new URLSearchParams( + stringify({ + logPosition: encode({ + start: new Date(startTimestamp), + end: new Date(endTimestamp), + streamLive: false, + }), + logFilter: encode({ + kind: 'kuery', + expression: query, + }), + }) + ); + + const viewInLogsUrl = http.basePath.prepend(`/app/logs/stream?${queryString}`); + + return ( + + + + + + ); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_search_bar.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_search_bar.tsx new file mode 100644 index 0000000000000..eec446c42a688 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_search_bar.tsx @@ -0,0 +1,40 @@ +/* + * 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, { useCallback, useState } from 'react'; +import useDebounce from 'react-use/lib/useDebounce'; +import { i18n } from '@kbn/i18n'; +import { EuiFieldSearch } from '@elastic/eui'; +import { useLogsSearchUrlState } from '../../../hooks/use_logs_search_url_state'; + +const debounceIntervalInMs = 1000; + +export const LogsSearchBar = () => { + const [filterQuery, setFilterQuery] = useLogsSearchUrlState(); + const [searchText, setSearchText] = useState(filterQuery.query); + + const onQueryChange = useCallback((e: React.ChangeEvent) => { + setSearchText(e.target.value); + }, []); + + useDebounce(() => setFilterQuery({ ...filterQuery, query: searchText }), debounceIntervalInMs, [ + searchText, + ]); + + return ( + + ); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx new file mode 100644 index 0000000000000..0fad370960f22 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx @@ -0,0 +1,92 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { InfraLoadingPanel } from '../../../../../../components/loading'; +import { SnapshotNode } from '../../../../../../../common/http_api'; +import { LogStream } from '../../../../../../components/log_stream'; +import { useHostsViewContext } from '../../../hooks/use_hosts_view'; +import { useUnifiedSearchContext } from '../../../hooks/use_unified_search'; +import { useLogsSearchUrlState } from '../../../hooks/use_logs_search_url_state'; +import { LogsLinkToStream } from './logs_link_to_stream'; +import { LogsSearchBar } from './logs_search_bar'; +import { createHostsFilter } from '../../../utils'; + +export const LogsTabContent = () => { + const [filterQuery] = useLogsSearchUrlState(); + const { getDateRangeAsTimestamp } = useUnifiedSearchContext(); + const { from, to } = useMemo(() => getDateRangeAsTimestamp(), [getDateRangeAsTimestamp]); + const { hostNodes, loading } = useHostsViewContext(); + + const hostsFilterQuery = useMemo(() => createHostsFilter(hostNodes), [hostNodes]); + + const logsLinkToStreamQuery = useMemo(() => { + const hostsFilterQueryParam = createHostsFilterQueryParam(hostNodes); + + if (filterQuery.query && hostsFilterQueryParam) { + return `${filterQuery.query} and ${hostsFilterQueryParam}`; + } + + return filterQuery.query || hostsFilterQueryParam; + }, [filterQuery.query, hostNodes]); + + if (loading) { + return ( + + + + } + /> + + + ); + } + + return ( + + + + + + + + + + + + + + + ); +}; + +const createHostsFilterQueryParam = (hostNodes: SnapshotNode[]): string => { + if (!hostNodes.length) { + return ''; + } + + const joinedHosts = hostNodes.map((p) => p.name).join(' or '); + const hostsQueryParam = `host.name:(${joinedHosts})`; + + return hostsQueryParam; +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/tabs.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/tabs.tsx index 4abe6d4498900..3d7ee78723732 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/tabs.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/tabs.tsx @@ -14,6 +14,7 @@ import { AlertsTabContent } from './alerts'; import { AlertsTabBadge } from './alerts_tab_badge'; import { TabIds, useTabId } from '../../hooks/use_tab_id'; +import { LogsTabContent } from './logs'; const tabs = [ { @@ -23,6 +24,13 @@ const tabs = [ }), 'data-test-subj': 'hostsView-tabs-metrics', }, + { + id: TabIds.LOGS, + name: i18n.translate('xpack.infra.hostsViewPage.tabs.logs.title', { + defaultMessage: 'Logs', + }), + 'data-test-subj': 'hostsView-tabs-logs', + }, { id: TabIds.ALERTS, name: i18n.translate('xpack.infra.hostsViewPage.tabs.alerts.title', { @@ -63,6 +71,11 @@ export const Tabs = () => { )} + {renderedTabsSet.current.has(TabIds.LOGS) && ( + + )} {renderedTabsSet.current.has(TabIds.ALERTS) && (