mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
Put APM links into header action menu (#82292)
This commit is contained in:
parent
81b3a48c2c
commit
5ab41f5845
34 changed files with 384 additions and 553 deletions
|
@ -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
|
||||
? [
|
|
@ -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,
|
|
@ -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 (
|
72
x-pack/plugins/apm/public/application/action_menu/index.tsx
Normal file
72
x-pack/plugins/apm/public/application/action_menu/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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}
|
||||
/>,
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) => (
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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 };
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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> = (
|
||||
|
|
|
@ -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 */}
|
||||
|
|
|
@ -27,8 +27,6 @@ export function LoadingObservability() {
|
|||
<WithHeaderLayout
|
||||
headerColor={theme.eui.euiColorEmptyShade}
|
||||
bodyColor={theme.eui.euiPageBackgroundColor}
|
||||
showAddData
|
||||
showGiveFeedback
|
||||
>
|
||||
<CentralizedFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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": "エコシステム全体の一元的な可視性",
|
||||
|
|
|
@ -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": "整个生态系统的统一可见性",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue