[8.x] [APM][ECO] Removing Entities inventory from APM (#195116) (#195226)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[APM][ECO] Removing Entities inventory from APM
(#195116)](https://github.com/elastic/kibana/pull/195116)

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

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

<!--BACKPORT [{"author":{"name":"Cauê
Marcondes","email":"55978943+cauemarcondes@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-10-07T10:46:47Z","message":"[APM][ECO]
Removing Entities inventory from APM (#195116)\n\ncloses
https://github.com/elastic/kibana/issues/194114\r\n\r\nWe will no longer
show the entity inventory page on the APM UI. So, I'm\r\nremoving all
its
code.","sha":"c44b7de7a23fbd5b11b31cc8d39c5baffb6c8d6f","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-infra_services","v8.16.0"],"title":"[APM][ECO]
Removing Entities inventory from
APM","number":195116,"url":"https://github.com/elastic/kibana/pull/195116","mergeCommit":{"message":"[APM][ECO]
Removing Entities inventory from APM (#195116)\n\ncloses
https://github.com/elastic/kibana/issues/194114\r\n\r\nWe will no longer
show the entity inventory page on the APM UI. So, I'm\r\nremoving all
its
code.","sha":"c44b7de7a23fbd5b11b31cc8d39c5baffb6c8d6f"}},"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/195116","number":195116,"mergeCommit":{"message":"[APM][ECO]
Removing Entities inventory from APM (#195116)\n\ncloses
https://github.com/elastic/kibana/issues/194114\r\n\r\nWe will no longer
show the entity inventory page on the APM UI. So, I'm\r\nremoving all
its
code.","sha":"c44b7de7a23fbd5b11b31cc8d39c5baffb6c8d6f"}},{"branch":"8.x","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Cauê Marcondes <55978943+cauemarcondes@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2024-10-07 23:42:44 +11:00 committed by GitHub
parent 078f98c8b0
commit aac149aff3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 431 additions and 1684 deletions

View file

@ -30,8 +30,7 @@
"lens",
"maps",
"uiActions",
"logsDataAccess",
"entityManager"
"logsDataAccess"
],
"optionalPlugins": [
"actions",

View file

@ -1,36 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { AnalyticsServiceSetup } from '@kbn/core/public';
import { BehaviorSubject } from 'rxjs';
import { ServiceInventoryView } from '../context/entity_manager_context/entity_manager_context';
export const SERVICE_INVENTORY_STORAGE_KEY = 'apm.service.inventory.view';
export let serviceInventoryViewType$: BehaviorSubject<{ serviceInventoryViewType: string }>;
export function registerServiceInventoryViewTypeContext(analytics: AnalyticsServiceSetup) {
const serviceInventoryLocalStorageValue = window.localStorage.getItem(
SERVICE_INVENTORY_STORAGE_KEY
);
serviceInventoryViewType$ = new BehaviorSubject({
serviceInventoryViewType:
serviceInventoryLocalStorageValue === null
? ServiceInventoryView.classic
: JSON.parse(serviceInventoryLocalStorageValue),
});
analytics.registerContextProvider({
name: 'serviceInventoryViewType',
context$: serviceInventoryViewType$,
schema: {
serviceInventoryViewType: {
type: 'keyword',
_meta: { description: 'The APM service inventory view type' },
},
},
});
}

View file

@ -11,8 +11,6 @@ import { EntityLink } from '.';
import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context';
import type { ServiceEntitySummary } from '../../../../context/apm_service/use_service_entity_summary_fetcher';
import * as useServiceEntitySummary from '../../../../context/apm_service/use_service_entity_summary_fetcher';
import type { EntityManagerEnablementContextValue } from '../../../../context/entity_manager_context/entity_manager_context';
import * as useEntityManagerEnablementContext from '../../../../context/entity_manager_context/use_entity_manager_enablement_context';
import * as useFetcher from '../../../../hooks/use_fetcher';
import { FETCH_STATUS } from '../../../../hooks/use_fetcher';
import { fromQuery } from '../../../shared/links/url_helpers';
@ -20,6 +18,7 @@ import { APIReturnType } from '../../../../services/rest/create_call_apm_api';
import { Redirect } from 'react-router-dom';
import { ApmPluginContextValue } from '../../../../context/apm_plugin/apm_plugin_context';
import { ApmThemeProvider } from '../../../routing/app_root';
import * as useEntityCentricExperienceSetting from '../../../../hooks/use_entity_centric_experience_setting';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'), // Keep other functionality intact
@ -29,12 +28,12 @@ jest.mock('react-router-dom', () => ({
export type HasApmData = APIReturnType<'GET /internal/apm/has_data'>;
const renderEntityLink = ({
entityManagerMockReturnValue,
isEntityCentricExperienceEnabled = true,
serviceEntitySummaryMockReturnValue,
hasApmDataFetcherMockReturnValue,
query = {},
}: {
entityManagerMockReturnValue: Partial<EntityManagerEnablementContextValue>;
isEntityCentricExperienceEnabled?: boolean;
serviceEntitySummaryMockReturnValue: ReturnType<
typeof useServiceEntitySummary.useServiceEntitySummaryFetcher
>;
@ -45,10 +44,8 @@ const renderEntityLink = ({
};
}) => {
jest
.spyOn(useEntityManagerEnablementContext, 'useEntityManagerEnablementContext')
.mockReturnValue(
entityManagerMockReturnValue as unknown as EntityManagerEnablementContextValue
);
.spyOn(useEntityCentricExperienceSetting, 'useEntityCentricExperienceSetting')
.mockReturnValue({ isEntityCentricExperienceEnabled });
jest
.spyOn(useServiceEntitySummary, 'useServiceEntitySummaryFetcher')
@ -101,30 +98,9 @@ describe('Entity link', () => {
jest.clearAllMocks();
});
it('renders a loading spinner while fetching data', () => {
renderEntityLink({
entityManagerMockReturnValue: {
isEntityCentricExperienceViewEnabled: undefined,
isEnablementPending: true,
},
serviceEntitySummaryMockReturnValue: {
serviceEntitySummary: undefined,
serviceEntitySummaryStatus: FETCH_STATUS.LOADING,
},
hasApmDataFetcherMockReturnValue: {
data: undefined,
status: FETCH_STATUS.LOADING,
},
});
expect(screen.queryByTestId('apmEntityLinkLoadingSpinner')).toBeInTheDocument();
});
it('renders EEM callout when EEM is enabled but service is not found on EEM indices', () => {
renderEntityLink({
entityManagerMockReturnValue: {
isEntityCentricExperienceViewEnabled: true,
isEnablementPending: false,
},
isEntityCentricExperienceEnabled: true,
serviceEntitySummaryMockReturnValue: {
serviceEntitySummary: undefined,
serviceEntitySummaryStatus: FETCH_STATUS.SUCCESS,
@ -141,10 +117,7 @@ describe('Entity link', () => {
it('renders Service Overview page when EEM is disabled', () => {
renderEntityLink({
entityManagerMockReturnValue: {
isEntityCentricExperienceViewEnabled: false,
isEnablementPending: false,
},
isEntityCentricExperienceEnabled: false,
serviceEntitySummaryMockReturnValue: {
serviceEntitySummary: undefined,
serviceEntitySummaryStatus: FETCH_STATUS.SUCCESS,
@ -171,10 +144,7 @@ describe('Entity link', () => {
it('renders Service Overview page when EEM is enabled but Service is not found on EEM but it has raw APM data', () => {
renderEntityLink({
entityManagerMockReturnValue: {
isEntityCentricExperienceViewEnabled: true,
isEnablementPending: false,
},
isEntityCentricExperienceEnabled: true,
serviceEntitySummaryMockReturnValue: {
serviceEntitySummary: undefined,
serviceEntitySummaryStatus: FETCH_STATUS.SUCCESS,
@ -201,10 +171,7 @@ describe('Entity link', () => {
it('renders Service Overview page when EEM is enabled and Service is found on EEM', () => {
renderEntityLink({
entityManagerMockReturnValue: {
isEntityCentricExperienceViewEnabled: true,
isEnablementPending: false,
},
isEntityCentricExperienceEnabled: true,
serviceEntitySummaryMockReturnValue: {
serviceEntitySummary: { dataStreamTypes: ['metrics'] } as unknown as ServiceEntitySummary,
serviceEntitySummaryStatus: FETCH_STATUS.SUCCESS,
@ -231,10 +198,7 @@ describe('Entity link', () => {
it('renders Service Overview page setting time range from data plugin', () => {
renderEntityLink({
entityManagerMockReturnValue: {
isEntityCentricExperienceViewEnabled: true,
isEnablementPending: false,
},
isEntityCentricExperienceEnabled: true,
serviceEntitySummaryMockReturnValue: {
serviceEntitySummary: { dataStreamTypes: ['metrics'] } as unknown as ServiceEntitySummary,
serviceEntitySummaryStatus: FETCH_STATUS.SUCCESS,

View file

@ -14,9 +14,9 @@ import React from 'react';
import { Redirect } from 'react-router-dom';
import { ENVIRONMENT_ALL_VALUE } from '../../../../../common/environment_filter_values';
import { useServiceEntitySummaryFetcher } from '../../../../context/apm_service/use_service_entity_summary_fetcher';
import { useEntityManagerEnablementContext } from '../../../../context/entity_manager_context/use_entity_manager_enablement_context';
import { useApmParams } from '../../../../hooks/use_apm_params';
import { useApmRouter } from '../../../../hooks/use_apm_router';
import { useEntityCentricExperienceSetting } from '../../../../hooks/use_entity_centric_experience_setting';
import { FETCH_STATUS, isPending, useFetcher } from '../../../../hooks/use_fetcher';
import { useTheme } from '../../../../hooks/use_theme';
import { ApmPluginStartDeps } from '../../../../plugin';
@ -36,8 +36,7 @@ export function EntityLink() {
path: { serviceName },
query: { rangeFrom = timeRange.from, rangeTo = timeRange.to },
} = useApmParams('/link-to/entity/{serviceName}');
const { isEntityCentricExperienceViewEnabled, isEnablementPending } =
useEntityManagerEnablementContext();
const { isEntityCentricExperienceEnabled } = useEntityCentricExperienceSetting();
const { serviceEntitySummary, serviceEntitySummaryStatus } = useServiceEntitySummaryFetcher({
serviceName,
@ -48,17 +47,13 @@ export function EntityLink() {
return callApmApi('GET /internal/apm/has_data');
}, []);
if (
isEnablementPending ||
serviceEntitySummaryStatus === FETCH_STATUS.LOADING ||
isPending(hasApmDataStatus)
) {
if (serviceEntitySummaryStatus === FETCH_STATUS.LOADING || isPending(hasApmDataStatus)) {
return <EuiLoadingSpinner data-test-subj="apmEntityLinkLoadingSpinner" />;
}
if (
// When EEM is enabled and the service is not found on the EEM indices and there's no APM data, display a callout guiding on the limitations of EEM
isEntityCentricExperienceViewEnabled === true &&
isEntityCentricExperienceEnabled === true &&
(serviceEntitySummary?.dataStreamTypes === undefined ||
serviceEntitySummary.dataStreamTypes.length === 0) &&
hasApmData?.hasData !== true

View file

@ -1,117 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiFlexGroup,
EuiFlexItem,
EuiImage,
EuiPanel,
EuiSpacer,
EuiText,
EuiTitle,
EuiButtonEmpty,
useEuiTheme,
EuiButtonIcon,
} from '@elastic/eui';
import { apmLight } from '@kbn/shared-svg';
import { FormattedMessage } from '@kbn/i18n-react';
import { useKibana } from '../../../../context/kibana_context/use_kibana';
import { ApmPluginStartDeps, ApmServices } from '../../../../plugin';
import { AddApmData } from '../../../shared/add_data_buttons/buttons';
interface Props {
onClose: () => void;
}
export function AddAPMCallOut({ onClose }: Props) {
const { euiTheme } = useEuiTheme();
const { services } = useKibana<ApmPluginStartDeps & ApmServices>();
function handleClick() {
services.telemetry.reportEntityInventoryAddData({
view: 'add_apm_cta',
});
}
return (
<EuiPanel color="subdued" hasShadow={false}>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem>
<EuiFlexGroup alignItems="center" gutterSize="s" justifyContent="flexStart">
<EuiFlexItem grow={0}>
<EuiImage
css={{
background: euiTheme.colors.emptyShade,
}}
width="160"
height="100"
size="m"
src={apmLight}
alt="apm-logo"
/>
</EuiFlexItem>
<EuiFlexItem grow={4}>
<EuiTitle size="xs">
<h1>
<FormattedMessage
id="xpack.apm.addAPMCallOut.title"
defaultMessage="Detect and resolve issues faster with deep visibility into your application"
/>
</h1>
</EuiTitle>
<EuiSpacer size="m" />
<EuiText size="s">
<p>
<FormattedMessage
id="xpack.apm.addAPMCallOut.description"
defaultMessage="Understanding your application performance, relationships and dependencies by
instrumenting with APM."
/>
</p>
</EuiText>
<EuiSpacer size="s" />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButtonIcon
data-test-subj="apmAddAPMCallOutButton"
iconType="cross"
onClick={onClose}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup alignItems="center" gutterSize="s" justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<div>
<AddApmData data-test-subj="apmAddDataLogOnlyCallout" onClick={handleClick} />
</div>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
data-test-subj="apmAddApmCallOutLearnMoreButton"
iconType="popout"
iconSide="right"
target="_blank"
href="https://www.elastic.co/observability/application-performance-monitoring"
>
{i18n.translate('xpack.apm.addAPMCallOut.linkToElasticcoButtonEmptyLabel', {
defaultMessage: 'Learn more',
})}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
}

View file

@ -24,14 +24,14 @@ import { Sort } from './sort';
import { RefreshServiceGroupsSubscriber } from '../refresh_service_groups_subscriber';
import { ServiceGroupSaveButton } from '../service_group_save';
import { BetaBadge } from '../../../shared/beta_badge';
import { useEntityManagerEnablementContext } from '../../../../context/entity_manager_context/use_entity_manager_enablement_context';
import { useEntityCentricExperienceSetting } from '../../../../hooks/use_entity_centric_experience_setting';
export type ServiceGroupsSortType = 'recently_added' | 'alphabetical';
const GET_STARTED_URL = 'https://www.elastic.co/guide/en/apm/get-started/current/index.html';
export function ServiceGroupsList() {
const { isEntityCentricExperienceViewEnabled } = useEntityManagerEnablementContext();
const { isEntityCentricExperienceEnabled } = useEntityCentricExperienceSetting();
const [filter, setFilter] = useState('');
@ -137,7 +137,7 @@ export function ServiceGroupsList() {
{i18n.translate('xpack.apm.serviceGroups.listDescription', {
defaultMessage: 'Displayed service counts reflect the last 24 hours.',
})}
{isEntityCentricExperienceViewEnabled && (
{isEntityCentricExperienceEnabled && (
<FormattedMessage
id="xpack.apm.serviceGroups.onlyApm"
defaultMessage="Only showing services {link}"

View file

@ -1,322 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { usePerformanceContext } from '@kbn/ebt-tools';
import { i18n } from '@kbn/i18n';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { ApmDocumentType } from '../../../../../common/document_type';
import {
ServiceInventoryFieldName,
ServiceListItem,
} from '../../../../../common/service_inventory';
import { useAnomalyDetectionJobsContext } from '../../../../context/anomaly_detection_jobs/use_anomaly_detection_jobs_context';
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
import { useApmParams } from '../../../../hooks/use_apm_params';
import { useStateDebounced } from '../../../../hooks/use_debounce';
import { FETCH_STATUS, isFailure, isPending } from '../../../../hooks/use_fetcher';
import { useLocalStorage } from '../../../../hooks/use_local_storage';
import { usePreferredDataSourceAndBucketSize } from '../../../../hooks/use_preferred_data_source_and_bucket_size';
import { useProgressiveFetcher } from '../../../../hooks/use_progressive_fetcher';
import { useTimeRange } from '../../../../hooks/use_time_range';
import { APIReturnType } from '../../../../services/rest/create_call_apm_api';
import { SortFunction } from '../../../shared/managed_table';
import { MLCallout, shouldDisplayMlCallout } from '../../../shared/ml_callout';
import { SearchBar } from '../../../shared/search_bar/search_bar';
import { isTimeComparison } from '../../../shared/time_comparison/get_comparison_options';
import { ApmServicesTable } from './service_list/apm_services_table';
import { orderServiceItems } from './service_list/order_service_items';
type MainStatisticsApiResponse = APIReturnType<'GET /internal/apm/services'>;
const INITIAL_PAGE_SIZE = 25;
const INITIAL_DATA: MainStatisticsApiResponse & { requestId: string } = {
requestId: '',
items: [],
serviceOverflowCount: 0,
maxCountExceeded: false,
};
function useServicesMainStatisticsFetcher(searchQuery: string | undefined) {
const {
query: {
rangeFrom,
rangeTo,
environment,
kuery,
serviceGroup,
page = 0,
pageSize = INITIAL_PAGE_SIZE,
sortDirection,
sortField,
},
} = useApmParams('/services');
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
const preferred = usePreferredDataSourceAndBucketSize({
start,
end,
kuery,
type: ApmDocumentType.ServiceTransactionMetric,
numBuckets: 20,
});
const shouldUseDurationSummary = !!preferred?.source?.hasDurationSummaryField;
const { data = INITIAL_DATA, status } = useProgressiveFetcher(
(callApmApi) => {
if (preferred) {
return callApmApi('GET /internal/apm/services', {
params: {
query: {
environment,
kuery,
start,
end,
serviceGroup,
useDurationSummary: shouldUseDurationSummary,
documentType: preferred.source.documentType,
rollupInterval: preferred.source.rollupInterval,
searchQuery,
},
},
}).then((mainStatisticsData) => {
return {
requestId: uuidv4(),
...mainStatisticsData,
};
});
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[
environment,
kuery,
start,
end,
serviceGroup,
preferred,
searchQuery,
// not used, but needed to update the requestId to call the details statistics API when table options are updated
page,
pageSize,
sortField,
sortDirection,
]
);
return { mainStatisticsData: data, mainStatisticsStatus: status };
}
function useServicesDetailedStatisticsFetcher({
mainStatisticsFetch,
renderedItems,
}: {
mainStatisticsFetch: ReturnType<typeof useServicesMainStatisticsFetcher>;
renderedItems: ServiceListItem[];
}) {
const {
query: { rangeFrom, rangeTo, environment, kuery, offset, comparisonEnabled },
} = useApmParams('/services');
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
const dataSourceOptions = usePreferredDataSourceAndBucketSize({
start,
end,
kuery,
type: ApmDocumentType.ServiceTransactionMetric,
numBuckets: 20,
});
const { mainStatisticsData, mainStatisticsStatus } = mainStatisticsFetch;
const comparisonFetch = useProgressiveFetcher(
(callApmApi) => {
const serviceNames = renderedItems.map(({ serviceName }) => serviceName);
if (
start &&
end &&
serviceNames.length > 0 &&
mainStatisticsStatus === FETCH_STATUS.SUCCESS &&
dataSourceOptions
) {
return callApmApi('POST /internal/apm/services/detailed_statistics', {
params: {
query: {
environment,
kuery,
start,
end,
offset: comparisonEnabled && isTimeComparison(offset) ? offset : undefined,
documentType: dataSourceOptions.source.documentType,
rollupInterval: dataSourceOptions.source.rollupInterval,
bucketSizeInSeconds: dataSourceOptions.bucketSizeInSeconds,
},
body: {
// Service name is sorted to guarantee the same order every time this API is called so the result can be cached.
serviceNames: JSON.stringify(serviceNames.sort()),
},
},
});
}
},
// only fetches detailed statistics when requestId is invalidated by main statistics api call or offset is changed
// eslint-disable-next-line react-hooks/exhaustive-deps
[mainStatisticsData.requestId, renderedItems, offset, comparisonEnabled],
{ preservePreviousData: false }
);
return { comparisonFetch };
}
export function ApmServiceInventory() {
const [debouncedSearchQuery, setDebouncedSearchQuery] = useStateDebounced('');
const { onPageReady } = usePerformanceContext();
const [renderedItems, setRenderedItems] = useState<ServiceListItem[]>([]);
const mainStatisticsFetch = useServicesMainStatisticsFetcher(debouncedSearchQuery);
const { mainStatisticsData, mainStatisticsStatus } = mainStatisticsFetch;
const displayHealthStatus = mainStatisticsData.items.some((item) => 'healthStatus' in item);
const serviceOverflowCount = mainStatisticsData?.serviceOverflowCount ?? 0;
const displayAlerts = mainStatisticsData.items.some(
(item) => ServiceInventoryFieldName.AlertsCount in item
);
const tiebreakerField = ServiceInventoryFieldName.Throughput;
const initialSortField = displayHealthStatus
? ServiceInventoryFieldName.HealthStatus
: tiebreakerField;
const initialSortDirection = 'desc';
const { comparisonFetch } = useServicesDetailedStatisticsFetcher({
mainStatisticsFetch,
renderedItems,
});
const { anomalyDetectionSetupState } = useAnomalyDetectionJobsContext();
const [userHasDismissedCallout, setUserHasDismissedCallout] = useLocalStorage(
`apm.userHasDismissedServiceInventoryMlCallout.${anomalyDetectionSetupState}`,
false
);
const displayMlCallout =
!userHasDismissedCallout && shouldDisplayMlCallout(anomalyDetectionSetupState);
const noItemsMessage = useMemo(() => {
return (
<EuiEmptyPrompt
title={
<div>
{i18n.translate('xpack.apm.servicesTable.notFoundLabel', {
defaultMessage: 'No services found',
})}
</div>
}
titleSize="s"
/>
);
}, []);
const mlCallout = (
<EuiFlexItem>
<MLCallout
isOnSettingsPage={false}
anomalyDetectionSetupState={anomalyDetectionSetupState}
onDismiss={() => setUserHasDismissedCallout(true)}
/>
</EuiFlexItem>
);
const sortFn: SortFunction<ServiceListItem> = useCallback(
(itemsToSort, sortField, sortDirection) => {
return orderServiceItems({
items: itemsToSort,
primarySortField: sortField,
sortDirection,
tiebreakerField,
});
},
[tiebreakerField]
);
// TODO verify this with AI team
const setScreenContext = useApmPluginContext().observabilityAIAssistant?.service.setScreenContext;
useEffect(() => {
if (!setScreenContext) {
return;
}
if (isFailure(mainStatisticsStatus)) {
return setScreenContext({
screenDescription: 'The services have failed to load',
});
}
if (isPending(mainStatisticsStatus)) {
return setScreenContext({
screenDescription: 'The services are still loading',
});
}
return setScreenContext({
data: [
{
name: 'services',
description: 'The list of services that the user is looking at',
value: mainStatisticsData.items,
},
],
});
}, [mainStatisticsStatus, mainStatisticsData.items, setScreenContext]);
useEffect(() => {
if (
mainStatisticsStatus === FETCH_STATUS.SUCCESS &&
comparisonFetch.status === FETCH_STATUS.SUCCESS
) {
onPageReady();
}
}, [mainStatisticsStatus, comparisonFetch.status, onPageReady]);
return (
<>
<SearchBar showTimeComparison />
<EuiFlexGroup direction="column" gutterSize="m">
{displayMlCallout && mlCallout}
<EuiFlexItem>
<ApmServicesTable
status={mainStatisticsStatus}
items={mainStatisticsData.items}
comparisonDataLoading={comparisonFetch.status === FETCH_STATUS.LOADING}
displayHealthStatus={displayHealthStatus}
displayAlerts={displayAlerts}
initialSortField={initialSortField}
initialSortDirection={initialSortDirection}
sortFn={sortFn}
comparisonData={comparisonFetch?.data}
noItemsMessage={noItemsMessage}
initialPageSize={INITIAL_PAGE_SIZE}
serviceOverflowCount={serviceOverflowCount}
onChangeSearchQuery={setDebouncedSearchQuery}
maxCountExceeded={mainStatisticsData?.maxCountExceeded ?? false}
onChangeRenderedItems={setRenderedItems}
/>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
}

View file

@ -4,9 +4,316 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { ApmServiceInventory } from './apm_signal_inventory';
import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { usePerformanceContext } from '@kbn/ebt-tools';
import { i18n } from '@kbn/i18n';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { ApmDocumentType } from '../../../../common/document_type';
import { ServiceInventoryFieldName, ServiceListItem } from '../../../../common/service_inventory';
import { useAnomalyDetectionJobsContext } from '../../../context/anomaly_detection_jobs/use_anomaly_detection_jobs_context';
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
import { useApmParams } from '../../../hooks/use_apm_params';
import { useStateDebounced } from '../../../hooks/use_debounce';
import { FETCH_STATUS, isFailure, isPending } from '../../../hooks/use_fetcher';
import { useLocalStorage } from '../../../hooks/use_local_storage';
import { usePreferredDataSourceAndBucketSize } from '../../../hooks/use_preferred_data_source_and_bucket_size';
import { useProgressiveFetcher } from '../../../hooks/use_progressive_fetcher';
import { useTimeRange } from '../../../hooks/use_time_range';
import { APIReturnType } from '../../../services/rest/create_call_apm_api';
import { SortFunction } from '../../shared/managed_table';
import { MLCallout, shouldDisplayMlCallout } from '../../shared/ml_callout';
import { SearchBar } from '../../shared/search_bar/search_bar';
import { isTimeComparison } from '../../shared/time_comparison/get_comparison_options';
import { ApmServicesTable } from './service_list/apm_services_table';
import { orderServiceItems } from './service_list/order_service_items';
type MainStatisticsApiResponse = APIReturnType<'GET /internal/apm/services'>;
const INITIAL_PAGE_SIZE = 25;
const INITIAL_DATA: MainStatisticsApiResponse & { requestId: string } = {
requestId: '',
items: [],
serviceOverflowCount: 0,
maxCountExceeded: false,
};
function useServicesMainStatisticsFetcher(searchQuery: string | undefined) {
const {
query: {
rangeFrom,
rangeTo,
environment,
kuery,
serviceGroup,
page = 0,
pageSize = INITIAL_PAGE_SIZE,
sortDirection,
sortField,
},
} = useApmParams('/services');
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
const preferred = usePreferredDataSourceAndBucketSize({
start,
end,
kuery,
type: ApmDocumentType.ServiceTransactionMetric,
numBuckets: 20,
});
const shouldUseDurationSummary = !!preferred?.source?.hasDurationSummaryField;
const { data = INITIAL_DATA, status } = useProgressiveFetcher(
(callApmApi) => {
if (preferred) {
return callApmApi('GET /internal/apm/services', {
params: {
query: {
environment,
kuery,
start,
end,
serviceGroup,
useDurationSummary: shouldUseDurationSummary,
documentType: preferred.source.documentType,
rollupInterval: preferred.source.rollupInterval,
searchQuery,
},
},
}).then((mainStatisticsData) => {
return {
requestId: uuidv4(),
...mainStatisticsData,
};
});
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[
environment,
kuery,
start,
end,
serviceGroup,
preferred,
searchQuery,
// not used, but needed to update the requestId to call the details statistics API when table options are updated
page,
pageSize,
sortField,
sortDirection,
]
);
return { mainStatisticsData: data, mainStatisticsStatus: status };
}
function useServicesDetailedStatisticsFetcher({
mainStatisticsFetch,
renderedItems,
}: {
mainStatisticsFetch: ReturnType<typeof useServicesMainStatisticsFetcher>;
renderedItems: ServiceListItem[];
}) {
const {
query: { rangeFrom, rangeTo, environment, kuery, offset, comparisonEnabled },
} = useApmParams('/services');
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
const dataSourceOptions = usePreferredDataSourceAndBucketSize({
start,
end,
kuery,
type: ApmDocumentType.ServiceTransactionMetric,
numBuckets: 20,
});
const { mainStatisticsData, mainStatisticsStatus } = mainStatisticsFetch;
const comparisonFetch = useProgressiveFetcher(
(callApmApi) => {
const serviceNames = renderedItems.map(({ serviceName }) => serviceName);
if (
start &&
end &&
serviceNames.length > 0 &&
mainStatisticsStatus === FETCH_STATUS.SUCCESS &&
dataSourceOptions
) {
return callApmApi('POST /internal/apm/services/detailed_statistics', {
params: {
query: {
environment,
kuery,
start,
end,
offset: comparisonEnabled && isTimeComparison(offset) ? offset : undefined,
documentType: dataSourceOptions.source.documentType,
rollupInterval: dataSourceOptions.source.rollupInterval,
bucketSizeInSeconds: dataSourceOptions.bucketSizeInSeconds,
},
body: {
// Service name is sorted to guarantee the same order every time this API is called so the result can be cached.
serviceNames: JSON.stringify(serviceNames.sort()),
},
},
});
}
},
// only fetches detailed statistics when requestId is invalidated by main statistics api call or offset is changed
// eslint-disable-next-line react-hooks/exhaustive-deps
[mainStatisticsData.requestId, renderedItems, offset, comparisonEnabled],
{ preservePreviousData: false }
);
return { comparisonFetch };
}
export function ServiceInventory() {
return <ApmServiceInventory />;
const [debouncedSearchQuery, setDebouncedSearchQuery] = useStateDebounced('');
const { onPageReady } = usePerformanceContext();
const [renderedItems, setRenderedItems] = useState<ServiceListItem[]>([]);
const mainStatisticsFetch = useServicesMainStatisticsFetcher(debouncedSearchQuery);
const { mainStatisticsData, mainStatisticsStatus } = mainStatisticsFetch;
const displayHealthStatus = mainStatisticsData.items.some((item) => 'healthStatus' in item);
const serviceOverflowCount = mainStatisticsData?.serviceOverflowCount ?? 0;
const displayAlerts = mainStatisticsData.items.some(
(item) => ServiceInventoryFieldName.AlertsCount in item
);
const tiebreakerField = ServiceInventoryFieldName.Throughput;
const initialSortField = displayHealthStatus
? ServiceInventoryFieldName.HealthStatus
: tiebreakerField;
const initialSortDirection = 'desc';
const { comparisonFetch } = useServicesDetailedStatisticsFetcher({
mainStatisticsFetch,
renderedItems,
});
const { anomalyDetectionSetupState } = useAnomalyDetectionJobsContext();
const [userHasDismissedCallout, setUserHasDismissedCallout] = useLocalStorage(
`apm.userHasDismissedServiceInventoryMlCallout.${anomalyDetectionSetupState}`,
false
);
const displayMlCallout =
!userHasDismissedCallout && shouldDisplayMlCallout(anomalyDetectionSetupState);
const noItemsMessage = useMemo(() => {
return (
<EuiEmptyPrompt
title={
<div>
{i18n.translate('xpack.apm.servicesTable.notFoundLabel', {
defaultMessage: 'No services found',
})}
</div>
}
titleSize="s"
/>
);
}, []);
const mlCallout = (
<EuiFlexItem>
<MLCallout
isOnSettingsPage={false}
anomalyDetectionSetupState={anomalyDetectionSetupState}
onDismiss={() => setUserHasDismissedCallout(true)}
/>
</EuiFlexItem>
);
const sortFn: SortFunction<ServiceListItem> = useCallback(
(itemsToSort, sortField, sortDirection) => {
return orderServiceItems({
items: itemsToSort,
primarySortField: sortField,
sortDirection,
tiebreakerField,
});
},
[tiebreakerField]
);
// TODO verify this with AI team
const setScreenContext = useApmPluginContext().observabilityAIAssistant?.service.setScreenContext;
useEffect(() => {
if (!setScreenContext) {
return;
}
if (isFailure(mainStatisticsStatus)) {
return setScreenContext({
screenDescription: 'The services have failed to load',
});
}
if (isPending(mainStatisticsStatus)) {
return setScreenContext({
screenDescription: 'The services are still loading',
});
}
return setScreenContext({
data: [
{
name: 'services',
description: 'The list of services that the user is looking at',
value: mainStatisticsData.items,
},
],
});
}, [mainStatisticsStatus, mainStatisticsData.items, setScreenContext]);
useEffect(() => {
if (
mainStatisticsStatus === FETCH_STATUS.SUCCESS &&
comparisonFetch.status === FETCH_STATUS.SUCCESS
) {
onPageReady();
}
}, [mainStatisticsStatus, comparisonFetch.status, onPageReady]);
return (
<>
<SearchBar showTimeComparison />
<EuiFlexGroup direction="column" gutterSize="m">
{displayMlCallout && mlCallout}
<EuiFlexItem>
<ApmServicesTable
status={mainStatisticsStatus}
items={mainStatisticsData.items}
comparisonDataLoading={comparisonFetch.status === FETCH_STATUS.LOADING}
displayHealthStatus={displayHealthStatus}
displayAlerts={displayAlerts}
initialSortField={initialSortField}
initialSortDirection={initialSortDirection}
sortFn={sortFn}
comparisonData={comparisonFetch?.data}
noItemsMessage={noItemsMessage}
initialPageSize={INITIAL_PAGE_SIZE}
serviceOverflowCount={serviceOverflowCount}
onChangeSearchQuery={setDebouncedSearchQuery}
maxCountExceeded={mainStatisticsData?.maxCountExceeded ?? false}
onChangeRenderedItems={setRenderedItems}
/>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
}

View file

@ -1,218 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useEffect } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { APIReturnType } from '../../../../services/rest/create_call_apm_api';
import { useApmParams } from '../../../../hooks/use_apm_params';
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
import { useTimeRange } from '../../../../hooks/use_time_range';
import { EmptyMessage } from '../../../shared/empty_message';
import { SearchBar } from '../../../shared/search_bar/search_bar';
import {
getItemsFilteredBySearchQuery,
TableSearchBar,
} from '../../../shared/table_search_bar/table_search_bar';
import {
MultiSignalServicesTable,
ServiceInventoryFieldName,
} from './table/multi_signal_services_table';
import { ServiceListItem } from '../../../../../common/service_inventory';
import { NoEntitiesEmptyState } from './table/no_entities_empty_state';
import { Welcome } from '../../../shared/entity_enablement/welcome_modal';
import { useKibana } from '../../../../context/kibana_context/use_kibana';
import { ApmPluginStartDeps, ApmServices } from '../../../../plugin';
import { useEntityManagerEnablementContext } from '../../../../context/entity_manager_context/use_entity_manager_enablement_context';
type MainStatisticsApiResponse = APIReturnType<'GET /internal/apm/entities/services'>;
const INITIAL_PAGE_SIZE = 25;
const INITIAL_SORT_DIRECTION = 'desc';
type MainStatisticsApiResponseWithRequestId = MainStatisticsApiResponse & { requestId: string };
const INITIAL_DATA: MainStatisticsApiResponseWithRequestId = {
services: [],
requestId: '',
};
function useServicesEntitiesMainStatisticsFetcher() {
const {
query: {
rangeFrom,
rangeTo,
environment,
kuery,
page = 0,
pageSize = INITIAL_PAGE_SIZE,
sortDirection,
sortField,
},
} = useApmParams('/services');
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
const { data = INITIAL_DATA, status } = useFetcher(
(callApmApi) => {
return callApmApi('GET /internal/apm/entities/services', {
params: {
query: {
environment,
kuery,
start,
end,
},
},
}).then((mainStatisticsData) => {
return {
requestId: uuidv4(),
...mainStatisticsData,
};
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[environment, kuery, start, end, page, pageSize, sortField, sortDirection]
);
return { mainStatisticsData: data, mainStatisticsStatus: status };
}
function useServicesEntitiesDetailedStatisticsFetcher({
mainStatisticsData,
mainStatisticsStatus,
services,
}: {
mainStatisticsData: MainStatisticsApiResponseWithRequestId;
mainStatisticsStatus: FETCH_STATUS;
services: ServiceListItem[];
}) {
const {
query: { rangeFrom, rangeTo, environment, kuery },
} = useApmParams('/services');
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
const timeseriesDataFetch = useFetcher(
(callApmApi) => {
const serviceNames = services.map(({ serviceName }) => serviceName);
if (
start &&
end &&
serviceNames.length > 0 &&
mainStatisticsStatus === FETCH_STATUS.SUCCESS
) {
return callApmApi('POST /internal/apm/entities/services/detailed_statistics', {
params: {
query: {
environment,
kuery,
start,
end,
},
body: {
// Service name is sorted to guarantee the same order every time this API is called so the result can be cached.
serviceNames: JSON.stringify(serviceNames.sort()),
},
},
});
}
},
// only fetches detailed statistics when requestId is invalidated by main statistics api call or offset is changed
// eslint-disable-next-line react-hooks/exhaustive-deps
[mainStatisticsData.requestId, services],
{ preservePreviousData: false }
);
return { timeseriesDataFetch };
}
export function MultiSignalInventory() {
const [searchQuery, setSearchQuery] = React.useState('');
const { services } = useKibana<ApmPluginStartDeps & ApmServices>();
const { mainStatisticsData, mainStatisticsStatus } = useServicesEntitiesMainStatisticsFetcher();
const { tourState, updateTourState } = useEntityManagerEnablementContext();
const initialSortField = ServiceInventoryFieldName.Throughput;
const filteredData = getItemsFilteredBySearchQuery({
items: mainStatisticsData.services,
searchQuery,
fieldsToSearch: [ServiceInventoryFieldName.ServiceName],
});
const { timeseriesDataFetch } = useServicesEntitiesDetailedStatisticsFetcher({
mainStatisticsData,
mainStatisticsStatus,
services: mainStatisticsData.services,
});
const { data, status } = useFetcher((callApmApi) => {
return callApmApi('GET /internal/apm/has_entities');
}, []);
useEffect(() => {
if (data?.hasData) {
services.telemetry.reportEntityInventoryPageState({ state: 'available' });
}
}, [services.telemetry, data?.hasData]);
function handleModalClose() {
updateTourState({ isModalVisible: false, isTourActive: true });
}
return (
<>
{!data?.hasData && status === FETCH_STATUS.SUCCESS ? (
<NoEntitiesEmptyState />
) : (
<>
<EuiFlexGroup gutterSize="m">
<EuiFlexItem grow>
<TableSearchBar
placeholder={i18n.translate('xpack.apm.servicesTable.filterServicesPlaceholder', {
defaultMessage: 'Search services by name',
})}
searchQuery={searchQuery}
onChangeSearchQuery={setSearchQuery}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<SearchBar showQueryInput={false} />
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem>
<MultiSignalServicesTable
status={mainStatisticsStatus}
data={filteredData}
initialSortField={initialSortField}
initialPageSize={INITIAL_PAGE_SIZE}
initialSortDirection={INITIAL_SORT_DIRECTION}
timeseriesData={timeseriesDataFetch?.data}
timeseriesDataLoading={timeseriesDataFetch.status === FETCH_STATUS.LOADING}
noItemsMessage={
<EmptyMessage
heading={i18n.translate('xpack.apm.servicesTable.notFoundLabel', {
defaultMessage: 'No services found',
})}
/>
}
/>
</EuiFlexItem>
</EuiFlexGroup>
</>
)}
<Welcome
isModalVisible={tourState.isModalVisible ?? false}
onClose={handleModalClose}
onConfirm={handleModalClose}
/>
</>
);
}

View file

@ -1,39 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { ReactElement } from 'react';
import { EuiFlexGroup } from '@elastic/eui';
import { css } from '@emotion/react';
import { TooltipContent } from './tooltip_content';
import { Popover } from './popover';
interface Props {
label: string;
toolTip?: ReactElement | string;
formula?: string;
}
export const ColumnHeader = React.memo(({ label, toolTip, formula }: Props) => (
<EuiFlexGroup gutterSize="xs">
<div
css={css`
overflow-wrap: break-word !important;
word-break: break-word;
min-width: 0;
text-overflow: ellipsis;
overflow: hidden;
`}
>
{label}
</div>
{toolTip && (
<Popover>
<TooltipContent formula={formula} description={toolTip} />
</Popover>
)}
</EuiFlexGroup>
));

View file

@ -1,268 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem, RIGHT_ALIGNMENT } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { TypeOf } from '@kbn/typed-react-router-config';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { css } from '@emotion/react';
import {
asDecimalOrInteger,
asMillisecondDuration,
asPercent,
asTransactionRate,
} from '../../../../../../common/utils/formatters';
import { Breakpoints } from '../../../../../hooks/use_breakpoints';
import { unit } from '../../../../../utils/style';
import { ApmRoutes } from '../../../../routing/apm_route_config';
import {
getTimeSeriesColor,
ChartType,
} from '../../../../shared/charts/helper/get_timeseries_color';
import {
getMetricsFormula,
ChartMetricType,
} from '../../../../shared/charts/helper/get_metrics_formulas';
import { EnvironmentBadge } from '../../../../shared/environment_badge';
import { ServiceLink } from '../../../../shared/links/apm/service_link';
import { ListMetric } from '../../../../shared/list_metric';
import { ITableColumn } from '../../../../shared/managed_table';
import { NotAvailableApmMetrics } from '../../../../shared/not_available_popover/not_available_apm_metrics';
import { TruncateWithTooltip } from '../../../../shared/truncate_with_tooltip';
import { ServiceInventoryFieldName } from './multi_signal_services_table';
import { EntityDataStreamType } from '../../../../../../common/entities/types';
import { isApmSignal } from '../../../../../utils/get_signal_type';
import { ColumnHeader } from './column_header';
import { APIReturnType } from '../../../../../services/rest/create_call_apm_api';
type ServicesDetailedStatisticsAPIResponse =
APIReturnType<'POST /internal/apm/entities/services/detailed_statistics'>;
type EntityServiceListItem = APIReturnType<'GET /internal/apm/entities/services'>['services'][0];
export function getServiceColumns({
query,
breakpoints,
timeseriesDataLoading,
timeseriesData,
}: {
query: TypeOf<ApmRoutes, '/services'>['query'];
breakpoints: Breakpoints;
timeseriesDataLoading: boolean;
timeseriesData?: ServicesDetailedStatisticsAPIResponse;
}): Array<ITableColumn<EntityServiceListItem>> {
const { isSmall, isLarge } = breakpoints;
const showWhenSmallOrGreaterThanLarge = isSmall || !isLarge;
return [
{
field: ServiceInventoryFieldName.ServiceName,
name: i18n.translate('xpack.apm.multiSignal.servicesTable.nameColumnLabel', {
defaultMessage: 'Name',
}),
sortable: true,
render: (_, { serviceName, agentName, dataStreamTypes }) => (
<TruncateWithTooltip
data-test-subj="apmServiceListAppLink"
text={serviceName}
content={
<EuiFlexGroup gutterSize="s" justifyContent="flexStart">
<EuiFlexItem grow={false}>
<ServiceLink serviceName={serviceName} agentName={agentName} query={query} />
</EuiFlexItem>
</EuiFlexGroup>
}
/>
),
},
{
field: ServiceInventoryFieldName.Environments,
name: i18n.translate('xpack.apm.multiSignal.servicesTable.environmentColumnLabel', {
defaultMessage: 'Environment',
}),
sortable: true,
width: `${unit * 9}px`,
dataType: 'number',
render: (_, { environments, dataStreamTypes }) => (
<EnvironmentBadge
environments={environments}
isMetricsSignalType={dataStreamTypes.includes(EntityDataStreamType.METRICS)}
/>
),
align: RIGHT_ALIGNMENT,
},
{
field: ServiceInventoryFieldName.Latency,
name: i18n.translate('xpack.apm.multiSignal.servicesTable.latencyAvgColumnLabel', {
defaultMessage: 'Latency (avg.)',
}),
sortable: true,
dataType: 'number',
align: RIGHT_ALIGNMENT,
render: (_, { metrics, serviceName, dataStreamTypes }) => {
const { currentPeriodColor } = getTimeSeriesColor(ChartType.LATENCY_AVG);
return !isApmSignal(dataStreamTypes) ? (
<NotAvailableApmMetrics />
) : (
<ListMetric
isLoading={timeseriesDataLoading}
series={timeseriesData?.currentPeriod?.[serviceName]?.latency}
color={currentPeriodColor}
valueLabel={asMillisecondDuration(metrics.latency)}
hideSeries={!showWhenSmallOrGreaterThanLarge}
/>
);
},
},
{
field: ServiceInventoryFieldName.Throughput,
name: i18n.translate('xpack.apm.multiSignal.servicesTable.throughputColumnLabel', {
defaultMessage: 'Throughput',
}),
sortable: true,
dataType: 'number',
align: RIGHT_ALIGNMENT,
render: (_, { metrics, serviceName, dataStreamTypes }) => {
const { currentPeriodColor } = getTimeSeriesColor(ChartType.THROUGHPUT);
return !isApmSignal(dataStreamTypes) ? (
<NotAvailableApmMetrics />
) : (
<ListMetric
color={currentPeriodColor}
valueLabel={asTransactionRate(metrics.throughput)}
isLoading={timeseriesDataLoading}
series={timeseriesData?.currentPeriod?.[serviceName]?.throughput}
hideSeries={!showWhenSmallOrGreaterThanLarge}
/>
);
},
},
{
field: ServiceInventoryFieldName.FailedTransactionRate,
name: i18n.translate('xpack.apm.multiSignal.servicesTable.transactionErrorRate', {
defaultMessage: 'Failed transaction rate',
}),
sortable: true,
dataType: 'number',
align: RIGHT_ALIGNMENT,
render: (_, { metrics, serviceName, dataStreamTypes }) => {
const { currentPeriodColor } = getTimeSeriesColor(ChartType.FAILED_TRANSACTION_RATE);
return !isApmSignal(dataStreamTypes) ? (
<NotAvailableApmMetrics />
) : (
<ListMetric
color={currentPeriodColor}
valueLabel={asPercent(metrics.failedTransactionRate, 1)}
isLoading={timeseriesDataLoading}
series={timeseriesData?.currentPeriod?.[serviceName]?.failedTransactionRate}
hideSeries={!showWhenSmallOrGreaterThanLarge}
/>
);
},
},
{
field: ServiceInventoryFieldName.logRate,
name: (
<ColumnHeader
label={i18n.translate('xpack.apm.multiSignal.servicesTable.logRate', {
defaultMessage: 'Log rate (per min.)',
})}
formula={getMetricsFormula(ChartMetricType.LOG_RATE)}
toolTip={
<FormattedMessage
defaultMessage="Rate of logs per minute observed for given {serviceName}."
id="xpack.apm.multiSignal.servicesTable.logRate.tooltip.description"
values={{
serviceName: (
<code
css={css`
word-break: break-word;
`}
>
{i18n.translate(
'xpack.apm.multiSignal.servicesTable.logRate.tooltip.serviceNameLabel',
{
defaultMessage: 'service.name',
}
)}
</code>
),
}}
/>
}
/>
),
sortable: true,
dataType: 'number',
align: RIGHT_ALIGNMENT,
render: (_, { metrics, serviceName, dataStreamTypes, hasLogMetrics }) => {
const { currentPeriodColor } = getTimeSeriesColor(ChartType.LOG_RATE);
return (
<ListMetric
isLoading={timeseriesDataLoading}
color={currentPeriodColor}
series={timeseriesData?.currentPeriod?.[serviceName]?.logRate}
valueLabel={asDecimalOrInteger(metrics.logRate)}
hideSeries={!showWhenSmallOrGreaterThanLarge}
/>
);
},
},
{
field: ServiceInventoryFieldName.LogErrorRate,
name: (
<ColumnHeader
label={i18n.translate('xpack.apm.multiSignal.servicesTable.logErrorRate', {
defaultMessage: 'Log error rate (per min.)',
})}
formula={getMetricsFormula(ChartMetricType.LOG_ERROR_RATE)}
toolTip={
<FormattedMessage
defaultMessage="Rate of error logs per minute observed for given {serviceName}."
id="xpack.apm.multiSignal.servicesTable.logErrorRate.tooltip.description"
values={{
serviceName: (
<code
css={css`
word-break: break-word;
`}
>
{i18n.translate(
'xpack.apm.multiSignal.servicesTable.logErrorRate.tooltip.serviceNameLabel',
{
defaultMessage: 'service.name',
}
)}
</code>
),
}}
/>
}
/>
),
sortable: true,
dataType: 'number',
align: RIGHT_ALIGNMENT,
render: (_, { metrics, serviceName, dataStreamTypes, hasLogMetrics }) => {
const { currentPeriodColor } = getTimeSeriesColor(ChartType.LOG_ERROR_RATE);
return (
<ListMetric
isLoading={timeseriesDataLoading}
color={currentPeriodColor}
series={timeseriesData?.currentPeriod?.[serviceName]?.logErrorRate}
valueLabel={asDecimalOrInteger(metrics.logErrorRate)}
hideSeries={!showWhenSmallOrGreaterThanLarge}
/>
);
},
},
];
}

View file

@ -1,82 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { omit } from 'lodash';
import React, { useMemo } from 'react';
import { useApmParams } from '../../../../../hooks/use_apm_params';
import { useBreakpoints } from '../../../../../hooks/use_breakpoints';
import { FETCH_STATUS, isFailure, isPending } from '../../../../../hooks/use_fetcher';
import { APIReturnType } from '../../../../../services/rest/create_call_apm_api';
import { ManagedTable } from '../../../../shared/managed_table';
import { getServiceColumns } from './get_service_columns';
type MainStatisticsApiResponse = APIReturnType<'GET /internal/apm/entities/services'>;
type ServicesDetailedStatisticsAPIResponse =
APIReturnType<'POST /internal/apm/entities/services/detailed_statistics'>;
export enum ServiceInventoryFieldName {
ServiceName = 'serviceName',
Environments = 'environments',
Throughput = 'metrics.throughput',
Latency = 'metrics.latency',
FailedTransactionRate = 'metrics.failedTransactionRate',
logRate = 'metrics.logRate',
LogErrorRate = 'metrics.logErrorRate',
}
interface Props {
status: FETCH_STATUS;
initialSortField: ServiceInventoryFieldName;
initialPageSize: number;
initialSortDirection: 'asc' | 'desc';
noItemsMessage: React.ReactNode;
data: MainStatisticsApiResponse['services'];
timeseriesDataLoading: boolean;
timeseriesData?: ServicesDetailedStatisticsAPIResponse;
}
export function MultiSignalServicesTable({
status,
data,
initialSortField,
initialPageSize,
initialSortDirection,
noItemsMessage,
timeseriesDataLoading,
timeseriesData,
}: Props) {
const breakpoints = useBreakpoints();
const { query } = useApmParams('/services');
const serviceColumns = useMemo(() => {
return getServiceColumns({
// removes pagination and sort instructions from the query so it won't be passed down to next route
query: omit(query, 'page', 'pageSize', 'sortDirection', 'sortField'),
breakpoints,
timeseriesDataLoading,
timeseriesData,
});
}, [query, breakpoints, timeseriesDataLoading, timeseriesData]);
return (
<EuiFlexGroup gutterSize="xs" direction="column" responsive={false}>
<EuiFlexItem>
<ManagedTable
isLoading={isPending(status)}
error={isFailure(status)}
columns={serviceColumns}
items={data}
noItemsMessage={noItemsMessage}
initialSortField={initialSortField}
initialSortDirection={initialSortDirection}
initialPageSize={initialPageSize}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -1,144 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
EuiCallOut,
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiImage,
EuiLink,
EuiText,
EuiTextColor,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { dashboardsLight } from '@kbn/shared-svg';
import useEffectOnce from 'react-use/lib/useEffectOnce';
import { useKibana } from '../../../../../context/kibana_context/use_kibana';
import { useLocalStorage } from '../../../../../hooks/use_local_storage';
import { ApmPluginStartDeps, ApmServices } from '../../../../../plugin';
import { EntityInventoryAddDataParams } from '../../../../../services/telemetry';
import {
AddApmData,
AssociateServiceLogs,
CollectServiceLogs,
} from '../../../../shared/add_data_buttons/buttons';
import { useBreakpoints } from '../../../../../hooks/use_breakpoints';
export function NoEntitiesEmptyState() {
const { isLarge } = useBreakpoints();
const { services } = useKibana<ApmPluginStartDeps & ApmServices>();
const [userHasDismissedCallout, setUserHasDismissedCallout] = useLocalStorage(
'apm.uiNewExperienceCallout',
false
);
useEffectOnce(() => {
services.telemetry.reportEntityInventoryPageState({ state: 'empty_state' });
});
function reportButtonClick(journey: EntityInventoryAddDataParams['journey']) {
services.telemetry.reportEntityInventoryAddData({
view: 'empty_state',
journey,
});
}
return (
<EuiFlexGroup direction="column">
{!userHasDismissedCallout && (
<EuiFlexItem>
<EuiCallOut
css={{ textAlign: 'left' }}
onDismiss={() => setUserHasDismissedCallout(true)}
title={i18n.translate('xpack.apm.noEntitiesEmptyState.callout.title', {
defaultMessage: 'Trying for the first time?',
})}
>
<p>
{i18n.translate('xpack.apm.noEntitiesEmptyState.description', {
defaultMessage:
'It can take up to a couple of minutes for your services to show. Try refreshing the page in a minute.',
})}
</p>
<EuiLink
external
target="_blank"
data-test-subj="apmNewExperienceEmptyStateLink"
href="https://ela.st/elastic-entity-model-first-time"
>
{i18n.translate('xpack.apm.noEntitiesEmptyState.learnMore.link', {
defaultMessage: 'Learn more',
})}
</EuiLink>
</EuiCallOut>
</EuiFlexItem>
)}
<EuiFlexItem>
<EuiEmptyPrompt
hasShadow={false}
hasBorder={false}
id="apmNewExperienceEmptyState"
icon={<EuiImage size="fullWidth" src={dashboardsLight} alt="" />}
title={
<h2>
{i18n.translate('xpack.apm.noEntitiesEmptyState.title', {
defaultMessage: 'No services available.',
})}
</h2>
}
layout={isLarge ? 'vertical' : 'horizontal'}
color="plain"
body={
<>
<p>
{i18n.translate('xpack.apm.noEntitiesEmptyState.body.description', {
defaultMessage:
'The services inventory provides an overview of the health and general performance of your services. To add data to this page, instrument your services using the APM agent or detect services from your logs.',
})}
</p>
<EuiHorizontalRule margin="m" />
<EuiText textAlign="left">
<h5>
<EuiTextColor color="default">
{i18n.translate('xpack.apm.noEntitiesEmptyState.actions.title', {
defaultMessage: 'Start observing your services:',
})}
</EuiTextColor>
</h5>
</EuiText>
</>
}
actions={
<EuiFlexGroup responsive={false} wrap gutterSize="xl" direction="column">
<EuiFlexGroup direction="row" gutterSize="xs">
<AddApmData
data-test-subj="apmAddDataEmptyState"
onClick={() => {
reportButtonClick('add_apm_agent');
}}
/>
<CollectServiceLogs
onClick={() => {
reportButtonClick('collect_new_service_logs');
}}
/>
<AssociateServiceLogs
onClick={() => {
reportButtonClick('associate_existing_service_logs');
}}
/>
</EuiFlexGroup>
</EuiFlexGroup>
}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -8,16 +8,16 @@
import { CoreStart } from '@kbn/core/public';
import { Meta, Story } from '@storybook/react';
import React from 'react';
import { ApmServiceInventory } from '.';
import { AnomalyDetectionSetupState } from '../../../../../common/anomaly_detection/get_anomaly_detection_setup_state';
import { AnomalyDetectionJobsContext } from '../../../../context/anomaly_detection_jobs/anomaly_detection_jobs_context';
import { ApmPluginContextValue } from '../../../../context/apm_plugin/apm_plugin_context';
import { MockApmPluginStorybook } from '../../../../context/apm_plugin/mock_apm_plugin_storybook';
import { FETCH_STATUS } from '../../../../hooks/use_fetcher';
import { ServiceInventory } from '.';
import { AnomalyDetectionSetupState } from '../../../../common/anomaly_detection/get_anomaly_detection_setup_state';
import { AnomalyDetectionJobsContext } from '../../../context/anomaly_detection_jobs/anomaly_detection_jobs_context';
import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context';
import { MockApmPluginStorybook } from '../../../context/apm_plugin/mock_apm_plugin_storybook';
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
const stories: Meta<{}> = {
title: 'app/ServiceInventory',
component: ApmServiceInventory,
component: ServiceInventory,
decorators: [
(StoryComponent) => {
const coreMock = {
@ -60,5 +60,5 @@ const stories: Meta<{}> = {
export default stories;
export const Example: Story<{}> = () => {
return <ApmServiceInventory />;
return <ServiceInventory />;
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { APIReturnType } from '../../../../../../services/rest/create_call_apm_api';
import { APIReturnType } from '../../../../../services/rest/create_call_apm_api';
type ServiceListAPIResponse = APIReturnType<'GET /internal/apm/services'>;

View file

@ -20,40 +20,37 @@ import { ALERT_STATUS_ACTIVE } from '@kbn/rule-data-utils';
import { TypeOf } from '@kbn/typed-react-router-config';
import { omit } from 'lodash';
import React, { useMemo } from 'react';
import { ServiceHealthStatus } from '../../../../../../common/service_health_status';
import { ServiceHealthStatus } from '../../../../../common/service_health_status';
import {
ServiceInventoryFieldName,
ServiceListItem,
} from '../../../../../../common/service_inventory';
import { isDefaultTransactionType } from '../../../../../../common/transaction_types';
} from '../../../../../common/service_inventory';
import { isDefaultTransactionType } from '../../../../../common/transaction_types';
import {
asMillisecondDuration,
asPercent,
asTransactionRate,
} from '../../../../../../common/utils/formatters';
import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context';
import { useApmParams } from '../../../../../hooks/use_apm_params';
import { useApmRouter } from '../../../../../hooks/use_apm_router';
import { Breakpoints, useBreakpoints } from '../../../../../hooks/use_breakpoints';
import { useFallbackToTransactionsFetcher } from '../../../../../hooks/use_fallback_to_transactions_fetcher';
import { FETCH_STATUS, isFailure, isPending } from '../../../../../hooks/use_fetcher';
import { APIReturnType } from '../../../../../services/rest/create_call_apm_api';
import { unit } from '../../../../../utils/style';
import { ApmRoutes } from '../../../../routing/apm_route_config';
import { AggregatedTransactionsBadge } from '../../../../shared/aggregated_transactions_badge';
import {
ChartType,
getTimeSeriesColor,
} from '../../../../shared/charts/helper/get_timeseries_color';
import { EnvironmentBadge } from '../../../../shared/environment_badge';
import { ServiceLink } from '../../../../shared/links/apm/service_link';
import { ListMetric } from '../../../../shared/list_metric';
} from '../../../../../common/utils/formatters';
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
import { useApmParams } from '../../../../hooks/use_apm_params';
import { useApmRouter } from '../../../../hooks/use_apm_router';
import { Breakpoints, useBreakpoints } from '../../../../hooks/use_breakpoints';
import { useFallbackToTransactionsFetcher } from '../../../../hooks/use_fallback_to_transactions_fetcher';
import { FETCH_STATUS, isFailure, isPending } from '../../../../hooks/use_fetcher';
import { APIReturnType } from '../../../../services/rest/create_call_apm_api';
import { unit } from '../../../../utils/style';
import { ApmRoutes } from '../../../routing/apm_route_config';
import { AggregatedTransactionsBadge } from '../../../shared/aggregated_transactions_badge';
import { ChartType, getTimeSeriesColor } from '../../../shared/charts/helper/get_timeseries_color';
import { EnvironmentBadge } from '../../../shared/environment_badge';
import { ServiceLink } from '../../../shared/links/apm/service_link';
import { ListMetric } from '../../../shared/list_metric';
import {
ITableColumn,
ManagedTable,
SortFunction,
TableSearchBar,
} from '../../../../shared/managed_table';
} from '../../../shared/managed_table';
import { ColumnHeaderWithTooltip } from './column_header_with_tooltip';
import { HealthBadge } from './health_badge';

View file

@ -11,8 +11,8 @@ import {
getServiceHealthStatusBadgeColor,
getServiceHealthStatusLabel,
ServiceHealthStatus,
} from '../../../../../../common/service_health_status';
import { useTheme } from '../../../../../hooks/use_theme';
} from '../../../../../common/service_health_status';
import { useTheme } from '../../../../hooks/use_theme';
export function HealthBadge({ healthStatus }: { healthStatus: ServiceHealthStatus }) {
const theme = useTheme();

View file

@ -4,8 +4,8 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ServiceHealthStatus } from '../../../../../../common/service_health_status';
import { ServiceInventoryFieldName } from '../../../../../../common/service_inventory';
import { ServiceHealthStatus } from '../../../../../common/service_health_status';
import { ServiceInventoryFieldName } from '../../../../../common/service_inventory';
import { orderServiceItems } from './order_service_items';
describe('orderServiceItems', () => {

View file

@ -5,11 +5,11 @@
* 2.0.
*/
import { orderBy } from 'lodash';
import { ServiceHealthStatus } from '../../../../../../common/service_health_status';
import { ServiceHealthStatus } from '../../../../../common/service_health_status';
import {
ServiceListItem,
ServiceInventoryFieldName,
} from '../../../../../../common/service_inventory';
} from '../../../../../common/service_inventory';
type SortValueGetter = (item: ServiceListItem) => string | number;

View file

@ -8,13 +8,13 @@
import { CoreStart } from '@kbn/core/public';
import { Meta, Story } from '@storybook/react';
import React, { ComponentProps } from 'react';
import { FETCH_STATUS } from '../../../../../hooks/use_fetcher';
import { FETCH_STATUS } from '../../../../hooks/use_fetcher';
import { ApmServicesTable } from './apm_services_table';
import { ServiceHealthStatus } from '../../../../../../common/service_health_status';
import { ServiceInventoryFieldName } from '../../../../../../common/service_inventory';
import type { ApmPluginContextValue } from '../../../../../context/apm_plugin/apm_plugin_context';
import { MockApmPluginStorybook } from '../../../../../context/apm_plugin/mock_apm_plugin_storybook';
import { mockApmApiCallResponse } from '../../../../../services/rest/call_apm_api_spy';
import { ServiceHealthStatus } from '../../../../../common/service_health_status';
import { ServiceInventoryFieldName } from '../../../../../common/service_inventory';
import type { ApmPluginContextValue } from '../../../../context/apm_plugin/apm_plugin_context';
import { MockApmPluginStorybook } from '../../../../context/apm_plugin/mock_apm_plugin_storybook';
import { mockApmApiCallResponse } from '../../../../services/rest/call_apm_api_spy';
import { items, overflowItems } from './__fixtures__/service_api_mock_data';
type Args = ComponentProps<typeof ApmServicesTable>;

View file

@ -9,10 +9,10 @@ import { composeStories } from '@storybook/testing-react';
import { render, screen } from '@testing-library/react';
import React from 'react';
import { getServiceColumns } from './apm_services_table';
import { ENVIRONMENT_ALL } from '../../../../../../common/environment_filter_values';
import { Breakpoints } from '../../../../../hooks/use_breakpoints';
import { apmRouter } from '../../../../routing/apm_route_config';
import * as timeSeriesColor from '../../../../shared/charts/helper/get_timeseries_color';
import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values';
import { Breakpoints } from '../../../../hooks/use_breakpoints';
import { apmRouter } from '../../../routing/apm_route_config';
import * as timeSeriesColor from '../../../shared/charts/helper/get_timeseries_color';
import * as stories from './service_list.stories';
const { ServiceListEmptyState, ServiceListWithItems } = composeStories(stories);

View file

@ -11,7 +11,6 @@ import { AnnotationsContextProvider } from '../../../context/annotations/annotat
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context';
import { useEntityManagerEnablementContext } from '../../../context/entity_manager_context/use_entity_manager_enablement_context';
import { useApmParams } from '../../../hooks/use_apm_params';
import { useTimeRange } from '../../../hooks/use_time_range';
import { isApmSignal, isLogsSignal, isLogsOnlySignal } from '../../../utils/get_signal_type';
@ -21,6 +20,7 @@ import { ServiceTabEmptyState } from '../service_tab_empty_state';
import { useLocalStorage } from '../../../hooks/use_local_storage';
import { SearchBar } from '../../shared/search_bar/search_bar';
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
import { useEntityCentricExperienceSetting } from '../../../hooks/use_entity_centric_experience_setting';
/**
* The height a chart should be if it's next to a table with 5 rows and a title.
* Add the height of the pagination row.
@ -28,7 +28,7 @@ import { FETCH_STATUS } from '../../../hooks/use_fetcher';
export const chartHeight = 288;
export function ServiceOverview() {
const { isEntityCentricExperienceViewEnabled } = useEntityManagerEnablementContext();
const { isEntityCentricExperienceEnabled } = useEntityCentricExperienceSetting();
const { serviceName, serviceEntitySummary, serviceEntitySummaryStatus } = useApmServiceContext();
const setScreenContext = useApmPluginContext().observabilityAIAssistant?.service.setScreenContext;
@ -68,8 +68,7 @@ export function ServiceOverview() {
const hasApmSignal = hasSignal && isApmSignal(serviceEntitySummary.dataStreamTypes);
// Shows APM overview when entity has APM signal or when Entity centric is not enabled or when entity has no signal
const showApmOverview =
isEntityCentricExperienceViewEnabled === false || hasApmSignal || !hasSignal;
const showApmOverview = isEntityCentricExperienceEnabled === false || hasApmSignal || !hasSignal;
if (serviceEntitySummaryStatus === FETCH_STATUS.LOADING) {
return (

View file

@ -7,9 +7,9 @@
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React from 'react';
import { LogRateChart } from '../../entities/charts/log_rate_chart';
import { LogErrorRateChart } from '../../entities/charts/log_error_rate_chart';
import { chartHeight } from '..';
import { LogRateChart } from '../../../shared/charts/log_rates/log_rate_chart';
import { LogErrorRateChart } from '../../../shared/charts/log_rates/log_error_rate_chart';
export function LogsOverview() {
return (

View file

@ -22,8 +22,6 @@ import {
collectServiceLogs,
addApmData,
} from '../../../shared/add_data_buttons/buttons';
import { ServiceEcoTour } from '../../../shared/entity_enablement/service_eco_tour';
import { useEntityManagerEnablementContext } from '../../../../context/entity_manager_context/use_entity_manager_enablement_context';
const addData = i18n.translate('xpack.apm.addDataContextMenu.link', {
defaultMessage: 'Add data',
@ -31,7 +29,6 @@ const addData = i18n.translate('xpack.apm.addDataContextMenu.link', {
export function AddDataContextMenu() {
const [popoverOpen, setPopoverOpen] = useState(false);
const { tourState, updateTourState } = useEntityManagerEnablementContext();
const { services } = useKibana<ApmPluginStartDeps & ApmServices>();
const {
core: {
@ -93,23 +90,17 @@ export function AddDataContextMenu() {
},
];
const handleTourClose = () => {
updateTourState({ isTourActive: false });
setPopoverOpen(false);
};
return (
<>
<EuiPopover
id="integrations-menu"
button={button}
isOpen={popoverOpen || tourState.isTourActive}
isOpen={popoverOpen}
closePopover={() => setPopoverOpen(false)}
panelPaddingSize="none"
anchorPosition="downRight"
>
<ServiceEcoTour onFinish={handleTourClose}>
<EuiContextMenu initialPanelId={0} panels={panels} />
</ServiceEcoTour>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
</>
);

View file

@ -17,7 +17,7 @@ import { AnomalyDetectionSetupLink } from './anomaly_detection_setup_link';
import { InspectorHeaderLink } from './inspector_header_link';
import { Labs } from './labs';
import { AddDataContextMenu } from './add_data_context_menu';
import { useEntityManagerEnablementContext } from '../../../../context/entity_manager_context/use_entity_manager_enablement_context';
import { useEntityCentricExperienceSetting } from '../../../../hooks/use_entity_centric_experience_setting';
export function ApmHeaderActionMenu() {
const { core, plugins, config } = useApmPluginContext();
@ -33,8 +33,7 @@ export function ApmHeaderActionMenu() {
capabilities
);
const canSaveApmAlerts = capabilities.apm.save && canSaveAlerts;
const { isEntityCentricExperienceViewEnabled, isEnablementPending } =
useEntityManagerEnablementContext();
const { isEntityCentricExperienceEnabled } = useEntityCentricExperienceSetting();
function apmHref(path: string) {
return getLegacyApmHref({ basePath, path, search });
@ -73,11 +72,10 @@ export function ApmHeaderActionMenu() {
canReadMlJobs={canReadMlJobs}
/>
)}
{isEntityCentricExperienceViewEnabled ? (
{isEntityCentricExperienceEnabled ? (
<AddDataContextMenu />
) : (
<EuiHeaderLink
isLoading={isEnablementPending}
color="primary"
href={kibanaHref('/app/apm/tutorial')}
iconType="indexOpen"

View file

@ -42,7 +42,6 @@ import { RedirectWithDefaultEnvironment } from './redirect_with_default_environm
import { RedirectWithOffset } from './redirect_with_offset';
import { ScrollToTopOnPathChange } from './scroll_to_top_on_path_change';
import { UpdateExecutionContextOnRouteChange } from './update_execution_context_on_route_change';
import { EntityManagerEnablementContextProvider } from '../../../context/entity_manager_context/entity_manager_context';
const storage = new Storage(localStorage);
@ -83,17 +82,15 @@ export function ApmAppRoot({
<BreadcrumbsContextProvider>
<UrlParamsProvider>
<LicenseProvider>
<EntityManagerEnablementContextProvider>
<AnomalyDetectionJobsContextProvider>
<InspectorContextProvider>
<ApmThemeProvider>
<MountApmHeaderActionMenu />
<Route component={ScrollToTopOnPathChange} />
<RouteRenderer />
</ApmThemeProvider>
</InspectorContextProvider>
</AnomalyDetectionJobsContextProvider>
</EntityManagerEnablementContextProvider>
<AnomalyDetectionJobsContextProvider>
<InspectorContextProvider>
<ApmThemeProvider>
<MountApmHeaderActionMenu />
<Route component={ScrollToTopOnPathChange} />
<RouteRenderer />
</ApmThemeProvider>
</InspectorContextProvider>
</AnomalyDetectionJobsContextProvider>
</LicenseProvider>
</UrlParamsProvider>
</BreadcrumbsContextProvider>

View file

@ -14,7 +14,6 @@ import React, { useContext } from 'react';
import { useLocation } from 'react-router-dom';
import { FeatureFeedbackButton } from '@kbn/observability-shared-plugin/public';
import { useLocalStorage } from '../../../../hooks/use_local_storage';
import { useEntityManagerEnablementContext } from '../../../../context/entity_manager_context/use_entity_manager_enablement_context';
import { useDefaultAiAssistantStarterPromptsForAPM } from '../../../../hooks/use_default_ai_assistant_starter_prompts_for_apm';
import { KibanaEnvironmentContext } from '../../../../context/kibana_environment_context/kibana_environment_context';
import { getPathForFeedback } from '../../../../utils/get_path_for_feedback';
@ -27,6 +26,7 @@ import { ApmEnvironmentFilter } from '../../../shared/environment_filter';
import { getNoDataConfig } from '../no_data_config';
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
import { EntitiesInventoryCallout } from './entities_inventory_callout';
import { useEntityCentricExperienceSetting } from '../../../../hooks/use_entity_centric_experience_setting';
// Paths that must skip the no data screen
const bypassNoDataScreenPaths = ['/settings', '/diagnostics'];
@ -77,7 +77,7 @@ export function ApmMainTemplate({
true
);
const { isEntityCentricExperienceViewEnabled } = useEntityManagerEnablementContext();
const { isEntityCentricExperienceEnabled } = useEntityCentricExperienceSetting();
const ObservabilityPageTemplate = observabilityShared.navigation.PageTemplate;
@ -146,7 +146,7 @@ export function ApmMainTemplate({
<FeatureFeedbackButton
data-test-subj="infraApmFeedbackLink"
formUrl={
isEntityCentricExperienceViewEnabled && sanitizedPath.includes('service')
isEntityCentricExperienceEnabled && sanitizedPath.includes('service')
? APM_NEW_EXPERIENCE_FEEDBACK_LINK
: APM_FEEDBACK_LINK
}

View file

@ -14,18 +14,15 @@ import { useApmParams } from '../../../../hooks/use_apm_params';
import { useFetcher } from '../../../../hooks/use_fetcher';
import { useTimeRange } from '../../../../hooks/use_time_range';
import { APIReturnType } from '../../../../services/rest/create_call_apm_api';
import { getTimeSeriesColor, ChartType } from '../../../shared/charts/helper/get_timeseries_color';
import { TimeseriesChartWithContext } from '../../../shared/charts/timeseries_chart_with_context';
import { asInteger } from '../../../../../common/utils/formatters';
import { TooltipContent } from '../../service_inventory/multi_signal_inventory/table/tooltip_content';
import { Popover } from '../../service_inventory/multi_signal_inventory/table/popover';
import {
ChartMetricType,
getMetricsFormula,
} from '../../../shared/charts/helper/get_metrics_formulas';
import { ExploreLogsButton } from '../../../shared/explore_logs_button/explore_logs_button';
import { TooltipContent } from './tooltip_content';
import { Popover } from './popover';
import { mergeKueries, toKueryFilterFormat } from '../../../../../common/utils/kuery_utils';
import { ERROR_LOG_LEVEL, LOG_LEVEL } from '../../../../../common/es_fields/apm';
import { ExploreLogsButton } from '../../explore_logs_button/explore_logs_button';
import { TimeseriesChartWithContext } from '../timeseries_chart_with_context';
import { ChartMetricType, getMetricsFormula } from '../helper/get_metrics_formulas';
import { getTimeSeriesColor, ChartType } from '../helper/get_timeseries_color';
type LogErrorRateReturnType =
APIReturnType<'GET /internal/apm/entities/services/{serviceName}/logs_error_rate_timeseries'>;

View file

@ -13,16 +13,13 @@ import { useApmParams } from '../../../../hooks/use_apm_params';
import { useFetcher } from '../../../../hooks/use_fetcher';
import { useTimeRange } from '../../../../hooks/use_time_range';
import { APIReturnType } from '../../../../services/rest/create_call_apm_api';
import { getTimeSeriesColor, ChartType } from '../../../shared/charts/helper/get_timeseries_color';
import { TimeseriesChartWithContext } from '../../../shared/charts/timeseries_chart_with_context';
import { asInteger } from '../../../../../common/utils/formatters';
import { TooltipContent } from '../../service_inventory/multi_signal_inventory/table/tooltip_content';
import { Popover } from '../../service_inventory/multi_signal_inventory/table/popover';
import {
getMetricsFormula,
ChartMetricType,
} from '../../../shared/charts/helper/get_metrics_formulas';
import { ExploreLogsButton } from '../../../shared/explore_logs_button/explore_logs_button';
import { TooltipContent } from './tooltip_content';
import { Popover } from './popover';
import { ChartType, getTimeSeriesColor } from '../helper/get_timeseries_color';
import { ChartMetricType, getMetricsFormula } from '../helper/get_metrics_formulas';
import { ExploreLogsButton } from '../../explore_logs_button/explore_logs_button';
import { TimeseriesChartWithContext } from '../timeseries_chart_with_context';
type LogRateReturnType =
APIReturnType<'GET /internal/apm/entities/services/{serviceName}/logs_rate_timeseries'>;

View file

@ -10,7 +10,7 @@ import { EuiText } from '@elastic/eui';
import { css } from '@emotion/react';
import { FormattedMessage } from '@kbn/i18n-react';
export interface TooltipContentProps extends Pick<HTMLAttributes<HTMLDivElement>, 'style'> {
interface TooltipContentProps extends Pick<HTMLAttributes<HTMLDivElement>, 'style'> {
description: ReactElement | string;
formula?: string;
}

View file

@ -1,49 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiText, EuiTourStep } from '@elastic/eui';
import { useEntityManagerEnablementContext } from '../../../context/entity_manager_context/use_entity_manager_enablement_context';
export function ServiceEcoTour({
children,
onFinish,
}: {
children: React.ReactElement;
onFinish: () => void;
}) {
const { tourState } = useEntityManagerEnablementContext();
return (
<EuiTourStep
content={
<EuiText>
<p>
{i18n.translate('xpack.apm.serviceEcoTour.content', {
defaultMessage: 'You can now add services from logs to the service inventory',
})}
</p>
</EuiText>
}
isStepOpen={tourState.isTourActive}
minWidth={200}
onFinish={onFinish}
step={1}
stepsTotal={1}
title={i18n.translate('xpack.apm.serviceEcoTour.title', {
defaultMessage: 'Add services from logs',
})}
subtitle={i18n.translate('xpack.apm.serviceEcoTour.subtitle', {
defaultMessage: 'New Services Inventory',
})}
anchorPosition="rightUp"
>
{children}
</EuiTourStep>
);
}

View file

@ -5,9 +5,9 @@
* 2.0.
*/
import { useEntityCentricExperienceSetting } from '../../hooks/use_entity_centric_experience_setting';
import { useFetcher } from '../../hooks/use_fetcher';
import { APIReturnType } from '../../services/rest/create_call_apm_api';
import { useEntityManagerEnablementContext } from '../entity_manager_context/use_entity_manager_enablement_context';
export type ServiceEntitySummary =
APIReturnType<'GET /internal/apm/entities/services/{serviceName}/summary'>;
@ -21,17 +21,17 @@ export function useServiceEntitySummaryFetcher({
end?: string;
environment?: string;
}) {
const { isEntityCentricExperienceViewEnabled } = useEntityManagerEnablementContext();
const { isEntityCentricExperienceEnabled } = useEntityCentricExperienceSetting();
const { data, status } = useFetcher(
(callAPI) => {
if (isEntityCentricExperienceViewEnabled && serviceName && environment) {
if (isEntityCentricExperienceEnabled && serviceName && environment) {
return callAPI('GET /internal/apm/entities/services/{serviceName}/summary', {
params: { path: { serviceName }, query: { environment } },
});
}
},
[environment, isEntityCentricExperienceViewEnabled, serviceName]
[environment, isEntityCentricExperienceEnabled, serviceName]
);
return { serviceEntitySummary: data, serviceEntitySummaryStatus: status };

View file

@ -1,94 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { entityCentricExperience } from '@kbn/observability-plugin/common';
import React, { createContext } from 'react';
import {
SERVICE_INVENTORY_STORAGE_KEY,
serviceInventoryViewType$,
} from '../../analytics/register_service_inventory_view_type_context';
import { useLocalStorage } from '../../hooks/use_local_storage';
import { ApmPluginStartDeps, ApmServices } from '../../plugin';
import { useApmPluginContext } from '../apm_plugin/use_apm_plugin_context';
import { useKibana } from '../kibana_context/use_kibana';
import { ENTITY_FETCH_STATUS, useEntityManager } from './use_entity_manager';
export interface EntityManagerEnablementContextValue {
isEntityManagerEnabled: boolean;
isEnablementPending: boolean;
refetch: () => void;
serviceInventoryViewLocalStorageSetting: ServiceInventoryView;
setServiceInventoryViewLocalStorageSetting: (view: ServiceInventoryView) => void;
isEntityCentricExperienceViewEnabled: boolean;
tourState: TourState;
updateTourState: (newState: Partial<TourState>) => void;
}
export enum ServiceInventoryView {
classic = 'classic',
entity = 'entity',
}
export const EntityManagerEnablementContext = createContext(
{} as EntityManagerEnablementContextValue
);
interface TourState {
isModalVisible?: boolean;
isTourActive: boolean;
}
const TOUR_INITIAL_STATE: TourState = {
isModalVisible: undefined,
isTourActive: false,
};
export function EntityManagerEnablementContextProvider({
children,
}: {
children: React.ReactChild;
}) {
const { core } = useApmPluginContext();
const { services } = useKibana<ApmPluginStartDeps & ApmServices>();
const { isEnabled: isEntityManagerEnabled, status, refetch } = useEntityManager();
const [tourState, setTourState] = useLocalStorage('apm.serviceEcoTour', TOUR_INITIAL_STATE);
const [serviceInventoryViewLocalStorageSetting, setServiceInventoryViewLocalStorageSetting] =
useLocalStorage(SERVICE_INVENTORY_STORAGE_KEY, ServiceInventoryView.classic);
const isEntityCentricExperienceSettingEnabled = core.uiSettings.get<boolean>(
entityCentricExperience,
true
);
function handleServiceInventoryViewChange(nextView: ServiceInventoryView) {
setServiceInventoryViewLocalStorageSetting(nextView);
// Updates the telemetry context variable every time the user switches views
serviceInventoryViewType$.next({ serviceInventoryViewType: nextView });
services.telemetry.reportEntityExperienceStatusChange({
status: nextView === ServiceInventoryView.entity ? 'enabled' : 'disabled',
});
}
function handleTourStateUpdate(newTourState: Partial<TourState>) {
setTourState({ ...tourState, ...newTourState });
}
return (
<EntityManagerEnablementContext.Provider
value={{
isEntityManagerEnabled,
isEnablementPending: status === ENTITY_FETCH_STATUS.LOADING,
refetch,
serviceInventoryViewLocalStorageSetting,
setServiceInventoryViewLocalStorageSetting: handleServiceInventoryViewChange,
isEntityCentricExperienceViewEnabled: isEntityCentricExperienceSettingEnabled,
tourState,
updateTourState: handleTourStateUpdate,
}}
>
{children}
</EntityManagerEnablementContext.Provider>
);
}

View file

@ -1,55 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useEffect, useMemo, useState } from 'react';
import { ApmPluginStartDeps } from '../../plugin';
export enum ENTITY_FETCH_STATUS {
LOADING = 'loading',
SUCCESS = 'success',
FAILURE = 'failure',
NOT_INITIATED = 'not_initiated',
}
export function useEntityManager() {
const {
services: { entityManager },
} = useKibana<ApmPluginStartDeps>();
const [counter, setCounter] = useState(0);
const [result, setResult] = useState({
isEnabled: false,
status: ENTITY_FETCH_STATUS.NOT_INITIATED,
});
useEffect(() => {
async function isManagedEntityDiscoveryEnabled() {
setResult({ isEnabled: false, status: ENTITY_FETCH_STATUS.LOADING });
try {
const response = await entityManager.entityClient.isManagedEntityDiscoveryEnabled();
setResult({ isEnabled: response?.enabled, status: ENTITY_FETCH_STATUS.SUCCESS });
} catch (err) {
setResult({ isEnabled: false, status: ENTITY_FETCH_STATUS.FAILURE });
console.error(err);
}
}
isManagedEntityDiscoveryEnabled();
}, [entityManager, counter]);
return useMemo(() => {
return {
...result,
refetch: () => {
// this will invalidate the deps to `useEffect` and will result in a new request
setCounter((count) => count + 1);
},
};
}, [result]);
}

View file

@ -1,13 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useContext } from 'react';
import { EntityManagerEnablementContext } from './entity_manager_context';
export function useEntityManagerEnablementContext() {
return useContext(EntityManagerEnablementContext);
}

View file

@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { entityCentricExperience } from '@kbn/observability-plugin/common';
import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context';
export function useEntityCentricExperienceSetting() {
const { core } = useApmPluginContext();
const isEntityCentricExperienceEnabled = core.uiSettings.get<boolean>(
entityCentricExperience,
true
);
return { isEntityCentricExperienceEnabled };
}

View file

@ -19,10 +19,6 @@ import {
PluginInitializerContext,
SecurityServiceStart,
} from '@kbn/core/public';
import {
EntityManagerPublicPluginSetup,
EntityManagerPublicPluginStart,
} from '@kbn/entityManager-plugin/public';
import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public';
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { DiscoverSetup, DiscoverStart } from '@kbn/discover-plugin/public';
@ -86,7 +82,6 @@ import { getLazyAPMPolicyEditExtension } from './components/fleet_integration/la
import { featureCatalogueEntry } from './feature_catalogue_entry';
import { APMServiceDetailLocator } from './locator/service_detail_locator';
import { ITelemetryClient, TelemetryService } from './services/telemetry';
import { registerServiceInventoryViewTypeContext } from './analytics/register_service_inventory_view_type_context';
export type ApmPluginSetup = ReturnType<ApmPlugin['setup']>;
export type ApmPluginStart = void;
@ -111,7 +106,6 @@ export interface ApmPluginSetupDeps {
uiActions: UiActionsSetup;
profiling?: ProfilingPluginSetup;
cloud?: CloudSetup;
entityManager: EntityManagerPublicPluginSetup;
}
export interface ApmServices {
@ -148,7 +142,6 @@ export interface ApmPluginStartDeps {
dashboard: DashboardStart;
metricsDataAccess: MetricsDataPluginStart;
uiSettings: IUiSettingsClient;
entityManager: EntityManagerPublicPluginStart;
}
const applicationsTitle = i18n.translate('xpack.apm.navigation.rootTitle', {
@ -279,7 +272,6 @@ export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
};
this.telemetry.setup({ analytics: core.analytics });
registerServiceInventoryViewTypeContext(core.analytics);
// Registers a status check callback for the tutorial to call and verify if the APM integration is installed on fleet.
pluginSetupDeps.home?.tutorials.registerCustomStatusCheck(

View file

@ -9,9 +9,7 @@ import { AnalyticsServiceSetup } from '@kbn/core-analytics-browser';
import {
ITelemetryClient,
SearchQuerySubmittedParams,
EntityExperienceStatusParams,
TelemetryEventTypes,
EntityInventoryPageStateParams,
EntityInventoryAddDataParams,
EmptyStateClickParams,
} from './types';
@ -31,14 +29,6 @@ export class TelemetryClient implements ITelemetryClient {
});
};
public reportEntityExperienceStatusChange = (params: EntityExperienceStatusParams) => {
this.analytics.reportEvent(TelemetryEventTypes.ENTITY_EXPERIENCE_STATUS, params);
};
public reportEntityInventoryPageState = (params: EntityInventoryPageStateParams) => {
this.analytics.reportEvent(TelemetryEventTypes.ENTITY_INVENTORY_PAGE_STATE, params);
};
public reportEntityInventoryAddData = (params: EntityInventoryAddDataParams) => {
this.analytics.reportEvent(TelemetryEventTypes.ENTITY_INVENTORY_ADD_DATA, params);
};

View file

@ -33,30 +33,6 @@ const searchQuerySubmittedEventType: TelemetryEvent = {
},
};
const entityExperienceStatusEventType: TelemetryEvent = {
eventType: TelemetryEventTypes.ENTITY_EXPERIENCE_STATUS,
schema: {
status: {
type: 'keyword',
_meta: {
description: 'The status of the Entity experience (Enabled or Disabled)',
},
},
},
};
const entityInventoryPageStateEventType: TelemetryEvent = {
eventType: TelemetryEventTypes.ENTITY_INVENTORY_PAGE_STATE,
schema: {
state: {
type: 'keyword',
_meta: {
description: 'The current entity inventory page state (empty_state or available)',
},
},
},
};
const entityInventoryAddDataEventType: TelemetryEvent = {
eventType: TelemetryEventTypes.ENTITY_INVENTORY_ADD_DATA,
schema: {
@ -106,8 +82,6 @@ const learnMoreClickEventType: TelemetryEvent = {
export const apmTelemetryEventBasedTypes = [
searchQuerySubmittedEventType,
entityExperienceStatusEventType,
entityInventoryPageStateEventType,
entityInventoryAddDataEventType,
tryItClickEventType,
learnMoreClickEventType,

View file

@ -21,14 +21,6 @@ export interface SearchQuerySubmittedParams {
action: SearchQueryActions;
}
export interface EntityExperienceStatusParams {
status: 'enabled' | 'disabled';
}
export interface EntityInventoryPageStateParams {
state: 'empty_state' | 'available';
}
export interface EntityInventoryAddDataParams {
view: 'empty_state' | 'add_data_button' | 'add_apm_cta' | 'add_apm_n/a';
journey?: 'add_apm_agent' | 'associate_existing_service_logs' | 'collect_new_service_logs';
@ -40,15 +32,11 @@ export interface EmptyStateClickParams {
export type TelemetryEventParams =
| SearchQuerySubmittedParams
| EntityExperienceStatusParams
| EntityInventoryPageStateParams
| EntityInventoryAddDataParams
| EmptyStateClickParams;
export interface ITelemetryClient {
reportSearchQuerySubmitted(params: SearchQuerySubmittedParams): void;
reportEntityExperienceStatusChange(params: EntityExperienceStatusParams): void;
reportEntityInventoryPageState(params: EntityInventoryPageStateParams): void;
reportEntityInventoryAddData(params: EntityInventoryAddDataParams): void;
reportTryItClick(params: EmptyStateClickParams): void;
reportLearnMoreClick(params: EmptyStateClickParams): void;
@ -56,8 +44,6 @@ export interface ITelemetryClient {
export enum TelemetryEventTypes {
SEARCH_QUERY_SUBMITTED = 'Search Query Submitted',
ENTITY_EXPERIENCE_STATUS = 'entity_experience_status',
ENTITY_INVENTORY_PAGE_STATE = 'entity_inventory_page_state',
ENTITY_INVENTORY_ADD_DATA = 'entity_inventory_add_data',
TRY_IT_CLICK = 'try_it_click',
LEARN_MORE_CLICK = 'learn_more_click',

View file

@ -119,7 +119,6 @@
"@kbn/react-kibana-context-theme",
"@kbn/test-jest-helpers",
"@kbn/security-plugin-types-common",
"@kbn/entityManager-plugin",
"@kbn/server-route-repository-utils",
"@kbn/core-analytics-browser",
"@kbn/apm-types",

View file

@ -10647,12 +10647,6 @@
"xpack.apm.mobileServiceDetails.serviceMapTabLabel": "Carte des services",
"xpack.apm.mobileServiceDetails.transactionsTabLabel": "Transactions",
"xpack.apm.mobileServices.breadcrumb.title": "Services",
"xpack.apm.multiSignal.servicesTable.environmentColumnLabel": "Environnement",
"xpack.apm.multiSignal.servicesTable.latencyAvgColumnLabel": "Latence (moy.)",
"xpack.apm.multiSignal.servicesTable.logErrorRate": "Taux d'erreur des logs",
"xpack.apm.multiSignal.servicesTable.nameColumnLabel": "Nom",
"xpack.apm.multiSignal.servicesTable.throughputColumnLabel": "Rendement",
"xpack.apm.multiSignal.servicesTable.transactionErrorRate": "Taux de transactions ayant échoué",
"xpack.apm.navigation.apmSettingsTitle": "Paramètres",
"xpack.apm.navigation.apmStorageExplorerTitle": "Explorateur de stockage",
"xpack.apm.navigation.apmTutorialTitle": "Tutoriel",

View file

@ -10396,12 +10396,6 @@
"xpack.apm.mobileServiceDetails.serviceMapTabLabel": "サービスマップ",
"xpack.apm.mobileServiceDetails.transactionsTabLabel": "トランザクション",
"xpack.apm.mobileServices.breadcrumb.title": "サービス",
"xpack.apm.multiSignal.servicesTable.environmentColumnLabel": "環境",
"xpack.apm.multiSignal.servicesTable.latencyAvgColumnLabel": "レイテンシ(平均)",
"xpack.apm.multiSignal.servicesTable.logErrorRate": "ログエラー率",
"xpack.apm.multiSignal.servicesTable.nameColumnLabel": "名前",
"xpack.apm.multiSignal.servicesTable.throughputColumnLabel": "スループット",
"xpack.apm.multiSignal.servicesTable.transactionErrorRate": "失敗したトランザクション率",
"xpack.apm.navigation.apmSettingsTitle": "設定",
"xpack.apm.navigation.apmStorageExplorerTitle": "ストレージエクスプローラー",
"xpack.apm.navigation.apmTutorialTitle": "チュートリアル",

View file

@ -10418,12 +10418,6 @@
"xpack.apm.mobileServiceDetails.serviceMapTabLabel": "服务地图",
"xpack.apm.mobileServiceDetails.transactionsTabLabel": "事务",
"xpack.apm.mobileServices.breadcrumb.title": "服务",
"xpack.apm.multiSignal.servicesTable.environmentColumnLabel": "环境",
"xpack.apm.multiSignal.servicesTable.latencyAvgColumnLabel": "延迟(平均值)",
"xpack.apm.multiSignal.servicesTable.logErrorRate": "日志错误率",
"xpack.apm.multiSignal.servicesTable.nameColumnLabel": "名称",
"xpack.apm.multiSignal.servicesTable.throughputColumnLabel": "吞吐量",
"xpack.apm.multiSignal.servicesTable.transactionErrorRate": "失败事务率",
"xpack.apm.navigation.apmSettingsTitle": "设置",
"xpack.apm.navigation.apmStorageExplorerTitle": "Storage Explorer",
"xpack.apm.navigation.apmTutorialTitle": "教程",