[Alerting UI] Replaced AlertsContextProvider with KibanaContextProvider and exposed components in API (#84604)

* [Alerting UI] Replaced AlertsContextProvider with KibanaContextProvider and exposed components in API

* removed AlertContextProvider

* exposed AlertAdd and EditAlert flyouts with triggers_actions_ui plugin start

* fixed type check

* fixed tests

* fixed typechecks

* fixed wrong consumer

* fixed monitoring flyout flickering

* fixed due to comments

* fixed typechecks

* fixed typechecks

* fixed typechecks

* fixed typechecks

* fixed due to comments
This commit is contained in:
Yuliia Naumenko 2020-12-07 16:44:40 -08:00 committed by GitHub
parent a6a8cc2175
commit 6757b95b1e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
73 changed files with 862 additions and 1172 deletions

View file

@ -22,7 +22,6 @@ import {
AlertConditionsGroup,
AlertTypeModel,
AlertTypeParamsExpressionProps,
AlertsContextValue,
} from '../../../../plugins/triggers_actions_ui/public';
import {
AlwaysFiringParams,
@ -65,7 +64,7 @@ const DEFAULT_THRESHOLDS: AlwaysFiringParams['thresholds'] = {
};
export const AlwaysFiringExpression: React.FunctionComponent<
AlertTypeParamsExpressionProps<AlwaysFiringParams, AlertsContextValue>
AlertTypeParamsExpressionProps<AlwaysFiringParams>
> = ({ alertParams, setAlertParams, actionGroups, defaultActionGroupId }) => {
const {
instances = DEFAULT_INSTANCES_TO_GENERATE,

View file

@ -4,26 +4,27 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import { EuiIcon, EuiFlexItem, EuiCard, EuiFlexGroup } from '@elastic/eui';
import { AlertsContextProvider, AlertAdd } from '../../../../plugins/triggers_actions_ui/public';
import { AlertingExampleComponentParams } from '../application';
import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants';
export const CreateAlert = ({
http,
triggersActionsUi,
charts,
uiSettings,
docLinks,
data,
toastNotifications,
capabilities,
}: AlertingExampleComponentParams) => {
export const CreateAlert = ({ triggersActionsUi }: AlertingExampleComponentParams) => {
const [alertFlyoutVisible, setAlertFlyoutVisibility] = useState<boolean>(false);
const AddAlertFlyout = useMemo(
() =>
triggersActionsUi.getAddAlertFlyout({
consumer: ALERTING_EXAMPLE_APP_ID,
addFlyoutVisible: alertFlyoutVisible,
setAddFlyoutVisibility: setAlertFlyoutVisibility,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[alertFlyoutVisible]
);
return (
<EuiFlexGroup>
<EuiFlexItem grow={false}>
@ -34,27 +35,7 @@ export const CreateAlert = ({
onClick={() => setAlertFlyoutVisibility(true)}
/>
</EuiFlexItem>
<EuiFlexItem>
<AlertsContextProvider
value={{
http,
actionTypeRegistry: triggersActionsUi.actionTypeRegistry,
alertTypeRegistry: triggersActionsUi.alertTypeRegistry,
toastNotifications,
uiSettings,
docLinks,
charts,
dataFieldsFormats: data.fieldFormats,
capabilities,
}}
>
<AlertAdd
consumer={ALERTING_EXAMPLE_APP_ID}
addFlyoutVisible={alertFlyoutVisible}
setAddFlyoutVisibility={setAlertFlyoutVisibility}
/>
</AlertsContextProvider>
</EuiFlexItem>
<EuiFlexItem>{AddAlertFlyout}</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -9,10 +9,12 @@ import { createMemoryHistory } from 'history';
import { Observable } from 'rxjs';
import { AppMountParameters, CoreStart, HttpSetup } from 'src/core/public';
import { mockApmPluginContextValue } from '../context/apm_plugin/mock_apm_plugin_context';
import { ApmPluginSetupDeps } from '../plugin';
import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin';
import { createCallApmApi } from '../services/rest/createCallApmApi';
import { renderApp } from './';
import { disableConsoleWarning } from '../utils/testHelpers';
import { dataPluginMock } from 'src/plugins/data/public/mocks';
import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks';
jest.mock('../services/rest/index_pattern', () => ({
createStaticIndexPattern: () => Promise.resolve(undefined),
@ -55,6 +57,19 @@ describe('renderApp', () => {
history: createMemoryHistory(),
setHeaderActionMenu: () => {},
};
const data = dataPluginMock.createStartContract();
const embeddable = embeddablePluginMock.createStartContract();
const startDeps = {
triggersActionsUi: {
actionTypeRegistry: {},
alertTypeRegistry: {},
getAddAlertFlyout: jest.fn(),
getEditAlertFlyout: jest.fn(),
},
data,
embeddable,
};
jest.spyOn(window, 'scrollTo').mockReturnValueOnce(undefined);
createCallApmApi((core.http as unknown) as HttpSetup);
@ -75,7 +90,8 @@ describe('renderApp', () => {
(core as unknown) as CoreStart,
(plugins as unknown) as ApmPluginSetupDeps,
(params as unknown) as AppMountParameters,
config
config,
(startDeps as unknown) as ApmPluginStartDeps
);
});

View file

@ -19,7 +19,6 @@ import {
RedirectAppLinks,
useUiSetting$,
} from '../../../../../src/plugins/kibana_react/public';
import { AlertsContextProvider } from '../../../triggers_actions_ui/public';
import { routes } from '../components/app/Main/route_config';
import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange';
import {
@ -29,7 +28,7 @@ import {
import { LicenseProvider } from '../context/license/license_context';
import { UrlParamsProvider } from '../context/url_params_context/url_params_context';
import { useBreadcrumbs } from '../hooks/use_breadcrumbs';
import { ApmPluginSetupDeps } from '../plugin';
import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin';
import { createCallApmApi } from '../services/rest/createCallApmApi';
import { createStaticIndexPattern } from '../services/rest/index_pattern';
import { setHelpExtension } from '../setHelpExtension';
@ -66,38 +65,29 @@ function App() {
export function ApmAppRoot({
apmPluginContextValue,
startDeps,
}: {
apmPluginContextValue: ApmPluginContextValue;
startDeps: ApmPluginStartDeps;
}) {
const { appMountParameters, core, plugins } = apmPluginContextValue;
const { appMountParameters, core } = apmPluginContextValue;
const { history } = appMountParameters;
const i18nCore = core.i18n;
return (
<RedirectAppLinks application={core.application}>
<ApmPluginContext.Provider value={apmPluginContextValue}>
<AlertsContextProvider
value={{
http: core.http,
docLinks: core.docLinks,
capabilities: core.application.capabilities,
toastNotifications: core.notifications.toasts,
actionTypeRegistry: plugins.triggersActionsUi.actionTypeRegistry,
alertTypeRegistry: plugins.triggersActionsUi.alertTypeRegistry,
}}
>
<KibanaContextProvider services={{ ...core, ...plugins }}>
<i18nCore.Context>
<Router history={history}>
<UrlParamsProvider>
<LicenseProvider>
<App />
</LicenseProvider>
</UrlParamsProvider>
</Router>
</i18nCore.Context>
</KibanaContextProvider>
</AlertsContextProvider>
<KibanaContextProvider services={{ ...core, ...startDeps }}>
<i18nCore.Context>
<Router history={history}>
<UrlParamsProvider>
<LicenseProvider>
<App />
</LicenseProvider>
</UrlParamsProvider>
</Router>
</i18nCore.Context>
</KibanaContextProvider>
</ApmPluginContext.Provider>
</RedirectAppLinks>
);
@ -111,7 +101,8 @@ export const renderApp = (
core: CoreStart,
setupDeps: ApmPluginSetupDeps,
appMountParameters: AppMountParameters,
config: ConfigSchema
config: ConfigSchema,
startDeps: ApmPluginStartDeps
) => {
const { element } = appMountParameters;
const apmPluginContextValue = {
@ -133,7 +124,10 @@ export const renderApp = (
});
ReactDOM.render(
<ApmAppRoot apmPluginContextValue={apmPluginContextValue} />,
<ApmAppRoot
apmPluginContextValue={apmPluginContextValue}
startDeps={startDeps}
/>,
element
);
return () => {

View file

@ -3,29 +3,37 @@
* 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 React, { useMemo } from 'react';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { AlertType } from '../../../../common/alert_types';
import { AlertAdd } from '../../../../../triggers_actions_ui/public';
type AlertAddProps = React.ComponentProps<typeof AlertAdd>;
import { TriggersAndActionsUIPublicPluginStart } from '../../../../../triggers_actions_ui/public';
interface Props {
addFlyoutVisible: AlertAddProps['addFlyoutVisible'];
setAddFlyoutVisibility: AlertAddProps['setAddFlyoutVisibility'];
addFlyoutVisible: boolean;
setAddFlyoutVisibility: React.Dispatch<React.SetStateAction<boolean>>;
alertType: AlertType | null;
}
interface KibanaDeps {
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
}
export function AlertingFlyout(props: Props) {
const { addFlyoutVisible, setAddFlyoutVisibility, alertType } = props;
return (
alertType && (
<AlertAdd
addFlyoutVisible={addFlyoutVisible}
setAddFlyoutVisibility={setAddFlyoutVisibility}
consumer="apm"
alertTypeId={alertType}
canChangeTrigger={false}
/>
)
const {
services: { triggersActionsUi },
} = useKibana<KibanaDeps>();
const addAlertFlyout = useMemo(
() =>
alertType &&
triggersActionsUi.getAddAlertFlyout({
consumer: 'apm',
addFlyoutVisible,
setAddFlyoutVisibility,
alertTypeId: alertType,
canChangeTrigger: false,
}),
[addFlyoutVisible, alertType, setAddFlyoutVisibility, triggersActionsUi]
);
return <>{addAlertFlyout}</>;
}

View file

@ -138,12 +138,18 @@ export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
async mount(params: AppMountParameters<unknown>) {
// Load application bundle and Get start services
const [{ renderApp }, [coreStart]] = await Promise.all([
const [{ renderApp }, [coreStart, corePlugins]] = await Promise.all([
import('./application'),
core.getStartServices(),
]);
return renderApp(coreStart, pluginSetupDeps, params, config);
return renderApp(
coreStart,
pluginSetupDeps,
params,
config,
corePlugins as ApmPluginStartDeps
);
},
});

View file

@ -3,7 +3,6 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { HttpSetup } from 'kibana/public';
import { omit } from 'lodash';
import React, { useCallback, useMemo, useState } from 'react';
import {
@ -20,6 +19,7 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { FORMATTERS } from '../../../../common/formatters';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { ValidationResult } from '../../../../../triggers_actions_ui/public/types';
@ -35,7 +35,6 @@ interface Props {
alertInterval: string;
alertThrottle: string;
alertType: PreviewableAlertTypes;
fetch: HttpSetup['fetch'];
alertParams: { criteria: any[]; sourceId: string } & Record<string, any>;
validate: (params: any) => ValidationResult;
showNoDataResults?: boolean;
@ -47,12 +46,13 @@ export const AlertPreview: React.FC<Props> = (props) => {
alertParams,
alertInterval,
alertThrottle,
fetch,
alertType,
validate,
showNoDataResults,
groupByDisplayName,
} = props;
const { http } = useKibana().services;
const [previewLookbackInterval, setPreviewLookbackInterval] = useState<string>('h');
const [isPreviewLoading, setIsPreviewLoading] = useState<boolean>(false);
const [previewError, setPreviewError] = useState<any | false>(false);
@ -70,7 +70,7 @@ export const AlertPreview: React.FC<Props> = (props) => {
setPreviewError(false);
try {
const result = await getAlertPreview({
fetch,
fetch: http!.fetch,
params: {
...alertParams,
lookback: previewLookbackInterval as 'h' | 'd' | 'w' | 'M',
@ -89,12 +89,12 @@ export const AlertPreview: React.FC<Props> = (props) => {
}, [
alertParams,
alertInterval,
fetch,
alertType,
groupByDisplayName,
previewLookbackInterval,
alertThrottle,
showNoDataResults,
http,
]);
const previewIntervalError = useMemo(() => {

View file

@ -4,12 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useContext } from 'react';
import { ApplicationStart, DocLinksStart, HttpStart, NotificationsStart } from 'src/core/public';
import React, { useContext, useMemo } from 'react';
import { AlertsContextProvider, AlertAdd } from '../../../../../triggers_actions_ui/public';
import { TriggerActionsContext } from '../../../utils/triggers_actions_context';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/inventory_metric_threshold/types';
import { InfraWaffleMapOptions } from '../../../lib/lib';
@ -24,48 +21,31 @@ interface Props {
setVisible: React.Dispatch<React.SetStateAction<boolean>>;
}
interface KibanaDeps {
notifications: NotificationsStart;
http: HttpStart;
docLinks: DocLinksStart;
application: ApplicationStart;
}
export const AlertFlyout = ({ options, nodeType, filter, visible, setVisible }: Props) => {
const { triggersActionsUI } = useContext(TriggerActionsContext);
const { services } = useKibana<KibanaDeps>();
const { inventoryPrefill } = useAlertPrefillContext();
const { customMetrics } = inventoryPrefill;
return (
<>
{triggersActionsUI && (
<AlertsContextProvider
value={{
metadata: {
options,
nodeType,
filter,
customMetrics,
},
toastNotifications: services.notifications?.toasts,
http: services.http,
docLinks: services.docLinks,
capabilities: services.application.capabilities,
actionTypeRegistry: triggersActionsUI.actionTypeRegistry,
alertTypeRegistry: triggersActionsUI.alertTypeRegistry,
}}
>
<AlertAdd
addFlyoutVisible={visible!}
setAddFlyoutVisibility={setVisible}
alertTypeId={METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID}
canChangeTrigger={false}
consumer={'infrastructure'}
/>
</AlertsContextProvider>
)}
</>
const AddAlertFlyout = useMemo(
() =>
triggersActionsUI &&
triggersActionsUI.getAddAlertFlyout({
consumer: 'infrastructure',
addFlyoutVisible: visible!,
setAddFlyoutVisibility: setVisible,
canChangeTrigger: false,
alertTypeId: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID,
metadata: {
options,
nodeType,
filter,
customMetrics,
},
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[triggersActionsUI, visible]
);
return <>{AddAlertFlyout}</>;
};

View file

@ -5,10 +5,8 @@
*/
import { mountWithIntl, nextTick } from '@kbn/test/jest';
import { actionTypeRegistryMock } from '../../../../../triggers_actions_ui/public/application/action_type_registry.mock';
import { alertTypeRegistryMock } from '../../../../../triggers_actions_ui/public/application/alert_type_registry.mock';
import { coreMock } from '../../../../../../../src/core/public/mocks';
import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context';
// We are using this inside a `jest.mock` call. Jest requires dynamic dependencies to be prefixed with `mock`
import { coreMock as mockCoreMock } from 'src/core/public/mocks';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { InventoryMetricConditions } from '../../../../server/lib/alerting/inventory_metric_threshold/types';
import React from 'react';
@ -25,6 +23,12 @@ jest.mock('../../../containers/source/use_source_via_http', () => ({
}),
}));
jest.mock('../../../hooks/use_kibana', () => ({
useKibanaContextForPlugin: () => ({
services: mockCoreMock.createStart(),
}),
}));
const exampleCustomMetric = {
id: 'this-is-an-id',
field: 'some.system.field',
@ -39,41 +43,15 @@ describe('Expression', () => {
nodeType: undefined,
filterQueryText: '',
};
const mocks = coreMock.createSetup();
const startMocks = coreMock.createStart();
const [
{
application: { capabilities },
},
] = await mocks.getStartServices();
const context: AlertsContextValue<AlertContextMeta> = {
http: mocks.http,
toastNotifications: mocks.notifications.toasts,
actionTypeRegistry: actionTypeRegistryMock.create() as any,
alertTypeRegistry: alertTypeRegistryMock.create() as any,
docLinks: startMocks.docLinks,
capabilities: {
...capabilities,
actions: {
delete: true,
save: true,
show: true,
},
},
metadata: currentOptions,
};
const wrapper = mountWithIntl(
<Expressions
alertsContext={context}
alertInterval="1m"
alertThrottle="1m"
alertParams={alertParams as any}
errors={[]}
setAlertParams={(key, value) => Reflect.set(alertParams, key, value)}
setAlertProperty={() => {}}
metadata={currentOptions}
/>
);
@ -153,9 +131,6 @@ describe('ExpressionRow', () => {
metric: [],
}}
expression={expression}
alertsContextMetadata={{
customMetrics: [],
}}
fields={[{ name: 'some.system.field', type: 'bzzz' }]}
/>
);

View file

@ -38,8 +38,6 @@ import {
} from '../../../../../triggers_actions_ui/public/common';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { IErrorObject } from '../../../../../triggers_actions_ui/public/types';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context';
import { MetricsExplorerKueryBar } from '../../../pages/metrics/metrics_explorer/components/kuery_bar';
import { useSourceViaHttp } from '../../../containers/source/use_source_via_http';
import { sqsMetricTypes } from '../../../../common/inventory_models/aws_sqs/toolbar_items';
@ -67,6 +65,7 @@ import {
} from '../../../../common/http_api/snapshot_api';
import { validateMetricThreshold } from './validation';
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
const FILTER_TYPING_DEBOUNCE_MS = 500;
@ -89,9 +88,9 @@ interface Props {
};
alertInterval: string;
alertThrottle: string;
alertsContext: AlertsContextValue<AlertContextMeta>;
setAlertParams(key: string, value: any): void;
setAlertProperty(key: string, value: any): void;
metadata: AlertContextMeta;
}
export const defaultExpression = {
@ -109,19 +108,13 @@ export const defaultExpression = {
} as InventoryMetricConditions;
export const Expressions: React.FC<Props> = (props) => {
const {
setAlertParams,
alertParams,
errors,
alertsContext,
alertInterval,
alertThrottle,
} = props;
const { http, notifications } = useKibanaContextForPlugin().services;
const { setAlertParams, alertParams, errors, alertInterval, alertThrottle, metadata } = props;
const { source, createDerivedIndexPattern } = useSourceViaHttp({
sourceId: 'default',
type: 'metrics',
fetch: alertsContext.http.fetch,
toastWarning: alertsContext.toastNotifications.addWarning,
fetch: http.fetch,
toastWarning: notifications.toasts.addWarning,
});
const [timeSize, setTimeSize] = useState<number | undefined>(1);
const [timeUnit, setTimeUnit] = useState<Unit>('m');
@ -221,7 +214,7 @@ export const Expressions: React.FC<Props> = (props) => {
);
const preFillAlertCriteria = useCallback(() => {
const md = alertsContext.metadata;
const md = metadata;
if (md && md.options) {
setAlertParams('criteria', [
{
@ -235,10 +228,10 @@ export const Expressions: React.FC<Props> = (props) => {
} else {
setAlertParams('criteria', [defaultExpression]);
}
}, [alertsContext.metadata, setAlertParams]);
}, [metadata, setAlertParams]);
const preFillAlertFilter = useCallback(() => {
const md = alertsContext.metadata;
const md = metadata;
if (md && md.filter) {
setAlertParams('filterQueryText', md.filter);
setAlertParams(
@ -246,10 +239,10 @@ export const Expressions: React.FC<Props> = (props) => {
convertKueryToElasticSearchQuery(md.filter, derivedIndexPattern) || ''
);
}
}, [alertsContext.metadata, derivedIndexPattern, setAlertParams]);
}, [metadata, derivedIndexPattern, setAlertParams]);
useEffect(() => {
const md = alertsContext.metadata;
const md = metadata;
if (!alertParams.nodeType) {
if (md && md.nodeType) {
setAlertParams('nodeType', md.nodeType);
@ -272,7 +265,7 @@ export const Expressions: React.FC<Props> = (props) => {
if (!alertParams.sourceId) {
setAlertParams('sourceId', source?.id || 'default');
}
}, [alertsContext.metadata, derivedIndexPattern, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps
}, [metadata, derivedIndexPattern, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<>
@ -308,7 +301,6 @@ export const Expressions: React.FC<Props> = (props) => {
setAlertParams={updateParams}
errors={errors[idx] || emptyError}
expression={e || {}}
alertsContextMetadata={alertsContext.metadata}
fields={derivedIndexPattern.fields}
/>
);
@ -371,7 +363,7 @@ export const Expressions: React.FC<Props> = (props) => {
fullWidth
display="rowCompressed"
>
{(alertsContext.metadata && (
{(metadata && (
<MetricsExplorerKueryBar
derivedIndexPattern={derivedIndexPattern}
onSubmit={onFilterChange}
@ -394,7 +386,6 @@ export const Expressions: React.FC<Props> = (props) => {
alertType={METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID}
alertParams={pick(alertParams, 'criteria', 'nodeType', 'sourceId', 'filterQuery')}
validate={validateMetricThreshold}
fetch={alertsContext.http.fetch}
groupByDisplayName={alertParams.nodeType}
showNoDataResults={alertParams.alertOnNoData}
/>
@ -418,7 +409,6 @@ interface ExpressionRowProps {
addExpression(): void;
remove(id: number): void;
setAlertParams(id: number, params: Partial<InventoryMetricConditions>): void;
alertsContextMetadata: AlertsContextValue<AlertContextMeta>['metadata'];
fields: IFieldType[];
}

View file

@ -4,11 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useContext } from 'react';
import { ApplicationStart, DocLinksStart, HttpStart, NotificationsStart } from 'src/core/public';
import { AlertsContextProvider, AlertAdd } from '../../../../../triggers_actions_ui/public';
import React, { useContext, useMemo } from 'react';
import { TriggerActionsContext } from '../../../utils/triggers_actions_context';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID } from '../../../../common/alerting/logs/log_threshold/types';
interface Props {
@ -16,42 +13,25 @@ interface Props {
setVisible: React.Dispatch<React.SetStateAction<boolean>>;
}
interface KibanaDeps {
notifications: NotificationsStart;
http: HttpStart;
docLinks: DocLinksStart;
application: ApplicationStart;
}
export const AlertFlyout = (props: Props) => {
const { triggersActionsUI } = useContext(TriggerActionsContext);
const { services } = useKibana<KibanaDeps>();
return (
<>
{triggersActionsUI && (
<AlertsContextProvider
value={{
metadata: {
isInternal: true,
},
toastNotifications: services.notifications.toasts,
http: services.http,
docLinks: services.docLinks,
capabilities: services.application.capabilities,
actionTypeRegistry: triggersActionsUI.actionTypeRegistry,
alertTypeRegistry: triggersActionsUI.alertTypeRegistry,
}}
>
<AlertAdd
addFlyoutVisible={props.visible!}
setAddFlyoutVisibility={props.setVisible}
alertTypeId={LOG_DOCUMENT_COUNT_ALERT_TYPE_ID}
canChangeTrigger={false}
consumer={'logs'}
/>
</AlertsContextProvider>
)}
</>
const AddAlertFlyout = useMemo(
() =>
triggersActionsUI &&
triggersActionsUI.getAddAlertFlyout({
consumer: 'logs',
addFlyoutVisible: props.visible!,
setAddFlyoutVisibility: props.setVisible,
canChangeTrigger: false,
alertTypeId: LOG_DOCUMENT_COUNT_ALERT_TYPE_ID,
metadata: {
isInternal: true,
},
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[triggersActionsUI, props.visible]
);
return <>{AddAlertFlyout}</>;
};

View file

@ -22,7 +22,7 @@ import {
getDenominator,
} from '../../../../../common/alerting/logs/log_threshold/types';
import { Errors, CriterionErrors } from '../../validation';
import { AlertsContext, ExpressionLike } from './editor';
import { ExpressionLike } from './editor';
import { CriterionPreview } from './criterion_preview_chart';
const DEFAULT_CRITERIA = { field: 'log.level', comparator: Comparator.EQ, value: 'error' };
@ -40,7 +40,6 @@ interface SharedProps {
criteria?: AlertParams['criteria'];
errors: Errors['criteria'];
alertParams: Partial<AlertParams>;
context: AlertsContext;
sourceId: string;
updateCriteria: (criteria: AlertParams['criteria']) => void;
}
@ -66,7 +65,6 @@ interface CriteriaWrapperProps {
addCriterion: () => void;
criteria: CriteriaType;
errors: CriterionErrors;
context: SharedProps['context'];
sourceId: SharedProps['sourceId'];
isRatio?: boolean;
}
@ -80,7 +78,6 @@ const CriteriaWrapper: React.FC<CriteriaWrapperProps> = (props) => {
fields,
errors,
alertParams,
context,
sourceId,
isRatio = false,
} = props;
@ -108,7 +105,6 @@ const CriteriaWrapper: React.FC<CriteriaWrapperProps> = (props) => {
>
<CriterionPreview
alertParams={alertParams}
context={context}
chartCriterion={criterion}
sourceId={sourceId}
showThreshold={!isRatio}
@ -127,7 +123,6 @@ interface RatioCriteriaProps {
fields: SharedProps['fields'];
criteria: RatioCriteriaType;
errors: Errors['criteria'];
context: SharedProps['context'];
sourceId: SharedProps['sourceId'];
updateCriteria: (criteria: AlertParams['criteria']) => void;
}
@ -201,7 +196,6 @@ interface CountCriteriaProps {
fields: SharedProps['fields'];
criteria: CountCriteriaType;
errors: Errors['criteria'];
context: SharedProps['context'];
sourceId: SharedProps['sourceId'];
updateCriteria: (criteria: AlertParams['criteria']) => void;
}

View file

@ -19,6 +19,7 @@ import {
} from '@elastic/charts';
import { EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import {
ChartContainer,
LoadingState,
@ -43,7 +44,6 @@ import {
GetLogAlertsChartPreviewDataAlertParamsSubset,
getLogAlertsChartPreviewDataAlertParamsSubsetRT,
} from '../../../../../common/http_api/log_alerts/';
import { AlertsContext } from './editor';
import { useChartPreviewData } from './hooks/use_chart_preview_data';
import { decodeOrThrow } from '../../../../../common/runtime_types';
@ -51,7 +51,6 @@ const GROUP_LIMIT = 5;
interface Props {
alertParams: Partial<AlertParams>;
context: AlertsContext;
chartCriterion: Partial<Criterion>;
sourceId: string;
showThreshold: boolean;
@ -59,7 +58,6 @@ interface Props {
export const CriterionPreview: React.FC<Props> = ({
alertParams,
context,
chartCriterion,
sourceId,
showThreshold,
@ -91,7 +89,6 @@ export const CriterionPreview: React.FC<Props> = ({
? NUM_BUCKETS
: NUM_BUCKETS / 4
} // Display less data for groups due to space limitations
context={context}
sourceId={sourceId}
threshold={alertParams.count}
chartAlertParams={chartAlertParams}
@ -102,7 +99,6 @@ export const CriterionPreview: React.FC<Props> = ({
interface ChartProps {
buckets: number;
context: AlertsContext;
sourceId: string;
threshold?: Threshold;
chartAlertParams: GetLogAlertsChartPreviewDataAlertParamsSubset;
@ -111,13 +107,13 @@ interface ChartProps {
const CriterionPreviewChart: React.FC<ChartProps> = ({
buckets,
context,
sourceId,
threshold,
chartAlertParams,
showThreshold,
}) => {
const isDarkMode = context.uiSettings?.get('theme:darkMode') || false;
const { uiSettings } = useKibana().services;
const isDarkMode = uiSettings?.get('theme:darkMode') || false;
const {
getChartPreviewData,
@ -125,7 +121,6 @@ const CriterionPreviewChart: React.FC<ChartProps> = ({
hasError,
chartPreviewData: series,
} = useChartPreviewData({
context,
sourceId,
alertParams: chartAlertParams,
buckets,

View file

@ -8,11 +8,9 @@ import React, { useCallback, useMemo, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiLoadingSpinner, EuiSpacer, EuiButton, EuiCallOut } from '@elastic/eui';
import useMount from 'react-use/lib/useMount';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import { GroupByExpression } from '../../../common/group_by_expression/group_by_expression';
import {
ForLastExpression,
AlertsContextValue,
} from '../../../../../../triggers_actions_ui/public';
import { ForLastExpression } from '../../../../../../triggers_actions_ui/public';
import {
AlertParams,
Comparator,
@ -36,14 +34,13 @@ interface LogsContextMeta {
isInternal?: boolean;
}
export type AlertsContext = AlertsContextValue<LogsContextMeta>;
interface Props {
errors: Errors;
alertParams: Partial<AlertParams>;
setAlertParams(key: string, value: any): void;
setAlertProperty(key: string, value: any): void;
alertsContext: AlertsContext;
sourceId: string;
metadata: LogsContextMeta;
}
const DEFAULT_CRITERIA = { field: 'log.level', comparator: Comparator.EQ, value: 'error' };
@ -75,8 +72,9 @@ const DEFAULT_RATIO_EXPRESSION = {
};
export const ExpressionEditor: React.FC<Props> = (props) => {
const isInternal = props.alertsContext.metadata?.isInternal;
const isInternal = props.metadata?.isInternal;
const [sourceId] = useSourceId();
const { http } = useKibana().services;
return (
<>
@ -85,7 +83,7 @@ export const ExpressionEditor: React.FC<Props> = (props) => {
<Editor {...props} sourceId={sourceId} />
</SourceStatusWrapper>
) : (
<LogSourceProvider sourceId={sourceId} fetch={props.alertsContext.http.fetch}>
<LogSourceProvider sourceId={sourceId} fetch={http!.fetch}>
<SourceStatusWrapper {...props}>
<Editor {...props} sourceId={sourceId} />
</SourceStatusWrapper>
@ -139,7 +137,7 @@ export const SourceStatusWrapper: React.FC<Props> = (props) => {
};
export const Editor: React.FC<Props> = (props) => {
const { setAlertParams, alertParams, errors, alertsContext, sourceId } = props;
const { setAlertParams, alertParams, errors, sourceId } = props;
const [hasSetDefaults, setHasSetDefaults] = useState<boolean>(false);
const { sourceStatus } = useLogSourceContext();
useMount(() => {
@ -228,7 +226,6 @@ export const Editor: React.FC<Props> = (props) => {
criteria={alertParams.criteria}
errors={errors.criteria}
alertParams={alertParams}
context={alertsContext}
sourceId={sourceId}
updateCriteria={updateCriteria}
/>

View file

@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { useState, useMemo } from 'react';
import { AlertsContext } from '../editor';
import { HttpHandler } from 'kibana/public';
import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public';
import { useTrackedPromise } from '../../../../../utils/use_tracked_promise';
import {
GetLogAlertsChartPreviewDataSuccessResponsePayload,
@ -17,12 +18,13 @@ import { GetLogAlertsChartPreviewDataAlertParamsSubset } from '../../../../../..
interface Options {
sourceId: string;
context: AlertsContext;
alertParams: GetLogAlertsChartPreviewDataAlertParamsSubset;
buckets: number;
}
export const useChartPreviewData = ({ context, sourceId, alertParams, buckets }: Options) => {
export const useChartPreviewData = ({ sourceId, alertParams, buckets }: Options) => {
const { http } = useKibana().services;
const [chartPreviewData, setChartPreviewData] = useState<
GetLogAlertsChartPreviewDataSuccessResponsePayload['data']['series']
>([]);
@ -32,7 +34,7 @@ export const useChartPreviewData = ({ context, sourceId, alertParams, buckets }:
cancelPreviousOn: 'creation',
createPromise: async () => {
setHasError(false);
return await callGetChartPreviewDataAPI(sourceId, context.http.fetch, alertParams, buckets);
return await callGetChartPreviewDataAPI(sourceId, http!.fetch, alertParams, buckets);
},
onResolve: ({ data: { series } }) => {
setHasError(false);
@ -42,7 +44,7 @@ export const useChartPreviewData = ({ context, sourceId, alertParams, buckets }:
setHasError(true);
},
},
[sourceId, context.http.fetch, alertParams, buckets]
[sourceId, http, alertParams, buckets]
);
const isLoading = useMemo(() => getChartPreviewDataRequest.state === 'pending', [
@ -59,7 +61,7 @@ export const useChartPreviewData = ({ context, sourceId, alertParams, buckets }:
export const callGetChartPreviewDataAPI = async (
sourceId: string,
fetch: AlertsContext['http']['fetch'],
fetch: HttpHandler,
alertParams: GetLogAlertsChartPreviewDataAlertParamsSubset,
buckets: number
) => {

View file

@ -4,12 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useContext } from 'react';
import { ApplicationStart, DocLinksStart, HttpStart, NotificationsStart } from 'src/core/public';
import { AlertsContextProvider, AlertAdd } from '../../../../../triggers_actions_ui/public';
import React, { useContext, useMemo } from 'react';
import { TriggerActionsContext } from '../../../utils/triggers_actions_context';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/metric_threshold/types';
import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer';
@ -22,43 +18,26 @@ interface Props {
setVisible: React.Dispatch<React.SetStateAction<boolean>>;
}
interface KibanaDeps {
notifications: NotificationsStart;
http: HttpStart;
docLinks: DocLinksStart;
application: ApplicationStart;
}
export const AlertFlyout = (props: Props) => {
const { triggersActionsUI } = useContext(TriggerActionsContext);
const { services } = useKibana<KibanaDeps>();
return (
<>
{triggersActionsUI && (
<AlertsContextProvider
value={{
metadata: {
currentOptions: props.options,
series: props.series,
},
toastNotifications: services.notifications.toasts,
http: services.http,
docLinks: services.docLinks,
capabilities: services.application.capabilities,
actionTypeRegistry: triggersActionsUI.actionTypeRegistry,
alertTypeRegistry: triggersActionsUI.alertTypeRegistry,
}}
>
<AlertAdd
addFlyoutVisible={props.visible!}
setAddFlyoutVisibility={props.setVisible}
alertTypeId={METRIC_THRESHOLD_ALERT_TYPE_ID}
canChangeTrigger={false}
consumer={'infrastructure'}
/>
</AlertsContextProvider>
)}
</>
const AddAlertFlyout = useMemo(
() =>
triggersActionsUI &&
triggersActionsUI.getAddAlertFlyout({
consumer: 'infrastructure',
addFlyoutVisible: props.visible!,
setAddFlyoutVisibility: props.setVisible,
canChangeTrigger: false,
alertTypeId: METRIC_THRESHOLD_ALERT_TYPE_ID,
metadata: {
currentOptions: props.options,
series: props.series,
},
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[triggersActionsUI, props.visible]
);
return <>{AddAlertFlyout}</>;
};

View file

@ -5,11 +5,8 @@
*/
import { mountWithIntl, nextTick } from '@kbn/test/jest';
import { actionTypeRegistryMock } from '../../../../../triggers_actions_ui/public/application/action_type_registry.mock';
import { alertTypeRegistryMock } from '../../../../../triggers_actions_ui/public/application/alert_type_registry.mock';
import { coreMock } from '../../../../../../../src/core/public/mocks';
import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context';
import { AlertContextMeta } from '../types';
// We are using this inside a `jest.mock` call. Jest requires dynamic dependencies to be prefixed with `mock`
import { coreMock as mockCoreMock } from 'src/core/public/mocks';
import { MetricsExplorerMetric } from '../../../../common/http_api/metrics_explorer';
import React from 'react';
import { Expressions } from './expression';
@ -24,6 +21,12 @@ jest.mock('../../../containers/source/use_source_via_http', () => ({
}),
}));
jest.mock('../../../hooks/use_kibana', () => ({
useKibanaContextForPlugin: () => ({
services: mockCoreMock.createStart(),
}),
}));
describe('Expression', () => {
async function setup(currentOptions: {
metrics?: MetricsExplorerMetric[];
@ -36,43 +39,17 @@ describe('Expression', () => {
filterQueryText: '',
sourceId: 'default',
};
const mocks = coreMock.createSetup();
const startMocks = coreMock.createStart();
const [
{
application: { capabilities },
},
] = await mocks.getStartServices();
const context: AlertsContextValue<AlertContextMeta> = {
http: mocks.http,
toastNotifications: mocks.notifications.toasts,
actionTypeRegistry: actionTypeRegistryMock.create() as any,
alertTypeRegistry: alertTypeRegistryMock.create() as any,
docLinks: startMocks.docLinks,
capabilities: {
...capabilities,
actions: {
delete: true,
save: true,
show: true,
},
},
metadata: {
currentOptions,
},
};
const wrapper = mountWithIntl(
<Expressions
alertsContext={context}
alertInterval="1m"
alertThrottle="1m"
alertParams={alertParams}
errors={[]}
setAlertParams={(key, value) => Reflect.set(alertParams, key, value)}
setAlertProperty={() => {}}
metadata={{
currentOptions,
}}
/>
);

View file

@ -31,8 +31,6 @@ import {
} from '../../../../../triggers_actions_ui/public/common';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { IErrorObject } from '../../../../../triggers_actions_ui/public/types';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context';
import { MetricsExplorerKueryBar } from '../../../pages/metrics/metrics_explorer/components/kuery_bar';
import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options';
import { MetricsExplorerGroupBy } from '../../../pages/metrics/metrics_explorer/components/group_by';
@ -40,20 +38,21 @@ import { useSourceViaHttp } from '../../../containers/source/use_source_via_http
import { convertKueryToElasticSearchQuery } from '../../../utils/kuery';
import { ExpressionRow } from './expression_row';
import { AlertContextMeta, MetricExpression, AlertParams } from '../types';
import { MetricExpression, AlertParams, AlertContextMeta } from '../types';
import { ExpressionChart } from './expression_chart';
import { validateMetricThreshold } from './validation';
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
const FILTER_TYPING_DEBOUNCE_MS = 500;
interface Props {
errors: IErrorObject[];
alertParams: AlertParams;
alertsContext: AlertsContextValue<AlertContextMeta>;
alertInterval: string;
alertThrottle: string;
setAlertParams(key: string, value: any): void;
setAlertProperty(key: string, value: any): void;
metadata: AlertContextMeta;
}
const defaultExpression = {
@ -66,19 +65,13 @@ const defaultExpression = {
export { defaultExpression };
export const Expressions: React.FC<Props> = (props) => {
const {
setAlertParams,
alertParams,
errors,
alertsContext,
alertInterval,
alertThrottle,
} = props;
const { setAlertParams, alertParams, errors, alertInterval, alertThrottle, metadata } = props;
const { http, notifications } = useKibanaContextForPlugin().services;
const { source, createDerivedIndexPattern } = useSourceViaHttp({
sourceId: 'default',
type: 'metrics',
fetch: alertsContext.http.fetch,
toastWarning: alertsContext.toastNotifications.addWarning,
fetch: http.fetch,
toastWarning: notifications.toasts.addWarning,
});
const [timeSize, setTimeSize] = useState<number | undefined>(1);
@ -88,15 +81,15 @@ export const Expressions: React.FC<Props> = (props) => {
]);
const options = useMemo<MetricsExplorerOptions>(() => {
if (alertsContext.metadata?.currentOptions?.metrics) {
return alertsContext.metadata.currentOptions as MetricsExplorerOptions;
if (metadata?.currentOptions?.metrics) {
return metadata.currentOptions as MetricsExplorerOptions;
} else {
return {
metrics: [],
aggregation: 'avg',
};
}
}, [alertsContext.metadata]);
}, [metadata]);
const updateParams = useCallback(
(id, e: MetricExpression) => {
@ -186,7 +179,7 @@ export const Expressions: React.FC<Props> = (props) => {
);
const preFillAlertCriteria = useCallback(() => {
const md = alertsContext.metadata;
const md = metadata;
if (md?.currentOptions?.metrics?.length) {
setAlertParams(
'criteria',
@ -202,10 +195,10 @@ export const Expressions: React.FC<Props> = (props) => {
} else {
setAlertParams('criteria', [defaultExpression]);
}
}, [alertsContext.metadata, setAlertParams, timeSize, timeUnit]);
}, [metadata, setAlertParams, timeSize, timeUnit]);
const preFillAlertFilter = useCallback(() => {
const md = alertsContext.metadata;
const md = metadata;
if (md && md.currentOptions?.filterQuery) {
setAlertParams('filterQueryText', md.currentOptions.filterQuery);
setAlertParams(
@ -223,14 +216,14 @@ export const Expressions: React.FC<Props> = (props) => {
convertKueryToElasticSearchQuery(filter, derivedIndexPattern) || ''
);
}
}, [alertsContext.metadata, derivedIndexPattern, setAlertParams]);
}, [metadata, derivedIndexPattern, setAlertParams]);
const preFillAlertGroupBy = useCallback(() => {
const md = alertsContext.metadata;
const md = metadata;
if (md && md.currentOptions?.groupBy && !md.series) {
setAlertParams('groupBy', md.currentOptions.groupBy);
}
}, [alertsContext.metadata, setAlertParams]);
}, [metadata, setAlertParams]);
useEffect(() => {
if (alertParams.criteria && alertParams.criteria.length) {
@ -251,7 +244,7 @@ export const Expressions: React.FC<Props> = (props) => {
if (!alertParams.sourceId) {
setAlertParams('sourceId', source?.id || 'default');
}
}, [alertsContext.metadata, source]); // eslint-disable-line react-hooks/exhaustive-deps
}, [metadata, source]); // eslint-disable-line react-hooks/exhaustive-deps
const handleFieldSearchChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => onFilterChange(e.target.value),
@ -291,7 +284,6 @@ export const Expressions: React.FC<Props> = (props) => {
>
<ExpressionChart
expression={e}
context={alertsContext}
derivedIndexPattern={derivedIndexPattern}
source={source}
filterQuery={alertParams.filterQueryText}
@ -361,7 +353,7 @@ export const Expressions: React.FC<Props> = (props) => {
fullWidth
display="rowCompressed"
>
{(alertsContext.metadata && (
{(metadata && (
<MetricsExplorerKueryBar
derivedIndexPattern={derivedIndexPattern}
onChange={debouncedOnFilterChange}
@ -407,7 +399,6 @@ export const Expressions: React.FC<Props> = (props) => {
alertParams={pick(alertParams, 'criteria', 'groupBy', 'filterQuery', 'sourceId')}
showNoDataResults={alertParams.alertOnNoData}
validate={validateMetricThreshold}
fetch={alertsContext.http.fetch}
groupByDisplayName={groupByPreviewDisplayName}
/>
<EuiSpacer size={'m'} />

View file

@ -5,11 +5,9 @@
*/
import { mountWithIntl, nextTick } from '@kbn/test/jest';
import { actionTypeRegistryMock } from '../../../../../triggers_actions_ui/public/application/action_type_registry.mock';
import { alertTypeRegistryMock } from '../../../../../triggers_actions_ui/public/application/alert_type_registry.mock';
import { coreMock } from '../../../../../../../src/core/public/mocks';
import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context';
import { AlertContextMeta, MetricExpression } from '../types';
// We are using this inside a `jest.mock` call. Jest requires dynamic dependencies to be prefixed with `mock`
import { coreMock as mockCoreMock } from 'src/core/public/mocks';
import { MetricExpression } from '../types';
import { IIndexPattern } from 'src/plugins/data/public';
import { InfraSource } from '../../../../common/http_api/source_api';
import React from 'react';
@ -17,38 +15,30 @@ import { ExpressionChart } from './expression_chart';
import { act } from 'react-dom/test-utils';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { Aggregators, Comparator } from '../../../../server/lib/alerting/metric_threshold/types';
import { MetricsExplorerResponse } from '../../../../common/http_api';
const mockStartServices = mockCoreMock.createStart();
jest.mock('../../../hooks/use_kibana', () => ({
useKibanaContextForPlugin: () => ({
services: {
...mockStartServices,
},
}),
}));
const mockResponse = {
pageInfo: {
afterKey: null,
total: 0,
},
series: [{ id: 'Everything', rows: [], columns: [] }],
};
jest.mock('../hooks/use_metrics_explorer_chart_data', () => ({
useMetricsExplorerChartData: () => ({ loading: false, data: mockResponse }),
}));
describe('ExpressionChart', () => {
async function setup(
expression: MetricExpression,
response: MetricsExplorerResponse | null,
filterQuery?: string,
groupBy?: string
) {
const mocks = coreMock.createSetup();
const startMocks = coreMock.createStart();
const [
{
application: { capabilities },
},
] = await mocks.getStartServices();
const context: AlertsContextValue<AlertContextMeta> = {
http: mocks.http,
toastNotifications: mocks.notifications.toasts,
actionTypeRegistry: actionTypeRegistryMock.create() as any,
alertTypeRegistry: alertTypeRegistryMock.create() as any,
docLinks: startMocks.docLinks,
capabilities: {
...capabilities,
actions: {
delete: true,
save: true,
show: true,
},
},
};
async function setup(expression: MetricExpression, filterQuery?: string, groupBy?: string) {
const derivedIndexPattern: IIndexPattern = {
title: 'metricbeat-*',
fields: [],
@ -76,11 +66,8 @@ describe('ExpressionChart', () => {
},
};
mocks.http.fetch.mockImplementation(() => Promise.resolve(response));
const wrapper = mountWithIntl(
<ExpressionChart
context={context}
expression={expression}
derivedIndexPattern={derivedIndexPattern}
source={source}
@ -97,7 +84,7 @@ describe('ExpressionChart', () => {
await update();
return { wrapper, update, fetchMock: mocks.http.fetch };
return { wrapper, update };
}
it('should display no data message', async () => {
@ -109,14 +96,7 @@ describe('ExpressionChart', () => {
threshold: [1],
comparator: Comparator.GT_OR_EQ,
};
const response = {
pageInfo: {
afterKey: null,
total: 0,
},
series: [{ id: 'Everything', rows: [], columns: [] }],
};
const { wrapper } = await setup(expression, response);
const { wrapper } = await setup(expression);
expect(wrapper.find('[data-test-subj~="noChartData"]').exists()).toBeTruthy();
});
});

View file

@ -21,8 +21,6 @@ import { i18n } from '@kbn/i18n';
import { EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { IIndexPattern } from 'src/plugins/data/public';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context';
import { InfraSource } from '../../../../common/http_api/source_api';
import {
Comparator,
@ -31,16 +29,16 @@ import {
import { Color, colorTransformer } from '../../../../common/color_palette';
import { MetricsExplorerRow, MetricsExplorerAggregation } from '../../../../common/http_api';
import { MetricExplorerSeriesChart } from '../../../pages/metrics/metrics_explorer/components/series_chart';
import { MetricExpression, AlertContextMeta } from '../types';
import { MetricExpression } from '../types';
import { MetricsExplorerChartType } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options';
import { getChartTheme } from '../../../pages/metrics/metrics_explorer/components/helpers/get_chart_theme';
import { createFormatterForMetric } from '../../../pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metric';
import { calculateDomain } from '../../../pages/metrics/metrics_explorer/components/helpers/calculate_domain';
import { useMetricsExplorerChartData } from '../hooks/use_metrics_explorer_chart_data';
import { getMetricId } from '../../../pages/metrics/metrics_explorer/components/helpers/get_metric_id';
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
interface Props {
context: AlertsContextValue<AlertContextMeta>;
expression: MetricExpression;
derivedIndexPattern: IIndexPattern;
source: InfraSource | null;
@ -62,7 +60,6 @@ const TIME_LABELS = {
export const ExpressionChart: React.FC<Props> = ({
expression,
context,
derivedIndexPattern,
source,
filterQuery,
@ -70,19 +67,20 @@ export const ExpressionChart: React.FC<Props> = ({
}) => {
const { loading, data } = useMetricsExplorerChartData(
expression,
context,
derivedIndexPattern,
source,
filterQuery,
groupBy
);
const { uiSettings } = useKibanaContextForPlugin().services;
const metric = {
field: expression.metric,
aggregation: expression.aggType as MetricsExplorerAggregation,
color: Color.color0,
};
const isDarkMode = context.uiSettings?.get('theme:darkMode') || false;
const isDarkMode = uiSettings?.get('theme:darkMode') || false;
const dateFormatter = useMemo(() => {
const firstSeries = first(data?.series);
const firstTimestamp = first(firstSeries?.rows)?.timestamp;

View file

@ -7,15 +7,12 @@
import { IIndexPattern } from 'src/plugins/data/public';
import { useMemo } from 'react';
import { InfraSource } from '../../../../common/http_api/source_api';
import { AlertContextMeta, MetricExpression } from '../types';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context';
import { MetricExpression } from '../types';
import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options';
import { useMetricsExplorerData } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data';
export const useMetricsExplorerChartData = (
expression: MetricExpression,
context: AlertsContextValue<AlertContextMeta>,
derivedIndexPattern: IIndexPattern,
source: InfraSource | null,
filterQuery?: string,
@ -54,7 +51,6 @@ export const useMetricsExplorerChartData = (
derivedIndexPattern,
timerange,
null,
null,
context.http.fetch
null
);
};

View file

@ -48,7 +48,6 @@ export const useMetricsExplorerState = (
currentTimerange,
afterKey,
refreshSignal,
undefined,
shouldLoadImmediately
);

View file

@ -7,7 +7,6 @@
import DateMath from '@elastic/datemath';
import { isEqual } from 'lodash';
import { useEffect, useState, useCallback } from 'react';
import { HttpHandler } from 'src/core/public';
import { IIndexPattern } from 'src/plugins/data/public';
import { SourceQuery } from '../../../../../common/graphql/types';
import {
@ -30,11 +29,10 @@ export function useMetricsExplorerData(
timerange: MetricsExplorerTimeOptions,
afterKey: string | null | Record<string, string | null>,
signal: any,
fetch?: HttpHandler,
shouldLoadImmediately = true
) {
const kibana = useKibana();
const fetchFn = fetch ? fetch : kibana.services.http?.fetch;
const fetchFn = kibana.services.http?.fetch;
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [data, setData] = useState<MetricsExplorerResponse | null>(null);

View file

@ -24,5 +24,5 @@
],
"server": true,
"ui": true,
"requiredBundles": ["kibanaUtils", "home", "alerts", "kibanaReact", "licenseManagement", "triggersActionsUi"]
"requiredBundles": ["kibanaUtils", "home", "alerts", "kibanaReact", "licenseManagement"]
}

View file

@ -18,7 +18,6 @@ import { alertTypeRegistryMock } from '../../../triggers_actions_ui/public/appli
import { ValidationResult, Alert } from '../../../triggers_actions_ui/public/types';
import { AlertForm } from '../../../triggers_actions_ui/public/application/sections/alert_form/alert_form';
import ActionForm from '../../../triggers_actions_ui/public/application/sections/action_connector_form/action_form';
import { AlertsContextProvider } from '../../../triggers_actions_ui/public/application/context/alerts_context';
import { Legacy } from '../legacy_shims';
import { I18nProvider } from '@kbn/i18n/react';
import { createKibanaReactContext } from '../../../../../src/plugins/kibana_react/public';
@ -100,7 +99,6 @@ describe('alert_form', () => {
let wrapper: ReactWrapper<any>;
beforeEach(async () => {
const coreStart = coreMock.createStart();
alertTypeRegistry.list.mockReturnValue([alertType]);
alertTypeRegistry.get.mockReturnValue(alertType);
alertTypeRegistry.has.mockReturnValue(true);
@ -108,12 +106,7 @@ describe('alert_form', () => {
actionTypeRegistry.has.mockReturnValue(true);
actionTypeRegistry.get.mockReturnValue(actionType);
const monitoringDependencies = {
toastNotifications: coreStart.notifications.toasts,
...Legacy.shims.kibanaServices,
actionTypeRegistry,
alertTypeRegistry,
} as any;
const KibanaReactContext = createKibanaReactContext(Legacy.shims.kibanaServices);
const initialAlert = ({
name: 'test',
@ -131,18 +124,18 @@ describe('alert_form', () => {
} as unknown) as Alert;
wrapper = mountWithIntl(
<AlertsContextProvider
value={{
...monitoringDependencies,
}}
>
<AlertForm
alert={initialAlert}
dispatch={() => {}}
errors={{ name: [], interval: [] }}
operation="create"
/>
</AlertsContextProvider>
<I18nProvider>
<KibanaReactContext.Provider>
<AlertForm
alert={initialAlert}
dispatch={() => {}}
errors={{ name: [], interval: [] }}
operation="create"
actionTypeRegistry={actionTypeRegistry}
alertTypeRegistry={alertTypeRegistry}
/>
</KibanaReactContext.Provider>
</I18nProvider>
);
await act(async () => {

View file

@ -3,7 +3,7 @@
* 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, { Fragment } from 'react';
import React, { Fragment, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
@ -21,8 +21,6 @@ import {
import { CommonAlertStatus, CommonAlertState, AlertMessage } from '../../common/types/alerts';
import { Legacy } from '../legacy_shims';
import { replaceTokens } from './lib/replace_tokens';
import { AlertsContextProvider } from '../../../triggers_actions_ui/public';
import { AlertEdit } from '../../../triggers_actions_ui/public';
import { isInSetupMode, hideBottomBar, showBottomBar } from '../lib/setup_mode';
import { BASE_ALERT_API_PATH } from '../../../alerts/common';
import { SetupModeContext } from '../components/setup_mode/setup_mode_context';
@ -44,6 +42,20 @@ export const AlertPanel: React.FC<Props> = (props: Props) => {
const [isSaving, setIsSaving] = React.useState(false);
const inSetupMode = isInSetupMode(React.useContext(SetupModeContext));
const flyoutUi = useMemo(
() =>
showFlyout &&
Legacy.shims.triggersActionsUi.getEditAlertFlyout({
initialAlert: alert.rawAlert,
onClose: () => {
setShowFlyout(false);
showBottomBar();
},
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[showFlyout]
);
if (!alert.rawAlert) {
return null;
}
@ -105,29 +117,6 @@ export const AlertPanel: React.FC<Props> = (props: Props) => {
setIsSaving(false);
}
const flyoutUi = showFlyout ? (
<AlertsContextProvider
value={{
http: Legacy.shims.http,
actionTypeRegistry: Legacy.shims.actionTypeRegistry,
alertTypeRegistry: Legacy.shims.alertTypeRegistry,
toastNotifications: Legacy.shims.toastNotifications,
uiSettings: Legacy.shims.uiSettings,
docLinks: Legacy.shims.docLinks,
reloadAlerts: async () => {},
capabilities: Legacy.shims.capabilities,
}}
>
<AlertEdit
initialAlert={alert.rawAlert}
onClose={() => {
setShowFlyout(false);
showBottomBar();
}}
/>
</AlertsContextProvider>
) : null;
const configurationUi = (
<Fragment>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center" gutterSize="m">

View file

@ -8,7 +8,7 @@ import { CoreStart, HttpSetup, IUiSettingsClient } from 'kibana/public';
import { Observable } from 'rxjs';
import { HttpRequestInit } from '../../../../src/core/public';
import { MonitoringStartPluginDependencies } from './types';
import { TriggersAndActionsUIPublicPluginSetup } from '../../triggers_actions_ui/public';
import { TriggersAndActionsUIPublicPluginStart } from '../../triggers_actions_ui/public';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { TypeRegistry } from '../../triggers_actions_ui/public/application/type_registry';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
@ -59,7 +59,7 @@ export interface IShims {
kfetchOptions?: KFetchKibanaOptions | undefined
) => Promise<any>;
isCloud: boolean;
triggersActionsUi: TriggersAndActionsUIPublicPluginSetup;
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
usageCollection: UsageCollectionSetup;
kibanaServices: CoreStart & { usageCollection: UsageCollectionSetup };
}

View file

@ -93,7 +93,7 @@ export class MonitoringPlugin
isCloud: Boolean(plugins.cloud?.isCloudEnabled),
pluginInitializerContext: this.initializerContext,
externalConfig: this.getExternalConfig(),
triggersActionsUi: plugins.triggersActionsUi,
triggersActionsUi: pluginsStart.triggersActionsUi,
usageCollection: plugins.usageCollection,
};

View file

@ -7,7 +7,7 @@
import { PluginInitializerContext, CoreStart } from 'kibana/public';
import { NavigationPublicPluginStart as NavigationStart } from '../../../../src/plugins/navigation/public';
import { DataPublicPluginStart } from '../../../../src/plugins/data/public';
import { TriggersAndActionsUIPublicPluginSetup } from '../../triggers_actions_ui/public';
import { TriggersAndActionsUIPublicPluginStart } from '../../triggers_actions_ui/public';
import { KibanaLegacyStart } from '../../../../src/plugins/kibana_legacy/public';
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public';
@ -23,6 +23,6 @@ export interface MonitoringStartPluginDependencies {
isCloud: boolean;
pluginInitializerContext: PluginInitializerContext;
externalConfig: Array<Array<string | number> | Array<string | boolean>>;
triggersActionsUi: TriggersAndActionsUIPublicPluginSetup;
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
usageCollection: UsageCollectionSetup;
}

View file

@ -7,9 +7,9 @@ import { lazy } from 'react';
import { i18n } from '@kbn/i18n';
import { validateExpression } from './validation';
import { GeoContainmentAlertParams } from './types';
import { AlertTypeModel, AlertsContextValue } from '../../../../triggers_actions_ui/public';
import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
export function getAlertType(): AlertTypeModel<GeoContainmentAlertParams, AlertsContextValue> {
export function getAlertType(): AlertTypeModel<GeoContainmentAlertParams> {
return {
id: '.geo-containment',
name: i18n.translate('xpack.stackAlerts.geoContainment.name.trackingContainment', {

View file

@ -16,13 +16,23 @@ exports[`should render BoundaryIndexExpression 1`] = `
labelType="label"
>
<GeoIndexPatternSelect
IndexPatternSelectComponent={null}
http={null}
IndexPatternSelectComponent={[MockFunction]}
includedGeoTypes={
Array [
"geo_shape",
]
}
indexPatternService={
Object {
"clearCache": [MockFunction],
"createField": [MockFunction],
"createFieldList": [MockFunction],
"ensureDefaultIndexPattern": [MockFunction],
"find": [MockFunction],
"get": [MockFunction],
"make": [Function],
}
}
onChange={[Function]}
/>
</EuiFormRow>
@ -82,13 +92,23 @@ exports[`should render EntityIndexExpression 1`] = `
labelType="label"
>
<GeoIndexPatternSelect
IndexPatternSelectComponent={null}
http={null}
IndexPatternSelectComponent={[MockFunction]}
includedGeoTypes={
Array [
"geo_point",
]
}
indexPatternService={
Object {
"clearCache": [MockFunction],
"createField": [MockFunction],
"createFieldList": [MockFunction],
"ensureDefaultIndexPattern": [MockFunction],
"find": [MockFunction],
"get": [MockFunction],
"make": [Function],
}
}
onChange={[Function]}
/>
</EuiFormRow>
@ -154,13 +174,23 @@ exports[`should render EntityIndexExpression w/ invalid flag if invalid 1`] = `
labelType="label"
>
<GeoIndexPatternSelect
IndexPatternSelectComponent={null}
http={null}
IndexPatternSelectComponent={[MockFunction]}
includedGeoTypes={
Array [
"geo_point",
]
}
indexPatternService={
Object {
"clearCache": [MockFunction],
"createField": [MockFunction],
"createFieldList": [MockFunction],
"ensureDefaultIndexPattern": [MockFunction],
"find": [MockFunction],
"get": [MockFunction],
"make": [Function],
}
}
onChange={[Function]}
/>
</EuiFormRow>

View file

@ -7,7 +7,10 @@
import React, { Fragment, FunctionComponent, useEffect, useRef } from 'react';
import { EuiFormRow } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { IErrorObject, AlertsContextValue } from '../../../../../../triggers_actions_ui/public';
import { DataPublicPluginStart } from 'src/plugins/data/public';
import { HttpSetup } from 'kibana/public';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import { IErrorObject } from '../../../../../../triggers_actions_ui/public';
import { ES_GEO_SHAPE_TYPES, GeoContainmentAlertParams } from '../../types';
import { GeoIndexPatternSelect } from '../util_components/geo_index_pattern_select';
import { SingleFieldSelect } from '../util_components/single_field_select';
@ -17,29 +20,33 @@ import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/i
interface Props {
alertParams: GeoContainmentAlertParams;
alertsContext: AlertsContextValue;
errors: IErrorObject;
boundaryIndexPattern: IIndexPattern;
boundaryNameField?: string;
setBoundaryIndexPattern: (boundaryIndexPattern?: IIndexPattern) => void;
setBoundaryGeoField: (boundaryGeoField?: string) => void;
setBoundaryNameField: (boundaryNameField?: string) => void;
data: DataPublicPluginStart;
}
interface KibanaDeps {
http: HttpSetup;
}
export const BoundaryIndexExpression: FunctionComponent<Props> = ({
alertParams,
alertsContext,
errors,
boundaryIndexPattern,
boundaryNameField,
setBoundaryIndexPattern,
setBoundaryGeoField,
setBoundaryNameField,
data,
}) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
const BOUNDARY_NAME_ENTITY_TYPES = ['string', 'number', 'ip'];
const { dataUi, dataIndexPatterns, http } = alertsContext;
const IndexPatternSelect = (dataUi && dataUi.IndexPatternSelect) || null;
const { http } = useKibana<KibanaDeps>().services;
const IndexPatternSelect = (data.ui && data.ui.IndexPatternSelect) || null;
const { boundaryGeoField } = alertParams;
// eslint-disable-next-line react-hooks/exhaustive-deps
const nothingSelected: IFieldType = {
@ -110,7 +117,7 @@ export const BoundaryIndexExpression: FunctionComponent<Props> = ({
}}
value={boundaryIndexPattern.id}
IndexPatternSelectComponent={IndexPatternSelect}
indexPatternService={dataIndexPatterns}
indexPatternService={data.indexPatterns}
http={http}
includedGeoTypes={ES_GEO_SHAPE_TYPES}
/>

View file

@ -8,9 +8,11 @@ import React, { Fragment, FunctionComponent, useEffect, useRef } from 'react';
import { EuiFormRow } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { DataPublicPluginStart } from 'src/plugins/data/public';
import { HttpSetup } from 'kibana/public';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import {
IErrorObject,
AlertsContextValue,
AlertTypeParamsExpressionProps,
} from '../../../../../../triggers_actions_ui/public';
import { ES_GEO_FIELD_TYPES } from '../../types';
@ -23,7 +25,6 @@ import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/i
interface Props {
dateField: string;
geoField: string;
alertsContext: AlertsContextValue;
errors: IErrorObject;
setAlertParamsDate: (date: string) => void;
setAlertParamsGeoField: (geoField: string) => void;
@ -31,21 +32,26 @@ interface Props {
setIndexPattern: (indexPattern: IIndexPattern) => void;
indexPattern: IIndexPattern;
isInvalid: boolean;
data: DataPublicPluginStart;
}
interface KibanaDeps {
http: HttpSetup;
}
export const EntityIndexExpression: FunctionComponent<Props> = ({
setAlertParamsDate,
setAlertParamsGeoField,
errors,
alertsContext,
setIndexPattern,
indexPattern,
isInvalid,
dateField: timeField,
geoField,
data,
}) => {
const { dataUi, dataIndexPatterns, http } = alertsContext;
const IndexPatternSelect = (dataUi && dataUi.IndexPatternSelect) || null;
const { http } = useKibana<KibanaDeps>().services;
const IndexPatternSelect = (data.ui && data.ui.IndexPatternSelect) || null;
const usePrevious = <T extends unknown>(value: T): T | undefined => {
const ref = useRef<T>();
@ -98,7 +104,7 @@ export const EntityIndexExpression: FunctionComponent<Props> = ({
}}
value={indexPattern.id}
IndexPatternSelectComponent={IndexPatternSelect}
indexPatternService={dataIndexPatterns}
indexPatternService={data.indexPatterns}
http={http}
includedGeoTypes={ES_GEO_FIELD_TYPES}
/>

View file

@ -8,22 +8,11 @@ import React from 'react';
import { shallow } from 'enzyme';
import { EntityIndexExpression } from './expressions/entity_index_expression';
import { BoundaryIndexExpression } from './expressions/boundary_index_expression';
import { ApplicationStart, DocLinksStart, HttpSetup, ToastsStart } from 'kibana/public';
import {
ActionTypeRegistryContract,
AlertTypeRegistryContract,
IErrorObject,
} from '../../../../../triggers_actions_ui/public';
import { IErrorObject } from '../../../../../triggers_actions_ui/public';
import { IIndexPattern } from '../../../../../../../src/plugins/data/common';
import { dataPluginMock } from 'src/plugins/data/public/mocks';
const alertsContext = {
http: (null as unknown) as HttpSetup,
alertTypeRegistry: (null as unknown) as AlertTypeRegistryContract,
actionTypeRegistry: (null as unknown) as ActionTypeRegistryContract,
toastNotifications: (null as unknown) as ToastsStart,
docLinks: (null as unknown) as DocLinksStart,
capabilities: (null as unknown) as ApplicationStart['capabilities'],
};
const dataStartMock = dataPluginMock.createStartContract();
const alertParams = {
index: '',
@ -42,7 +31,6 @@ test('should render EntityIndexExpression', async () => {
<EntityIndexExpression
dateField={'testDateField'}
geoField={'testGeoField'}
alertsContext={alertsContext}
errors={{} as IErrorObject}
setAlertParamsDate={() => {}}
setAlertParamsGeoField={() => {}}
@ -50,6 +38,7 @@ test('should render EntityIndexExpression', async () => {
setIndexPattern={() => {}}
indexPattern={('' as unknown) as IIndexPattern}
isInvalid={false}
data={dataStartMock}
/>
);
@ -61,7 +50,6 @@ test('should render EntityIndexExpression w/ invalid flag if invalid', async ()
<EntityIndexExpression
dateField={'testDateField'}
geoField={'testGeoField'}
alertsContext={alertsContext}
errors={{} as IErrorObject}
setAlertParamsDate={() => {}}
setAlertParamsGeoField={() => {}}
@ -69,6 +57,7 @@ test('should render EntityIndexExpression w/ invalid flag if invalid', async ()
setIndexPattern={() => {}}
indexPattern={('' as unknown) as IIndexPattern}
isInvalid={true}
data={dataStartMock}
/>
);
@ -79,13 +68,13 @@ test('should render BoundaryIndexExpression', async () => {
const component = shallow(
<BoundaryIndexExpression
alertParams={alertParams}
alertsContext={alertsContext}
errors={{} as IErrorObject}
boundaryIndexPattern={('' as unknown) as IIndexPattern}
setBoundaryIndexPattern={() => {}}
setBoundaryGeoField={() => {}}
setBoundaryNameField={() => {}}
boundaryNameField={'testNameField'}
data={dataStartMock}
/>
);

View file

@ -8,10 +8,7 @@ import React, { Fragment, useEffect, useState } from 'react';
import { EuiCallOut, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import {
AlertTypeParamsExpressionProps,
AlertsContextValue,
} from '../../../../../triggers_actions_ui/public';
import { AlertTypeParamsExpressionProps } from '../../../../../triggers_actions_ui/public';
import { GeoContainmentAlertParams } from '../types';
import { EntityIndexExpression } from './expressions/entity_index_expression';
import { EntityByExpression } from './expressions/entity_by_expression';
@ -52,8 +49,8 @@ function validateQuery(query: Query) {
}
export const GeoContainmentAlertTypeExpression: React.FunctionComponent<
AlertTypeParamsExpressionProps<GeoContainmentAlertParams, AlertsContextValue>
> = ({ alertParams, alertInterval, setAlertParams, setAlertProperty, errors, alertsContext }) => {
AlertTypeParamsExpressionProps<GeoContainmentAlertParams>
> = ({ alertParams, alertInterval, setAlertParams, setAlertProperty, errors, data }) => {
const {
index,
indexId,
@ -137,15 +134,15 @@ export const GeoContainmentAlertTypeExpression: React.FunctionComponent<
boundaryGeoField: boundaryGeoField ?? DEFAULT_VALUES.BOUNDARY_GEO_FIELD,
boundaryNameField: boundaryNameField ?? DEFAULT_VALUES.BOUNDARY_NAME_FIELD,
});
if (!alertsContext.dataIndexPatterns) {
if (!data.indexPatterns) {
return;
}
if (indexId) {
const _indexPattern = await alertsContext.dataIndexPatterns.get(indexId);
const _indexPattern = await data.indexPatterns.get(indexId);
setIndexPattern(_indexPattern);
}
if (boundaryIndexId) {
const _boundaryIndexPattern = await alertsContext.dataIndexPatterns.get(boundaryIndexId);
const _boundaryIndexPattern = await data.indexPatterns.get(boundaryIndexId);
setBoundaryIndexPattern(_boundaryIndexPattern);
}
};
@ -175,7 +172,6 @@ export const GeoContainmentAlertTypeExpression: React.FunctionComponent<
<EntityIndexExpression
dateField={dateField}
geoField={geoField}
alertsContext={alertsContext}
errors={errors}
setAlertParamsDate={(_date) => setAlertParams('dateField', _date)}
setAlertParamsGeoField={(_geoField) => setAlertParams('geoField', _geoField)}
@ -183,6 +179,7 @@ export const GeoContainmentAlertTypeExpression: React.FunctionComponent<
setIndexPattern={setIndexPattern}
indexPattern={indexPattern}
isInvalid={!indexId || !dateField || !geoField}
data={data}
/>
<EntityByExpression
errors={errors}
@ -220,7 +217,6 @@ export const GeoContainmentAlertTypeExpression: React.FunctionComponent<
<EuiSpacer size="s" />
<BoundaryIndexExpression
alertParams={alertParams}
alertsContext={alertsContext}
errors={errors}
boundaryIndexPattern={boundaryIndexPattern}
setBoundaryIndexPattern={setBoundaryIndexPattern}
@ -233,6 +229,7 @@ export const GeoContainmentAlertTypeExpression: React.FunctionComponent<
: setAlertParams('boundaryNameField', '')
}
boundaryNameField={boundaryNameField}
data={data}
/>
<EuiSpacer size="s" />
<EuiFlexItem>

View file

@ -7,9 +7,9 @@ import { lazy } from 'react';
import { i18n } from '@kbn/i18n';
import { validateExpression } from './validation';
import { GeoThresholdAlertParams } from './types';
import { AlertTypeModel, AlertsContextValue } from '../../../../triggers_actions_ui/public';
import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
export function getAlertType(): AlertTypeModel<GeoThresholdAlertParams, AlertsContextValue> {
export function getAlertType(): AlertTypeModel<GeoThresholdAlertParams> {
return {
id: '.geo-threshold',
name: i18n.translate('xpack.stackAlerts.geoThreshold.name.trackingThreshold', {

View file

@ -16,13 +16,23 @@ exports[`should render BoundaryIndexExpression 1`] = `
labelType="label"
>
<GeoIndexPatternSelect
IndexPatternSelectComponent={null}
http={null}
IndexPatternSelectComponent={[MockFunction]}
includedGeoTypes={
Array [
"geo_shape",
]
}
indexPatternService={
Object {
"clearCache": [MockFunction],
"createField": [MockFunction],
"createFieldList": [MockFunction],
"ensureDefaultIndexPattern": [MockFunction],
"find": [MockFunction],
"get": [MockFunction],
"make": [Function],
}
}
onChange={[Function]}
/>
</EuiFormRow>
@ -82,13 +92,23 @@ exports[`should render EntityIndexExpression 1`] = `
labelType="label"
>
<GeoIndexPatternSelect
IndexPatternSelectComponent={null}
http={null}
IndexPatternSelectComponent={[MockFunction]}
includedGeoTypes={
Array [
"geo_point",
]
}
indexPatternService={
Object {
"clearCache": [MockFunction],
"createField": [MockFunction],
"createFieldList": [MockFunction],
"ensureDefaultIndexPattern": [MockFunction],
"find": [MockFunction],
"get": [MockFunction],
"make": [Function],
}
}
onChange={[Function]}
/>
</EuiFormRow>
@ -154,13 +174,23 @@ exports[`should render EntityIndexExpression w/ invalid flag if invalid 1`] = `
labelType="label"
>
<GeoIndexPatternSelect
IndexPatternSelectComponent={null}
http={null}
IndexPatternSelectComponent={[MockFunction]}
includedGeoTypes={
Array [
"geo_point",
]
}
indexPatternService={
Object {
"clearCache": [MockFunction],
"createField": [MockFunction],
"createFieldList": [MockFunction],
"ensureDefaultIndexPattern": [MockFunction],
"find": [MockFunction],
"get": [MockFunction],
"make": [Function],
}
}
onChange={[Function]}
/>
</EuiFormRow>

View file

@ -7,7 +7,10 @@
import React, { Fragment, FunctionComponent, useEffect, useRef } from 'react';
import { EuiFormRow } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { IErrorObject, AlertsContextValue } from '../../../../../../triggers_actions_ui/public';
import { DataPublicPluginStart } from 'src/plugins/data/public';
import { HttpSetup } from 'kibana/public';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import { IErrorObject } from '../../../../../../triggers_actions_ui/public';
import { ES_GEO_SHAPE_TYPES, GeoThresholdAlertParams } from '../../types';
import { GeoIndexPatternSelect } from '../util_components/geo_index_pattern_select';
import { SingleFieldSelect } from '../util_components/single_field_select';
@ -17,29 +20,33 @@ import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/i
interface Props {
alertParams: GeoThresholdAlertParams;
alertsContext: AlertsContextValue;
errors: IErrorObject;
boundaryIndexPattern: IIndexPattern;
boundaryNameField?: string;
setBoundaryIndexPattern: (boundaryIndexPattern?: IIndexPattern) => void;
setBoundaryGeoField: (boundaryGeoField?: string) => void;
setBoundaryNameField: (boundaryNameField?: string) => void;
data: DataPublicPluginStart;
}
interface KibanaDeps {
http: HttpSetup;
}
export const BoundaryIndexExpression: FunctionComponent<Props> = ({
alertParams,
alertsContext,
errors,
boundaryIndexPattern,
boundaryNameField,
setBoundaryIndexPattern,
setBoundaryGeoField,
setBoundaryNameField,
data,
}) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
const BOUNDARY_NAME_ENTITY_TYPES = ['string', 'number', 'ip'];
const { dataUi, dataIndexPatterns, http } = alertsContext;
const IndexPatternSelect = (dataUi && dataUi.IndexPatternSelect) || null;
const { http } = useKibana<KibanaDeps>().services;
const IndexPatternSelect = (data.ui && data.ui.IndexPatternSelect) || null;
const { boundaryGeoField } = alertParams;
// eslint-disable-next-line react-hooks/exhaustive-deps
const nothingSelected: IFieldType = {
@ -110,7 +117,7 @@ export const BoundaryIndexExpression: FunctionComponent<Props> = ({
}}
value={boundaryIndexPattern.id}
IndexPatternSelectComponent={IndexPatternSelect}
indexPatternService={dataIndexPatterns}
indexPatternService={data.indexPatterns}
http={http}
includedGeoTypes={ES_GEO_SHAPE_TYPES}
/>

View file

@ -8,9 +8,10 @@ import React, { Fragment, FunctionComponent, useEffect, useRef } from 'react';
import { EuiFormRow } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { DataPublicPluginStart } from 'src/plugins/data/public';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import {
IErrorObject,
AlertsContextValue,
AlertTypeParamsExpressionProps,
} from '../../../../../../triggers_actions_ui/public';
import { ES_GEO_FIELD_TYPES } from '../../types';
@ -23,7 +24,6 @@ import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/i
interface Props {
dateField: string;
geoField: string;
alertsContext: AlertsContextValue;
errors: IErrorObject;
setAlertParamsDate: (date: string) => void;
setAlertParamsGeoField: (geoField: string) => void;
@ -31,21 +31,22 @@ interface Props {
setIndexPattern: (indexPattern: IIndexPattern) => void;
indexPattern: IIndexPattern;
isInvalid: boolean;
data: DataPublicPluginStart;
}
export const EntityIndexExpression: FunctionComponent<Props> = ({
setAlertParamsDate,
setAlertParamsGeoField,
errors,
alertsContext,
setIndexPattern,
indexPattern,
isInvalid,
dateField: timeField,
geoField,
data,
}) => {
const { dataUi, dataIndexPatterns, http } = alertsContext;
const IndexPatternSelect = (dataUi && dataUi.IndexPatternSelect) || null;
const { http } = useKibana().services;
const IndexPatternSelect = (data.ui && data.ui.IndexPatternSelect) || null;
const usePrevious = <T extends unknown>(value: T): T | undefined => {
const ref = useRef<T>();
@ -98,8 +99,8 @@ export const EntityIndexExpression: FunctionComponent<Props> = ({
}}
value={indexPattern.id}
IndexPatternSelectComponent={IndexPatternSelect}
indexPatternService={dataIndexPatterns}
http={http}
indexPatternService={data.indexPatterns}
http={http!}
includedGeoTypes={ES_GEO_FIELD_TYPES}
/>
</EuiFormRow>

View file

@ -8,22 +8,9 @@ import React from 'react';
import { shallow } from 'enzyme';
import { EntityIndexExpression } from './expressions/entity_index_expression';
import { BoundaryIndexExpression } from './expressions/boundary_index_expression';
import { ApplicationStart, DocLinksStart, HttpSetup, ToastsStart } from 'kibana/public';
import {
ActionTypeRegistryContract,
AlertTypeRegistryContract,
IErrorObject,
} from '../../../../../triggers_actions_ui/public';
import { IErrorObject } from '../../../../../triggers_actions_ui/public';
import { IIndexPattern } from '../../../../../../../src/plugins/data/common';
const alertsContext = {
http: (null as unknown) as HttpSetup,
alertTypeRegistry: (null as unknown) as AlertTypeRegistryContract,
actionTypeRegistry: (null as unknown) as ActionTypeRegistryContract,
toastNotifications: (null as unknown) as ToastsStart,
docLinks: (null as unknown) as DocLinksStart,
capabilities: (null as unknown) as ApplicationStart['capabilities'],
};
import { dataPluginMock } from 'src/plugins/data/public/mocks';
const alertParams = {
index: '',
@ -38,12 +25,13 @@ const alertParams = {
boundaryGeoField: '',
};
const dataStartMock = dataPluginMock.createStartContract();
test('should render EntityIndexExpression', async () => {
const component = shallow(
<EntityIndexExpression
dateField={'testDateField'}
geoField={'testGeoField'}
alertsContext={alertsContext}
errors={{} as IErrorObject}
setAlertParamsDate={() => {}}
setAlertParamsGeoField={() => {}}
@ -51,6 +39,7 @@ test('should render EntityIndexExpression', async () => {
setIndexPattern={() => {}}
indexPattern={('' as unknown) as IIndexPattern}
isInvalid={false}
data={dataStartMock}
/>
);
@ -62,7 +51,6 @@ test('should render EntityIndexExpression w/ invalid flag if invalid', async ()
<EntityIndexExpression
dateField={'testDateField'}
geoField={'testGeoField'}
alertsContext={alertsContext}
errors={{} as IErrorObject}
setAlertParamsDate={() => {}}
setAlertParamsGeoField={() => {}}
@ -70,6 +58,7 @@ test('should render EntityIndexExpression w/ invalid flag if invalid', async ()
setIndexPattern={() => {}}
indexPattern={('' as unknown) as IIndexPattern}
isInvalid={true}
data={dataStartMock}
/>
);
@ -80,13 +69,13 @@ test('should render BoundaryIndexExpression', async () => {
const component = shallow(
<BoundaryIndexExpression
alertParams={alertParams}
alertsContext={alertsContext}
errors={{} as IErrorObject}
boundaryIndexPattern={('' as unknown) as IIndexPattern}
setBoundaryIndexPattern={() => {}}
setBoundaryGeoField={() => {}}
setBoundaryNameField={() => {}}
boundaryNameField={'testNameField'}
data={dataStartMock}
/>
);

View file

@ -22,7 +22,6 @@ import { i18n } from '@kbn/i18n';
import {
AlertTypeParamsExpressionProps,
getTimeOptions,
AlertsContextValue,
} from '../../../../../triggers_actions_ui/public';
import { GeoThresholdAlertParams, TrackingEvent } from '../types';
import { ExpressionWithPopover } from './util_components/expression_with_popover';
@ -86,8 +85,8 @@ function validateQuery(query: Query) {
}
export const GeoThresholdAlertTypeExpression: React.FunctionComponent<
AlertTypeParamsExpressionProps<GeoThresholdAlertParams, AlertsContextValue>
> = ({ alertParams, alertInterval, setAlertParams, setAlertProperty, errors, alertsContext }) => {
AlertTypeParamsExpressionProps<GeoThresholdAlertParams>
> = ({ alertParams, alertInterval, setAlertParams, setAlertProperty, errors, data }) => {
const {
index,
indexId,
@ -181,15 +180,15 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent<
boundaryNameField: boundaryNameField ?? DEFAULT_VALUES.BOUNDARY_NAME_FIELD,
delayOffsetWithUnits: delayOffsetWithUnits ?? DEFAULT_VALUES.DELAY_OFFSET_WITH_UNITS,
});
if (!alertsContext.dataIndexPatterns) {
if (!data?.indexPatterns) {
return;
}
if (indexId) {
const _indexPattern = await alertsContext.dataIndexPatterns.get(indexId);
const _indexPattern = await data?.indexPatterns.get(indexId);
setIndexPattern(_indexPattern);
}
if (boundaryIndexId) {
const _boundaryIndexPattern = await alertsContext.dataIndexPatterns.get(boundaryIndexId);
const _boundaryIndexPattern = await data?.indexPatterns.get(boundaryIndexId);
setBoundaryIndexPattern(_boundaryIndexPattern);
}
if (delayOffsetWithUnits) {
@ -263,7 +262,6 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent<
<EntityIndexExpression
dateField={dateField}
geoField={geoField}
alertsContext={alertsContext}
errors={errors}
setAlertParamsDate={(_date) => setAlertParams('dateField', _date)}
setAlertParamsGeoField={(_geoField) => setAlertParams('geoField', _geoField)}
@ -271,6 +269,7 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent<
setIndexPattern={setIndexPattern}
indexPattern={indexPattern}
isInvalid={!indexId || !dateField || !geoField}
data={data}
/>
<EntityByExpression
errors={errors}
@ -347,7 +346,6 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent<
<EuiSpacer size="s" />
<BoundaryIndexExpression
alertParams={alertParams}
alertsContext={alertsContext}
errors={errors}
boundaryIndexPattern={boundaryIndexPattern}
setBoundaryIndexPattern={setBoundaryIndexPattern}
@ -360,6 +358,7 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent<
: setAlertParams('boundaryNameField', '')
}
boundaryNameField={boundaryNameField}
data={data}
/>
<EuiSpacer size="s" />
<EuiFlexItem>

View file

@ -24,6 +24,8 @@ import {
EuiTitle,
} from '@elastic/eui';
import { EuiButtonIcon } from '@elastic/eui';
import { HttpSetup } from 'kibana/public';
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
import {
firstFieldOption,
getIndexPatterns,
@ -39,7 +41,6 @@ import {
WhenExpression,
builtInAggregationTypes,
AlertTypeParamsExpressionProps,
AlertsContextValue,
} from '../../../../triggers_actions_ui/public';
import { ThresholdVisualization } from './visualization';
import { IndexThresholdAlertParams } from './types';
@ -66,9 +67,13 @@ const expressionFieldsWithValidation = [
'timeWindowSize',
];
interface KibanaDeps {
http: HttpSetup;
}
export const IndexThresholdAlertTypeExpression: React.FunctionComponent<
AlertTypeParamsExpressionProps<IndexThresholdAlertParams, AlertsContextValue>
> = ({ alertParams, alertInterval, setAlertParams, setAlertProperty, errors, alertsContext }) => {
AlertTypeParamsExpressionProps<IndexThresholdAlertParams>
> = ({ alertParams, alertInterval, setAlertParams, setAlertProperty, errors, charts, data }) => {
const {
index,
timeField,
@ -83,7 +88,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<
timeWindowUnit,
} = alertParams;
const { http } = alertsContext;
const { http } = useKibana<KibanaDeps>().services;
const [indexPopoverOpen, setIndexPopoverOpen] = useState(false);
const [indexPatterns, setIndexPatterns] = useState([]);
@ -208,7 +213,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<
});
return;
}
const currentEsFields = await getFields(http, indices);
const currentEsFields = await getFields(http!, indices);
const timeFields = getTimeFieldOptions(currentEsFields);
setEsFields(currentEsFields);
@ -216,7 +221,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<
}}
onSearchChange={async (search) => {
setIsIndiciesLoading(true);
setIndexOptions(await getIndexOptions(http, search, indexPatterns));
setIndexOptions(await getIndexOptions(http!, search, indexPatterns));
setIsIndiciesLoading(false);
}}
onBlur={() => {
@ -433,7 +438,8 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<
alertInterval={alertInterval}
aggregationTypes={builtInAggregationTypes}
comparators={builtInComparators}
alertsContext={alertsContext}
charts={charts}
dataFieldsFormats={data!.fieldFormats}
/>
</Fragment>
)}

View file

@ -7,9 +7,9 @@ import { lazy } from 'react';
import { i18n } from '@kbn/i18n';
import { validateExpression } from './validation';
import { IndexThresholdAlertParams } from './types';
import { AlertTypeModel, AlertsContextValue } from '../../../../triggers_actions_ui/public';
import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
export function getAlertType(): AlertTypeModel<IndexThresholdAlertParams, AlertsContextValue> {
export function getAlertType(): AlertTypeModel<IndexThresholdAlertParams> {
return {
id: '.index-threshold',
name: i18n.translate('xpack.stackAlerts.threshold.ui.alertType.nameText', {

View file

@ -29,15 +29,14 @@ import {
EuiLoadingSpinner,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { ChartsPluginSetup } from 'src/plugins/charts/public';
import { FieldFormatsStart } from 'src/plugins/data/public';
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
import {
getThresholdAlertVisualizationData,
GetThresholdAlertVisualizationDataParams,
} from './index_threshold_api';
import {
AlertsContextValue,
AggregationType,
Comparator,
} from '../../../../triggers_actions_ui/public';
import { AggregationType, Comparator } from '../../../../triggers_actions_ui/public';
import { IndexThresholdAlertParams } from './types';
import { parseDuration } from '../../../../alerts/common/parse_duration';
@ -94,8 +93,9 @@ interface Props {
comparators: {
[key: string]: Comparator;
};
alertsContext: AlertsContextValue;
refreshRateInMilliseconds?: number;
charts: ChartsPluginSetup;
dataFieldsFormats: FieldFormatsStart;
}
const DEFAULT_REFRESH_RATE = 5000;
@ -112,8 +112,9 @@ export const ThresholdVisualization: React.FunctionComponent<Props> = ({
alertInterval,
aggregationTypes,
comparators,
alertsContext,
refreshRateInMilliseconds = DEFAULT_REFRESH_RATE,
charts,
dataFieldsFormats,
}) => {
const {
index,
@ -128,8 +129,7 @@ export const ThresholdVisualization: React.FunctionComponent<Props> = ({
groupBy,
threshold,
} = alertParams;
const { http, toastNotifications, charts, uiSettings, dataFieldsFormats } = alertsContext;
const { http, notifications, uiSettings } = useKibana().services;
const [loadingState, setLoadingState] = useState<LoadingStateType | null>(null);
const [error, setError] = useState<undefined | Error>(undefined);
const [visualizationData, setVisualizationData] = useState<Record<string, MetricResult[]>>();
@ -150,11 +150,11 @@ export const ThresholdVisualization: React.FunctionComponent<Props> = ({
try {
setLoadingState(loadingState ? LoadingStateType.Refresh : LoadingStateType.FirstLoad);
setVisualizationData(
await getVisualizationData(alertWithoutActions, visualizeOptions, http)
await getVisualizationData(alertWithoutActions, visualizeOptions, http!)
);
} catch (e) {
if (toastNotifications) {
toastNotifications.addDanger({
if (notifications) {
notifications.toasts.addDanger({
title: i18n.translate(
'xpack.stackAlerts.threshold.ui.visualization.unableToLoadVisualizationMessage',
{ defaultMessage: 'Unable to load visualization' }

View file

@ -86,7 +86,6 @@ interface IndexThresholdProps {
setAlertParams: (property: string, value: any) => void;
setAlertProperty: (key: string, value: any) => void;
errors: { [key: string]: string[] };
alertsContext: AlertsContextValue;
}
```
@ -96,7 +95,6 @@ interface IndexThresholdProps {
|setAlertParams|Alert reducer method, which is used to create a new copy of alert object with the changed params property any subproperty value.|
|setAlertProperty|Alert reducer method, which is used to create a new copy of alert object with the changed any direct alert property value.|
|errors|Alert level errors tracking object.|
|alertsContext|Alert context, which is used to pass down common objects like http client.|
Alert reducer is defined on the AlertAdd functional component level and passed down to the subcomponents to provide a new state of Alert object:
@ -181,7 +179,6 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<IndexThr
setAlertParams,
setAlertProperty,
errors,
alertsContext,
}) => {
....
@ -244,7 +241,7 @@ Each alert type should be defined as `AlertTypeModel` object with the these prop
iconClass: string;
validate: (alertParams: any) => ValidationResult;
alertParamsExpression: React.LazyExoticComponent<
ComponentType<AlertTypeParamsExpressionProps<AlertParamsType, AlertsContextValue>>
ComponentType<AlertTypeParamsExpressionProps<AlertParamsType>>
>;
defaultActionMessage?: string;
```
@ -787,18 +784,15 @@ This component renders a standard EuiTitle foe each action group, wrapping the A
## Embed the Create Alert flyout within any Kibana plugin
Follow the instructions bellow to embed the Create Alert flyout within any Kibana plugin:
1. Add TriggersAndActionsUIPublicPluginSetup to Kibana plugin setup dependencies:
1. Add TriggersAndActionsUIPublicPluginStart to Kibana plugin setup dependencies:
```
triggersActionsUi: TriggersAndActionsUIPublicPluginSetup;
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
```
Then this dependency will be used to embed Create Alert flyout or register new alert/action type.
Then this dependency will be used to embed Create Alert flyout.
2. Add Create Alert flyout to React component:
2. Add Create Alert flyout to React component using triggersActionsUi start contract:
```
// import section
import { AlertsContextProvider, AlertAdd } from '../../../../../../../triggers_actions_ui/public';
// in the component state definition section
const [alertFlyoutVisible, setAlertFlyoutVisibility] = useState<boolean>(false);
@ -815,26 +809,22 @@ const [alertFlyoutVisible, setAlertFlyoutVisibility] = useState<boolean>(false);
/>
</EuiButton>
const AddAlertFlyout = useMemo(
() =>
triggersActionsUi.getAddAlertFlyout({
consumer: ALERTING_EXAMPLE_APP_ID,
addFlyoutVisible: alertFlyoutVisible,
setAddFlyoutVisibility: setAlertFlyoutVisibility,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[alertFlyoutVisible]
);
// in render section of component
<AlertsContextProvider
value={{
http,
actionTypeRegistry: triggersActionsUi.actionTypeRegistry,
alertTypeRegistry: triggersActionsUi.alertTypeRegistry,
toastNotifications: toasts,
uiSettings,
docLinks,
charts,
dataFieldsFormats,
metadata: { test: 'some value', fields: ['test'] },
}}
>
<AlertAdd consumer={'watcher'} addFlyoutVisible={alertFlyoutVisible}
setAddFlyoutVisibility={setAlertFlyoutVisibility} />
</AlertsContextProvider>
return <>{AddAlertFlyout}</>;
```
AlertAdd Props definition:
getAddAlertFlyout variables definition:
```
interface AlertAddProps {
consumer: string;
@ -842,6 +832,9 @@ interface AlertAddProps {
setAddFlyoutVisibility: React.Dispatch<React.SetStateAction<boolean>>;
alertTypeId?: string;
canChangeTrigger?: boolean;
initialValues?: Partial<Alert>;
reloadAlerts?: () => Promise<void>;
metadata?: MetaData;
}
```
@ -852,37 +845,8 @@ interface AlertAddProps {
|setAddFlyoutVisibility|Function for changing visibility state of the Create Alert flyout.|
|alertTypeId|Optional property to preselect alert type.|
|canChangeTrigger|Optional property, that hides change alert type possibility.|
AlertsContextProvider value options:
```
export interface AlertsContextValue<MetaData = Record<string, any>> {
reloadAlerts?: () => Promise<void>;
http: HttpSetup;
alertTypeRegistry: TypeRegistry<AlertTypeModel>;
actionTypeRegistry: TypeRegistry<ActionTypeModel>;
uiSettings?: IUiSettingsClient;
docLinks: DocLinksStart;
toastNotifications: Pick<
ToastsApi,
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
>;
charts?: ChartsPluginSetup;
dataFieldsFormats?: Pick<FieldFormatsRegistry, 'register'>;
metadata?: MetaData;
}
```
|Property|Description|
|---|---|
|reloadAlerts|Optional function, which will be executed if alert was saved sucsessfuly.|
|http|HttpSetup needed for executing API calls.|
|alertTypeRegistry|Registry for alert types.|
|actionTypeRegistry|Registry for action types.|
|uiSettings|Optional property, which is needed to display visualization of alert type expression. Will be changed after visualization refactoring.|
|docLinks|Documentation Links, needed to link to the documentation from informational callouts.|
|toastNotifications|Toast messages.|
|charts|Optional property, which is needed to display visualization of alert type expression. Will be changed after visualization refactoring.|
|dataFieldsFormats|Optional property, which is needed to display visualization of alert type expression. Will be changed after visualization refactoring.|
|initialValues|Default values for Alert properties.|
|metadata|Optional generic property, which allows to define component specific metadata. This metadata can be used for passing down preloaded data for Alert type expression component.|
## Build and register Action Types
@ -1504,25 +1468,6 @@ interface ActionAccordionFormProps {
|defaultActionMessage|Optional property, which allowes to define a message value for action with 'message' property.|
|capabilities|Kibana core's Capabilities ApplicationStart['capabilities'].|
AlertsContextProvider value options:
```
export interface AlertsContextValue {
reloadAlerts?: () => Promise<void>;
http: HttpSetup;
alertTypeRegistry: TypeRegistry<AlertTypeModel>;
actionTypeRegistry: TypeRegistry<ActionTypeModel>;
uiSettings?: IUiSettingsClient;
docLinks: DocLinksStart;
toastNotifications: Pick<
ToastsApi,
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
>;
charts?: ChartsPluginSetup;
dataFieldsFormats?: Pick<FieldFormatsRegistry, 'register'>;
}
```
|Property|Description|
|---|---|
|reloadAlerts|Optional function, which will be executed if alert was saved sucsessfuly.|
@ -1618,32 +1563,6 @@ export interface ConnectorAddFlyoutProps {
|setAddFlyoutVisibility|Function for changing visibility state of the Create Connector flyout.|
|actionTypes|Optional property, that allows to define only specific action types list which is available for a current plugin.|
ActionsConnectorsContextValue options:
```
export interface ActionsConnectorsContextValue {
http: HttpSetup;
actionTypeRegistry: TypeRegistry<ActionTypeModel>;
toastNotifications: Pick<
ToastsApi,
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
>;
capabilities: ApplicationStart['capabilities'];
docLinks: DocLinksStart;
reloadConnectors?: () => Promise<void>;
consumer: string;
}
```
|Property|Description|
|---|---|
|http|HttpSetup needed for executing API calls.|
|actionTypeRegistry|Registry for action types.|
|capabilities|Property, which is defining action current user usage capabilities like canSave or canDelete.|
|toastNotifications|Toast messages.|
|reloadConnectors|Optional function, which will be executed if connector was saved sucsessfuly, like reload list of connecotrs.|
|consumer|Optional name of the plugin that creates an action.|
## Embed the Edit Connector flyout within any Kibana plugin
Follow the instructions bellow to embed the Edit Connector flyout within any Kibana plugin:

View file

@ -9,20 +9,20 @@ import { render } from '@testing-library/react';
import { HealthCheck } from './health_check';
import { act } from 'react-dom/test-utils';
import { httpServiceMock } from '../../../../../../src/core/public/mocks';
import { HealthContextProvider } from '../context/health_context';
import { useKibana } from '../../common/lib/kibana';
jest.mock('../../common/lib/kibana');
const docLinks = { ELASTIC_WEBSITE_URL: 'elastic.co/', DOC_LINK_VERSION: 'current' };
const http = httpServiceMock.createStartContract();
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
describe('health check', () => {
test('renders spinner while health is loading', async () => {
http.get.mockImplementationOnce(() => new Promise(() => {}));
useKibanaMock().services.http.get = jest
.fn()
.mockImplementationOnce(() => new Promise(() => {}));
const { queryByText, container } = render(
<HealthContextProvider>
<HealthCheck http={http} docLinks={docLinks} waitForCheck={true}>
<HealthCheck waitForCheck={true}>
<p>{'shouldnt render'}</p>
</HealthCheck>
</HealthContextProvider>
@ -36,11 +36,13 @@ describe('health check', () => {
});
it('renders children immediately if waitForCheck is false', async () => {
http.get.mockImplementationOnce(() => new Promise(() => {}));
useKibanaMock().services.http.get = jest
.fn()
.mockImplementationOnce(() => new Promise(() => {}));
const { queryByText, container } = render(
<HealthContextProvider>
<HealthCheck http={http} docLinks={docLinks} waitForCheck={false}>
<HealthCheck waitForCheck={false}>
<p>{'should render'}</p>
</HealthCheck>
</HealthContextProvider>
@ -54,11 +56,12 @@ describe('health check', () => {
});
it('renders children if keys are enabled', async () => {
http.get.mockResolvedValue({ isSufficientlySecure: true, hasPermanentEncryptionKey: true });
useKibanaMock().services.http.get = jest
.fn()
.mockResolvedValue({ isSufficientlySecure: true, hasPermanentEncryptionKey: true });
const { queryByText } = render(
<HealthContextProvider>
<HealthCheck http={http} docLinks={docLinks} waitForCheck={true}>
<HealthCheck waitForCheck={true}>
<p>{'should render'}</p>
</HealthCheck>
</HealthContextProvider>
@ -70,14 +73,13 @@ describe('health check', () => {
});
test('renders warning if keys are disabled', async () => {
http.get.mockImplementationOnce(async () => ({
useKibanaMock().services.http.get = jest.fn().mockImplementationOnce(async () => ({
isSufficientlySecure: false,
hasPermanentEncryptionKey: true,
}));
const { queryAllByText } = render(
<HealthContextProvider>
<HealthCheck http={http} docLinks={docLinks} waitForCheck={true}>
<HealthCheck waitForCheck={true}>
<p>{'should render'}</p>
</HealthCheck>
</HealthContextProvider>
@ -97,19 +99,18 @@ describe('health check', () => {
);
expect(action.getAttribute('href')).toMatchInlineSnapshot(
`"elastic.co/guide/en/kibana/current/configuring-tls.html"`
`"https://www.elastic.co/guide/en/kibana/mocked-test-branch/configuring-tls.html"`
);
});
test('renders warning if encryption key is ephemeral', async () => {
http.get.mockImplementationOnce(async () => ({
useKibanaMock().services.http.get = jest.fn().mockImplementationOnce(async () => ({
isSufficientlySecure: true,
hasPermanentEncryptionKey: false,
}));
const { queryByText, queryByRole } = render(
<HealthContextProvider>
<HealthCheck http={http} docLinks={docLinks} waitForCheck={true}>
<HealthCheck waitForCheck={true}>
<p>{'should render'}</p>
</HealthCheck>
</HealthContextProvider>
@ -126,19 +127,19 @@ describe('health check', () => {
const action = queryByText(/Learn/i);
expect(action!.textContent).toMatchInlineSnapshot(`"Learn how.(opens in a new tab or window)"`);
expect(action!.getAttribute('href')).toMatchInlineSnapshot(
`"elastic.co/guide/en/kibana/current/alert-action-settings-kb.html#general-alert-action-settings"`
`"https://www.elastic.co/guide/en/kibana/mocked-test-branch/alert-action-settings-kb.html#general-alert-action-settings"`
);
});
test('renders warning if encryption key is ephemeral and keys are disabled', async () => {
http.get.mockImplementationOnce(async () => ({
useKibanaMock().services.http.get = jest.fn().mockImplementationOnce(async () => ({
isSufficientlySecure: false,
hasPermanentEncryptionKey: false,
}));
const { queryByText } = render(
<HealthContextProvider>
<HealthCheck http={http} docLinks={docLinks} waitForCheck={true}>
<HealthCheck waitForCheck={true}>
<p>{'should render'}</p>
</HealthCheck>
</HealthContextProvider>
@ -156,7 +157,7 @@ describe('health check', () => {
const action = queryByText(/Learn/i);
expect(action!.textContent).toMatchInlineSnapshot(`"Learn how(opens in a new tab or window)"`);
expect(action!.getAttribute('href')).toMatchInlineSnapshot(
`"elastic.co/guide/en/kibana/current/alerting-getting-started.html#alerting-setup-prerequisites"`
`"https://www.elastic.co/guide/en/kibana/mocked-test-branch/alerting-getting-started.html#alerting-setup-prerequisites"`
);
});
});

View file

@ -12,28 +12,25 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { EuiLink, EuiLoadingSpinner } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { DocLinksStart, HttpSetup } from 'kibana/public';
import { EuiEmptyPrompt, EuiCode } from '@elastic/eui';
import { DocLinksStart } from 'kibana/public';
import { AlertingFrameworkHealth } from '../../types';
import { health } from '../lib/alert_api';
import './health_check.scss';
import { useHealthContext } from '../context/health_context';
import { useKibana } from '../../common/lib/kibana';
interface Props {
docLinks: Pick<DocLinksStart, 'ELASTIC_WEBSITE_URL' | 'DOC_LINK_VERSION'>;
http: HttpSetup;
inFlyout?: boolean;
waitForCheck: boolean;
}
export const HealthCheck: React.FunctionComponent<Props> = ({
docLinks,
http,
children,
waitForCheck,
inFlyout = false,
}) => {
const { http, docLinks } = useKibana().services;
const { setLoadingHealthCheck } = useHealthContext();
const [alertingHealth, setAlertingHealth] = React.useState<Option<AlertingFrameworkHealth>>(none);
@ -66,9 +63,10 @@ export const HealthCheck: React.FunctionComponent<Props> = ({
);
};
type PromptErrorProps = Pick<Props, 'docLinks'> & {
interface PromptErrorProps {
docLinks: Pick<DocLinksStart, 'ELASTIC_WEBSITE_URL' | 'DOC_LINK_VERSION'>;
className?: string;
};
}
const TlsAndEncryptionError = ({
// eslint-disable-next-line @typescript-eslint/naming-convention

View file

@ -1,59 +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, { useContext, createContext } from 'react';
import {
HttpSetup,
IUiSettingsClient,
ToastsStart,
DocLinksStart,
ApplicationStart,
} from 'kibana/public';
import { ChartsPluginSetup } from 'src/plugins/charts/public';
import {
DataPublicPluginSetup,
DataPublicPluginStartUi,
IndexPatternsContract,
} from 'src/plugins/data/public';
import { KibanaFeature } from '../../../../features/common';
import { AlertTypeRegistryContract, ActionTypeRegistryContract } from '../../types';
export interface AlertsContextValue<MetaData = Record<string, any>> {
reloadAlerts?: () => Promise<void>;
http: HttpSetup;
alertTypeRegistry: AlertTypeRegistryContract;
actionTypeRegistry: ActionTypeRegistryContract;
toastNotifications: ToastsStart;
uiSettings?: IUiSettingsClient;
charts?: ChartsPluginSetup;
docLinks: DocLinksStart;
capabilities: ApplicationStart['capabilities'];
dataFieldsFormats?: DataPublicPluginSetup['fieldFormats'];
metadata?: MetaData;
dataUi?: DataPublicPluginStartUi;
dataIndexPatterns?: IndexPatternsContract;
kibanaFeatures?: KibanaFeature[];
}
const AlertsContext = createContext<AlertsContextValue>(null as any);
export const AlertsContextProvider = ({
children,
value,
}: {
value: AlertsContextValue;
children: React.ReactNode;
}) => {
return <AlertsContext.Provider value={value}>{children}</AlertsContext.Provider>;
};
export const useAlertsContext = () => {
const ctx = useContext(AlertsContext);
if (!ctx) {
throw new Error('AlertsContext has not been set.');
}
return ctx;
};

View file

@ -46,7 +46,6 @@ export const TriggersActionsUIHome: React.FunctionComponent<RouteComponentProps<
application: { capabilities },
setBreadcrumbs,
docLinks,
http,
} = useKibana().services;
const canShowActions = hasShowActionsCapability(capabilities);
@ -144,7 +143,7 @@ export const TriggersActionsUIHome: React.FunctionComponent<RouteComponentProps<
path={routeToConnectors}
component={() => (
<HealthContextProvider>
<HealthCheck docLinks={docLinks} http={http} waitForCheck={true}>
<HealthCheck waitForCheck={true}>
<ActionsConnectorsList />
</HealthCheck>
</HealthContextProvider>
@ -156,7 +155,7 @@ export const TriggersActionsUIHome: React.FunctionComponent<RouteComponentProps<
path={routeToAlerts}
component={() => (
<HealthContextProvider>
<HealthCheck docLinks={docLinks} http={http} inFlyout={true} waitForCheck={true}>
<HealthCheck inFlyout={true} waitForCheck={true}>
<AlertsList />
</HealthCheck>
</HealthContextProvider>

View file

@ -37,7 +37,6 @@ import {
import { AlertInstancesRouteWithApi } from './alert_instances_route';
import { ViewInApp } from './view_in_app';
import { AlertEdit } from '../../alert_form';
import { AlertsContextProvider } from '../../../context/alerts_context';
import { routeToAlertDetails } from '../../../constants';
import { alertsErrorReasonTranslationsMapping } from '../../alerts_list/translations';
import { useKibana } from '../../../../common/lib/kibana';
@ -62,15 +61,9 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
}) => {
const history = useHistory();
const {
http,
notifications: { toasts },
application: { capabilities },
alertTypeRegistry,
actionTypeRegistry,
uiSettings,
docLinks,
charts,
data,
setBreadcrumbs,
chrome,
} = useKibana().services;
@ -153,30 +146,16 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
/>
</EuiButtonEmpty>
{editFlyoutVisible && (
<AlertsContextProvider
value={{
http,
actionTypeRegistry,
alertTypeRegistry,
toastNotifications: toasts,
uiSettings,
docLinks,
charts,
dataFieldsFormats: data.fieldFormats,
reloadAlerts: setAlert,
capabilities,
dataUi: data.ui,
dataIndexPatterns: data.indexPatterns,
<AlertEdit
initialAlert={alert}
onClose={() => {
setInitialAlert(alert);
setEditFlyoutVisibility(false);
}}
>
<AlertEdit
initialAlert={alert}
onClose={() => {
setInitialAlert(alert);
setEditFlyoutVisibility(false);
}}
/>
</AlertsContextProvider>
actionTypeRegistry={actionTypeRegistry}
alertTypeRegistry={alertTypeRegistry}
reloadAlerts={setAlert}
/>
)}
</Fragment>
</EuiFlexItem>

View file

@ -12,39 +12,34 @@ import { coreMock } from '../../../../../../../src/core/public/mocks';
import AlertAdd from './alert_add';
import { actionTypeRegistryMock } from '../../action_type_registry.mock';
import { Alert, ValidationResult } from '../../../types';
import { AlertsContextProvider, useAlertsContext } from '../../context/alerts_context';
import { alertTypeRegistryMock } from '../../alert_type_registry.mock';
import { chartPluginMock } from '../../../../../../../src/plugins/charts/public/mocks';
import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks';
import { ReactWrapper } from 'enzyme';
import { ALERTS_FEATURE_ID } from '../../../../../alerts/common';
import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public';
import { useKibana } from '../../../common/lib/kibana';
jest.mock('../../../common/lib/kibana');
jest.mock('../../lib/alert_api', () => ({
loadAlertTypes: jest.fn(),
health: jest.fn((async) => ({ isSufficientlySecure: true, hasPermanentEncryptionKey: true })),
health: jest.fn(() => ({ isSufficientlySecure: true, hasPermanentEncryptionKey: true })),
}));
const actionTypeRegistry = actionTypeRegistryMock.create();
const alertTypeRegistry = alertTypeRegistryMock.create();
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
export const TestExpression: React.FunctionComponent<any> = () => {
const alertsContext = useAlertsContext();
const { metadata } = alertsContext;
return (
<EuiFormLabel>
<FormattedMessage
defaultMessage="Metadata: {val}. Fields: {fields}."
id="xpack.triggersActionsUI.sections.alertAdd.metadataTest"
values={{ val: metadata!.test, fields: metadata!.fields.join(' ') }}
values={{ val: 'test', fields: '' }}
/>
</EuiFormLabel>
);
};
describe('alert_add', () => {
let deps: any;
let wrapper: ReactWrapper<any>;
async function setup(initialValues?: Partial<Alert>) {
@ -80,15 +75,14 @@ describe('alert_add', () => {
application: { capabilities },
},
] = await mocks.getStartServices();
deps = {
toastNotifications: mocks.notifications.toasts,
http: mocks.http,
uiSettings: mocks.uiSettings,
data: dataPluginMock.createStartContract(),
charts: chartPluginMock.createStartContract(),
actionTypeRegistry,
alertTypeRegistry,
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' },
// eslint-disable-next-line react-hooks/rules-of-hooks
useKibanaMock().services.application.capabilities = {
...capabilities,
alerts: {
show: true,
save: true,
delete: true,
},
};
mocks.http.get.mockResolvedValue({
@ -132,37 +126,18 @@ describe('alert_add', () => {
actionTypeRegistry.has.mockReturnValue(true);
wrapper = mountWithIntl(
<KibanaContextProvider services={{ ...deps }}>
<AlertsContextProvider
value={{
reloadAlerts: () => {
return new Promise<void>(() => {});
},
http: deps.http,
actionTypeRegistry: deps.actionTypeRegistry,
alertTypeRegistry: deps.alertTypeRegistry,
toastNotifications: deps.toastNotifications,
uiSettings: deps.uiSettings,
docLinks: deps.docLinks,
metadata: { test: 'some value', fields: ['test'] },
capabilities: {
...capabilities,
actions: {
delete: true,
save: true,
show: true,
},
},
}}
>
<AlertAdd
consumer={ALERTS_FEATURE_ID}
addFlyoutVisible={true}
setAddFlyoutVisibility={() => {}}
initialValues={initialValues}
/>
</AlertsContextProvider>
</KibanaContextProvider>
<AlertAdd
consumer={ALERTS_FEATURE_ID}
addFlyoutVisible={true}
setAddFlyoutVisibility={() => {}}
initialValues={initialValues}
reloadAlerts={() => {
return new Promise<void>(() => {});
}}
actionTypeRegistry={actionTypeRegistry}
alertTypeRegistry={alertTypeRegistry}
metadata={{ test: 'some value', fields: ['test'] }}
/>
);
// Wait for active space to resolve before requesting the component to update

View file

@ -7,8 +7,13 @@ import React, { useCallback, useReducer, useMemo, useState, useEffect } from 're
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiTitle, EuiFlyoutHeader, EuiFlyout, EuiFlyoutBody, EuiPortal } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useAlertsContext } from '../../context/alerts_context';
import { Alert, AlertAction, IErrorObject } from '../../../types';
import {
ActionTypeRegistryContract,
Alert,
AlertAction,
AlertTypeRegistryContract,
IErrorObject,
} from '../../../types';
import { AlertForm, isValidAlert, validateBaseProperties } from './alert_form';
import { alertReducer, InitialAlert, InitialAlertReducer } from './alert_reducer';
import { createAlert } from '../../lib/alert_api';
@ -17,23 +22,32 @@ import { ConfirmAlertSave } from './confirm_alert_save';
import { hasShowActionsCapability } from '../../lib/capabilities';
import AlertAddFooter from './alert_add_footer';
import { HealthContextProvider } from '../../context/health_context';
import { useKibana } from '../../../common/lib/kibana';
interface AlertAddProps {
export interface AlertAddProps<MetaData = Record<string, any>> {
consumer: string;
addFlyoutVisible: boolean;
alertTypeRegistry: AlertTypeRegistryContract;
actionTypeRegistry: ActionTypeRegistryContract;
setAddFlyoutVisibility: React.Dispatch<React.SetStateAction<boolean>>;
alertTypeId?: string;
canChangeTrigger?: boolean;
initialValues?: Partial<Alert>;
reloadAlerts?: () => Promise<void>;
metadata?: MetaData;
}
export const AlertAdd = ({
const AlertAdd = ({
consumer,
addFlyoutVisible,
alertTypeRegistry,
actionTypeRegistry,
setAddFlyoutVisibility,
canChangeTrigger,
alertTypeId,
initialValues,
reloadAlerts,
metadata,
}: AlertAddProps) => {
const initialAlert: InitialAlert = useMemo(
() => ({
@ -65,14 +79,10 @@ export const AlertAdd = ({
};
const {
reloadAlerts,
http,
toastNotifications,
alertTypeRegistry,
actionTypeRegistry,
docLinks,
capabilities,
} = useAlertsContext();
notifications: { toasts },
application: { capabilities },
} = useKibana().services;
const canShowActions = hasShowActionsCapability(capabilities);
@ -127,7 +137,7 @@ export const AlertAdd = ({
try {
if (isValidAlert(alert, errors)) {
const newAlert = await createAlert({ http, alert });
toastNotifications.addSuccess(
toasts.addSuccess(
i18n.translate('xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText', {
defaultMessage: 'Created alert "{alertName}"',
values: {
@ -138,7 +148,7 @@ export const AlertAdd = ({
return newAlert;
}
} catch (errorRes) {
toastNotifications.addDanger(
toasts.addDanger(
errorRes.body?.message ??
i18n.translate('xpack.triggersActionsUI.sections.alertAdd.saveErrorNotificationText', {
defaultMessage: 'Cannot create alert.',
@ -166,7 +176,7 @@ export const AlertAdd = ({
</EuiTitle>
</EuiFlyoutHeader>
<HealthContextProvider>
<HealthCheck docLinks={docLinks} http={http} inFlyout={true} waitForCheck={false}>
<HealthCheck inFlyout={true} waitForCheck={false}>
<EuiFlyoutBody>
<AlertForm
alert={alert}
@ -179,6 +189,9 @@ export const AlertAdd = ({
defaultMessage: 'create',
}
)}
actionTypeRegistry={actionTypeRegistry}
alertTypeRegistry={alertTypeRegistry}
metadata={metadata}
/>
</EuiFlyoutBody>
<AlertAddFooter

View file

@ -9,16 +9,16 @@ import { act } from 'react-dom/test-utils';
import { coreMock } from '../../../../../../../src/core/public/mocks';
import { actionTypeRegistryMock } from '../../action_type_registry.mock';
import { ValidationResult, Alert } from '../../../types';
import { AlertsContextProvider } from '../../context/alerts_context';
import { alertTypeRegistryMock } from '../../alert_type_registry.mock';
import { ReactWrapper } from 'enzyme';
import AlertEdit from './alert_edit';
import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public';
import { useKibana } from '../../../common/lib/kibana';
jest.mock('../../../common/lib/kibana');
const actionTypeRegistry = actionTypeRegistryMock.create();
const alertTypeRegistry = alertTypeRegistryMock.create();
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
describe('alert_edit', () => {
let deps: any;
let wrapper: ReactWrapper<any>;
let mockedCoreSetup: ReturnType<typeof coreMock.createSetup>;
@ -32,17 +32,19 @@ describe('alert_edit', () => {
application: { capabilities },
},
] = await mockedCoreSetup.getStartServices();
deps = {
toastNotifications: mockedCoreSetup.notifications.toasts,
http: mockedCoreSetup.http,
uiSettings: mockedCoreSetup.uiSettings,
actionTypeRegistry,
alertTypeRegistry,
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' },
capabilities,
// eslint-disable-next-line react-hooks/rules-of-hooks
useKibanaMock().services.application.capabilities = {
...capabilities,
alerts: {
show: true,
save: true,
delete: true,
execute: true,
},
};
mockedCoreSetup.http.get.mockResolvedValue({
// eslint-disable-next-line react-hooks/rules-of-hooks
useKibanaMock().services.http.get = jest.fn().mockResolvedValue({
isSufficientlySecure: true,
hasPermanentEncryptionKey: true,
});
@ -122,24 +124,15 @@ describe('alert_edit', () => {
actionTypeRegistry.has.mockReturnValue(true);
wrapper = mountWithIntl(
<KibanaContextProvider services={deps}>
<AlertsContextProvider
value={{
reloadAlerts: () => {
return new Promise<void>(() => {});
},
http: deps!.http,
actionTypeRegistry: deps!.actionTypeRegistry,
alertTypeRegistry: deps!.alertTypeRegistry,
toastNotifications: deps!.toastNotifications,
uiSettings: deps!.uiSettings,
docLinks: deps.docLinks,
capabilities: deps!.capabilities,
}}
>
<AlertEdit onClose={() => {}} initialAlert={alert} />
</AlertsContextProvider>
</KibanaContextProvider>
<AlertEdit
onClose={() => {}}
initialAlert={alert}
reloadAlerts={() => {
return new Promise<void>(() => {});
}}
actionTypeRegistry={actionTypeRegistry}
alertTypeRegistry={alertTypeRegistry}
/>
);
// Wait for active space to resolve before requesting the component to update
await act(async () => {
@ -155,15 +148,17 @@ describe('alert_edit', () => {
});
it('displays a toast message on save for server errors', async () => {
mockedCoreSetup.http.get.mockResolvedValue([]);
useKibanaMock().services.http.get = jest.fn().mockResolvedValue([]);
await setup();
const err = new Error() as any;
err.body = {};
err.body.message = 'Fail message';
mockedCoreSetup.http.put.mockRejectedValue(err);
useKibanaMock().services.http.put = jest.fn().mockRejectedValue(err);
await act(async () => {
wrapper.find('[data-test-subj="saveEditedAlertButton"]').first().simulate('click');
});
expect(mockedCoreSetup.notifications.toasts.addDanger).toHaveBeenCalledWith('Fail message');
expect(useKibanaMock().services.notifications.toasts.addDanger).toHaveBeenCalledWith(
'Fail message'
);
});
});

View file

@ -20,20 +20,37 @@ import {
EuiSpacer,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useAlertsContext } from '../../context/alerts_context';
import { Alert, AlertAction, IErrorObject } from '../../../types';
import {
ActionTypeRegistryContract,
Alert,
AlertAction,
AlertTypeRegistryContract,
IErrorObject,
} from '../../../types';
import { AlertForm, validateBaseProperties } from './alert_form';
import { alertReducer, ConcreteAlertReducer } from './alert_reducer';
import { updateAlert } from '../../lib/alert_api';
import { HealthCheck } from '../../components/health_check';
import { HealthContextProvider } from '../../context/health_context';
import { useKibana } from '../../../common/lib/kibana';
interface AlertEditProps {
export interface AlertEditProps<MetaData = Record<string, any>> {
initialAlert: Alert;
alertTypeRegistry: AlertTypeRegistryContract;
actionTypeRegistry: ActionTypeRegistryContract;
onClose(): void;
reloadAlerts?: () => Promise<void>;
metadata?: MetaData;
}
export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => {
export const AlertEdit = ({
initialAlert,
onClose,
reloadAlerts,
alertTypeRegistry,
actionTypeRegistry,
metadata,
}: AlertEditProps) => {
const [{ alert }, dispatch] = useReducer(alertReducer as ConcreteAlertReducer, {
alert: initialAlert,
});
@ -44,13 +61,9 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => {
);
const {
reloadAlerts,
http,
toastNotifications,
alertTypeRegistry,
actionTypeRegistry,
docLinks,
} = useAlertsContext();
notifications: { toasts },
} = useKibana().services;
const alertType = alertTypeRegistry.get(alert.alertTypeId);
@ -76,7 +89,7 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => {
async function onSaveAlert(): Promise<Alert | undefined> {
try {
const newAlert = await updateAlert({ http, alert, id: alert.id });
toastNotifications.addSuccess(
toasts.addSuccess(
i18n.translate('xpack.triggersActionsUI.sections.alertEdit.saveSuccessNotificationText', {
defaultMessage: "Updated '{alertName}'",
values: {
@ -86,7 +99,7 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => {
);
return newAlert;
} catch (errorRes) {
toastNotifications.addDanger(
toasts.addDanger(
errorRes.body?.message ??
i18n.translate('xpack.triggersActionsUI.sections.alertEdit.saveErrorNotificationText', {
defaultMessage: 'Cannot update alert.',
@ -114,7 +127,7 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => {
</EuiTitle>
</EuiFlyoutHeader>
<HealthContextProvider>
<HealthCheck docLinks={docLinks} http={http} inFlyout={true} waitForCheck={true}>
<HealthCheck inFlyout={true} waitForCheck={true}>
<EuiFlyoutBody>
{hasActionsDisabled && (
<Fragment>
@ -135,12 +148,15 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => {
alert={alert}
dispatch={dispatch}
errors={errors}
actionTypeRegistry={actionTypeRegistry}
alertTypeRegistry={alertTypeRegistry}
canChangeTrigger={false}
setHasActionsDisabled={setHasActionsDisabled}
setHasActionsWithBrokenConnector={setHasActionsWithBrokenConnector}
operation="i18n.translate('xpack.triggersActionsUI.sections.alertEdit.operationName', {
defaultMessage: 'edit',
})"
metadata={metadata}
/>
</EuiFlyoutBody>
<EuiFlyoutFooter>

View file

@ -11,22 +11,18 @@ import { actionTypeRegistryMock } from '../../action_type_registry.mock';
import { alertTypeRegistryMock } from '../../alert_type_registry.mock';
import { ValidationResult, Alert } from '../../../types';
import { AlertForm } from './alert_form';
import { AlertsContextProvider } from '../../context/alerts_context';
import { coreMock } from 'src/core/public/mocks';
import { ALERTS_FEATURE_ID, RecoveredActionGroup } from '../../../../../alerts/common';
import { useKibana } from '../../../common/lib/kibana';
const actionTypeRegistry = actionTypeRegistryMock.create();
const alertTypeRegistry = alertTypeRegistryMock.create();
jest.mock('../../lib/alert_api', () => ({
loadAlertTypes: jest.fn(),
}));
jest.mock('../../../common/lib/kibana');
describe('alert_form', () => {
beforeEach(() => {
jest.resetAllMocks();
});
let deps: any;
const alertType = {
id: 'my-alert-type',
iconClass: 'test',
@ -67,6 +63,7 @@ describe('alert_form', () => {
alertParamsExpression: () => <Fragment />,
requiresAppContext: true,
};
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
describe('alert_form create alert', () => {
let wrapper: ReactWrapper<any>;
@ -99,14 +96,14 @@ describe('alert_form', () => {
application: { capabilities },
},
] = await mocks.getStartServices();
deps = {
toastNotifications: mocks.notifications.toasts,
http: mocks.http,
uiSettings: mocks.uiSettings,
actionTypeRegistry,
alertTypeRegistry,
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' },
capabilities,
// eslint-disable-next-line react-hooks/rules-of-hooks
useKibanaMock().services.application.capabilities = {
...capabilities,
alerts: {
show: true,
save: true,
delete: true,
},
};
alertTypeRegistry.list.mockReturnValue([alertType, alertTypeNonEditable]);
alertTypeRegistry.has.mockReturnValue(true);
@ -128,27 +125,14 @@ describe('alert_form', () => {
} as unknown) as Alert;
wrapper = mountWithIntl(
<AlertsContextProvider
value={{
reloadAlerts: () => {
return new Promise<void>(() => {});
},
http: deps!.http,
docLinks: deps.docLinks,
actionTypeRegistry: deps!.actionTypeRegistry,
alertTypeRegistry: deps!.alertTypeRegistry,
toastNotifications: deps!.toastNotifications,
uiSettings: deps!.uiSettings,
capabilities: deps!.capabilities,
}}
>
<AlertForm
alert={initialAlert}
dispatch={() => {}}
errors={{ name: [], interval: [] }}
operation="create"
/>
</AlertsContextProvider>
<AlertForm
alert={initialAlert}
dispatch={() => {}}
errors={{ name: [], interval: [] }}
operation="create"
actionTypeRegistry={actionTypeRegistry}
alertTypeRegistry={alertTypeRegistry}
/>
);
await act(async () => {
@ -208,6 +192,7 @@ describe('alert_form', () => {
async function setup() {
const { loadAlertTypes } = jest.requireMock('../../lib/alert_api');
loadAlertTypes.mockResolvedValue([
{
id: 'other-consumer-producer-alert-type',
@ -250,14 +235,14 @@ describe('alert_form', () => {
application: { capabilities },
},
] = await mocks.getStartServices();
deps = {
toastNotifications: mocks.notifications.toasts,
http: mocks.http,
uiSettings: mocks.uiSettings,
actionTypeRegistry,
alertTypeRegistry,
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' },
capabilities,
// eslint-disable-next-line react-hooks/rules-of-hooks
useKibanaMock().services.application.capabilities = {
...capabilities,
alerts: {
show: true,
save: true,
delete: true,
},
};
alertTypeRegistry.list.mockReturnValue([
{
@ -302,27 +287,14 @@ describe('alert_form', () => {
} as unknown) as Alert;
wrapper = mountWithIntl(
<AlertsContextProvider
value={{
reloadAlerts: () => {
return new Promise<void>(() => {});
},
http: deps!.http,
docLinks: deps.docLinks,
actionTypeRegistry: deps!.actionTypeRegistry,
alertTypeRegistry: deps!.alertTypeRegistry,
toastNotifications: deps!.toastNotifications,
uiSettings: deps!.uiSettings,
capabilities: deps!.capabilities,
}}
>
<AlertForm
alert={initialAlert}
dispatch={() => {}}
errors={{ name: [], interval: [] }}
operation="create"
/>
</AlertsContextProvider>
<AlertForm
alert={initialAlert}
dispatch={() => {}}
errors={{ name: [], interval: [] }}
operation="create"
actionTypeRegistry={actionTypeRegistry}
alertTypeRegistry={alertTypeRegistry}
/>
);
await act(async () => {
@ -354,14 +326,6 @@ describe('alert_form', () => {
let wrapper: ReactWrapper<any>;
async function setup() {
const mockes = coreMock.createSetup();
deps = {
toastNotifications: mockes.notifications.toasts,
http: mockes.http,
uiSettings: mockes.uiSettings,
actionTypeRegistry,
alertTypeRegistry,
};
alertTypeRegistry.list.mockReturnValue([alertType]);
alertTypeRegistry.get.mockReturnValue(alertType);
alertTypeRegistry.has.mockReturnValue(true);
@ -385,27 +349,14 @@ describe('alert_form', () => {
} as unknown) as Alert;
wrapper = mountWithIntl(
<AlertsContextProvider
value={{
reloadAlerts: () => {
return new Promise<void>(() => {});
},
http: deps!.http,
docLinks: deps.docLinks,
actionTypeRegistry: deps!.actionTypeRegistry,
alertTypeRegistry: deps!.alertTypeRegistry,
toastNotifications: deps!.toastNotifications,
uiSettings: deps!.uiSettings,
capabilities: deps!.capabilities,
}}
>
<AlertForm
alert={initialAlert}
dispatch={() => {}}
errors={{ name: [], interval: [] }}
operation="create"
/>
</AlertsContextProvider>
<AlertForm
alert={initialAlert}
dispatch={() => {}}
errors={{ name: [], interval: [] }}
operation="create"
actionTypeRegistry={actionTypeRegistry}
alertTypeRegistry={alertTypeRegistry}
/>
);
await act(async () => {

View file

@ -50,14 +50,16 @@ import {
AlertTypeIndex,
AlertType,
ValidationResult,
AlertTypeRegistryContract,
ActionTypeRegistryContract,
} from '../../../types';
import { getTimeOptions } from '../../../common/lib/get_time_options';
import { useAlertsContext } from '../../context/alerts_context';
import { ActionForm } from '../action_connector_form';
import { AlertActionParam, ALERTS_FEATURE_ID } from '../../../../../alerts/common';
import { hasAllPrivilege, hasShowActionsCapability } from '../../lib/capabilities';
import { SolutionFilter } from './solution_filter';
import './alert_form.scss';
import { useKibana } from '../../../common/lib/kibana';
import { recoveredActionGroupMessage } from '../../constants';
import { getDefaultsForActionParams } from '../../lib/get_defaults_for_action_params';
@ -113,14 +115,17 @@ function getProducerFeatureName(producer: string, kibanaFeatures: KibanaFeature[
return kibanaFeatures.find((featureItem) => featureItem.id === producer)?.name;
}
interface AlertFormProps {
interface AlertFormProps<MetaData = Record<string, any>> {
alert: InitialAlert;
dispatch: React.Dispatch<AlertReducerAction>;
errors: IErrorObject;
alertTypeRegistry: AlertTypeRegistryContract;
actionTypeRegistry: ActionTypeRegistryContract;
operation: string;
canChangeTrigger?: boolean; // to hide Change trigger button
setHasActionsDisabled?: (value: boolean) => void;
setHasActionsWithBrokenConnector?: (value: boolean) => void;
operation: string;
metadata?: MetaData;
}
export const AlertForm = ({
@ -131,17 +136,19 @@ export const AlertForm = ({
setHasActionsDisabled,
setHasActionsWithBrokenConnector,
operation,
alertTypeRegistry,
actionTypeRegistry,
metadata,
}: AlertFormProps) => {
const alertsContext = useAlertsContext();
const {
http,
toastNotifications,
alertTypeRegistry,
actionTypeRegistry,
notifications: { toasts },
docLinks,
capabilities,
application: { capabilities },
kibanaFeatures,
} = alertsContext;
charts,
data,
} = useKibana().services;
const canShowActions = hasShowActionsCapability(capabilities);
const [alertTypeModel, setAlertTypeModel] = useState<AlertTypeModel | null>(null);
@ -207,7 +214,7 @@ export const AlertForm = ({
new Map([...solutionsResult.entries()].sort(([, a], [, b]) => a.localeCompare(b)))
);
} catch (e) {
toastNotifications.addDanger({
toasts.addDanger({
title: i18n.translate(
'xpack.triggersActionsUI.sections.alertForm.unableToLoadAlertTypesMessage',
{ defaultMessage: 'Unable to load alert types' }
@ -486,9 +493,11 @@ export const AlertForm = ({
errors={errors}
setAlertParams={setAlertParams}
setAlertProperty={setAlertProperty}
alertsContext={alertsContext}
defaultActionGroupId={defaultActionGroupId}
actionGroups={selectedAlertType.actionGroups}
metadata={metadata}
charts={charts}
data={data}
/>
</Suspense>
</EuiErrorBoundary>

View file

@ -30,7 +30,6 @@ import {
import { useHistory } from 'react-router-dom';
import { isEmpty } from 'lodash';
import { AlertsContextProvider } from '../../../context/alerts_context';
import { ActionType, Alert, AlertTableItem, AlertTypeIndex, Pagination } from '../../../../types';
import { AlertAdd } from '../../alert_form';
import { BulkOperationPopover } from '../../common/components/bulk_operation_popover';
@ -80,10 +79,6 @@ export const AlertsList: React.FunctionComponent = () => {
application: { capabilities },
alertTypeRegistry,
actionTypeRegistry,
uiSettings,
docLinks,
charts,
data,
kibanaFeatures,
} = useKibana().services;
const canExecuteActions = hasExecuteActionsCapability(capabilities);
@ -619,7 +614,7 @@ export const AlertsList: React.FunctionComponent = () => {
return (
<section data-test-subj="alertsList">
<DeleteModalConfirmation
onDeleted={async (deleted: string[]) => {
onDeleted={async () => {
setAlertsToDelete([]);
setSelectedIds([]);
await loadAlertsData();
@ -658,29 +653,14 @@ export const AlertsList: React.FunctionComponent = () => {
) : (
noPermissionPrompt
)}
<AlertsContextProvider
value={{
reloadAlerts: loadAlertsData,
http,
actionTypeRegistry,
alertTypeRegistry,
toastNotifications: toasts,
uiSettings,
docLinks,
charts,
dataFieldsFormats: data.fieldFormats,
capabilities,
dataUi: data.ui,
dataIndexPatterns: data.indexPatterns,
kibanaFeatures,
}}
>
<AlertAdd
consumer={ALERTS_FEATURE_ID}
addFlyoutVisible={alertFlyoutVisible}
setAddFlyoutVisibility={setAlertFlyoutVisibility}
/>
</AlertsContextProvider>
<AlertAdd
consumer={ALERTS_FEATURE_ID}
addFlyoutVisible={alertFlyoutVisible}
setAddFlyoutVisibility={setAlertFlyoutVisibility}
actionTypeRegistry={actionTypeRegistry}
alertTypeRegistry={alertTypeRegistry}
reloadAlerts={loadAlertsData}
/>
</section>
);
};

View file

@ -21,9 +21,6 @@ export function withActionOperations<T>(
): React.FunctionComponent<PropsWithOptionalApiHandlers<T>> {
return (props: PropsWithOptionalApiHandlers<T>) => {
const { http } = useKibana().services;
if (!http) {
throw new Error('KibanaContext has not been initalized correctly.');
}
return (
<WrappedComponent {...(props as T)} loadActionTypes={async () => loadActionTypes({ http })} />
);

View file

@ -70,9 +70,6 @@ export function withBulkAlertOperations<T>(
): React.FunctionComponent<PropsWithOptionalApiHandlers<T>> {
return (props: PropsWithOptionalApiHandlers<T>) => {
const { http } = useKibana().services;
if (!http) {
throw new Error('KibanaContext has not been initalized correctly.');
}
return (
<WrappedComponent
{...(props as T)}

View file

@ -0,0 +1,16 @@
/*
* 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, { lazy, Suspense } from 'react';
import type { AlertAddProps } from '../application/sections/alert_form/alert_add';
export const getAddAlertFlyoutLazy = (props: AlertAddProps) => {
const AlertAddFlyoutLazy = lazy(() => import('../application/sections/alert_form/alert_add'));
return (
<Suspense fallback={null}>
<AlertAddFlyoutLazy {...props} />
</Suspense>
);
};

View file

@ -0,0 +1,16 @@
/*
* 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, { lazy, Suspense } from 'react';
import type { AlertEditProps } from '../application/sections/alert_form/alert_edit';
export const getEditAlertFlyoutLazy = (props: AlertEditProps) => {
const AlertEditFlyoutLazy = lazy(() => import('../application/sections/alert_form/alert_edit'));
return (
<Suspense fallback={null}>
<AlertEditFlyoutLazy {...props} />
</Suspense>
);
};

View file

@ -23,7 +23,6 @@ export const createStartServicesMock = (): TriggersAndActionsUiServices => {
get: jest.fn(),
list: jest.fn(),
} as AlertTypeRegistryContract,
notifications: core.notifications,
dataPlugin: jest.fn(),
navigateToApp: jest.fn(),
alerts: {

View file

@ -6,7 +6,6 @@
import { Plugin } from './plugin';
export { AlertsContextProvider, AlertsContextValue } from './application/context/alerts_context';
export { AlertAdd } from './application/sections/alert_form';
export {
AlertEdit,

View file

@ -28,6 +28,10 @@ import type { ConnectorAddFlyoutProps } from './application/sections/action_conn
import type { ConnectorEditFlyoutProps } from './application/sections/action_connector_form/connector_edit_flyout';
import { getAddConnectorFlyoutLazy } from './common/get_add_connector_flyout';
import { getEditConnectorFlyoutLazy } from './common/get_edit_connector_flyout';
import { getAddAlertFlyoutLazy } from './common/get_add_alert_flyout';
import { getEditAlertFlyoutLazy } from './common/get_edit_alert_flyout';
import { AlertAddProps } from './application/sections/alert_form/alert_add';
import { AlertEditProps } from './application/sections/alert_form/alert_edit';
export interface TriggersAndActionsUIPublicPluginSetup {
actionTypeRegistry: TypeRegistry<ActionTypeModel>;
@ -39,10 +43,16 @@ export interface TriggersAndActionsUIPublicPluginStart {
alertTypeRegistry: TypeRegistry<AlertTypeModel>;
getAddConnectorFlyout: (
props: Omit<ConnectorAddFlyoutProps, 'actionTypeRegistry'>
) => ReactElement<ConnectorAddFlyoutProps> | null;
) => ReactElement<ConnectorAddFlyoutProps>;
getEditConnectorFlyout: (
props: Omit<ConnectorEditFlyoutProps, 'actionTypeRegistry'>
) => ReactElement<ConnectorEditFlyoutProps> | null;
) => ReactElement<ConnectorEditFlyoutProps>;
getAddAlertFlyout: (
props: Omit<AlertAddProps, 'actionTypeRegistry' | 'alertTypeRegistry'>
) => ReactElement<AlertAddProps>;
getEditAlertFlyout: (
props: Omit<AlertEditProps, 'actionTypeRegistry' | 'alertTypeRegistry'>
) => ReactElement<AlertEditProps>;
}
interface PluginsSetup {
@ -152,6 +162,24 @@ export class Plugin
actionTypeRegistry: this.actionTypeRegistry,
});
},
getAddAlertFlyout: (
props: Omit<AlertAddProps, 'actionTypeRegistry' | 'alertTypeRegistry'>
) => {
return getAddAlertFlyoutLazy({
...props,
actionTypeRegistry: this.actionTypeRegistry,
alertTypeRegistry: this.alertTypeRegistry,
});
},
getEditAlertFlyout: (
props: Omit<AlertEditProps, 'actionTypeRegistry' | 'alertTypeRegistry'>
) => {
return getEditAlertFlyoutLazy({
...props,
actionTypeRegistry: this.actionTypeRegistry,
alertTypeRegistry: this.alertTypeRegistry,
});
},
};
}

View file

@ -6,6 +6,8 @@
import type { PublicMethodsOf } from '@kbn/utility-types';
import type { DocLinksStart } from 'kibana/public';
import { ComponentType } from 'react';
import { ChartsPluginSetup } from 'src/plugins/charts/public';
import { DataPublicPluginStart } from 'src/plugins/data/public';
import { ActionGroup, AlertActionParam } from '../../alerts/common';
import { ActionType } from '../../actions/common';
import { TypeRegistry } from './application/type_registry';
@ -158,7 +160,7 @@ export interface AlertTableItem extends Alert {
export interface AlertTypeParamsExpressionProps<
AlertParamsType = unknown,
AlertsContextValue = unknown
MetaData = Record<string, any>
> {
alertParams: AlertParamsType;
alertInterval: string;
@ -166,12 +168,14 @@ export interface AlertTypeParamsExpressionProps<
setAlertParams: (property: string, value: any) => void;
setAlertProperty: <Key extends keyof Alert>(key: Key, value: Alert[Key] | null) => void;
errors: IErrorObject;
alertsContext: AlertsContextValue;
defaultActionGroupId: string;
actionGroups: ActionGroup[];
metadata?: MetaData;
charts: ChartsPluginSetup;
data: DataPublicPluginStart;
}
export interface AlertTypeModel<AlertParamsType = any, AlertsContextValue = any> {
export interface AlertTypeModel<AlertParamsType = any> {
id: string;
name: string | JSX.Element;
description: string;
@ -180,9 +184,7 @@ export interface AlertTypeModel<AlertParamsType = any, AlertsContextValue = any>
validate: (alertParams: AlertParamsType) => ValidationResult;
alertParamsExpression:
| React.FunctionComponent<any>
| React.LazyExoticComponent<
ComponentType<AlertTypeParamsExpressionProps<AlertParamsType, AlertsContextValue>>
>;
| React.LazyExoticComponent<ComponentType<AlertTypeParamsExpressionProps<AlertParamsType>>>;
requiresAppContext: boolean;
defaultActionMessage?: string;
}

View file

@ -25,10 +25,7 @@ import {
import { CommonlyUsedRange } from '../components/common/uptime_date_picker';
import { setBasePath } from '../state/actions';
import { PageRouter } from '../routes';
import {
UptimeAlertsContextProvider,
UptimeAlertsFlyoutWrapper,
} from '../components/overview/alerts';
import { UptimeAlertsFlyoutWrapper } from '../components/overview/alerts';
import { store } from '../state';
import { kibanaService } from '../state/kibana_service';
import { ScopedHistory } from '../../../../../src/core/public';
@ -110,16 +107,14 @@ const Application = (props: UptimeAppProps) => {
<UptimeSettingsContextProvider {...props}>
<UptimeThemeContextProvider darkMode={darkMode}>
<UptimeStartupPluginsContextProvider {...startPlugins}>
<UptimeAlertsContextProvider>
<EuiPage className="app-wrapper-panel " data-test-subj="uptimeApp">
<RedirectAppLinks application={core.application}>
<main>
<UptimeAlertsFlyoutWrapper />
<PageRouter />
</main>
</RedirectAppLinks>
</EuiPage>
</UptimeAlertsContextProvider>
<EuiPage className="app-wrapper-panel " data-test-subj="uptimeApp">
<RedirectAppLinks application={core.application}>
<main>
<UptimeAlertsFlyoutWrapper />
<PageRouter />
</main>
</RedirectAppLinks>
</EuiPage>
</UptimeStartupPluginsContextProvider>
</UptimeThemeContextProvider>
</UptimeSettingsContextProvider>

View file

@ -4,8 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { Alert, AlertEdit } from '../../../../../../plugins/triggers_actions_ui/public';
import React, { useMemo } from 'react';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import {
Alert,
TriggersAndActionsUIPublicPluginStart,
} from '../../../../../../plugins/triggers_actions_ui/public';
interface Props {
alertFlyoutVisible: boolean;
@ -13,6 +17,10 @@ interface Props {
setAlertFlyoutVisibility: React.Dispatch<React.SetStateAction<boolean>>;
}
interface KibanaDeps {
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
}
export const UptimeEditAlertFlyoutComponent = ({
alertFlyoutVisible,
initialAlert,
@ -21,5 +29,16 @@ export const UptimeEditAlertFlyoutComponent = ({
const onClose = () => {
setAlertFlyoutVisibility(false);
};
return alertFlyoutVisible ? <AlertEdit initialAlert={initialAlert} onClose={onClose} /> : null;
const { triggersActionsUi } = useKibana<KibanaDeps>().services;
const EditAlertFlyout = useMemo(
() =>
triggersActionsUi.getEditAlertFlyout({
initialAlert,
onClose,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
return <>{alertFlyoutVisible && EditAlertFlyout}</>;
};

View file

@ -8,7 +8,10 @@ import React from 'react';
import { MLIntegrationComponent } from '../ml_integeration';
import { renderWithRouter, shallowWithRouter } from '../../../../lib';
import * as redux from 'react-redux';
import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public';
import { coreMock } from 'src/core/public/mocks';
const core = coreMock.createStart();
describe('ML Integrations', () => {
beforeEach(() => {
const spy = jest.spyOn(redux, 'useDispatch');
@ -24,7 +27,13 @@ describe('ML Integrations', () => {
});
it('renders without errors', () => {
const wrapper = renderWithRouter(<MLIntegrationComponent />);
const wrapper = renderWithRouter(
<KibanaContextProvider
services={{ ...core, triggersActionsUi: { getEditAlertFlyout: jest.fn() } }}
>
<MLIntegrationComponent />
</KibanaContextProvider>
);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -5,9 +5,12 @@
*/
import React from 'react';
import { coreMock } from 'src/core/public/mocks';
import { renderWithRouter, shallowWithRouter } from '../../../../lib';
import { MLJobLink } from '../ml_job_link';
import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public';
const core = coreMock.createStart();
describe('ML JobLink', () => {
it('shallow renders without errors', () => {
const wrapper = shallowWithRouter(
@ -18,7 +21,11 @@ describe('ML JobLink', () => {
it('renders without errors', () => {
const wrapper = renderWithRouter(
<MLJobLink dateRange={{ to: '', from: '' }} basePath="" monitorId="testMonitor" />
<KibanaContextProvider
services={{ ...core, triggersActionsUi: { getEditAlertFlyout: jest.fn() } }}
>
<MLJobLink dateRange={{ to: '', from: '' }} basePath="" monitorId="testMonitor" />
</KibanaContextProvider>
);
expect(wrapper).toMatchSnapshot();
});

View file

@ -5,10 +5,13 @@
*/
import React from 'react';
import { coreMock } from 'src/core/public/mocks';
import { ManageMLJobComponent } from '../manage_ml_job';
import * as redux from 'react-redux';
import { renderWithRouter, shallowWithRouter } from '../../../../lib';
import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public';
const core = coreMock.createStart();
describe('Manage ML Job', () => {
it('shallow renders without errors', () => {
jest.spyOn(redux, 'useSelector').mockReturnValue(true);
@ -25,7 +28,11 @@ describe('Manage ML Job', () => {
jest.spyOn(redux, 'useSelector').mockReturnValue(true);
const wrapper = renderWithRouter(
<ManageMLJobComponent hasMLJob={true} onEnableJob={jest.fn()} onJobDelete={jest.fn()} />
<KibanaContextProvider
services={{ ...core, triggersActionsUi: { getEditAlertFlyout: jest.fn() } }}
>
<ManageMLJobComponent hasMLJob={true} onEnableJob={jest.fn()} onJobDelete={jest.fn()} />
</KibanaContextProvider>
);
expect(wrapper).toMatchSnapshot();
});

View file

@ -6,6 +6,5 @@
export { AlertMonitorStatusComponent } from './alert_monitor_status';
export { ToggleAlertFlyoutButtonComponent } from './toggle_alert_flyout_button';
export { UptimeAlertsContextProvider } from './uptime_alerts_context_provider';
export { UptimeAlertsFlyoutWrapperComponent } from './uptime_alerts_flyout_wrapper';
export * from './alerts_containers';

View file

@ -1,66 +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 {
HttpStart,
NotificationsStart,
IUiSettingsClient,
DocLinksStart,
ApplicationStart,
} from 'src/core/public';
import { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public';
import { ChartsPluginStart } from '../../../../../../../src/plugins/charts/public';
import {
AlertsContextProvider,
TriggersAndActionsUIPublicPluginStart,
} from '../../../../../../plugins/triggers_actions_ui/public';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
interface KibanaDeps {
http: HttpStart;
notifications: NotificationsStart;
uiSettings: IUiSettingsClient;
docLinks: DocLinksStart;
application: ApplicationStart;
data: DataPublicPluginStart;
charts: ChartsPluginStart;
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
}
export const UptimeAlertsContextProvider: React.FC = ({ children }) => {
const {
services: {
data: { fieldFormats },
http,
charts,
notifications,
triggersActionsUi: { actionTypeRegistry, alertTypeRegistry },
uiSettings,
docLinks,
application: { capabilities },
},
} = useKibana<KibanaDeps>();
return (
<AlertsContextProvider
value={{
actionTypeRegistry,
alertTypeRegistry,
charts,
docLinks,
dataFieldsFormats: fieldFormats,
http,
toastNotifications: notifications?.toasts,
uiSettings,
capabilities,
}}
>
{children}
</AlertsContextProvider>
);
};

View file

@ -4,8 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { AlertAdd } from '../../../../../../plugins/triggers_actions_ui/public';
import React, { useMemo } from 'react';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { TriggersAndActionsUIPublicPluginStart } from '../../../../../../plugins/triggers_actions_ui/public';
interface Props {
alertFlyoutVisible: boolean;
@ -13,18 +14,29 @@ interface Props {
setAlertFlyoutVisibility: React.Dispatch<React.SetStateAction<boolean>>;
}
interface KibanaDeps {
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
}
export const UptimeAlertsFlyoutWrapperComponent = ({
alertFlyoutVisible,
alertTypeId,
setAlertFlyoutVisibility,
}: Props) => (
<AlertAdd
addFlyoutVisible={alertFlyoutVisible}
consumer="uptime"
setAddFlyoutVisibility={setAlertFlyoutVisibility}
alertTypeId={alertTypeId}
// if we don't have an alert type pre-specified, we need to
// let the user choose
canChangeTrigger={!alertTypeId}
/>
);
}: Props) => {
const { triggersActionsUi } = useKibana<KibanaDeps>().services;
const AddAlertFlyout = useMemo(
() =>
triggersActionsUi.getAddAlertFlyout({
consumer: 'uptime',
addFlyoutVisible: alertFlyoutVisible,
setAddFlyoutVisibility: setAlertFlyoutVisibility,
alertTypeId,
canChangeTrigger: !alertTypeId,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[alertFlyoutVisible, alertTypeId]
);
return <>{AddAlertFlyout}</>;
};