mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Infra UI] Replace node details flyout with asset details flyout in the inventory page (#166965)
Closes #161754 Closes #166807 To make the testing and review easier I merged the old components [cleanup PR](https://github.com/jennypavlova/kibana/pull/5) into this one ## Summary This PR replaces the old node details view with the asset details flyout ### Old  ### New  ### Testing 1. Go to inventory 2. Click on a host in the waffle map 3. Click on any **host** - These changes are related only if a `Host` is selected- in the case of a pod the view shouldn't be changed:  4. Check the new flyout functionality3557821c
-7964-466e-8514-84c2f81bc2fd Note: the selected host should have a border like in the previous version (this I fixed in the [last commit](ff4753aa06
)) so it should be added if there is a selected node: <img width="1193" alt="image" src="6646fe47
-6333-435a-a5ec-248339402224"> --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
726f212d0c
commit
549195ce4f
43 changed files with 673 additions and 2725 deletions
|
@ -6,9 +6,9 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ContentTabIds, type Tab } from '../../../../../components/asset_details/types';
|
||||
import { ContentTabIds, type Tab } from '../../components/asset_details/types';
|
||||
|
||||
export const orderedFlyoutTabs: Tab[] = [
|
||||
export const commonFlyoutTabs: Tab[] = [
|
||||
{
|
||||
id: ContentTabIds.OVERVIEW,
|
||||
name: i18n.translate('xpack.infra.nodeDetails.tabs.overview.title', {
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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 createContainter from 'constate';
|
||||
import { fold } from 'fp-ts/lib/Either';
|
||||
import { identity } from 'fp-ts/lib/function';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { useEffect } from 'react';
|
||||
import { ProcessListAPIResponse, ProcessListAPIResponseRT } from '../../../../common/http_api';
|
||||
import { throwErrors, createPlainError } from '../../../../common/runtime_types';
|
||||
import { useHTTPRequest } from '../../../hooks/use_http_request';
|
||||
import { useSourceContext } from '../../../containers/metrics_source';
|
||||
|
||||
export interface SortBy {
|
||||
name: string;
|
||||
isAscending: boolean;
|
||||
}
|
||||
|
||||
export function useProcessList(
|
||||
hostTerm: Record<string, string>,
|
||||
to: number,
|
||||
sortBy: SortBy,
|
||||
searchFilter: object
|
||||
) {
|
||||
const { createDerivedIndexPattern } = useSourceContext();
|
||||
const indexPattern = createDerivedIndexPattern().title;
|
||||
|
||||
const decodeResponse = (response: any) => {
|
||||
return pipe(
|
||||
ProcessListAPIResponseRT.decode(response),
|
||||
fold(throwErrors(createPlainError), identity)
|
||||
);
|
||||
};
|
||||
|
||||
const parsedSortBy =
|
||||
sortBy.name === 'runtimeLength'
|
||||
? {
|
||||
...sortBy,
|
||||
name: 'startTime',
|
||||
}
|
||||
: sortBy;
|
||||
|
||||
const { error, loading, response, makeRequest } = useHTTPRequest<ProcessListAPIResponse>(
|
||||
'/api/metrics/process_list',
|
||||
'POST',
|
||||
JSON.stringify({
|
||||
hostTerm,
|
||||
indexPattern,
|
||||
to,
|
||||
sortBy: parsedSortBy,
|
||||
searchFilter,
|
||||
}),
|
||||
decodeResponse
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
makeRequest();
|
||||
}, [makeRequest]);
|
||||
|
||||
return {
|
||||
error: (error && error.message) || null,
|
||||
loading,
|
||||
response,
|
||||
makeRequest,
|
||||
};
|
||||
}
|
||||
|
||||
function useProcessListParams(props: { hostTerm: Record<string, string>; to: number }) {
|
||||
const { hostTerm, to } = props;
|
||||
const { createDerivedIndexPattern } = useSourceContext();
|
||||
const indexPattern = createDerivedIndexPattern().title;
|
||||
return { hostTerm, indexPattern, to };
|
||||
}
|
||||
const ProcessListContext = createContainter(useProcessListParams);
|
||||
export const [ProcessListContextProvider, useProcessListContext] = ProcessListContext;
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 { fold } from 'fp-ts/lib/Either';
|
||||
import { identity } from 'fp-ts/lib/function';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
ProcessListAPIChartResponse,
|
||||
ProcessListAPIChartResponseRT,
|
||||
} from '../../../../common/http_api';
|
||||
import { throwErrors, createPlainError } from '../../../../common/runtime_types';
|
||||
import { useHTTPRequest } from '../../../hooks/use_http_request';
|
||||
import { useProcessListContext } from './use_process_list';
|
||||
|
||||
export function useProcessListRowChart(command: string) {
|
||||
const [inErrorState, setInErrorState] = useState(false);
|
||||
const decodeResponse = (response: any) => {
|
||||
return pipe(
|
||||
ProcessListAPIChartResponseRT.decode(response),
|
||||
fold(throwErrors(createPlainError), identity)
|
||||
);
|
||||
};
|
||||
const { hostTerm, indexPattern, to } = useProcessListContext();
|
||||
|
||||
const { error, loading, response, makeRequest } = useHTTPRequest<ProcessListAPIChartResponse>(
|
||||
'/api/metrics/process_list/chart',
|
||||
'POST',
|
||||
JSON.stringify({
|
||||
hostTerm,
|
||||
indexPattern,
|
||||
to,
|
||||
command,
|
||||
}),
|
||||
decodeResponse
|
||||
);
|
||||
|
||||
useEffect(() => setInErrorState(true), [error]);
|
||||
useEffect(() => setInErrorState(false), [loading]);
|
||||
|
||||
useEffect(() => {
|
||||
makeRequest();
|
||||
}, [makeRequest]);
|
||||
|
||||
return {
|
||||
error: inErrorState,
|
||||
loading,
|
||||
response,
|
||||
makeRequest,
|
||||
};
|
||||
}
|
|
@ -19,15 +19,15 @@ import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
|||
import { first, last } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTimelineChartTheme } from '../../../../../../../utils/use_timeline_chart_theme';
|
||||
import { Color } from '../../../../../../../../common/color_palette';
|
||||
import { createFormatter } from '../../../../../../../../common/formatters';
|
||||
import { MetricsExplorerAggregation } from '../../../../../../../../common/http_api';
|
||||
import { calculateDomain } from '../../../../../metrics_explorer/components/helpers/calculate_domain';
|
||||
import { MetricExplorerSeriesChart } from '../../../../../metrics_explorer/components/series_chart';
|
||||
import { MetricsExplorerChartType } from '../../../../../metrics_explorer/hooks/use_metrics_explorer_options';
|
||||
import { useProcessListRowChart } from '../../../../hooks/use_process_list_row_chart';
|
||||
import { calculateDomain } from '../../../../pages/metrics/metrics_explorer/components/helpers/calculate_domain';
|
||||
import { useProcessListRowChart } from '../../hooks/use_process_list_row_chart';
|
||||
import { useTimelineChartTheme } from '../../../../utils/use_timeline_chart_theme';
|
||||
import { MetricExplorerSeriesChart } from '../../../../pages/metrics/metrics_explorer/components/series_chart';
|
||||
import { Color } from '../../../../../common/color_palette';
|
||||
import { createFormatter } from '../../../../../common/formatters';
|
||||
import { MetricsExplorerAggregation } from '../../../../../common/http_api';
|
||||
import { Process } from './types';
|
||||
import { MetricsExplorerChartType } from '../../../../../common/metrics_explorer_views/types';
|
||||
|
||||
interface Props {
|
||||
command: string;
|
|
@ -30,9 +30,9 @@ import { css } from '@emotion/react';
|
|||
import { EuiTableRow } from '@elastic/eui';
|
||||
import { EuiIcon } from '@elastic/eui';
|
||||
import { FORMATTERS } from '../../../../../common/formatters';
|
||||
import type { SortBy } from '../../../../pages/metrics/inventory_view/hooks/use_process_list';
|
||||
import type { SortBy } from '../../hooks/use_process_list';
|
||||
import type { Process } from './types';
|
||||
import { ProcessRow } from '../../../../pages/metrics/inventory_view/components/node_details/tabs/processes/process_row';
|
||||
import { ProcessRow } from './process_row';
|
||||
import { StateBadge } from './state_badge';
|
||||
import { STATE_ORDER } from './states';
|
||||
import type { ProcessListAPIResponse } from '../../../../../common/http_api';
|
||||
|
|
|
@ -10,7 +10,7 @@ import { useSourceContext } from '../../../../../containers/metrics_source';
|
|||
import { useUnifiedSearchContext } from '../../hooks/use_unified_search';
|
||||
import type { HostNodeRow } from '../../hooks/use_hosts_table';
|
||||
import { AssetDetails } from '../../../../../components/asset_details/asset_details';
|
||||
import { orderedFlyoutTabs } from './tabs';
|
||||
import { commonFlyoutTabs } from '../../../../../common/asset_details_config/asset_details_tabs';
|
||||
|
||||
export interface Props {
|
||||
node: HostNodeRow;
|
||||
|
@ -31,7 +31,7 @@ export const FlyoutWrapper = ({ node: { name }, closeFlyout }: Props) => {
|
|||
showActionsColumn: true,
|
||||
},
|
||||
}}
|
||||
tabs={orderedFlyoutTabs}
|
||||
tabs={commonFlyoutTabs}
|
||||
links={['apmServices', 'nodeDetails']}
|
||||
renderMode={{
|
||||
mode: 'flyout',
|
||||
|
|
|
@ -1,233 +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 { EuiPortal, EuiTabs, EuiTab, EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui';
|
||||
import { EuiOutsideClickDetector } from '@elastic/eui';
|
||||
import { EuiIcon, EuiButtonIcon } from '@elastic/eui';
|
||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { useLinkProps } from '@kbn/observability-shared-plugin/public';
|
||||
import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../../lib/lib';
|
||||
import { InventoryItemType } from '../../../../../../common/inventory_models/types';
|
||||
import { MetricsTab } from './tabs/metrics/metrics';
|
||||
import { LogsTab } from './tabs/logs';
|
||||
import { ProcessesTab } from './tabs/processes';
|
||||
import { PropertiesTab } from './tabs/properties';
|
||||
import { AnomaliesTab } from './tabs/anomalies/anomalies';
|
||||
import { OsqueryTab } from './tabs/osquery';
|
||||
import { OVERLAY_Y_START, OVERLAY_BOTTOM_MARGIN } from './tabs/shared';
|
||||
import { useNodeDetailsRedirect } from '../../../../link_to';
|
||||
import { findInventoryModel } from '../../../../../../common/inventory_models';
|
||||
import { navigateToUptime } from '../../lib/navigate_to_uptime';
|
||||
import { InfraClientCoreStart, InfraClientStartDeps } from '../../../../../types';
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
onClose(): void;
|
||||
options: InfraWaffleMapOptions;
|
||||
currentTime: number;
|
||||
node: InfraWaffleMapNode;
|
||||
nodeType: InventoryItemType;
|
||||
openAlertFlyout(): void;
|
||||
}
|
||||
export const NodeContextPopover = ({
|
||||
isOpen,
|
||||
node,
|
||||
nodeType,
|
||||
currentTime,
|
||||
options,
|
||||
onClose,
|
||||
openAlertFlyout,
|
||||
}: Props) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const tabConfigs = [MetricsTab, LogsTab, ProcessesTab, PropertiesTab, AnomaliesTab, OsqueryTab];
|
||||
const inventoryModel = findInventoryModel(nodeType);
|
||||
const nodeDetailFrom = currentTime - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000;
|
||||
const { application, share } = useKibana<InfraClientCoreStart & InfraClientStartDeps>().services;
|
||||
const { getNodeDetailUrl } = useNodeDetailsRedirect();
|
||||
const uiCapabilities = application?.capabilities;
|
||||
const canCreateAlerts = useMemo(
|
||||
() => Boolean(uiCapabilities?.infrastructure?.save),
|
||||
[uiCapabilities]
|
||||
);
|
||||
|
||||
const tabs = useMemo(() => {
|
||||
return tabConfigs.map((m) => {
|
||||
const TabContent = m.content;
|
||||
return {
|
||||
...m,
|
||||
content: (
|
||||
<TabContent
|
||||
onClose={onClose}
|
||||
node={node}
|
||||
nodeType={nodeType}
|
||||
currentTime={currentTime}
|
||||
options={options}
|
||||
/>
|
||||
),
|
||||
};
|
||||
});
|
||||
}, [tabConfigs, node, nodeType, currentTime, onClose, options]);
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState(0);
|
||||
|
||||
const nodeDetailMenuItemLinkProps = useLinkProps({
|
||||
...getNodeDetailUrl({
|
||||
assetType: nodeType,
|
||||
assetId: node.id,
|
||||
search: {
|
||||
from: nodeDetailFrom,
|
||||
to: currentTime,
|
||||
name: node.name,
|
||||
},
|
||||
}),
|
||||
});
|
||||
const apmField = nodeType === 'host' ? 'host.hostname' : inventoryModel.fields.id;
|
||||
const apmTracesMenuItemLinkProps = useLinkProps({
|
||||
app: 'apm',
|
||||
hash: 'traces',
|
||||
search: {
|
||||
kuery: `${apmField}:"${node.id}"`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiPortal>
|
||||
<EuiOutsideClickDetector onOutsideClick={onClose}>
|
||||
<OverlayPanel>
|
||||
<OverlayHeader>
|
||||
<EuiFlexGroup responsive={false} gutterSize="m">
|
||||
<OverlayTitle grow={true}>
|
||||
<EuiTitle size="xs">
|
||||
<h4>{node.name}</h4>
|
||||
</EuiTitle>
|
||||
</OverlayTitle>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="m" responsive={false}>
|
||||
{canCreateAlerts && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="infraNodeContextPopoverCreateInventoryRuleButton"
|
||||
onClick={openAlertFlyout}
|
||||
size="xs"
|
||||
iconSide={'left'}
|
||||
flush="both"
|
||||
iconType="bell"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.infra.nodeDetails.createAlertLink"
|
||||
defaultMessage="Create inventory rule"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="infraNodeContextPopoverOpenAsPageButton"
|
||||
size="xs"
|
||||
iconSide={'left'}
|
||||
iconType={'popout'}
|
||||
flush="both"
|
||||
{...nodeDetailMenuItemLinkProps}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.infra.nodeDetails.openAsPage"
|
||||
defaultMessage="Open as page"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="infraNodeContextPopoverButton"
|
||||
size="s"
|
||||
onClick={onClose}
|
||||
iconType="cross"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiTabs size="s">
|
||||
{tabs.map((tab, i) => (
|
||||
<EuiTab
|
||||
key={tab.id}
|
||||
isSelected={i === selectedTab}
|
||||
onClick={() => setSelectedTab(i)}
|
||||
>
|
||||
{tab.name}
|
||||
</EuiTab>
|
||||
))}
|
||||
<EuiTab {...apmTracesMenuItemLinkProps}>
|
||||
<EuiIcon type="popout" />{' '}
|
||||
<FormattedMessage
|
||||
id="xpack.infra.infra.nodeDetails.apmTabLabel"
|
||||
defaultMessage="APM"
|
||||
/>
|
||||
</EuiTab>
|
||||
<EuiTab onClick={() => navigateToUptime(share.url.locators, nodeType, node)}>
|
||||
<EuiIcon type="popout" />{' '}
|
||||
<FormattedMessage
|
||||
id="xpack.infra.infra.nodeDetails.updtimeTabLabel"
|
||||
defaultMessage="Uptime"
|
||||
/>
|
||||
</EuiTab>
|
||||
</EuiTabs>
|
||||
</OverlayHeader>
|
||||
{tabs[selectedTab].content}
|
||||
</OverlayPanel>
|
||||
</EuiOutsideClickDetector>
|
||||
</EuiPortal>
|
||||
);
|
||||
};
|
||||
|
||||
const OverlayHeader = euiStyled.div`
|
||||
padding-top: ${(props) => props.theme.eui.euiSizeM};
|
||||
padding-right: ${(props) => props.theme.eui.euiSizeM};
|
||||
padding-left: ${(props) => props.theme.eui.euiSizeM};
|
||||
background-color: ${(props) => props.theme.eui.euiPageBackgroundColor};
|
||||
box-shadow: inset 0 -1px ${(props) => props.theme.eui.euiBorderColor};
|
||||
`;
|
||||
|
||||
const OverlayPanel = euiStyled(EuiPanel).attrs({ paddingSize: 'none' })`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: ${OVERLAY_Y_START}px;
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
z-index: 2;
|
||||
max-height: calc(100vh - ${OVERLAY_Y_START + OVERLAY_BOTTOM_MARGIN}px);
|
||||
overflow: hidden;
|
||||
|
||||
@media (max-width: 752px) {
|
||||
border-radius: 0px !important;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
top: 97px;
|
||||
bottom: 0;
|
||||
max-height: calc(100vh - 97px);
|
||||
max-width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const OverlayTitle = euiStyled(EuiFlexItem)`
|
||||
overflow: hidden;
|
||||
& h4 {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`;
|
|
@ -1,29 +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 React from 'react';
|
||||
import { AnomaliesTable } from '../../../ml/anomaly_detection/anomalies_table/anomalies_table';
|
||||
import { TabContent, TabProps } from '../shared';
|
||||
|
||||
const TabComponent = (props: TabProps) => {
|
||||
const { node, onClose } = props;
|
||||
|
||||
return (
|
||||
<TabContent>
|
||||
<AnomaliesTable closeFlyout={onClose} hostName={node.name} />
|
||||
</TabContent>
|
||||
);
|
||||
};
|
||||
|
||||
export const AnomaliesTab = {
|
||||
id: 'anomalies',
|
||||
name: i18n.translate('xpack.infra.nodeDetails.tabs.anomalies', {
|
||||
defaultMessage: 'Anomalies',
|
||||
}),
|
||||
content: TabComponent,
|
||||
};
|
|
@ -1,110 +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, useMemo, useState } from 'react';
|
||||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFieldSearch } from '@elastic/eui';
|
||||
import { EuiFlexGroup } from '@elastic/eui';
|
||||
import { EuiFlexItem } from '@elastic/eui';
|
||||
import { EuiButtonEmpty } from '@elastic/eui';
|
||||
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
|
||||
import { LogStream } from '@kbn/logs-shared-plugin/public';
|
||||
import { useKibanaContextForPlugin } from '../../../../../../hooks/use_kibana';
|
||||
import { TabContent, TabProps } from './shared';
|
||||
import { useWaffleOptionsContext } from '../../../hooks/use_waffle_options';
|
||||
import { findInventoryFields } from '../../../../../../../common/inventory_models';
|
||||
|
||||
const TabComponent = (props: TabProps) => {
|
||||
const { services } = useKibanaContextForPlugin();
|
||||
const { locators } = services;
|
||||
const [textQuery, setTextQuery] = useState('');
|
||||
const [textQueryDebounced, setTextQueryDebounced] = useState('');
|
||||
const endTimestamp = props.currentTime;
|
||||
const startTimestamp = endTimestamp - 60 * 60 * 1000; // 60 minutes
|
||||
const { nodeType } = useWaffleOptionsContext();
|
||||
const { node } = props;
|
||||
|
||||
useDebounce(() => setTextQueryDebounced(textQuery), textQueryThrottleInterval, [textQuery]);
|
||||
|
||||
const filter = useMemo(() => {
|
||||
const query = [
|
||||
`${findInventoryFields(nodeType).id}: "${node.id}"`,
|
||||
...(textQueryDebounced !== '' ? [textQueryDebounced] : []),
|
||||
].join(' and ');
|
||||
|
||||
return {
|
||||
language: 'kuery',
|
||||
query,
|
||||
};
|
||||
}, [nodeType, node.id, textQueryDebounced]);
|
||||
|
||||
const onQueryChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTextQuery(e.target.value);
|
||||
}, []);
|
||||
|
||||
const logsUrl = useMemo(() => {
|
||||
return locators.nodeLogsLocator.getRedirectUrl({
|
||||
nodeType,
|
||||
nodeId: node.id,
|
||||
time: startTimestamp,
|
||||
filter: textQueryDebounced,
|
||||
});
|
||||
}, [locators.nodeLogsLocator, node.id, nodeType, startTimestamp, textQueryDebounced]);
|
||||
|
||||
return (
|
||||
<TabContent>
|
||||
<EuiFlexGroup gutterSize={'m'} alignItems={'center'} responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<EuiFieldSearch
|
||||
data-test-subj="infraTabComponentFieldSearch"
|
||||
fullWidth
|
||||
placeholder={i18n.translate('xpack.infra.nodeDetails.logs.textFieldPlaceholder', {
|
||||
defaultMessage: 'Search for log entries...',
|
||||
})}
|
||||
value={textQuery}
|
||||
isClearable
|
||||
onChange={onQueryChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<RedirectAppLinks coreStart={services}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="infraTabComponentOpenInLogsButton"
|
||||
size={'xs'}
|
||||
flush={'both'}
|
||||
iconType={'popout'}
|
||||
href={logsUrl}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.nodeDetails.logs.openLogsLink"
|
||||
defaultMessage="Open in Logs"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</RedirectAppLinks>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<LogStream
|
||||
logView={{ type: 'log-view-reference', logViewId: 'default' }}
|
||||
startTimestamp={startTimestamp}
|
||||
endTimestamp={endTimestamp}
|
||||
query={filter}
|
||||
/>
|
||||
</TabContent>
|
||||
);
|
||||
};
|
||||
|
||||
export const LogsTab = {
|
||||
id: 'logs',
|
||||
name: i18n.translate('xpack.infra.nodeDetails.tabs.logs', {
|
||||
defaultMessage: 'Logs',
|
||||
}),
|
||||
content: TabComponent,
|
||||
};
|
||||
|
||||
const textQueryThrottleInterval = 1000; // milliseconds
|
|
@ -1,63 +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 { EuiText } from '@elastic/eui';
|
||||
import { EuiFlexItem } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup } from '@elastic/eui';
|
||||
import { EuiIcon } from '@elastic/eui';
|
||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import { colorTransformer } from '../../../../../../../../common/color_palette';
|
||||
import { MetricsExplorerOptionsMetric } from '../../../../../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,103 +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,
|
||||
ChartSizeArray,
|
||||
PointerUpdateListener,
|
||||
Position,
|
||||
Settings,
|
||||
TickFormatter,
|
||||
TooltipProps,
|
||||
Tooltip,
|
||||
} from '@elastic/charts';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import { useTimelineChartTheme } from '../../../../../../../utils/use_timeline_chart_theme';
|
||||
import { MetricsExplorerSeries } from '../../../../../../../../common/http_api';
|
||||
import { MetricExplorerSeriesChart } from '../../../../../metrics_explorer/components/series_chart';
|
||||
import {
|
||||
MetricsExplorerChartType,
|
||||
MetricsExplorerOptionsMetric,
|
||||
} from '../../../../../metrics_explorer/hooks/use_metrics_explorer_options';
|
||||
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 chartTheme = useTimelineChartTheme();
|
||||
const metrics = series.map((chartSeries) => chartSeries.metric);
|
||||
const tooltipProps: TooltipProps = {
|
||||
headerFormatter: ({ value }) => moment(value).format('Y-MM-DD HH:mm:ss.SSS'),
|
||||
};
|
||||
|
||||
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 {...tooltipProps} />
|
||||
<Settings
|
||||
onPointerUpdate={onPointerUpdate}
|
||||
baseTheme={chartTheme.baseTheme}
|
||||
theme={chartTheme.theme}
|
||||
/>
|
||||
</Chart>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,8 +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.
|
||||
*/
|
||||
|
||||
export * from './metrics';
|
|
@ -1,460 +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 { i18n } from '@kbn/i18n';
|
||||
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 { TabContent, TabProps } from '../shared';
|
||||
import { useSnapshot } from '../../../../hooks/use_snaphot';
|
||||
import { useWaffleOptionsContext } from '../../../../hooks/use_waffle_options';
|
||||
import { useSourceContext } from '../../../../../../../containers/metrics_source';
|
||||
import { findInventoryFields } from '../../../../../../../../common/inventory_models';
|
||||
import { convertKueryToElasticSearchQuery } from '../../../../../../../utils/kuery';
|
||||
import { SnapshotMetricType } from '../../../../../../../../common/inventory_models/types';
|
||||
import {
|
||||
MetricsExplorerChartType,
|
||||
MetricsExplorerOptionsMetric,
|
||||
} from '../../../../../metrics_explorer/hooks/use_metrics_explorer_options';
|
||||
import { Color } from '../../../../../../../../common/color_palette';
|
||||
import {
|
||||
MetricsExplorerAggregation,
|
||||
MetricsExplorerSeries,
|
||||
} from '../../../../../../../../common/http_api';
|
||||
import { createInventoryMetricFormatter } from '../../../../lib/create_inventory_metric_formatter';
|
||||
import { calculateDomain } from '../../../../../metrics_explorer/components/helpers/calculate_domain';
|
||||
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';
|
||||
import { getCustomMetricLabel } from '../../../../../../../../common/formatters/get_custom_metric_label';
|
||||
import { createFormatterForMetric } from '../../../../../metrics_explorer/components/helpers/create_formatter_for_metric';
|
||||
|
||||
const ONE_HOUR = 60 * 60 * 1000;
|
||||
|
||||
const TabComponent = (props: TabProps) => {
|
||||
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 { nodeType, accountId, region, customMetrics } = useWaffleOptionsContext();
|
||||
const { currentTime, node } = props;
|
||||
const derivedIndexPattern = useMemo(
|
||||
() => createDerivedIndexPattern(),
|
||||
[createDerivedIndexPattern]
|
||||
);
|
||||
let filter = `${findInventoryFields(nodeType).id}: "${node.id}"`;
|
||||
|
||||
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 (
|
||||
<TabContent>
|
||||
<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>
|
||||
</TabContent>
|
||||
);
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export const MetricsTab = {
|
||||
id: 'metrics',
|
||||
name: i18n.translate('xpack.infra.nodeDetails.tabs.metrics', {
|
||||
defaultMessage: 'Metrics',
|
||||
}),
|
||||
content: TabComponent,
|
||||
};
|
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -1,63 +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 { EuiSkeletonText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useKibanaContextForPlugin } from '../../../../../../../hooks/use_kibana';
|
||||
import { TabContent, TabProps } from '../shared';
|
||||
import { useSourceContext } from '../../../../../../../containers/metrics_source';
|
||||
import { InventoryItemType } from '../../../../../../../../common/inventory_models/types';
|
||||
import { useMetadata } from '../../../../../../../components/asset_details/hooks/use_metadata';
|
||||
import { useWaffleTimeContext } from '../../../../hooks/use_waffle_time';
|
||||
|
||||
const TabComponent = (props: TabProps) => {
|
||||
const nodeId = props.node.id;
|
||||
const nodeType = props.nodeType as InventoryItemType;
|
||||
const { sourceId } = useSourceContext();
|
||||
const { currentTimeRange } = useWaffleTimeContext();
|
||||
const { loading, metadata } = useMetadata({
|
||||
assetId: nodeId,
|
||||
assetType: nodeType,
|
||||
sourceId,
|
||||
timeRange: currentTimeRange,
|
||||
});
|
||||
const {
|
||||
services: { osquery },
|
||||
} = useKibanaContextForPlugin();
|
||||
|
||||
// @ts-expect-error
|
||||
const OsqueryAction = osquery?.OsqueryAction;
|
||||
|
||||
// avoids component rerender when resizing the popover
|
||||
const content = useMemo(() => {
|
||||
// TODO: Add info when Osquery plugin is not available
|
||||
if (loading || !OsqueryAction) {
|
||||
return (
|
||||
<TabContent>
|
||||
<EuiSkeletonText lines={10} />
|
||||
</TabContent>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TabContent>
|
||||
<OsqueryAction agentId={metadata?.info?.agent?.id} hideAgentsField formType="simple" />
|
||||
</TabContent>
|
||||
);
|
||||
}, [OsqueryAction, loading, metadata]);
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
export const OsqueryTab = {
|
||||
id: 'osquery',
|
||||
name: i18n.translate('xpack.infra.nodeDetails.tabs.osquery', {
|
||||
defaultMessage: 'Osquery',
|
||||
}),
|
||||
content: TabComponent,
|
||||
};
|
|
@ -1,171 +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, { useMemo, useState, useCallback } from 'react';
|
||||
import { debounce } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiSearchBar,
|
||||
EuiSpacer,
|
||||
EuiEmptyPrompt,
|
||||
EuiButton,
|
||||
EuiText,
|
||||
EuiIconTip,
|
||||
Query,
|
||||
} from '@elastic/eui';
|
||||
import { getFieldByType } from '../../../../../../../../common/inventory_models';
|
||||
import {
|
||||
useProcessList,
|
||||
SortBy,
|
||||
ProcessListContextProvider,
|
||||
} from '../../../../hooks/use_process_list';
|
||||
import { TabContent, TabProps } from '../shared';
|
||||
import { STATE_NAMES } from './states';
|
||||
import { SummaryTable } from './summary_table';
|
||||
import { ProcessesTable } from './processes_table';
|
||||
import { parseSearchString } from './parse_search_string';
|
||||
|
||||
const TabComponent = ({ currentTime, node, nodeType }: TabProps) => {
|
||||
const [searchBarState, setSearchBarState] = useState<Query>(Query.MATCH_ALL);
|
||||
const [searchFilter, setSearchFilter] = useState<string>('');
|
||||
const [sortBy, setSortBy] = useState<SortBy>({
|
||||
name: 'cpu',
|
||||
isAscending: false,
|
||||
});
|
||||
|
||||
const hostTerm = useMemo(() => {
|
||||
const field = getFieldByType(nodeType) ?? nodeType;
|
||||
return { [field]: node.name };
|
||||
}, [node, nodeType]);
|
||||
|
||||
const {
|
||||
loading,
|
||||
error,
|
||||
response,
|
||||
makeRequest: reload,
|
||||
} = useProcessList(hostTerm, currentTime, sortBy, parseSearchString(searchFilter));
|
||||
|
||||
const debouncedSearchOnChange = useMemo(
|
||||
() => debounce<(queryText: string) => void>((queryText) => setSearchFilter(queryText), 500),
|
||||
[setSearchFilter]
|
||||
);
|
||||
|
||||
const searchBarOnChange = useCallback(
|
||||
({ query, queryText }) => {
|
||||
setSearchBarState(query);
|
||||
debouncedSearchOnChange(queryText);
|
||||
},
|
||||
[setSearchBarState, debouncedSearchOnChange]
|
||||
);
|
||||
|
||||
const clearSearchBar = useCallback(() => {
|
||||
setSearchBarState(Query.MATCH_ALL);
|
||||
setSearchFilter('');
|
||||
}, [setSearchBarState, setSearchFilter]);
|
||||
|
||||
return (
|
||||
<TabContent>
|
||||
<ProcessListContextProvider hostTerm={hostTerm} to={currentTime}>
|
||||
<SummaryTable
|
||||
isLoading={loading}
|
||||
processSummary={(!error ? response?.summary : null) ?? { total: 0 }}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiText>
|
||||
<h4>
|
||||
{i18n.translate('xpack.infra.metrics.nodeDetails.processesHeader', {
|
||||
defaultMessage: 'Top processes',
|
||||
})}{' '}
|
||||
<EuiIconTip
|
||||
aria-label={i18n.translate(
|
||||
'xpack.infra.metrics.nodeDetails.processesHeader.tooltipLabel',
|
||||
{
|
||||
defaultMessage: 'More info',
|
||||
}
|
||||
)}
|
||||
size="m"
|
||||
type="iInCircle"
|
||||
content={i18n.translate(
|
||||
'xpack.infra.metrics.nodeDetails.processesHeader.tooltipBody',
|
||||
{
|
||||
defaultMessage:
|
||||
'The table below aggregates the top CPU and top memory consuming processes. It does not display all processes.',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</h4>
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiSearchBar
|
||||
query={searchBarState}
|
||||
onChange={searchBarOnChange}
|
||||
box={{
|
||||
incremental: true,
|
||||
placeholder: i18n.translate('xpack.infra.metrics.nodeDetails.searchForProcesses', {
|
||||
defaultMessage: 'Search for processes…',
|
||||
}),
|
||||
}}
|
||||
filters={[
|
||||
{
|
||||
type: 'field_value_selection',
|
||||
field: 'state',
|
||||
name: 'State',
|
||||
operator: 'exact',
|
||||
multiSelect: false,
|
||||
options: Object.entries(STATE_NAMES).map(([value, view]: [string, string]) => ({
|
||||
value,
|
||||
view,
|
||||
})),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
{!error ? (
|
||||
<ProcessesTable
|
||||
currentTime={currentTime}
|
||||
isLoading={loading || !response}
|
||||
processList={response?.processList ?? []}
|
||||
sortBy={sortBy}
|
||||
setSortBy={setSortBy}
|
||||
clearSearchBar={clearSearchBar}
|
||||
/>
|
||||
) : (
|
||||
<EuiEmptyPrompt
|
||||
iconType="warning"
|
||||
title={
|
||||
<h4>
|
||||
{i18n.translate('xpack.infra.metrics.nodeDetails.processListError', {
|
||||
defaultMessage: 'Unable to load process data',
|
||||
})}
|
||||
</h4>
|
||||
}
|
||||
actions={
|
||||
<EuiButton
|
||||
data-test-subj="infraTabComponentTryAgainButton"
|
||||
color="primary"
|
||||
fill
|
||||
onClick={reload}
|
||||
>
|
||||
{i18n.translate('xpack.infra.metrics.nodeDetails.processListRetry', {
|
||||
defaultMessage: 'Try again',
|
||||
})}
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</ProcessListContextProvider>
|
||||
</TabContent>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProcessesTab = {
|
||||
id: 'processes',
|
||||
name: i18n.translate('xpack.infra.metrics.nodeDetails.tabs.processes', {
|
||||
defaultMessage: 'Processes',
|
||||
}),
|
||||
content: TabComponent,
|
||||
};
|
|
@ -1,39 +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.
|
||||
*/
|
||||
|
||||
export const parseSearchString = (query: string) => {
|
||||
if (query.trim() === '') {
|
||||
return [
|
||||
{
|
||||
match_all: {},
|
||||
},
|
||||
];
|
||||
}
|
||||
const elements = query
|
||||
.split(' ')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
const stateFilter = elements.filter((s) => s.startsWith('state='));
|
||||
const cmdlineFilters = elements.filter((s) => !s.startsWith('state='));
|
||||
return [
|
||||
...cmdlineFilters.map((clause) => ({
|
||||
query_string: {
|
||||
fields: ['system.process.cmdline'],
|
||||
query: `*${escapeReservedCharacters(clause)}*`,
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
})),
|
||||
...stateFilter.map((state) => ({
|
||||
match: {
|
||||
'system.process.state': state.replace('state=', ''),
|
||||
},
|
||||
})),
|
||||
];
|
||||
};
|
||||
|
||||
const escapeReservedCharacters = (clause: string) =>
|
||||
clause.replace(/([+\-=!\(\)\{\}\[\]^"~*?:\\/!]|&&|\|\|)/g, '\\$1');
|
|
@ -1,311 +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, { useMemo, useState, useCallback } from 'react';
|
||||
import { omit } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiTable,
|
||||
EuiTableHeader,
|
||||
EuiTableBody,
|
||||
EuiTableHeaderCell,
|
||||
EuiTableRowCell,
|
||||
EuiLoadingChart,
|
||||
EuiEmptyPrompt,
|
||||
EuiText,
|
||||
EuiLink,
|
||||
EuiButton,
|
||||
SortableProperties,
|
||||
LEFT_ALIGNMENT,
|
||||
RIGHT_ALIGNMENT,
|
||||
} from '@elastic/eui';
|
||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import { ProcessListAPIResponse } from '../../../../../../../../common/http_api';
|
||||
import { FORMATTERS } from '../../../../../../../../common/formatters';
|
||||
import { SortBy } from '../../../../hooks/use_process_list';
|
||||
import { Process } from './types';
|
||||
import { ProcessRow } from './process_row';
|
||||
import { StateBadge } from './state_badge';
|
||||
import { STATE_ORDER } from './states';
|
||||
|
||||
interface TableProps {
|
||||
processList: ProcessListAPIResponse['processList'];
|
||||
currentTime: number;
|
||||
isLoading: boolean;
|
||||
sortBy: SortBy;
|
||||
setSortBy: (s: SortBy) => void;
|
||||
clearSearchBar: () => void;
|
||||
}
|
||||
|
||||
function useSortableProperties<T>(
|
||||
sortablePropertyItems: Array<{
|
||||
name: string;
|
||||
getValue: (obj: T) => any;
|
||||
isAscending: boolean;
|
||||
}>,
|
||||
defaultSortProperty: string,
|
||||
callback: (s: SortBy) => void
|
||||
) {
|
||||
const [sortableProperties] = useState<SortableProperties<T>>(
|
||||
new SortableProperties(sortablePropertyItems, defaultSortProperty)
|
||||
);
|
||||
|
||||
return {
|
||||
updateSortableProperties: useCallback(
|
||||
(property) => {
|
||||
sortableProperties.sortOn(property);
|
||||
callback(omit(sortableProperties.getSortedProperty(), 'getValue'));
|
||||
},
|
||||
[sortableProperties, callback]
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export const ProcessesTable = ({
|
||||
processList,
|
||||
currentTime,
|
||||
isLoading,
|
||||
sortBy,
|
||||
setSortBy,
|
||||
clearSearchBar,
|
||||
}: TableProps) => {
|
||||
const { updateSortableProperties } = useSortableProperties<Process>(
|
||||
[
|
||||
{
|
||||
name: 'startTime',
|
||||
getValue: (item: any) => Date.parse(item.startTime),
|
||||
isAscending: true,
|
||||
},
|
||||
{
|
||||
name: 'cpu',
|
||||
getValue: (item: any) => item.cpu,
|
||||
isAscending: false,
|
||||
},
|
||||
{
|
||||
name: 'memory',
|
||||
getValue: (item: any) => item.memory,
|
||||
isAscending: false,
|
||||
},
|
||||
],
|
||||
'cpu',
|
||||
setSortBy
|
||||
);
|
||||
|
||||
const currentItems = useMemo(
|
||||
() =>
|
||||
processList.sort(
|
||||
(a, b) => STATE_ORDER.indexOf(a.state) - STATE_ORDER.indexOf(b.state)
|
||||
) as Process[],
|
||||
[processList]
|
||||
);
|
||||
|
||||
if (isLoading) return <LoadingPlaceholder />;
|
||||
|
||||
if (currentItems.length === 0)
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="search"
|
||||
titleSize="s"
|
||||
title={
|
||||
<strong>
|
||||
{i18n.translate('xpack.infra.metrics.nodeDetails.noProcesses', {
|
||||
defaultMessage: 'No processes found',
|
||||
})}
|
||||
</strong>
|
||||
}
|
||||
body={
|
||||
<EuiText size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.infra.metrics.nodeDetails.noProcessesBody"
|
||||
defaultMessage="Try modifying your filter. Only processes that are within the configured {metricbeatDocsLink} will display here."
|
||||
values={{
|
||||
metricbeatDocsLink: (
|
||||
<EuiLink
|
||||
data-test-subj="infraProcessesTableTopNByCpuOrMemoryLink"
|
||||
href="https://www.elastic.co/guide/en/beats/metricbeat/current/metricbeat-module-system.html"
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.metrics.nodeDetails.noProcessesBody.metricbeatDocsLinkText"
|
||||
defaultMessage="top N by CPU or Memory"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
}
|
||||
actions={
|
||||
<EuiButton
|
||||
data-test-subj="infraProcessesTableClearFiltersButton"
|
||||
onClick={clearSearchBar}
|
||||
>
|
||||
{i18n.translate('xpack.infra.metrics.nodeDetails.noProcessesClearFilters', {
|
||||
defaultMessage: 'Clear filters',
|
||||
})}
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTable data-test-subj="infraProcessesTable" responsive={false}>
|
||||
<EuiTableHeader>
|
||||
<EuiTableHeaderCell width={24} />
|
||||
{columns.map((column) => (
|
||||
<EuiTableHeaderCell
|
||||
key={`${String(column.field)}-header`}
|
||||
align={column.align ?? LEFT_ALIGNMENT}
|
||||
width={column.width}
|
||||
onSort={column.sortable ? () => updateSortableProperties(column.field) : undefined}
|
||||
isSorted={sortBy.name === column.field}
|
||||
isSortAscending={sortBy.name === column.field && sortBy.isAscending}
|
||||
>
|
||||
{column.name}
|
||||
</EuiTableHeaderCell>
|
||||
))}
|
||||
</EuiTableHeader>
|
||||
<StyledTableBody>
|
||||
<ProcessesTableBody items={currentItems} currentTime={currentTime} />
|
||||
</StyledTableBody>
|
||||
</EuiTable>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const LoadingPlaceholder = () => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '200px',
|
||||
padding: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<EuiLoadingChart size="xl" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface TableBodyProps {
|
||||
items: Process[];
|
||||
currentTime: number;
|
||||
}
|
||||
const ProcessesTableBody = ({ items, currentTime }: TableBodyProps) => (
|
||||
<>
|
||||
{items.map((item, i) => {
|
||||
const cells = columns.map((column) => (
|
||||
<EuiTableRowCell
|
||||
key={`${String(column.field)}-${i}`}
|
||||
mobileOptions={{ header: column.name }}
|
||||
align={column.align ?? LEFT_ALIGNMENT}
|
||||
textOnly={column.textOnly ?? true}
|
||||
>
|
||||
{column.render ? column.render(item[column.field], currentTime) : item[column.field]}
|
||||
</EuiTableRowCell>
|
||||
));
|
||||
return <ProcessRow cells={cells} item={item} key={`row-${i}`} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
const StyledTableBody = euiStyled(EuiTableBody)`
|
||||
& .euiTableCellContent {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
|
||||
}
|
||||
`;
|
||||
|
||||
const ONE_MINUTE = 60 * 1000;
|
||||
const ONE_HOUR = ONE_MINUTE * 60;
|
||||
const RuntimeCell = ({ startTime, currentTime }: { startTime: number; currentTime: number }) => {
|
||||
const runtimeLength = currentTime - startTime;
|
||||
let remainingRuntimeMS = runtimeLength;
|
||||
const runtimeHours = Math.floor(remainingRuntimeMS / ONE_HOUR);
|
||||
remainingRuntimeMS -= runtimeHours * ONE_HOUR;
|
||||
const runtimeMinutes = Math.floor(remainingRuntimeMS / ONE_MINUTE);
|
||||
remainingRuntimeMS -= runtimeMinutes * ONE_MINUTE;
|
||||
const runtimeSeconds = Math.floor(remainingRuntimeMS / 1000);
|
||||
remainingRuntimeMS -= runtimeSeconds * 1000;
|
||||
|
||||
const runtimeDisplayHours = runtimeHours ? `${runtimeHours}:` : '';
|
||||
const runtimeDisplayMinutes = runtimeMinutes < 10 ? `0${runtimeMinutes}:` : `${runtimeMinutes}:`;
|
||||
const runtimeDisplaySeconds = runtimeSeconds < 10 ? `0${runtimeSeconds}` : runtimeSeconds;
|
||||
|
||||
return <>{`${runtimeDisplayHours}${runtimeDisplayMinutes}${runtimeDisplaySeconds}`}</>;
|
||||
};
|
||||
|
||||
const columns: Array<{
|
||||
field: keyof Process;
|
||||
name: string;
|
||||
sortable: boolean;
|
||||
render?: Function;
|
||||
width?: string | number;
|
||||
textOnly?: boolean;
|
||||
align?: typeof RIGHT_ALIGNMENT | typeof LEFT_ALIGNMENT;
|
||||
}> = [
|
||||
{
|
||||
field: 'state',
|
||||
name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelState', {
|
||||
defaultMessage: 'State',
|
||||
}),
|
||||
sortable: false,
|
||||
render: (state: string) => <StateBadge state={state} />,
|
||||
width: 84,
|
||||
textOnly: false,
|
||||
},
|
||||
{
|
||||
field: 'command',
|
||||
name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelCommand', {
|
||||
defaultMessage: 'Command',
|
||||
}),
|
||||
sortable: false,
|
||||
width: '40%',
|
||||
render: (command: string) => <CodeLine>{command}</CodeLine>,
|
||||
},
|
||||
{
|
||||
field: 'startTime',
|
||||
name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelTime', {
|
||||
defaultMessage: 'Time',
|
||||
}),
|
||||
align: RIGHT_ALIGNMENT,
|
||||
sortable: true,
|
||||
render: (startTime: number, currentTime: number) => (
|
||||
<RuntimeCell startTime={startTime} currentTime={currentTime} />
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'cpu',
|
||||
name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelCPU', {
|
||||
defaultMessage: 'CPU',
|
||||
}),
|
||||
sortable: true,
|
||||
render: (value: number) => FORMATTERS.percent(value),
|
||||
},
|
||||
{
|
||||
field: 'memory',
|
||||
name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelMemory', {
|
||||
defaultMessage: 'Mem.',
|
||||
}),
|
||||
sortable: true,
|
||||
render: (value: number) => FORMATTERS.percent(value),
|
||||
},
|
||||
];
|
||||
|
||||
const CodeLine = euiStyled.div`
|
||||
font-family: ${(props) => props.theme.eui.euiCodeFontFamily};
|
||||
font-size: ${(props) => props.theme.eui.euiFontSizeS};
|
||||
white-space: pre;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`;
|
|
@ -1,29 +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 { EuiBadge } from '@elastic/eui';
|
||||
import { STATE_NAMES } from './states';
|
||||
|
||||
export const StateBadge = ({ state }: { state: string }) => {
|
||||
switch (state) {
|
||||
case 'running':
|
||||
return <EuiBadge color="success">{STATE_NAMES.running}</EuiBadge>;
|
||||
case 'sleeping':
|
||||
return <EuiBadge color="default">{STATE_NAMES.sleeping}</EuiBadge>;
|
||||
case 'dead':
|
||||
return <EuiBadge color="danger">{STATE_NAMES.dead}</EuiBadge>;
|
||||
case 'stopped':
|
||||
return <EuiBadge color="warning">{STATE_NAMES.stopped}</EuiBadge>;
|
||||
case 'idle':
|
||||
return <EuiBadge color="primary">{STATE_NAMES.idle}</EuiBadge>;
|
||||
case 'zombie':
|
||||
return <EuiBadge color="danger">{STATE_NAMES.zombie}</EuiBadge>;
|
||||
default:
|
||||
return <EuiBadge color="hollow">{STATE_NAMES.unknown}</EuiBadge>;
|
||||
}
|
||||
};
|
|
@ -1,34 +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 STATE_NAMES = {
|
||||
running: i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateRunning', {
|
||||
defaultMessage: 'Running',
|
||||
}),
|
||||
sleeping: i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateSleeping', {
|
||||
defaultMessage: 'Sleeping',
|
||||
}),
|
||||
dead: i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateDead', {
|
||||
defaultMessage: 'Dead',
|
||||
}),
|
||||
stopped: i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateStopped', {
|
||||
defaultMessage: 'Stopped',
|
||||
}),
|
||||
idle: i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateIdle', {
|
||||
defaultMessage: 'Idle',
|
||||
}),
|
||||
zombie: i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateZombie', {
|
||||
defaultMessage: 'Zombie',
|
||||
}),
|
||||
unknown: i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateUnknown', {
|
||||
defaultMessage: 'Unknown',
|
||||
}),
|
||||
};
|
||||
|
||||
export const STATE_ORDER = ['running', 'sleeping', 'stopped', 'idle', 'dead', 'zombie', 'unknown'];
|
|
@ -1,93 +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, { useMemo } from 'react';
|
||||
import { mapValues } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiLoadingSpinner,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListTitle,
|
||||
EuiDescriptionListDescription,
|
||||
EuiHorizontalRule,
|
||||
} from '@elastic/eui';
|
||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import { ProcessListAPIResponse } from '../../../../../../../../common/http_api';
|
||||
import { STATE_NAMES } from './states';
|
||||
|
||||
interface Props {
|
||||
processSummary: ProcessListAPIResponse['summary'];
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
type SummaryRecord = {
|
||||
total: number;
|
||||
} & Record<keyof typeof STATE_NAMES, number>;
|
||||
|
||||
const NOT_AVAILABLE_LABEL = i18n.translate('xpack.infra.notAvailableLabel', {
|
||||
defaultMessage: 'N/A',
|
||||
});
|
||||
|
||||
const processSummaryNotAvailable = {
|
||||
total: NOT_AVAILABLE_LABEL,
|
||||
running: NOT_AVAILABLE_LABEL,
|
||||
sleeping: NOT_AVAILABLE_LABEL,
|
||||
dead: NOT_AVAILABLE_LABEL,
|
||||
stopped: NOT_AVAILABLE_LABEL,
|
||||
idle: NOT_AVAILABLE_LABEL,
|
||||
zombie: NOT_AVAILABLE_LABEL,
|
||||
unknown: NOT_AVAILABLE_LABEL,
|
||||
};
|
||||
|
||||
export const SummaryTable = ({ processSummary, isLoading }: Props) => {
|
||||
const summary = !processSummary?.total ? processSummaryNotAvailable : processSummary;
|
||||
|
||||
const processCount = useMemo(
|
||||
() =>
|
||||
({
|
||||
total: isLoading ? -1 : summary.total,
|
||||
...mapValues(STATE_NAMES, () => (isLoading ? -1 : 0)),
|
||||
...(isLoading ? {} : summary),
|
||||
} as SummaryRecord),
|
||||
[summary, isLoading]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup gutterSize="m" responsive={false} wrap={true}>
|
||||
{Object.entries(processCount).map(([field, value]) => (
|
||||
<EuiFlexItem key={field}>
|
||||
<EuiDescriptionList data-test-subj="infraProcessesSummaryTableItem" compressed>
|
||||
<ColumnTitle>{columnTitles[field as keyof SummaryRecord]}</ColumnTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
{value === -1 ? <LoadingSpinner /> : value}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
<EuiHorizontalRule margin="m" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const columnTitles = {
|
||||
total: i18n.translate('xpack.infra.metrics.nodeDetails.processes.headingTotalProcesses', {
|
||||
defaultMessage: 'Total processes',
|
||||
}),
|
||||
...STATE_NAMES,
|
||||
};
|
||||
|
||||
const LoadingSpinner = euiStyled(EuiLoadingSpinner).attrs({ size: 'm' })`
|
||||
margin-top: 2px;
|
||||
margin-bottom: 3px;
|
||||
`;
|
||||
|
||||
const ColumnTitle = euiStyled(EuiDescriptionListTitle)`
|
||||
white-space: nowrap;
|
||||
`;
|
|
@ -1,23 +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 { MetricsExplorerSeries } from '../../../../../../../../common/http_api';
|
||||
import { STATE_NAMES } from './states';
|
||||
|
||||
export interface Process {
|
||||
command: string;
|
||||
cpu: number;
|
||||
memory: number;
|
||||
startTime: number;
|
||||
state: keyof typeof STATE_NAMES;
|
||||
pid: number;
|
||||
user: string;
|
||||
timeseries: {
|
||||
[x: string]: MetricsExplorerSeries;
|
||||
};
|
||||
apmTrace?: string; // Placeholder
|
||||
}
|
|
@ -1,117 +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 { InfraMetadata } from '../../../../../../../../common/http_api';
|
||||
|
||||
export const getFields = (metadata: InfraMetadata, group: 'cloud' | 'host' | 'agent') => {
|
||||
switch (group) {
|
||||
case 'host':
|
||||
return prune([
|
||||
{
|
||||
name: 'host.architecture',
|
||||
value: metadata.info?.host?.architecture,
|
||||
},
|
||||
{
|
||||
name: 'host.hostname',
|
||||
value: metadata.info?.host?.name,
|
||||
},
|
||||
{
|
||||
name: 'host.id',
|
||||
value: metadata.info?.host?.id,
|
||||
},
|
||||
{
|
||||
name: 'host.ip',
|
||||
value: metadata.info?.host?.ip,
|
||||
},
|
||||
{
|
||||
name: 'host.mac',
|
||||
value: metadata.info?.host?.mac,
|
||||
},
|
||||
{
|
||||
name: 'host.name',
|
||||
value: metadata.info?.host?.name,
|
||||
},
|
||||
{
|
||||
name: 'host.os.build',
|
||||
value: metadata.info?.host?.os?.build,
|
||||
},
|
||||
{
|
||||
name: 'host.os.family',
|
||||
value: metadata.info?.host?.os?.family,
|
||||
},
|
||||
{
|
||||
name: 'host.os.name',
|
||||
value: metadata.info?.host?.os?.name,
|
||||
},
|
||||
{
|
||||
name: 'host.os.kernel',
|
||||
value: metadata.info?.host?.os?.kernel,
|
||||
},
|
||||
{
|
||||
name: 'host.os.platform',
|
||||
value: metadata.info?.host?.os?.platform,
|
||||
},
|
||||
{
|
||||
name: 'host.os.version',
|
||||
value: metadata.info?.host?.os?.version,
|
||||
},
|
||||
]);
|
||||
case 'cloud':
|
||||
return prune([
|
||||
{
|
||||
name: 'cloud.account.id',
|
||||
value: metadata.info?.cloud?.account?.id,
|
||||
},
|
||||
{
|
||||
name: 'cloud.account.name',
|
||||
value: metadata.info?.cloud?.account?.name,
|
||||
},
|
||||
{
|
||||
name: 'cloud.availability_zone',
|
||||
value: metadata.info?.cloud?.availability_zone,
|
||||
},
|
||||
{
|
||||
name: 'cloud.instance.id',
|
||||
value: metadata.info?.cloud?.instance?.id,
|
||||
},
|
||||
{
|
||||
name: 'cloud.instance.name',
|
||||
value: metadata.info?.cloud?.instance?.name,
|
||||
},
|
||||
{
|
||||
name: 'cloud.machine.type',
|
||||
value: metadata.info?.cloud?.machine?.type,
|
||||
},
|
||||
{
|
||||
name: 'cloud.provider',
|
||||
value: metadata.info?.cloud?.provider,
|
||||
},
|
||||
{
|
||||
name: 'cloud.region',
|
||||
value: metadata.info?.cloud?.region,
|
||||
},
|
||||
]);
|
||||
case 'agent':
|
||||
return prune([
|
||||
{
|
||||
name: 'agent.id',
|
||||
value: metadata.info?.agent?.id,
|
||||
},
|
||||
{
|
||||
name: 'agent.version',
|
||||
value: metadata.info?.agent?.version,
|
||||
},
|
||||
{
|
||||
name: 'agent.policy',
|
||||
value: metadata.info?.agent?.policy,
|
||||
},
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
const prune = (fields: Array<{ name: string; value: string | string[] | undefined }>) =>
|
||||
fields.filter((f) => !!f.value);
|
|
@ -1,131 +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, useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiLoadingChart } from '@elastic/eui';
|
||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import { TabContent, TabProps } from '../shared';
|
||||
import { useSourceContext } from '../../../../../../../containers/metrics_source';
|
||||
import { InventoryItemType } from '../../../../../../../../common/inventory_models/types';
|
||||
import { useMetadata } from '../../../../../../../components/asset_details/hooks/use_metadata';
|
||||
import { getFields } from './build_fields';
|
||||
import { useWaffleTimeContext } from '../../../../hooks/use_waffle_time';
|
||||
import { Table } from './table';
|
||||
import { useWaffleFiltersContext } from '../../../../hooks/use_waffle_filters';
|
||||
|
||||
const TabComponent = (props: TabProps) => {
|
||||
const nodeId = props.node.id;
|
||||
const nodeType = props.nodeType as InventoryItemType;
|
||||
const { sourceId } = useSourceContext();
|
||||
const { currentTimeRange } = useWaffleTimeContext();
|
||||
const { applyFilterQuery } = useWaffleFiltersContext();
|
||||
const { loading: metadataLoading, metadata } = useMetadata({
|
||||
assetId: nodeId,
|
||||
assetType: nodeType,
|
||||
sourceId,
|
||||
timeRange: currentTimeRange,
|
||||
});
|
||||
|
||||
const hostFields = useMemo(() => {
|
||||
if (!metadata) return null;
|
||||
return getFields(metadata, 'host');
|
||||
}, [metadata]);
|
||||
|
||||
const cloudFields = useMemo(() => {
|
||||
if (!metadata) return null;
|
||||
return getFields(metadata, 'cloud');
|
||||
}, [metadata]);
|
||||
|
||||
const agentFields = useMemo(() => {
|
||||
if (!metadata) return null;
|
||||
return getFields(metadata, 'agent');
|
||||
}, [metadata]);
|
||||
|
||||
const onFilter = useCallback(
|
||||
(item: { name: string; value: string }) => {
|
||||
applyFilterQuery({
|
||||
kind: 'kuery',
|
||||
expression: `${item.name}: "${item.value}"`,
|
||||
});
|
||||
},
|
||||
[applyFilterQuery]
|
||||
);
|
||||
|
||||
if (metadataLoading) {
|
||||
return <LoadingPlaceholder />;
|
||||
}
|
||||
|
||||
return (
|
||||
<TabContent>
|
||||
{hostFields && hostFields.length > 0 && (
|
||||
<TableWrapper>
|
||||
<Table
|
||||
title={i18n.translate('xpack.infra.nodeDetails.tabs.metadata.hostsHeader', {
|
||||
defaultMessage: 'Hosts',
|
||||
})}
|
||||
onClick={onFilter}
|
||||
rows={hostFields}
|
||||
/>
|
||||
</TableWrapper>
|
||||
)}
|
||||
{cloudFields && cloudFields.length > 0 && (
|
||||
<TableWrapper>
|
||||
<Table
|
||||
title={i18n.translate('xpack.infra.nodeDetails.tabs.metadata.cloudHeader', {
|
||||
defaultMessage: 'Cloud',
|
||||
})}
|
||||
onClick={onFilter}
|
||||
rows={cloudFields}
|
||||
/>
|
||||
</TableWrapper>
|
||||
)}
|
||||
{agentFields && agentFields.length > 0 && (
|
||||
<TableWrapper>
|
||||
<Table
|
||||
title={i18n.translate('xpack.infra.nodeDetails.tabs.metadata.agentHeader', {
|
||||
defaultMessage: 'Agent',
|
||||
})}
|
||||
onClick={onFilter}
|
||||
rows={agentFields}
|
||||
/>
|
||||
</TableWrapper>
|
||||
)}
|
||||
</TabContent>
|
||||
);
|
||||
};
|
||||
|
||||
const TableWrapper = euiStyled.div`
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 16px
|
||||
}
|
||||
`;
|
||||
|
||||
const LoadingPlaceholder = () => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '200px',
|
||||
padding: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<EuiLoadingChart size="xl" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const PropertiesTab = {
|
||||
id: 'properties',
|
||||
name: i18n.translate('xpack.infra.nodeDetails.tabs.metadata.title', {
|
||||
defaultMessage: 'Metadata',
|
||||
}),
|
||||
content: TabComponent,
|
||||
};
|
|
@ -1,167 +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 {
|
||||
EuiText,
|
||||
EuiToolTip,
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLink,
|
||||
EuiBasicTable,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useMemo } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import useToggle from 'react-use/lib/useToggle';
|
||||
|
||||
interface Row {
|
||||
name: string;
|
||||
value: string | string[] | undefined;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
rows: Row[];
|
||||
title: string;
|
||||
onClick(item: Row): void;
|
||||
}
|
||||
|
||||
export const Table = (props: Props) => {
|
||||
const { rows, title, onClick } = props;
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
field: 'name',
|
||||
name: '',
|
||||
width: '35%',
|
||||
sortable: false,
|
||||
render: (name: string, item: Row) => (
|
||||
<EuiText size="xs">
|
||||
<strong>{item.name}</strong>
|
||||
</EuiText>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'value',
|
||||
name: '',
|
||||
width: '65%',
|
||||
sortable: false,
|
||||
render: (_name: string, item: Row) => {
|
||||
return (
|
||||
<span>
|
||||
<EuiFlexGroup gutterSize={'xs'} alignItems={'center'} responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={i18n.translate(
|
||||
'xpack.infra.nodeDetails.tabs.metadata.setFilterTooltip',
|
||||
{
|
||||
defaultMessage: 'View event with filter',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="infraColumnsButton"
|
||||
color="text"
|
||||
size="s"
|
||||
iconType="filter"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.infra.nodeDetails.tabs.metadata.filterAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Filter',
|
||||
}
|
||||
)}
|
||||
onClick={() => onClick(item)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<ExpandableContent values={item.value} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[onClick]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiText>
|
||||
<h4>{title}</h4>
|
||||
</EuiText>
|
||||
<EuiSpacer size={'s'} />
|
||||
<TableWithoutHeader
|
||||
tableLayout={'fixed'}
|
||||
compressed
|
||||
responsive={false}
|
||||
columns={columns}
|
||||
items={rows}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
class TableWithoutHeader extends EuiBasicTable {
|
||||
renderTableHead() {
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
|
||||
interface ExpandableContentProps {
|
||||
values: string | string[] | undefined;
|
||||
}
|
||||
|
||||
const ExpandableContent = (props: ExpandableContentProps) => {
|
||||
const { values } = props;
|
||||
const [isExpanded, toggle] = useToggle(false);
|
||||
|
||||
const list = Array.isArray(values) ? values : [values];
|
||||
const [first, ...others] = list;
|
||||
const hasOthers = others.length > 0;
|
||||
const shouldShowMore = hasOthers && !isExpanded;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
gutterSize={'xs'}
|
||||
responsive={false}
|
||||
alignItems={'baseline'}
|
||||
wrap={true}
|
||||
direction="column"
|
||||
>
|
||||
<div>
|
||||
{first}
|
||||
{shouldShowMore && (
|
||||
<>
|
||||
{' ... '}
|
||||
<EuiLink data-test-subj="infraArrayValueCountMoreLink" onClick={toggle}>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.nodeDetails.tabs.metadata.seeMore"
|
||||
defaultMessage="+{count} more"
|
||||
values={{
|
||||
count: others.length,
|
||||
}}
|
||||
/>
|
||||
</EuiLink>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{isExpanded && others.map((item) => <EuiFlexItem key={item}>{item}</EuiFlexItem>)}
|
||||
{hasOthers && isExpanded && (
|
||||
<EuiFlexItem>
|
||||
<EuiLink data-test-subj="infraArrayValueShowLessLink" onClick={toggle}>
|
||||
{i18n.translate('xpack.infra.nodeDetails.tabs.metadata.seeLess', {
|
||||
defaultMessage: 'Show less',
|
||||
})}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -1,27 +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 { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import { InventoryItemType } from '../../../../../../../common/inventory_models/types';
|
||||
import { InfraWaffleMapOptions, InfraWaffleMapNode } from '../../../../../../lib/lib';
|
||||
|
||||
export interface TabProps {
|
||||
options: InfraWaffleMapOptions;
|
||||
currentTime: number;
|
||||
node: InfraWaffleMapNode;
|
||||
nodeType: InventoryItemType;
|
||||
onClose(): void;
|
||||
}
|
||||
|
||||
export const OVERLAY_Y_START = 266;
|
||||
export const OVERLAY_BOTTOM_MARGIN = 16;
|
||||
export const TabContent = euiStyled.div`
|
||||
padding: ${(props) => props.theme.eui.euiSizeM};
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
`;
|
|
@ -19,6 +19,8 @@ import { TableView } from './table_view';
|
|||
import { SnapshotNode } from '../../../../../common/http_api/snapshot_api';
|
||||
import { calculateBoundsFromNodes } from '../lib/calculate_bounds_from_nodes';
|
||||
import { Legend } from './waffle/legend';
|
||||
import { useAssetDetailsFlyoutState } from '../hooks/use_asset_details_flyout_url_state';
|
||||
import { AssetDetailsFlyout } from './waffle/asset_details_flyout';
|
||||
|
||||
export interface KueryFilterQuery {
|
||||
kind: 'kuery';
|
||||
|
@ -57,6 +59,12 @@ export const NodesOverview = ({
|
|||
showLoading,
|
||||
}: Props) => {
|
||||
const currentBreakpoint = useCurrentEuiBreakpoint();
|
||||
const [{ detailsItemId }, setFlyoutUrlState] = useAssetDetailsFlyoutState();
|
||||
|
||||
const closeFlyout = useCallback(
|
||||
() => setFlyoutUrlState({ detailsItemId: null }),
|
||||
[setFlyoutUrlState]
|
||||
);
|
||||
|
||||
const handleDrilldown = useCallback(
|
||||
(filter: string) => {
|
||||
|
@ -123,6 +131,7 @@ export const NodesOverview = ({
|
|||
<Map
|
||||
nodeType={nodeType}
|
||||
nodes={nodes}
|
||||
detailsItemId={detailsItemId}
|
||||
options={options}
|
||||
formatter={formatter}
|
||||
currentTime={currentTime}
|
||||
|
@ -132,6 +141,14 @@ export const NodesOverview = ({
|
|||
bottomMargin={bottomMargin}
|
||||
staticHeight={isStatic}
|
||||
/>
|
||||
{nodeType === 'host' && detailsItemId && (
|
||||
<AssetDetailsFlyout
|
||||
closeFlyout={closeFlyout}
|
||||
assetName={detailsItemId}
|
||||
assetType={nodeType}
|
||||
currentTime={currentTime}
|
||||
/>
|
||||
)}
|
||||
<Legend
|
||||
formatter={formatter}
|
||||
bounds={bounds}
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { ContentTabIds } from '../../../../../components/asset_details/types';
|
||||
import { InventoryItemType } from '../../../../../../common/inventory_models/types';
|
||||
import AssetDetails from '../../../../../components/asset_details/asset_details';
|
||||
import { useSourceContext } from '../../../../../containers/metrics_source';
|
||||
import { commonFlyoutTabs } from '../../../../../common/asset_details_config/asset_details_tabs';
|
||||
|
||||
interface Props {
|
||||
assetName: string;
|
||||
assetType: InventoryItemType;
|
||||
closeFlyout: () => void;
|
||||
currentTime: number;
|
||||
}
|
||||
|
||||
const ONE_HOUR = 60 * 60 * 1000;
|
||||
|
||||
const flyoutTabs = [
|
||||
...commonFlyoutTabs,
|
||||
{
|
||||
id: ContentTabIds.LINK_TO_APM,
|
||||
name: i18n.translate('xpack.infra.nodeDetails.tabs.linkToApm', {
|
||||
defaultMessage: 'APM',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
export const AssetDetailsFlyout = ({ assetName, assetType, closeFlyout, currentTime }: Props) => {
|
||||
const { source } = useSourceContext();
|
||||
|
||||
return source ? (
|
||||
<AssetDetails
|
||||
asset={{ id: assetName, name: assetName }}
|
||||
assetType={assetType}
|
||||
overrides={{
|
||||
metadata: {
|
||||
showActionsColumn: false,
|
||||
},
|
||||
}}
|
||||
tabs={flyoutTabs}
|
||||
links={['nodeDetails']}
|
||||
renderMode={{
|
||||
mode: 'flyout',
|
||||
closeFlyout,
|
||||
}}
|
||||
metricAlias={source.configuration.metricAlias}
|
||||
dateRange={{
|
||||
from: new Date(currentTime - ONE_HOUR).toISOString(),
|
||||
to: new Date(currentTime).toISOString(),
|
||||
}}
|
||||
/>
|
||||
) : null;
|
||||
};
|
|
@ -25,6 +25,7 @@ interface Props {
|
|||
bounds: InfraWaffleMapBounds;
|
||||
nodeType: InventoryItemType;
|
||||
currentTime: number;
|
||||
detailsItemId: string | null;
|
||||
}
|
||||
|
||||
export const GroupOfGroups: React.FC<Props> = (props) => {
|
||||
|
@ -43,6 +44,7 @@ export const GroupOfGroups: React.FC<Props> = (props) => {
|
|||
bounds={props.bounds}
|
||||
nodeType={props.nodeType}
|
||||
currentTime={props.currentTime}
|
||||
detailsItemId={props.detailsItemId}
|
||||
/>
|
||||
))}
|
||||
</Groups>
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
import { GroupName } from './group_name';
|
||||
import { Node } from './node';
|
||||
import { InventoryItemType } from '../../../../../../common/inventory_models/types';
|
||||
import { useAssetDetailsFlyoutState } from '../../hooks/use_asset_details_flyout_url_state';
|
||||
|
||||
interface Props {
|
||||
onDrilldown: (filter: string) => void;
|
||||
|
@ -27,6 +28,7 @@ interface Props {
|
|||
bounds: InfraWaffleMapBounds;
|
||||
nodeType: InventoryItemType;
|
||||
currentTime: number;
|
||||
detailsItemId: string | null;
|
||||
}
|
||||
|
||||
// custom comparison function for rendering the nodes to prevent unncessary rerendering
|
||||
|
@ -42,8 +44,20 @@ const isEqualGroupOfNodes = (prevProps: Props, nextProps: Props) => {
|
|||
};
|
||||
|
||||
export const GroupOfNodes = React.memo<Props>(
|
||||
({ group, options, formatter, onDrilldown, isChild = false, bounds, nodeType, currentTime }) => {
|
||||
({
|
||||
group,
|
||||
options,
|
||||
formatter,
|
||||
onDrilldown,
|
||||
isChild = false,
|
||||
bounds,
|
||||
nodeType,
|
||||
currentTime,
|
||||
detailsItemId,
|
||||
}) => {
|
||||
const width = group.width > 200 ? group.width : 200;
|
||||
const [_, setFlyoutUrlState] = useAssetDetailsFlyoutState();
|
||||
|
||||
return (
|
||||
<GroupOfNodesContainer style={{ width }}>
|
||||
<GroupName group={group} onDrilldown={onDrilldown} isChild={isChild} options={options} />
|
||||
|
@ -59,6 +73,8 @@ export const GroupOfNodes = React.memo<Props>(
|
|||
bounds={bounds}
|
||||
nodeType={nodeType}
|
||||
currentTime={currentTime}
|
||||
detailsItemId={detailsItemId}
|
||||
setFlyoutUrlState={setFlyoutUrlState}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
|
|
|
@ -30,6 +30,7 @@ interface Props {
|
|||
dataBounds: InfraWaffleMapBounds;
|
||||
bottomMargin: number;
|
||||
staticHeight: boolean;
|
||||
detailsItemId: string | null;
|
||||
}
|
||||
|
||||
export const Map: React.FC<Props> = ({
|
||||
|
@ -43,6 +44,7 @@ export const Map: React.FC<Props> = ({
|
|||
dataBounds,
|
||||
bottomMargin,
|
||||
staticHeight,
|
||||
detailsItemId,
|
||||
}) => {
|
||||
const sortedNodes = sortNodes(options.sort, nodes);
|
||||
const map = nodesToWaffleMap(sortedNodes);
|
||||
|
@ -70,6 +72,7 @@ export const Map: React.FC<Props> = ({
|
|||
bounds={bounds}
|
||||
nodeType={nodeType}
|
||||
currentTime={currentTime}
|
||||
detailsItemId={detailsItemId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -85,6 +88,7 @@ export const Map: React.FC<Props> = ({
|
|||
bounds={bounds}
|
||||
nodeType={nodeType}
|
||||
currentTime={currentTime}
|
||||
detailsItemId={detailsItemId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,14 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { darken, readableColor } from 'polished';
|
||||
import React from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { first } from 'lodash';
|
||||
import { EuiPopover, EuiToolTip } from '@elastic/eui';
|
||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import { useBoolean } from '../../../../../hooks/use_boolean';
|
||||
import {
|
||||
InfraWaffleMapBounds,
|
||||
InfraWaffleMapNode,
|
||||
|
@ -21,20 +18,10 @@ import {
|
|||
import { ConditionalToolTip } from './conditional_tooltip';
|
||||
import { colorFromValue } from '../../lib/color_from_value';
|
||||
import { InventoryItemType } from '../../../../../../common/inventory_models/types';
|
||||
import { NodeContextPopover } from '../node_details/overlay';
|
||||
|
||||
import { NodeContextMenu } from './node_context_menu';
|
||||
import { AlertFlyout } from '../../../../../alerting/inventory/components/alert_flyout';
|
||||
import { findInventoryFields } from '../../../../../../common/inventory_models';
|
||||
|
||||
const initialState = {
|
||||
isPopoverOpen: false,
|
||||
isOverlayOpen: false,
|
||||
isAlertFlyoutVisible: false,
|
||||
isToolTipOpen: false,
|
||||
};
|
||||
|
||||
type State = Readonly<typeof initialState>;
|
||||
import { NodeSquare } from './node_square';
|
||||
import { type AssetDetailsFlyoutPropertiesUpdater } from '../../hooks/use_asset_details_flyout_url_state';
|
||||
|
||||
interface Props {
|
||||
squareSize: number;
|
||||
|
@ -44,247 +31,78 @@ interface Props {
|
|||
bounds: InfraWaffleMapBounds;
|
||||
nodeType: InventoryItemType;
|
||||
currentTime: number;
|
||||
setFlyoutUrlState: AssetDetailsFlyoutPropertiesUpdater;
|
||||
detailsItemId: string | null;
|
||||
}
|
||||
|
||||
export class Node extends React.PureComponent<Props, State> {
|
||||
public readonly state: State = initialState;
|
||||
public render() {
|
||||
const { nodeType, node, options, squareSize, bounds, formatter, currentTime } = this.props;
|
||||
const { isPopoverOpen, isAlertFlyoutVisible, isToolTipOpen } = this.state;
|
||||
const metric = first(node.metrics);
|
||||
const valueMode = squareSize > 70;
|
||||
const ellipsisMode = squareSize > 30;
|
||||
const rawValue = (metric && metric.value) || 0;
|
||||
const color = colorFromValue(options.legend, rawValue, bounds);
|
||||
const value = formatter(rawValue);
|
||||
const nodeAriaLabel = i18n.translate('xpack.infra.node.ariaLabel', {
|
||||
defaultMessage: '{nodeName}, click to open menu',
|
||||
values: { nodeName: node.name },
|
||||
});
|
||||
export const Node = ({
|
||||
nodeType,
|
||||
node,
|
||||
options,
|
||||
squareSize,
|
||||
bounds,
|
||||
formatter,
|
||||
currentTime,
|
||||
setFlyoutUrlState,
|
||||
detailsItemId,
|
||||
}: Props) => {
|
||||
const [isToolTipOpen, { off: hideToolTip, on: showToolTip }] = useBoolean(false);
|
||||
const [isPopoverOpen, { off: closePopover, toggle: togglePopover }] = useBoolean(false);
|
||||
|
||||
const nodeBorder = this.state.isOverlayOpen ? { border: 'solid 4px #000' } : undefined;
|
||||
const metric = first(node.metrics);
|
||||
const rawValue = (metric && metric.value) || 0;
|
||||
const color = colorFromValue(options.legend, rawValue, bounds);
|
||||
const value = formatter(rawValue);
|
||||
|
||||
const bigSquare = (
|
||||
<NodeContainer
|
||||
data-test-subj="nodeContainer"
|
||||
style={{ width: squareSize || 0, height: squareSize || 0 }}
|
||||
onClick={this.togglePopover}
|
||||
onMouseOver={this.showToolTip}
|
||||
onMouseLeave={this.hideToolTip}
|
||||
className="buttonContainer"
|
||||
>
|
||||
<SquareOuter color={color} style={nodeBorder}>
|
||||
<SquareInner color={color}>
|
||||
{valueMode ? (
|
||||
<ValueInner aria-label={nodeAriaLabel}>
|
||||
<Label data-test-subj="nodeName" color={color}>
|
||||
{node.name}
|
||||
</Label>
|
||||
<Value data-test-subj="nodeValue" color={color}>
|
||||
{value}
|
||||
</Value>
|
||||
</ValueInner>
|
||||
) : (
|
||||
ellipsisMode && (
|
||||
<ValueInner aria-label={nodeAriaLabel}>
|
||||
<Label color={color}>...</Label>
|
||||
</ValueInner>
|
||||
)
|
||||
)}
|
||||
</SquareInner>
|
||||
</SquareOuter>
|
||||
</NodeContainer>
|
||||
);
|
||||
const toggleAssetPopover = () => {
|
||||
if (nodeType === 'host') {
|
||||
setFlyoutUrlState({ detailsItemId: node.name });
|
||||
} else {
|
||||
togglePopover();
|
||||
}
|
||||
};
|
||||
|
||||
const smallSquare = (
|
||||
<NodeContainerSmall
|
||||
data-test-subj="nodeContainer"
|
||||
style={{ width: squareSize || 0, height: squareSize || 0, ...nodeBorder }}
|
||||
onClick={this.togglePopover}
|
||||
onMouseOver={this.showToolTip}
|
||||
onMouseLeave={this.hideToolTip}
|
||||
color={color}
|
||||
/>
|
||||
);
|
||||
const nodeSquare = (
|
||||
<NodeSquare
|
||||
squareSize={squareSize}
|
||||
togglePopover={toggleAssetPopover}
|
||||
showToolTip={showToolTip}
|
||||
hideToolTip={hideToolTip}
|
||||
color={color}
|
||||
nodeName={node.name}
|
||||
value={value}
|
||||
showBorder={detailsItemId === node.name || isPopoverOpen}
|
||||
/>
|
||||
);
|
||||
|
||||
const nodeSquare = valueMode || ellipsisMode ? bigSquare : smallSquare;
|
||||
|
||||
return (
|
||||
<>
|
||||
{isPopoverOpen ? (
|
||||
<EuiPopover
|
||||
button={nodeSquare}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={this.closePopover}
|
||||
anchorPosition="downCenter"
|
||||
style={{ height: squareSize }}
|
||||
>
|
||||
<NodeContextMenu
|
||||
node={node}
|
||||
nodeType={nodeType}
|
||||
options={options}
|
||||
currentTime={currentTime}
|
||||
/>
|
||||
</EuiPopover>
|
||||
) : isToolTipOpen ? (
|
||||
<EuiToolTip
|
||||
delay="regular"
|
||||
position="right"
|
||||
content={
|
||||
<ConditionalToolTip currentTime={currentTime} node={node} nodeType={nodeType} />
|
||||
}
|
||||
>
|
||||
{nodeSquare}
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
nodeSquare
|
||||
)}
|
||||
|
||||
{this.state.isOverlayOpen && (
|
||||
<NodeContextPopover
|
||||
openAlertFlyout={this.openAlertFlyout}
|
||||
return (
|
||||
<>
|
||||
{isPopoverOpen ? (
|
||||
<EuiPopover
|
||||
button={nodeSquare}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
anchorPosition="downCenter"
|
||||
style={{ height: squareSize }}
|
||||
>
|
||||
<NodeContextMenu
|
||||
node={node}
|
||||
nodeType={nodeType}
|
||||
isOpen={this.state.isOverlayOpen}
|
||||
options={options}
|
||||
currentTime={currentTime}
|
||||
onClose={this.toggleNewOverlay}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isAlertFlyoutVisible && (
|
||||
<AlertFlyout
|
||||
filter={`${findInventoryFields(nodeType).id}: "${node.id}"`}
|
||||
options={options}
|
||||
nodeType={nodeType}
|
||||
setVisible={this.setAlertFlyoutVisible}
|
||||
visible={isAlertFlyoutVisible}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private openAlertFlyout = () => {
|
||||
this.setState({
|
||||
isOverlayOpen: false,
|
||||
isAlertFlyoutVisible: true,
|
||||
});
|
||||
};
|
||||
|
||||
private setAlertFlyoutVisible = (isOpen: boolean) => {
|
||||
this.setState({
|
||||
isAlertFlyoutVisible: isOpen,
|
||||
});
|
||||
};
|
||||
|
||||
private togglePopover = () => {
|
||||
const { nodeType } = this.props;
|
||||
if (nodeType === 'host') {
|
||||
this.toggleNewOverlay();
|
||||
} else {
|
||||
this.setState((prevState) => ({ isPopoverOpen: !prevState.isPopoverOpen }));
|
||||
}
|
||||
};
|
||||
|
||||
private toggleNewOverlay = () => {
|
||||
this.setState((prevState) => ({
|
||||
isPopoverOpen: !prevState.isOverlayOpen === true ? false : prevState.isPopoverOpen,
|
||||
isOverlayOpen: !prevState.isOverlayOpen,
|
||||
}));
|
||||
};
|
||||
|
||||
private closePopover = () => {
|
||||
if (this.state.isPopoverOpen) {
|
||||
this.setState({ isPopoverOpen: false });
|
||||
}
|
||||
};
|
||||
private showToolTip = () => {
|
||||
this.setState({ isToolTipOpen: true });
|
||||
};
|
||||
private hideToolTip = () => {
|
||||
this.setState({ isToolTipOpen: false });
|
||||
};
|
||||
}
|
||||
|
||||
const NodeContainer = euiStyled.div`
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
`;
|
||||
const NodeContainerSmall = euiStyled.div<ColorProps>`
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
background-color: ${(props) => darken(0.1, props.color)};
|
||||
border-radius: 3px;
|
||||
margin: 2px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2);
|
||||
`;
|
||||
|
||||
interface ColorProps {
|
||||
color: string;
|
||||
}
|
||||
|
||||
const SquareOuter = euiStyled.div<ColorProps>`
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
right: 4px;
|
||||
background-color: ${(props) => darken(0.1, props.color)};
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2);
|
||||
`;
|
||||
|
||||
const SquareInner = euiStyled.div<ColorProps>`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 2px;
|
||||
left: 0;
|
||||
border-radius: 3px;
|
||||
background-color: ${(props) => props.color};
|
||||
`;
|
||||
|
||||
const ValueInner = euiStyled.button`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
line-height: 1.2em;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
padding: 1em;
|
||||
overflow: hidden;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
border: none;
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
border: ${(params) => params.theme?.eui.euiFocusRingSize} solid
|
||||
${(params) => params.theme?.eui.euiFocusRingColor};
|
||||
box-shadow: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const SquareTextContent = euiStyled.div<ColorProps>`
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1 0 auto;
|
||||
color: ${(props) => readableColor(props.color)};
|
||||
`;
|
||||
|
||||
const Value = euiStyled(SquareTextContent)`
|
||||
font-weight: bold;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.2em;
|
||||
`;
|
||||
|
||||
const Label = euiStyled(SquareTextContent)`
|
||||
font-size: 0.7em;
|
||||
margin-bottom: 0.7em;
|
||||
`;
|
||||
</EuiPopover>
|
||||
) : isToolTipOpen ? (
|
||||
<EuiToolTip
|
||||
delay="regular"
|
||||
position="right"
|
||||
content={<ConditionalToolTip currentTime={currentTime} node={node} nodeType={nodeType} />}
|
||||
>
|
||||
{nodeSquare}
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
nodeSquare
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,199 @@
|
|||
/*
|
||||
* 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 { darken, readableColor } from 'polished';
|
||||
import React, { CSSProperties } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { css } from '@emotion/react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { DispatchWithOptionalAction } from '../../../../../hooks/use_boolean';
|
||||
|
||||
const SquareTextContentStyles = (color: string) => `
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1 0 auto;
|
||||
color: ${readableColor(color)};
|
||||
`;
|
||||
const styles = {
|
||||
nodeContainerSmall: (color: string) => `
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
background-color: ${darken(0.1, color)};
|
||||
border-radius: 3px;
|
||||
margin: 2px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2);
|
||||
`,
|
||||
valueInner: `
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
line-height: 1.2em;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
padding: 1em;
|
||||
overflow: hidden;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
border: none;
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
border: ${euiThemeVars.euiFocusRingSize} solid ${euiThemeVars.euiFocusRingColor};
|
||||
box-shadow: none;
|
||||
}
|
||||
`,
|
||||
squareOuter: (color: string) => `
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
right: 4px;
|
||||
background-color: ${darken(0.1, color)};
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2);
|
||||
`,
|
||||
squareInner: (color: string) => `
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 2px;
|
||||
left: 0;
|
||||
border-radius: 3px;
|
||||
background-color: ${color};
|
||||
`,
|
||||
label: (color: string) => `
|
||||
font-size: 0.7em;
|
||||
margin-bottom: 0.7em;
|
||||
${SquareTextContentStyles(color)}
|
||||
`,
|
||||
value: (color: string) => `
|
||||
font-weight: bold;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.2em;
|
||||
${SquareTextContentStyles(color)}
|
||||
`,
|
||||
};
|
||||
|
||||
export const NodeSquare = ({
|
||||
squareSize,
|
||||
togglePopover,
|
||||
showToolTip,
|
||||
hideToolTip,
|
||||
color,
|
||||
nodeName,
|
||||
value,
|
||||
showBorder,
|
||||
}: {
|
||||
squareSize: number;
|
||||
togglePopover: DispatchWithOptionalAction<boolean>;
|
||||
showToolTip: () => void;
|
||||
hideToolTip: () => void;
|
||||
color: string;
|
||||
nodeName: string;
|
||||
value: string;
|
||||
showBorder?: boolean;
|
||||
}) => {
|
||||
const valueMode = squareSize > 70;
|
||||
const ellipsisMode = squareSize > 30;
|
||||
const nodeAriaLabel = i18n.translate('xpack.infra.node.ariaLabel', {
|
||||
defaultMessage: '{nodeName}, click to open menu',
|
||||
values: { nodeName },
|
||||
});
|
||||
const style: CSSProperties | undefined = showBorder ? { border: 'solid 4px #000' } : undefined;
|
||||
|
||||
return valueMode || ellipsisMode ? (
|
||||
<div
|
||||
css={css`
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
`}
|
||||
data-test-subj="nodeContainer"
|
||||
style={{ width: squareSize || 0, height: squareSize || 0 }}
|
||||
onClick={togglePopover}
|
||||
onKeyPress={togglePopover}
|
||||
onFocus={showToolTip}
|
||||
onMouseOver={showToolTip}
|
||||
onMouseLeave={hideToolTip}
|
||||
className="buttonContainer"
|
||||
>
|
||||
<div
|
||||
css={css`
|
||||
${styles.squareOuter(color)}
|
||||
`}
|
||||
style={style}
|
||||
>
|
||||
<div
|
||||
css={css`
|
||||
${styles.squareInner(color)}
|
||||
`}
|
||||
>
|
||||
{valueMode ? (
|
||||
<button
|
||||
css={css`
|
||||
${styles.valueInner}
|
||||
`}
|
||||
aria-label={nodeAriaLabel}
|
||||
>
|
||||
<div
|
||||
css={css`
|
||||
${styles.label(color)}
|
||||
`}
|
||||
data-test-subj="nodeName"
|
||||
>
|
||||
{nodeName}
|
||||
</div>
|
||||
<div
|
||||
css={css`
|
||||
${styles.value(color)}
|
||||
`}
|
||||
data-test-subj="nodeValue"
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
</button>
|
||||
) : (
|
||||
ellipsisMode && (
|
||||
<button
|
||||
css={css`
|
||||
${styles.valueInner}
|
||||
`}
|
||||
aria-label={nodeAriaLabel}
|
||||
>
|
||||
<div
|
||||
css={css`
|
||||
${styles.label(color)}
|
||||
`}
|
||||
>
|
||||
...
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
css={styles.nodeContainerSmall(color)}
|
||||
data-test-subj="nodeContainer"
|
||||
style={{ width: squareSize || 0, height: squareSize || 0, ...style }}
|
||||
onClick={togglePopover}
|
||||
onKeyPress={togglePopover}
|
||||
onMouseOver={showToolTip}
|
||||
onFocus={showToolTip}
|
||||
onMouseLeave={hideToolTip}
|
||||
color={color}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 * as rt from 'io-ts';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { fold } from 'fp-ts/lib/Either';
|
||||
import { constant, identity } from 'fp-ts/lib/function';
|
||||
import { useUrlState } from '../../../../utils/use_url_state';
|
||||
|
||||
export const GET_DEFAULT_PROPERTIES: AssetDetailsFlyoutProperties = {
|
||||
detailsItemId: null,
|
||||
};
|
||||
|
||||
const ASSET_DETAILS_FLYOUT_URL_STATE_KEY = 'assetDetailsFlyout';
|
||||
|
||||
export const useAssetDetailsFlyoutState = (): [
|
||||
AssetDetailsFlyoutProperties,
|
||||
AssetDetailsFlyoutPropertiesUpdater
|
||||
] => {
|
||||
const [urlState, setUrlState] = useUrlState<AssetDetailsFlyoutProperties>({
|
||||
defaultState: {
|
||||
...GET_DEFAULT_PROPERTIES,
|
||||
},
|
||||
decodeUrlState,
|
||||
encodeUrlState,
|
||||
urlStateKey: ASSET_DETAILS_FLYOUT_URL_STATE_KEY,
|
||||
});
|
||||
|
||||
return [urlState, setUrlState];
|
||||
};
|
||||
|
||||
const AssetDetailsFlyoutStateRT = rt.type({
|
||||
detailsItemId: rt.union([rt.string, rt.null]),
|
||||
});
|
||||
|
||||
export type AssetDetailsFlyoutState = rt.TypeOf<typeof AssetDetailsFlyoutStateRT>;
|
||||
export type AssetDetailsFlyoutPropertiesUpdater = (params: AssetDetailsFlyoutState) => void;
|
||||
|
||||
type AssetDetailsFlyoutProperties = rt.TypeOf<typeof AssetDetailsFlyoutStateRT>;
|
||||
|
||||
const encodeUrlState = AssetDetailsFlyoutStateRT.encode;
|
||||
const decodeUrlState = (value: unknown) => {
|
||||
return pipe(AssetDetailsFlyoutStateRT.decode(value), fold(constant(undefined), identity));
|
||||
};
|
|
@ -7,54 +7,14 @@
|
|||
|
||||
import React from 'react';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { NoRemoteCluster } from '../../../components/empty_states';
|
||||
import { SourceErrorPage } from '../../../components/source_error_page';
|
||||
import { SourceLoadingPage } from '../../../components/source_loading_page';
|
||||
import { useSourceContext } from '../../../containers/metrics_source';
|
||||
import { ContentTabIds, type Tab } from '../../../components/asset_details/types';
|
||||
import type { InventoryItemType } from '../../../../common/inventory_models/types';
|
||||
import { AssetDetails } from '../../../components/asset_details/asset_details';
|
||||
import { MetricsPageTemplate } from '../page_template';
|
||||
|
||||
const orderedFlyoutTabs: Tab[] = [
|
||||
{
|
||||
id: ContentTabIds.OVERVIEW,
|
||||
name: i18n.translate('xpack.infra.nodeDetails.tabs.overview.title', {
|
||||
defaultMessage: 'Overview',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: ContentTabIds.METADATA,
|
||||
name: i18n.translate('xpack.infra.nodeDetails.tabs.metadata.title', {
|
||||
defaultMessage: 'Metadata',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: ContentTabIds.PROCESSES,
|
||||
name: i18n.translate('xpack.infra.metrics.nodeDetails.tabs.processes', {
|
||||
defaultMessage: 'Processes',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: ContentTabIds.LOGS,
|
||||
name: i18n.translate('xpack.infra.nodeDetails.tabs.logs.title', {
|
||||
defaultMessage: 'Logs',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: ContentTabIds.ANOMALIES,
|
||||
name: i18n.translate('xpack.infra.nodeDetails.tabs.anomalies', {
|
||||
defaultMessage: 'Anomalies',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: ContentTabIds.OSQUERY,
|
||||
name: i18n.translate('xpack.infra.nodeDetails.tabs.osquery', {
|
||||
defaultMessage: 'Osquery',
|
||||
}),
|
||||
},
|
||||
];
|
||||
import { commonFlyoutTabs } from '../../../common/asset_details_config/asset_details_tabs';
|
||||
|
||||
export const AssetDetailPage = () => {
|
||||
const { isLoading, loadSourceFailureMessage, loadSource, source } = useSourceContext();
|
||||
|
@ -91,7 +51,7 @@ export const AssetDetailPage = () => {
|
|||
id: nodeId,
|
||||
}}
|
||||
assetType={nodeType}
|
||||
tabs={orderedFlyoutTabs}
|
||||
tabs={commonFlyoutTabs}
|
||||
links={['apmServices']}
|
||||
renderMode={{
|
||||
mode: 'page',
|
||||
|
|
|
@ -18741,9 +18741,7 @@
|
|||
"xpack.infra.hostsViewPage.tabs.metricsCharts.tx": "Réseau sortant (TX)",
|
||||
"xpack.infra.hostsViewPage.tooltip.whatAreTheseMetricsLink": "Que sont ces indicateurs ?",
|
||||
"xpack.infra.infra.nodeDetails.apmTabLabel": "APM",
|
||||
"xpack.infra.infra.nodeDetails.createAlertLink": "Créer une règle d'inventaire",
|
||||
"xpack.infra.infra.nodeDetails.openAsPage": "Ouvrir en tant que page",
|
||||
"xpack.infra.infra.nodeDetails.updtimeTabLabel": "Uptime",
|
||||
"xpack.infra.inventory.alerting.groupActionVariableDescription": "Nom des données de reporting du groupe",
|
||||
"xpack.infra.inventoryModel.container.displayName": "Conteneurs Docker",
|
||||
"xpack.infra.inventoryModel.container.singularDisplayName": "Conteneur Docker",
|
||||
|
@ -19233,7 +19231,6 @@
|
|||
"xpack.infra.metrics.nodeDetails.processes.stateZombie": "Zombie",
|
||||
"xpack.infra.metrics.nodeDetails.processes.viewTraceInAPM": "Afficher la trace dans APM",
|
||||
"xpack.infra.metrics.nodeDetails.processesHeader": "Processus principaux",
|
||||
"xpack.infra.metrics.nodeDetails.processesHeader.tooltipBody": "Le tableau ci-dessous agrège les principaux processus de consommation de CPU et de mémoire. Il n'affiche pas tous les processus.",
|
||||
"xpack.infra.metrics.nodeDetails.processesHeader.tooltipLabel": "Plus d'infos",
|
||||
"xpack.infra.metrics.nodeDetails.processListError": "Impossible de charger les données de processus",
|
||||
"xpack.infra.metrics.nodeDetails.processListRetry": "Réessayer",
|
||||
|
@ -19381,41 +19378,16 @@
|
|||
"xpack.infra.nodeDetails.labels.showMoreDetails": "Afficher plus de détails",
|
||||
"xpack.infra.nodeDetails.logs.openLogsLink": "Ouvrir dans Logs",
|
||||
"xpack.infra.nodeDetails.logs.textFieldPlaceholder": "Rechercher les entrées de logs...",
|
||||
"xpack.infra.nodeDetails.metrics.cached": "En cache",
|
||||
"xpack.infra.nodeDetails.metrics.charts.loadTitle": "Charge",
|
||||
"xpack.infra.nodeDetails.metrics.charts.logRateTitle": "Taux de log",
|
||||
"xpack.infra.nodeDetails.metrics.charts.memoryTitle": "Mémoire",
|
||||
"xpack.infra.nodeDetails.metrics.charts.networkTitle": "Réseau",
|
||||
"xpack.infra.nodeDetails.metrics.fcharts.cpuTitle": "CPU",
|
||||
"xpack.infra.nodeDetails.metrics.free": "Gratuit",
|
||||
"xpack.infra.nodeDetails.metrics.inbound": "Entrant",
|
||||
"xpack.infra.nodeDetails.metrics.last15Minutes": "15 dernières minutes",
|
||||
"xpack.infra.nodeDetails.metrics.last24Hours": "Dernières 24 heures",
|
||||
"xpack.infra.nodeDetails.metrics.last3Hours": "3 dernières heures",
|
||||
"xpack.infra.nodeDetails.metrics.last7Days": "7 derniers jours",
|
||||
"xpack.infra.nodeDetails.metrics.lastHour": "Dernière heure",
|
||||
"xpack.infra.nodeDetails.metrics.logRate": "Taux de log",
|
||||
"xpack.infra.nodeDetails.metrics.outbound": "Sortant",
|
||||
"xpack.infra.nodeDetails.metrics.system": "Système",
|
||||
"xpack.infra.nodeDetails.metrics.used": "Utilisé",
|
||||
"xpack.infra.nodeDetails.metrics.user": "Utilisateur",
|
||||
"xpack.infra.nodeDetails.no": "Non",
|
||||
"xpack.infra.nodeDetails.tabs.anomalies": "Anomalies",
|
||||
"xpack.infra.nodeDetails.tabs.logs": "Logs",
|
||||
"xpack.infra.nodeDetails.tabs.logs.title": "Logs",
|
||||
"xpack.infra.nodeDetails.tabs.metadata.agentHeader": "Agent",
|
||||
"xpack.infra.nodeDetails.tabs.metadata.cloudHeader": "Cloud",
|
||||
"xpack.infra.nodeDetails.tabs.metadata.filterAriaLabel": "Filtre",
|
||||
"xpack.infra.nodeDetails.tabs.metadata.hostsHeader": "Hôtes",
|
||||
"xpack.infra.nodeDetails.tabs.metadata.seeLess": "Afficher moins",
|
||||
"xpack.infra.nodeDetails.tabs.metadata.setFilterTooltip": "Afficher l'événement avec filtre",
|
||||
"xpack.infra.nodeDetails.tabs.metadata.title": "Métadonnées",
|
||||
"xpack.infra.nodeDetails.tabs.metrics": "Indicateurs",
|
||||
"xpack.infra.nodeDetails.tabs.osquery": "Osquery",
|
||||
"xpack.infra.nodeDetails.yes": "Oui",
|
||||
"xpack.infra.nodesToWaffleMap.groupsWithGroups.allName": "Tous",
|
||||
"xpack.infra.nodesToWaffleMap.groupsWithNodes.allName": "Tous",
|
||||
"xpack.infra.notAvailableLabel": "S. O.",
|
||||
"xpack.infra.openView.actionNames.deleteConfirmation": "Supprimer la vue ?",
|
||||
"xpack.infra.openView.cancelButton": "Annuler",
|
||||
"xpack.infra.openView.columnNames.actions": "Actions",
|
||||
|
|
|
@ -18755,9 +18755,7 @@
|
|||
"xpack.infra.hostsViewPage.tabs.metricsCharts.tx": "ネットワーク送信(TX)",
|
||||
"xpack.infra.hostsViewPage.tooltip.whatAreTheseMetricsLink": "これらのメトリックは何か。",
|
||||
"xpack.infra.infra.nodeDetails.apmTabLabel": "APM",
|
||||
"xpack.infra.infra.nodeDetails.createAlertLink": "インベントリルールの作成",
|
||||
"xpack.infra.infra.nodeDetails.openAsPage": "ページとして開く",
|
||||
"xpack.infra.infra.nodeDetails.updtimeTabLabel": "アップタイム",
|
||||
"xpack.infra.inventory.alerting.groupActionVariableDescription": "データを報告するグループの名前",
|
||||
"xpack.infra.inventoryModel.container.displayName": "Dockerコンテナー",
|
||||
"xpack.infra.inventoryModel.container.singularDisplayName": "Docker コンテナー",
|
||||
|
@ -19247,7 +19245,6 @@
|
|||
"xpack.infra.metrics.nodeDetails.processes.stateZombie": "ゾンビ",
|
||||
"xpack.infra.metrics.nodeDetails.processes.viewTraceInAPM": "APM でトレースを表示",
|
||||
"xpack.infra.metrics.nodeDetails.processesHeader": "上位のプロセス",
|
||||
"xpack.infra.metrics.nodeDetails.processesHeader.tooltipBody": "次の表は、上位の CPU および上位のメモリ消費プロセスの集計です。一部のプロセスは表示されません。",
|
||||
"xpack.infra.metrics.nodeDetails.processesHeader.tooltipLabel": "詳細",
|
||||
"xpack.infra.metrics.nodeDetails.processListError": "プロセスデータを読み込めません",
|
||||
"xpack.infra.metrics.nodeDetails.processListRetry": "再試行",
|
||||
|
@ -19395,41 +19392,16 @@
|
|||
"xpack.infra.nodeDetails.labels.showMoreDetails": "他の詳細を表示",
|
||||
"xpack.infra.nodeDetails.logs.openLogsLink": "ログで開く",
|
||||
"xpack.infra.nodeDetails.logs.textFieldPlaceholder": "ログエントリーを検索...",
|
||||
"xpack.infra.nodeDetails.metrics.cached": "キャッシュ",
|
||||
"xpack.infra.nodeDetails.metrics.charts.loadTitle": "読み込み",
|
||||
"xpack.infra.nodeDetails.metrics.charts.logRateTitle": "ログレート",
|
||||
"xpack.infra.nodeDetails.metrics.charts.memoryTitle": "メモリー",
|
||||
"xpack.infra.nodeDetails.metrics.charts.networkTitle": "ネットワーク",
|
||||
"xpack.infra.nodeDetails.metrics.fcharts.cpuTitle": "CPU",
|
||||
"xpack.infra.nodeDetails.metrics.free": "空き",
|
||||
"xpack.infra.nodeDetails.metrics.inbound": "受信",
|
||||
"xpack.infra.nodeDetails.metrics.last15Minutes": "過去15分間",
|
||||
"xpack.infra.nodeDetails.metrics.last24Hours": "過去 24 時間",
|
||||
"xpack.infra.nodeDetails.metrics.last3Hours": "過去 3 時間",
|
||||
"xpack.infra.nodeDetails.metrics.last7Days": "過去 7 日間",
|
||||
"xpack.infra.nodeDetails.metrics.lastHour": "過去 1 時間",
|
||||
"xpack.infra.nodeDetails.metrics.logRate": "ログレート",
|
||||
"xpack.infra.nodeDetails.metrics.outbound": "送信",
|
||||
"xpack.infra.nodeDetails.metrics.system": "システム",
|
||||
"xpack.infra.nodeDetails.metrics.used": "使用中",
|
||||
"xpack.infra.nodeDetails.metrics.user": "ユーザー",
|
||||
"xpack.infra.nodeDetails.no": "いいえ",
|
||||
"xpack.infra.nodeDetails.tabs.anomalies": "異常",
|
||||
"xpack.infra.nodeDetails.tabs.logs": "ログ",
|
||||
"xpack.infra.nodeDetails.tabs.logs.title": "ログ",
|
||||
"xpack.infra.nodeDetails.tabs.metadata.agentHeader": "エージェント",
|
||||
"xpack.infra.nodeDetails.tabs.metadata.cloudHeader": "クラウド",
|
||||
"xpack.infra.nodeDetails.tabs.metadata.filterAriaLabel": "フィルター",
|
||||
"xpack.infra.nodeDetails.tabs.metadata.hostsHeader": "ホスト",
|
||||
"xpack.infra.nodeDetails.tabs.metadata.seeLess": "簡易表示",
|
||||
"xpack.infra.nodeDetails.tabs.metadata.setFilterTooltip": "フィルターでイベントを表示",
|
||||
"xpack.infra.nodeDetails.tabs.metadata.title": "メタデータ",
|
||||
"xpack.infra.nodeDetails.tabs.metrics": "メトリック",
|
||||
"xpack.infra.nodeDetails.tabs.osquery": "Osquery",
|
||||
"xpack.infra.nodeDetails.yes": "はい",
|
||||
"xpack.infra.nodesToWaffleMap.groupsWithGroups.allName": "すべて",
|
||||
"xpack.infra.nodesToWaffleMap.groupsWithNodes.allName": "すべて",
|
||||
"xpack.infra.notAvailableLabel": "N/A",
|
||||
"xpack.infra.openView.actionNames.deleteConfirmation": "ビューを削除しますか?",
|
||||
"xpack.infra.openView.cancelButton": "キャンセル",
|
||||
"xpack.infra.openView.columnNames.actions": "アクション",
|
||||
|
|
|
@ -18755,9 +18755,7 @@
|
|||
"xpack.infra.hostsViewPage.tabs.metricsCharts.tx": "网络出站数据 (TX)",
|
||||
"xpack.infra.hostsViewPage.tooltip.whatAreTheseMetricsLink": "这些指标是什么?",
|
||||
"xpack.infra.infra.nodeDetails.apmTabLabel": "APM",
|
||||
"xpack.infra.infra.nodeDetails.createAlertLink": "创建库存规则",
|
||||
"xpack.infra.infra.nodeDetails.openAsPage": "以页面形式打开",
|
||||
"xpack.infra.infra.nodeDetails.updtimeTabLabel": "运行时间",
|
||||
"xpack.infra.inventory.alerting.groupActionVariableDescription": "报告数据的组名称",
|
||||
"xpack.infra.inventoryModel.container.displayName": "Docker 容器",
|
||||
"xpack.infra.inventoryModel.container.singularDisplayName": "Docker 容器",
|
||||
|
@ -19247,7 +19245,6 @@
|
|||
"xpack.infra.metrics.nodeDetails.processes.stateZombie": "僵停",
|
||||
"xpack.infra.metrics.nodeDetails.processes.viewTraceInAPM": "在 APM 中查看跟踪",
|
||||
"xpack.infra.metrics.nodeDetails.processesHeader": "排序靠前的进程",
|
||||
"xpack.infra.metrics.nodeDetails.processesHeader.tooltipBody": "下表聚合了 CPU 和内存消耗靠前的进程。不显示所有进程。",
|
||||
"xpack.infra.metrics.nodeDetails.processesHeader.tooltipLabel": "更多信息",
|
||||
"xpack.infra.metrics.nodeDetails.processListError": "无法加载进程数据",
|
||||
"xpack.infra.metrics.nodeDetails.processListRetry": "重试",
|
||||
|
@ -19395,41 +19392,16 @@
|
|||
"xpack.infra.nodeDetails.labels.showMoreDetails": "显示更多详情",
|
||||
"xpack.infra.nodeDetails.logs.openLogsLink": "在日志中打开",
|
||||
"xpack.infra.nodeDetails.logs.textFieldPlaceholder": "搜索日志条目......",
|
||||
"xpack.infra.nodeDetails.metrics.cached": "已缓存",
|
||||
"xpack.infra.nodeDetails.metrics.charts.loadTitle": "加载",
|
||||
"xpack.infra.nodeDetails.metrics.charts.logRateTitle": "日志速率",
|
||||
"xpack.infra.nodeDetails.metrics.charts.memoryTitle": "内存",
|
||||
"xpack.infra.nodeDetails.metrics.charts.networkTitle": "网络",
|
||||
"xpack.infra.nodeDetails.metrics.fcharts.cpuTitle": "CPU",
|
||||
"xpack.infra.nodeDetails.metrics.free": "可用",
|
||||
"xpack.infra.nodeDetails.metrics.inbound": "入站",
|
||||
"xpack.infra.nodeDetails.metrics.last15Minutes": "过去 15 分钟",
|
||||
"xpack.infra.nodeDetails.metrics.last24Hours": "过去 24 小时",
|
||||
"xpack.infra.nodeDetails.metrics.last3Hours": "过去 3 小时",
|
||||
"xpack.infra.nodeDetails.metrics.last7Days": "过去 7 天",
|
||||
"xpack.infra.nodeDetails.metrics.lastHour": "过去一小时",
|
||||
"xpack.infra.nodeDetails.metrics.logRate": "日志速率",
|
||||
"xpack.infra.nodeDetails.metrics.outbound": "出站",
|
||||
"xpack.infra.nodeDetails.metrics.system": "系统",
|
||||
"xpack.infra.nodeDetails.metrics.used": "已使用",
|
||||
"xpack.infra.nodeDetails.metrics.user": "用户",
|
||||
"xpack.infra.nodeDetails.no": "否",
|
||||
"xpack.infra.nodeDetails.tabs.anomalies": "异常",
|
||||
"xpack.infra.nodeDetails.tabs.logs": "日志",
|
||||
"xpack.infra.nodeDetails.tabs.logs.title": "日志",
|
||||
"xpack.infra.nodeDetails.tabs.metadata.agentHeader": "代理",
|
||||
"xpack.infra.nodeDetails.tabs.metadata.cloudHeader": "云",
|
||||
"xpack.infra.nodeDetails.tabs.metadata.filterAriaLabel": "筛选",
|
||||
"xpack.infra.nodeDetails.tabs.metadata.hostsHeader": "主机",
|
||||
"xpack.infra.nodeDetails.tabs.metadata.seeLess": "显示更少",
|
||||
"xpack.infra.nodeDetails.tabs.metadata.setFilterTooltip": "使用筛选查看事件",
|
||||
"xpack.infra.nodeDetails.tabs.metadata.title": "元数据",
|
||||
"xpack.infra.nodeDetails.tabs.metrics": "指标",
|
||||
"xpack.infra.nodeDetails.tabs.osquery": "Osquery",
|
||||
"xpack.infra.nodeDetails.yes": "是",
|
||||
"xpack.infra.nodesToWaffleMap.groupsWithGroups.allName": "全部",
|
||||
"xpack.infra.nodesToWaffleMap.groupsWithNodes.allName": "全部",
|
||||
"xpack.infra.notAvailableLabel": "不可用",
|
||||
"xpack.infra.openView.actionNames.deleteConfirmation": "删除视图?",
|
||||
"xpack.infra.openView.cancelButton": "取消",
|
||||
"xpack.infra.openView.columnNames.actions": "操作",
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { parse } from 'url';
|
||||
import { KUBERNETES_TOUR_STORAGE_KEY } from '@kbn/infra-plugin/public/pages/metrics/inventory_view/components/kubernetes_tour';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import { DATES, INVENTORY_PATH } from './constants';
|
||||
|
@ -18,7 +19,14 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
const esArchiver = getService('esArchiver');
|
||||
const browser = getService('browser');
|
||||
const retry = getService('retry');
|
||||
const pageObjects = getPageObjects(['common', 'header', 'infraHome', 'infraSavedViews']);
|
||||
const pageObjects = getPageObjects([
|
||||
'common',
|
||||
'header',
|
||||
'infraHome',
|
||||
'timePicker',
|
||||
'assetDetails',
|
||||
'infraSavedViews',
|
||||
]);
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const testSubjects = getService('testSubjects');
|
||||
|
||||
|
@ -119,6 +127,92 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
await pageObjects.infraHome.getWaffleMap();
|
||||
// await pageObjects.infraHome.getWaffleMapTooltips(); see https://github.com/elastic/kibana/issues/137903
|
||||
});
|
||||
|
||||
describe('Asset Details flyout', () => {
|
||||
before(async () => {
|
||||
await pageObjects.infraHome.goToTime(DATE_WITH_DATA);
|
||||
await pageObjects.infraHome.getWaffleMap();
|
||||
await pageObjects.infraHome.inputAddHostNameFilter('demo-stack-nginx-01');
|
||||
await pageObjects.infraHome.clickOnNode();
|
||||
});
|
||||
|
||||
describe('Overview Tab', () => {
|
||||
before(async () => {
|
||||
await pageObjects.assetDetails.clickOverviewTab();
|
||||
});
|
||||
|
||||
[
|
||||
{ metric: 'cpuUsage', value: '0.8%' },
|
||||
{ metric: 'normalizedLoad1m', value: '1.4%' },
|
||||
{ metric: 'memoryUsage', value: '18.0%' },
|
||||
{ metric: 'diskSpaceUsage', value: '17.5%' },
|
||||
].forEach(({ metric, value }) => {
|
||||
it(`${metric} tile should show ${value}`, async () => {
|
||||
await retry.tryForTime(3 * 1000, async () => {
|
||||
const tileValue = await pageObjects.assetDetails.getAssetDetailsKPITileValue(
|
||||
metric
|
||||
);
|
||||
expect(tileValue).to.eql(value);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should render 9 charts in the Metrics section', async () => {
|
||||
const hosts = await pageObjects.assetDetails.getAssetDetailsMetricsCharts();
|
||||
expect(hosts.length).to.equal(9);
|
||||
});
|
||||
|
||||
it('should show alerts', async () => {
|
||||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
await pageObjects.assetDetails.overviewAlertsTitleExists();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Metadata Tab', () => {
|
||||
before(async () => {
|
||||
await pageObjects.assetDetails.clickMetadataTab();
|
||||
});
|
||||
|
||||
it('should show metadata table', async () => {
|
||||
await pageObjects.assetDetails.metadataTableExists();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Logs Tab', () => {
|
||||
before(async () => {
|
||||
await pageObjects.assetDetails.clickLogsTab();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await retry.try(async () => {
|
||||
await pageObjects.infraHome.closeFlyout();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render logs tab', async () => {
|
||||
await pageObjects.assetDetails.logsExists();
|
||||
});
|
||||
});
|
||||
|
||||
describe('APM Link Tab', () => {
|
||||
before(async () => {
|
||||
await pageObjects.infraHome.clickOnNode();
|
||||
await pageObjects.assetDetails.clickApmTabLink();
|
||||
});
|
||||
|
||||
it('should navigate to APM traces', async () => {
|
||||
const url = parse(await browser.getCurrentUrl());
|
||||
const query = decodeURIComponent(url.query ?? '');
|
||||
const kuery = 'kuery=host.hostname:"demo-stack-nginx-01"';
|
||||
|
||||
expect(url.pathname).to.eql('/app/apm/traces');
|
||||
expect(query).to.contain(kuery);
|
||||
|
||||
await returnTo(INVENTORY_PATH);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('shows query suggestions', async () => {
|
||||
await pageObjects.infraHome.goToTime(DATE_WITH_DATA);
|
||||
await pageObjects.infraHome.clickQueryBar();
|
||||
|
|
|
@ -180,5 +180,10 @@ export function AssetDetailsProvider({ getService }: FtrProviderContext) {
|
|||
async clickOsqueryTab() {
|
||||
return testSubjects.click('infraAssetDetailsOsqueryTab');
|
||||
},
|
||||
|
||||
// APM Tab link
|
||||
async clickApmTabLink() {
|
||||
return testSubjects.click('infraAssetDetailsApmServicesLinkTab');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -105,7 +105,7 @@ export function InfraHomePageProvider({ getService, getPageObjects }: FtrProvide
|
|||
|
||||
async clickOnNodeDetailsFlyoutOpenAsPage() {
|
||||
await retry.try(async () => {
|
||||
await testSubjects.click('infraNodeContextPopoverOpenAsPageButton');
|
||||
await testSubjects.click('infraAssetDetailsOpenAsPageButton');
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -434,6 +434,14 @@ export function InfraHomePageProvider({ getService, getPageObjects }: FtrProvide
|
|||
await queryBar.type('h');
|
||||
},
|
||||
|
||||
async inputAddHostNameFilter(hostName: string) {
|
||||
await this.enterSearchTerm(`host.name:"${hostName}"`);
|
||||
},
|
||||
|
||||
async clickOnNode() {
|
||||
return testSubjects.click('nodeContainer');
|
||||
},
|
||||
|
||||
async ensureSuggestionsPanelVisible() {
|
||||
await testSubjects.find('infraSuggestionsPanel');
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue