mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
b157217f3d
commit
25ff25959d
51 changed files with 789 additions and 1319 deletions
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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',
|
||||
};
|
|
@ -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: [],
|
||||
},
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'>;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 } })}
|
||||
/>
|
||||
|
|
|
@ -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} />,
|
||||
|
|
|
@ -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;
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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]);
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
`;
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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}
|
||||
/>
|
||||
);
|
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -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 },
|
||||
|
|
|
@ -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>
|
|
@ -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}
|
||||
/>
|
|
@ -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" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -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;
|
||||
`}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -18,7 +18,7 @@ const stories: Meta<ProcessesProps> = {
|
|||
args: {
|
||||
nodeName: 'host1',
|
||||
nodeType: 'host',
|
||||
currentTime: 1683630468,
|
||||
currentTimestamp: 1683630468,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 };
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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%' },
|
||||
|
|
|
@ -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');
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue