mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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||\r\n|Metrics||\r\n|Processes||\r\n\r\n**Container**\r\n|Tab||\r\n|-|-|\r\n|Overview||\r\n|Metrics||\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||\r\n|Metrics||\r\n|Processes||\r\n\r\n**Container**\r\n|Tab||\r\n|-|-|\r\n|Overview||\r\n|Metrics||\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||\r\n|Metrics||\r\n|Processes||\r\n\r\n**Container**\r\n|Tab||\r\n|-|-|\r\n|Overview||\r\n|Metrics||\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:
parent
d216933327
commit
e6392b297e
38 changed files with 1509 additions and 227 deletions
|
@ -43,6 +43,7 @@ const STORYBOOKS = [
|
|||
'lists',
|
||||
'observability',
|
||||
'observability_ai_assistant',
|
||||
'observability_shared',
|
||||
'presentation',
|
||||
'security_solution',
|
||||
'security_solution_packages',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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 };
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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(),
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>;
|
||||
};
|
||||
|
|
|
@ -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']);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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];
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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/**/*"]
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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',
|
||||
}
|
|
@ -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',
|
||||
}
|
|
@ -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';
|
|
@ -217,3 +217,5 @@ export {
|
|||
} from './locators';
|
||||
|
||||
export { COMMON_OBSERVABILITY_GROUPING } from './embeddable_grouping';
|
||||
|
||||
export { EntityType, EntityDataStreamType } from './entity';
|
||||
|
|
|
@ -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>;
|
|
@ -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 }} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue