diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/components/kpis/container_kpi_charts.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/components/kpis/container_kpi_charts.tsx index c2f61b97257fc..95291d157745e 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/components/kpis/container_kpi_charts.tsx +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/components/kpis/container_kpi_charts.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiFlexItem, useEuiTheme } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; import type { DataView } from '@kbn/data-views-plugin/public'; import type { Filter, Query, TimeRange } from '@kbn/es-query'; import { Kpi } from './kpi'; @@ -79,10 +79,8 @@ const DockerKpiCharts = ({ searchSessionId, loading = false, }: ContainerKpiChartsProps) => { - const { euiTheme } = useEuiTheme(); const charts = useDockerContainerKpiCharts({ dataViewId: dataView?.id, - seriesColor: euiTheme.colors.lightestShade, }); return ( @@ -112,10 +110,8 @@ const KubernetesKpiCharts = ({ searchSessionId, loading = false, }: ContainerKpiChartsProps) => { - const { euiTheme } = useEuiTheme(); const charts = useK8sContainerKpiCharts({ dataViewId: dataView?.id, - seriesColor: euiTheme.colors.lightestShade, }); return ( diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/components/kpis/host_kpi_charts.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/components/kpis/host_kpi_charts.tsx index adccf59e519d5..64345efb1af8a 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/components/kpis/host_kpi_charts.tsx +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/components/kpis/host_kpi_charts.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiFlexItem, useEuiTheme } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; import type { DataView } from '@kbn/data-views-plugin/public'; import type { Filter, Query, TimeRange } from '@kbn/es-query'; import { Kpi } from './kpi'; @@ -31,11 +31,9 @@ export const HostKpiCharts = ({ searchSessionId, loading = false, }: HostKpiChartsProps) => { - const { euiTheme } = useEuiTheme(); const charts = useHostKpiCharts({ dataViewId: dataView?.id, getSubtitle, - seriesColor: euiTheme.colors.lightestShade, }); return ( diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_chart_series_color.test.ts b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_chart_series_color.test.ts new file mode 100644 index 0000000000000..7c58445a503db --- /dev/null +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_chart_series_color.test.ts @@ -0,0 +1,43 @@ +/* + * 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 { useEuiTheme } from '@elastic/eui'; +import { renderHook } from '@testing-library/react'; +import { useChartSeriesColor } from './use_chart_series_color'; + +describe('useChartSeriesColor', () => { + let seriesDefaultColor: string; + + beforeEach(() => { + const { result } = renderHook(() => useEuiTheme()); + + // Don't try to test a hardcoded value, just use what is provided by EUI. + // If in the future this value changes, the tests won't break. + seriesDefaultColor = result.current.euiTheme.colors.backgroundLightText; + }); + + it('returns a default color value if given no input', () => { + const { result } = renderHook(() => useChartSeriesColor()); + + expect(result.current).not.toBe(''); + expect(result.current).toBe(seriesDefaultColor); + }); + + it('returns a default color value if given an empty string', () => { + const { result } = renderHook(() => useChartSeriesColor('')); + + expect(result.current).not.toBe(''); + expect(result.current).toBe(seriesDefaultColor); + }); + + it('returns the provided color input', () => { + const { result } = renderHook(() => useChartSeriesColor('#fff')); + + expect(result.current).not.toBe(seriesDefaultColor); + expect(result.current).toBe('#fff'); + }); +}); diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_chart_series_color.ts b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_chart_series_color.ts new file mode 100644 index 0000000000000..d71408982226f --- /dev/null +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_chart_series_color.ts @@ -0,0 +1,21 @@ +/* + * 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 { useEuiTheme } from '@elastic/eui'; + +/** + * Provides either the input color, or yields the default EUI theme + * color for use as the KPI chart series color. + * @param seriesColor A user-defined color value + * @returns Either the input `seriesColor` or the default color from EUI + */ +export const useChartSeriesColor = (seriesColor?: string): string => { + const { euiTheme } = useEuiTheme(); + + // Prevent empty string being used as a valid color + return seriesColor || euiTheme.colors.backgroundLightText; +}; diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_container_metrics_charts.ts b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_container_metrics_charts.ts index cf1d5062a5c6c..c453725f3f527 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_container_metrics_charts.ts +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_container_metrics_charts.ts @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n'; import { findInventoryModel } from '@kbn/metrics-data-access-plugin/common'; import useAsync from 'react-use/lib/useAsync'; import { ContainerMetricTypes } from '../charts/types'; +import { useChartSeriesColor } from './use_chart_series_color'; const getSubtitleFromFormula = (value: string) => value.startsWith('max') @@ -106,6 +107,8 @@ export const useDockerContainerKpiCharts = ({ dataViewId?: string; seriesColor?: string; }) => { + seriesColor = useChartSeriesColor(seriesColor); + const { value: charts = [] } = useAsync(async () => { const model = findInventoryModel('container'); const { cpu, memory } = await model.metrics.getCharts(); @@ -134,6 +137,8 @@ export const useK8sContainerKpiCharts = ({ dataViewId?: string; seriesColor?: string; }) => { + seriesColor = useChartSeriesColor(seriesColor); + const { value: charts = [] } = useAsync(async () => { const model = findInventoryModel('container'); const { cpu, memory } = await model.metrics.getCharts(); diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_host_metrics_charts.ts b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_host_metrics_charts.ts index 57c9a5a0d7d42..ba3e3f973b35f 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_host_metrics_charts.ts +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_host_metrics_charts.ts @@ -10,6 +10,7 @@ import { findInventoryModel } from '@kbn/metrics-data-access-plugin/common'; import { useMemo } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { HostMetricTypes } from '../charts/types'; +import { useChartSeriesColor } from './use_chart_series_color'; export const useHostCharts = ({ metric, @@ -87,6 +88,8 @@ export const useHostKpiCharts = ({ seriesColor?: string; getSubtitle?: (formulaValue: string) => string; }) => { + seriesColor = useChartSeriesColor(seriesColor); + const { value: charts = [] } = useAsync(async () => { const model = findInventoryModel('host'); const { cpu, memory, disk } = await model.metrics.getCharts(); diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_log_charts.ts b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_log_charts.ts new file mode 100644 index 0000000000000..e373e214a63c2 --- /dev/null +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_log_charts.ts @@ -0,0 +1,77 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { useMemo } from 'react'; +import { LensConfig } from '@kbn/lens-embeddable-utils/config_builder'; +import { useChartSeriesColor } from './use_chart_series_color'; + +const LOG_RATE = i18n.translate('xpack.infra.assetDetails.charts.logRate', { + defaultMessage: 'Log Rate', +}); + +const LOG_ERROR_RATE = i18n.translate('xpack.infra.assetDetails.charts.logErrorRate', { + defaultMessage: 'Log Error Rate', +}); + +const logRateMetric: LensConfig & { id: string } = { + id: 'logMetric', + chartType: 'metric', + title: LOG_RATE, + label: LOG_RATE, + trendLine: true, + value: 'count()', + format: 'number', + decimals: 1, + normalizeByUnit: 's', +}; + +const logErrorRateMetric: LensConfig & { id: string } = { + id: 'logErrorMetric', + chartType: 'metric', + title: LOG_ERROR_RATE, + label: LOG_ERROR_RATE, + trendLine: true, + value: + 'count(kql=\'log.level: "error" OR log.level: "ERROR" OR error.log.level: "error" OR error.log.level: "ERROR"\')', + format: 'number', + decimals: 1, + normalizeByUnit: 's', +}; + +export const useLogsCharts = ({ + dataViewId, + seriesColor, +}: { + dataViewId?: string; + seriesColor?: string; +}) => { + seriesColor = useChartSeriesColor(seriesColor); + + return useMemo(() => { + const dataset = dataViewId && { + dataset: { + index: dataViewId, + }, + }; + + return { + charts: [ + { + ...logRateMetric, + ...dataset, + seriesColor, + }, + { + ...logErrorRateMetric, + ...dataset, + seriesColor, + }, + ], + }; + }, [dataViewId, seriesColor]); +}; diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_page_header.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_page_header.tsx index efd8c6028b032..aeb85617015e9 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_page_header.tsx +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_page_header.tsx @@ -26,6 +26,8 @@ import { LinkToNodeDetails } from '../links'; import { ContentTabIds, type LinkOptions, type Tab, type TabIds } from '../types'; import { useAssetDetailsRenderPropsContext } from './use_asset_details_render_props'; import { useTabSwitcherContext } from './use_tab_switcher'; +import { useEntitySummary } from './use_entity_summary'; +import { isMetricsSignal } from '../utils/get_data_stream_types'; type TabItem = NonNullable['tabs']>[number]; @@ -140,9 +142,31 @@ const useFeatureFlagTabs = () => { }; }; +const useMetricsTabs = () => { + const { asset } = useAssetDetailsRenderPropsContext(); + const { dataStreams } = useEntitySummary({ + entityType: asset.type, + entityId: asset.id, + }); + + const isMetrics = isMetricsSignal(dataStreams); + + const hasMetricsTab = useCallback( + (tabItem: Tab) => { + return isMetrics || tabItem.id !== ContentTabIds.METRICS; + }, + [isMetrics] + ); + + return { + hasMetricsTab, + }; +}; + const useTabs = (tabs: Tab[]) => { const { showTab, activeTabId } = useTabSwitcherContext(); const { isTabEnabled } = useFeatureFlagTabs(); + const { hasMetricsTab } = useMetricsTabs(); const onTabClick = useCallback( (tabId: TabIds) => { @@ -153,16 +177,18 @@ const useTabs = (tabs: Tab[]) => { const tabEntries: TabItem[] = useMemo( () => - tabs.filter(isTabEnabled).map(({ name, ...tab }) => { - return { - ...tab, - 'data-test-subj': `infraAssetDetails${capitalize(tab.id)}Tab`, - onClick: () => onTabClick(tab.id), - isSelected: tab.id === activeTabId, - label: name, - }; - }), - [activeTabId, isTabEnabled, onTabClick, tabs] + tabs + .filter((tab) => isTabEnabled(tab) && hasMetricsTab(tab)) + .map(({ name, ...tab }) => { + return { + ...tab, + 'data-test-subj': `infraAssetDetails${capitalize(tab.id)}Tab`, + onClick: () => onTabClick(tab.id), + isSelected: tab.id === activeTabId, + label: name, + }; + }), + [activeTabId, isTabEnabled, hasMetricsTab, onTabClick, tabs] ); return { tabEntries }; diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/overview/logs.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/overview/logs.tsx new file mode 100644 index 0000000000000..5ca334acf20ec --- /dev/null +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/overview/logs.tsx @@ -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 React, { useMemo } from 'react'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import type { TimeRange } from '@kbn/es-query'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { + findInventoryFields, + type InventoryItemType, +} from '@kbn/metrics-data-access-plugin/common'; +import { buildCombinedAssetFilter } from '../../../../utils/filters/build'; +import { useSearchSessionContext } from '../../../../hooks/use_search_session'; +import { useLogsCharts } from '../../hooks/use_log_charts'; +import { Kpi } from '../../components/kpis/kpi'; + +interface Props { + dataView?: DataView; + assetId: string; + assetType: InventoryItemType; + dateRange: TimeRange; +} + +export const LogsContent = ({ assetId, assetType, dataView, dateRange }: Props) => { + const { searchSessionId } = useSearchSessionContext(); + + const filters = useMemo(() => { + return [ + buildCombinedAssetFilter({ + field: findInventoryFields(assetType).id, + values: [assetId], + dataView, + }), + ]; + }, [dataView, assetId, assetType]); + + const { charts } = useLogsCharts({ + dataViewId: dataView?.id, + }); + + return ( + + {charts.map((chartProps, index) => ( + + + + ))} + + ); +}; diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/overview/overview.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/overview/overview.tsx index 9ace5606599d7..a25ee35cb75fa 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/overview/overview.tsx +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/overview/overview.tsx @@ -26,7 +26,8 @@ import { MetricsContent } from './metrics/metrics'; import { AddMetricsCallout } from '../../add_metrics_callout'; import { AddMetricsCalloutKey } from '../../add_metrics_callout/constants'; import { useEntitySummary } from '../../hooks/use_entity_summary'; -import { isMetricsSignal } from '../../utils/get_data_stream_types'; +import { isMetricsSignal, isLogsSignal } from '../../utils/get_data_stream_types'; +import { LogsContent } from './logs'; export const Overview = () => { const { dateRange } = useDatePickerContext(); @@ -36,7 +37,7 @@ export const Overview = () => { loading: metadataLoading, error: fetchMetadataError, } = useMetadataStateContext(); - const { metrics } = useDataViewsContext(); + const { metrics, logs } = useDataViewsContext(); const isFullPageView = renderMode.mode === 'page'; const { dataStreams, status: dataStreamsStatus } = useEntitySummary({ entityType: asset.type, @@ -59,6 +60,10 @@ export const Overview = () => { /> ); + const isMetrics = isMetricsSignal(dataStreams); + const isLogs = isLogsSignal(dataStreams); + const isLogsOnly = !isMetrics && isLogs; + const shouldShowCallout = () => { if ( dataStreamsStatus !== 'success' || @@ -68,14 +73,14 @@ export const Overview = () => { return false; } - return !isMetricsSignal(dataStreams); + return !isMetrics; }; const showAddMetricsCallout = shouldShowCallout(); return ( - {showAddMetricsCallout ? ( + {showAddMetricsCallout && ( { }} /> - ) : ( + )} + {isLogsOnly ? ( + + + + ) : null} + {!showAddMetricsCallout && isMetrics ? ( { /> {asset.type === 'host' ? : null} - )} + ) : null} {fetchMetadataError && !metadataLoading ? : metadataSummarySection} @@ -111,14 +127,16 @@ export const Overview = () => { ) : null} - - - + {isMetrics ? ( + + + + ) : null} ); }; diff --git a/x-pack/test/functional/apps/infra/node_details.ts b/x-pack/test/functional/apps/infra/node_details.ts index 0f1de8cb9c8bf..763652944bf93 100644 --- a/x-pack/test/functional/apps/infra/node_details.ts +++ b/x-pack/test/functional/apps/infra/node_details.ts @@ -671,16 +671,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - describe('Metrics Tab', () => { - before(async () => { - await pageObjects.assetDetails.clickMetricsTab(); - }); - - it('should show add metrics callout', async () => { - await pageObjects.assetDetails.addMetricsCalloutExists(); - }); - }); - describe('Processes Tab', () => { before(async () => { await pageObjects.assetDetails.clickProcessesTab();