[Infrastructure UI] Add metric charts to Overview tab (#161559)

closes [#160381](https://github.com/elastic/kibana/issues/160381)

## Summary

Adds metric charts to the asset details flyout and removes the metrics
tab components



6ae8aa9f-21dc-435d-a6c6-870e4469138a


This PR creates a context to store the state used by the tab components
and removes unused code

### How to test this PR
- Start a local Kibana instance
- Navigate to `Infrastructure` > `Hosts`
- Open the asset detail flyout

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Carlos Crespo 2023-07-17 14:58:06 +02:00 committed by GitHub
parent b157217f3d
commit 25ff25959d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 789 additions and 1319 deletions

View file

@ -13,6 +13,7 @@ import {
diskWriteThroughput,
diskSpaceAvailable,
diskSpaceUsage,
logRate,
normalizedLoad1m,
memoryUsage,
memoryFree,
@ -30,6 +31,7 @@ export const hostLensFormulas = {
diskSpaceAvailable,
diskSpaceUsage,
hostCount,
logRate,
normalizedLoad1m,
memoryUsage,
memoryFree,

View file

@ -13,6 +13,7 @@ export { diskWriteThroughput } from './disk_write_throughput';
export { diskSpaceAvailable } from './disk_space_available';
export { diskSpaceUsage } from './disk_space_usage';
export { hostCount } from './host_count';
export { logRate } from './log_rate';
export { normalizedLoad1m } from './normalized_load_1m';
export { memoryUsage } from './memory_usage';
export { memoryFree } from './memory_free';

View file

@ -0,0 +1,20 @@
/*
* 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 { FormulaConfig } from '../../../types';
export const logRate: FormulaConfig = {
label: 'Log Rate',
value: 'differences(cumulative_sum(count()))',
format: {
id: 'number',
params: {
decimals: 0,
},
},
timeScale: 's',
};

View file

@ -236,7 +236,7 @@ describe('lens_attributes_builder', () => {
layerId: 'layer_0',
layerType: 'data',
seriesType: 'line',
splitAccessor: 'aggs_breakdown',
splitAccessor: undefined,
xAccessor: 'x_date_histogram',
yConfig: [],
},
@ -292,7 +292,7 @@ describe('lens_attributes_builder', () => {
layerId: 'layer_0',
layerType: 'data',
seriesType: 'line',
splitAccessor: 'aggs_breakdown',
splitAccessor: undefined,
xAccessor: 'x_date_histogram',
yConfig: [],
},
@ -349,7 +349,7 @@ describe('lens_attributes_builder', () => {
layerId: 'layer_0',
layerType: 'data',
seriesType: 'line',
splitAccessor: 'aggs_breakdown',
splitAccessor: undefined,
xAccessor: 'x_date_histogram',
yConfig: [],
},

View file

@ -8,25 +8,34 @@ import React, { useEffect, useState, useRef, useCallback } from 'react';
import { Action } from '@kbn/ui-actions-plugin/public';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { TimeRange } from '@kbn/es-query';
import type { TimeRange } from '@kbn/es-query';
import { TypedLensByValueInput } from '@kbn/lens-plugin/public';
import { css } from '@emotion/react';
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
import { useIntersectedOnce } from '../../../hooks/use_intersection_once';
import { ChartLoader } from './chart_loader';
import type { LensAttributes } from '../types';
export interface LensWrapperProps
extends Pick<
TypedLensByValueInput,
'id' | 'overrides' | 'query' | 'filters' | 'style' | 'onBrushEnd' | 'onLoad' | 'disableTriggers'
> {
export type LensWrapperProps = Pick<
TypedLensByValueInput,
| 'id'
| 'filters'
| 'query'
| 'style'
| 'onBrushEnd'
| 'hidePanelTitles'
| 'overrides'
| 'hidePanelTitles'
| 'disabledActions'
| 'disableTriggers'
> & {
attributes: LensAttributes | null;
dateRange: TimeRange;
extraActions: Action[];
lastReloadRequestTime?: number;
loading?: boolean;
hasTitle?: boolean;
}
};
export const LensWrapper = React.memo(
({
@ -92,7 +101,14 @@ export const LensWrapper = React.memo(
}, [loadedOnce]);
return (
<div ref={intersectionRef}>
<div
ref={intersectionRef}
css={css`
.echLegend .echLegendList {
display: flex;
}
`}
>
<ChartLoader
loading={loading || !isReady}
loadedOnce={loadedOnce}

View file

@ -12,6 +12,7 @@ import type {
FormBasedPersistedState,
PersistedIndexPatternLayer,
XYDataLayerConfig,
SeriesType,
} from '@kbn/lens-plugin/public';
import type { ChartColumn, ChartLayer, FormulaConfig } from '../../../types';
import { getDefaultReferences, getHistogramColumn, getTopValuesColumn } from '../../utils';
@ -25,6 +26,7 @@ export interface XYLayerOptions {
size: number;
sourceField: string;
};
seriesType?: SeriesType;
}
interface XYLayerConfig {
@ -95,12 +97,12 @@ export class XYDataLayer implements ChartLayer<XYDataLayerConfig> {
getLayerConfig(layerId: string, accessorId: string): XYDataLayerConfig {
return {
layerId,
seriesType: 'line',
seriesType: this.layerConfig.options?.seriesType ?? 'line',
accessors: this.column.map((_, index) => `${accessorId}_${index}`),
yConfig: [],
layerType: 'data',
xAccessor: HISTOGRAM_COLUMN_NAME,
splitAccessor: BREAKDOWN_COLUMN_NAME,
splitAccessor: this.layerConfig.options?.breakdown ? BREAKDOWN_COLUMN_NAME : undefined,
};
}
}

View file

@ -13,8 +13,8 @@ import type {
PersistedIndexPatternLayer,
TypedLensByValueInput,
XYState,
XYDataLayerConfig,
FormulaPublicApi,
XYLayerConfig,
} from '@kbn/lens-plugin/public';
import { hostLensFormulas } from './constants';
export type LensAttributes = TypedLensByValueInput['attributes'];
@ -37,7 +37,7 @@ export interface ChartColumn {
}
// Layer
export type LensLayerConfig = XYDataLayerConfig | MetricVisualizationState;
export type LensLayerConfig = XYLayerConfig | MetricVisualizationState;
export interface ChartLayer<TLayerConfig extends LensLayerConfig> {
getName(): string | undefined;
@ -69,12 +69,11 @@ export interface ChartConfig<
// Formula
type LensFormula = Parameters<FormulaPublicApi['insertOrReplaceFormulaColumn']>[1];
export interface FormulaConfig {
label?: string;
export type FormulaConfig = Omit<LensFormula, 'format' | 'formula'> & {
color?: string;
format: NonNullable<LensFormula['format']>;
value: string;
}
};
export type HostsLensFormulas = keyof typeof hostLensFormulas;
export type HostsLensMetricChartFormulas = Exclude<HostsLensFormulas, 'diskIORead' | 'diskIOWrite'>;

View file

@ -25,13 +25,6 @@ const tabs: Tab[] = [
}),
'data-test-subj': 'hostsView-flyout-tabs-overview',
},
{
id: FlyoutTabIds.METRICS,
name: i18n.translate('xpack.infra.nodeDetails.tabs.metrics', {
defaultMessage: 'Metrics',
}),
'data-test-subj': 'hostsView-flyout-tabs-metrics',
},
{
id: FlyoutTabIds.LOGS,
name: i18n.translate('xpack.infra.nodeDetails.tabs.logs', {
@ -85,40 +78,27 @@ const stories: Meta<AssetDetailsProps> = {
node: {
name: 'host1',
id: 'host1-macOS',
title: {
name: 'host1',
cloudProvider: null,
},
os: 'macOS',
ip: '192.168.0.1',
rx: 123179.18222222221,
tx: 123030.54555555557,
memory: 0.9044444444444445,
cpu: 0.3979674157303371,
diskSpaceUsage: 0.3979674157303371,
normalizedLoad1m: 0.15291777273162221,
memoryFree: 34359738368,
},
overrides: {
overview: {
dataView: {
metricsDataView: {
id: 'default',
getFieldByName: () => 'hostname' as unknown as DataViewField,
} as unknown as DataView,
logsDataView: {
id: 'default',
getFieldByName: () => 'hostname' as unknown as DataViewField,
} as unknown as DataView,
dateRange: {
from: '168363046800',
to: '168363046900',
},
},
metadata: {
showActionsColumn: true,
},
},
nodeType: 'host',
currentTimeRange: {
interval: '1s',
from: 168363046800,
to: 168363046900,
dateRange: {
from: '2023-04-09T11:07:49Z',
to: '2023-04-09T11:23:49Z',
},
tabs,
links,

View file

@ -11,6 +11,7 @@ import type { AssetDetailsProps, RenderMode } from './types';
import { Content } from './content/content';
import { Header } from './header/header';
import { TabSwitcherProvider } from './hooks/use_tab_switcher';
import { AssetDetailsStateProvider } from './hooks/use_asset_details_state';
interface ContentTemplateProps {
header: React.ReactElement;
@ -34,7 +35,7 @@ const ContentTemplate = ({ header, body, renderMode }: ContentTemplateProps) =>
export const AssetDetails = ({
node,
currentTimeRange,
dateRange,
activeTabId,
overrides,
onTabsStateChange,
@ -46,34 +47,17 @@ export const AssetDetails = ({
},
}: AssetDetailsProps) => {
return (
<TabSwitcherProvider
initialActiveTabId={tabs.length > 0 ? activeTabId ?? tabs[0].id : undefined}
onTabsStateChange={onTabsStateChange}
>
<ContentTemplate
header={
<Header
node={node}
nodeType={nodeType}
currentTimeRange={currentTimeRange}
compact={renderMode.showInFlyout}
tabs={tabs}
links={links}
overrides={overrides}
/>
}
body={
<Content
node={node}
nodeType={nodeType}
currentTimeRange={currentTimeRange}
overrides={overrides}
onTabsStateChange={onTabsStateChange}
/>
}
renderMode={renderMode}
/>
</TabSwitcherProvider>
<AssetDetailsStateProvider state={{ node, nodeType, overrides, onTabsStateChange, dateRange }}>
<TabSwitcherProvider
initialActiveTabId={tabs.length > 0 ? activeTabId ?? tabs[0].id : undefined}
>
<ContentTemplate
header={<Header compact={renderMode.showInFlyout} tabs={tabs} links={links} />}
body={<Content />}
renderMode={renderMode}
/>
</TabSwitcherProvider>
</AssetDetailsStateProvider>
);
};

View file

@ -72,7 +72,7 @@ export class AssetDetailsEmbeddable extends Embeddable<AssetDetailsEmbeddableInp
<div style={{ width: '100%' }}>
<LazyAssetDetailsWrapper
activeTabId={this.input.activeTabId}
currentTimeRange={this.input.currentTimeRange}
dateRange={this.input.dateRange}
node={this.input.node}
nodeType={this.input.nodeType}
overrides={this.input.overrides}

View file

@ -6,22 +6,15 @@
*/
import React from 'react';
import { useAssetDetailsStateContext } from '../hooks/use_asset_details_state';
import { useTabSwitcherContext } from '../hooks/use_tab_switcher';
import { Anomalies, Metadata, Processes, Osquery, Metrics, Logs, Overview } from '../tabs';
import { FlyoutTabIds, type TabState, type AssetDetailsProps } from '../types';
import { Anomalies, Metadata, Processes, Osquery, Logs, Overview } from '../tabs';
import { FlyoutTabIds, type TabState } from '../types';
import { toTimestampRange } from '../utils';
type Props = Pick<
AssetDetailsProps,
'currentTimeRange' | 'node' | 'nodeType' | 'overrides' | 'onTabsStateChange'
>;
export const Content = () => {
const { node, nodeType, overrides, dateRange, onTabsStateChange } = useAssetDetailsStateContext();
export const Content = ({
overrides,
currentTimeRange,
node,
onTabsStateChange,
nodeType = 'host',
}: Props) => {
const onChange = (state: TabState) => {
if (!onTabsStateChange) {
return;
@ -30,6 +23,7 @@ export const Content = ({
onTabsStateChange(state);
};
const dateRangeTs = toTimestampRange(dateRange);
return (
<>
<TabPanel activeWhen={FlyoutTabIds.ANOMALIES}>
@ -37,18 +31,18 @@ export const Content = ({
</TabPanel>
<TabPanel activeWhen={FlyoutTabIds.OVERVIEW}>
<Overview
currentTimeRange={currentTimeRange}
dateRange={dateRange}
nodeName={node.name}
nodeType={nodeType}
dataView={overrides?.overview?.dataView}
dateRange={overrides?.overview?.dateRange}
metricsDataView={overrides?.overview?.metricsDataView}
logsDataView={overrides?.overview?.logsDataView}
/>
</TabPanel>
<TabPanel activeWhen={FlyoutTabIds.LOGS}>
<Logs
nodeName={node.name}
nodeType={nodeType}
currentTime={currentTimeRange.to}
currentTimestamp={dateRangeTs.to}
logViewReference={overrides?.logs?.logView?.reference}
logViewLoading={overrides?.logs?.logView?.loading}
search={overrides?.logs?.query}
@ -57,7 +51,7 @@ export const Content = ({
</TabPanel>
<TabPanel activeWhen={FlyoutTabIds.METADATA}>
<Metadata
currentTimeRange={currentTimeRange}
dateRange={dateRange}
nodeName={node.name}
nodeType={nodeType}
showActionsColumn={overrides?.metadata?.showActionsColumn}
@ -65,24 +59,14 @@ export const Content = ({
onSearchChange={(query) => onChange({ metadata: { query } })}
/>
</TabPanel>
<TabPanel activeWhen={FlyoutTabIds.METRICS}>
<Metrics
currentTime={currentTimeRange.to}
accountId={overrides?.metrics?.accountId}
customMetrics={overrides?.metrics?.customMetrics}
region={overrides?.metrics?.region}
nodeId={node.id}
nodeType={nodeType}
/>
</TabPanel>
<TabPanel activeWhen={FlyoutTabIds.OSQUERY}>
<Osquery nodeName={node.name} nodeType={nodeType} currentTimeRange={currentTimeRange} />
<Osquery nodeName={node.name} nodeType={nodeType} dateRange={dateRange} />
</TabPanel>
<TabPanel activeWhen={FlyoutTabIds.PROCESSES}>
<Processes
nodeName={node.name}
nodeType={nodeType}
currentTime={currentTimeRange.to}
currentTimestamp={dateRangeTs.to}
search={overrides?.processes?.query}
onSearchFilterChange={(query) => onChange({ processes: { query } })}
/>

View file

@ -26,25 +26,17 @@ import {
TabToUptime,
} from '../links';
import { useTabSwitcherContext } from '../hooks/use_tab_switcher';
import { useAssetDetailsStateContext } from '../hooks/use_asset_details_state';
import { toTimestampRange } from '../utils';
type Props = Pick<
AssetDetailsProps,
'currentTimeRange' | 'overrides' | 'node' | 'nodeType' | 'links' | 'tabs'
> & {
type Props = Pick<AssetDetailsProps, 'links' | 'tabs'> & {
compact: boolean;
};
const APM_FIELD = 'host.hostname';
export const Header = ({
nodeType = 'host',
node,
tabs = [],
links = [],
compact,
currentTimeRange,
overrides,
}: Props) => {
export const Header = ({ tabs = [], links = [], compact }: Props) => {
const { node, nodeType, overrides, dateRange: timeRange } = useAssetDetailsStateContext();
const { euiTheme } = useEuiTheme();
const { showTab, activeTabId } = useTabSwitcherContext();
@ -66,7 +58,7 @@ export const Header = ({
<LinkToNodeDetails
nodeName={node.name}
nodeType={nodeType}
currentTime={currentTimeRange.to}
currentTimestamp={toTimestampRange(timeRange).to}
/>
),
alertRule: <LinkToAlertsRule onClick={overrides?.alertRule?.onCreateRuleClick} />,

View file

@ -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 createContainer from 'constate';
import { useMemo } from 'react';
import { parseDateRange } from '../../../utils/datemath';
import type { AssetDetailsProps } from '../types';
const DEFAULT_DATE_RANGE = {
from: 'now-15m',
to: 'now',
};
export interface UseAssetDetailsStateProps {
state: Pick<
AssetDetailsProps,
'node' | 'nodeType' | 'overrides' | 'dateRange' | 'onTabsStateChange'
>;
}
export function useAssetDetailsState({ state }: UseAssetDetailsStateProps) {
const { node, nodeType, dateRange: rawDateRange, onTabsStateChange, overrides } = state;
const dateRange = useMemo(() => {
const { from = DEFAULT_DATE_RANGE.from, to = DEFAULT_DATE_RANGE.to } =
parseDateRange(rawDateRange);
return { from, to };
}, [rawDateRange]);
return {
node,
nodeType,
dateRange,
onTabsStateChange,
overrides,
};
}
export const AssetDetailsState = createContainer(useAssetDetailsState);
export const [AssetDetailsStateProvider, useAssetDetailsStateContext] = AssetDetailsState;

View file

@ -12,7 +12,6 @@ import { pipe } from 'fp-ts/lib/pipeable';
import { useHTTPRequest } from '../../../hooks/use_http_request';
import { type InfraMetadata, InfraMetadataRT } from '../../../../common/http_api/metadata_api';
import { throwErrors, createPlainError } from '../../../../common/runtime_types';
import type { MetricsTimeInput } from '../../../pages/metrics/metric_detail/hooks/use_metrics_time';
import { getFilteredMetrics } from '../../../pages/metrics/metric_detail/lib/get_filtered_metrics';
import type { InventoryItemType, InventoryMetric } from '../../../../common/inventory_models/types';
@ -21,7 +20,10 @@ export function useMetadata(
nodeType: InventoryItemType,
requiredMetrics: InventoryMetric[],
sourceId: string,
timeRange: MetricsTimeInput
timeRange: {
from: number;
to: number;
}
) {
const decodeResponse = (response: any) => {
return pipe(InfraMetadataRT.decode(response), fold(throwErrors(createPlainError), identity));
@ -33,7 +35,7 @@ export function useMetadata(
nodeId,
nodeType,
sourceId,
timeRange: { from: timeRange.from, to: timeRange.to },
timeRange,
}),
decodeResponse
);

View file

@ -8,14 +8,15 @@
import { useState } from 'react';
import createContainer from 'constate';
import { useLazyRef } from '../../../hooks/use_lazy_ref';
import type { TabIds, TabsStateChangeFn } from '../types';
import type { TabIds } from '../types';
import { useAssetDetailsStateContext } from './use_asset_details_state';
interface TabSwitcherParams {
initialActiveTabId?: TabIds;
onTabsStateChange?: TabsStateChangeFn;
}
export function useTabSwitcher({ initialActiveTabId, onTabsStateChange }: TabSwitcherParams) {
export function useTabSwitcher({ initialActiveTabId }: TabSwitcherParams) {
const { onTabsStateChange } = useAssetDetailsStateContext();
const [activeTabId, setActiveTabId] = useState<TabIds | undefined>(initialActiveTabId);
// This set keeps track of which tabs content have been rendered the first time.

View file

@ -13,21 +13,25 @@ import { findInventoryModel } from '../../../../common/inventory_models';
import type { InventoryItemType } from '../../../../common/inventory_models/types';
export interface LinkToNodeDetailsProps {
currentTime: number;
currentTimestamp: number;
nodeName: string;
nodeType: InventoryItemType;
}
export const LinkToNodeDetails = ({ nodeName, nodeType, currentTime }: LinkToNodeDetailsProps) => {
export const LinkToNodeDetails = ({
nodeName,
nodeType,
currentTimestamp,
}: LinkToNodeDetailsProps) => {
const inventoryModel = findInventoryModel(nodeType);
const nodeDetailFrom = currentTime - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000;
const nodeDetailFrom = currentTimestamp - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000;
const nodeDetailMenuItemLinkProps = useLinkProps({
...getNodeDetailUrl({
nodeType,
nodeId: nodeName,
from: nodeDetailFrom,
to: currentTime,
to: currentTimestamp,
}),
});

View file

@ -9,6 +9,5 @@ export { Anomalies } from './anomalies/anomalies';
export { Metadata } from './metadata/metadata';
export { Processes } from './processes/processes';
export { Osquery } from './osquery/osquery';
export { Metrics } from './metrics/metrics';
export { Logs } from './logs/logs';
export { Overview } from './overview/overview';

View file

@ -19,7 +19,7 @@ import { findInventoryFields } from '../../../../../common/inventory_models';
import { InfraLoadingPanel } from '../../../loading';
export interface LogsProps {
currentTime: number;
currentTimestamp: number;
logViewReference?: LogViewReference | null;
logViewLoading?: boolean;
nodeName: string;
@ -32,7 +32,7 @@ const TEXT_QUERY_THROTTLE_INTERVAL_MS = 500;
export const Logs = ({
nodeName,
currentTime,
currentTimestamp,
nodeType,
logViewReference,
search,
@ -43,7 +43,7 @@ export const Logs = ({
const { locators } = services;
const [textQuery, setTextQuery] = useState(search ?? '');
const [textQueryDebounced, setTextQueryDebounced] = useState(search ?? '');
const startTimestamp = currentTime - 60 * 60 * 1000; // 60 minutes
const startTimestamp = currentTimestamp - 60 * 60 * 1000; // 60 minutes
useDebounce(
() => {
@ -137,7 +137,7 @@ export const Logs = ({
<LogStream
logView={logView}
startTimestamp={startTimestamp}
endTimestamp={currentTime}
endTimestamp={currentTimestamp}
query={filter}
height="60vh"
showFlyoutAction

View file

@ -1,18 +0,0 @@
/*
* 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 { EuiLoadingSpinner } from '@elastic/eui';
import type { MetadataProps } from './metadata';
const Metadata = React.lazy(() => import('./metadata'));
export const LazyMetadataWrapper = (props: MetadataProps) => (
<React.Suspense fallback={<EuiLoadingSpinner />}>
<Metadata {...props} />
</React.Suspense>
);

View file

@ -16,10 +16,9 @@ const stories: Meta<MetadataProps> = {
decorators: [decorateWithGlobalStorybookThemeProviders, DecorateWithKibanaContext],
component: Metadata,
args: {
currentTimeRange: {
from: 1679316685686,
to: 1679585836087,
interval: '1m',
dateRange: {
from: '2023-04-09T11:07:49Z',
to: '2023-04-09T11:23:49Z',
},
nodeType: 'host',
nodeName: 'host-1',

View file

@ -18,10 +18,9 @@ jest.mock('../../../../containers/metrics_source');
jest.mock('../../hooks/use_metadata');
const metadataProps: MetadataProps = {
currentTimeRange: {
from: 1679316685686,
to: 1679585836087,
interval: '1m',
dateRange: {
from: '2023-04-09T11:07:49Z',
to: '2023-04-09T11:23:49Z',
},
nodeType: 'host',
nodeName: 'host-1',

View file

@ -9,13 +9,14 @@ import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiCallOut, EuiLink } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { TimeRange } from '@kbn/es-query';
import type { InventoryItemType } from '../../../../../common/inventory_models/types';
import { findInventoryModel } from '../../../../../common/inventory_models';
import type { MetricsTimeInput } from '../../../../pages/metrics/metric_detail/hooks/use_metrics_time';
import { useMetadata } from '../../hooks/use_metadata';
import { useSourceContext } from '../../../../containers/metrics_source';
import { Table } from './table';
import { getAllFields } from './utils';
import { toTimestampRange } from '../../utils';
export interface MetadataSearchUrlState {
metadataSearchUrlState: string;
@ -23,7 +24,7 @@ export interface MetadataSearchUrlState {
}
export interface MetadataProps {
currentTimeRange: MetricsTimeInput;
dateRange: TimeRange;
nodeName: string;
nodeType: InventoryItemType;
showActionsColumn?: boolean;
@ -33,7 +34,7 @@ export interface MetadataProps {
export const Metadata = ({
nodeName,
currentTimeRange,
dateRange,
nodeType,
search,
showActionsColumn = false,
@ -45,7 +46,13 @@ export const Metadata = ({
loading: metadataLoading,
error: fetchMetadataError,
metadata,
} = useMetadata(nodeName, nodeType, inventoryModel.requiredMetrics, sourceId, currentTimeRange);
} = useMetadata(
nodeName,
nodeType,
inventoryModel.requiredMetrics,
sourceId,
toTimestampRange(dateRange)
);
const fields = useMemo(() => getAllFields(metadata), [metadata]);

View file

@ -1,87 +0,0 @@
/*
* 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 { CoreStart } from '@kbn/core/public';
import React from 'react';
import ReactDOM from 'react-dom';
import { Subscription } from 'rxjs';
import { Embeddable, EmbeddableInput, IContainer } from '@kbn/embeddable-plugin/public';
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
import { CoreProviders } from '../../../../apps/common_providers';
import { InfraClientStartDeps, InfraClientStartExports } from '../../../../types';
import { LazyMetadataWrapper } from './lazy_metadata_wrapper';
import type { MetadataProps } from './metadata';
export const METADATA_EMBEDDABLE = 'METADATA_EMBEDDABLE';
export interface MetadataEmbeddableInput extends EmbeddableInput, MetadataProps {}
export class MetadataEmbeddable extends Embeddable<MetadataEmbeddableInput> {
public readonly type = METADATA_EMBEDDABLE;
private node?: HTMLElement;
private subscription: Subscription;
constructor(
private core: CoreStart,
private pluginDeps: InfraClientStartDeps,
private pluginStart: InfraClientStartExports,
initialInput: MetadataEmbeddableInput,
parent?: IContainer
) {
super(initialInput, {}, parent);
this.subscription = this.getInput$().subscribe(() => this.renderComponent());
}
public render(node: HTMLElement) {
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
}
this.node = node;
this.renderComponent();
}
public destroy() {
super.destroy();
this.subscription.unsubscribe();
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
}
}
public async reload() {}
private renderComponent() {
if (!this.node) {
return;
}
ReactDOM.render(
<CoreProviders
core={this.core}
plugins={this.pluginDeps}
pluginStart={this.pluginStart}
theme$={this.core.theme.theme$}
>
<EuiThemeProvider>
<div style={{ width: '100%' }}>
<LazyMetadataWrapper
currentTimeRange={this.input.currentTimeRange}
nodeName={this.input.nodeName}
nodeType={this.input.nodeType}
showActionsColumn={this.input.showActionsColumn}
onSearchChange={this.input.onSearchChange}
search={this.input.search}
/>
</div>
</EuiThemeProvider>
</CoreProviders>,
this.node
);
}
}

View file

@ -1,56 +0,0 @@
/*
* 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 type { EmbeddableFactoryDefinition, IContainer } from '@kbn/embeddable-plugin/public';
import type { InfraClientStartServicesAccessor } from '../../../../types';
import {
MetadataEmbeddable,
MetadataEmbeddableInput,
METADATA_EMBEDDABLE,
} from './metadata_embeddable';
export class MetadataEmbeddableFactoryDefinition
implements EmbeddableFactoryDefinition<MetadataEmbeddableInput>
{
public readonly type = METADATA_EMBEDDABLE;
constructor(private getStartServices: InfraClientStartServicesAccessor) {}
public async isEditable() {
return false;
}
public async create(initialInput: MetadataEmbeddableInput, parent?: IContainer) {
const [core, plugins, pluginStart] = await this.getStartServices();
return new MetadataEmbeddable(core, plugins, pluginStart, initialInput, parent);
}
public getDisplayName() {
return i18n.translate('xpack.infra.metadataEmbeddable.displayName', {
defaultMessage: 'Metadata',
});
}
public getDescription() {
return i18n.translate('xpack.infra.metadataEmbeddable.description', {
defaultMessage: 'Add a table of asset metadata.',
});
}
public getIconType() {
return 'metricsApp';
}
public async getExplicitInput() {
return {
title: i18n.translate('xpack.infra.metadataEmbeddable.title', {
defaultMessage: 'Metadata',
}),
};
}
}

View file

@ -1,61 +0,0 @@
/*
* 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 { EuiText } from '@elastic/eui';
import { EuiFlexItem, EuiFlexGroup, EuiIcon } from '@elastic/eui';
import { euiStyled } from '@kbn/kibana-react-plugin/common';
import { colorTransformer } from '../../../../../common/color_palette';
import type { MetricsExplorerOptionsMetric } from '../../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options';
interface Props {
title: string;
metrics: MetricsExplorerOptionsMetric[];
}
export const ChartHeader = ({ title, metrics }: Props) => {
return (
<EuiFlexGroup gutterSize="s" responsive={false}>
<HeaderItem grow={1}>
<EuiText size="s">
<H4>{title}</H4>
</EuiText>
</HeaderItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
{metrics.map((chartMetric) => (
<EuiFlexItem key={chartMetric.label!}>
<EuiFlexGroup
key={chartMetric.label!}
gutterSize="xs"
alignItems="center"
responsive={false}
>
<EuiFlexItem grow={false}>
<EuiIcon color={colorTransformer(chartMetric.color!)} type="dot" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="xs">{chartMetric.label}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
))}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
);
};
const HeaderItem = euiStyled(EuiFlexItem).attrs({ grow: 1 })`
overflow: hidden;
`;
const H4 = euiStyled('h4')`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;

View file

@ -1,99 +0,0 @@
/*
* 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 {
Axis,
Chart,
Settings,
Position,
type ChartSizeArray,
type PointerUpdateListener,
type TickFormatter,
Tooltip,
} from '@elastic/charts';
import moment from 'moment';
import React from 'react';
import {
MetricsExplorerChartType,
type MetricsExplorerOptionsMetric,
} from '../../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options';
import { useIsDarkMode } from '../../../../hooks/use_is_dark_mode';
import { MetricExplorerSeriesChart } from '../../../../pages/metrics/metrics_explorer/components/series_chart';
import type { MetricsExplorerSeries } from '../../../../../common/http_api';
import { getTimelineChartThemes } from '../../../../utils/get_chart_theme';
import { ChartHeader } from './chart_header';
const CHART_SIZE: ChartSizeArray = ['100%', 160];
interface Props {
title: string;
style: MetricsExplorerChartType;
chartRef: React.Ref<Chart>;
series: ChartSectionSeries[];
tickFormatterForTime: TickFormatter<any>;
tickFormatter: TickFormatter<any>;
onPointerUpdate: PointerUpdateListener;
domain: { max: number; min: number };
stack?: boolean;
}
export interface ChartSectionSeries {
metric: MetricsExplorerOptionsMetric;
series: MetricsExplorerSeries;
}
export const ChartSection = ({
title,
style,
chartRef,
series,
tickFormatterForTime,
tickFormatter,
onPointerUpdate,
domain,
stack = false,
}: Props) => {
const isDarkMode = useIsDarkMode();
const metrics = series.map((chartSeries) => chartSeries.metric);
return (
<>
<ChartHeader title={title} metrics={metrics} />
<Chart ref={chartRef} size={CHART_SIZE}>
{series.map((chartSeries, index) => (
<MetricExplorerSeriesChart
type={style}
metric={chartSeries.metric}
id="0"
key={chartSeries.series.id}
series={chartSeries.series}
stack={stack}
/>
))}
<Axis
id="timestamp"
position={Position.Bottom}
showOverlappingTicks={true}
tickFormat={tickFormatterForTime}
/>
<Axis
id="values"
position={Position.Left}
tickFormat={tickFormatter}
domain={domain}
ticks={6}
gridLine={{
visible: true,
}}
/>
<Tooltip headerFormatter={({ value }) => moment(value).format('Y-MM-DD HH:mm:ss.SSS')} />
<Settings {...getTimelineChartThemes(isDarkMode)} onPointerUpdate={onPointerUpdate} />
</Chart>
</>
);
};

View file

@ -1,45 +0,0 @@
/*
* 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 type { Meta, Story } from '@storybook/react/types-6-0';
import { Metrics, type MetricsProps } from './metrics';
import { decorateWithGlobalStorybookThemeProviders } from '../../../../test_utils/use_global_storybook_theme';
import { DecorateWithKibanaContext } from '../../__stories__/decorator';
const stories: Meta<MetricsProps> = {
title: 'infra/Asset Details View/Components/Metrics',
decorators: [decorateWithGlobalStorybookThemeProviders, DecorateWithKibanaContext],
component: Metrics,
args: {
nodeId: 'host-1',
currentTime: 1683630468,
nodeType: 'host',
},
};
const Template: Story<MetricsProps> = (args) => {
return <Metrics {...args} />;
};
export const Default = Template.bind({});
export const NoData = Template.bind({});
NoData.parameters = {
apiResponse: {
mock: 'noData',
},
};
export const LoadingState = Template.bind({});
LoadingState.parameters = {
apiResponse: {
mock: 'loading',
},
};
export default stories;

View file

@ -1,467 +0,0 @@
/*
* 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, useEffect, useMemo, useRef, useState } from 'react';
import { Chart, niceTimeFormatter, PointerEvent } from '@elastic/charts';
import { EuiLoadingChart, EuiSpacer, EuiFlexGrid, EuiFlexItem } from '@elastic/eui';
import { first, last } from 'lodash';
import { euiStyled } from '@kbn/kibana-react-plugin/common';
import { createFormatterForMetric } from '../../../../pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metric';
import { getCustomMetricLabel } from '../../../../../common/formatters/get_custom_metric_label';
import { calculateDomain } from '../../../../pages/metrics/metrics_explorer/components/helpers/calculate_domain';
import { createInventoryMetricFormatter } from '../../../../pages/metrics/inventory_view/lib/create_inventory_metric_formatter';
import {
MetricsExplorerChartType,
type MetricsExplorerOptionsMetric,
} from '../../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options';
import { findInventoryFields } from '../../../../../common/inventory_models';
import { useSnapshot } from '../../../../pages/metrics/inventory_view/hooks/use_snaphot';
import { useSourceContext } from '../../../../containers/metrics_source';
import { convertKueryToElasticSearchQuery } from '../../../../utils/kuery';
import type {
InventoryItemType,
SnapshotMetricType,
} from '../../../../../common/inventory_models/types';
import { Color } from '../../../../../common/color_palette';
import type {
MetricsExplorerAggregation,
MetricsExplorerSeries,
SnapshotCustomMetricInput,
} from '../../../../../common/http_api';
import { ChartSection } from './chart_section';
import {
SYSTEM_METRIC_NAME,
USER_METRIC_NAME,
INBOUND_METRIC_NAME,
OUTBOUND_METRIC_NAME,
USED_MEMORY_METRIC_NAME,
FREE_MEMORY_METRIC_NAME,
CPU_CHART_TITLE,
LOAD_CHART_TITLE,
MEMORY_CHART_TITLE,
NETWORK_CHART_TITLE,
LOG_RATE_METRIC_NAME,
LOG_RATE_CHART_TITLE,
} from './translations';
import { TimeDropdown } from './time_dropdown';
const ONE_HOUR = 60 * 60 * 1000;
export interface MetricsProps {
accountId?: string;
currentTime: number;
customMetrics?: SnapshotCustomMetricInput[];
nodeId: string;
nodeType: InventoryItemType;
region?: string;
}
export const Metrics = ({
accountId,
currentTime,
customMetrics = [],
nodeType,
nodeId,
region,
}: MetricsProps) => {
const cpuChartRef = useRef<Chart>(null);
const networkChartRef = useRef<Chart>(null);
const memoryChartRef = useRef<Chart>(null);
const loadChartRef = useRef<Chart>(null);
const logRateChartRef = useRef<Chart>(null);
const customMetricRefs = useRef<Record<string, Chart | null>>({});
const [time, setTime] = useState(ONE_HOUR);
const chartRefs = useMemo(() => {
const refs = [cpuChartRef, networkChartRef, memoryChartRef, loadChartRef, logRateChartRef];
return [...refs, customMetricRefs];
}, [
cpuChartRef,
networkChartRef,
memoryChartRef,
loadChartRef,
logRateChartRef,
customMetricRefs,
]);
const { sourceId, createDerivedIndexPattern } = useSourceContext();
const derivedIndexPattern = useMemo(
() => createDerivedIndexPattern(),
[createDerivedIndexPattern]
);
let filter = `${findInventoryFields(nodeType).id}: "${nodeId}"`;
if (filter) {
filter = convertKueryToElasticSearchQuery(filter, derivedIndexPattern);
}
const buildCustomMetric = useCallback(
(field: string, id: string, aggregation: string = 'avg') => ({
type: 'custom' as SnapshotMetricType,
aggregation,
field,
id,
}),
[]
);
const updateTime = useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) => {
setTime(Number(e.currentTarget.value));
},
[setTime]
);
const timeRange = {
interval: '1m',
to: currentTime,
from: currentTime - time,
ignoreLookback: true,
};
const defaultMetrics: Array<{ type: SnapshotMetricType }> = [
{ type: 'rx' },
{ type: 'tx' },
buildCustomMetric('system.cpu.user.pct', 'user'),
buildCustomMetric('system.cpu.system.pct', 'system'),
buildCustomMetric('system.load.1', 'load1m'),
buildCustomMetric('system.load.5', 'load5m'),
buildCustomMetric('system.load.15', 'load15m'),
buildCustomMetric('system.memory.actual.used.bytes', 'usedMemory'),
buildCustomMetric('system.memory.actual.free', 'freeMemory'),
buildCustomMetric('system.cpu.cores', 'cores', 'max'),
];
const { nodes, reload } = useSnapshot({
filterQuery: filter,
metrics: [...defaultMetrics, ...customMetrics],
groupBy: [],
nodeType,
sourceId,
currentTime,
accountId,
region,
sendRequestImmediately: false,
timerange: timeRange,
});
const { nodes: logRateNodes, reload: reloadLogRate } = useSnapshot({
filterQuery: filter,
metrics: [{ type: 'logRate' }],
groupBy: [],
nodeType,
sourceId,
currentTime,
accountId,
region,
sendRequestImmediately: false,
timerange: timeRange,
});
const getDomain = useCallback(
(timeseries: MetricsExplorerSeries, ms: MetricsExplorerOptionsMetric[]) => {
const dataDomain = timeseries ? calculateDomain(timeseries, ms, false) : null;
return dataDomain
? {
max: dataDomain.max * 1.1, // add 10% headroom.
min: dataDomain.min,
}
: { max: 0, min: 0 };
},
[]
);
const dateFormatter = useCallback((timeseries: MetricsExplorerSeries) => {
if (!timeseries) return () => '';
const firstTimestamp = first(timeseries.rows)?.timestamp;
const lastTimestamp = last(timeseries.rows)?.timestamp;
if (firstTimestamp == null || lastTimestamp == null) {
return (value: number) => `${value}`;
}
return niceTimeFormatter([firstTimestamp, lastTimestamp]);
}, []);
const networkFormatter = useMemo(() => createInventoryMetricFormatter({ type: 'rx' }), []);
const cpuFormatter = useMemo(() => createInventoryMetricFormatter({ type: 'cpu' }), []);
const memoryFormatter = useMemo(
() => createInventoryMetricFormatter({ type: 's3BucketSize' }),
[]
);
const loadFormatter = useMemo(() => createInventoryMetricFormatter({ type: 'load' }), []);
const logRateFormatter = useMemo(() => createInventoryMetricFormatter({ type: 'logRate' }), []);
const mergeTimeseries = useCallback((...series: MetricsExplorerSeries[]) => {
const base = series[0];
const otherSeries = series.slice(1);
base.rows = base.rows.map((b, rowIdx) => {
const newRow = { ...b };
otherSeries.forEach((o, idx) => {
newRow[`metric_${idx + 1}`] = o.rows[rowIdx].metric_0;
});
return newRow;
});
return base;
}, []);
const buildChartMetricLabels = useCallback(
(labels: string[], aggregation: MetricsExplorerAggregation) => {
const baseMetric = {
color: Color.color0,
aggregation,
label: 'System',
};
return labels.map((label, idx) => {
return { ...baseMetric, color: Color[`color${idx}` as Color], label };
});
},
[]
);
const pointerUpdate = useCallback(
(event: PointerEvent) => {
chartRefs.forEach((ref) => {
if (ref.current) {
if (ref.current instanceof Chart) {
ref.current.dispatchExternalPointerEvent(event);
} else {
const charts = Object.values(ref.current);
charts.forEach((c) => {
if (c) {
c.dispatchExternalPointerEvent(event);
}
});
}
}
});
},
[chartRefs]
);
const getTimeseries = useCallback(
(metricName: string) => {
if (!nodes || !nodes.length) {
return null;
}
return nodes[0].metrics.find((m) => m.name === metricName)!.timeseries!;
},
[nodes]
);
const getLogRateTimeseries = useCallback(() => {
if (!logRateNodes) {
return null;
}
if (logRateNodes.length === 0) {
return { rows: [], columns: [], id: '0' };
}
return logRateNodes[0].metrics.find((m) => m.name === 'logRate')!.timeseries!;
}, [logRateNodes]);
const systemMetricsTs = useMemo(() => getTimeseries('system'), [getTimeseries]);
const userMetricsTs = useMemo(() => getTimeseries('user'), [getTimeseries]);
const rxMetricsTs = useMemo(() => getTimeseries('rx'), [getTimeseries]);
const txMetricsTs = useMemo(() => getTimeseries('tx'), [getTimeseries]);
const load1mMetricsTs = useMemo(() => getTimeseries('load1m'), [getTimeseries]);
const load5mMetricsTs = useMemo(() => getTimeseries('load5m'), [getTimeseries]);
const load15mMetricsTs = useMemo(() => getTimeseries('load15m'), [getTimeseries]);
const usedMemoryMetricsTs = useMemo(() => getTimeseries('usedMemory'), [getTimeseries]);
const freeMemoryMetricsTs = useMemo(() => getTimeseries('freeMemory'), [getTimeseries]);
const coresMetricsTs = useMemo(() => getTimeseries('cores'), [getTimeseries]);
const logRateMetricsTs = useMemo(() => getLogRateTimeseries(), [getLogRateTimeseries]);
useEffect(() => {
reload();
reloadLogRate();
}, [time, reload, reloadLogRate]);
if (
!systemMetricsTs ||
!userMetricsTs ||
!rxMetricsTs ||
!txMetricsTs ||
!load1mMetricsTs ||
!load5mMetricsTs ||
!load15mMetricsTs ||
!usedMemoryMetricsTs ||
!freeMemoryMetricsTs ||
!logRateMetricsTs
) {
return <LoadingPlaceholder />;
}
const cpuChartMetrics = buildChartMetricLabels([SYSTEM_METRIC_NAME, USER_METRIC_NAME], 'avg');
const logRateChartMetrics = buildChartMetricLabels([LOG_RATE_METRIC_NAME], 'rate');
const networkChartMetrics = buildChartMetricLabels(
[INBOUND_METRIC_NAME, OUTBOUND_METRIC_NAME],
'rate'
);
const loadChartMetrics = buildChartMetricLabels(['1m', '5m', '15m'], 'avg');
const memoryChartMetrics = buildChartMetricLabels(
[USED_MEMORY_METRIC_NAME, FREE_MEMORY_METRIC_NAME],
'rate'
);
systemMetricsTs.rows = systemMetricsTs.rows.slice().map((r, idx) => {
const metric = r.metric_0 as number | undefined;
const cores = coresMetricsTs!.rows[idx].metric_0 as number | undefined;
if (metric && cores) {
r.metric_0 = metric / cores;
}
return r;
});
userMetricsTs.rows = userMetricsTs.rows.slice().map((r, idx) => {
const metric = r.metric_0 as number | undefined;
const cores = coresMetricsTs!.rows[idx].metric_0 as number | undefined;
if (metric && cores) {
r.metric_0 = metric / cores;
}
return r;
});
const cpuTimeseries = mergeTimeseries(systemMetricsTs, userMetricsTs);
const logRateTimeseries = mergeTimeseries(logRateMetricsTs);
const networkTimeseries = mergeTimeseries(rxMetricsTs, txMetricsTs);
const loadTimeseries = mergeTimeseries(load1mMetricsTs, load5mMetricsTs, load15mMetricsTs);
const memoryTimeseries = mergeTimeseries(usedMemoryMetricsTs, freeMemoryMetricsTs);
const formatter = dateFormatter(rxMetricsTs);
return (
<>
<TimeDropdown value={time} onChange={updateTime} />
<EuiSpacer size={'l'} />
<EuiFlexGrid columns={2} gutterSize={'l'} responsive={false}>
<ChartGridItem>
<ChartSection
title={CPU_CHART_TITLE}
style={MetricsExplorerChartType.line}
chartRef={cpuChartRef}
series={[
{ metric: cpuChartMetrics[0], series: systemMetricsTs },
{ metric: cpuChartMetrics[1], series: userMetricsTs },
]}
tickFormatterForTime={formatter}
tickFormatter={cpuFormatter}
onPointerUpdate={pointerUpdate}
domain={getDomain(cpuTimeseries, cpuChartMetrics)}
/>
</ChartGridItem>
<ChartGridItem>
<ChartSection
title={LOAD_CHART_TITLE}
style={MetricsExplorerChartType.line}
chartRef={loadChartRef}
series={[
{ metric: loadChartMetrics[0], series: load1mMetricsTs },
{ metric: loadChartMetrics[1], series: load5mMetricsTs },
{ metric: loadChartMetrics[2], series: load15mMetricsTs },
]}
tickFormatterForTime={formatter}
tickFormatter={loadFormatter}
onPointerUpdate={pointerUpdate}
domain={getDomain(loadTimeseries, loadChartMetrics)}
/>
</ChartGridItem>
<ChartGridItem>
<ChartSection
title={MEMORY_CHART_TITLE}
style={MetricsExplorerChartType.line}
chartRef={memoryChartRef}
series={[
{ metric: memoryChartMetrics[0], series: usedMemoryMetricsTs },
{ metric: memoryChartMetrics[1], series: freeMemoryMetricsTs },
]}
tickFormatterForTime={formatter}
tickFormatter={memoryFormatter}
onPointerUpdate={pointerUpdate}
domain={getDomain(memoryTimeseries, memoryChartMetrics)}
/>
</ChartGridItem>
<ChartGridItem>
<ChartSection
title={NETWORK_CHART_TITLE}
style={MetricsExplorerChartType.line}
chartRef={networkChartRef}
series={[
{ metric: networkChartMetrics[0], series: rxMetricsTs },
{ metric: networkChartMetrics[1], series: txMetricsTs },
]}
tickFormatterForTime={formatter}
tickFormatter={networkFormatter}
onPointerUpdate={pointerUpdate}
domain={getDomain(networkTimeseries, networkChartMetrics)}
stack={true}
/>
</ChartGridItem>
<ChartGridItem>
<ChartSection
title={LOG_RATE_CHART_TITLE}
style={MetricsExplorerChartType.line}
chartRef={logRateChartRef}
series={[{ metric: logRateChartMetrics[0], series: logRateMetricsTs }]}
tickFormatterForTime={formatter}
tickFormatter={logRateFormatter}
onPointerUpdate={pointerUpdate}
domain={getDomain(logRateTimeseries, logRateChartMetrics)}
stack={true}
/>
</ChartGridItem>
{customMetrics.map((c) => {
const metricTS = getTimeseries(c.id);
const chartMetrics = buildChartMetricLabels([c.field], c.aggregation);
if (!metricTS) return null;
return (
<ChartGridItem>
<ChartSection
title={getCustomMetricLabel(c)}
style={MetricsExplorerChartType.line}
chartRef={(r) => {
customMetricRefs.current[c.id] = r;
}}
series={[{ metric: chartMetrics[0], series: metricTS }]}
tickFormatterForTime={formatter}
tickFormatter={createFormatterForMetric(c)}
onPointerUpdate={pointerUpdate}
domain={getDomain(mergeTimeseries(metricTS), chartMetrics)}
stack={true}
/>
</ChartGridItem>
);
})}
</EuiFlexGrid>
</>
);
};
const ChartGridItem = euiStyled(EuiFlexItem)`
overflow: hidden
`;
const LoadingPlaceholder = () => {
return (
<div
style={{
width: '100%',
height: '200px',
padding: '16px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<EuiLoadingChart size="xl" />
</div>
);
};

View file

@ -1,56 +0,0 @@
/*
* 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 { EuiSelect } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
interface Props {
value: number;
onChange(event: React.ChangeEvent<HTMLSelectElement>): void;
}
export const TimeDropdown = (props: Props) => (
<EuiSelect
data-test-subj="infraTimeDropdownSelect"
fullWidth={true}
options={[
{
text: i18n.translate('xpack.infra.nodeDetails.metrics.last15Minutes', {
defaultMessage: 'Last 15 minutes',
}),
value: 15 * 60 * 1000,
},
{
text: i18n.translate('xpack.infra.nodeDetails.metrics.lastHour', {
defaultMessage: 'Last hour',
}),
value: 60 * 60 * 1000,
},
{
text: i18n.translate('xpack.infra.nodeDetails.metrics.last3Hours', {
defaultMessage: 'Last 3 hours',
}),
value: 3 * 60 * 60 * 1000,
},
{
text: i18n.translate('xpack.infra.nodeDetails.metrics.last24Hours', {
defaultMessage: 'Last 24 hours',
}),
value: 24 * 60 * 60 * 1000,
},
{
text: i18n.translate('xpack.infra.nodeDetails.metrics.last7Days', {
defaultMessage: 'Last 7 days',
}),
value: 7 * 24 * 60 * 60 * 1000,
},
]}
value={props.value}
onChange={props.onChange}
/>
);

View file

@ -1,66 +0,0 @@
/*
* 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';
export const SYSTEM_METRIC_NAME = i18n.translate('xpack.infra.nodeDetails.metrics.system', {
defaultMessage: 'System',
});
export const USER_METRIC_NAME = i18n.translate('xpack.infra.nodeDetails.metrics.user', {
defaultMessage: 'User',
});
export const INBOUND_METRIC_NAME = i18n.translate('xpack.infra.nodeDetails.metrics.inbound', {
defaultMessage: 'Inbound',
});
export const OUTBOUND_METRIC_NAME = i18n.translate('xpack.infra.nodeDetails.metrics.outbound', {
defaultMessage: 'Outbound',
});
export const USED_MEMORY_METRIC_NAME = i18n.translate('xpack.infra.nodeDetails.metrics.used', {
defaultMessage: 'Used',
});
export const CACHED_MEMORY_METRIC_NAME = i18n.translate('xpack.infra.nodeDetails.metrics.cached', {
defaultMessage: 'Cached',
});
export const FREE_MEMORY_METRIC_NAME = i18n.translate('xpack.infra.nodeDetails.metrics.free', {
defaultMessage: 'Free',
});
export const NETWORK_CHART_TITLE = i18n.translate(
'xpack.infra.nodeDetails.metrics.charts.networkTitle',
{
defaultMessage: 'Network',
}
);
export const MEMORY_CHART_TITLE = i18n.translate(
'xpack.infra.nodeDetails.metrics.charts.memoryTitle',
{
defaultMessage: 'Memory',
}
);
export const CPU_CHART_TITLE = i18n.translate('xpack.infra.nodeDetails.metrics.fcharts.cpuTitle', {
defaultMessage: 'CPU',
});
export const LOAD_CHART_TITLE = i18n.translate('xpack.infra.nodeDetails.metrics.charts.loadTitle', {
defaultMessage: 'Load',
});
export const LOG_RATE_METRIC_NAME = i18n.translate('xpack.infra.nodeDetails.metrics.logRate', {
defaultMessage: 'Log Rate',
});
export const LOG_RATE_CHART_TITLE = i18n.translate(
'xpack.infra.nodeDetails.metrics.charts.logRateTitle',
{
defaultMessage: 'Log Rate',
}
);

View file

@ -7,20 +7,21 @@
import { EuiSkeletonText } from '@elastic/eui';
import React, { useMemo } from 'react';
import type { MetricsTimeInput } from '../../../../pages/metrics/metric_detail/hooks/use_metrics_time';
import { TimeRange } from '@kbn/es-query';
import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana';
import { useSourceContext } from '../../../../containers/metrics_source';
import { findInventoryModel } from '../../../../../common/inventory_models';
import type { InventoryItemType } from '../../../../../common/inventory_models/types';
import { useMetadata } from '../../hooks/use_metadata';
import { toTimestampRange } from '../../utils';
export interface OsqueryProps {
nodeName: string;
nodeType: InventoryItemType;
currentTimeRange: MetricsTimeInput;
dateRange: TimeRange;
}
export const Osquery = ({ nodeName, nodeType, currentTimeRange }: OsqueryProps) => {
export const Osquery = ({ nodeName, nodeType, dateRange }: OsqueryProps) => {
const inventoryModel = findInventoryModel(nodeType);
const { sourceId } = useSourceContext();
const { loading, metadata } = useMetadata(
@ -28,7 +29,7 @@ export const Osquery = ({ nodeName, nodeType, currentTimeRange }: OsqueryProps)
nodeType,
inventoryModel.requiredMetrics,
sourceId,
currentTimeRange
toTimestampRange(dateRange)
);
const {
services: { osquery },

View file

@ -5,22 +5,13 @@
* 2.0.
*/
import React from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { Tile, type TileProps } from './tile';
import { KPI_CHARTS } from '../../../../../common/visualizations/lens/dashboards/host/kpi_grid_config';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { Tile } from './tile';
import { KPI_CHARTS } from '../../../../common/visualizations/lens/dashboards/host/kpi_grid_config';
import type { KPIProps } from './overview';
import type { StringDateRange } from '../../types';
export interface KPIGridProps extends KPIProps {
nodeName: string;
dateRange: StringDateRange;
}
export const KPIGrid = React.memo(({ nodeName, dataView, dateRange }: KPIGridProps) => {
export const KPIGrid = React.memo(({ nodeName, dataView, timeRange: dateRange }: TileProps) => {
return (
<>
<EuiSpacer size="s" />
<EuiFlexGroup
direction="row"
gutterSize="s"
@ -29,7 +20,7 @@ export const KPIGrid = React.memo(({ nodeName, dataView, dateRange }: KPIGridPro
>
{KPI_CHARTS.map((chartProp, index) => (
<EuiFlexItem key={index}>
<Tile {...chartProp} nodeName={nodeName} dataView={dataView} dateRange={dateRange} />
<Tile {...chartProp} nodeName={nodeName} dataView={dataView} timeRange={dateRange} />
</EuiFlexItem>
))}
</EuiFlexGroup>

View file

@ -5,28 +5,27 @@
* 2.0.
*/
import React, { useMemo } from 'react';
import type { DataView } from '@kbn/data-views-plugin/public';
import { i18n } from '@kbn/i18n';
import {
EuiIcon,
EuiPanel,
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiI18n,
EuiToolTip,
} from '@elastic/eui';
import { EuiIcon, EuiPanel, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui';
import styled from 'styled-components';
import type { Action } from '@kbn/ui-actions-plugin/public';
import type { KPIChartProps } from '../../../../common/visualizations/lens/dashboards/host/kpi_grid_config';
import { useLensAttributes } from '../../../../hooks/use_lens_attributes';
import { LensWrapper } from '../../../../common/visualizations/lens/lens_wrapper';
import { buildCombinedHostsFilter, buildExistsHostsFilter } from '../../../../utils/filters/build';
import { TooltipContent } from '../../../../common/visualizations/metric_explanation/tooltip_content';
import type { KPIGridProps } from './kpi_grid';
import { TimeRange } from '@kbn/es-query';
import { FormattedMessage } from '@kbn/i18n-react';
import type { KPIChartProps } from '../../../../../common/visualizations/lens/dashboards/host/kpi_grid_config';
import { useLensAttributes } from '../../../../../hooks/use_lens_attributes';
import { LensWrapper } from '../../../../../common/visualizations/lens/lens_wrapper';
import { buildCombinedHostsFilter } from '../../../../../utils/filters/build';
import { TooltipContent } from '../../../../../common/visualizations/metric_explanation/tooltip_content';
const MIN_HEIGHT = 150;
export interface TileProps {
timeRange: TimeRange;
dataView?: DataView;
nodeName: string;
}
export const Tile = ({
id,
layers,
@ -34,8 +33,8 @@ export const Tile = ({
toolTip,
dataView,
nodeName,
dateRange,
}: KPIChartProps & KPIGridProps) => {
timeRange,
}: KPIChartProps & TileProps) => {
const getSubtitle = () =>
i18n.translate('xpack.infra.assetDetailsEmbeddable.overview.metricTrend.subtitle.average', {
defaultMessage: 'Average',
@ -55,17 +54,16 @@ export const Tile = ({
values: [nodeName],
dataView,
}),
buildExistsHostsFilter({ field: 'host.name', dataView }),
];
}, [dataView, nodeName]);
const extraActions: Action[] = useMemo(
() =>
getExtraActions({
timeRange: dateRange,
timeRange,
filters,
}),
[filters, getExtraActions, dateRange]
[filters, getExtraActions, timeRange]
);
const loading = !attributes;
@ -90,9 +88,9 @@ export const Tile = ({
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s" textAlign="center">
<EuiI18n
token="'xpack.infra.assetDetailsEmbeddable.overview.errorOnLoadingLensDependencies'"
default="There was an error trying to load Lens Plugin."
<FormattedMessage
id="xpack.infra.assetDetailsEmbeddable.overview.errorOnLoadingLensDependencies"
defaultMessage="There was an error trying to load Lens Plugin."
/>
</EuiText>
</EuiFlexItem>
@ -108,7 +106,7 @@ export const Tile = ({
attributes={attributes}
style={{ height: MIN_HEIGHT }}
extraActions={extraActions}
dateRange={dateRange}
dateRange={timeRange}
filters={filters}
loading={loading}
/>

View file

@ -12,7 +12,6 @@ import {
EuiFlexItem,
EuiDescriptionList,
EuiDescriptionListDescription,
EuiHorizontalRule,
EuiLoadingSpinner,
} from '@elastic/eui';
import { css } from '@emotion/react';
@ -103,7 +102,6 @@ export const MetadataSummary = ({ metadata, metadataLoading }: MetadataSummaryPr
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule margin="m" />
</>
);
};

View file

@ -0,0 +1,117 @@
/*
* 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 { Action } from '@kbn/ui-actions-plugin/public';
import { EuiIcon, EuiPanel, EuiFlexGroup, EuiFlexItem, EuiText, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { TypedLensByValueInput } from '@kbn/lens-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { TimeRange } from '@kbn/es-query';
import { FormattedMessage } from '@kbn/i18n-react';
import { buildCombinedHostsFilter } from '../../../../../utils/filters/build';
import { LensWrapper } from '../../../../../common/visualizations/lens/lens_wrapper';
import { useLensAttributes, type Layer } from '../../../../../hooks/use_lens_attributes';
import type { FormulaConfig, XYLayerOptions } from '../../../../../common/visualizations';
export interface MetricChartProps extends Pick<TypedLensByValueInput, 'id' | 'overrides'> {
title: string;
layers: Array<Layer<XYLayerOptions, FormulaConfig[]>>;
dataView?: DataView;
timeRange: TimeRange;
nodeName: string;
}
const MIN_HEIGHT = 250;
export const MetricChart = ({
id,
title,
layers,
nodeName,
timeRange,
dataView,
overrides,
}: MetricChartProps) => {
const { euiTheme } = useEuiTheme();
const { attributes, getExtraActions, error } = useLensAttributes({
dataView,
layers,
title,
visualizationType: 'lnsXY',
});
const filters = useMemo(() => {
return [
buildCombinedHostsFilter({
field: 'host.name',
values: [nodeName],
dataView,
}),
];
}, [dataView, nodeName]);
const extraActions: Action[] = useMemo(
() =>
getExtraActions({
timeRange,
filters,
}),
[timeRange, filters, getExtraActions]
);
const loading = !attributes;
return (
<EuiPanel
borderRadius="m"
hasShadow={false}
hasBorder
paddingSize={error ? 'm' : 'none'}
css={css`
min-height: calc(${MIN_HEIGHT}px + ${euiTheme.size.l});
position: relative;
`}
data-test-subj={`assetDetailsMetricsChart${id}`}
>
{error ? (
<EuiFlexGroup
style={{ minHeight: '100%', alignContent: 'center' }}
gutterSize="xs"
justifyContent="center"
alignItems="center"
direction="column"
>
<EuiFlexItem grow={false}>
<EuiIcon type="warning" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s" textAlign="center">
<FormattedMessage
id="xpack.infra.hostsViewPage.errorOnLoadingLensDependencies"
defaultMessage="There was an error trying to load Lens Plugin."
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
) : (
<LensWrapper
id={`assetDetailsMetricsChart${id}`}
attributes={attributes}
style={{ height: MIN_HEIGHT }}
extraActions={extraActions}
dateRange={timeRange}
filters={filters}
overrides={overrides}
loading={loading}
disableTriggers
hasTitle
/>
)}
</EuiPanel>
);
};

View file

@ -0,0 +1,295 @@
/*
* 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 { EuiFlexGrid, EuiFlexItem, EuiTitle, EuiSpacer, EuiFlexGroup } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { DataView } from '@kbn/data-views-plugin/public';
import { TimeRange } from '@kbn/es-query';
import { FormattedMessage } from '@kbn/i18n-react';
import { HostMetricsDocsLink } from '../../../../../common/visualizations/metric_explanation/host_metrics_docs_link';
import { MetricChart, type MetricChartProps } from './metric_chart';
import { hostLensFormulas } from '../../../../../common/visualizations';
const PERCENT_LEFT_AXIS: Pick<MetricChartProps, 'overrides'>['overrides'] = {
axisLeft: {
domain: {
min: 0,
max: 1,
},
},
};
const LEGEND_SETTINGS: Pick<MetricChartProps, 'overrides'>['overrides'] = {
settings: {
showLegend: true,
legendPosition: 'bottom',
legendSize: 35,
},
};
const CHARTS_IN_ORDER: Array<
Pick<MetricChartProps, 'id' | 'title' | 'layers' | 'overrides'> & {
dataViewType: 'logs' | 'metrics';
}
> = [
{
id: 'cpuUsage',
title: i18n.translate('xpack.infra.assetDetails.metricsCharts.cpuUsage', {
defaultMessage: 'CPU Usage',
}),
layers: [
{
data: [hostLensFormulas.cpuUsage],
layerType: 'data',
},
],
dataViewType: 'metrics',
overrides: {
axisLeft: PERCENT_LEFT_AXIS.axisLeft,
},
},
{
id: 'memoryUsage',
title: i18n.translate('xpack.infra.assetDetails.metricsCharts.memoryUsage', {
defaultMessage: 'Memory Usage',
}),
layers: [
{
data: [hostLensFormulas.memoryUsage],
layerType: 'data',
},
],
dataViewType: 'metrics',
overrides: {
axisLeft: PERCENT_LEFT_AXIS.axisLeft,
},
},
{
id: 'normalizedLoad1m',
title: i18n.translate('xpack.infra.assetDetails.metricsCharts.normalizedLoad1m', {
defaultMessage: 'Normalized Load',
}),
layers: [
{
data: [hostLensFormulas.normalizedLoad1m],
layerType: 'data',
},
{
data: [
{
value: '1',
format: {
id: 'percent',
params: {
decimals: 0,
},
},
color: '#6092c0',
},
],
layerType: 'referenceLine',
},
],
dataViewType: 'metrics',
},
{
id: 'logRate',
title: i18n.translate('xpack.infra.assetDetails.metricsCharts.logRate', {
defaultMessage: 'Log Rate',
}),
layers: [
{
data: [hostLensFormulas.logRate],
layerType: 'data',
},
],
dataViewType: 'logs',
},
{
id: 'diskSpaceUsageAvailable',
title: i18n.translate('xpack.infra.assetDetails.metricsCharts.diskSpace', {
defaultMessage: 'Disk Space',
}),
layers: [
{
data: [
{
...hostLensFormulas.diskSpaceUsage,
label: i18n.translate('xpack.infra.assetDetails.metricsCharts.diskSpace.label.used', {
defaultMessage: 'Used',
}),
},
{
...hostLensFormulas.diskSpaceAvailable,
label: i18n.translate(
'xpack.infra.assetDetails.metricsCharts.diskSpace.label.available',
{
defaultMessage: 'Available',
}
),
},
],
layerType: 'data',
options: {
seriesType: 'area',
},
},
],
overrides: {
axisRight: {
style: {
axisTitle: {
visible: false,
},
},
},
axisLeft: PERCENT_LEFT_AXIS.axisLeft,
settings: LEGEND_SETTINGS.settings,
},
dataViewType: 'metrics',
},
{
id: 'diskThroughputReadWrite',
title: i18n.translate('xpack.infra.assetDetails.metricsCharts.diskIOPS', {
defaultMessage: 'Disk IOPS',
}),
layers: [
{
data: [
{
...hostLensFormulas.diskReadThroughput,
label: i18n.translate('xpack.infra.assetDetails.metricsCharts.metric.label.read', {
defaultMessage: 'Read',
}),
},
{
...hostLensFormulas.diskWriteThroughput,
label: i18n.translate('xpack.infra.assetDetails.metricsCharts.metric.label.write', {
defaultMessage: 'Write',
}),
},
],
layerType: 'data',
options: {
seriesType: 'area',
},
},
],
overrides: {
settings: LEGEND_SETTINGS.settings,
},
dataViewType: 'metrics',
},
{
id: 'diskIOReadWrite',
title: i18n.translate('xpack.infra.assetDetails.metricsCharts.diskThroughput', {
defaultMessage: 'Disk Throughput',
}),
layers: [
{
data: [
{
...hostLensFormulas.diskIORead,
label: i18n.translate('xpack.infra.assetDetails.metricsCharts.metric.label.read', {
defaultMessage: 'Read',
}),
},
{
...hostLensFormulas.diskIOWrite,
label: i18n.translate('xpack.infra.assetDetails.metricsCharts.metric.label.write', {
defaultMessage: 'Write',
}),
},
],
layerType: 'data',
options: {
seriesType: 'area',
},
},
],
overrides: {
settings: LEGEND_SETTINGS.settings,
},
dataViewType: 'metrics',
},
{
id: 'rxTx',
title: i18n.translate('xpack.infra.assetDetails.metricsCharts.network', {
defaultMessage: 'Network',
}),
layers: [
{
data: [
{
...hostLensFormulas.rx,
label: i18n.translate('xpack.infra.assetDetails.metricsCharts.network.label.rx', {
defaultMessage: 'Inbound (RX)',
}),
},
{
...hostLensFormulas.tx,
label: i18n.translate('xpack.infra.assetDetails.metricsCharts.network.label.tx', {
defaultMessage: 'Outbound (TX)',
}),
},
],
layerType: 'data',
options: {
seriesType: 'area',
},
},
],
overrides: {
settings: LEGEND_SETTINGS.settings,
},
dataViewType: 'metrics',
},
];
export interface MetricsGridProps {
nodeName: string;
timeRange: TimeRange;
metricsDataView?: DataView;
logsDataView?: DataView;
}
export const MetricsGrid = React.memo(
({ nodeName, metricsDataView, logsDataView, timeRange }: MetricsGridProps) => {
return (
<EuiFlexGroup gutterSize="m" direction="column">
<EuiFlexItem grow={false}>
<EuiTitle size="xxs">
<h5>
<FormattedMessage
id="xpack.infra.assetDetails.overview.metricsSectionTitle"
defaultMessage="Metrics"
/>
</h5>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<HostMetricsDocsLink />
<EuiSpacer size="s" />
<EuiFlexGrid columns={2} gutterSize="s" data-test-subj="assetDetailsMetricsChartGrid">
{CHARTS_IN_ORDER.map(({ dataViewType, ...chartProp }, index) => (
<EuiFlexItem key={index} grow={false}>
<MetricChart
nodeName={nodeName}
dataView={dataViewType === 'metrics' ? metricsDataView : logsDataView}
timeRange={timeRange}
{...chartProp}
/>
</EuiFlexItem>
))}
</EuiFlexGrid>
</EuiFlexItem>
</EuiFlexGroup>
);
}
);

View file

@ -7,45 +7,39 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui';
import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiLink, EuiHorizontalRule } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import type { TimeRange } from '@kbn/es-query';
import type { DataView } from '@kbn/data-views-plugin/public';
import { css } from '@emotion/react';
import type { InventoryItemType } from '../../../../../common/inventory_models/types';
import { findInventoryModel } from '../../../../../common/inventory_models';
import type { MetricsTimeInput } from '../../../../pages/metrics/metric_detail/hooks/use_metrics_time';
import { useMetadata } from '../../hooks/use_metadata';
import { useSourceContext } from '../../../../containers/metrics_source';
import { MetadataSummary } from './metadata_summary';
import { KPIGrid } from './kpi_grid';
import type { StringDateRange } from '../../types';
import { KPIGrid } from './kpis/kpi_grid';
import { MetricsGrid } from './metrics/metrics_grid';
import { toTimestampRange } from '../../utils';
export interface MetadataSearchUrlState {
metadataSearchUrlState: string;
setMetadataSearchUrlState: (metadataSearch: { metadataSearch?: string }) => void;
}
export interface KPIProps {
dateRange?: StringDateRange;
dataView?: DataView;
}
export interface OverviewProps extends KPIProps {
currentTimeRange: MetricsTimeInput;
export interface OverviewProps {
dateRange: TimeRange;
nodeName: string;
nodeType: InventoryItemType;
metricsDataView?: DataView;
logsDataView?: DataView;
}
const DEFAULT_DATE_RANGE = {
from: 'now-15m',
to: 'now',
mode: 'absolute' as const,
};
export const Overview = ({
nodeName,
currentTimeRange,
nodeType,
dateRange,
dataView,
nodeType,
metricsDataView,
logsDataView,
}: OverviewProps) => {
const inventoryModel = findInventoryModel(nodeType);
const { sourceId } = useSourceContext();
@ -53,16 +47,18 @@ export const Overview = ({
loading: metadataLoading,
error: fetchMetadataError,
metadata,
} = useMetadata(nodeName, nodeType, inventoryModel.requiredMetrics, sourceId, currentTimeRange);
} = useMetadata(
nodeName,
nodeType,
inventoryModel.requiredMetrics,
sourceId,
toTimestampRange(dateRange)
);
return (
<EuiFlexGroup direction="column">
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem grow={false}>
<KPIGrid
nodeName={nodeName}
dateRange={dateRange ?? DEFAULT_DATE_RANGE}
dataView={dataView}
/>
<KPIGrid nodeName={nodeName} timeRange={dateRange} dataView={metricsDataView} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
{fetchMetadataError ? (
@ -94,7 +90,25 @@ export const Overview = ({
) : (
<MetadataSummary metadata={metadata} metadataLoading={metadataLoading} />
)}
<SectionSeparator />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<MetricsGrid
timeRange={dateRange}
logsDataView={logsDataView}
metricsDataView={metricsDataView}
nodeName={nodeName}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};
const SectionSeparator = () => (
<EuiHorizontalRule
margin="m"
css={css`
margin-bottom: 0;
`}
/>
);

View file

@ -18,7 +18,7 @@ const stories: Meta<ProcessesProps> = {
args: {
nodeName: 'host1',
nodeType: 'host',
currentTime: 1683630468,
currentTimestamp: 1683630468,
},
};

View file

@ -32,7 +32,7 @@ import type { InventoryItemType } from '../../../../../common/inventory_models/t
export interface ProcessesProps {
nodeName: string;
nodeType: InventoryItemType;
currentTime: number;
currentTimestamp: number;
search?: string;
onSearchFilterChange?: (searchFilter: string) => void;
}
@ -43,7 +43,7 @@ const options = Object.entries(STATE_NAMES).map(([value, view]: [string, string]
}));
export const Processes = ({
currentTime,
currentTimestamp,
nodeName,
nodeType,
search,
@ -69,7 +69,7 @@ export const Processes = ({
error,
response,
makeRequest: reload,
} = useProcessList(hostTerm, currentTime, sortBy, parseSearchString(searchText));
} = useProcessList(hostTerm, currentTimestamp, sortBy, parseSearchString(searchText));
const debouncedSearchOnChange = useMemo(() => {
return debounce<(queryText: string) => void>((queryText) => {
@ -97,7 +97,7 @@ export const Processes = ({
}, [onSearchFilterChange]);
return (
<ProcessListContextProvider hostTerm={hostTerm} to={currentTime}>
<ProcessListContextProvider hostTerm={hostTerm} to={currentTimestamp}>
<SummaryTable
isLoading={loading}
processSummary={(!error ? response?.summary : null) ?? { total: 0 }}
@ -148,7 +148,7 @@ export const Processes = ({
<EuiSpacer size="m" />
{!error ? (
<ProcessesTable
currentTime={currentTime}
currentTime={currentTimestamp}
isLoading={loading || !response}
processList={response?.processList ?? []}
sortBy={sortBy}

View file

@ -7,27 +7,19 @@
import type { DataView } from '@kbn/data-views-plugin/public';
import type { LogViewReference } from '@kbn/logs-shared-plugin/common';
import { TimeRange } from '@kbn/es-query';
import type { InventoryItemType } from '../../../common/inventory_models/types';
import type { InfraAssetMetricType, SnapshotCustomMetricInput } from '../../../common/http_api';
export type CloudProvider = 'gcp' | 'aws' | 'azure' | 'unknownProvider';
type HostMetrics = Record<InfraAssetMetricType, number | null>;
interface HostMetadata {
os?: string | null;
interface Metadata {
ip?: string | null;
servicesOnHost?: number | null;
title: { name: string; cloudProvider?: CloudProvider | null };
id: string;
}
export type HostNodeRow = HostMetadata &
HostMetrics & {
name: string;
};
export type Node = Metadata & {
id: string;
name: string;
};
export enum FlyoutTabIds {
OVERVIEW = 'overview',
METRICS = 'metrics',
METADATA = 'metadata',
PROCESSES = 'processes',
ANOMALIES = 'anomalies',
@ -39,16 +31,10 @@ export enum FlyoutTabIds {
export type TabIds = `${FlyoutTabIds}`;
export interface StringDateRange {
from: string;
to: string;
mode?: 'absolute' | 'relative' | undefined;
}
export interface TabState {
overview?: {
dateRange: StringDateRange;
dataView?: DataView;
metricsDataView?: DataView;
logsDataView?: DataView;
};
metadata?: {
query?: string;
@ -60,11 +46,6 @@ export interface TabState {
anomalies?: {
onClose?: () => void;
};
metrics?: {
accountId?: string;
region?: string;
customMetrics?: SnapshotCustomMetricInput[];
};
alertRule?: {
onCreateRuleClick?: () => void;
};
@ -97,13 +78,9 @@ export interface Tab {
export type LinkOptions = 'alertRule' | 'nodeDetails' | 'apmServices';
export interface AssetDetailsProps {
node: HostNodeRow;
node: Node;
nodeType: InventoryItemType;
currentTimeRange: {
interval: string;
from: number;
to: number;
};
dateRange: TimeRange;
tabs: Tab[];
activeTabId?: TabIds;
overrides?: TabState;

View file

@ -0,0 +1,13 @@
/*
* 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 const toTimestampRange = ({ from, to }: { from: string; to: string }) => {
const fromTs = new Date(from).getTime();
const toTs = new Date(to).getTime();
return { from: fromTs, to: toTs };
};

View file

@ -5,7 +5,8 @@
* 2.0.
*/
import React, { useMemo } from 'react';
import React from 'react';
import useAsync from 'react-use/lib/useAsync';
import type { InventoryItemType } from '../../../../../../common/inventory_models/types';
import { useUnifiedSearchContext } from '../../hooks/use_unified_search';
import type { HostNodeRow } from '../../hooks/use_hosts_table';
@ -23,18 +24,15 @@ export interface Props {
const NODE_TYPE = 'host' as InventoryItemType;
export const FlyoutWrapper = ({ node, closeFlyout }: Props) => {
const { getDateRangeAsTimestamp } = useUnifiedSearchContext();
const { searchCriteria } = useUnifiedSearchContext();
const { dataView } = useMetricsDataViewContext();
const { logViewReference, loading } = useLogViewReference({
const { logViewReference, loading, getLogsDataView } = useLogViewReference({
id: 'hosts-flyout-logs-view',
});
const currentTimeRange = useMemo(
() => ({
...getDateRangeAsTimestamp(),
interval: '1m',
}),
[getDateRangeAsTimestamp]
const { value: logsDataView } = useAsync(
() => getLogsDataView(logViewReference),
[logViewReference]
);
const [hostFlyoutState, setHostFlyoutState] = useHostFlyoutUrlState();
@ -43,12 +41,12 @@ export const FlyoutWrapper = ({ node, closeFlyout }: Props) => {
<AssetDetails
node={node}
nodeType={NODE_TYPE}
currentTimeRange={currentTimeRange}
dateRange={searchCriteria.dateRange}
activeTabId={hostFlyoutState?.tabId}
overrides={{
overview: {
dateRange: searchCriteria.dateRange,
dataView,
logsDataView,
metricsDataView: dataView,
},
metadata: {
query: hostFlyoutState?.metadataSearch,

View file

@ -8,17 +8,10 @@ import React, { useMemo, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { BrushTriggerEvent } from '@kbn/charts-plugin/public';
import {
EuiIcon,
EuiPanel,
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiI18n,
EuiToolTip,
} from '@elastic/eui';
import { EuiIcon, EuiPanel, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui';
import styled from 'styled-components';
import { Action } from '@kbn/ui-actions-plugin/public';
import { FormattedMessage } from '@kbn/i18n-react';
import { KPIChartProps } from '../../../../../common/visualizations/lens/dashboards/host/kpi_grid_config';
import { buildCombinedHostsFilter } from '../../../../../utils/filters/build';
import { useLensAttributes } from '../../../../../hooks/use_lens_attributes';
@ -131,9 +124,9 @@ export const Tile = ({ id, title, layers, style, toolTip, ...props }: KPIChartPr
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s" textAlign="center">
<EuiI18n
token="'xpack.infra.hostsViewPage.errorOnLoadingLensDependencies'"
default="There was an error trying to load Lens Plugin."
<FormattedMessage
id="xpack.infra.hostsViewPage.errorOnLoadingLensDependencies"
defaultMessage="There was an error trying to load Lens Plugin."
/>
</EuiText>
</EuiFlexItem>

View file

@ -7,19 +7,12 @@
import React, { CSSProperties, useCallback, useMemo } from 'react';
import { Action } from '@kbn/ui-actions-plugin/public';
import { BrushTriggerEvent } from '@kbn/charts-plugin/public';
import {
EuiIcon,
EuiPanel,
EuiI18n,
EuiFlexGroup,
EuiFlexItem,
EuiText,
useEuiTheme,
} from '@elastic/eui';
import { EuiIcon, EuiPanel, EuiFlexGroup, EuiFlexItem, EuiText, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { TypedLensByValueInput } from '@kbn/lens-plugin/public';
import { FormattedMessage } from '@kbn/i18n-react';
import { LensWrapper } from '../../../../../../common/visualizations/lens/lens_wrapper';
import { useLensAttributes, Layer, LayerType } from '../../../../../../hooks/use_lens_attributes';
import { useLensAttributes, Layer } from '../../../../../../hooks/use_lens_attributes';
import { useMetricsDataViewContext } from '../../../hooks/use_data_view';
import { useUnifiedSearchContext } from '../../../hooks/use_unified_search';
import { FormulaConfig, XYLayerOptions } from '../../../../../../common/visualizations';
@ -31,7 +24,7 @@ import { METRIC_CHART_MIN_HEIGHT } from '../../../constants';
export interface MetricChartProps extends Pick<TypedLensByValueInput, 'id' | 'overrides'> {
title: string;
layers: Array<Layer<XYLayerOptions, FormulaConfig[], LayerType>>;
layers: Array<Layer<XYLayerOptions, FormulaConfig[]>>;
}
const lensStyle: CSSProperties = {
@ -128,9 +121,9 @@ export const MetricChart = ({ id, title, layers, overrides }: MetricChartProps)
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s" textAlign="center">
<EuiI18n
token="'xpack.infra.hostsViewPage.errorOnLoadingLensDependencies'"
default="There was an error trying to load Lens Plugin."
<FormattedMessage
id="xpack.infra.hostsViewPage.errorOnLoadingLensDependencies"
defaultMessage="There was an error trying to load Lens Plugin."
/>
</EuiText>
</EuiFlexItem>

View file

@ -214,7 +214,7 @@ export const MetricsGrid = React.memo(() => {
<EuiSpacer size="s" />
<EuiFlexGrid columns={2} gutterSize="s" data-test-subj="hostsView-metricChart">
{CHARTS_IN_ORDER.map((chartProp, index) => (
<EuiFlexItem key={index}>
<EuiFlexItem key={index} grow={false}>
<MetricChart {...chartProp} />
</EuiFlexItem>
))}

View file

@ -8,6 +8,7 @@
import useAsync from 'react-use/lib/useAsync';
import { v4 as uuidv4 } from 'uuid';
import { DEFAULT_LOG_VIEW, LogViewReference } from '@kbn/logs-shared-plugin/common';
import { useCallback } from 'react';
import { useLazyRef } from '../../../../hooks/use_lazy_ref';
import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana';
@ -57,5 +58,16 @@ export const useLogViewReference = ({ id, extraFields = [] }: Props) => {
};
});
return { logViewReference: logViewReference.current, loading };
const getLogsDataView = useCallback(
async (reference?: LogViewReference | null) => {
if (reference) {
const resolvedLogview = await logsShared.logViews.client.getResolvedLogView(reference);
return resolvedLogview.dataViewReference;
}
},
[logsShared.logViews.client]
);
return { logViewReference: logViewReference.current, loading, getLogsDataView };
};

View file

@ -6,12 +6,12 @@
*/
import createContainer from 'constate';
import { useCallback, useEffect, useState } from 'react';
import DateMath from '@kbn/datemath';
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 { parseDateRange } from '../../../../utils/datemath';
import { useKibanaQuerySettings } from '../../../../utils/use_kibana_query_settings';
import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana';
import { telemetryTimeRangeFormatter } from '../../../../../common/formatters/telemetry_time_range';
@ -102,9 +102,7 @@ export const useUnifiedSearch = () => {
const getParsedDateRange = useCallback(() => {
const defaults = getDefaultTimestamps();
const from = DateMath.parse(searchCriteria.dateRange.from)?.toISOString() ?? defaults.from;
const to =
DateMath.parse(searchCriteria.dateRange.to, { roundUp: true })?.toISOString() ?? defaults.to;
const { from = defaults.from, to = defaults.to } = parseDateRange(searchCriteria.dateRange);
return { from, to };
}, [searchCriteria.dateRange]);

View file

@ -6,6 +6,7 @@
*/
import dateMath, { Unit } from '@kbn/datemath';
import { TimeRange } from '@kbn/es-query';
import { chain } from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/pipeable';
import * as rt from 'io-ts';
@ -307,3 +308,10 @@ function isDateInRange(date: string | number): boolean {
return false;
}
}
export function parseDateRange(dateRange: TimeRange) {
const from = dateMath.parse(dateRange.from)?.toISOString();
const to = dateMath.parse(dateRange.to, { roundUp: true })?.toISOString();
return { from, to };
}

View file

@ -9,33 +9,11 @@ import {
BooleanRelation,
buildCombinedFilter,
buildPhraseFilter,
buildExistsFilter,
Filter,
isCombinedFilter,
} from '@kbn/es-query';
import type { DataView } from '@kbn/data-views-plugin/common';
export const buildExistsHostsFilter = ({
field,
dataView,
}: {
field: string;
dataView?: DataView;
}) => {
if (!dataView) {
return {
meta: {},
query: {
exists: {
field,
},
},
};
}
const indexField = dataView.getFieldByName(field)!;
return buildExistsFilter(indexField, dataView);
};
export const buildCombinedHostsFilter = ({
field,
values,
@ -45,7 +23,8 @@ export const buildCombinedHostsFilter = ({
field: string;
dataView?: DataView;
}) => {
if (!dataView) {
const indexField = dataView?.getFieldByName(field);
if (!dataView || !indexField) {
return {
query: {
terms: {
@ -55,7 +34,6 @@ export const buildCombinedHostsFilter = ({
meta: {},
};
}
const indexField = dataView.getFieldByName(field)!;
const filtersFromValues = values.map((value) => buildPhraseFilter(indexField, value, dataView));
return buildCombinedFilter(BooleanRelation.OR, filtersFromValues, dataView);

View file

@ -254,22 +254,18 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
START_HOST_PROCESSES_DATE.format(timepickerFormat),
END_HOST_PROCESSES_DATE.format(timepickerFormat)
);
});
beforeEach(async () => {
await pageObjects.infraHostsView.clickTableOpenFlyoutButton();
});
afterEach(async () => {
after(async () => {
await retry.try(async () => {
await pageObjects.infraHostsView.clickCloseFlyoutButton();
});
});
describe('Overview Tab', () => {
it('should render 4 metrics trend tiles', async () => {
const hosts = await pageObjects.infraHostsView.getAllKPITiles();
expect(hosts.length).to.equal(5);
before(async () => {
await pageObjects.infraHostsView.clickOverviewFlyoutTab();
});
[
@ -287,6 +283,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
});
});
it('should render 8 charts in the Metrics section', async () => {
const hosts = await pageObjects.infraHostsView.getAssetDetailsMetricsCharts();
expect(hosts.length).to.equal(8);
});
it('should navigate to metadata tab', async () => {
await pageObjects.infraHostsView.clickShowAllMetadataOverviewTab();
await pageObjects.header.waitUntilLoadingHasFinished();
@ -295,11 +297,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
describe('Metadata Tab', () => {
it('should render metadata tab, pin/unpin row, add and remove filter', async () => {
before(async () => {
await pageObjects.infraHostsView.clickMetadataFlyoutTab();
});
const metadataTab = await pageObjects.infraHostsView.getMetadataTabName();
expect(metadataTab).to.contain('Metadata');
it('should render metadata tab, add and remove filter', async () => {
await pageObjects.infraHostsView.metadataTableExist();
// Add Pin
@ -334,16 +336,31 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await pageObjects.infraHostsView.getRemoveFilterExist();
expect(removeFilterShouldNotExist).to.be(false);
});
});
it('should render metadata tab, pin and unpin table row', async () => {
const metadataTab = await pageObjects.infraHostsView.getMetadataTabName();
expect(metadataTab).to.contain('Metadata');
it('should render metadata tab, pin and unpin table row', async () => {
// Add Pin
await pageObjects.infraHostsView.clickAddMetadataPin();
expect(await pageObjects.infraHostsView.getRemovePinExist()).to.be(true);
// Persist pin after refresh
await browser.refresh();
await retry.try(async () => {
await pageObjects.infraHome.waitForLoading();
const removePinExist = await pageObjects.infraHostsView.getRemovePinExist();
expect(removePinExist).to.be(true);
});
// Remove Pin
await pageObjects.infraHostsView.clickRemoveMetadataPin();
expect(await pageObjects.infraHostsView.getRemovePinExist()).to.be(false);
});
});
describe('Processes Tab', () => {
it('should render processes tab and with Total Value summary', async () => {
before(async () => {
await pageObjects.infraHostsView.clickProcessesFlyoutTab();
});
it('should render processes tab and with Total Value summary', async () => {
const processesTotalValue =
await pageObjects.infraHostsView.getProcessesTabContentTotalValue();
const processValue = await processesTotalValue.getVisibleText();
@ -351,7 +368,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
it('should expand processes table row', async () => {
await pageObjects.infraHostsView.clickProcessesFlyoutTab();
await pageObjects.infraHostsView.getProcessesTable();
await pageObjects.infraHostsView.getProcessesTableBody();
await pageObjects.infraHostsView.clickProcessesTableExpandButton();
@ -359,8 +375,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
describe('Logs Tab', () => {
it('should render logs tab', async () => {
before(async () => {
await pageObjects.infraHostsView.clickLogsFlyoutTab();
});
it('should render logs tab', async () => {
await testSubjects.existOrFail('infraAssetDetailsLogsTabContent');
});
});
@ -382,23 +400,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await returnTo(HOSTS_VIEW_PATH);
});
describe('Processes Tab', () => {
it('should render processes tab and with Total Value summary', async () => {
await pageObjects.infraHostsView.clickProcessesFlyoutTab();
const processesTotalValue =
await pageObjects.infraHostsView.getProcessesTabContentTotalValue();
const processValue = await processesTotalValue.getVisibleText();
expect(processValue).to.eql('313');
});
it('should expand processes table row', async () => {
await pageObjects.infraHostsView.clickProcessesFlyoutTab();
await pageObjects.infraHostsView.getProcessesTable();
await pageObjects.infraHostsView.getProcessesTableBody();
await pageObjects.infraHostsView.clickProcessesTableExpandButton();
});
});
});
describe('#Page Content', () => {
@ -487,11 +488,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
describe('KPI tiles', () => {
it('should render 5 metrics trend tiles', async () => {
const hosts = await pageObjects.infraHostsView.getAllKPITiles();
expect(hosts.length).to.equal(5);
});
[
{ metric: 'hostsCount', value: '6' },
{ metric: 'cpuUsage', value: '0.8%' },

View file

@ -40,6 +40,10 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) {
return testSubjects.click('euiFlyoutCloseButton');
},
async clickOverviewFlyoutTab() {
return testSubjects.click('hostsView-flyout-tabs-overview');
},
async clickMetadataFlyoutTab() {
return testSubjects.click('hostsView-flyout-tabs-metadata');
},
@ -199,6 +203,11 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) {
return div.getAttribute('title');
},
async getAssetDetailsMetricsCharts() {
const container = await testSubjects.find('assetDetailsMetricsChartGrid');
return container.findAllByCssSelector('[data-test-subj*="assetDetailsMetricsChart"]');
},
getMetadataTab() {
return testSubjects.find('hostsView-flyout-tabs-metadata');
},