mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Adds Navigation APIs to Alerting.
Parts to this PR:
Adds a client side (Public) plugin to Alerting, including two APIs: registerNavigation & registerDefaultNavigation. These allow a plugin to register navigation handlers for any alerts which it is the consumer of- one for specific AlertTypes and one for a default handler for all AlertTypes created by the plugin.
The Alert Details page now uses these navigation handlers for the View In App button. If there's an AlertType specific handler it uses that, otherwise it uses a default one and if the consumer has not registered a handler - it remains disabled.
A generic Alerting Example plugin that demonstrates usage of these APIs including two AlertTypes - one that always fires, and another that checks how many people are in Outer Space and allows you to trigger based on that. 😉 To enable the plugin run yarn start --ssl --run-examples
This commit is contained in:
parent
a8c61473e5
commit
08384027cb
62 changed files with 2545 additions and 56 deletions
5
examples/alerting_example/README.md
Normal file
5
examples/alerting_example/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
## Alerting Example
|
||||
|
||||
This example plugin shows you how to create a custom Alert Type, create alerts based on that type and corresponding UI for viewing the details of all the alerts within the custom plugin.
|
||||
|
||||
To run this example, use the command `yarn start --run-examples`.
|
34
examples/alerting_example/common/constants.ts
Normal file
34
examples/alerting_example/common/constants.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export const ALERTING_EXAMPLE_APP_ID = 'AlertingExample';
|
||||
|
||||
// always firing
|
||||
export const DEFAULT_INSTANCES_TO_GENERATE = 5;
|
||||
|
||||
// Astros
|
||||
export enum Craft {
|
||||
OuterSpace = 'Outer Space',
|
||||
ISS = 'ISS',
|
||||
}
|
||||
export enum Operator {
|
||||
AreAbove = 'Are above',
|
||||
AreBelow = 'Are below',
|
||||
AreExactly = 'Are exactly',
|
||||
}
|
10
examples/alerting_example/kibana.json
Normal file
10
examples/alerting_example/kibana.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"id": "alertingExample",
|
||||
"version": "0.0.1",
|
||||
"kibanaVersion": "kibana",
|
||||
"configPath": ["alerting_example"],
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"requiredPlugins": ["triggers_actions_ui", "charts", "data", "alerting", "actions"],
|
||||
"optionalPlugins": []
|
||||
}
|
17
examples/alerting_example/package.json
Normal file
17
examples/alerting_example/package.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "alerting_example",
|
||||
"version": "1.0.0",
|
||||
"main": "target/examples/alerting_example",
|
||||
"kibana": {
|
||||
"version": "kibana",
|
||||
"templateVersion": "1.0.0"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"kbn": "node ../../scripts/kbn.js",
|
||||
"build": "rm -rf './target' && tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "3.7.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiFieldNumber, EuiFormRow } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { AlertTypeModel } from '../../../../x-pack/plugins/triggers_actions_ui/public';
|
||||
import { DEFAULT_INSTANCES_TO_GENERATE } from '../../common/constants';
|
||||
|
||||
interface AlwaysFiringParamsProps {
|
||||
alertParams: { instances?: number };
|
||||
setAlertParams: (property: string, value: any) => void;
|
||||
errors: { [key: string]: string[] };
|
||||
}
|
||||
|
||||
export function getAlertType(): AlertTypeModel {
|
||||
return {
|
||||
id: 'example.always-firing',
|
||||
name: 'Always Fires',
|
||||
iconClass: 'bolt',
|
||||
alertParamsExpression: AlwaysFiringExpression,
|
||||
validate: (alertParams: AlwaysFiringParamsProps['alertParams']) => {
|
||||
const { instances } = alertParams;
|
||||
const validationResult = {
|
||||
errors: {
|
||||
instances: new Array<string>(),
|
||||
},
|
||||
};
|
||||
if (instances && instances < 0) {
|
||||
validationResult.errors.instances.push(
|
||||
i18n.translate('AlertingExample.addAlert.error.invalidRandomInstances', {
|
||||
defaultMessage: 'instances must be equal or greater than zero.',
|
||||
})
|
||||
);
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const AlwaysFiringExpression: React.FunctionComponent<AlwaysFiringParamsProps> = ({
|
||||
alertParams,
|
||||
setAlertParams,
|
||||
}) => {
|
||||
const { instances = DEFAULT_INSTANCES_TO_GENERATE } = alertParams;
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiFlexGroup gutterSize="s" wrap direction="column">
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiFormRow
|
||||
label="Random Instances to generate"
|
||||
helpText="How many randomly generated Alert Instances do you wish to activate on each alert run?"
|
||||
>
|
||||
<EuiFieldNumber
|
||||
name="instances"
|
||||
value={instances}
|
||||
onChange={event => {
|
||||
setAlertParams('instances', event.target.valueAsNumber);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
277
examples/alerting_example/public/alert_types/astros.tsx
Normal file
277
examples/alerting_example/public/alert_types/astros.tsx
Normal file
|
@ -0,0 +1,277 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, Fragment } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPopover,
|
||||
EuiFieldNumber,
|
||||
EuiPopoverTitle,
|
||||
EuiSelect,
|
||||
EuiCallOut,
|
||||
EuiExpression,
|
||||
EuiTextColor,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { flatten } from 'lodash';
|
||||
import { ALERTING_EXAMPLE_APP_ID, Craft, Operator } from '../../common/constants';
|
||||
import { SanitizedAlert } from '../../../../x-pack/plugins/alerting/common';
|
||||
import { PluginSetupContract as AlertingSetup } from '../../../../x-pack/plugins/alerting/public';
|
||||
import { AlertTypeModel } from '../../../../x-pack/plugins/triggers_actions_ui/public';
|
||||
|
||||
export function registerNavigation(alerting: AlertingSetup) {
|
||||
alerting.registerNavigation(
|
||||
ALERTING_EXAMPLE_APP_ID,
|
||||
'example.people-in-space',
|
||||
(alert: SanitizedAlert) => `/astros/${alert.id}`
|
||||
);
|
||||
}
|
||||
|
||||
interface PeopleinSpaceParamsProps {
|
||||
alertParams: { outerSpaceCapacity?: number; craft?: string; op?: string };
|
||||
setAlertParams: (property: string, value: any) => void;
|
||||
errors: { [key: string]: string[] };
|
||||
}
|
||||
|
||||
function isValueInEnum(enumeratin: Record<string, any>, value: any): boolean {
|
||||
return !!Object.values(enumeratin).find(enumVal => enumVal === value);
|
||||
}
|
||||
|
||||
export function getAlertType(): AlertTypeModel {
|
||||
return {
|
||||
id: 'example.people-in-space',
|
||||
name: 'People Are In Space Right Now',
|
||||
iconClass: 'globe',
|
||||
alertParamsExpression: PeopleinSpaceExpression,
|
||||
validate: (alertParams: PeopleinSpaceParamsProps['alertParams']) => {
|
||||
const { outerSpaceCapacity, craft, op } = alertParams;
|
||||
|
||||
const validationResult = {
|
||||
errors: {
|
||||
outerSpaceCapacity: new Array<string>(),
|
||||
craft: new Array<string>(),
|
||||
},
|
||||
};
|
||||
if (!isValueInEnum(Craft, craft)) {
|
||||
validationResult.errors.craft.push(
|
||||
i18n.translate('AlertingExample.addAlert.error.invalidCraft', {
|
||||
defaultMessage: 'You must choose one of the following Craft: {crafts}',
|
||||
values: {
|
||||
crafts: Object.values(Craft).join(', '),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
if (!(typeof outerSpaceCapacity === 'number' && outerSpaceCapacity >= 0)) {
|
||||
validationResult.errors.outerSpaceCapacity.push(
|
||||
i18n.translate('AlertingExample.addAlert.error.invalidOuterSpaceCapacity', {
|
||||
defaultMessage: 'outerSpaceCapacity must be a number greater than or equal to zero.',
|
||||
})
|
||||
);
|
||||
}
|
||||
if (!isValueInEnum(Operator, op)) {
|
||||
validationResult.errors.outerSpaceCapacity.push(
|
||||
i18n.translate('AlertingExample.addAlert.error.invalidCraft', {
|
||||
defaultMessage: 'You must choose one of the following Operator: {crafts}',
|
||||
values: {
|
||||
crafts: Object.values(Operator).join(', '),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const PeopleinSpaceExpression: React.FunctionComponent<PeopleinSpaceParamsProps> = ({
|
||||
alertParams,
|
||||
setAlertParams,
|
||||
errors,
|
||||
}) => {
|
||||
const { outerSpaceCapacity = 0, craft = Craft.OuterSpace, op = Operator.AreAbove } = alertParams;
|
||||
|
||||
// store defaults
|
||||
useEffect(() => {
|
||||
if (outerSpaceCapacity !== alertParams.outerSpaceCapacity) {
|
||||
setAlertParams('outerSpaceCapacity', outerSpaceCapacity);
|
||||
}
|
||||
if (craft !== alertParams.craft) {
|
||||
setAlertParams('craft', craft);
|
||||
}
|
||||
if (op !== alertParams.op) {
|
||||
setAlertParams('op', op);
|
||||
}
|
||||
}, [alertParams, craft, op, outerSpaceCapacity, setAlertParams]);
|
||||
|
||||
const [craftTrigger, setCraftTrigger] = useState<{ craft: string; isOpen: boolean }>({
|
||||
craft,
|
||||
isOpen: false,
|
||||
});
|
||||
const [outerSpaceCapacityTrigger, setOuterSpaceCapacity] = useState<{
|
||||
outerSpaceCapacity: number;
|
||||
op: string;
|
||||
isOpen: boolean;
|
||||
}>({
|
||||
outerSpaceCapacity,
|
||||
op,
|
||||
isOpen: false,
|
||||
});
|
||||
|
||||
const errorsCallout = flatten(
|
||||
Object.entries(errors).map(([field, errs]: [string, string[]]) =>
|
||||
errs.map(e => (
|
||||
<p>
|
||||
<EuiTextColor color="accent">{field}:</EuiTextColor>`: ${errs}`
|
||||
</p>
|
||||
))
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{errorsCallout.length ? (
|
||||
<EuiCallOut title="Sorry, there was an error" color="danger" iconType="alert">
|
||||
{errorsCallout}
|
||||
</EuiCallOut>
|
||||
) : (
|
||||
<Fragment />
|
||||
)}
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
id="craft"
|
||||
button={
|
||||
<EuiExpression
|
||||
description="When the People in"
|
||||
value={craftTrigger.craft}
|
||||
isActive={craftTrigger.isOpen}
|
||||
onClick={() => {
|
||||
setCraftTrigger({
|
||||
...craftTrigger,
|
||||
isOpen: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
isOpen={craftTrigger.isOpen}
|
||||
closePopover={() => {
|
||||
setCraftTrigger({
|
||||
...craftTrigger,
|
||||
isOpen: false,
|
||||
});
|
||||
}}
|
||||
ownFocus
|
||||
panelPaddingSize="s"
|
||||
anchorPosition="downLeft"
|
||||
>
|
||||
<div style={{ zIndex: 200 }}>
|
||||
<EuiPopoverTitle>When the People in</EuiPopoverTitle>
|
||||
<EuiSelect
|
||||
compressed
|
||||
value={craftTrigger.craft}
|
||||
onChange={event => {
|
||||
setAlertParams('craft', event.target.value);
|
||||
setCraftTrigger({
|
||||
craft: event.target.value,
|
||||
isOpen: false,
|
||||
});
|
||||
}}
|
||||
options={[
|
||||
{ value: Craft.OuterSpace, text: 'Outer Space' },
|
||||
{ value: Craft.ISS, text: 'the International Space Station' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
id="outerSpaceCapacity"
|
||||
button={
|
||||
<EuiExpression
|
||||
description={outerSpaceCapacityTrigger.op}
|
||||
value={outerSpaceCapacityTrigger.outerSpaceCapacity}
|
||||
isActive={outerSpaceCapacityTrigger.isOpen}
|
||||
onClick={() => {
|
||||
setOuterSpaceCapacity({
|
||||
...outerSpaceCapacityTrigger,
|
||||
isOpen: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
isOpen={outerSpaceCapacityTrigger.isOpen}
|
||||
closePopover={() => {
|
||||
setOuterSpaceCapacity({
|
||||
...outerSpaceCapacityTrigger,
|
||||
isOpen: false,
|
||||
});
|
||||
}}
|
||||
ownFocus
|
||||
panelPaddingSize="s"
|
||||
anchorPosition="downLeft"
|
||||
>
|
||||
<div style={{ zIndex: 200 }}>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={false} style={{ width: 150 }}>
|
||||
<EuiSelect
|
||||
compressed
|
||||
value={outerSpaceCapacityTrigger.op}
|
||||
onChange={event => {
|
||||
setAlertParams('op', event.target.value);
|
||||
setOuterSpaceCapacity({
|
||||
...outerSpaceCapacityTrigger,
|
||||
op: event.target.value,
|
||||
isOpen: false,
|
||||
});
|
||||
}}
|
||||
options={[
|
||||
{ value: Operator.AreAbove, text: 'Are above' },
|
||||
{ value: Operator.AreBelow, text: 'Are below' },
|
||||
{ value: Operator.AreExactly, text: 'Are exactly' },
|
||||
]}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false} style={{ width: 100 }}>
|
||||
<EuiFieldNumber
|
||||
compressed
|
||||
value={outerSpaceCapacityTrigger.outerSpaceCapacity}
|
||||
onChange={event => {
|
||||
setAlertParams('outerSpaceCapacity', event.target.valueAsNumber);
|
||||
setOuterSpaceCapacity({
|
||||
...outerSpaceCapacityTrigger,
|
||||
outerSpaceCapacity: event.target.valueAsNumber,
|
||||
isOpen: false,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
33
examples/alerting_example/public/alert_types/index.ts
Normal file
33
examples/alerting_example/public/alert_types/index.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { registerNavigation as registerPeopleInSpaceNavigation } from './astros';
|
||||
import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants';
|
||||
import { SanitizedAlert } from '../../../../x-pack/plugins/alerting/common';
|
||||
import { PluginSetupContract as AlertingSetup } from '../../../../x-pack/plugins/alerting/public';
|
||||
|
||||
export function registerNavigation(alerting: AlertingSetup) {
|
||||
// register default navigation
|
||||
alerting.registerDefaultNavigation(
|
||||
ALERTING_EXAMPLE_APP_ID,
|
||||
(alert: SanitizedAlert) => `/alert/${alert.id}`
|
||||
);
|
||||
|
||||
registerPeopleInSpaceNavigation(alerting);
|
||||
}
|
108
examples/alerting_example/public/application.tsx
Normal file
108
examples/alerting_example/public/application.tsx
Normal file
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { BrowserRouter as Router, Route, RouteComponentProps } from 'react-router-dom';
|
||||
import { EuiPage } from '@elastic/eui';
|
||||
import {
|
||||
AppMountParameters,
|
||||
CoreStart,
|
||||
IUiSettingsClient,
|
||||
ToastsSetup,
|
||||
} from '../../../src/core/public';
|
||||
import { DataPublicPluginStart } from '../../../src/plugins/data/public';
|
||||
import { ChartsPluginStart } from '../../../src/plugins/charts/public';
|
||||
|
||||
import { Page } from './components/page';
|
||||
import { DocumentationPage } from './components/documentation';
|
||||
import { ViewAlertPage } from './components/view_alert';
|
||||
import { TriggersAndActionsUIPublicPluginStart } from '../../../x-pack/plugins/triggers_actions_ui/public';
|
||||
import { AlertingExamplePublicStartDeps } from './plugin';
|
||||
import { ViewPeopleInSpaceAlertPage } from './components/view_astros_alert';
|
||||
|
||||
export interface AlertingExampleComponentParams {
|
||||
application: CoreStart['application'];
|
||||
http: CoreStart['http'];
|
||||
basename: string;
|
||||
triggers_actions_ui: TriggersAndActionsUIPublicPluginStart;
|
||||
data: DataPublicPluginStart;
|
||||
charts: ChartsPluginStart;
|
||||
uiSettings: IUiSettingsClient;
|
||||
toastNotifications: ToastsSetup;
|
||||
}
|
||||
|
||||
const AlertingExampleApp = (deps: AlertingExampleComponentParams) => {
|
||||
const { basename, http } = deps;
|
||||
return (
|
||||
<Router basename={basename}>
|
||||
<EuiPage>
|
||||
<Route
|
||||
path={`/`}
|
||||
exact={true}
|
||||
render={() => (
|
||||
<Page title={`Home`} isHome={true}>
|
||||
<DocumentationPage {...deps} />
|
||||
</Page>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={`/alert/:id`}
|
||||
render={(props: RouteComponentProps<{ id: string }>) => {
|
||||
return (
|
||||
<Page title={`View Alert`} crumb={`View Alert ${props.match.params.id}`}>
|
||||
<ViewAlertPage http={http} id={props.match.params.id} />
|
||||
</Page>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
path={`/astros/:id`}
|
||||
render={(props: RouteComponentProps<{ id: string }>) => {
|
||||
return (
|
||||
<Page title={`View People In Space Alert`} crumb={`Astros ${props.match.params.id}`}>
|
||||
<ViewPeopleInSpaceAlertPage http={http} id={props.match.params.id} />
|
||||
</Page>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</EuiPage>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
||||
export const renderApp = (
|
||||
{ application, notifications, http, uiSettings }: CoreStart,
|
||||
deps: AlertingExamplePublicStartDeps,
|
||||
{ appBasePath, element }: AppMountParameters
|
||||
) => {
|
||||
ReactDOM.render(
|
||||
<AlertingExampleApp
|
||||
basename={appBasePath}
|
||||
application={application}
|
||||
toastNotifications={notifications.toasts}
|
||||
http={http}
|
||||
uiSettings={uiSettings}
|
||||
{...deps}
|
||||
/>,
|
||||
element
|
||||
);
|
||||
|
||||
return () => ReactDOM.unmountComponentAtNode(element);
|
||||
};
|
72
examples/alerting_example/public/components/create_alert.tsx
Normal file
72
examples/alerting_example/public/components/create_alert.tsx
Normal file
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { EuiIcon, EuiFlexItem, EuiCard, EuiFlexGroup } from '@elastic/eui';
|
||||
|
||||
import {
|
||||
AlertsContextProvider,
|
||||
AlertAdd,
|
||||
} from '../../../../x-pack/plugins/triggers_actions_ui/public';
|
||||
import { AlertingExampleComponentParams } from '../application';
|
||||
import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants';
|
||||
|
||||
export const CreateAlert = ({
|
||||
http,
|
||||
triggers_actions_ui,
|
||||
charts,
|
||||
uiSettings,
|
||||
data,
|
||||
toastNotifications,
|
||||
}: AlertingExampleComponentParams) => {
|
||||
const [alertFlyoutVisible, setAlertFlyoutVisibility] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiCard
|
||||
icon={<EuiIcon size="xxl" type={`bell`} />}
|
||||
title={`Create Alert`}
|
||||
description="Create an new Alert based on one of our example Alert Types ."
|
||||
onClick={() => setAlertFlyoutVisibility(true)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<AlertsContextProvider
|
||||
value={{
|
||||
http,
|
||||
actionTypeRegistry: triggers_actions_ui.actionTypeRegistry,
|
||||
alertTypeRegistry: triggers_actions_ui.alertTypeRegistry,
|
||||
toastNotifications,
|
||||
uiSettings,
|
||||
charts,
|
||||
dataFieldsFormats: data.fieldFormats,
|
||||
}}
|
||||
>
|
||||
<AlertAdd
|
||||
consumer={ALERTING_EXAMPLE_APP_ID}
|
||||
addFlyoutVisible={alertFlyoutVisible}
|
||||
setAddFlyoutVisibility={setAlertFlyoutVisibility}
|
||||
/>
|
||||
</AlertsContextProvider>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
EuiText,
|
||||
EuiPageBody,
|
||||
EuiPageContent,
|
||||
EuiPageContentBody,
|
||||
EuiPageContentHeader,
|
||||
EuiPageContentHeaderSection,
|
||||
EuiPageHeader,
|
||||
EuiPageHeaderSection,
|
||||
EuiTitle,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { CreateAlert } from './create_alert';
|
||||
import { AlertingExampleComponentParams } from '../application';
|
||||
|
||||
export const DocumentationPage = (deps: AlertingExampleComponentParams) => (
|
||||
<EuiPageBody>
|
||||
<EuiPageHeader>
|
||||
<EuiPageHeaderSection>
|
||||
<EuiTitle size="l">
|
||||
<h1>Welcome to the Alerting plugin example</h1>
|
||||
</EuiTitle>
|
||||
</EuiPageHeaderSection>
|
||||
</EuiPageHeader>
|
||||
<EuiPageContent>
|
||||
<EuiPageContentHeader>
|
||||
<EuiPageContentHeaderSection>
|
||||
<EuiTitle>
|
||||
<h2>Documentation links</h2>
|
||||
</EuiTitle>
|
||||
</EuiPageContentHeaderSection>
|
||||
</EuiPageContentHeader>
|
||||
<EuiPageContentBody>
|
||||
<EuiText>
|
||||
<h2>Plugin Structure</h2>
|
||||
<p>
|
||||
This example solution has both `server` and a `public` plugins. The `server` handles
|
||||
registration of example the AlertTypes, while the `public` handles creation of, and
|
||||
navigation for, these alert types.
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer />
|
||||
<CreateAlert {...deps} />
|
||||
</EuiPageContentBody>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
);
|
74
examples/alerting_example/public/components/page.tsx
Normal file
74
examples/alerting_example/public/components/page.tsx
Normal file
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
EuiPageBody,
|
||||
EuiPageContent,
|
||||
EuiPageContentBody,
|
||||
EuiPageHeader,
|
||||
EuiPageHeaderSection,
|
||||
EuiTitle,
|
||||
EuiBreadcrumbs,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
type PageProps = RouteComponentProps & {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
crumb?: string;
|
||||
isHome?: boolean;
|
||||
};
|
||||
|
||||
export const Page = withRouter(({ title, crumb, children, isHome = false, history }: PageProps) => {
|
||||
const breadcrumbs: Array<{
|
||||
text: string;
|
||||
onClick?: () => void;
|
||||
}> = [
|
||||
{
|
||||
text: crumb ?? title,
|
||||
},
|
||||
];
|
||||
if (!isHome) {
|
||||
breadcrumbs.splice(0, 0, {
|
||||
text: 'Home',
|
||||
onClick: () => {
|
||||
history.push(`/`);
|
||||
},
|
||||
});
|
||||
}
|
||||
return (
|
||||
<EuiPageBody data-test-subj="searchTestPage">
|
||||
<EuiPageHeader>
|
||||
<EuiPageHeaderSection>
|
||||
<EuiTitle size="l">
|
||||
<h1>{title}</h1>
|
||||
</EuiTitle>
|
||||
</EuiPageHeaderSection>
|
||||
</EuiPageHeader>
|
||||
<EuiBreadcrumbs responsive={false} breadcrumbs={breadcrumbs} />
|
||||
<EuiSpacer />
|
||||
<EuiPageContent>
|
||||
<EuiPageContentBody>{children}</EuiPageContentBody>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
);
|
||||
});
|
116
examples/alerting_example/public/components/view_alert.tsx
Normal file
116
examples/alerting_example/public/components/view_alert.tsx
Normal file
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useState, useEffect, Fragment } from 'react';
|
||||
|
||||
import {
|
||||
EuiText,
|
||||
EuiLoadingKibana,
|
||||
EuiCallOut,
|
||||
EuiTextColor,
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListTitle,
|
||||
EuiDescriptionListDescription,
|
||||
EuiCodeBlock,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import { CoreStart } from 'kibana/public';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { Alert, AlertTaskState } from '../../../../x-pack/plugins/alerting/common';
|
||||
import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants';
|
||||
|
||||
type Props = RouteComponentProps & {
|
||||
http: CoreStart['http'];
|
||||
id: string;
|
||||
};
|
||||
export const ViewAlertPage = withRouter(({ http, id }: Props) => {
|
||||
const [alert, setAlert] = useState<Alert | null>(null);
|
||||
const [alertState, setAlertState] = useState<AlertTaskState | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!alert) {
|
||||
http.get(`/api/alert/${id}`).then(setAlert);
|
||||
}
|
||||
if (!alertState) {
|
||||
http.get(`/api/alert/${id}/state`).then(setAlertState);
|
||||
}
|
||||
}, [alert, alertState, http, id]);
|
||||
|
||||
return alert && alertState ? (
|
||||
<Fragment>
|
||||
<EuiCallOut title={`Alert "${alert.name}"`} iconType="search">
|
||||
<p>
|
||||
This is a generic view for all Alerts created by the
|
||||
<EuiTextColor color="accent"> {ALERTING_EXAMPLE_APP_ID} </EuiTextColor>
|
||||
plugin.
|
||||
</p>
|
||||
<p>
|
||||
You are now viewing the <EuiTextColor color="accent">{`${alert.name}`} </EuiTextColor>
|
||||
Alert, whose ID is <EuiTextColor color="accent">{`${alert.id}`}</EuiTextColor>.
|
||||
</p>
|
||||
<p>
|
||||
Its AlertType is <EuiTextColor color="accent">{`${alert.alertTypeId}`}</EuiTextColor> and
|
||||
its scheduled to run at an interval of
|
||||
<EuiTextColor color="accent"> {`${alert.schedule.interval}`}</EuiTextColor>.
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiText>
|
||||
<h2>Alert Instances</h2>
|
||||
</EuiText>
|
||||
{isEmpty(alertState.alertInstances) ? (
|
||||
<EuiCallOut title="No Alert Instances!" color="warning" iconType="help">
|
||||
<p>This Alert doesn't have any active alert instances at the moment.</p>
|
||||
</EuiCallOut>
|
||||
) : (
|
||||
<Fragment>
|
||||
<EuiCallOut title="Active State" color="success" iconType="user">
|
||||
<p>
|
||||
Bellow are the active Alert Instances which were activated on the alerts last run.
|
||||
<br />
|
||||
For each instance id you can see its current state in JSON format.
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiDescriptionList compressed>
|
||||
{Object.entries(alertState.alertInstances ?? {}).map(([instance, { state }]) => (
|
||||
<Fragment>
|
||||
<EuiDescriptionListTitle>{instance}</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
<EuiCodeBlock
|
||||
language="json"
|
||||
fontSize="m"
|
||||
paddingSize="m"
|
||||
color="dark"
|
||||
overflowHeight={300}
|
||||
isCopyable
|
||||
>
|
||||
{`${JSON.stringify(state)}`}
|
||||
</EuiCodeBlock>
|
||||
</EuiDescriptionListDescription>
|
||||
</Fragment>
|
||||
))}
|
||||
</EuiDescriptionList>
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
) : (
|
||||
<EuiLoadingKibana size="xl" />
|
||||
);
|
||||
});
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useState, useEffect, Fragment } from 'react';
|
||||
|
||||
import {
|
||||
EuiText,
|
||||
EuiLoadingKibana,
|
||||
EuiCallOut,
|
||||
EuiTextColor,
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListTitle,
|
||||
EuiDescriptionListDescription,
|
||||
EuiSpacer,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiStat,
|
||||
} from '@elastic/eui';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import { CoreStart } from 'kibana/public';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { Alert, AlertTaskState } from '../../../../x-pack/plugins/alerting/common';
|
||||
import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants';
|
||||
|
||||
type Props = RouteComponentProps & {
|
||||
http: CoreStart['http'];
|
||||
id: string;
|
||||
};
|
||||
|
||||
function hasCraft(state: any): state is { craft: string } {
|
||||
return state && state.craft;
|
||||
}
|
||||
export const ViewPeopleInSpaceAlertPage = withRouter(({ http, id }: Props) => {
|
||||
const [alert, setAlert] = useState<Alert | null>(null);
|
||||
const [alertState, setAlertState] = useState<AlertTaskState | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!alert) {
|
||||
http.get(`/api/alert/${id}`).then(setAlert);
|
||||
}
|
||||
if (!alertState) {
|
||||
http.get(`/api/alert/${id}/state`).then(setAlertState);
|
||||
}
|
||||
}, [alert, alertState, http, id]);
|
||||
|
||||
return alert && alertState ? (
|
||||
<Fragment>
|
||||
<EuiCallOut title={`Alert "${alert.name}"`} iconType="search">
|
||||
<p>
|
||||
This is a specific view for all
|
||||
<EuiTextColor color="accent"> example.people-in-space </EuiTextColor> Alerts created by
|
||||
the
|
||||
<EuiTextColor color="accent"> {ALERTING_EXAMPLE_APP_ID} </EuiTextColor>
|
||||
plugin.
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiText>
|
||||
<h2>Alert Instances</h2>
|
||||
</EuiText>
|
||||
{isEmpty(alertState.alertInstances) ? (
|
||||
<EuiCallOut title="No Alert Instances!" color="warning" iconType="help">
|
||||
<p>
|
||||
The people in {alert.params.craft} at the moment <b>are not</b> {alert.params.op}{' '}
|
||||
{alert.params.outerSpaceCapacity}
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
) : (
|
||||
<Fragment>
|
||||
<EuiCallOut title="Active State" color="success" iconType="user">
|
||||
<p>
|
||||
The alert has been triggered because the people in {alert.params.craft} at the moment{' '}
|
||||
{alert.params.op} {alert.params.outerSpaceCapacity}
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="l" />
|
||||
<div>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiStat
|
||||
title={Object.keys(alertState.alertInstances ?? {}).length}
|
||||
description={`People in ${alert.params.craft}`}
|
||||
titleColor="primary"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionList compressed>
|
||||
{Object.entries(alertState.alertInstances ?? {}).map(
|
||||
([instance, { state }], index) => (
|
||||
<Fragment key={index}>
|
||||
<EuiDescriptionListTitle>{instance}</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
{hasCraft(state) ? state.craft : 'Unknown Craft'}
|
||||
</EuiDescriptionListDescription>
|
||||
</Fragment>
|
||||
)
|
||||
)}
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
) : (
|
||||
<EuiLoadingKibana size="xl" />
|
||||
);
|
||||
});
|
22
examples/alerting_example/public/index.ts
Normal file
22
examples/alerting_example/public/index.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { AlertingExamplePlugin } from './plugin';
|
||||
|
||||
export const plugin = () => new AlertingExamplePlugin();
|
70
examples/alerting_example/public/plugin.tsx
Normal file
70
examples/alerting_example/public/plugin.tsx
Normal file
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { Plugin, CoreSetup, AppMountParameters, CoreStart } from 'kibana/public';
|
||||
import { PluginSetupContract as AlertingSetup } from '../../../x-pack/plugins/alerting/public';
|
||||
import { ChartsPluginStart } from '../../../src/plugins/charts/public';
|
||||
import { TriggersAndActionsUIPublicPluginSetup } from '../../../x-pack/plugins/triggers_actions_ui/public';
|
||||
import { DataPublicPluginStart } from '../../../src/plugins/data/public';
|
||||
import { getAlertType as getAlwaysFiringAlertType } from './alert_types/always_firing';
|
||||
import { getAlertType as getPeopleInSpaceAlertType } from './alert_types/astros';
|
||||
import { registerNavigation } from './alert_types';
|
||||
|
||||
export type Setup = void;
|
||||
export type Start = void;
|
||||
|
||||
export interface AlertingExamplePublicSetupDeps {
|
||||
alerting: AlertingSetup;
|
||||
triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup;
|
||||
}
|
||||
|
||||
export interface AlertingExamplePublicStartDeps {
|
||||
alerting: AlertingSetup;
|
||||
triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup;
|
||||
charts: ChartsPluginStart;
|
||||
data: DataPublicPluginStart;
|
||||
}
|
||||
|
||||
export class AlertingExamplePlugin implements Plugin<Setup, Start, AlertingExamplePublicSetupDeps> {
|
||||
public setup(
|
||||
core: CoreSetup<AlertingExamplePublicStartDeps>,
|
||||
{ alerting, triggers_actions_ui }: AlertingExamplePublicSetupDeps
|
||||
) {
|
||||
core.application.register({
|
||||
id: 'AlertingExample',
|
||||
title: 'Alerting Example',
|
||||
async mount(params: AppMountParameters) {
|
||||
const [coreStart, depsStart]: [
|
||||
CoreStart,
|
||||
AlertingExamplePublicStartDeps
|
||||
] = await core.getStartServices();
|
||||
const { renderApp } = await import('./application');
|
||||
return renderApp(coreStart, depsStart, params);
|
||||
},
|
||||
});
|
||||
|
||||
triggers_actions_ui.alertTypeRegistry.register(getAlwaysFiringAlertType());
|
||||
triggers_actions_ui.alertTypeRegistry.register(getPeopleInSpaceAlertType());
|
||||
|
||||
registerNavigation(alerting);
|
||||
}
|
||||
|
||||
public start() {}
|
||||
public stop() {}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import uuid from 'uuid';
|
||||
import { range } from 'lodash';
|
||||
import { AlertType } from '../../../../x-pack/plugins/alerting/server';
|
||||
import { DEFAULT_INSTANCES_TO_GENERATE } from '../../common/constants';
|
||||
|
||||
export const alertType: AlertType = {
|
||||
id: 'example.always-firing',
|
||||
name: 'Always firing',
|
||||
actionGroups: [{ id: 'default', name: 'default' }],
|
||||
defaultActionGroupId: 'default',
|
||||
async executor({ services, params: { instances = DEFAULT_INSTANCES_TO_GENERATE }, state }) {
|
||||
const count = (state.count ?? 0) + 1;
|
||||
|
||||
range(instances)
|
||||
.map(() => ({ id: uuid.v4() }))
|
||||
.forEach((instance: { id: string }) => {
|
||||
services
|
||||
.alertInstanceFactory(instance.id)
|
||||
.replaceState({ triggerdOnCycle: count })
|
||||
.scheduleActions('default');
|
||||
});
|
||||
|
||||
return {
|
||||
count,
|
||||
};
|
||||
},
|
||||
};
|
82
examples/alerting_example/server/alert_types/astros.ts
Normal file
82
examples/alerting_example/server/alert_types/astros.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import { AlertType } from '../../../../x-pack/plugins/alerting/server';
|
||||
import { Operator, Craft } from '../../common/constants';
|
||||
|
||||
interface PeopleInSpace {
|
||||
people: Array<{
|
||||
craft: string;
|
||||
name: string;
|
||||
}>;
|
||||
number: number;
|
||||
}
|
||||
|
||||
function getOperator(op: string) {
|
||||
switch (op) {
|
||||
case Operator.AreAbove:
|
||||
return (left: number, right: number) => left > right;
|
||||
case Operator.AreBelow:
|
||||
return (left: number, right: number) => left < right;
|
||||
case Operator.AreExactly:
|
||||
return (left: number, right: number) => left === right;
|
||||
default:
|
||||
return () => {
|
||||
throw new Error(
|
||||
`Invalid Operator "${op}" [${Operator.AreAbove},${Operator.AreBelow},${Operator.AreExactly}]`
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getCraftFilter(craft: string) {
|
||||
return (person: { craft: string; name: string }) =>
|
||||
craft === Craft.OuterSpace ? true : craft === person.craft;
|
||||
}
|
||||
|
||||
export const alertType: AlertType = {
|
||||
id: 'example.people-in-space',
|
||||
name: 'People In Space Right Now',
|
||||
actionGroups: [{ id: 'default', name: 'default' }],
|
||||
defaultActionGroupId: 'default',
|
||||
async executor({ services, params }) {
|
||||
const { outerSpaceCapacity, craft: craftToTriggerBy, op } = params;
|
||||
|
||||
const response = await axios.get<PeopleInSpace>('http://api.open-notify.org/astros.json');
|
||||
const {
|
||||
data: { number: peopleInSpace, people = [] },
|
||||
} = response;
|
||||
|
||||
const peopleInCraft = people.filter(getCraftFilter(craftToTriggerBy));
|
||||
|
||||
if (getOperator(op)(peopleInCraft.length, outerSpaceCapacity)) {
|
||||
peopleInCraft.forEach(({ craft, name }) => {
|
||||
services
|
||||
.alertInstanceFactory(name)
|
||||
.replaceState({ craft })
|
||||
.scheduleActions('default');
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
peopleInSpace,
|
||||
};
|
||||
},
|
||||
};
|
23
examples/alerting_example/server/index.ts
Normal file
23
examples/alerting_example/server/index.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { PluginInitializer } from 'kibana/server';
|
||||
import { AlertingExamplePlugin } from './plugin';
|
||||
|
||||
export const plugin: PluginInitializer<void, void> = () => new AlertingExamplePlugin();
|
39
examples/alerting_example/server/plugin.ts
Normal file
39
examples/alerting_example/server/plugin.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { Plugin, CoreSetup } from 'kibana/server';
|
||||
import { PluginSetupContract as AlertingSetup } from '../../../x-pack/plugins/alerting/server';
|
||||
|
||||
import { alertType as alwaysFiringAlert } from './alert_types/always_firing';
|
||||
import { alertType as peopleInSpaceAlert } from './alert_types/astros';
|
||||
|
||||
// this plugin's dependendencies
|
||||
export interface AlertingExampleDeps {
|
||||
alerting: AlertingSetup;
|
||||
}
|
||||
|
||||
export class AlertingExamplePlugin implements Plugin<void, void, AlertingExampleDeps> {
|
||||
public setup(core: CoreSetup, { alerting }: AlertingExampleDeps) {
|
||||
alerting.registerType(alwaysFiringAlert);
|
||||
alerting.registerType(peopleInSpaceAlert);
|
||||
}
|
||||
|
||||
public start() {}
|
||||
public stop() {}
|
||||
}
|
17
examples/alerting_example/tsconfig.json
Normal file
17
examples/alerting_example/tsconfig.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./target",
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": [
|
||||
"index.ts",
|
||||
"public/**/*.ts",
|
||||
"public/**/*.tsx",
|
||||
"server/**/*.ts",
|
||||
"common/**/*.ts",
|
||||
"../../typings/**/*",
|
||||
],
|
||||
"exclude": []
|
||||
}
|
|
@ -101,7 +101,8 @@
|
|||
"x-pack/legacy/plugins/*",
|
||||
"examples/*",
|
||||
"test/plugin_functional/plugins/*",
|
||||
"test/interpreter_functional/plugins/*"
|
||||
"test/interpreter_functional/plugins/*",
|
||||
"x-pack/test/functional_with_es_ssl/fixtures/plugins/*"
|
||||
],
|
||||
"nohoist": [
|
||||
"**/@types/*",
|
||||
|
|
1
packages/kbn-pm/dist/index.js
vendored
1
packages/kbn-pm/dist/index.js
vendored
|
@ -56996,6 +56996,7 @@ function getProjectPaths({
|
|||
projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'x-pack'));
|
||||
projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'x-pack/plugins/*'));
|
||||
projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'x-pack/legacy/plugins/*'));
|
||||
projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'x-pack/test/functional_with_es_ssl/fixtures/plugins/*'));
|
||||
}
|
||||
|
||||
if (!skipKibanaPlugins) {
|
||||
|
|
|
@ -48,6 +48,7 @@ export function getProjectPaths({ rootPath, ossOnly, skipKibanaPlugins }: Option
|
|||
projectPaths.push(resolve(rootPath, 'x-pack'));
|
||||
projectPaths.push(resolve(rootPath, 'x-pack/plugins/*'));
|
||||
projectPaths.push(resolve(rootPath, 'x-pack/legacy/plugins/*'));
|
||||
projectPaths.push(resolve(rootPath, 'x-pack/test/functional_with_es_ssl/fixtures/plugins/*'));
|
||||
}
|
||||
|
||||
if (!skipKibanaPlugins) {
|
||||
|
|
|
@ -6,6 +6,7 @@ source src/dev/ci_setup/setup_env.sh
|
|||
echo " -> building kibana platform plugins"
|
||||
node scripts/build_kibana_platform_plugins \
|
||||
--scan-dir "$XPACK_DIR/test/plugin_functional/plugins" \
|
||||
--scan-dir "$XPACK_DIR/test/functional_with_es_ssl/fixtures/plugins" \
|
||||
--verbose;
|
||||
|
||||
# doesn't persist, also set in kibanaPipeline.groovy
|
||||
|
|
|
@ -5,3 +5,5 @@
|
|||
*/
|
||||
|
||||
export * from './types';
|
||||
|
||||
export const BASE_ACTION_API_PATH = '/api/action';
|
||||
|
|
|
@ -18,6 +18,7 @@ Table of Contents
|
|||
- [Methods](#methods)
|
||||
- [Executor](#executor)
|
||||
- [Example](#example)
|
||||
- [Alert Navigation](#alert-navigation)
|
||||
- [RESTful API](#restful-api)
|
||||
- [`POST /api/alert`: Create alert](#post-apialert-create-alert)
|
||||
- [`DELETE /api/alert/{id}`: Delete alert](#delete-apialertid-delete-alert)
|
||||
|
@ -268,6 +269,61 @@ server.newPlatform.setup.plugins.alerting.registerType({
|
|||
});
|
||||
```
|
||||
|
||||
## Alert Navigation
|
||||
When registering an Alert Type, you'll likely want to provide a way of viewing alerts of that type within your own plugin, or perhaps you want to provide a view for all alerts created from within your solution within your own UI.
|
||||
|
||||
In order for the Alerting framework to know that your plugin has its own internal view for displaying an alert, you must resigter a navigation handler within the framework.
|
||||
|
||||
A navigation handler is nothing more than a function that receives an Alert and its corresponding AlertType, and is expected to then return the path *within your plugin* which knows how to display this alert.
|
||||
|
||||
The signature of such a handler is:
|
||||
|
||||
```
|
||||
type AlertNavigationHandler = (
|
||||
alert: SanitizedAlert,
|
||||
alertType: AlertType
|
||||
) => string;
|
||||
```
|
||||
|
||||
There are two ways to register this handler.
|
||||
By specifying _alerting_ as a dependency of your *public* (client side) plugin, you'll gain access to two apis: _alerting.registerNavigation_ and _alerting.registerDefaultNavigation_.
|
||||
|
||||
### registerNavigation
|
||||
The _registerNavigation_ api allows you to register a handler for a specific alert type within your solution:
|
||||
|
||||
```
|
||||
alerting.registerNavigation(
|
||||
'my-application-id',
|
||||
'my-application-id.my-alert-type',
|
||||
(alert: SanitizedAlert, alertType: AlertType) => `/my-unique-alert/${alert.id}`
|
||||
);
|
||||
```
|
||||
|
||||
This tells the Alerting framework that, given an alert of the AlertType whose ID is `my-application-id.my-unique-alert-type`, if that Alert's `consumer` value (which is set when the alert is created by your plugin) is your application (whose id is `my-application-id`), then it will navigate to your application using the path `/my-unique-alert/${the id of the alert}`.
|
||||
|
||||
The navigation is handled using the `navigateToApp` api, meaning that the path will be automatically picked up by your `react-router-dom` **Route** component, so all you have top do is configure a Route that handles the path `/my-unique-alert/:id`.
|
||||
|
||||
You can look at the `alerting-example` plugin to see an example of using this API, which is enabled using the `--run-examples` flag when you run `yarn start`.
|
||||
|
||||
### registerDefaultNavigation
|
||||
The _registerDefaultNavigation_ api allows you to register a handler for any alert type within your solution:
|
||||
|
||||
```
|
||||
alerting.registerDefaultNavigation(
|
||||
'my-application-id',
|
||||
(alert: SanitizedAlert, alertType: AlertType) => `/my-other-alerts/${alert.id}`
|
||||
);
|
||||
```
|
||||
|
||||
This tells the Alerting framework that, given any alert whose `consumer` value is your application, as long as then it will navigate to your application using the path `/my-other-alerts/${the id of the alert}`.
|
||||
|
||||
### balancing both APIs side by side
|
||||
As we mentioned, using `registerDefaultNavigation` will tell the Alerting Framework that your application can handle any type of Alert we throw at it, as long as your application created it, using the handler you provide it.
|
||||
|
||||
The only case in which this handler will not be used to evaluate the navigation for an alert (assuming your application is the `consumer`) is if you have also used `registerNavigation` api, along side your `registerDefaultNavigation` usage, to handle that alert's specific AlertType.
|
||||
|
||||
You can use the `registerNavigation` api to specify as many AlertType specific handlers as you like, but you can only use it once per AlertType as we wouldn't know which handler to use if you specified two for the same AlertType. For the same reason, you can only use `registerDefaultNavigation` once per plugin, as it covers all cases for your specific plugin.
|
||||
|
||||
## RESTful API
|
||||
|
||||
Using an alert type requires you to create an alert that will contain parameters and actions for a given alert type. See below for CRUD operations using the API.
|
||||
|
@ -480,4 +536,3 @@ The templating system will take the alert and alert type as described above and
|
|||
```
|
||||
|
||||
There are limitations that we are aware of using only templates, and we are gathering feedback and use cases for these. (for example passing an array of strings to an action).
|
||||
|
||||
|
|
14
x-pack/plugins/alerting/common/alert_navigation.ts
Normal file
14
x-pack/plugins/alerting/common/alert_navigation.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 { JsonObject } from '../../infra/common/typed_json';
|
||||
export interface AlertUrlNavigation {
|
||||
path: string;
|
||||
}
|
||||
export interface AlertStateNavigation {
|
||||
state: JsonObject;
|
||||
}
|
||||
export type AlertNavigation = AlertUrlNavigation | AlertStateNavigation;
|
18
x-pack/plugins/alerting/common/alert_type.ts
Normal file
18
x-pack/plugins/alerting/common/alert_type.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export interface AlertType {
|
||||
id: string;
|
||||
name: string;
|
||||
actionGroups: ActionGroup[];
|
||||
actionVariables: string[];
|
||||
defaultActionGroupId: ActionGroup['id'];
|
||||
}
|
||||
|
||||
export interface ActionGroup {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
|
@ -5,10 +5,9 @@
|
|||
*/
|
||||
|
||||
export * from './alert';
|
||||
export * from './alert_type';
|
||||
export * from './alert_instance';
|
||||
export * from './alert_task_instance';
|
||||
export * from './alert_navigation';
|
||||
|
||||
export interface ActionGroup {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
export const BASE_ALERT_API_PATH = '/api/alert';
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"id": "alerting",
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"version": "8.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"configPath": ["xpack", "alerting"],
|
||||
"requiredPlugins": ["licensing", "taskManager", "encryptedSavedObjects", "actions"],
|
||||
"optionalPlugins": ["usageCollection", "spaces", "security"],
|
||||
"ui": false
|
||||
}
|
||||
"optionalPlugins": ["usageCollection", "spaces", "security"]
|
||||
}
|
||||
|
|
176
x-pack/plugins/alerting/public/alert_api.test.ts
Normal file
176
x-pack/plugins/alerting/public/alert_api.test.ts
Normal file
|
@ -0,0 +1,176 @@
|
|||
/*
|
||||
* 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 { AlertType } from '../common';
|
||||
import { httpServiceMock } from '../../../../src/core/public/mocks';
|
||||
import { loadAlert, loadAlertState, loadAlertType, loadAlertTypes } from './alert_api';
|
||||
import uuid from 'uuid';
|
||||
|
||||
const http = httpServiceMock.createStartContract();
|
||||
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('loadAlertTypes', () => {
|
||||
test('should call get alert types API', async () => {
|
||||
const resolvedValue: AlertType[] = [
|
||||
{
|
||||
id: 'test',
|
||||
name: 'Test',
|
||||
actionVariables: ['var1'],
|
||||
actionGroups: [{ id: 'default', name: 'Default' }],
|
||||
defaultActionGroupId: 'default',
|
||||
},
|
||||
];
|
||||
http.get.mockResolvedValueOnce(resolvedValue);
|
||||
|
||||
const result = await loadAlertTypes({ http });
|
||||
expect(result).toEqual(resolvedValue);
|
||||
expect(http.get.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"/api/alert/types",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadAlertType', () => {
|
||||
test('should call get alert types API', async () => {
|
||||
const alertType: AlertType = {
|
||||
id: 'test',
|
||||
name: 'Test',
|
||||
actionVariables: ['var1'],
|
||||
actionGroups: [{ id: 'default', name: 'Default' }],
|
||||
defaultActionGroupId: 'default',
|
||||
};
|
||||
http.get.mockResolvedValueOnce([alertType]);
|
||||
|
||||
await loadAlertType({ http, id: alertType.id });
|
||||
|
||||
expect(http.get.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"/api/alert/types",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('should find the required alertType', async () => {
|
||||
const alertType: AlertType = {
|
||||
id: 'test-another',
|
||||
name: 'Test Another',
|
||||
actionVariables: [],
|
||||
actionGroups: [{ id: 'default', name: 'Default' }],
|
||||
defaultActionGroupId: 'default',
|
||||
};
|
||||
http.get.mockResolvedValueOnce([alertType]);
|
||||
|
||||
expect(await loadAlertType({ http, id: 'test-another' })).toEqual(alertType);
|
||||
});
|
||||
|
||||
test('should throw if required alertType is missing', async () => {
|
||||
http.get.mockResolvedValueOnce([
|
||||
{
|
||||
id: 'test-another',
|
||||
name: 'Test Another',
|
||||
actionVariables: [],
|
||||
actionGroups: [{ id: 'default', name: 'Default' }],
|
||||
defaultActionGroupId: 'default',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(loadAlertType({ http, id: 'test' })).rejects.toMatchInlineSnapshot(
|
||||
`[Error: Alert type "test" is not registered.]`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadAlert', () => {
|
||||
test('should call get API with base parameters', async () => {
|
||||
const alertId = uuid.v4();
|
||||
const resolvedValue = {
|
||||
id: alertId,
|
||||
name: 'name',
|
||||
tags: [],
|
||||
enabled: true,
|
||||
alertTypeId: '.noop',
|
||||
schedule: { interval: '1s' },
|
||||
actions: [],
|
||||
params: {},
|
||||
createdBy: null,
|
||||
updatedBy: null,
|
||||
throttle: null,
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
};
|
||||
http.get.mockResolvedValueOnce(resolvedValue);
|
||||
|
||||
expect(await loadAlert({ http, alertId })).toEqual(resolvedValue);
|
||||
expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadAlertState', () => {
|
||||
test('should call get API with base parameters', async () => {
|
||||
const alertId = uuid.v4();
|
||||
const resolvedValue = {
|
||||
alertTypeState: {
|
||||
some: 'value',
|
||||
},
|
||||
alertInstances: {
|
||||
first_instance: {},
|
||||
second_instance: {},
|
||||
},
|
||||
};
|
||||
http.get.mockResolvedValueOnce(resolvedValue);
|
||||
|
||||
expect(await loadAlertState({ http, alertId })).toEqual(resolvedValue);
|
||||
expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}/state`);
|
||||
});
|
||||
|
||||
test('should parse AlertInstances', async () => {
|
||||
const alertId = uuid.v4();
|
||||
const resolvedValue = {
|
||||
alertTypeState: {
|
||||
some: 'value',
|
||||
},
|
||||
alertInstances: {
|
||||
first_instance: {
|
||||
state: {},
|
||||
meta: {
|
||||
lastScheduledActions: {
|
||||
group: 'first_group',
|
||||
date: '2020-02-09T23:15:41.941Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
http.get.mockResolvedValueOnce(resolvedValue);
|
||||
|
||||
expect(await loadAlertState({ http, alertId })).toEqual({
|
||||
...resolvedValue,
|
||||
alertInstances: {
|
||||
first_instance: {
|
||||
state: {},
|
||||
meta: {
|
||||
lastScheduledActions: {
|
||||
group: 'first_group',
|
||||
date: new Date('2020-02-09T23:15:41.941Z'),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}/state`);
|
||||
});
|
||||
|
||||
test('should handle empty response from api', async () => {
|
||||
const alertId = uuid.v4();
|
||||
http.get.mockResolvedValueOnce('');
|
||||
|
||||
expect(await loadAlertState({ http, alertId })).toEqual({});
|
||||
expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}/state`);
|
||||
});
|
||||
});
|
74
x-pack/plugins/alerting/public/alert_api.ts
Normal file
74
x-pack/plugins/alerting/public/alert_api.ts
Normal file
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 { HttpSetup } from 'kibana/public';
|
||||
import * as t from 'io-ts';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { fold } from 'fp-ts/lib/Either';
|
||||
import { findFirst } from 'fp-ts/lib/Array';
|
||||
import { isNone } from 'fp-ts/lib/Option';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { BASE_ALERT_API_PATH, alertStateSchema } from '../common';
|
||||
import { Alert, AlertType, AlertTaskState } from '../common';
|
||||
|
||||
export async function loadAlertTypes({ http }: { http: HttpSetup }): Promise<AlertType[]> {
|
||||
return await http.get(`${BASE_ALERT_API_PATH}/types`);
|
||||
}
|
||||
|
||||
export async function loadAlertType({
|
||||
http,
|
||||
id,
|
||||
}: {
|
||||
http: HttpSetup;
|
||||
id: AlertType['id'];
|
||||
}): Promise<AlertType> {
|
||||
const maybeAlertType = findFirst<AlertType>(type => type.id === id)(
|
||||
await http.get(`${BASE_ALERT_API_PATH}/types`)
|
||||
);
|
||||
if (isNone(maybeAlertType)) {
|
||||
throw new Error(
|
||||
i18n.translate('xpack.alerting.loadAlertType.missingAlertTypeError', {
|
||||
defaultMessage: 'Alert type "{id}" is not registered.',
|
||||
values: {
|
||||
id,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
return maybeAlertType.value;
|
||||
}
|
||||
|
||||
export async function loadAlert({
|
||||
http,
|
||||
alertId,
|
||||
}: {
|
||||
http: HttpSetup;
|
||||
alertId: string;
|
||||
}): Promise<Alert> {
|
||||
return await http.get(`${BASE_ALERT_API_PATH}/${alertId}`);
|
||||
}
|
||||
|
||||
type EmptyHttpResponse = '';
|
||||
export async function loadAlertState({
|
||||
http,
|
||||
alertId,
|
||||
}: {
|
||||
http: HttpSetup;
|
||||
alertId: string;
|
||||
}): Promise<AlertTaskState> {
|
||||
return await http
|
||||
.get(`${BASE_ALERT_API_PATH}/${alertId}/state`)
|
||||
.then((state: AlertTaskState | EmptyHttpResponse) => (state ? state : {}))
|
||||
.then((state: AlertTaskState) => {
|
||||
return pipe(
|
||||
alertStateSchema.decode(state),
|
||||
fold((e: t.Errors) => {
|
||||
throw new Error(`Alert "${alertId}" has invalid state`);
|
||||
}, t.identity)
|
||||
);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 { AlertNavigationRegistry } from './alert_navigation_registry';
|
||||
|
||||
type Schema = PublicMethodsOf<AlertNavigationRegistry>;
|
||||
|
||||
const createAlertNavigationRegistryMock = () => {
|
||||
const mocked: jest.Mocked<Schema> = {
|
||||
has: jest.fn(),
|
||||
hasDefaultHandler: jest.fn(),
|
||||
hasTypedHandler: jest.fn(),
|
||||
register: jest.fn(),
|
||||
registerDefault: jest.fn(),
|
||||
get: jest.fn(),
|
||||
};
|
||||
return mocked;
|
||||
};
|
||||
|
||||
export const alertNavigationRegistryMock = {
|
||||
create: createAlertNavigationRegistryMock,
|
||||
};
|
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* 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 { AlertNavigationRegistry } from './alert_navigation_registry';
|
||||
import { AlertType, SanitizedAlert } from '../../common';
|
||||
import uuid from 'uuid';
|
||||
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
|
||||
const mockAlertType = (id: string): AlertType => ({
|
||||
id,
|
||||
name: id,
|
||||
actionGroups: [],
|
||||
actionVariables: [],
|
||||
defaultActionGroupId: 'default',
|
||||
});
|
||||
|
||||
describe('AlertNavigationRegistry', () => {
|
||||
function handler(alert: SanitizedAlert, alertType: AlertType) {
|
||||
return {};
|
||||
}
|
||||
|
||||
describe('has()', () => {
|
||||
test('returns false for unregistered consumer handlers', () => {
|
||||
const registry = new AlertNavigationRegistry();
|
||||
expect(registry.has('siem', mockAlertType(uuid.v4()))).toEqual(false);
|
||||
});
|
||||
|
||||
test('returns false for unregistered alert types handlers', () => {
|
||||
const registry = new AlertNavigationRegistry();
|
||||
expect(registry.has('siem', mockAlertType('index_threshold'))).toEqual(false);
|
||||
});
|
||||
|
||||
test('returns true for registered consumer & alert types handlers', () => {
|
||||
const registry = new AlertNavigationRegistry();
|
||||
const alertType = mockAlertType('index_threshold');
|
||||
registry.register('siem', alertType, handler);
|
||||
expect(registry.has('siem', alertType)).toEqual(true);
|
||||
});
|
||||
|
||||
test('returns true for registered consumer with default handler', () => {
|
||||
const registry = new AlertNavigationRegistry();
|
||||
const alertType = mockAlertType('index_threshold');
|
||||
registry.registerDefault('siem', handler);
|
||||
expect(registry.has('siem', alertType)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasDefaultHandler()', () => {
|
||||
test('returns false for unregistered consumer handlers', () => {
|
||||
const registry = new AlertNavigationRegistry();
|
||||
expect(registry.hasDefaultHandler('siem')).toEqual(false);
|
||||
});
|
||||
|
||||
test('returns true for registered consumer handlers', () => {
|
||||
const registry = new AlertNavigationRegistry();
|
||||
|
||||
registry.registerDefault('siem', handler);
|
||||
expect(registry.hasDefaultHandler('siem')).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('register()', () => {
|
||||
test('registers a handler by consumer & Alert Type', () => {
|
||||
const registry = new AlertNavigationRegistry();
|
||||
const alertType = mockAlertType('index_threshold');
|
||||
registry.register('siem', alertType, handler);
|
||||
expect(registry.has('siem', alertType)).toEqual(true);
|
||||
});
|
||||
|
||||
test('allows registeration of multiple handlers for the same consumer', () => {
|
||||
const registry = new AlertNavigationRegistry();
|
||||
|
||||
const indexThresholdAlertType = mockAlertType('index_threshold');
|
||||
registry.register('siem', indexThresholdAlertType, handler);
|
||||
expect(registry.has('siem', indexThresholdAlertType)).toEqual(true);
|
||||
|
||||
const geoAlertType = mockAlertType('geogrid');
|
||||
registry.register('siem', geoAlertType, handler);
|
||||
expect(registry.has('siem', geoAlertType)).toEqual(true);
|
||||
});
|
||||
|
||||
test('allows registeration of multiple handlers for the same Alert Type', () => {
|
||||
const registry = new AlertNavigationRegistry();
|
||||
|
||||
const indexThresholdAlertType = mockAlertType('geogrid');
|
||||
registry.register('siem', indexThresholdAlertType, handler);
|
||||
expect(registry.has('siem', indexThresholdAlertType)).toEqual(true);
|
||||
|
||||
registry.register('apm', indexThresholdAlertType, handler);
|
||||
expect(registry.has('apm', indexThresholdAlertType)).toEqual(true);
|
||||
});
|
||||
|
||||
test('throws if an existing handler is registered', () => {
|
||||
const registry = new AlertNavigationRegistry();
|
||||
const alertType = mockAlertType('index_threshold');
|
||||
registry.register('siem', alertType, handler);
|
||||
expect(() => {
|
||||
registry.register('siem', alertType, handler);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Navigation for Alert type \\"index_threshold\\" within \\"siem\\" is already registered."`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('registerDefault()', () => {
|
||||
test('registers a handler by consumer', () => {
|
||||
const registry = new AlertNavigationRegistry();
|
||||
registry.registerDefault('siem', handler);
|
||||
expect(registry.hasDefaultHandler('siem')).toEqual(true);
|
||||
});
|
||||
|
||||
test('allows registeration of default and typed handlers for the same consumer', () => {
|
||||
const registry = new AlertNavigationRegistry();
|
||||
|
||||
registry.registerDefault('siem', handler);
|
||||
expect(registry.hasDefaultHandler('siem')).toEqual(true);
|
||||
|
||||
const geoAlertType = mockAlertType('geogrid');
|
||||
registry.register('siem', geoAlertType, handler);
|
||||
expect(registry.has('siem', geoAlertType)).toEqual(true);
|
||||
});
|
||||
|
||||
test('throws if an existing handler is registered', () => {
|
||||
const registry = new AlertNavigationRegistry();
|
||||
registry.registerDefault('siem', handler);
|
||||
expect(() => {
|
||||
registry.registerDefault('siem', handler);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Default Navigation within \\"siem\\" is already registered."`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get()', () => {
|
||||
test('returns registered handlers by consumer & Alert Type', () => {
|
||||
const registry = new AlertNavigationRegistry();
|
||||
|
||||
function indexThresholdHandler(alert: SanitizedAlert, alertType: AlertType) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const indexThresholdAlertType = mockAlertType('indexThreshold');
|
||||
registry.register('siem', indexThresholdAlertType, indexThresholdHandler);
|
||||
expect(registry.get('siem', indexThresholdAlertType)).toEqual(indexThresholdHandler);
|
||||
});
|
||||
|
||||
test('returns default handlers by consumer when there is no handler for requested alert type', () => {
|
||||
const registry = new AlertNavigationRegistry();
|
||||
|
||||
function defaultHandler(alert: SanitizedAlert, alertType: AlertType) {
|
||||
return {};
|
||||
}
|
||||
|
||||
registry.registerDefault('siem', defaultHandler);
|
||||
expect(registry.get('siem', mockAlertType('geogrid'))).toEqual(defaultHandler);
|
||||
});
|
||||
|
||||
test('returns default handlers by consumer when there are other alert type handler', () => {
|
||||
const registry = new AlertNavigationRegistry();
|
||||
|
||||
registry.register('siem', mockAlertType('indexThreshold'), () => ({}));
|
||||
|
||||
function defaultHandler(alert: SanitizedAlert, alertType: AlertType) {
|
||||
return {};
|
||||
}
|
||||
|
||||
registry.registerDefault('siem', defaultHandler);
|
||||
expect(registry.get('siem', mockAlertType('geogrid'))).toEqual(defaultHandler);
|
||||
});
|
||||
|
||||
test('throws if a handler isnt registered', () => {
|
||||
const registry = new AlertNavigationRegistry();
|
||||
const alertType = mockAlertType('index_threshold');
|
||||
|
||||
expect(() => registry.get('siem', alertType)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Navigation for Alert type \\"index_threshold\\" within \\"siem\\" is not registered."`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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 Boom from 'boom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { AlertType } from '../../common';
|
||||
import { AlertNavigationHandler } from './types';
|
||||
|
||||
const DEFAULT_HANDLER = Symbol('*');
|
||||
export class AlertNavigationRegistry {
|
||||
private readonly alertNavigations: Map<
|
||||
string,
|
||||
Map<string | symbol, AlertNavigationHandler>
|
||||
> = new Map();
|
||||
|
||||
public has(consumer: string, alertType: AlertType) {
|
||||
return this.hasTypedHandler(consumer, alertType) || this.hasDefaultHandler(consumer);
|
||||
}
|
||||
|
||||
public hasTypedHandler(consumer: string, alertType: AlertType) {
|
||||
return this.alertNavigations.get(consumer)?.has(alertType.id) ?? false;
|
||||
}
|
||||
|
||||
public hasDefaultHandler(consumer: string) {
|
||||
return this.alertNavigations.get(consumer)?.has(DEFAULT_HANDLER) ?? false;
|
||||
}
|
||||
|
||||
private createConsumerNavigation(consumer: string) {
|
||||
const consumerNavigations = new Map<string, AlertNavigationHandler>();
|
||||
this.alertNavigations.set(consumer, consumerNavigations);
|
||||
return consumerNavigations;
|
||||
}
|
||||
|
||||
public registerDefault(consumer: string, handler: AlertNavigationHandler) {
|
||||
if (this.hasDefaultHandler(consumer)) {
|
||||
throw Boom.badRequest(
|
||||
i18n.translate('xpack.alerting.alertNavigationRegistry.register.duplicateDefaultError', {
|
||||
defaultMessage: 'Default Navigation within "{consumer}" is already registered.',
|
||||
values: {
|
||||
consumer,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const consumerNavigations =
|
||||
this.alertNavigations.get(consumer) ?? this.createConsumerNavigation(consumer);
|
||||
|
||||
consumerNavigations.set(DEFAULT_HANDLER, handler);
|
||||
}
|
||||
|
||||
public register(consumer: string, alertType: AlertType, handler: AlertNavigationHandler) {
|
||||
if (this.hasTypedHandler(consumer, alertType)) {
|
||||
throw Boom.badRequest(
|
||||
i18n.translate('xpack.alerting.alertNavigationRegistry.register.duplicateNavigationError', {
|
||||
defaultMessage:
|
||||
'Navigation for Alert type "{alertType}" within "{consumer}" is already registered.',
|
||||
values: {
|
||||
alertType: alertType.id,
|
||||
consumer,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const consumerNavigations =
|
||||
this.alertNavigations.get(consumer) ?? this.createConsumerNavigation(consumer);
|
||||
|
||||
consumerNavigations.set(alertType.id, handler);
|
||||
}
|
||||
|
||||
public get(consumer: string, alertType: AlertType): AlertNavigationHandler {
|
||||
if (this.has(consumer, alertType)) {
|
||||
const consumerHandlers = this.alertNavigations.get(consumer)!;
|
||||
return (consumerHandlers.get(alertType.id) ?? consumerHandlers.get(DEFAULT_HANDLER))!;
|
||||
}
|
||||
|
||||
throw Boom.badRequest(
|
||||
i18n.translate('xpack.alerting.alertNavigationRegistry.get.missingNavigationError', {
|
||||
defaultMessage:
|
||||
'Navigation for Alert type "{alertType}" within "{consumer}" is not registered.',
|
||||
values: {
|
||||
alertType: alertType.id,
|
||||
consumer,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './types';
|
||||
export * from './alert_navigation_registry';
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 { JsonObject } from '../../../infra/common/typed_json';
|
||||
import { AlertType, SanitizedAlert } from '../../common';
|
||||
|
||||
export type AlertNavigationHandler = (
|
||||
alert: SanitizedAlert,
|
||||
alertType: AlertType
|
||||
) => JsonObject | string;
|
12
x-pack/plugins/alerting/public/index.ts
Normal file
12
x-pack/plugins/alerting/public/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 { AlertingPublicPlugin } from './plugin';
|
||||
export { PluginSetupContract, PluginStartContract } from './plugin';
|
||||
|
||||
export function plugin() {
|
||||
return new AlertingPublicPlugin();
|
||||
}
|
24
x-pack/plugins/alerting/public/mocks.ts
Normal file
24
x-pack/plugins/alerting/public/mocks.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 { AlertingPublicPlugin } from './plugin';
|
||||
|
||||
export type Setup = jest.Mocked<ReturnType<AlertingPublicPlugin['setup']>>;
|
||||
export type Start = jest.Mocked<ReturnType<AlertingPublicPlugin['start']>>;
|
||||
|
||||
const createSetupContract = (): Setup => ({
|
||||
registerNavigation: jest.fn(),
|
||||
registerDefaultNavigation: jest.fn(),
|
||||
});
|
||||
|
||||
const createStartContract = (): Start => ({
|
||||
getNavigation: jest.fn(),
|
||||
});
|
||||
|
||||
export const alertingPluginMock = {
|
||||
createSetupContract,
|
||||
createStartContract,
|
||||
};
|
64
x-pack/plugins/alerting/public/plugin.ts
Normal file
64
x-pack/plugins/alerting/public/plugin.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 { CoreSetup, Plugin, CoreStart } from 'src/core/public';
|
||||
|
||||
import { AlertNavigationRegistry, AlertNavigationHandler } from './alert_navigation_registry';
|
||||
import { loadAlert, loadAlertType } from './alert_api';
|
||||
import { Alert, AlertNavigation } from '../common';
|
||||
|
||||
export interface PluginSetupContract {
|
||||
registerNavigation: (
|
||||
consumer: string,
|
||||
alertType: string,
|
||||
handler: AlertNavigationHandler
|
||||
) => void;
|
||||
registerDefaultNavigation: (consumer: string, handler: AlertNavigationHandler) => void;
|
||||
}
|
||||
export interface PluginStartContract {
|
||||
getNavigation: (alertId: Alert['id']) => Promise<AlertNavigation | undefined>;
|
||||
}
|
||||
|
||||
export class AlertingPublicPlugin implements Plugin<PluginSetupContract, PluginStartContract> {
|
||||
private alertNavigationRegistry?: AlertNavigationRegistry;
|
||||
public setup(core: CoreSetup) {
|
||||
this.alertNavigationRegistry = new AlertNavigationRegistry();
|
||||
|
||||
const registerNavigation = async (
|
||||
consumer: string,
|
||||
alertType: string,
|
||||
handler: AlertNavigationHandler
|
||||
) =>
|
||||
this.alertNavigationRegistry!.register(
|
||||
consumer,
|
||||
await loadAlertType({ http: core.http, id: alertType }),
|
||||
handler
|
||||
);
|
||||
|
||||
const registerDefaultNavigation = async (consumer: string, handler: AlertNavigationHandler) =>
|
||||
this.alertNavigationRegistry!.registerDefault(consumer, handler);
|
||||
|
||||
return {
|
||||
registerNavigation,
|
||||
registerDefaultNavigation,
|
||||
};
|
||||
}
|
||||
|
||||
public start(core: CoreStart) {
|
||||
return {
|
||||
getNavigation: async (alertId: Alert['id']) => {
|
||||
const alert = await loadAlert({ http: core.http, alertId });
|
||||
const alertType = await loadAlertType({ http: core.http, id: alert.alertTypeId });
|
||||
|
||||
if (this.alertNavigationRegistry!.has(alert.consumer, alertType)) {
|
||||
const navigationHandler = this.alertNavigationRegistry!.get(alert.consumer, alertType);
|
||||
const state = navigationHandler(alert, alertType);
|
||||
return typeof state === 'string' ? { path: state } : { state };
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -137,6 +137,7 @@ export class AlertingPlugin {
|
|||
taskRunnerFactory: this.taskRunnerFactory,
|
||||
});
|
||||
this.alertTypeRegistry = alertTypeRegistry;
|
||||
|
||||
this.serverBasePath = core.http.basePath.serverBasePath;
|
||||
|
||||
const usageCollection = plugins.usageCollection;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"id": "triggers_actions_ui",
|
||||
"version": "kibana",
|
||||
"server": false,
|
||||
"ui": true,
|
||||
"optionalPlugins": ["alerting", "alertingBuiltins"],
|
||||
"requiredPlugins": ["management", "charts", "data"]
|
||||
}
|
||||
"id": "triggers_actions_ui",
|
||||
"version": "kibana",
|
||||
"server": false,
|
||||
"ui": true,
|
||||
"optionalPlugins": ["alerting", "alertingBuiltins"],
|
||||
"requiredPlugins": ["management", "charts", "data"]
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
IUiSettingsClient,
|
||||
ApplicationStart,
|
||||
ChromeBreadcrumb,
|
||||
CoreStart,
|
||||
} from 'kibana/public';
|
||||
import { BASE_PATH, Section, routeToAlertDetails } from './constants';
|
||||
import { TriggersActionsUIHome } from './home';
|
||||
|
@ -23,11 +24,14 @@ import { TypeRegistry } from './type_registry';
|
|||
import { AlertDetailsRouteWithApi as AlertDetailsRoute } from './sections/alert_details/components/alert_details_route';
|
||||
import { ChartsPluginStart } from '../../../../../src/plugins/charts/public';
|
||||
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
|
||||
import { PluginStartContract as AlertingStart } from '../../../alerting/public';
|
||||
|
||||
export interface AppDeps {
|
||||
dataPlugin: DataPublicPluginStart;
|
||||
charts: ChartsPluginStart;
|
||||
chrome: ChromeStart;
|
||||
alerting?: AlertingStart;
|
||||
navigateToApp: CoreStart['application']['navigateToApp'];
|
||||
docLinks: DocLinksStart;
|
||||
toastNotifications: ToastsSetup;
|
||||
http: HttpSetup;
|
||||
|
|
|
@ -4,9 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { BASE_ALERT_API_PATH } from '../../../../alerting/common';
|
||||
export { BASE_ACTION_API_PATH } from '../../../../actions/common';
|
||||
|
||||
export const BASE_PATH = '/management/kibana/triggersActions';
|
||||
export const BASE_ACTION_API_PATH = '/api/action';
|
||||
export const BASE_ALERT_API_PATH = '/api/alert';
|
||||
|
||||
export type Section = 'connectors' | 'alerts';
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import { actionTypeRegistryMock } from '../../../action_type_registry.mock';
|
|||
import { AppContextProvider } from '../../../app_context';
|
||||
import { chartPluginMock } from '../../../../../../../../src/plugins/charts/public/mocks';
|
||||
import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks';
|
||||
import { alertingPluginMock } from '../../../../../../alerting/public/mocks';
|
||||
|
||||
jest.mock('../../../lib/action_connector_api', () => ({
|
||||
loadAllActions: jest.fn(),
|
||||
|
@ -49,7 +50,7 @@ describe('actions_connectors_list component empty', () => {
|
|||
{
|
||||
chrome,
|
||||
docLinks,
|
||||
application: { capabilities },
|
||||
application: { capabilities, navigateToApp },
|
||||
},
|
||||
] = await mockes.getStartServices();
|
||||
const deps = {
|
||||
|
@ -57,9 +58,11 @@ describe('actions_connectors_list component empty', () => {
|
|||
docLinks,
|
||||
dataPlugin: dataPluginMock.createStartContract(),
|
||||
charts: chartPluginMock.createStartContract(),
|
||||
alerting: alertingPluginMock.createStartContract(),
|
||||
toastNotifications: mockes.notifications.toasts,
|
||||
http: mockes.http,
|
||||
uiSettings: mockes.uiSettings,
|
||||
navigateToApp,
|
||||
capabilities: {
|
||||
...capabilities,
|
||||
siem: {
|
||||
|
@ -145,7 +148,7 @@ describe('actions_connectors_list component with items', () => {
|
|||
{
|
||||
chrome,
|
||||
docLinks,
|
||||
application: { capabilities },
|
||||
application: { capabilities, navigateToApp },
|
||||
},
|
||||
] = await mockes.getStartServices();
|
||||
const deps = {
|
||||
|
@ -153,9 +156,11 @@ describe('actions_connectors_list component with items', () => {
|
|||
docLinks,
|
||||
dataPlugin: dataPluginMock.createStartContract(),
|
||||
charts: chartPluginMock.createStartContract(),
|
||||
alerting: alertingPluginMock.createStartContract(),
|
||||
toastNotifications: mockes.notifications.toasts,
|
||||
http: mockes.http,
|
||||
uiSettings: mockes.uiSettings,
|
||||
navigateToApp,
|
||||
capabilities: {
|
||||
...capabilities,
|
||||
siem: {
|
||||
|
@ -228,7 +233,7 @@ describe('actions_connectors_list component empty with show only capability', ()
|
|||
{
|
||||
chrome,
|
||||
docLinks,
|
||||
application: { capabilities },
|
||||
application: { capabilities, navigateToApp },
|
||||
},
|
||||
] = await mockes.getStartServices();
|
||||
const deps = {
|
||||
|
@ -236,9 +241,11 @@ describe('actions_connectors_list component empty with show only capability', ()
|
|||
docLinks,
|
||||
dataPlugin: dataPluginMock.createStartContract(),
|
||||
charts: chartPluginMock.createStartContract(),
|
||||
alerting: alertingPluginMock.createStartContract(),
|
||||
toastNotifications: mockes.notifications.toasts,
|
||||
http: mockes.http,
|
||||
uiSettings: mockes.uiSettings,
|
||||
navigateToApp,
|
||||
capabilities: {
|
||||
...capabilities,
|
||||
siem: {
|
||||
|
@ -316,7 +323,7 @@ describe('actions_connectors_list with show only capability', () => {
|
|||
{
|
||||
chrome,
|
||||
docLinks,
|
||||
application: { capabilities },
|
||||
application: { capabilities, navigateToApp },
|
||||
},
|
||||
] = await mockes.getStartServices();
|
||||
const deps = {
|
||||
|
@ -324,9 +331,11 @@ describe('actions_connectors_list with show only capability', () => {
|
|||
docLinks,
|
||||
dataPlugin: dataPluginMock.createStartContract(),
|
||||
charts: chartPluginMock.createStartContract(),
|
||||
alerting: alertingPluginMock.createStartContract(),
|
||||
toastNotifications: mockes.notifications.toasts,
|
||||
http: mockes.http,
|
||||
uiSettings: mockes.uiSettings,
|
||||
navigateToApp,
|
||||
capabilities: {
|
||||
...capabilities,
|
||||
siem: {
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
import { times, random } from 'lodash';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ViewInApp } from './view_in_app';
|
||||
|
||||
jest.mock('../../../app_context', () => ({
|
||||
useAppDependencies: jest.fn(() => ({
|
||||
|
@ -247,14 +248,7 @@ describe('alert_details', () => {
|
|||
expect(
|
||||
shallow(
|
||||
<AlertDetails alert={alert} alertType={alertType} actionTypes={[]} {...mockAlertApis} />
|
||||
).containsMatchingElement(
|
||||
<EuiButtonEmpty disabled={true} iconType="popout">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.alertDetails.viewAlertInAppButtonLabel"
|
||||
defaultMessage="View in app"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
)
|
||||
).containsMatchingElement(<ViewInApp alert={alert} />)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ import {
|
|||
withBulkAlertOperations,
|
||||
} from '../../common/components/with_bulk_alert_api_operations';
|
||||
import { AlertInstancesRouteWithApi } from './alert_instances_route';
|
||||
import { ViewInApp } from './view_in_app';
|
||||
|
||||
type AlertDetailsProps = {
|
||||
alert: Alert;
|
||||
|
@ -95,12 +96,7 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
|
|||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty disabled={true} iconType="popout">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.alertDetails.viewAlertInAppButtonLabel"
|
||||
defaultMessage="View in app"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
<ViewInApp alert={alert} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty disabled={true} iconType="menuLeft">
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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 * as React from 'react';
|
||||
import uuid from 'uuid';
|
||||
import { mount, ReactWrapper } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import { Alert } from '../../../../types';
|
||||
import { ViewInApp } from './view_in_app';
|
||||
import { useAppDependencies } from '../../../app_context';
|
||||
|
||||
jest.mock('../../../app_context', () => {
|
||||
const alerting = {
|
||||
getNavigation: jest.fn(async id => (id === 'alert-with-nav' ? { path: '/alert' } : undefined)),
|
||||
};
|
||||
const navigateToApp = jest.fn();
|
||||
return {
|
||||
useAppDependencies: jest.fn(() => ({
|
||||
http: jest.fn(),
|
||||
navigateToApp,
|
||||
alerting,
|
||||
legacy: {
|
||||
capabilities: {
|
||||
get: jest.fn(() => ({})),
|
||||
},
|
||||
},
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../lib/capabilities', () => ({
|
||||
hasSaveAlertsCapability: jest.fn(() => true),
|
||||
}));
|
||||
|
||||
describe('alert_details', () => {
|
||||
describe('link to the app that created the alert', () => {
|
||||
it('is disabled when there is no navigation', async () => {
|
||||
const alert = mockAlert();
|
||||
const { alerting } = useAppDependencies();
|
||||
|
||||
let component: ReactWrapper;
|
||||
await act(async () => {
|
||||
// use mount as we need useEffect to run
|
||||
component = mount(<ViewInApp alert={alert} />);
|
||||
|
||||
await waitForUseEffect();
|
||||
|
||||
expect(component!.find('button').prop('disabled')).toBe(true);
|
||||
expect(component!.text()).toBe('View in app');
|
||||
|
||||
expect(alerting!.getNavigation).toBeCalledWith(alert.id);
|
||||
});
|
||||
});
|
||||
|
||||
it('enabled when there is navigation', async () => {
|
||||
const alert = mockAlert({ id: 'alert-with-nav', consumer: 'siem' });
|
||||
const { navigateToApp } = useAppDependencies();
|
||||
|
||||
let component: ReactWrapper;
|
||||
act(async () => {
|
||||
// use mount as we need useEffect to run
|
||||
component = mount(<ViewInApp alert={alert} />);
|
||||
|
||||
await waitForUseEffect();
|
||||
|
||||
expect(component!.find('button').prop('disabled')).toBe(undefined);
|
||||
|
||||
component!.find('button').prop('onClick')!({
|
||||
currentTarget: {},
|
||||
} as React.MouseEvent<{}, MouseEvent>);
|
||||
|
||||
expect(navigateToApp).toBeCalledWith('siem', '/alert');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function waitForUseEffect() {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
}
|
||||
|
||||
function mockAlert(overloads: Partial<Alert> = {}): Alert {
|
||||
return {
|
||||
id: uuid.v4(),
|
||||
enabled: true,
|
||||
name: `alert-${uuid.v4()}`,
|
||||
tags: [],
|
||||
alertTypeId: '.noop',
|
||||
consumer: 'consumer',
|
||||
schedule: { interval: '1m' },
|
||||
actions: [],
|
||||
params: {},
|
||||
createdBy: null,
|
||||
updatedBy: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
apiKeyOwner: null,
|
||||
throttle: null,
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
...overloads,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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, { useState, useEffect } from 'react';
|
||||
import { EuiButtonEmpty } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { CoreStart } from 'kibana/public';
|
||||
import { fromNullable, fold } from 'fp-ts/lib/Option';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { useAppDependencies } from '../../../app_context';
|
||||
|
||||
import {
|
||||
AlertNavigation,
|
||||
AlertStateNavigation,
|
||||
AlertUrlNavigation,
|
||||
} from '../../../../../../alerting/common';
|
||||
import { Alert } from '../../../../types';
|
||||
|
||||
export interface ViewInAppProps {
|
||||
alert: Alert;
|
||||
}
|
||||
|
||||
const NO_NAVIGATION = false;
|
||||
|
||||
type AlertNavigationLoadingState = AlertNavigation | false | null;
|
||||
|
||||
export const ViewInApp: React.FunctionComponent<ViewInAppProps> = ({ alert }) => {
|
||||
const { navigateToApp, alerting: maybeAlerting } = useAppDependencies();
|
||||
|
||||
const [alertNavigation, setAlertNavigation] = useState<AlertNavigationLoadingState>(null);
|
||||
useEffect(() => {
|
||||
pipe(
|
||||
fromNullable(maybeAlerting),
|
||||
fold(
|
||||
/**
|
||||
* If the alerting plugin is disabled,
|
||||
* navigation isn't supported
|
||||
*/
|
||||
() => setAlertNavigation(NO_NAVIGATION),
|
||||
alerting =>
|
||||
alerting
|
||||
.getNavigation(alert.id)
|
||||
.then(nav => (nav ? setAlertNavigation(nav) : setAlertNavigation(NO_NAVIGATION)))
|
||||
.catch(() => {
|
||||
setAlertNavigation(NO_NAVIGATION);
|
||||
})
|
||||
)
|
||||
);
|
||||
}, [alert.id, maybeAlerting]);
|
||||
|
||||
return (
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="alertDetails-viewInApp"
|
||||
isLoading={alertNavigation === null}
|
||||
disabled={!hasNavigation(alertNavigation)}
|
||||
iconType="popout"
|
||||
{...getNavigationHandler(alertNavigation, alert, navigateToApp)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.alertDetails.viewAlertInAppButtonLabel"
|
||||
defaultMessage="View in app"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
};
|
||||
|
||||
function hasNavigation(
|
||||
alertNavigation: AlertNavigationLoadingState
|
||||
): alertNavigation is AlertStateNavigation | AlertUrlNavigation {
|
||||
return alertNavigation
|
||||
? alertNavigation.hasOwnProperty('state') || alertNavigation.hasOwnProperty('path')
|
||||
: NO_NAVIGATION;
|
||||
}
|
||||
|
||||
function getNavigationHandler(
|
||||
alertNavigation: AlertNavigationLoadingState,
|
||||
alert: Alert,
|
||||
navigateToApp: CoreStart['application']['navigateToApp']
|
||||
): object {
|
||||
return hasNavigation(alertNavigation)
|
||||
? {
|
||||
onClick: () => {
|
||||
navigateToApp(alert.consumer, alertNavigation);
|
||||
},
|
||||
}
|
||||
: {};
|
||||
}
|
|
@ -15,6 +15,7 @@ import { ValidationResult } from '../../../../types';
|
|||
import { AppContextProvider } from '../../../app_context';
|
||||
import { chartPluginMock } from '../../../../../../../../src/plugins/charts/public/mocks';
|
||||
import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks';
|
||||
import { alertingPluginMock } from '../../../../../../alerting/public/mocks';
|
||||
|
||||
jest.mock('../../../lib/action_connector_api', () => ({
|
||||
loadActionTypes: jest.fn(),
|
||||
|
@ -83,7 +84,7 @@ describe('alerts_list component empty', () => {
|
|||
{
|
||||
chrome,
|
||||
docLinks,
|
||||
application: { capabilities },
|
||||
application: { capabilities, navigateToApp },
|
||||
},
|
||||
] = await mockes.getStartServices();
|
||||
const deps = {
|
||||
|
@ -91,9 +92,11 @@ describe('alerts_list component empty', () => {
|
|||
docLinks,
|
||||
dataPlugin: dataPluginMock.createStartContract(),
|
||||
charts: chartPluginMock.createStartContract(),
|
||||
alerting: alertingPluginMock.createStartContract(),
|
||||
toastNotifications: mockes.notifications.toasts,
|
||||
http: mockes.http,
|
||||
uiSettings: mockes.uiSettings,
|
||||
navigateToApp,
|
||||
capabilities: {
|
||||
...capabilities,
|
||||
siem: {
|
||||
|
@ -204,7 +207,7 @@ describe('alerts_list component with items', () => {
|
|||
{
|
||||
chrome,
|
||||
docLinks,
|
||||
application: { capabilities },
|
||||
application: { capabilities, navigateToApp },
|
||||
},
|
||||
] = await mockes.getStartServices();
|
||||
const deps = {
|
||||
|
@ -212,9 +215,11 @@ describe('alerts_list component with items', () => {
|
|||
docLinks,
|
||||
dataPlugin: dataPluginMock.createStartContract(),
|
||||
charts: chartPluginMock.createStartContract(),
|
||||
alerting: alertingPluginMock.createStartContract(),
|
||||
toastNotifications: mockes.notifications.toasts,
|
||||
http: mockes.http,
|
||||
uiSettings: mockes.uiSettings,
|
||||
navigateToApp,
|
||||
capabilities: {
|
||||
...capabilities,
|
||||
siem: {
|
||||
|
@ -292,7 +297,7 @@ describe('alerts_list component empty with show only capability', () => {
|
|||
{
|
||||
chrome,
|
||||
docLinks,
|
||||
application: { capabilities },
|
||||
application: { capabilities, navigateToApp },
|
||||
},
|
||||
] = await mockes.getStartServices();
|
||||
const deps = {
|
||||
|
@ -300,9 +305,11 @@ describe('alerts_list component empty with show only capability', () => {
|
|||
docLinks,
|
||||
dataPlugin: dataPluginMock.createStartContract(),
|
||||
charts: chartPluginMock.createStartContract(),
|
||||
alerting: alertingPluginMock.createStartContract(),
|
||||
toastNotifications: mockes.notifications.toasts,
|
||||
http: mockes.http,
|
||||
uiSettings: mockes.uiSettings,
|
||||
navigateToApp,
|
||||
capabilities: {
|
||||
...capabilities,
|
||||
siem: {
|
||||
|
@ -409,7 +416,7 @@ describe('alerts_list with show only capability', () => {
|
|||
{
|
||||
chrome,
|
||||
docLinks,
|
||||
application: { capabilities },
|
||||
application: { capabilities, navigateToApp },
|
||||
},
|
||||
] = await mockes.getStartServices();
|
||||
const deps = {
|
||||
|
@ -417,9 +424,11 @@ describe('alerts_list with show only capability', () => {
|
|||
docLinks,
|
||||
dataPlugin: dataPluginMock.createStartContract(),
|
||||
charts: chartPluginMock.createStartContract(),
|
||||
alerting: alertingPluginMock.createStartContract(),
|
||||
toastNotifications: mockes.notifications.toasts,
|
||||
http: mockes.http,
|
||||
uiSettings: mockes.uiSettings,
|
||||
navigateToApp,
|
||||
capabilities: {
|
||||
...capabilities,
|
||||
siem: {
|
||||
|
|
|
@ -11,7 +11,7 @@ export { AlertsContextProvider } from './application/context/alerts_context';
|
|||
export { ActionsConnectorsContextProvider } from './application/context/actions_connectors_context';
|
||||
export { AlertAdd } from './application/sections/alert_form';
|
||||
export { ActionForm } from './application/sections/action_connector_form';
|
||||
export { AlertAction, Alert } from './types';
|
||||
export { AlertAction, Alert, AlertTypeModel } from './types';
|
||||
export {
|
||||
ConnectorAddFlyout,
|
||||
ConnectorEditFlyout,
|
||||
|
|
|
@ -15,6 +15,7 @@ import { TypeRegistry } from './application/type_registry';
|
|||
import { ManagementStart } from '../../../../src/plugins/management/public';
|
||||
import { boot } from './application/boot';
|
||||
import { ChartsPluginStart } from '../../../../src/plugins/charts/public';
|
||||
import { PluginStartContract as AlertingStart } from '../../alerting/public';
|
||||
import { DataPublicPluginStart } from '../../../../src/plugins/data/public';
|
||||
|
||||
export interface TriggersAndActionsUIPublicPluginSetup {
|
||||
|
@ -31,6 +32,8 @@ interface PluginsStart {
|
|||
data: DataPublicPluginStart;
|
||||
charts: ChartsPluginStart;
|
||||
management: ManagementStart;
|
||||
alerting?: AlertingStart;
|
||||
navigateToApp: CoreStart['application']['navigateToApp'];
|
||||
}
|
||||
|
||||
export class Plugin
|
||||
|
@ -80,6 +83,7 @@ export class Plugin
|
|||
boot({
|
||||
dataPlugin: plugins.data,
|
||||
charts: plugins.charts,
|
||||
alerting: plugins.alerting,
|
||||
element: params.element,
|
||||
toastNotifications: core.notifications.toasts,
|
||||
http: core.http,
|
||||
|
@ -89,6 +93,7 @@ export class Plugin
|
|||
savedObjects: core.savedObjects.client,
|
||||
I18nContext: core.i18n.Context,
|
||||
capabilities: core.application.capabilities,
|
||||
navigateToApp: core.application.navigateToApp,
|
||||
setBreadcrumbs: params.setBreadcrumbs,
|
||||
actionTypeRegistry: this.actionTypeRegistry,
|
||||
alertTypeRegistry: this.alertTypeRegistry,
|
||||
|
|
|
@ -148,6 +148,34 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
});
|
||||
});
|
||||
|
||||
describe.skip('View In App', function() {
|
||||
const testRunUuid = uuid.v4();
|
||||
before(async () => {
|
||||
await pageObjects.common.navigateToApp('triggersActions');
|
||||
});
|
||||
|
||||
it('renders the alert details view in app button', async () => {
|
||||
const alert = await alerting.alerts.createNoOp(`test-alert-${testRunUuid}`);
|
||||
|
||||
// refresh to see alert
|
||||
await browser.refresh();
|
||||
|
||||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
||||
// Verify content
|
||||
await testSubjects.existOrFail('alertsList');
|
||||
|
||||
// click on first alert
|
||||
await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name);
|
||||
|
||||
expect(await pageObjects.alertDetailsUI.isViewInAppEnabled()).to.be(true);
|
||||
|
||||
await pageObjects.alertDetailsUI.clickViewInAppEnabled();
|
||||
|
||||
expect(await pageObjects.alertDetailsUI.getNoOpAppTitle()).to.be(`View Alert ${alert.id}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Alert Instances', function() {
|
||||
const testRunUuid = uuid.v4();
|
||||
let alert: any;
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"id": "alerting_fixture",
|
||||
"version": "1.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"configPath": ["xpack"],
|
||||
"requiredPlugins": ["alerting"],
|
||||
"server": true,
|
||||
"ui": true
|
||||
}
|
|
@ -3,5 +3,13 @@
|
|||
"version": "0.0.0",
|
||||
"kibana": {
|
||||
"version": "kibana"
|
||||
},
|
||||
"main": "target/test/functional_with_es_ssl/fixtures/plugins/alerts",
|
||||
"scripts": {
|
||||
"kbn": "node ../../../../../../scripts/kbn.js",
|
||||
"build": "rm -rf './target' && tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "3.7.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 ReactDOM from 'react-dom';
|
||||
import { BrowserRouter as Router, Route, RouteComponentProps } from 'react-router-dom';
|
||||
import { EuiPage, EuiText } from '@elastic/eui';
|
||||
import { AppMountParameters, CoreStart } from '../../../../../../../src/core/public';
|
||||
|
||||
export interface AlertingExampleComponentParams {
|
||||
basename: string;
|
||||
}
|
||||
|
||||
const AlertingExampleApp = (deps: AlertingExampleComponentParams) => {
|
||||
const { basename } = deps;
|
||||
return (
|
||||
<Router basename={basename}>
|
||||
<EuiPage>
|
||||
<Route
|
||||
path={`/alert/:id`}
|
||||
render={(props: RouteComponentProps<{ id: string }>) => {
|
||||
return (
|
||||
<EuiText data-test-subj="noop-title">
|
||||
<h2>View Alert {props.match.params.id}</h2>
|
||||
</EuiText>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</EuiPage>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
||||
export const renderApp = (
|
||||
core: CoreStart,
|
||||
deps: any,
|
||||
{ appBasePath, element }: AppMountParameters
|
||||
) => {
|
||||
ReactDOM.render(<AlertingExampleApp basename={appBasePath} {...deps} />, element);
|
||||
|
||||
return () => ReactDOM.unmountComponentAtNode(element);
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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 { AlertingFixturePlugin } from './plugin';
|
||||
|
||||
export const plugin = () => new AlertingFixturePlugin();
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 { Plugin, CoreSetup, AppMountParameters } from 'kibana/public';
|
||||
import { PluginSetupContract as AlertingSetup } from '../../../../../../plugins/alerting/public';
|
||||
import { AlertType, SanitizedAlert } from '../../../../../../plugins/alerting/common';
|
||||
|
||||
export type Setup = void;
|
||||
export type Start = void;
|
||||
|
||||
export interface AlertingExamplePublicSetupDeps {
|
||||
alerting: AlertingSetup;
|
||||
}
|
||||
|
||||
export class AlertingFixturePlugin implements Plugin<Setup, Start, AlertingExamplePublicSetupDeps> {
|
||||
public setup(core: CoreSetup, { alerting }: AlertingExamplePublicSetupDeps) {
|
||||
alerting.registerNavigation(
|
||||
'consumer-noop',
|
||||
'test.noop',
|
||||
(alert: SanitizedAlert, alertType: AlertType) => `/alert/${alert.id}`
|
||||
);
|
||||
|
||||
core.application.register({
|
||||
id: 'consumer-noop',
|
||||
title: 'No Op App',
|
||||
async mount(params: AppMountParameters) {
|
||||
const [coreStart, depsStart] = await core.getStartServices();
|
||||
const { renderApp } = await import('./application');
|
||||
return renderApp(coreStart, depsStart, params);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public start() {}
|
||||
public stop() {}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 { PluginInitializer } from 'kibana/server';
|
||||
import { AlertingFixturePlugin } from './plugin';
|
||||
|
||||
export const plugin: PluginInitializer<void, void> = () => new AlertingFixturePlugin();
|
|
@ -4,21 +4,28 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AlertType } from '../../../../../plugins/alerting/server';
|
||||
import { Plugin, CoreSetup } from 'kibana/server';
|
||||
import {
|
||||
PluginSetupContract as AlertingSetup,
|
||||
AlertType,
|
||||
} from '../../../../../../plugins/alerting/server';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function(kibana: any) {
|
||||
return new kibana.Plugin({
|
||||
require: ['alerting'],
|
||||
name: 'alerts',
|
||||
init(server: any) {
|
||||
createNoopAlertType(server.newPlatform.setup.plugins.alerting);
|
||||
createAlwaysFiringAlertType(server.newPlatform.setup.plugins.alerting);
|
||||
},
|
||||
});
|
||||
// this plugin's dependendencies
|
||||
export interface AlertingExampleDeps {
|
||||
alerting: AlertingSetup;
|
||||
}
|
||||
|
||||
function createNoopAlertType(setupContract: any) {
|
||||
export class AlertingFixturePlugin implements Plugin<void, void, AlertingExampleDeps> {
|
||||
public setup(core: CoreSetup, { alerting }: AlertingExampleDeps) {
|
||||
createNoopAlertType(alerting);
|
||||
createAlwaysFiringAlertType(alerting);
|
||||
}
|
||||
|
||||
public start() {}
|
||||
public stop() {}
|
||||
}
|
||||
|
||||
function createNoopAlertType(alerting: AlertingSetup) {
|
||||
const noopAlertType: AlertType = {
|
||||
id: 'test.noop',
|
||||
name: 'Test: Noop',
|
||||
|
@ -26,10 +33,10 @@ function createNoopAlertType(setupContract: any) {
|
|||
defaultActionGroupId: 'default',
|
||||
async executor() {},
|
||||
};
|
||||
setupContract.registerType(noopAlertType);
|
||||
alerting.registerType(noopAlertType);
|
||||
}
|
||||
|
||||
function createAlwaysFiringAlertType(setupContract: any) {
|
||||
function createAlwaysFiringAlertType(alerting: AlertingSetup) {
|
||||
// Alert types
|
||||
const alwaysFiringAlertType: any = {
|
||||
id: 'test.always-firing',
|
||||
|
@ -54,5 +61,5 @@ function createAlwaysFiringAlertType(setupContract: any) {
|
|||
};
|
||||
},
|
||||
};
|
||||
setupContract.registerType(alwaysFiringAlertType);
|
||||
alerting.registerType(alwaysFiringAlertType);
|
||||
}
|
|
@ -102,5 +102,16 @@ export function AlertDetailsPageProvider({ getService }: FtrProviderContext) {
|
|||
const nextButton = await testSubjects.find(`pagination-button-next`);
|
||||
nextButton.click();
|
||||
},
|
||||
async isViewInAppEnabled() {
|
||||
const viewInAppButton = await testSubjects.find(`alertDetails-viewInApp`);
|
||||
return (await viewInAppButton.getAttribute('disabled')) !== 'disabled';
|
||||
},
|
||||
async clickViewInAppEnabled() {
|
||||
const viewInAppButton = await testSubjects.find(`alertDetails-viewInApp`);
|
||||
return viewInAppButton.click();
|
||||
},
|
||||
async getNoOpAppTitle() {
|
||||
return await testSubjects.getVisibleText('noop-title');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -22,6 +22,31 @@ export class Alerts {
|
|||
});
|
||||
}
|
||||
|
||||
public async createNoOp(name: string) {
|
||||
this.log.debug(`creating alert ${name}`);
|
||||
|
||||
const { data: alert, status, statusText } = await this.axios.post(`/api/alert`, {
|
||||
enabled: true,
|
||||
name,
|
||||
tags: ['foo'],
|
||||
alertTypeId: 'test.noop',
|
||||
consumer: 'consumer-noop',
|
||||
schedule: { interval: '1m' },
|
||||
throttle: '1m',
|
||||
actions: [],
|
||||
params: {},
|
||||
});
|
||||
if (status !== 200) {
|
||||
throw new Error(
|
||||
`Expected status code of 200, received ${status} ${statusText}: ${util.inspect(alert)}`
|
||||
);
|
||||
}
|
||||
|
||||
this.log.debug(`created alert ${alert.id}`);
|
||||
|
||||
return alert;
|
||||
}
|
||||
|
||||
public async createAlwaysFiringWithActions(
|
||||
name: string,
|
||||
actions: Array<{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue