Allow editing of APM rules (#106598)

By pulling out most of the things that depend on the URL into where we open the flyout and passing them in as metadata props, we can make it so editing rules while in Stack Management.

You cannot edit a rule's service name, transaction type, or environment once it has been created (#106786 has been created to allow editing of these other values), but all other values can be edited.

In order for useFetcher to work outside of the APM plugin, it has been changed to use useKibana instead of useApmContext for toast notifications. The notifications API from useKibana is slightly different and allows passing a react element instead of a mount point as the body.

Fixes #76316.
This commit is contained in:
Nathan L Smith 2021-07-27 10:05:39 -05:00 committed by GitHub
parent 76989b57eb
commit a6211f86f2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 206 additions and 224 deletions

View file

@ -14,6 +14,10 @@ import {
import { getInitialAlertValues } from '../get_initial_alert_values'; import { getInitialAlertValues } from '../get_initial_alert_values';
import { ApmPluginStartDeps } from '../../../plugin'; import { ApmPluginStartDeps } from '../../../plugin';
import { useServiceName } from '../../../hooks/use_service_name'; import { useServiceName } from '../../../hooks/use_service_name';
import { useApmParams } from '../../../hooks/use_apm_params';
import { AlertMetadata } from '../helper';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
interface Props { interface Props {
addFlyoutVisible: boolean; addFlyoutVisible: boolean;
@ -23,7 +27,17 @@ interface Props {
export function AlertingFlyout(props: Props) { export function AlertingFlyout(props: Props) {
const { addFlyoutVisible, setAddFlyoutVisibility, alertType } = props; const { addFlyoutVisible, setAddFlyoutVisibility, alertType } = props;
const serviceName = useServiceName(); const serviceName = useServiceName();
const { query } = useApmParams('/*');
const {
urlParams: { start, end },
} = useUrlParams();
const environment =
'environment' in query ? query.environment : ENVIRONMENT_ALL.value;
const transactionType =
'transactionType' in query ? query.transactionType : undefined;
const { services } = useKibana<ApmPluginStartDeps>(); const { services } = useKibana<ApmPluginStartDeps>();
const initialValues = getInitialAlertValues(alertType, serviceName); const initialValues = getInitialAlertValues(alertType, serviceName);
@ -40,9 +54,26 @@ export function AlertingFlyout(props: Props) {
alertTypeId: alertType, alertTypeId: alertType,
canChangeTrigger: false, canChangeTrigger: false,
initialValues, initialValues,
metadata: {
environment,
serviceName,
transactionType,
start,
end,
} as AlertMetadata,
}), }),
/* eslint-disable-next-line react-hooks/exhaustive-deps */ /* eslint-disable-next-line react-hooks/exhaustive-deps */
[alertType, onCloseAddFlyout, services.triggersActionsUi] [
alertType,
environment,
onCloseAddFlyout,
services.triggersActionsUi,
serviceName,
transactionType,
environment,
start,
end,
]
); );
return <>{addFlyoutVisible && addAlertFlyout}</>; return <>{addFlyoutVisible && addAlertFlyout}</>;
} }

View file

@ -5,44 +5,47 @@
* 2.0. * 2.0.
*/ */
import React from 'react'; import React, { useState } from 'react';
import { MemoryRouter } from 'react-router-dom'; import { AlertParams, ErrorCountAlertTrigger } from '.';
import { ErrorCountAlertTrigger } from '.'; import { CoreStart } from '../../../../../../../src/core/public';
import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public';
import {
mockApmPluginContextValue, const KibanaReactContext = createKibanaReactContext(({
MockApmPluginContextWrapper, notifications: { toasts: { add: () => {} } },
} from '../../../context/apm_plugin/mock_apm_plugin_context'; } as unknown) as Partial<CoreStart>);
export default { export default {
title: 'app/ErrorCountAlertTrigger', title: 'alerting/ErrorCountAlertTrigger',
component: ErrorCountAlertTrigger, component: ErrorCountAlertTrigger,
decorators: [ decorators: [
(Story: React.ComponentClass) => ( (Story: React.ComponentClass) => (
<MockApmPluginContextWrapper <KibanaReactContext.Provider>
value={(mockApmPluginContextValue as unknown) as ApmPluginContextValue} <div style={{ width: 400 }}>
> <Story />
<MemoryRouter> </div>
<div style={{ width: 400 }}> </KibanaReactContext.Provider>
<Story />
</div>
</MemoryRouter>
</MockApmPluginContextWrapper>
), ),
], ],
}; };
export function Example() { export function Example() {
const params = { const [params, setParams] = useState<AlertParams>({
serviceName: 'testServiceName',
environment: 'testEnvironment',
threshold: 2, threshold: 2,
window: '5m', windowSize: 5,
}; windowUnit: 'm',
});
function setAlertParams(property: string, value: any) {
setParams({ ...params, [property]: value });
}
return ( return (
<ErrorCountAlertTrigger <ErrorCountAlertTrigger
alertParams={params as any} alertParams={params}
setAlertParams={() => undefined} setAlertParams={setAlertParams}
setAlertProperty={() => undefined} setAlertProperty={() => {}}
/> />
); );
} }

