[8.x] [ECO][Infra] Add callout for ingesting metrics data in Host and Container views (#195378) (#196158)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[ECO][Infra] Add callout for ingesting metrics data in Host and
Container views
(#195378)](https://github.com/elastic/kibana/pull/195378)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Irene
Blanco","email":"irene.blanco@elastic.co"},"sourceCommit":{"committedDate":"2024-10-14T14:42:44Z","message":"[ECO][Infra]
Add callout for ingesting metrics data in Host and Container views
(#195378)\n\n## Summary\r\n\r\nCloses
https://github.com/elastic/kibana/issues/193703\r\n\r\nThis PR
introduces a callout designed to prompt users to ingest metrics\r\ndata
in the Host and Container views.\r\nThe callout will be displayed on the
following tabs:\r\n\r\n- **Hosts**: Overview, Metrics, Processes\r\n-
**Containers**: Overview, Metrics\r\n\r\nThe primary condition for
showing the callout is that the asset does not\r\ncurrently have any
metrics data available. This enhancement aims to\r\nencourage users to
take action and improve their monitoring experience.\r\n\r\nAdditional
details include:\r\n\r\n- The callout will be positioned below the date
picker for better\r\nvisibility.\r\n- Links for \"Add Metrics\" will
guide users to the appropriate onboarding\r\npages based on their asset
type.\r\n- The callout will be dismissible on the Overview tab, and the
KPI\r\nsection will be hidden in favor of the callout for a cleaner
interface.\r\n- Custom telemetry events will be tracked to measure user
interactions\r\nwith the callout.\r\n- Only Docker and K8 containers
will show the
callout.\r\n\r\n**Host**\r\n|Tab||\r\n|-|-|\r\n|Overview|![Screenshot
2024-10-08 at 12
19\r\n22](https://github.com/user-attachments/assets/e357d6c6-2423-40f9-a513-361c642dc07c)|\r\n|Metrics|![Screenshot
2024-10-08 at 12
19\r\n31](https://github.com/user-attachments/assets/559a6e71-344a-4b4a-9ad6-8d229a1d9bcb)|\r\n|Processes|![Screenshot
2024-10-08 at 12
19\r\n39](https://github.com/user-attachments/assets/070f6fb1-0756-4b21-abce-4b395be943df)|\r\n\r\n**Container**\r\n|Tab||\r\n|-|-|\r\n|Overview|![Screenshot
2024-10-08 at 12
24\r\n10](https://github.com/user-attachments/assets/101cfc7b-f445-44e7-9aa3-bec8928c3ed5)|\r\n|Metrics|![Screenshot
2024-10-08 at 12
21\r\n22](https://github.com/user-attachments/assets/d516d449-2af4-441f-9047-39c9362c5a86)|\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by:
Caue Marcondes
<caue.marcondes@elastic.co>","sha":"96966c5113678b8840c7a311e7e4ed1e977b4dac","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","backport:prev-minor","ci:project-deploy-observability","Team:obs-ux-logs","Team:obs-ux-infra_services","v8.16.0"],"title":"[ECO][Infra]
Add callout for ingesting metrics data in Host and Container
views","number":195378,"url":"https://github.com/elastic/kibana/pull/195378","mergeCommit":{"message":"[ECO][Infra]
Add callout for ingesting metrics data in Host and Container views
(#195378)\n\n## Summary\r\n\r\nCloses
https://github.com/elastic/kibana/issues/193703\r\n\r\nThis PR
introduces a callout designed to prompt users to ingest metrics\r\ndata
in the Host and Container views.\r\nThe callout will be displayed on the
following tabs:\r\n\r\n- **Hosts**: Overview, Metrics, Processes\r\n-
**Containers**: Overview, Metrics\r\n\r\nThe primary condition for
showing the callout is that the asset does not\r\ncurrently have any
metrics data available. This enhancement aims to\r\nencourage users to
take action and improve their monitoring experience.\r\n\r\nAdditional
details include:\r\n\r\n- The callout will be positioned below the date
picker for better\r\nvisibility.\r\n- Links for \"Add Metrics\" will
guide users to the appropriate onboarding\r\npages based on their asset
type.\r\n- The callout will be dismissible on the Overview tab, and the
KPI\r\nsection will be hidden in favor of the callout for a cleaner
interface.\r\n- Custom telemetry events will be tracked to measure user
interactions\r\nwith the callout.\r\n- Only Docker and K8 containers
will show the
callout.\r\n\r\n**Host**\r\n|Tab||\r\n|-|-|\r\n|Overview|![Screenshot
2024-10-08 at 12
19\r\n22](https://github.com/user-attachments/assets/e357d6c6-2423-40f9-a513-361c642dc07c)|\r\n|Metrics|![Screenshot
2024-10-08 at 12
19\r\n31](https://github.com/user-attachments/assets/559a6e71-344a-4b4a-9ad6-8d229a1d9bcb)|\r\n|Processes|![Screenshot
2024-10-08 at 12
19\r\n39](https://github.com/user-attachments/assets/070f6fb1-0756-4b21-abce-4b395be943df)|\r\n\r\n**Container**\r\n|Tab||\r\n|-|-|\r\n|Overview|![Screenshot
2024-10-08 at 12
24\r\n10](https://github.com/user-attachments/assets/101cfc7b-f445-44e7-9aa3-bec8928c3ed5)|\r\n|Metrics|![Screenshot
2024-10-08 at 12
21\r\n22](https://github.com/user-attachments/assets/d516d449-2af4-441f-9047-39c9362c5a86)|\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by:
Caue Marcondes
<caue.marcondes@elastic.co>","sha":"96966c5113678b8840c7a311e7e4ed1e977b4dac"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/195378","number":195378,"mergeCommit":{"message":"[ECO][Infra]
Add callout for ingesting metrics data in Host and Container views
(#195378)\n\n## Summary\r\n\r\nCloses
https://github.com/elastic/kibana/issues/193703\r\n\r\nThis PR
introduces a callout designed to prompt users to ingest metrics\r\ndata
in the Host and Container views.\r\nThe callout will be displayed on the
following tabs:\r\n\r\n- **Hosts**: Overview, Metrics, Processes\r\n-
**Containers**: Overview, Metrics\r\n\r\nThe primary condition for
showing the callout is that the asset does not\r\ncurrently have any
metrics data available. This enhancement aims to\r\nencourage users to
take action and improve their monitoring experience.\r\n\r\nAdditional
details include:\r\n\r\n- The callout will be positioned below the date
picker for better\r\nvisibility.\r\n- Links for \"Add Metrics\" will
guide users to the appropriate onboarding\r\npages based on their asset
type.\r\n- The callout will be dismissible on the Overview tab, and the
KPI\r\nsection will be hidden in favor of the callout for a cleaner
interface.\r\n- Custom telemetry events will be tracked to measure user
interactions\r\nwith the callout.\r\n- Only Docker and K8 containers
will show the
callout.\r\n\r\n**Host**\r\n|Tab||\r\n|-|-|\r\n|Overview|![Screenshot
2024-10-08 at 12
19\r\n22](https://github.com/user-attachments/assets/e357d6c6-2423-40f9-a513-361c642dc07c)|\r\n|Metrics|![Screenshot
2024-10-08 at 12
19\r\n31](https://github.com/user-attachments/assets/559a6e71-344a-4b4a-9ad6-8d229a1d9bcb)|\r\n|Processes|![Screenshot
2024-10-08 at 12
19\r\n39](https://github.com/user-attachments/assets/070f6fb1-0756-4b21-abce-4b395be943df)|\r\n\r\n**Container**\r\n|Tab||\r\n|-|-|\r\n|Overview|![Screenshot
2024-10-08 at 12
24\r\n10](https://github.com/user-attachments/assets/101cfc7b-f445-44e7-9aa3-bec8928c3ed5)|\r\n|Metrics|![Screenshot
2024-10-08 at 12
21\r\n22](https://github.com/user-attachments/assets/d516d449-2af4-441f-9047-39c9362c5a86)|\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by:
Caue Marcondes
<caue.marcondes@elastic.co>","sha":"96966c5113678b8840c7a311e7e4ed1e977b4dac"}},{"branch":"8.x","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Irene Blanco <irene.blanco@elastic.co>
This commit is contained in:
Kibana Machine 2024-10-16 19:47:24 +11:00 committed by GitHub
parent d216933327
commit e6392b297e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 1509 additions and 227 deletions

View file

@ -43,6 +43,7 @@ const STORYBOOKS = [
'lists',
'observability',
'observability_ai_assistant',
'observability_shared',
'presentation',
'security_solution',
'security_solution_packages',

View file

@ -56,6 +56,7 @@ export const storybookAliases = {
'x-pack/plugins/observability_solution/observability_ai_assistant/.storybook',
observability_ai_assistant_app:
'x-pack/plugins/observability_solution/observability_ai_assistant_app/.storybook',
observability_shared: 'x-pack/plugins/observability_solution/observability_shared/.storybook',
observability_slo: 'x-pack/plugins/observability_solution/slo/.storybook',
presentation: 'src/plugins/presentation_util/storybook',
random_sampling: 'x-pack/packages/kbn-random-sampling/.storybook',

View file

@ -7,12 +7,12 @@
import { Meta, Story } from '@storybook/react';
import React from 'react';
import { EntityDataStreamType } from '@kbn/observability-shared-plugin/common';
import { ServiceOverview } from '.';
import { MockApmPluginStorybook } from '../../../context/apm_plugin/mock_apm_plugin_storybook';
import { APMServiceContextValue } from '../../../context/apm_service/apm_service_context';
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
import { mockApmApiCallResponse } from '../../../services/rest/call_apm_api_spy';
import { EntityDataStreamType } from '../../../../common/entities/types';
const stories: Meta<{}> = {
title: 'app/ServiceOverview',

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { EntityDataStreamType } from '../../common/entities/types';
import { EntityDataStreamType } from '@kbn/observability-shared-plugin/common';
export function isApmSignal(dataStreamTypes: EntityDataStreamType[]) {
return (

View file

@ -6,9 +6,9 @@
*/
import { compact, uniq } from 'lodash';
import { EntityDataStreamType } from '@kbn/observability-shared-plugin/common';
import type { EntityLatestServiceRaw } from '../types';
import type { AgentName } from '../../../../typings/es_schemas/ui/fields/agent';
import type { EntityDataStreamType } from '../../../../common/entities/types';
export interface MergedServiceEntity {
serviceName: string;

View file

@ -0,0 +1,111 @@
/*
* 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 { ObservabilityOnboardingLocatorParams } from '@kbn/deeplinks-observability';
import { i18n } from '@kbn/i18n';
import { AddDataPanelProps } from '@kbn/observability-shared-plugin/public';
import { LocatorPublic } from '@kbn/share-plugin/common';
import { OnboardingFlow } from '../../shared/templates/no_data_config';
export type AddMetricsCalloutKey =
| 'hostOverview'
| 'hostMetrics'
| 'hostProcesses'
| 'containerOverview'
| 'containerMetrics';
const defaultPrimaryActionLabel = i18n.translate(
'xpack.infra.addDataCallout.hostOverviewPrimaryActionLabel',
{
defaultMessage: 'Add Metrics',
}
);
const defaultContent = {
content: {
title: i18n.translate('xpack.infra.addDataCallout.defaultTitle', {
defaultMessage: 'View core metrics to understand your host performance',
}),
content: i18n.translate('xpack.infra.addDataCallout.defaultContent', {
defaultMessage:
'Collect metrics such as CPU and memory usage to identify performance bottlenecks that could be affecting your users.',
}),
},
};
const hostDefaultActions = (
locator: LocatorPublic<ObservabilityOnboardingLocatorParams> | undefined
) => {
return {
actions: {
primary: {
href: locator?.getRedirectUrl({ category: OnboardingFlow.Hosts }),
label: defaultPrimaryActionLabel,
},
secondary: {
href: 'https://ela.st/demo-cluster-hosts',
},
link: {
href: 'https://ela.st/docs-hosts-add-metrics',
},
},
};
};
const containerDefaultActions = (
locator: LocatorPublic<ObservabilityOnboardingLocatorParams> | undefined
) => {
return {
actions: {
primary: {
href: locator?.getRedirectUrl({ category: OnboardingFlow.Infra }),
label: defaultPrimaryActionLabel,
},
link: {
href: 'https://ela.st/docs-containers-add-metrics',
},
},
};
};
export const addMetricsCalloutDefinitions = (
locator: LocatorPublic<ObservabilityOnboardingLocatorParams> | undefined
): Record<
AddMetricsCalloutKey,
Omit<AddDataPanelProps, 'onDismiss' | 'onAddData' | 'onLearnMore' | 'onTryIt'>
> => {
return {
hostOverview: {
...defaultContent,
...hostDefaultActions(locator),
},
hostMetrics: {
...defaultContent,
...hostDefaultActions(locator),
},
hostProcesses: {
content: {
title: i18n.translate('xpack.infra.addDataCallout.hostProcessesTitle', {
defaultMessage: 'View host processes to identify performance bottlenecks',
}),
content: i18n.translate('xpack.infra.addDataCallout.hostProcessesContent', {
defaultMessage:
'Collect process data to understand what is consuming resource on your hosts.',
}),
},
...hostDefaultActions(locator),
},
containerOverview: {
...defaultContent,
...containerDefaultActions(locator),
},
containerMetrics: {
...defaultContent,
...containerDefaultActions(locator),
},
};
};

View file

@ -0,0 +1,62 @@
/*
* 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 { AddDataPanel } from '@kbn/observability-shared-plugin/public';
import {
OBSERVABILITY_ONBOARDING_LOCATOR,
ObservabilityOnboardingLocatorParams,
} from '@kbn/deeplinks-observability';
import { AddMetricsCalloutEventParams } from '../../../services/telemetry';
import { addMetricsCalloutDefinitions, AddMetricsCalloutKey } from './constants';
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
export interface AddMetricsCalloutProps {
id: AddMetricsCalloutKey;
onDismiss?: () => void;
}
const defaultEventParams: AddMetricsCalloutEventParams = { view: 'add_metrics_cta' };
export function AddMetricsCallout({ id, onDismiss }: AddMetricsCalloutProps) {
const {
services: { telemetry, share },
} = useKibanaContextForPlugin();
const onboardingLocator = share.url.locators.get<ObservabilityOnboardingLocatorParams>(
OBSERVABILITY_ONBOARDING_LOCATOR
);
function handleAddMetricsClick() {
telemetry.reportAddMetricsCalloutAddMetricsClicked(defaultEventParams);
}
function handleTryItClick() {
telemetry.reportAddMetricsCalloutTryItClicked(defaultEventParams);
}
function handleLearnMoreClick() {
telemetry.reportAddMetricsCalloutLearnMoreClicked(defaultEventParams);
}
function handleDismiss() {
telemetry.reportAddMetricsCalloutDismissed(defaultEventParams);
onDismiss?.();
}
return (
<AddDataPanel
data-test-subj="infraAddMetricsCallout"
content={addMetricsCalloutDefinitions(onboardingLocator)[id].content}
actions={addMetricsCalloutDefinitions(onboardingLocator)[id].actions}
onAddData={handleAddMetricsClick}
onTryIt={handleTryItClick}
onLearnMore={handleLearnMoreClick}
onDissmiss={onDismiss && handleDismiss}
/>
);
}

View file

@ -0,0 +1,49 @@
/*
* 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 z from '@kbn/zod';
import { EntityDataStreamType, EntityType } from '@kbn/observability-shared-plugin/common';
import { useFetcher } from '../../../hooks/use_fetcher';
const EntityTypeSchema = z.union([z.literal(EntityType.HOST), z.literal(EntityType.CONTAINER)]);
const EntityDataStreamSchema = z.union([
z.literal(EntityDataStreamType.METRICS),
z.literal(EntityDataStreamType.LOGS),
]);
const EntitySummarySchema = z.object({
entityType: EntityTypeSchema,
entityId: z.string(),
sourceDataStreams: z.array(EntityDataStreamSchema),
});
export type EntitySummary = z.infer<typeof EntitySummarySchema>;
export function useEntitySummary({
entityType,
entityId,
}: {
entityType: string;
entityId: string;
}) {
const { data, status } = useFetcher(
async (callApi) => {
if (!entityType || !entityId) {
return undefined;
}
const response = await callApi(`/api/infra/entities/${entityType}/${entityId}/summary`, {
method: 'GET',
});
return EntitySummarySchema.parse(response);
},
[entityType, entityId]
);
return { dataStreams: data?.sourceDataStreams ?? [], status };
}

View file

@ -22,16 +22,25 @@ import {
useResizeObserver,
EuiListGroup,
EuiListGroupItem,
EuiSpacer,
} from '@elastic/eui';
import { css, cx } from '@emotion/css';
import { useAssetDetailsRenderPropsContext } from '../../hooks/use_asset_details_render_props';
import { useTabSwitcherContext } from '../../hooks/use_tab_switcher';
import { AddMetricsCalloutKey } from '../../add_metrics_callout/constants';
import { AddMetricsCallout } from '../../add_metrics_callout';
import { useEntitySummary } from '../../hooks/use_entity_summary';
import { isMetricsSignal } from '../../utils/get_data_stream_types';
export const MetricsTemplate = React.forwardRef<HTMLDivElement, { children: React.ReactNode }>(
({ children }, ref) => {
const { euiTheme } = useEuiTheme();
const { renderMode } = useAssetDetailsRenderPropsContext();
const { asset, renderMode } = useAssetDetailsRenderPropsContext();
const { scrollTo, setScrollTo } = useTabSwitcherContext();
const { dataStreams, status: dataStreamsStatus } = useEntitySummary({
entityType: asset.type,
entityId: asset.id,
});
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const initialScrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
@ -111,94 +120,110 @@ export const MetricsTemplate = React.forwardRef<HTMLDivElement, { children: Reac
const quickAccessItems = [...quickAccessItemsRef.current];
const showAddMetricsCallout =
dataStreamsStatus === 'success' &&
!isMetricsSignal(dataStreams) &&
renderMode.mode === 'page';
const addMetricsCalloutId: AddMetricsCalloutKey =
asset.type === 'host' ? 'hostMetrics' : 'containerMetrics';
return (
<EuiFlexGroup
gutterSize="s"
direction="row"
css={css`
${useEuiMaxBreakpoint('xl')} {
flex-direction: column;
}
`}
data-test-subj="infraAssetDetailsMetricChartsContent"
ref={ref}
>
<EuiFlexItem
grow={false}
<>
{showAddMetricsCallout && (
<>
<AddMetricsCallout id={addMetricsCalloutId} />
<EuiSpacer size="s" />
</>
)}
<EuiFlexGroup
gutterSize="s"
direction="row"
css={css`
position: sticky;
top: ${kibanaHeaderOffset};
background: ${euiTheme.colors.emptyShade};
min-width: 100px;
z-index: ${euiTheme.levels.navigation};
${useEuiMinBreakpoint('xl')} {
align-self: flex-start;
${useEuiMaxBreakpoint('xl')} {
flex-direction: column;
}
`}
data-test-subj="infraAssetDetailsMetricChartsContent"
ref={ref}
>
<div ref={quickAccessRef}>
<EuiListGroup
flush
css={css`
${useEuiMaxBreakpoint('xl')} {
flex-direction: row;
flex-wrap: wrap;
gap: 0px ${euiTheme.size.xl};
min-width: 100%;
border-bottom: ${euiTheme.border.thin};
}
`}
>
{quickAccessItems.map(([sectionId, label]) => (
<EuiListGroupItem
data-test-subj={`infraMetricsQuickAccessItem${sectionId}`}
onClick={() => onQuickAccessItemClick(sectionId)}
color="text"
size="s"
className={cx({
[css`
text-decoration: underline;
`]: sectionId === scrollTo,
})}
css={css`
background-color: unset;
& > button {
padding-block: ${euiTheme.size.s};
padding-inline: 0px;
}
&:hover,
&:focus-within {
background-color: unset;
}
`}
label={label}
/>
))}
</EuiListGroup>
</div>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup
gutterSize="m"
direction="column"
<EuiFlexItem
grow={false}
css={css`
padding-top: ${euiTheme.size.s};
& > [data-section-id] {
scroll-margin-top: ${quickAccessOffset};
position: sticky;
top: ${kibanaHeaderOffset};
background: ${euiTheme.colors.emptyShade};
min-width: 100px;
z-index: ${euiTheme.levels.navigation};
${useEuiMinBreakpoint('xl')} {
align-self: flex-start;
}
`}
>
{React.Children.map(children, (child, index) => {
if (React.isValidElement(child)) {
return React.cloneElement(child as React.ReactElement, {
ref: setContentRef,
key: index,
});
}
})}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
<div ref={quickAccessRef}>
<EuiListGroup
flush
css={css`
${useEuiMaxBreakpoint('xl')} {
flex-direction: row;
flex-wrap: wrap;
gap: 0px ${euiTheme.size.xl};
min-width: 100%;
border-bottom: ${euiTheme.border.thin};
}
`}
>
{quickAccessItems.map(([sectionId, label]) => (
<EuiListGroupItem
data-test-subj={`infraMetricsQuickAccessItem${sectionId}`}
key={sectionId}
onClick={() => onQuickAccessItemClick(sectionId)}
color="text"
size="s"
className={cx({
[css`
text-decoration: underline;
`]: sectionId === scrollTo,
})}
css={css`
background-color: unset;
& > button {
padding-block: ${euiTheme.size.s};
padding-inline: 0px;
}
&:hover,
&:focus-within {
background-color: unset;
}
`}
label={label}
/>
))}
</EuiListGroup>
</div>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup
gutterSize="m"
direction="column"
css={css`
padding-top: ${euiTheme.size.s};
& > [data-section-id] {
scroll-margin-top: ${quickAccessOffset};
}
`}
>
{React.Children.map(children, (child, index) => {
if (React.isValidElement(child)) {
return React.cloneElement(child as React.ReactElement, {
ref: setContentRef,
key: index,
});
}
})}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
}
);

View file

@ -8,6 +8,7 @@
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui';
import { css } from '@emotion/react';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import {
MetadataSummaryList,
MetadataSummaryListCompact,
@ -22,6 +23,12 @@ import { MetadataErrorCallout } from '../../components/metadata_error_callout';
import { CpuProfilingPrompt } from './kpis/cpu_profiling_prompt';
import { ServicesContent } from './services';
import { MetricsContent } from './metrics/metrics';
import { AddMetricsCallout } from '../../add_metrics_callout';
import { AddMetricsCalloutKey } from '../../add_metrics_callout/constants';
import { useEntitySummary } from '../../hooks/use_entity_summary';
import { isMetricsSignal } from '../../utils/get_data_stream_types';
import { INTEGRATIONS } from '../../constants';
import { useIntegrationCheck } from '../../hooks/use_integration_check';
export const Overview = () => {
const { dateRange } = useDatePickerContext();
@ -33,6 +40,20 @@ export const Overview = () => {
} = useMetadataStateContext();
const { metrics } = useDataViewsContext();
const isFullPageView = renderMode.mode === 'page';
const { dataStreams, status: dataStreamsStatus } = useEntitySummary({
entityType: asset.type,
entityId: asset.id,
});
const addMetricsCalloutId: AddMetricsCalloutKey =
asset.type === 'host' ? 'hostOverview' : 'containerOverview';
const [dismissedAddMetricsCallout, setDismissedAddMetricsCallout] = useLocalStorage(
`infra.dismissedAddMetricsCallout.${addMetricsCalloutId}`,
false
);
const isDockerContainer = useIntegrationCheck({ dependsOn: INTEGRATIONS.docker });
const isKubernetesContainer = useIntegrationCheck({
dependsOn: INTEGRATIONS.kubernetesContainer,
});
const metadataSummarySection = isFullPageView ? (
<MetadataSummaryList metadata={metadata} loading={metadataLoading} assetType={asset.type} />
@ -44,18 +65,48 @@ export const Overview = () => {
/>
);
const shouldShowCallout = () => {
if (
dataStreamsStatus !== 'success' ||
renderMode.mode !== 'page' ||
dismissedAddMetricsCallout
) {
return false;
}
const { type } = asset;
const baseCondition = !isMetricsSignal(dataStreams);
const isRelevantContainer =
type === 'container' && (isDockerContainer || isKubernetesContainer);
return baseCondition && (type === 'host' || isRelevantContainer);
};
const showAddMetricsCallout = shouldShowCallout();
return (
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem grow={false}>
<KPIGrid
assetId={asset.id}
assetType={asset.type}
dateRange={dateRange}
dataView={metrics.dataView}
/>
{asset.type === 'host' ? <CpuProfilingPrompt /> : null}
</EuiFlexItem>
{showAddMetricsCallout ? (
<EuiFlexItem grow={false}>
<AddMetricsCallout
id={addMetricsCalloutId}
onDismiss={() => {
setDismissedAddMetricsCallout(true);
}}
/>
</EuiFlexItem>
) : (
<EuiFlexItem grow={false}>
<KPIGrid
assetId={asset.id}
assetType={asset.type}
dateRange={dateRange}
dataView={metrics.dataView}
/>
{asset.type === 'host' ? <CpuProfilingPrompt /> : null}
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
{fetchMetadataError && !metadataLoading ? <MetadataErrorCallout /> : metadataSummarySection}
<SectionSeparator />

View file

@ -16,11 +16,14 @@ import {
Query,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiLoadingSpinner,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiLoadingSpinner } from '@elastic/eui';
import { getFieldByType } from '@kbn/metrics-data-access-plugin/common';
import { decodeOrThrow } from '@kbn/io-ts-utils';
import { EntityType } from '@kbn/observability-shared-plugin/common';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import { useSourceContext } from '../../../../containers/metrics_source';
import { isPending, useFetcher } from '../../../../hooks/use_fetcher';
import { parseSearchString } from './parse_search_string';
@ -36,6 +39,10 @@ import { TopProcessesTooltip } from '../../components/top_processes_tooltip';
import { ProcessListAPIResponseRT } from '../../../../../common/http_api';
import { useRequestObservable } from '../../hooks/use_request_observable';
import { useTabSwitcherContext } from '../../hooks/use_tab_switcher';
import { AddMetricsCalloutKey } from '../../add_metrics_callout/constants';
import { AddMetricsCallout } from '../../add_metrics_callout';
import { useEntitySummary } from '../../hooks/use_entity_summary';
import { isMetricsSignal } from '../../utils/get_data_stream_types';
const options = Object.entries(STATE_NAMES).map(([value, view]: [string, string]) => ({
value,
@ -46,10 +53,19 @@ export const Processes = () => {
const ref = useRef<HTMLDivElement>(null);
const { getDateRangeInTimestamp } = useDatePickerContext();
const [urlState, setUrlState] = useAssetDetailsUrlState();
const { asset } = useAssetDetailsRenderPropsContext();
const { asset, renderMode } = useAssetDetailsRenderPropsContext();
const { sourceId } = useSourceContext();
const { request$ } = useRequestObservable();
const { isActiveTab } = useTabSwitcherContext();
const { dataStreams, status: dataStreamsStatus } = useEntitySummary({
entityType: EntityType.HOST,
entityId: asset.name,
});
const addMetricsCalloutId: AddMetricsCalloutKey = 'hostProcesses';
const [dismissedAddMetricsCallout, setDismissedAddMetricsCallout] = useLocalStorage(
`infra.dismissedAddMetricsCallout.${addMetricsCalloutId}`,
false
);
const [searchText, setSearchText] = useState(urlState?.processSearch ?? '');
const [searchQueryError, setSearchQueryError] = useState<Error | null>(null);
@ -132,105 +148,124 @@ export const Processes = () => {
const isLoading = isPending(status);
const showAddMetricsCallout =
dataStreamsStatus === 'success' &&
!isMetricsSignal(dataStreams) &&
!dismissedAddMetricsCallout &&
renderMode.mode === 'page';
return (
<ProcessListContextProvider hostTerm={hostTerm} to={toTimestamp}>
<EuiFlexGroup direction="column" gutterSize="m" ref={ref}>
<EuiFlexItem grow={false}>
<SummaryTable
isLoading={isLoading}
processSummary={error || !data?.summary ? { total: 0 } : data?.summary}
<>
{showAddMetricsCallout && (
<>
<AddMetricsCallout
id={addMetricsCalloutId}
onDismiss={() => {
setDismissedAddMetricsCallout(true);
}}
/>
</EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="xs">
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle data-test-subj="infraAssetDetailsTopProcessesTitle" size="xxs">
<span>
<FormattedMessage
id="xpack.infra.metrics.nodeDetails.processesHeader"
defaultMessage="Top processes"
/>
</span>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<TopProcessesTooltip />
</EuiFlexItem>
</EuiFlexGroup>
{!error && (
<EuiFlexGroup alignItems="flexStart">
<EuiFlexItem>
{isLoading ? (
<EuiLoadingSpinner />
) : (
(data?.processList ?? []).length > 0 && <ProcessesExplanationMessage />
)}
<EuiSpacer />
</>
)}
<ProcessListContextProvider hostTerm={hostTerm} to={toTimestamp}>
<EuiFlexGroup direction="column" gutterSize="m" ref={ref}>
<EuiFlexItem grow={false}>
<SummaryTable
isLoading={isLoading}
processSummary={error || !data?.summary ? { total: 0 } : data?.summary}
/>
</EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="xs">
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle data-test-subj="infraAssetDetailsTopProcessesTitle" size="xxs">
<span>
<FormattedMessage
id="xpack.infra.metrics.nodeDetails.processesHeader"
defaultMessage="Top processes"
/>
</span>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<TopProcessesTooltip />
</EuiFlexItem>
</EuiFlexGroup>
)}
{!error && (
<EuiFlexGroup alignItems="flexStart">
<EuiFlexItem>
{isLoading ? (
<EuiLoadingSpinner />
) : (
(data?.processList ?? []).length > 0 && <ProcessesExplanationMessage />
)}
</EuiFlexItem>
</EuiFlexGroup>
)}
</EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiSearchBar
query={searchBarState}
onChange={searchBarOnChange}
box={{
'data-test-subj': 'infraAssetDetailsProcessesSearchBarInput',
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,
},
]}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{!error ? (
<ProcessesTable
currentTime={toTimestamp}
isLoading={isLoading}
processList={data?.processList ?? []}
sortBy={sortBy}
error={searchQueryError?.message}
setSortBy={setSortBy}
clearSearchBar={clearSearchBar}
/>
) : (
<EuiEmptyPrompt
iconType="warning"
title={
<h4>
<FormattedMessage
id="xpack.infra.metrics.nodeDetails.processListError"
defaultMessage="Unable to load process data"
/>
</h4>
}
actions={
<EuiButton
data-test-subj="infraAssetDetailsTabComponentTryAgainButton"
color="primary"
fill
onClick={refetch}
>
<FormattedMessage
id="xpack.infra.metrics.nodeDetails.processListRetry"
defaultMessage="Try again"
/>
</EuiButton>
}
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiSearchBar
query={searchBarState}
onChange={searchBarOnChange}
box={{
'data-test-subj': 'infraAssetDetailsProcessesSearchBarInput',
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,
},
]}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{!error ? (
<ProcessesTable
currentTime={toTimestamp}
isLoading={isLoading}
processList={data?.processList ?? []}
sortBy={sortBy}
error={searchQueryError?.message}
setSortBy={setSortBy}
clearSearchBar={clearSearchBar}
/>
) : (
<EuiEmptyPrompt
iconType="warning"
title={
<h4>
<FormattedMessage
id="xpack.infra.metrics.nodeDetails.processListError"
defaultMessage="Unable to load process data"
/>
</h4>
}
actions={
<EuiButton
data-test-subj="infraAssetDetailsTabComponentTryAgainButton"
color="primary"
fill
onClick={refetch}
>
<FormattedMessage
id="xpack.infra.metrics.nodeDetails.processListRetry"
defaultMessage="Try again"
/>
</EuiButton>
}
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
</ProcessListContextProvider>
</ProcessListContextProvider>
</>
);
};

View file

@ -23,6 +23,8 @@ import { getIntegrationsAvailable } from '../utils';
import { InfraPageTemplate } from '../../shared/templates/infra_page_template';
import { OnboardingFlow } from '../../shared/templates/no_data_config';
import { PageTitleWithPopover } from '../header/page_title_with_popover';
import { useEntitySummary } from '../hooks/use_entity_summary';
import { isMetricsSignal } from '../utils/get_data_stream_types';
const DATA_AVAILABILITY_PER_TYPE: Partial<Record<InventoryItemType, string[]>> = {
host: [SYSTEM_INTEGRATION],
@ -34,7 +36,10 @@ export const Page = ({ tabs = [], links = [] }: ContentTemplateProps) => {
const { rightSideItems, tabEntries, breadcrumbs: headerBreadcrumbs } = usePageHeader(tabs, links);
const { asset } = useAssetDetailsRenderPropsContext();
const trackOnlyOnce = React.useRef(false);
const { dataStreams } = useEntitySummary({
entityType: asset.type,
entityId: asset.id,
});
const { activeTabId } = useTabSwitcherContext();
const {
services: { telemetry },
@ -79,6 +84,8 @@ export const Page = ({ tabs = [], links = [] }: ContentTemplateProps) => {
}
}, [activeTabId, asset.type, metadata, metadataLoading, telemetry]);
const showPageTitleWithPopover = asset.type === 'host' && !isMetricsSignal(dataStreams);
return (
<InfraPageTemplate
onboardingFlow={asset.type === 'host' ? OnboardingFlow.Hosts : OnboardingFlow.Infra}
@ -86,7 +93,7 @@ export const Page = ({ tabs = [], links = [] }: ContentTemplateProps) => {
pageHeader={{
pageTitle: loading ? (
<EuiLoadingSpinner size="m" />
) : asset.type === 'host' ? (
) : showPageTitleWithPopover ? (
<PageTitleWithPopover name={asset.name} />
) : (
asset.name

View file

@ -0,0 +1,12 @@
/*
* 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 { EntityDataStreamType } from '@kbn/observability-shared-plugin/common';
export function isMetricsSignal(dataStreamTypes: EntityDataStreamType[] = []) {
return dataStreamTypes?.includes(EntityDataStreamType.METRICS);
}

View file

@ -17,4 +17,8 @@ export const createTelemetryClientMock = (): jest.Mocked<ITelemetryClient> => ({
reportAssetDetailsPageViewed: jest.fn(),
reportPerformanceMetricEvent: jest.fn(),
reportAssetDashboardLoaded: jest.fn(),
reportAddMetricsCalloutAddMetricsClicked: jest.fn(),
reportAddMetricsCalloutTryItClicked: jest.fn(),
reportAddMetricsCalloutLearnMoreClicked: jest.fn(),
reportAddMetricsCalloutDismissed: jest.fn(),
});

View file

@ -8,6 +8,7 @@
import type { AnalyticsServiceSetup } from '@kbn/core-analytics-server';
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
import {
AddMetricsCalloutEventParams,
AssetDashboardLoadedParams,
AssetDetailsFlyoutViewedParams,
AssetDetailsPageViewedParams,
@ -91,4 +92,26 @@ export class TelemetryClient implements ITelemetryClient {
...innerEvents,
});
};
public reportAddMetricsCalloutAddMetricsClicked = (params: AddMetricsCalloutEventParams) => {
this.analytics.reportEvent(
InfraTelemetryEventTypes.ADD_METRICS_CALLOUT_ADD_METRICS_CLICKED,
params
);
};
public reportAddMetricsCalloutTryItClicked = (params: AddMetricsCalloutEventParams) => {
this.analytics.reportEvent(InfraTelemetryEventTypes.ADD_METRICS_CALLOUT_TRY_IT_CLICKED, params);
};
public reportAddMetricsCalloutLearnMoreClicked = (params: AddMetricsCalloutEventParams) => {
this.analytics.reportEvent(
InfraTelemetryEventTypes.ADD_METRICS_CALLOUT_LEARN_MORE_CLICKED,
params
);
};
public reportAddMetricsCalloutDismissed = (params: AddMetricsCalloutEventParams) => {
this.analytics.reportEvent(InfraTelemetryEventTypes.ADD_METRICS_CALLOUT_DISMISSED, params);
};
}

View file

@ -216,6 +216,54 @@ const assetDashboardLoaded: InfraTelemetryEvent = {
},
};
const addMetricsCalloutAddMetricsClicked: InfraTelemetryEvent = {
eventType: InfraTelemetryEventTypes.ADD_METRICS_CALLOUT_ADD_METRICS_CLICKED,
schema: {
view: {
type: 'keyword',
_meta: {
description: 'Where the action was initiated (add_metrics_cta)',
},
},
},
};
const addMetricsCalloutTryItClicked: InfraTelemetryEvent = {
eventType: InfraTelemetryEventTypes.ADD_METRICS_CALLOUT_TRY_IT_CLICKED,
schema: {
view: {
type: 'keyword',
_meta: {
description: 'Where the action was initiated (add_metrics_cta)',
},
},
},
};
const addMetricsCalloutLearnMoreClicked: InfraTelemetryEvent = {
eventType: InfraTelemetryEventTypes.ADD_METRICS_CALLOUT_LEARN_MORE_CLICKED,
schema: {
view: {
type: 'keyword',
_meta: {
description: 'Where the action was initiated (add_metrics_cta)',
},
},
},
};
const addMetricsCalloutDismissed: InfraTelemetryEvent = {
eventType: InfraTelemetryEventTypes.ADD_METRICS_CALLOUT_DISMISSED,
schema: {
view: {
type: 'keyword',
_meta: {
description: 'Where the action was initiated (add_metrics_cta)',
},
},
},
};
export const infraTelemetryEvents = [
assetDetailsFlyoutViewed,
assetDetailsPageViewed,
@ -225,4 +273,8 @@ export const infraTelemetryEvents = [
hostFlyoutAddFilter,
hostViewTotalHostCountRetrieved,
assetDashboardLoaded,
addMetricsCalloutAddMetricsClicked,
addMetricsCalloutTryItClicked,
addMetricsCalloutLearnMoreClicked,
addMetricsCalloutDismissed,
];

View file

@ -261,4 +261,86 @@ describe('TelemetryService', () => {
);
});
});
describe('#reportAddMetricsCalloutAddMetricsClicked', () => {
it('should report add metrics callout add data click with properties', async () => {
const setupParams = getSetupParams();
service.setup(setupParams);
const telemetry = service.start();
const view = 'testView';
telemetry.reportAddMetricsCalloutAddMetricsClicked({ view });
expect(setupParams.analytics.reportEvent).toHaveBeenCalledTimes(1);
expect(setupParams.analytics.reportEvent).toHaveBeenCalledWith(
'Add Metrics Callout Add Metrics Clicked',
{
view,
}
);
});
});
describe('#reportAddMetricsCalloutTryItClicked', () => {
it('should report add metrics callout try it click with properties', async () => {
const setupParams = getSetupParams();
service.setup(setupParams);
const telemetry = service.start();
const view = 'testView';
telemetry.reportAddMetricsCalloutTryItClicked({
view,
});
expect(setupParams.analytics.reportEvent).toHaveBeenCalledTimes(1);
expect(setupParams.analytics.reportEvent).toHaveBeenCalledWith(
'Add Metrics Callout Try It Clicked',
{
view,
}
);
});
});
describe('#reportAddMetricsCalloutLearnMoreClicked', () => {
it('should report add metrics callout learn more click with properties', async () => {
const setupParams = getSetupParams();
service.setup(setupParams);
const telemetry = service.start();
const view = 'testView';
telemetry.reportAddMetricsCalloutLearnMoreClicked({
view,
});
expect(setupParams.analytics.reportEvent).toHaveBeenCalledTimes(1);
expect(setupParams.analytics.reportEvent).toHaveBeenCalledWith(
'Add Metrics Callout Learn More Clicked',
{
view,
}
);
});
});
describe('#reportAddMetricsCalloutDismissed', () => {
it('should report add metrics callout dismiss click with properties', async () => {
const setupParams = getSetupParams();
service.setup(setupParams);
const telemetry = service.start();
const view = 'testView';
telemetry.reportAddMetricsCalloutDismissed({
view,
});
expect(setupParams.analytics.reportEvent).toHaveBeenCalledTimes(1);
expect(setupParams.analytics.reportEvent).toHaveBeenCalledWith(
'Add Metrics Callout Dismissed',
{
view,
}
);
});
});
});

View file

@ -22,6 +22,10 @@ export enum InfraTelemetryEventTypes {
ASSET_DETAILS_FLYOUT_VIEWED = 'Asset Details Flyout Viewed',
ASSET_DETAILS_PAGE_VIEWED = 'Asset Details Page Viewed',
ASSET_DASHBOARD_LOADED = 'Asset Dashboard Loaded',
ADD_METRICS_CALLOUT_ADD_METRICS_CLICKED = 'Add Metrics Callout Add Metrics Clicked',
ADD_METRICS_CALLOUT_TRY_IT_CLICKED = 'Add Metrics Callout Try It Clicked',
ADD_METRICS_CALLOUT_LEARN_MORE_CLICKED = 'Add Metrics Callout Learn More Clicked',
ADD_METRICS_CALLOUT_DISMISSED = 'Add Metrics Callout Dismissed',
}
export interface HostsViewQuerySubmittedParams {
@ -61,13 +65,18 @@ export interface AssetDashboardLoadedParams {
filtered_by?: string[];
}
export interface AddMetricsCalloutEventParams {
view: string;
}
export type InfraTelemetryEventParams =
| HostsViewQuerySubmittedParams
| HostEntryClickedParams
| HostFlyoutFilterActionParams
| HostsViewQueryHostsCountRetrievedParams
| AssetDetailsFlyoutViewedParams
| AssetDashboardLoadedParams;
| AssetDashboardLoadedParams
| AddMetricsCalloutEventParams;
export interface PerformanceMetricInnerEvents {
key1?: string;
@ -89,6 +98,10 @@ export interface ITelemetryClient {
meta: Record<string, unknown>
): void;
reportAssetDashboardLoaded(params: AssetDashboardLoadedParams): void;
reportAddMetricsCalloutAddMetricsClicked(params: AddMetricsCalloutEventParams): void;
reportAddMetricsCalloutTryItClicked(params: AddMetricsCalloutEventParams): void;
reportAddMetricsCalloutLearnMoreClicked(params: AddMetricsCalloutEventParams): void;
reportAddMetricsCalloutDismissed(params: AddMetricsCalloutEventParams): void;
}
export type InfraTelemetryEvent =
@ -123,4 +136,20 @@ export type InfraTelemetryEvent =
| {
eventType: InfraTelemetryEventTypes.ASSET_DASHBOARD_LOADED;
schema: RootSchema<AssetDashboardLoadedParams>;
}
| {
eventType: InfraTelemetryEventTypes.ADD_METRICS_CALLOUT_ADD_METRICS_CLICKED;
schema: RootSchema<AddMetricsCalloutEventParams>;
}
| {
eventType: InfraTelemetryEventTypes.ADD_METRICS_CALLOUT_LEARN_MORE_CLICKED;
schema: RootSchema<AddMetricsCalloutEventParams>;
}
| {
eventType: InfraTelemetryEventTypes.ADD_METRICS_CALLOUT_TRY_IT_CLICKED;
schema: RootSchema<AddMetricsCalloutEventParams>;
}
| {
eventType: InfraTelemetryEventTypes.ADD_METRICS_CALLOUT_DISMISSED;
schema: RootSchema<AddMetricsCalloutEventParams>;
};

View file

@ -0,0 +1,136 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client';
import { type EntityClient } from '@kbn/entityManager-plugin/server/lib/entity_client';
import { type InfraMetricsClient } from '../../lib/helpers/get_infra_metrics_client';
import { getDataStreamTypes } from './get_data_stream_types';
import { getHasMetricsData } from './get_has_metrics_data';
import { getLatestEntity } from './get_latest_entity';
jest.mock('./get_has_metrics_data', () => ({
getHasMetricsData: jest.fn(),
}));
jest.mock('./get_latest_entity', () => ({
getLatestEntity: jest.fn(),
}));
type EntityType = 'host' | 'container';
describe('getDataStreamTypes', () => {
let infraMetricsClient: jest.Mocked<InfraMetricsClient>;
let obsEsClient: jest.Mocked<ObservabilityElasticsearchClient>;
let entityManagerClient: jest.Mocked<EntityClient>;
beforeEach(() => {
infraMetricsClient = {} as jest.Mocked<InfraMetricsClient>;
obsEsClient = {} as jest.Mocked<ObservabilityElasticsearchClient>;
entityManagerClient = {} as jest.Mocked<EntityClient>;
jest.clearAllMocks();
});
it('should return only metrics when entityCentriExperienceEnabled is false and hasMetricsData is true', async () => {
(getHasMetricsData as jest.Mock).mockResolvedValue(true);
const params = {
entityId: 'entity123',
entityType: 'host' as EntityType,
entityCentriExperienceEnabled: false,
infraMetricsClient,
obsEsClient,
entityManagerClient,
};
const result = await getDataStreamTypes(params);
expect(result).toEqual(['metrics']);
expect(getHasMetricsData).toHaveBeenCalledWith({
infraMetricsClient,
entityId: 'entity123',
field: 'host.name',
});
});
it('should return an empty array when entityCentriExperienceEnabled is false and hasMetricsData is false', async () => {
(getHasMetricsData as jest.Mock).mockResolvedValue(false);
const params = {
entityId: 'entity123',
entityType: 'container' as EntityType,
entityCentriExperienceEnabled: false,
infraMetricsClient,
obsEsClient,
entityManagerClient,
};
const result = await getDataStreamTypes(params);
expect(result).toEqual([]);
});
it('should return metrics and entity source_data_stream types when entityCentriExperienceEnabled is true and has entity data', async () => {
(getHasMetricsData as jest.Mock).mockResolvedValue(true);
(getLatestEntity as jest.Mock).mockResolvedValue({
'source_data_stream.type': ['logs', 'metrics'],
});
const params = {
entityId: 'entity123',
entityType: 'host' as EntityType,
entityCentriExperienceEnabled: true,
infraMetricsClient,
obsEsClient,
entityManagerClient,
};
const result = await getDataStreamTypes(params);
expect(result).toEqual(['metrics', 'logs']);
expect(getHasMetricsData).toHaveBeenCalled();
expect(getLatestEntity).toHaveBeenCalledWith({
inventoryEsClient: obsEsClient,
entityId: 'entity123',
entityType: 'host',
entityManagerClient,
});
});
it('should return only metrics when entityCentriExperienceEnabled is true but entity data is undefined', async () => {
(getHasMetricsData as jest.Mock).mockResolvedValue(true);
(getLatestEntity as jest.Mock).mockResolvedValue(undefined);
const params = {
entityId: 'entity123',
entityType: 'host' as EntityType,
entityCentriExperienceEnabled: true,
infraMetricsClient,
obsEsClient,
entityManagerClient,
};
const result = await getDataStreamTypes(params);
expect(result).toEqual(['metrics']);
});
it('should return entity source_data_stream types when has no metrics', async () => {
(getHasMetricsData as jest.Mock).mockResolvedValue(false);
(getLatestEntity as jest.Mock).mockResolvedValue({
'source_data_stream.type': ['logs', 'traces'],
});
const params = {
entityId: 'entity123',
entityType: 'host' as EntityType,
entityCentriExperienceEnabled: true,
infraMetricsClient,
obsEsClient,
entityManagerClient,
};
const result = await getDataStreamTypes(params);
expect(result).toEqual(['logs', 'traces']);
});
});

View file

@ -0,0 +1,62 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { type EntityClient } from '@kbn/entityManager-plugin/server/lib/entity_client';
import { findInventoryFields } from '@kbn/metrics-data-access-plugin/common';
import {
EntityDataStreamType,
SOURCE_DATA_STREAM_TYPE,
} from '@kbn/observability-shared-plugin/common';
import type { ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client';
import { type InfraMetricsClient } from '../../lib/helpers/get_infra_metrics_client';
import { getHasMetricsData } from './get_has_metrics_data';
import { getLatestEntity } from './get_latest_entity';
interface Params {
entityId: string;
entityType: 'host' | 'container';
entityCentriExperienceEnabled: boolean;
infraMetricsClient: InfraMetricsClient;
obsEsClient: ObservabilityElasticsearchClient;
entityManagerClient: EntityClient;
}
export async function getDataStreamTypes({
entityCentriExperienceEnabled,
entityId,
entityManagerClient,
entityType,
infraMetricsClient,
obsEsClient,
}: Params) {
const hasMetricsData = await getHasMetricsData({
infraMetricsClient,
entityId,
field: findInventoryFields(entityType).id,
});
const sourceDataStreams = new Set(hasMetricsData ? [EntityDataStreamType.METRICS] : []);
if (!entityCentriExperienceEnabled) {
return Array.from(sourceDataStreams);
}
const entity = await getLatestEntity({
inventoryEsClient: obsEsClient,
entityId,
entityType,
entityManagerClient,
});
if (entity?.[SOURCE_DATA_STREAM_TYPE]) {
[entity[SOURCE_DATA_STREAM_TYPE]].flat().forEach((item) => {
sourceDataStreams.add(item as EntityDataStreamType);
});
}
return Array.from(sourceDataStreams);
}

View file

@ -0,0 +1,31 @@
/*
* 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 { termQuery } from '@kbn/observability-plugin/server';
import { InfraMetricsClient } from '../../lib/helpers/get_infra_metrics_client';
export async function getHasMetricsData({
infraMetricsClient,
field,
entityId,
}: {
infraMetricsClient: InfraMetricsClient;
field: string;
entityId: string;
}) {
const results = await infraMetricsClient.search({
allow_no_indices: true,
ignore_unavailable: true,
body: {
track_total_hits: true,
terminate_after: 1,
size: 0,
query: { bool: { filter: termQuery(field, entityId) } },
},
});
return results.hits.total.value !== 0;
}

View file

@ -5,14 +5,14 @@
* 2.0.
*/
import { type ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client';
import { esqlResultToPlainObjects } from '@kbn/observability-utils/es/utils/esql_result_to_plain_objects';
import { ENTITY_LATEST, EntityDefinition, entitiesAliasPattern } from '@kbn/entities-schema';
import { type EntityDefinitionWithState } from '@kbn/entityManager-plugin/server/lib/entities/types';
import { ENTITY_LATEST, entitiesAliasPattern } from '@kbn/entities-schema';
import { type EntityClient } from '@kbn/entityManager-plugin/server/lib/entity_client';
import {
ENTITY_TYPE,
SOURCE_DATA_STREAM_TYPE,
} from '@kbn/observability-shared-plugin/common/field_names/elasticsearch';
import type { ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client';
import { esqlResultToPlainObjects } from '@kbn/observability-utils/es/utils/esql_result_to_plain_objects';
const ENTITIES_LATEST_ALIAS = entitiesAliasPattern({
type: '*',
@ -27,23 +27,30 @@ export async function getLatestEntity({
inventoryEsClient,
entityId,
entityType,
entityDefinitions,
entityManagerClient,
}: {
inventoryEsClient: ObservabilityElasticsearchClient;
entityType: 'host' | 'container';
entityId: string;
entityDefinitions: EntityDefinition[] | EntityDefinitionWithState[];
}) {
const hostOrContainerIdentityField = entityDefinitions[0]?.identityFields?.[0]?.field;
entityManagerClient: EntityClient;
}): Promise<Entity | undefined> {
const { definitions } = await entityManagerClient.getEntityDefinitions({
builtIn: true,
type: entityType,
});
const hostOrContainerIdentityField = definitions[0]?.identityFields?.[0]?.field;
if (hostOrContainerIdentityField === undefined) {
return;
return { [SOURCE_DATA_STREAM_TYPE]: [] };
}
const latestEntitiesEsqlResponse = await inventoryEsClient.esql('get_latest_entities', {
query: `FROM ${ENTITIES_LATEST_ALIAS}
| WHERE ${ENTITY_TYPE} == "${entityType}"
| WHERE ${hostOrContainerIdentityField} == "${entityId}"
| WHERE ${ENTITY_TYPE} == ?
| WHERE ${hostOrContainerIdentityField} == ?
| KEEP ${SOURCE_DATA_STREAM_TYPE}
`,
params: [entityType, entityId],
});
return esqlResultToPlainObjects<Entity>(latestEntitiesEsqlResponse)[0];

View file

@ -7,10 +7,11 @@
import { schema } from '@kbn/config-schema';
import { METRICS_APP_ID } from '@kbn/deeplinks-observability/constants';
import { SOURCE_DATA_STREAM_TYPE } from '@kbn/observability-shared-plugin/common/field_names/elasticsearch';
import { entityCentricExperience } from '@kbn/observability-plugin/common';
import { createObservabilityEsClient } from '@kbn/observability-utils/es/client/create_observability_es_client';
import { getInfraMetricsClient } from '../../lib/helpers/get_infra_metrics_client';
import { InfraBackendLibs } from '../../lib/infra_types';
import { getLatestEntity } from './get_latest_entity';
import { getDataStreamTypes } from './get_data_stream_types';
export const initEntitiesConfigurationRoutes = (libs: InfraBackendLibs) => {
const { framework, logger } = libs;
@ -33,36 +34,36 @@ export const initEntitiesConfigurationRoutes = (libs: InfraBackendLibs) => {
const { entityId, entityType } = request.params;
const coreContext = await requestContext.core;
const infraContext = await requestContext.infra;
const entityManager = await infraContext.entityManager.getScopedClient({ request });
const entityManagerClient = await infraContext.entityManager.getScopedClient({ request });
const infraMetricsClient = await getInfraMetricsClient({
request,
libs,
context: requestContext,
});
const client = createObservabilityEsClient({
const obsEsClient = createObservabilityEsClient({
client: coreContext.elasticsearch.client.asCurrentUser,
logger,
plugin: `@kbn/${METRICS_APP_ID}-plugin`,
});
try {
// Only fetch built in definitions
const { definitions } = await entityManager.getEntityDefinitions({
builtIn: true,
type: entityType,
});
if (definitions.length === 0) {
return response.ok({
body: { sourceDataStreams: [], entityId, entityType },
});
}
const entityCentriExperienceEnabled = await coreContext.uiSettings.client.get(
entityCentricExperience
);
const entity = await getLatestEntity({
inventoryEsClient: client,
try {
const sourceDataStreamTypes = await getDataStreamTypes({
entityCentriExperienceEnabled,
entityId,
entityManagerClient,
entityType,
entityDefinitions: definitions,
infraMetricsClient,
obsEsClient,
});
return response.ok({
body: {
sourceDataStreams: [entity?.[SOURCE_DATA_STREAM_TYPE] || []].flat() as string[],
sourceDataStreams: sourceDataStreamTypes,
entityId,
entityType,
},

View file

@ -115,7 +115,8 @@
"@kbn/core-ui-settings-common",
"@kbn/entityManager-plugin",
"@kbn/observability-utils",
"@kbn/entities-schema"
"@kbn/entities-schema",
"@kbn/zod"
],
"exclude": ["target/**/*"]
}

View file

@ -0,0 +1,19 @@
/*
* 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 { coreMock } from '@kbn/core/public/mocks';
import type { CoreStart } from '@kbn/core/public';
export type ObservabilitySharedKibanaContext = CoreStart;
export function getMockContext(): ObservabilitySharedKibanaContext {
const coreStart = coreMock.createStart();
return {
...coreStart,
};
}

View file

@ -6,5 +6,12 @@
*/
import { EuiThemeProviderDecorator } from '@kbn/kibana-react-plugin/common';
import { addDecorator } from '@storybook/react';
import { KibanaReactStorybookDecorator } from './storybook_decorator';
import * as jest from 'jest-mock';
export const decorators = [EuiThemeProviderDecorator];
window.jest = jest;
addDecorator(KibanaReactStorybookDecorator);

View file

@ -0,0 +1,29 @@
/*
* 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, { ComponentType, useMemo } from 'react';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { getMockContext, ObservabilitySharedKibanaContext } from './get_mock_context';
export function ObservabilitySharedContextProvider({
context,
children,
}: {
context: ObservabilitySharedKibanaContext;
children: React.ReactNode;
}) {
return <KibanaContextProvider services={context}>{children}</KibanaContextProvider>;
}
export function KibanaReactStorybookDecorator(Story: ComponentType) {
const context = useMemo(() => getMockContext(), []);
return (
<ObservabilitySharedContextProvider context={context}>
<Story />
</ObservabilitySharedContextProvider>
);
}

View file

@ -0,0 +1,12 @@
/*
* 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 enum EntityDataStreamType {
METRICS = 'metrics',
TRACES = 'traces',
LOGS = 'logs',
}

View file

@ -0,0 +1,11 @@
/*
* 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 enum EntityType {
HOST = 'host',
CONTAINER = 'container',
}

View file

@ -0,0 +1,9 @@
/*
* 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 { EntityType } from './entity_types';
export { EntityDataStreamType } from './entity_data_stream_types';

View file

@ -217,3 +217,5 @@ export {
} from './locators';
export { COMMON_OBSERVABILITY_GROUPING } from './embeddable_grouping';
export { EntityType, EntityDataStreamType } from './entity';

View file

@ -0,0 +1,152 @@
/*
* 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, { ComponentProps, ComponentType } from 'react';
import { AddDataPanel } from '.';
export default {
title: 'APP/AddDataPanel',
component: AddDataPanel,
decorators: [(Story: ComponentType) => <Story />],
};
const defaultFunctions = {
onDissmiss: () => alert('Dismissed'),
onAddData: () => alert('Add Data'),
onTryIt: () => alert('Try It'),
onLearnMore: () => alert('Learn More'),
};
const defaultContent = (imagePosition: 'inside' | 'below' = 'inside') => {
return {
content: {
title: 'Sample Title',
content: 'Sample content',
img: {
baseFolderPath: 'path/to/base/folder',
name: 'sample_image.png',
position: imagePosition,
},
},
};
};
const defaultPrimaryAction = {
label: 'Primary Action',
href: 'https://primary-action.com',
};
export function Default(props: ComponentProps<typeof AddDataPanel>) {
return <AddDataPanel {...props} />;
}
Default.args = {
...defaultContent(),
...defaultFunctions,
actions: {
primary: defaultPrimaryAction,
secondary: {
href: 'https://secondary-action.com',
},
link: {
href: 'https://link-action.com',
},
},
} as ComponentProps<typeof AddDataPanel>;
export function TwoActions(props: ComponentProps<typeof AddDataPanel>) {
return <AddDataPanel {...props} />;
}
TwoActions.args = {
...defaultContent(),
...defaultFunctions,
actions: {
primary: defaultPrimaryAction,
link: {
href: 'https://link-action.com',
},
},
} as ComponentProps<typeof AddDataPanel>;
export function ImageBelow(props: ComponentProps<typeof AddDataPanel>) {
return <AddDataPanel {...props} />;
}
ImageBelow.args = {
...defaultContent('below'),
...defaultFunctions,
actions: {
primary: defaultPrimaryAction,
secondary: {
href: 'https://secondary-action.com',
},
link: {
href: 'https://link-action.com',
},
},
} as ComponentProps<typeof AddDataPanel>;
export function WithoutImage(props: ComponentProps<typeof AddDataPanel>) {
return <AddDataPanel {...props} />;
}
WithoutImage.args = {
content: {
...defaultContent().content,
img: undefined,
},
...defaultFunctions,
actions: {
primary: defaultPrimaryAction,
secondary: {
href: 'https://secondary-action.com',
},
link: {
href: 'https://link-action.com',
},
},
} as ComponentProps<typeof AddDataPanel>;
export function CustomActionLabels(props: ComponentProps<typeof AddDataPanel>) {
return <AddDataPanel {...props} />;
}
CustomActionLabels.args = {
...defaultContent(),
...defaultFunctions,
actions: {
primary: defaultPrimaryAction,
secondary: {
label: 'Secondary Action',
href: 'https://secondary-action.com',
},
link: {
label: 'Link Action',
href: 'https://link-action.com',
},
},
} as ComponentProps<typeof AddDataPanel>;
export function NotDismissable(props: ComponentProps<typeof AddDataPanel>) {
return <AddDataPanel {...props} />;
}
NotDismissable.args = {
...defaultContent(),
...defaultFunctions,
onDissmiss: undefined,
actions: {
primary: defaultPrimaryAction,
secondary: {
href: 'https://secondary-action.com',
},
link: {
href: 'https://link-action.com',
},
},
} as ComponentProps<typeof AddDataPanel>;

View file

@ -0,0 +1,180 @@
/*
* 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.
*/
/* eslint-disable @elastic/eui/href-or-on-click */
import {
EuiButton,
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiImage,
EuiLink,
EuiPanel,
EuiSpacer,
EuiText,
EuiTitle,
useEuiTheme,
} from '@elastic/eui';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { useTheme } from '../../hooks/use_theme';
interface AddDataPanelContent {
title: string;
content: string;
img?: {
name: string;
baseFolderPath: string;
position: 'inside' | 'below';
};
}
interface AddDataPanelButton {
href: string | undefined;
label?: string;
}
type AddDataPanelButtonWithLabel = Required<AddDataPanelButton>;
export interface AddDataPanelProps {
content: AddDataPanelContent;
onDissmiss?: () => void;
onAddData: () => void;
onTryIt?: () => void;
onLearnMore: () => void;
actions: {
primary: AddDataPanelButtonWithLabel;
secondary?: AddDataPanelButton;
link: AddDataPanelButton;
};
'data-test-subj'?: string;
}
const tryItDefaultLabel = i18n.translate(
'xpack.observabilityShared.addDataPabel.tryItButtonLabel',
{
defaultMessage: 'Try it now in our demo cluster',
}
);
const learnMoreDefaultLabel = i18n.translate(
'xpack.observabilityShared.addDataPabel.learnMoreLinkLabel',
{
defaultMessage: 'Learn more',
}
);
export function AddDataPanel({
content,
actions,
onDissmiss,
onLearnMore,
onTryIt,
onAddData,
'data-test-subj': dataTestSubj,
}: AddDataPanelProps) {
const { euiTheme } = useEuiTheme();
const theme = useTheme();
const imgSrc = `${content.img?.baseFolderPath}/${theme.darkMode ? 'dark' : 'light'}/${
content.img?.name
}`;
return (
<>
<EuiPanel
color="subdued"
paddingSize="xl"
style={{ position: 'relative' }}
data-test-subj={dataTestSubj}
>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiTitle size="s">
<h3>{content.title}</h3>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText size="s">{content.content}</EuiText>
<EuiSpacer size="m" />
<EuiFlexGroup alignItems="center" gutterSize="m">
{actions.primary.href && (
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="AddDataPanelAddDataButton"
fill
href={actions.primary.href}
onClick={onAddData}
>
{actions.primary.label}
</EuiButton>
</EuiFlexItem>
)}
{actions.secondary?.href && (
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="AddDataPanelTryItNowButton"
iconType="launch"
iconSide="right"
href={actions.secondary.href}
onClick={onTryIt}
target="_blank"
>
{actions.secondary.label || tryItDefaultLabel}
</EuiButton>
</EuiFlexItem>
)}
{actions.link?.href && (
<EuiFlexItem grow={false}>
<EuiLink
href={actions.link.href}
onClick={onLearnMore}
target="_blank"
data-test-subj="AddDataPanelLearnMoreButton"
external
>
{actions.link.label || learnMoreDefaultLabel}
</EuiLink>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
{content.img && content.img?.position === 'inside' && (
<EuiFlexItem
style={{
maxHeight: `${euiTheme.base * 16}px`,
overflow: 'hidden',
borderRadius: `${euiTheme.border.radius.medium}`,
border: `${euiTheme.border.thin}`,
}}
>
<EuiImage src={imgSrc} alt={content.content} />
</EuiFlexItem>
)}
{onDissmiss && (
<EuiButtonIcon
style={{
position: 'absolute',
top: `${euiTheme.size.s}`,
right: `${euiTheme.size.s}`,
}}
data-test-subj="AddDataPanelDismissButton"
iconType="cross"
onClick={onDissmiss}
/>
)}
</EuiFlexGroup>
</EuiPanel>
{content.img && content.img?.position === 'below' && (
<>
<EuiSpacer size="l" />
<EuiImage src={imgSrc} alt={content.content} size="fullWidth" style={{ opacity: 0.4 }} />
</>
)}
</>
);
}

View file

@ -102,3 +102,5 @@ export {
} from './components/feature_feedback_button/feature_feedback_button';
export { BottomBarActions } from './components/bottom_bar_actions/bottom_bar_actions';
export { FieldValueSelection, FieldValueSuggestions } from './components';
export { AddDataPanel, type AddDataPanelProps } from './components/add_data_panel';

View file

@ -9,7 +9,8 @@
"public/**/*.json",
"server/**/*",
"typings/**/*",
"../../../../typings/**/*"
"../../../../typings/**/*",
".storybook/**/*"
],
"kbn_references": [
"@kbn/core",
@ -45,5 +46,5 @@
"@kbn/es-query",
"@kbn/serverless",
],
"exclude": ["target/**/*"]
"exclude": ["target/**/*", ".storybook/**/*.js"]
}

View file

@ -77,7 +77,7 @@ export function generateHostData({
}: {
from: string;
to: string;
hosts: Array<{ hostName: string; cpuValue: number }>;
hosts: Array<{ hostName: string; cpuValue?: number }>;
}) {
const range = timerange(from, to);

View file

@ -66,6 +66,12 @@ const HOSTS = [
cpuValue: 0.1,
},
];
const HOSTS_WITHOUT_DATA = [
{
hostName: 'host-7',
},
];
interface QueryParams {
name?: string;
alertMetric?: string;
@ -623,6 +629,67 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
});
describe('#Asset Type: host without metrics', () => {
before(async () => {
await synthEsClient.index(
generateHostData({
from: DATE_WITH_HOSTS_DATA_FROM,
to: DATE_WITH_HOSTS_DATA_TO,
hosts: HOSTS_WITHOUT_DATA,
})
);
await navigateToNodeDetails('host-1', 'host', {
name: 'host-1',
});
await pageObjects.header.waitUntilLoadingHasFinished();
});
after(() => synthEsClient.clean());
describe('Overview Tab', () => {
before(async () => {
await pageObjects.assetDetails.clickOverviewTab();
});
[
{ metric: 'cpuUsage' },
{ metric: 'normalizedLoad1m' },
{ metric: 'memoryUsage' },
{ metric: 'diskUsage' },
].forEach(({ metric }) => {
it(`${metric} tile should not be shown`, async () => {
await pageObjects.assetDetails.assetDetailsKPITileMissing(metric);
});
});
it('should show add metrics callout', async () => {
await pageObjects.assetDetails.addMetricsCalloutExists();
});
});
describe('Metrics Tab', () => {
before(async () => {
await pageObjects.assetDetails.clickMetricsTab();
});
it('should show add metrics callout', async () => {
await pageObjects.assetDetails.addMetricsCalloutExists();
});
});
describe('Processes Tab', () => {
before(async () => {
await pageObjects.assetDetails.clickProcessesTab();
});
it('should show add metrics callout', async () => {
await pageObjects.assetDetails.addMetricsCalloutExists();
});
});
});
describe('#Asset type: host with kubernetes section', () => {
before(async () => {
await synthEsClient.index(

View file

@ -25,6 +25,11 @@ export function AssetDetailsProvider({ getService }: FtrProviderContext) {
return testSubjects.existOrFail(`infraAssetDetailsHostChartsSection${metric}`);
},
// Add metrics callout
async addMetricsCalloutExists() {
return testSubjects.existOrFail('infraAddMetricsCallout');
},
// Overview
async clickOverviewTab() {
return testSubjects.click('infraAssetDetailsOverviewTab');
@ -34,6 +39,10 @@ export function AssetDetailsProvider({ getService }: FtrProviderContext) {
return testSubjects.find('infraAssetDetailsOverviewTab');
},
async assetDetailsKPITileMissing(type: string) {
return testSubjects.missingOrFail(`infraAssetDetailsKPI${type}`);
},
async getAssetDetailsKPITileValue(type: string) {
const element = await testSubjects.find(`infraAssetDetailsKPI${type}`);
const div = await element.findByClassName('echMetricText__value');