Put APM links into header action menu (#82292)

This commit is contained in:
Nathan L Smith 2020-11-10 19:16:02 -06:00 committed by GitHub
parent 81b3a48c2c
commit 5ab41f5845
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 384 additions and 553 deletions

View file

@ -5,16 +5,16 @@
*/
import {
EuiButtonEmpty,
EuiContextMenu,
EuiContextMenuPanelDescriptor,
EuiHeaderLink,
EuiPopover,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useState } from 'react';
import { AlertType } from '../../../../../common/alert_types';
import { useApmPluginContext } from '../../../../hooks/useApmPluginContext';
import { AlertingFlyout } from '../../../alerting/AlertingFlyout';
import { IBasePath } from '../../../../../../src/core/public';
import { AlertType } from '../../../common/alert_types';
import { AlertingFlyout } from '../../components/alerting/AlertingFlyout';
const alertLabel = i18n.translate('xpack.apm.home.alertsMenu.alerts', {
defaultMessage: 'Alerts',
@ -46,28 +46,32 @@ const CREATE_TRANSACTION_ERROR_RATE_ALERT_PANEL_ID =
const CREATE_ERROR_COUNT_ALERT_PANEL_ID = 'create_error_count_panel';
interface Props {
basePath: IBasePath;
canReadAlerts: boolean;
canSaveAlerts: boolean;
canReadAnomalies: boolean;
includeTransactionDuration: boolean;
}
export function AlertingPopoverAndFlyout(props: Props) {
const { canSaveAlerts, canReadAlerts, canReadAnomalies } = props;
const plugin = useApmPluginContext();
export function AlertingPopoverAndFlyout({
basePath,
canSaveAlerts,
canReadAlerts,
canReadAnomalies,
includeTransactionDuration,
}: Props) {
const [popoverOpen, setPopoverOpen] = useState(false);
const [alertType, setAlertType] = useState<AlertType | null>(null);
const button = (
<EuiButtonEmpty
<EuiHeaderLink
color="primary"
iconType="arrowDown"
iconSide="right"
onClick={() => setPopoverOpen(true)}
onClick={() => setPopoverOpen((prevState) => !prevState)}
>
{alertLabel}
</EuiButtonEmpty>
</EuiHeaderLink>
);
const panels: EuiContextMenuPanelDescriptor[] = [
@ -98,7 +102,7 @@ export function AlertingPopoverAndFlyout(props: Props) {
'xpack.apm.home.alertsMenu.viewActiveAlerts',
{ defaultMessage: 'View active alerts' }
),
href: plugin.core.http.basePath.prepend(
href: basePath.prepend(
'/app/management/insightsAndAlerting/triggersActions/alerts'
),
icon: 'tableOfContents',
@ -113,6 +117,19 @@ export function AlertingPopoverAndFlyout(props: Props) {
id: CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID,
title: transactionDurationLabel,
items: [
// threshold alerts
...(includeTransactionDuration
? [
{
name: createThresholdAlertLabel,
onClick: () => {
setAlertType(AlertType.TransactionDuration);
setPopoverOpen(false);
},
},
]
: []),
// anomaly alerts
...(canReadAnomalies
? [

View file

@ -6,8 +6,8 @@
import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react';
import { MissingJobsAlert } from './AnomalyDetectionSetupLink';
import * as hooks from '../../../../hooks/useFetcher';
import { MissingJobsAlert } from './anomaly_detection_setup_link';
import * as hooks from '../../hooks/useFetcher';
async function renderTooltipAnchor({
jobs,

View file

@ -3,19 +3,25 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiButtonEmpty, EuiToolTip, EuiIcon } from '@elastic/eui';
import {
EuiHeaderLink,
EuiIcon,
EuiLoadingSpinner,
EuiToolTip,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useApmPluginContext } from '../../../../hooks/useApmPluginContext';
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
import { APMLink } from './APMLink';
import React from 'react';
import {
ENVIRONMENT_ALL,
getEnvironmentLabel,
} from '../../../../../common/environment_filter_values';
import { useUrlParams } from '../../../../hooks/useUrlParams';
import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher';
import { useLicense } from '../../../../hooks/useLicense';
} from '../../../common/environment_filter_values';
import { getAPMHref } from '../../components/shared/Links/apm/APMLink';
import { useApmPluginContext } from '../../hooks/useApmPluginContext';
import { FETCH_STATUS, useFetcher } from '../../hooks/useFetcher';
import { useLicense } from '../../hooks/useLicense';
import { useUrlParams } from '../../hooks/useUrlParams';
import { APIReturnType } from '../../services/rest/createCallApmApi';
import { units } from '../../style/variables';
export type AnomalyDetectionApiResponse = APIReturnType<
'/api/apm/settings/anomaly-detection',
@ -27,24 +33,27 @@ const DEFAULT_DATA = { jobs: [], hasLegacyJobs: false };
export function AnomalyDetectionSetupLink() {
const { uiFilters } = useUrlParams();
const environment = uiFilters.environment;
const plugin = useApmPluginContext();
const canGetJobs = !!plugin.core.application.capabilities.ml?.canGetJobs;
const { core } = useApmPluginContext();
const canGetJobs = !!core.application.capabilities.ml?.canGetJobs;
const license = useLicense();
const hasValidLicense = license?.isActive && license?.hasAtLeast('platinum');
const { basePath } = core.http;
return (
<APMLink
path="/settings/anomaly-detection"
<EuiHeaderLink
color="primary"
href={getAPMHref({ basePath, path: '/settings/anomaly-detection' })}
style={{ whiteSpace: 'nowrap' }}
>
<EuiButtonEmpty size="s" color="primary" iconType="inspect">
{ANOMALY_DETECTION_LINK_LABEL}
</EuiButtonEmpty>
{canGetJobs && hasValidLicense ? (
<MissingJobsAlert environment={environment} />
) : null}
</APMLink>
) : (
<EuiIcon type="inspect" color="primary" />
)}
<span style={{ marginInlineStart: units.half }}>
{ANOMALY_DETECTION_LINK_LABEL}
</span>
</EuiHeaderLink>
);
}
@ -56,8 +65,14 @@ export function MissingJobsAlert({ environment }: { environment?: string }) {
{ preservePreviousData: false, showToastOnError: false }
);
const defaultIcon = <EuiIcon type="inspect" color="primary" />;
if (status === FETCH_STATUS.LOADING) {
return <EuiLoadingSpinner />;
}
if (status !== FETCH_STATUS.SUCCESS) {
return null;
return defaultIcon;
}
const isEnvironmentSelected =
@ -65,7 +80,7 @@ export function MissingJobsAlert({ environment }: { environment?: string }) {
// there are jobs for at least one environment
if (!isEnvironmentSelected && data.jobs.length > 0) {
return null;
return defaultIcon;
}
// there are jobs for the selected environment
@ -73,7 +88,7 @@ export function MissingJobsAlert({ environment }: { environment?: string }) {
isEnvironmentSelected &&
data.jobs.some((job) => environment === job.environment)
) {
return null;
return defaultIcon;
}
return (

View file

@ -0,0 +1,72 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiHeaderLink, EuiHeaderLinks } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useParams } from 'react-router-dom';
import { getAlertingCapabilities } from '../../components/alerting/get_alert_capabilities';
import { getAPMHref } from '../../components/shared/Links/apm/APMLink';
import { useApmPluginContext } from '../../hooks/useApmPluginContext';
import { AlertingPopoverAndFlyout } from './alerting_popover_flyout';
import { AnomalyDetectionSetupLink } from './anomaly_detection_setup_link';
export function ActionMenu() {
const { core, plugins } = useApmPluginContext();
const { serviceName } = useParams<{ serviceName?: string }>();
const { search } = window.location;
const { application, http } = core;
const { basePath } = http;
const { capabilities } = application;
const canAccessML = !!capabilities.ml?.canAccessML;
const {
isAlertingAvailable,
canReadAlerts,
canSaveAlerts,
canReadAnomalies,
} = getAlertingCapabilities(plugins, capabilities);
function apmHref(path: string) {
return getAPMHref({ basePath, path, search });
}
function kibanaHref(path: string) {
return basePath.prepend(path);
}
return (
<EuiHeaderLinks>
<EuiHeaderLink
color="primary"
href={apmHref('/settings')}
iconType="gear"
>
{i18n.translate('xpack.apm.settingsLinkLabel', {
defaultMessage: 'Settings',
})}
</EuiHeaderLink>
{isAlertingAvailable && (
<AlertingPopoverAndFlyout
basePath={basePath}
canReadAlerts={canReadAlerts}
canSaveAlerts={canSaveAlerts}
canReadAnomalies={canReadAnomalies}
includeTransactionDuration={serviceName !== undefined}
/>
)}
{canAccessML && <AnomalyDetectionSetupLink />}
<EuiHeaderLink
color="primary"
href={kibanaHref('/app/home#/tutorial/apm')}
iconType="indexOpen"
>
{i18n.translate('xpack.apm.addDataButtonLabel', {
defaultMessage: 'Add data',
})}
</EuiHeaderLink>
</EuiHeaderLinks>
);
}

View file

@ -53,6 +53,7 @@ describe('renderApp', () => {
const params = {
element: document.createElement('div'),
history: createMemoryHistory(),
setHeaderActionMenu: () => {},
};
jest.spyOn(window, 'scrollTo').mockReturnValueOnce(undefined);
createCallApmApi((core.http as unknown) as HttpSetup);

View file

@ -66,21 +66,23 @@ function CsmApp() {
}
export function CsmAppRoot({
appMountParameters,
core,
deps,
history,
config,
corePlugins: { embeddable },
}: {
appMountParameters: AppMountParameters;
core: CoreStart;
deps: ApmPluginSetupDeps;
history: AppMountParameters['history'];
config: ConfigSchema;
corePlugins: ApmPluginStartDeps;
}) {
const { history } = appMountParameters;
const i18nCore = core.i18n;
const plugins = deps;
const apmPluginContextValue = {
appMountParameters,
config,
core,
plugins,
@ -109,10 +111,12 @@ export function CsmAppRoot({
export const renderApp = (
core: CoreStart,
deps: ApmPluginSetupDeps,
{ element, history }: AppMountParameters,
appMountParameters: AppMountParameters,
config: ConfigSchema,
corePlugins: ApmPluginStartDeps
) => {
const { element } = appMountParameters;
createCallApmApi(core.http);
// Automatically creates static index pattern and stores as saved object
@ -123,9 +127,9 @@ export const renderApp = (
ReactDOM.render(
<CsmAppRoot
appMountParameters={appMountParameters}
core={core}
deps={deps}
history={history}
config={config}
corePlugins={corePlugins}
/>,

View file

@ -22,7 +22,10 @@ import {
import { AlertsContextProvider } from '../../../triggers_actions_ui/public';
import { routes } from '../components/app/Main/route_config';
import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange';
import { ApmPluginContext } from '../context/ApmPluginContext';
import {
ApmPluginContext,
ApmPluginContextValue,
} from '../context/ApmPluginContext';
import { LicenseProvider } from '../context/LicenseContext';
import { UrlParamsProvider } from '../context/UrlParamsContext';
import { useBreadcrumbs } from '../hooks/use_breadcrumbs';
@ -64,23 +67,14 @@ function App() {
}
export function ApmAppRoot({
core,
deps,
history,
config,
apmPluginContextValue,
}: {
core: CoreStart;
deps: ApmPluginSetupDeps;
history: AppMountParameters['history'];
config: ConfigSchema;
apmPluginContextValue: ApmPluginContextValue;
}) {
const { appMountParameters, core, plugins } = apmPluginContextValue;
const { history } = appMountParameters;
const i18nCore = core.i18n;
const plugins = deps;
const apmPluginContextValue = {
config,
core,
plugins,
};
return (
<RedirectAppLinks application={core.application}>
<ApmPluginContext.Provider value={apmPluginContextValue}>
@ -117,14 +111,21 @@ export function ApmAppRoot({
export const renderApp = (
core: CoreStart,
deps: ApmPluginSetupDeps,
{ element, history }: AppMountParameters,
setupDeps: ApmPluginSetupDeps,
appMountParameters: AppMountParameters,
config: ConfigSchema
) => {
const { element } = appMountParameters;
const apmPluginContextValue = {
appMountParameters,
config,
core,
plugins: setupDeps,
};
// render APM feedback link in global help menu
setHelpExtension(core);
setReadonlyBadge(core);
createCallApmApi(core.http);
// Automatically creates static index pattern and stores as saved object
@ -134,7 +135,7 @@ export const renderApp = (
});
ReactDOM.render(
<ApmAppRoot core={core} deps={deps} history={history} config={config} />,
<ApmAppRoot apmPluginContextValue={apmPluginContextValue} />,
element
);
return () => {

View file

@ -4,6 +4,9 @@ exports[`Home component should render services 1`] = `
<ContextProvider
value={
Object {
"appMountParameters": Object {
"setHeaderActionMenu": [Function],
},
"config": Object {
"serviceMapEnabled": true,
"ui": Object {
@ -14,6 +17,7 @@ exports[`Home component should render services 1`] = `
"application": Object {
"capabilities": Object {
"apm": Object {},
"ml": Object {},
},
"currentAppId$": Observable {
"_isScalar": false,
@ -87,6 +91,9 @@ exports[`Home component should render traces 1`] = `
<ContextProvider
value={
Object {
"appMountParameters": Object {
"setHeaderActionMenu": [Function],
},
"config": Object {
"serviceMapEnabled": true,
"ui": Object {
@ -97,6 +104,7 @@ exports[`Home component should render traces 1`] = `
"application": Object {
"capabilities": Object {
"apm": Object {},
"ml": Object {},
},
"currentAppId$": Observable {
"_isScalar": false,

View file

@ -4,136 +4,70 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiTabs,
EuiTitle,
} from '@elastic/eui';
import { EuiTabs, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { $ElementType } from 'utility-types';
import { useApmPluginContext } from '../../../hooks/useApmPluginContext';
import { getAlertingCapabilities } from '../../alerting/get_alert_capabilities';
import { ApmHeader } from '../../shared/ApmHeader';
import { EuiTabLink } from '../../shared/EuiTabLink';
import { AnomalyDetectionSetupLink } from '../../shared/Links/apm/AnomalyDetectionSetupLink';
import { ServiceMapLink } from '../../shared/Links/apm/ServiceMapLink';
import { ServiceInventoryLink } from '../../shared/Links/apm/service_inventory_link';
import { SettingsLink } from '../../shared/Links/apm/SettingsLink';
import { TraceOverviewLink } from '../../shared/Links/apm/TraceOverviewLink';
import { SetupInstructionsLink } from '../../shared/Links/SetupInstructionsLink';
import { ServiceMap } from '../ServiceMap';
import { ServiceInventory } from '../service_inventory';
import { TraceOverview } from '../TraceOverview';
import { AlertingPopoverAndFlyout } from './alerting_popover_flyout';
function getHomeTabs({
serviceMapEnabled = true,
}: {
serviceMapEnabled: boolean;
}) {
const homeTabs = [
{
link: (
<ServiceInventoryLink>
{i18n.translate('xpack.apm.home.servicesTabLabel', {
defaultMessage: 'Services',
})}
</ServiceInventoryLink>
),
render: () => <ServiceInventory />,
name: 'services',
},
{
link: (
<TraceOverviewLink>
{i18n.translate('xpack.apm.home.tracesTabLabel', {
defaultMessage: 'Traces',
})}
</TraceOverviewLink>
),
render: () => <TraceOverview />,
name: 'traces',
},
];
if (serviceMapEnabled) {
homeTabs.push({
link: (
<ServiceMapLink>
{i18n.translate('xpack.apm.home.serviceMapTabLabel', {
defaultMessage: 'Service Map',
})}
</ServiceMapLink>
),
render: () => <ServiceMap />,
name: 'service-map',
});
}
return homeTabs;
}
const SETTINGS_LINK_LABEL = i18n.translate('xpack.apm.settingsLinkLabel', {
defaultMessage: 'Settings',
});
const homeTabs = [
{
link: (
<ServiceInventoryLink>
{i18n.translate('xpack.apm.home.servicesTabLabel', {
defaultMessage: 'Services',
})}
</ServiceInventoryLink>
),
render: () => <ServiceInventory />,
name: 'services',
},
{
link: (
<TraceOverviewLink>
{i18n.translate('xpack.apm.home.tracesTabLabel', {
defaultMessage: 'Traces',
})}
</TraceOverviewLink>
),
render: () => <TraceOverview />,
name: 'traces',
},
{
link: (
<ServiceMapLink>
{i18n.translate('xpack.apm.home.serviceMapTabLabel', {
defaultMessage: 'Service Map',
})}
</ServiceMapLink>
),
render: () => <ServiceMap />,
name: 'service-map',
},
];
interface Props {
tab: 'traces' | 'services' | 'service-map';
}
export function Home({ tab }: Props) {
const { config, core, plugins } = useApmPluginContext();
const capabilities = core.application.capabilities;
const canAccessML = !!capabilities.ml?.canAccessML;
const homeTabs = getHomeTabs(config);
const selectedTab = homeTabs.find(
(homeTab) => homeTab.name === tab
) as $ElementType<typeof homeTabs, number>;
const {
isAlertingAvailable,
canReadAlerts,
canSaveAlerts,
canReadAnomalies,
} = getAlertingCapabilities(plugins, core.application.capabilities);
return (
<div>
<ApmHeader>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle size="l">
<h1>APM</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<SettingsLink>
<EuiButtonEmpty size="s" color="primary" iconType="gear">
{SETTINGS_LINK_LABEL}
</EuiButtonEmpty>
</SettingsLink>
</EuiFlexItem>
{isAlertingAvailable && (
<EuiFlexItem grow={false}>
<AlertingPopoverAndFlyout
canReadAlerts={canReadAlerts}
canSaveAlerts={canSaveAlerts}
canReadAnomalies={canReadAnomalies}
/>
</EuiFlexItem>
)}
{canAccessML && (
<EuiFlexItem grow={false}>
<AnomalyDetectionSetupLink />
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<SetupInstructionsLink />
</EuiFlexItem>
</EuiFlexGroup>
<EuiTitle size="l">
<h1>APM</h1>
</EuiTitle>
</ApmHeader>
<EuiTabs>
{homeTabs.map((homeTab) => (

View file

@ -1,197 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import {
EuiButtonEmpty,
EuiContextMenu,
EuiContextMenuPanelDescriptor,
EuiPopover,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useState } from 'react';
import { AlertType } from '../../../../../common/alert_types';
import { useApmPluginContext } from '../../../../hooks/useApmPluginContext';
import { AlertingFlyout } from '../../../alerting/AlertingFlyout';
const alertLabel = i18n.translate(
'xpack.apm.serviceDetails.alertsMenu.alerts',
{ defaultMessage: 'Alerts' }
);
const transactionDurationLabel = i18n.translate(
'xpack.apm.serviceDetails.alertsMenu.transactionDuration',
{ defaultMessage: 'Transaction duration' }
);
const transactionErrorRateLabel = i18n.translate(
'xpack.apm.serviceDetails.alertsMenu.transactionErrorRate',
{ defaultMessage: 'Transaction error rate' }
);
const errorCountLabel = i18n.translate(
'xpack.apm.serviceDetails.alertsMenu.errorCount',
{ defaultMessage: 'Error count' }
);
const createThresholdAlertLabel = i18n.translate(
'xpack.apm.serviceDetails.alertsMenu.createThresholdAlert',
{ defaultMessage: 'Create threshold alert' }
);
const createAnomalyAlertAlertLabel = i18n.translate(
'xpack.apm.serviceDetails.alertsMenu.createAnomalyAlert',
{ defaultMessage: 'Create anomaly alert' }
);
const CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID =
'create_transaction_duration_panel';
const CREATE_TRANSACTION_ERROR_RATE_ALERT_PANEL_ID =
'create_transaction_error_rate_panel';
const CREATE_ERROR_COUNT_ALERT_PANEL_ID = 'create_error_count_panel';
interface Props {
canReadAlerts: boolean;
canSaveAlerts: boolean;
canReadAnomalies: boolean;
}
export function AlertingPopoverAndFlyout(props: Props) {
const { canSaveAlerts, canReadAlerts, canReadAnomalies } = props;
const plugin = useApmPluginContext();
const [popoverOpen, setPopoverOpen] = useState(false);
const [alertType, setAlertType] = useState<AlertType | null>(null);
const button = (
<EuiButtonEmpty
iconType="arrowDown"
iconSide="right"
onClick={() => setPopoverOpen(true)}
>
{alertLabel}
</EuiButtonEmpty>
);
const panels: EuiContextMenuPanelDescriptor[] = [
{
id: 0,
title: alertLabel,
items: [
...(canSaveAlerts
? [
{
name: transactionDurationLabel,
panel: CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID,
},
{
name: transactionErrorRateLabel,
panel: CREATE_TRANSACTION_ERROR_RATE_ALERT_PANEL_ID,
},
{
name: errorCountLabel,
panel: CREATE_ERROR_COUNT_ALERT_PANEL_ID,
},
]
: []),
...(canReadAlerts
? [
{
name: i18n.translate(
'xpack.apm.serviceDetails.alertsMenu.viewActiveAlerts',
{ defaultMessage: 'View active alerts' }
),
href: plugin.core.http.basePath.prepend(
'/app/management/insightsAndAlerting/triggersActions/alerts'
),
icon: 'tableOfContents',
},
]
: []),
],
},
// transaction duration panel
{
id: CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID,
title: transactionDurationLabel,
items: [
// threshold alerts
{
name: createThresholdAlertLabel,
onClick: () => {
setAlertType(AlertType.TransactionDuration);
setPopoverOpen(false);
},
},
// anomaly alerts
...(canReadAnomalies
? [
{
name: createAnomalyAlertAlertLabel,
onClick: () => {
setAlertType(AlertType.TransactionDurationAnomaly);
setPopoverOpen(false);
},
},
]
: []),
],
},
// transaction error rate panel
{
id: CREATE_TRANSACTION_ERROR_RATE_ALERT_PANEL_ID,
title: transactionErrorRateLabel,
items: [
// threshold alerts
{
name: createThresholdAlertLabel,
onClick: () => {
setAlertType(AlertType.TransactionErrorRate);
setPopoverOpen(false);
},
},
],
},
// error alerts panel
{
id: CREATE_ERROR_COUNT_ALERT_PANEL_ID,
title: errorCountLabel,
items: [
{
name: createThresholdAlertLabel,
onClick: () => {
setAlertType(AlertType.ErrorCount);
setPopoverOpen(false);
},
},
],
},
];
return (
<>
<EuiPopover
id="integrations-menu"
button={button}
isOpen={popoverOpen}
closePopover={() => setPopoverOpen(false)}
panelPaddingSize="none"
anchorPosition="downRight"
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
<AlertingFlyout
alertType={alertType}
addFlyoutVisible={!!alertType}
setAddFlyoutVisibility={(visible) => {
if (!visible) {
setAlertType(null);
}
}}
/>
</>
);
}

View file

@ -4,19 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EuiTitle } from '@elastic/eui';
import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { useApmPluginContext } from '../../../hooks/useApmPluginContext';
import { getAlertingCapabilities } from '../../alerting/get_alert_capabilities';
import { ApmHeader } from '../../shared/ApmHeader';
import { AlertingPopoverAndFlyout } from './alerting_popover_flyout';
import { ServiceDetailTabs } from './ServiceDetailTabs';
interface Props extends RouteComponentProps<{ serviceName: string }> {
@ -24,51 +15,15 @@ interface Props extends RouteComponentProps<{ serviceName: string }> {
}
export function ServiceDetails({ match, tab }: Props) {
const { core, plugins } = useApmPluginContext();
const { serviceName } = match.params;
const {
isAlertingAvailable,
canReadAlerts,
canSaveAlerts,
canReadAnomalies,
} = getAlertingCapabilities(plugins, core.application.capabilities);
const ADD_DATA_LABEL = i18n.translate('xpack.apm.addDataButtonLabel', {
defaultMessage: 'Add data',
});
return (
<div>
<ApmHeader>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle size="l">
<h1>{serviceName}</h1>
</EuiTitle>
</EuiFlexItem>
{isAlertingAvailable && (
<EuiFlexItem grow={false}>
<AlertingPopoverAndFlyout
canReadAlerts={canReadAlerts}
canSaveAlerts={canSaveAlerts}
canReadAnomalies={canReadAnomalies}
/>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiButtonEmpty
href={core.http.basePath.prepend('/app/home#/tutorial/apm')}
size="s"
color="primary"
iconType="plusInCircle"
>
{ADD_DATA_LABEL}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
<EuiTitle size="l">
<h1>{serviceName}</h1>
</EuiTitle>
</ApmHeader>
<ServiceDetailTabs serviceName={serviceName} tab={tab} />
</div>
);

View file

@ -14,6 +14,8 @@ import {
import { i18n } from '@kbn/i18n';
import React, { ReactNode } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { HeaderMenuPortal } from '../../../../../observability/public';
import { ActionMenu } from '../../../application/action_menu';
import { useApmPluginContext } from '../../../hooks/useApmPluginContext';
import { getAPMHref } from '../../shared/Links/apm/APMLink';
import { HomeLink } from '../../shared/Links/apm/HomeLink';
@ -23,7 +25,7 @@ interface SettingsProps extends RouteComponentProps<{}> {
}
export function Settings({ children, location }: SettingsProps) {
const { core } = useApmPluginContext();
const { appMountParameters, core } = useApmPluginContext();
const { basePath } = core.http;
const canAccessML = !!core.application.capabilities.ml?.canAccessML;
const { search, pathname } = location;
@ -34,6 +36,11 @@ export function Settings({ children, location }: SettingsProps) {
return (
<>
<HeaderMenuPortal
setHeaderActionMenu={appMountParameters.setHeaderActionMenu}
>
<ActionMenu />
</HeaderMenuPortal>
<HomeLink>
<EuiButtonEmpty size="s" color="primary" iconType="arrowLeft">
{i18n.translate('xpack.apm.settings.returnLinkLabel', {

View file

@ -149,7 +149,7 @@ describe('ServiceInventory', () => {
"Looks like you don't have any APM services installed. Let's add some!"
);
expect(gettingStartedMessage).not.toBeEmpty();
expect(gettingStartedMessage).not.toBeEmptyDOMElement();
});
it('should render empty message, when list is empty and historical data is found', async () => {
@ -165,7 +165,7 @@ describe('ServiceInventory', () => {
await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(1));
const noServicesText = await findByText('No services found');
expect(noServicesText).not.toBeEmpty();
expect(noServicesText).not.toBeEmptyDOMElement();
});
describe('when legacy data is found', () => {

View file

@ -6,13 +6,21 @@
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import React, { ReactNode } from 'react';
import { HeaderMenuPortal } from '../../../../../observability/public';
import { ActionMenu } from '../../../application/action_menu';
import { useApmPluginContext } from '../../../hooks/useApmPluginContext';
import { DatePicker } from '../DatePicker';
import { EnvironmentFilter } from '../EnvironmentFilter';
import { KueryBar } from '../KueryBar';
export function ApmHeader({ children }: { children: ReactNode }) {
const { setHeaderActionMenu } = useApmPluginContext().appMountParameters;
return (
<>
<HeaderMenuPortal setHeaderActionMenu={setHeaderActionMenu}>
<ActionMenu />
</HeaderMenuPortal>
<EuiFlexGroup alignItems="center" gutterSize="s" wrap={true}>
<EuiFlexItem>{children}</EuiFlexItem>
<EuiFlexItem grow={false}>

View file

@ -34,7 +34,7 @@ export function SetupInstructionsLink({
{SETUP_INSTRUCTIONS_LABEL}
</EuiButton>
) : (
<EuiButtonEmpty size="s" color="primary" iconType="plusInCircle">
<EuiButtonEmpty size="s" color="primary" iconType="indexOpen">
{ADD_DATA_LABEL}
</EuiButtonEmpty>
)}

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;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { APMLink, APMLinkExtendProps } from './APMLink';
function SettingsLink(props: APMLinkExtendProps) {
return <APMLink path="/settings" {...props} />;
}
export { SettingsLink };

View file

@ -38,6 +38,7 @@ const mockCore = {
application: {
capabilities: {
apm: {},
ml: {},
},
currentAppId$: new Observable(),
navigateToUrl: (url: string) => {},
@ -93,7 +94,13 @@ const mockPlugin = {
},
},
};
const mockAppMountParameters = {
setHeaderActionMenu: () => {},
};
export const mockApmPluginContextValue = {
appMountParameters: mockAppMountParameters,
config: mockConfig,
core: mockCore,
plugins: mockPlugin,

View file

@ -4,12 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { CoreStart } from 'kibana/public';
import { AppMountParameters, CoreStart } from 'kibana/public';
import { createContext } from 'react';
import { ConfigSchema } from '../../';
import { ApmPluginSetupDeps } from '../../plugin';
export interface ApmPluginContextValue {
appMountParameters: AppMountParameters;
config: ConfigSchema;
core: CoreStart;
plugins: ApmPluginSetupDeps;

View file

@ -17,7 +17,11 @@ describe('renderApp', () => {
} as unknown) as ObservabilityPluginSetupDeps;
const core = ({
application: { currentAppId$: new Observable(), navigateToUrl: () => {} },
chrome: { docTitle: { change: () => {} }, setBreadcrumbs: () => {} },
chrome: {
docTitle: { change: () => {} },
setBreadcrumbs: () => {},
setHelpExtension: () => {},
},
i18n: { Context: ({ children }: { children: React.ReactNode }) => children },
uiSettings: { get: () => false },
http: { basePath: { prepend: (path: string) => path } },
@ -25,6 +29,7 @@ describe('renderApp', () => {
const params = ({
element: window.document.createElement('div'),
history: createMemoryHistory(),
setHeaderActionMenu: () => {},
} as unknown) as AppMountParameters;
expect(() => {

View file

@ -16,8 +16,8 @@ import { EuiThemeProvider } from '../../../xpack_legacy/common';
import { PluginContext } from '../context/plugin_context';
import { usePluginContext } from '../hooks/use_plugin_context';
import { useRouteParams } from '../hooks/use_route_params';
import { Breadcrumbs, routes } from '../routes';
import { ObservabilityPluginSetupDeps } from '../plugin';
import { Breadcrumbs, routes } from '../routes';
const observabilityLabelBreadcrumb = {
text: i18n.translate('xpack.observability.observability.breadcrumb.', {
@ -58,14 +58,22 @@ function App() {
export const renderApp = (
core: CoreStart,
plugins: ObservabilityPluginSetupDeps,
{ element, history }: AppMountParameters
appMountParameters: AppMountParameters
) => {
const { element, history } = appMountParameters;
const i18nCore = core.i18n;
const isDarkMode = core.uiSettings.get('theme:darkMode');
core.chrome.setHelpExtension({
appName: i18n.translate('xpack.observability.feedbackMenu.appName', {
defaultMessage: 'Observability',
}),
links: [{ linkType: 'discuss', href: 'https://ela.st/observability-discuss' }],
});
ReactDOM.render(
<KibanaContextProvider services={{ ...core, ...plugins }}>
<PluginContext.Provider value={{ core, plugins }}>
<PluginContext.Provider value={{ appMountParameters, core, plugins }}>
<Router history={history}>
<EuiThemeProvider darkMode={isDarkMode}>
<i18nCore.Context>

View file

@ -8,16 +8,9 @@ import { render } from '../../../utils/test_helper';
import { Header } from './';
describe('Header', () => {
it('renders without add data button', () => {
const { getByText, queryAllByText, getByTestId } = render(<Header color="#fff" />);
it('renders', () => {
const { getByText, getByTestId } = render(<Header color="#fff" />);
expect(getByTestId('observability-logo')).toBeInTheDocument();
expect(getByText('Observability')).toBeInTheDocument();
expect(queryAllByText('Add data')).toEqual([]);
});
it('renders with add data button', () => {
const { getByText, getByTestId } = render(<Header color="#fff" showAddData />);
expect(getByTestId('observability-logo')).toBeInTheDocument();
expect(getByText('Observability')).toBeInTheDocument();
expect(getByText('Add data')).toBeInTheDocument();
});
});

View file

@ -5,17 +5,19 @@
*/
import {
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiHeaderLink,
EuiHeaderLinks,
EuiIcon,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import React, { ReactNode } from 'react';
import styled from 'styled-components';
import { usePluginContext } from '../../../hooks/use_plugin_context';
import { HeaderMenuPortal } from '../../shared/header_menu_portal';
const Container = styled.div<{ color: string }>`
background: ${(props) => props.color};
@ -32,54 +34,48 @@ const Wrapper = styled.div<{ restrictWidth?: number }>`
interface Props {
color: string;
showAddData?: boolean;
datePicker?: ReactNode;
restrictWidth?: number;
showGiveFeedback?: boolean;
}
export function Header({
color,
restrictWidth,
showAddData = false,
showGiveFeedback = false,
}: Props) {
const { core } = usePluginContext();
export function Header({ color, datePicker = null, restrictWidth }: Props) {
const { appMountParameters, core } = usePluginContext();
const { setHeaderActionMenu } = appMountParameters;
const { prepend } = core.http.basePath;
return (
<Container color={color}>
<HeaderMenuPortal setHeaderActionMenu={setHeaderActionMenu}>
<EuiHeaderLinks>
<EuiHeaderLink
color="primary"
href={prepend('/app/home#/tutorial_directory/logging')}
iconType="indexOpen"
>
{i18n.translate('xpack.observability.home.addData', { defaultMessage: 'Add data' })}
</EuiHeaderLink>
</EuiHeaderLinks>
</HeaderMenuPortal>
<Wrapper restrictWidth={restrictWidth}>
<EuiSpacer size="l" />
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiIcon type="logoObservability" size="xxl" data-test-subj="observability-logo" />
</EuiFlexItem>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
<EuiTitle size="m">
<h1>
{i18n.translate('xpack.observability.home.title', {
defaultMessage: 'Observability',
})}
</h1>
</EuiTitle>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiIcon type="logoObservability" size="xxl" data-test-subj="observability-logo" />
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle size="m">
<h1>
{i18n.translate('xpack.observability.home.title', {
defaultMessage: 'Observability',
})}
</h1>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
{showGiveFeedback && (
<EuiFlexItem style={{ alignItems: 'flex-end' }} grow={false}>
<EuiButtonEmpty href={'https://ela.st/observability-discuss'} iconType="popout">
{i18n.translate('xpack.observability.home.feedback', {
defaultMessage: 'Give us feedback',
})}
</EuiButtonEmpty>
</EuiFlexItem>
)}
{showAddData && (
<EuiFlexItem style={{ alignItems: 'flex-end' }} grow={false}>
<EuiButtonEmpty
href={core.http.basePath.prepend('/app/home#/tutorial_directory/logging')}
iconType="plusInCircle"
>
{i18n.translate('xpack.observability.home.addData', { defaultMessage: 'Add data' })}
</EuiButtonEmpty>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>{datePicker}</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="l" />
</Wrapper>

View file

@ -5,7 +5,7 @@
*/
import { EuiPage, EuiPageBody, EuiPageProps } from '@elastic/eui';
import React from 'react';
import React, { ReactNode } from 'react';
import styled from 'styled-components';
import { Header } from '../header/index';
@ -20,30 +20,23 @@ const Container = styled.div<{ color?: string }>`
`;
interface Props {
datePicker?: ReactNode;
headerColor: string;
bodyColor: string;
children?: React.ReactNode;
children?: ReactNode;
restrictWidth?: number;
showAddData?: boolean;
showGiveFeedback?: boolean;
}
export function WithHeaderLayout({
datePicker,
headerColor,
bodyColor,
children,
restrictWidth,
showAddData,
showGiveFeedback,
}: Props) {
return (
<Container color={bodyColor}>
<Header
color={headerColor}
restrictWidth={restrictWidth}
showAddData={showAddData}
showGiveFeedback={showGiveFeedback}
/>
<Header color={headerColor} datePicker={datePicker} restrictWidth={restrictWidth} />
<Page restrictWidth={restrictWidth}>
<EuiPageBody>{children}</EuiPageBody>
</Page>

View file

@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { ReactNode, useEffect, useMemo } from 'react';
import { createPortalNode, InPortal, OutPortal } from 'react-reverse-portal';
import { AppMountParameters } from '../../../../../../src/core/public';
import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public';
interface HeaderMenuPortalProps {
children: ReactNode;
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
}
export function HeaderMenuPortal({ children, setHeaderActionMenu }: HeaderMenuPortalProps) {
const portalNode = useMemo(() => createPortalNode(), []);
useEffect(() => {
let unmount = () => {};
setHeaderActionMenu((element) => {
const mount = toMountPoint(<OutPortal node={portalNode} />);
unmount = mount(element);
return unmount;
});
return () => {
portalNode.unmount();
unmount();
};
}, [portalNode, setHeaderActionMenu]);
return <InPortal node={portalNode}>{children}</InPortal>;
}

View file

@ -5,10 +5,11 @@
*/
import { createContext } from 'react';
import { CoreStart } from 'kibana/public';
import { AppMountParameters, CoreStart } from 'kibana/public';
import { ObservabilityPluginSetupDeps } from '../plugin';
export interface PluginContextValue {
appMountParameters: AppMountParameters;
core: CoreStart;
plugins: ObservabilityPluginSetupDeps;
}

View file

@ -8,7 +8,7 @@ import { useLocation } from 'react-router-dom';
import { useMemo } from 'react';
import { parse } from 'query-string';
import { UI_SETTINGS, useKibanaUISettings } from './use_kibana_ui_settings';
import { TimePickerTime } from '../components/shared/data_picker';
import { TimePickerTime } from '../components/shared/date_picker';
import { getAbsoluteTime } from '../utils/date';
const getParsedParams = (search: string) => {

View file

@ -6,7 +6,7 @@
import { PluginInitializerContext, PluginInitializer } from 'kibana/public';
import { Plugin, ObservabilityPluginSetup, ObservabilityPluginStart } from './plugin';
export { HeaderMenuPortal } from './components/shared/header_menu_portal';
export { ObservabilityPluginSetup, ObservabilityPluginStart };
export const plugin: PluginInitializer<ObservabilityPluginSetup, ObservabilityPluginStart> = (

View file

@ -3,28 +3,28 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import React, { useContext } from 'react';
import { ThemeContext } from 'styled-components';
import { useTrackPageview, UXHasDataResponse } from '../..';
import { EmptySection } from '../../components/app/empty_section';
import { WithHeaderLayout } from '../../components/app/layout/with_header';
import { NewsFeed } from '../../components/app/news_feed';
import { Resources } from '../../components/app/resources';
import { AlertsSection } from '../../components/app/section/alerts';
import { DatePicker, TimePickerTime } from '../../components/shared/data_picker';
import { NewsFeed } from '../../components/app/news_feed';
import { DatePicker, TimePickerTime } from '../../components/shared/date_picker';
import { fetchHasData } from '../../data_handler';
import { FETCH_STATUS, useFetcher } from '../../hooks/use_fetcher';
import { UI_SETTINGS, useKibanaUISettings } from '../../hooks/use_kibana_ui_settings';
import { usePluginContext } from '../../hooks/use_plugin_context';
import { RouteParams } from '../../routes';
import { getNewsFeed } from '../../services/get_news_feed';
import { getObservabilityAlerts } from '../../services/get_observability_alerts';
import { getAbsoluteTime } from '../../utils/date';
import { getBucketSize } from '../../utils/get_bucket_size';
import { DataSections } from './data_sections';
import { getEmptySections } from './empty_section';
import { LoadingObservability } from './loading_observability';
import { getNewsFeed } from '../../services/get_news_feed';
import { DataSections } from './data_sections';
import { useTrackPageview, UXHasDataResponse } from '../..';
interface Props {
routeParams: RouteParams<'/overview'>;
@ -101,27 +101,15 @@ export function OverviewPage({ routeParams }: Props) {
<WithHeaderLayout
headerColor={theme.eui.euiColorEmptyShade}
bodyColor={theme.eui.euiPageBackgroundColor}
showAddData
showGiveFeedback
datePicker={
<DatePicker
rangeFrom={relativeTime.start}
rangeTo={relativeTime.end}
refreshInterval={refreshInterval}
refreshPaused={refreshPaused}
/>
}
>
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<DatePicker
rangeFrom={relativeTime.start}
rangeTo={relativeTime.end}
refreshInterval={refreshInterval}
refreshPaused={refreshPaused}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule
style={{
width: 'auto', // full width
margin: '24px -24px', // counteract page paddings
}}
/>
<EuiFlexGroup>
<EuiFlexItem grow={6}>
{/* Data sections */}

View file

@ -27,8 +27,6 @@ export function LoadingObservability() {
<WithHeaderLayout
headerColor={theme.eui.euiColorEmptyShade}
bodyColor={theme.eui.euiPageBackgroundColor}
showAddData
showGiveFeedback
>
<CentralizedFlexGroup>
<EuiFlexItem grow={false}>

View file

@ -6,7 +6,7 @@
import { makeDecorator } from '@storybook/addons';
import { storiesOf } from '@storybook/react';
import { CoreStart } from 'kibana/public';
import { AppMountParameters, CoreStart } from 'kibana/public';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { UI_SETTINGS } from '../../../../../../src/plugins/data/public';
@ -39,6 +39,9 @@ const withCore = makeDecorator({
<MemoryRouter>
<PluginContext.Provider
value={{
appMountParameters: ({
setHeaderActionMenu: () => {},
} as unknown) as AppMountParameters,
core: options as CoreStart,
plugins: ({
data: {

View file

@ -3,14 +3,18 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { render as testLibRender } from '@testing-library/react';
import { CoreStart } from 'kibana/public';
import { AppMountParameters, CoreStart } from 'kibana/public';
import React from 'react';
import { IntlProvider } from 'react-intl';
import { of } from 'rxjs';
import { PluginContext } from '../context/plugin_context';
import { EuiThemeProvider } from '../typings';
import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public';
import translations from '../../../translations/translations/ja-JP.json';
import { PluginContext } from '../context/plugin_context';
import { ObservabilityPluginSetupDeps } from '../plugin';
import { EuiThemeProvider } from '../typings';
const appMountParameters = ({ setHeaderActionMenu: () => {} } as unknown) as AppMountParameters;
export const core = ({
http: {
@ -30,10 +34,12 @@ const plugins = ({
export const render = (component: React.ReactNode) => {
return testLibRender(
<KibanaContextProvider services={{ ...core }}>
<PluginContext.Provider value={{ core, plugins }}>
<EuiThemeProvider>{component}</EuiThemeProvider>
</PluginContext.Provider>
</KibanaContextProvider>
<IntlProvider locale="en-US" messages={translations.messages}>
<KibanaContextProvider services={{ ...core }}>
<PluginContext.Provider value={{ appMountParameters, core, plugins }}>
<EuiThemeProvider>{component}</EuiThemeProvider>
</PluginContext.Provider>
</KibanaContextProvider>
</IntlProvider>
);
};

View file

@ -5019,13 +5019,6 @@
"xpack.apm.rum.visitorBreakdownMap.pageLoadDurationByRegion": "地域別ページ読み込み時間(平均)",
"xpack.apm.searchInput.filter": "フィルター...",
"xpack.apm.selectPlaceholder": "オプションを選択:",
"xpack.apm.serviceDetails.alertsMenu.alerts": "アラート",
"xpack.apm.serviceDetails.alertsMenu.createAnomalyAlert": "異常アラートを作成",
"xpack.apm.serviceDetails.alertsMenu.createThresholdAlert": "しきい値アラートを作成",
"xpack.apm.serviceDetails.alertsMenu.errorCount": "エラー数",
"xpack.apm.serviceDetails.alertsMenu.transactionDuration": "トランザクション期間",
"xpack.apm.serviceDetails.alertsMenu.transactionErrorRate": "トランザクションエラー率",
"xpack.apm.serviceDetails.alertsMenu.viewActiveAlerts": "アクティブアラートを表示",
"xpack.apm.serviceDetails.errorsTabLabel": "エラー",
"xpack.apm.serviceDetails.metrics.cpuUsageChartTitle": "CPU 使用状況",
"xpack.apm.serviceDetails.metrics.errorOccurrencesChartTitle": "エラーのオカレンス",
@ -15156,7 +15149,6 @@
"xpack.observability.featureCatalogueTitle": "オブザーバビリティ",
"xpack.observability.home.addData": "データの追加",
"xpack.observability.home.breadcrumb": "概要",
"xpack.observability.home.feedback": "フィードバックを送信する",
"xpack.observability.home.getStatedButton": "使ってみる",
"xpack.observability.home.sectionsubtitle": "ログ、メトリック、トレースを大規模に、1つのスタックにまとめて、環境内のあらゆる場所で生じるイベントの監視、分析、対応を行います。",
"xpack.observability.home.sectionTitle": "エコシステム全体の一元的な可視性",

View file

@ -5023,13 +5023,6 @@
"xpack.apm.rum.visitorBreakdownMap.pageLoadDurationByRegion": "按区域列出的页面加载持续时间(平均值)",
"xpack.apm.searchInput.filter": "筛选...",
"xpack.apm.selectPlaceholder": "选择选项:",
"xpack.apm.serviceDetails.alertsMenu.alerts": "告警",
"xpack.apm.serviceDetails.alertsMenu.createAnomalyAlert": "创建异常告警",
"xpack.apm.serviceDetails.alertsMenu.createThresholdAlert": "创建阈值告警",
"xpack.apm.serviceDetails.alertsMenu.errorCount": "错误计数",
"xpack.apm.serviceDetails.alertsMenu.transactionDuration": "事务持续时间",
"xpack.apm.serviceDetails.alertsMenu.transactionErrorRate": "事务错误率",
"xpack.apm.serviceDetails.alertsMenu.viewActiveAlerts": "查看活动的告警",
"xpack.apm.serviceDetails.errorsTabLabel": "错误",
"xpack.apm.serviceDetails.metrics.cpuUsageChartTitle": "CPU 使用",
"xpack.apm.serviceDetails.metrics.errorOccurrencesChartTitle": "错误发生次数",
@ -15174,7 +15167,6 @@
"xpack.observability.featureCatalogueTitle": "可观测性",
"xpack.observability.home.addData": "添加数据",
"xpack.observability.home.breadcrumb": "概览",
"xpack.observability.home.feedback": "提供反馈",
"xpack.observability.home.getStatedButton": "开始使用",
"xpack.observability.home.sectionsubtitle": "通过根据需要将日志、指标和跟踪都置于单个堆栈上,来监测、分析和响应环境中任何位置发生的事件。",
"xpack.observability.home.sectionTitle": "整个生态系统的统一可见性",