View file

@ -6,19 +6,16 @@
*/ */
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { defaults, omit } from 'lodash';
import React from 'react'; import React from 'react';
import { defaults } from 'lodash';
import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public';
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
import { asInteger } from '../../../../common/utils/formatters'; import { asInteger } from '../../../../common/utils/formatters';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher';
import { useFetcher } from '../../../hooks/use_fetcher'; import { useFetcher } from '../../../hooks/use_fetcher';
import { ChartPreview } from '../chart_preview'; import { ChartPreview } from '../chart_preview';
import { EnvironmentField, IsAboveField, ServiceField } from '../fields'; import { EnvironmentField, IsAboveField, ServiceField } from '../fields';
import { getAbsoluteTimeRange } from '../helper'; import { AlertMetadata, getAbsoluteTimeRange } from '../helper';
import { ServiceAlertTrigger } from '../service_alert_trigger'; import { ServiceAlertTrigger } from '../service_alert_trigger';
import { useServiceName } from '../../../hooks/use_service_name';
export interface AlertParams { export interface AlertParams {
windowSize: number; windowSize: number;
@ -30,33 +27,26 @@ export interface AlertParams {
interface Props { interface Props {
alertParams: AlertParams; alertParams: AlertParams;
metadata?: AlertMetadata;
setAlertParams: (key: string, value: any) => void; setAlertParams: (key: string, value: any) => void;
setAlertProperty: (key: string, value: any) => void; setAlertProperty: (key: string, value: any) => void;
} }
export function ErrorCountAlertTrigger(props: Props) { export function ErrorCountAlertTrigger(props: Props) {
const { setAlertParams, setAlertProperty, alertParams } = props; const { alertParams, metadata, setAlertParams, setAlertProperty } = props;
const serviceNameFromUrl = useServiceName();
const { urlParams } = useUrlParams();
const { start, end, environment: environmentFromUrl } = urlParams;
const { environmentOptions } = useEnvironmentsFetcher({ const { environmentOptions } = useEnvironmentsFetcher({
serviceName: serviceNameFromUrl, serviceName: metadata?.serviceName,
start, start: metadata?.start,
end, end: metadata?.end,
}); });
const params = defaults( const params = defaults(
{ { ...omit(metadata, ['start', 'end']), ...alertParams },
...alertParams,
},
{ {
threshold: 25, threshold: 25,
windowSize: 1, windowSize: 1,
windowUnit: 'm', windowUnit: 'm',
environment: environmentFromUrl || ENVIRONMENT_ALL.value,
serviceName: serviceNameFromUrl,
} }
); );

View file

@ -36,13 +36,17 @@ export function EnvironmentField({
options: EuiSelectOption[]; options: EuiSelectOption[];
onChange: (event: React.ChangeEvent<HTMLSelectElement>) => void; onChange: (event: React.ChangeEvent<HTMLSelectElement>) => void;
}) { }) {
const title = i18n.translate('xpack.apm.alerting.fields.environment', {
defaultMessage: 'Environment',
});
// "1" means "All" is the only option and we should not show a select.
if (options.length === 1) {
return <EuiExpression description={title} value={currentValue} />;
}
return ( return (
<PopoverExpression <PopoverExpression value={getEnvironmentLabel(currentValue)} title={title}>
value={getEnvironmentLabel(currentValue)}
title={i18n.translate('xpack.apm.alerting.fields.environment', {
defaultMessage: 'Environment',
})}
>
<EuiSelect <EuiSelect
defaultValue={currentValue} defaultValue={currentValue}
options={options} options={options}

View file

@ -7,6 +7,14 @@
import datemath from '@elastic/datemath'; import datemath from '@elastic/datemath';
export interface AlertMetadata {
environment: string;
serviceName?: string;
transactionType?: string;
start?: string;
end?: string;
}
export function getAbsoluteTimeRange(windowSize: number, windowUnit: string) { export function getAbsoluteTimeRange(windowSize: number, windowUnit: string) {
const now = new Date().toISOString(); const now = new Date().toISOString();

View file

@ -71,7 +71,7 @@ export function registerApmAlerts(
validate: () => ({ validate: () => ({
errors: [], errors: [],
}), }),
requiresAppContext: true, requiresAppContext: false,
defaultActionMessage: i18n.translate( defaultActionMessage: i18n.translate(
'xpack.apm.alertTypes.errorCount.defaultActionMessage', 'xpack.apm.alertTypes.errorCount.defaultActionMessage',
{ {
@ -126,7 +126,7 @@ export function registerApmAlerts(
validate: () => ({ validate: () => ({
errors: [], errors: [],
}), }),
requiresAppContext: true, requiresAppContext: false,
defaultActionMessage: i18n.translate( defaultActionMessage: i18n.translate(
'xpack.apm.alertTypes.transactionDuration.defaultActionMessage', 'xpack.apm.alertTypes.transactionDuration.defaultActionMessage',
{ {
@ -182,7 +182,7 @@ export function registerApmAlerts(
validate: () => ({ validate: () => ({
errors: [], errors: [],
}), }),
requiresAppContext: true, requiresAppContext: false,
defaultActionMessage: i18n.translate( defaultActionMessage: i18n.translate(
'xpack.apm.alertTypes.transactionErrorRate.defaultActionMessage', 'xpack.apm.alertTypes.transactionErrorRate.defaultActionMessage',
{ {
@ -237,7 +237,7 @@ export function registerApmAlerts(
validate: () => ({ validate: () => ({
errors: [], errors: [],
}), }),
requiresAppContext: true, requiresAppContext: false,
defaultActionMessage: i18n.translate( defaultActionMessage: i18n.translate(
'xpack.apm.alertTypes.transactionDurationAnomaly.defaultActionMessage', 'xpack.apm.alertTypes.transactionDurationAnomaly.defaultActionMessage',
{ {

View file

@ -6,69 +6,51 @@
*/ */
import { Story } from '@storybook/react'; import { Story } from '@storybook/react';
import { cloneDeep, merge } from 'lodash'; import React, { ComponentType, useState } from 'react';
import React, { ComponentType } from 'react'; import { AlertParams, TransactionDurationAlertTrigger } from '.';
import { MemoryRouter, Route } from 'react-router-dom'; import { CoreStart } from '../../../../../../../src/core/public';
import { TransactionDurationAlertTrigger } from '.'; import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public';
import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context';
import { const KibanaReactContext = createKibanaReactContext(({
mockApmPluginContextValue, notifications: { toasts: { add: () => {} } },
MockApmPluginContextWrapper, } as unknown) as Partial<CoreStart>);
} from '../../../context/apm_plugin/mock_apm_plugin_context';
import { ApmServiceContextProvider } from '../../../context/apm_service/apm_service_context';
import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider';
export default { export default {
title: 'alerting/TransactionDurationAlertTrigger', title: 'alerting/TransactionDurationAlertTrigger',
component: TransactionDurationAlertTrigger, component: TransactionDurationAlertTrigger,
decorators: [ decorators: [
(StoryComponent: ComponentType) => { (StoryComponent: ComponentType) => {
const contextMock = (merge(cloneDeep(mockApmPluginContextValue), {
core: {
http: {
get: (endpoint: string) => {
if (endpoint === '/api/apm/environments') {
return Promise.resolve({ environments: ['production'] });
} else {
return Promise.resolve({
transactionTypes: ['request'],
});
}
},
},
},
}) as unknown) as ApmPluginContextValue;
return ( return (
<div style={{ width: 400 }}> <KibanaReactContext.Provider>
<MemoryRouter initialEntries={['/services/test-service-name']}> <div style={{ width: 400 }}>
<Route path="/services/:serviceName"> <StoryComponent />
<MockApmPluginContextWrapper value={contextMock}> </div>
<MockUrlParamsContextProvider> </KibanaReactContext.Provider>
<ApmServiceContextProvider>
<StoryComponent />
</ApmServiceContextProvider>
</MockUrlParamsContextProvider>
</MockApmPluginContextWrapper>
</Route>
</MemoryRouter>
</div>
); );
}, },
], ],
}; };
export const Example: Story = () => { export const Example: Story = () => {
const params = { const [params, setParams] = useState<AlertParams>({
threshold: 1500,
aggregationType: 'avg' as const, aggregationType: 'avg' as const,
window: '5m', environment: 'testEnvironment',
}; serviceName: 'testServiceName',
threshold: 1500,
transactionType: 'testTransactionType',
windowSize: 5,
windowUnit: 'm',
});
function setAlertParams(property: string, value: any) {
setParams({ ...params, [property]: value });
}
return ( return (
<TransactionDurationAlertTrigger <TransactionDurationAlertTrigger
alertParams={params as any} alertParams={params}
setAlertParams={() => undefined} setAlertParams={setAlertParams}
setAlertProperty={() => undefined} setAlertProperty={() => {}}
/> />
); );
}; };

View file

@ -7,18 +7,16 @@
import { EuiSelect } from '@elastic/eui'; import { EuiSelect } from '@elastic/eui';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { map, defaults } from 'lodash'; import { defaults, map, omit } from 'lodash';
import React from 'react'; import React from 'react';
import { CoreStart } from '../../../../../../../src/core/public';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public';
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
import { getDurationFormatter } from '../../../../common/utils/formatters'; import { getDurationFormatter } from '../../../../common/utils/formatters';
import { getTransactionType } from '../../../context/apm_service/apm_service_context';
import { useServiceAgentNameFetcher } from '../../../context/apm_service/use_service_agent_name_fetcher';
import { useServiceTransactionTypesFetcher } from '../../../context/apm_service/use_service_transaction_types_fetcher'; import { useServiceTransactionTypesFetcher } from '../../../context/apm_service/use_service_transaction_types_fetcher';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher';
import { useFetcher } from '../../../hooks/use_fetcher'; import { useFetcher } from '../../../hooks/use_fetcher';
import { useServiceName } from '../../../hooks/use_service_name'; import { createCallApmApi } from '../../../services/rest/createCallApmApi';
import { import {
getMaxY, getMaxY,
getResponseTimeTickFormatter, getResponseTimeTickFormatter,
@ -30,18 +28,18 @@ import {
ServiceField, ServiceField,
TransactionTypeField, TransactionTypeField,
} from '../fields'; } from '../fields';
import { getAbsoluteTimeRange } from '../helper'; import { AlertMetadata, getAbsoluteTimeRange } from '../helper';
import { ServiceAlertTrigger } from '../service_alert_trigger'; import { ServiceAlertTrigger } from '../service_alert_trigger';
import { PopoverExpression } from '../service_alert_trigger/popover_expression'; import { PopoverExpression } from '../service_alert_trigger/popover_expression';
interface AlertParams { export interface AlertParams {
aggregationType: 'avg' | '95th' | '99th';
environment: string;
serviceName: string;
threshold: number;
transactionType: string;
windowSize: number; windowSize: number;
windowUnit: string; windowUnit: string;
threshold: number;
aggregationType: 'avg' | '95th' | '99th';
serviceName: string;
transactionType: string;
environment: string;
} }
const TRANSACTION_ALERT_AGGREGATION_TYPES = { const TRANSACTION_ALERT_AGGREGATION_TYPES = {
@ -67,48 +65,38 @@ const TRANSACTION_ALERT_AGGREGATION_TYPES = {
interface Props { interface Props {
alertParams: AlertParams; alertParams: AlertParams;
metadata?: AlertMetadata;
setAlertParams: (key: string, value: any) => void; setAlertParams: (key: string, value: any) => void;
setAlertProperty: (key: string, value: any) => void; setAlertProperty: (key: string, value: any) => void;
} }
export function TransactionDurationAlertTrigger(props: Props) { export function TransactionDurationAlertTrigger(props: Props) {
const { setAlertParams, alertParams, setAlertProperty } = props; const { services } = useKibana();
const { urlParams } = useUrlParams(); const { alertParams, metadata, setAlertParams, setAlertProperty } = props;
const { start, end, environment: environmentFromUrl } = urlParams; createCallApmApi(services as CoreStart);
const serviceNameFromUrl = useServiceName();
const transactionTypes = useServiceTransactionTypesFetcher( const transactionTypes = useServiceTransactionTypesFetcher(
serviceNameFromUrl metadata?.serviceName
); );
const { agentName } = useServiceAgentNameFetcher(serviceNameFromUrl);
const transactionTypeFromUrl = getTransactionType({
transactionType: urlParams.transactionType,
transactionTypes,
agentName,
});
const params = defaults( const params = defaults(
{ {
...omit(metadata, ['start', 'end']),
...alertParams, ...alertParams,
}, },
{ {
aggregationType: 'avg', aggregationType: 'avg',
environment: environmentFromUrl || ENVIRONMENT_ALL.value,
threshold: 1500, threshold: 1500,
windowSize: 5, windowSize: 5,
windowUnit: 'm', windowUnit: 'm',
transactionType: transactionTypeFromUrl,
serviceName: serviceNameFromUrl,
} }
); );
const { environmentOptions } = useEnvironmentsFetcher({ const { environmentOptions } = useEnvironmentsFetcher({
serviceName: params.serviceName, serviceName: params.serviceName,
start, start: metadata?.start,
end, end: metadata?.end,
}); });
const { data } = useFetcher( const { data } = useFetcher(
@ -155,7 +143,7 @@ export function TransactionDurationAlertTrigger(props: Props) {
/> />
); );
if (!transactionTypes.length || !params.serviceName) { if (!params.serviceName) {
return null; return null;
} }

View file

@ -6,84 +6,67 @@
*/ */
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { defaults, omit } from 'lodash';
import React from 'react'; import React from 'react';
import { defaults } from 'lodash';
import { ANOMALY_SEVERITY } from '../../../../common/ml_constants'; import { ANOMALY_SEVERITY } from '../../../../common/ml_constants';
import { useServiceTransactionTypesFetcher } from '../../../context/apm_service/use_service_transaction_types_fetcher';
import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher';
import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import {
EnvironmentField,
ServiceField,
TransactionTypeField,
} from '../fields';
import { AlertMetadata } from '../helper';
import { ServiceAlertTrigger } from '../service_alert_trigger'; import { ServiceAlertTrigger } from '../service_alert_trigger';
import { PopoverExpression } from '../service_alert_trigger/popover_expression'; import { PopoverExpression } from '../service_alert_trigger/popover_expression';
import { import {
AnomalySeverity, AnomalySeverity,
SelectAnomalySeverity, SelectAnomalySeverity,
} from './select_anomaly_severity'; } from './select_anomaly_severity';
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
import {
EnvironmentField,
ServiceField,
TransactionTypeField,
} from '../fields';
import { useServiceName } from '../../../hooks/use_service_name';
import { useServiceTransactionTypesFetcher } from '../../../context/apm_service/use_service_transaction_types_fetcher';
import { useServiceAgentNameFetcher } from '../../../context/apm_service/use_service_agent_name_fetcher';
import { getTransactionType } from '../../../context/apm_service/apm_service_context';
interface AlertParams { interface AlertParams {
windowSize: number;
windowUnit: string;
serviceName?: string;
transactionType?: string;
environment: string;
anomalySeverityType: anomalySeverityType:
| ANOMALY_SEVERITY.CRITICAL | ANOMALY_SEVERITY.CRITICAL
| ANOMALY_SEVERITY.MAJOR | ANOMALY_SEVERITY.MAJOR
| ANOMALY_SEVERITY.MINOR | ANOMALY_SEVERITY.MINOR
| ANOMALY_SEVERITY.WARNING; | ANOMALY_SEVERITY.WARNING;
environment: string;
serviceName?: string;
transactionType?: string;
windowSize: number;
windowUnit: string;
} }
interface Props { interface Props {
alertParams: AlertParams; alertParams: AlertParams;
metadata?: AlertMetadata;
setAlertParams: (key: string, value: any) => void; setAlertParams: (key: string, value: any) => void;
setAlertProperty: (key: string, value: any) => void; setAlertProperty: (key: string, value: any) => void;
} }
export function TransactionDurationAnomalyAlertTrigger(props: Props) { export function TransactionDurationAnomalyAlertTrigger(props: Props) {
const { setAlertParams, alertParams, setAlertProperty } = props; const { alertParams, metadata, setAlertParams, setAlertProperty } = props;
const { urlParams } = useUrlParams();
const serviceNameFromUrl = useServiceName();
const transactionTypes = useServiceTransactionTypesFetcher( const transactionTypes = useServiceTransactionTypesFetcher(
serviceNameFromUrl metadata?.serviceName
); );
const { agentName } = useServiceAgentNameFetcher(serviceNameFromUrl);
const transactionTypeFromUrl = getTransactionType({
transactionType: urlParams.transactionType,
transactionTypes,
agentName,
});
const { start, end, environment: environmentFromUrl } = urlParams;
const params = defaults( const params = defaults(
{ {
...omit(metadata, ['start', 'end']),
...alertParams, ...alertParams,
}, },
{ {
windowSize: 15, windowSize: 15,
windowUnit: 'm', windowUnit: 'm',
transactionType: transactionTypeFromUrl,
environment: environmentFromUrl || ENVIRONMENT_ALL.value,
anomalySeverityType: ANOMALY_SEVERITY.CRITICAL, anomalySeverityType: ANOMALY_SEVERITY.CRITICAL,
serviceName: serviceNameFromUrl,
} }
); );
const { environmentOptions } = useEnvironmentsFetcher({ const { environmentOptions } = useEnvironmentsFetcher({
serviceName: params.serviceName, serviceName: params.serviceName,
start, start: metadata?.start,
end, end: metadata?.end,
}); });
const fields = [ const fields = [

View file

@ -5,14 +5,16 @@
* 2.0. * 2.0.
*/ */
import { defaults, omit } from 'lodash';
import React from 'react'; import React from 'react';
import { defaults } from 'lodash'; import { CoreStart } from '../../../../../../../src/core/public';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public';
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
import { asPercent } from '../../../../common/utils/formatters'; import { asPercent } from '../../../../common/utils/formatters';
import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useServiceTransactionTypesFetcher } from '../../../context/apm_service/use_service_transaction_types_fetcher';
import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher';
import { useFetcher } from '../../../hooks/use_fetcher'; import { useFetcher } from '../../../hooks/use_fetcher';
import { createCallApmApi } from '../../../services/rest/createCallApmApi';
import { ChartPreview } from '../chart_preview'; import { ChartPreview } from '../chart_preview';
import { import {
EnvironmentField, EnvironmentField,
@ -20,12 +22,8 @@ import {
ServiceField, ServiceField,
TransactionTypeField, TransactionTypeField,
} from '../fields'; } from '../fields';
import { getAbsoluteTimeRange } from '../helper'; import { AlertMetadata, getAbsoluteTimeRange } from '../helper';
import { ServiceAlertTrigger } from '../service_alert_trigger'; import { ServiceAlertTrigger } from '../service_alert_trigger';
import { useServiceName } from '../../../hooks/use_service_name';
import { useServiceTransactionTypesFetcher } from '../../../context/apm_service/use_service_transaction_types_fetcher';
import { useServiceAgentNameFetcher } from '../../../context/apm_service/use_service_agent_name_fetcher';
import { getTransactionType } from '../../../context/apm_service/apm_service_context';
interface AlertParams { interface AlertParams {
windowSize: number; windowSize: number;
@ -38,45 +36,34 @@ interface AlertParams {
interface Props { interface Props {
alertParams: AlertParams; alertParams: AlertParams;
metadata?: AlertMetadata;
setAlertParams: (key: string, value: any) => void; setAlertParams: (key: string, value: any) => void;
setAlertProperty: (key: string, value: any) => void; setAlertProperty: (key: string, value: any) => void;
} }
export function TransactionErrorRateAlertTrigger(props: Props) { export function TransactionErrorRateAlertTrigger(props: Props) {
const { setAlertParams, alertParams, setAlertProperty } = props; const { services } = useKibana();
const { urlParams } = useUrlParams(); const { alertParams, metadata, setAlertParams, setAlertProperty } = props;
const serviceNameFromUrl = useServiceName(); createCallApmApi(services as CoreStart);
const transactionTypes = useServiceTransactionTypesFetcher( const transactionTypes = useServiceTransactionTypesFetcher(
serviceNameFromUrl metadata?.serviceName
); );
const { agentName } = useServiceAgentNameFetcher(serviceNameFromUrl);
const transactionTypeFromUrl = getTransactionType({
transactionType: urlParams.transactionType,
transactionTypes,
agentName,
});
const { start, end, environment: environmentFromUrl } = urlParams;
const params = defaults( const params = defaults(
{ ...alertParams }, { ...omit(metadata, ['start', 'end']), ...alertParams },
{ {
threshold: 30, threshold: 30,
windowSize: 5, windowSize: 5,
windowUnit: 'm', windowUnit: 'm',
transactionType: transactionTypeFromUrl,
environment: environmentFromUrl || ENVIRONMENT_ALL.value,
serviceName: serviceNameFromUrl,
} }
); );
const { environmentOptions } = useEnvironmentsFetcher({ const { environmentOptions } = useEnvironmentsFetcher({
serviceName: params.serviceName, serviceName: params.serviceName,
start, start: metadata?.start,
end, end: metadata?.end,
}); });
const thresholdAsPercent = (params.threshold ?? 0) / 100; const thresholdAsPercent = (params.threshold ?? 0) / 100;
@ -106,10 +93,6 @@ export function TransactionErrorRateAlertTrigger(props: Props) {
] ]
); );
if (params.serviceName && !transactionTypes.length) {
return null;
}
const fields = [ const fields = [
<ServiceField value={params.serviceName} />, <ServiceField value={params.serviceName} />,
<TransactionTypeField <TransactionTypeField

View file

@ -32,8 +32,11 @@ import { fromQuery } from '../../shared/Links/url_helpers';
import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider';
import { uiSettingsServiceMock } from '../../../../../../../src/core/public/mocks'; import { uiSettingsServiceMock } from '../../../../../../../src/core/public/mocks';
const uiSettings = uiSettingsServiceMock.create().setup({} as any);
const KibanaReactContext = createKibanaReactContext(({ const KibanaReactContext = createKibanaReactContext(({
uiSettings: { get: () => true }, notifications: { toasts: { add: () => {} } },
uiSettings,
usageCollection: { reportUiCounter: () => {} }, usageCollection: { reportUiCounter: () => {} },
} as unknown) as Partial<CoreStart>); } as unknown) as Partial<CoreStart>);
@ -48,8 +51,6 @@ const location = {
search: fromQuery(mockParams), search: fromQuery(mockParams),
}; };
const uiSettings = uiSettingsServiceMock.create().setup({} as any);
function Wrapper({ children }: { children?: ReactNode }) { function Wrapper({ children }: { children?: ReactNode }) {
const value = ({ const value = ({
...mockApmPluginContextValue, ...mockApmPluginContextValue,
@ -64,11 +65,7 @@ function Wrapper({ children }: { children?: ReactNode }) {
return ( return (
<MemoryRouter initialEntries={[location]}> <MemoryRouter initialEntries={[location]}>
<KibanaReactContext.Provider <KibanaReactContext.Provider>
services={{
uiSettings,
}}
>
<MockApmPluginContextWrapper value={value}> <MockApmPluginContextWrapper value={value}>
<MockUrlParamsContextProvider params={mockParams}> <MockUrlParamsContextProvider params={mockParams}>
{children} {children}

View file

@ -6,22 +6,35 @@
*/ */
import { renderHook, RenderHookResult } from '@testing-library/react-hooks'; import { renderHook, RenderHookResult } from '@testing-library/react-hooks';
import React, { ReactNode } from 'react';
import { CoreStart } from '../../../../../src/core/public';
import { createKibanaReactContext } from '../../../../../src/plugins/kibana_react/public';
import { delay } from '../utils/testHelpers'; import { delay } from '../utils/testHelpers';
import { FetcherResult, useFetcher } from './use_fetcher'; import { FetcherResult, useFetcher } from './use_fetcher';
import { MockApmPluginContextWrapper } from '../context/apm_plugin/mock_apm_plugin_context';
import { ApmPluginContextValue } from '../context/apm_plugin/apm_plugin_context';
// Wrap the hook with a provider so it can useApmPluginContext // Wrap the hook with a provider so it can useKibana
const wrapper = MockApmPluginContextWrapper; const KibanaReactContext = createKibanaReactContext(({
notifications: { toasts: { add: () => {}, danger: () => {} } },
} as unknown) as Partial<CoreStart>);
interface WrapperProps {
children?: ReactNode;
callback: () => Promise<string>;
args: string[];
}
function wrapper({ children }: WrapperProps) {
return <KibanaReactContext.Provider>{children}</KibanaReactContext.Provider>;
}
describe('useFetcher', () => { describe('useFetcher', () => {
describe('when resolving after 500ms', () => { describe('when resolving after 500ms', () => {
let hook: RenderHookResult< let hook: RenderHookResult<
{ children?: React.ReactNode; value?: ApmPluginContextValue }, WrapperProps,
FetcherResult<string> & { FetcherResult<string> & {
refetch: () => void; refetch: () => void;
} }
>; >;
beforeEach(() => { beforeEach(() => {
jest.useFakeTimers(); jest.useFakeTimers();
async function fn() { async function fn() {
@ -66,14 +79,15 @@ describe('useFetcher', () => {
describe('when throwing after 500ms', () => { describe('when throwing after 500ms', () => {
let hook: RenderHookResult< let hook: RenderHookResult<
{ children?: React.ReactNode; value?: ApmPluginContextValue }, WrapperProps,
FetcherResult<void> & { FetcherResult<string> & {
refetch: () => void; refetch: () => void;
} }
>; >;
beforeEach(() => { beforeEach(() => {
jest.useFakeTimers(); jest.useFakeTimers();
async function fn() { async function fn(): Promise<string> {
await delay(500); await delay(500);
throw new Error('Something went wrong'); throw new Error('Something went wrong');
} }

View file

@ -8,13 +8,12 @@
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { IHttpFetchError } from 'src/core/public'; import { IHttpFetchError } from 'src/core/public';
import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; import { useKibana } from '../../../../../src/plugins/kibana_react/public';
import {
callApmApi,
AutoAbortedAPMClient,
} from '../services/rest/createCallApmApi';
import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context';
import { useUrlParams } from '../context/url_params_context/use_url_params'; import { useUrlParams } from '../context/url_params_context/use_url_params';
import {
AutoAbortedAPMClient,
callApmApi,
} from '../services/rest/createCallApmApi';
export enum FETCH_STATUS { export enum FETCH_STATUS {
LOADING = 'loading', LOADING = 'loading',
@ -68,7 +67,7 @@ export function useFetcher<TReturn>(
showToastOnError?: boolean; showToastOnError?: boolean;
} = {} } = {}
): FetcherResult<InferResponseType<TReturn>> & { refetch: () => void } { ): FetcherResult<InferResponseType<TReturn>> & { refetch: () => void } {
const { notifications } = useApmPluginContext().core; const { notifications } = useKibana();
const { preservePreviousData = true, showToastOnError = true } = options; const { preservePreviousData = true, showToastOnError = true } = options;
const [result, setResult] = useState< const [result, setResult] = useState<
FetcherResult<InferResponseType<TReturn>> FetcherResult<InferResponseType<TReturn>>
@ -124,12 +123,12 @@ export function useFetcher<TReturn>(
'response' in err ? getDetailsFromErrorResponse(err) : err.message; 'response' in err ? getDetailsFromErrorResponse(err) : err.message;
if (showToastOnError) { if (showToastOnError) {
notifications.toasts.addDanger({ notifications.toasts.danger({
title: i18n.translate('xpack.apm.fetcher.error.title', { title: i18n.translate('xpack.apm.fetcher.error.title', {
defaultMessage: `Error while fetching resource`, defaultMessage: `Error while fetching resource`,
}), }),
text: toMountPoint( body: (
<div> <div>
<h5> <h5>
{i18n.translate('xpack.apm.fetcher.error.status', { {i18n.translate('xpack.apm.fetcher.error.status', {