Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support creating alert in the chat window #999

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions opensearch_dashboards.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"visAugmenter",
"opensearchDashboardsUtils"
],
"optionalPlugins": ["assistantDashboards"],
"server": true,
"ui": true,
"supportedOSDataSourceVersions": ">=2.13.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ exports[`AddAlertingMonitor renders 1`] = `
"searchType": "graph",
"timeField": "",
"timezone": Array [],
"triggerDefinitions": undefined,
"uri": Object {
"api_type": "",
"clusters": Array [],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { httpServiceMock, notificationServiceMock, uiSettingsServiceMock} from '../../../../../../src/core/public/mocks';
import { shallow } from 'enzyme';
import { setClient, setNotifications, setUISettings} from '../../../services';
import AlertContainer from './AlertContainer';

describe('AlertContainer', () => {
const httpClient = httpServiceMock.createStartContract();
setClient(httpClient);
const notifications = notificationServiceMock.createStartContract();
setNotifications(notifications);
const uiSettings = uiSettingsServiceMock.createStartContract();
setUISettings(uiSettings)
const rawContent = 'name=Flight%20Delay%20Alert&index=opensearch_dashboards_sample_data_flights&timeField=timestamp&bucketValue=12&bucketUnitOfTime=h&filters=%5B%7B%22fieldName%22%3A%5B%7B%22label%22%3A%22FlightDelayMin%22%2C%22type%22%3A%22integer%22%7D%5D%2C%22fieldValue%22%3A0%2C%22operator%22%3A%22is_greater%22%7D%5D&aggregations=%5B%7B%22aggregationType%22%3A%22sum%22%2C%22fieldName%22%3A%22FlightDelayMin%22%7D%5D&triggers=%5B%7B%22name%22%3A%22Delayed%20Time%20Exceeds%201000%20Minutes%22%2C%22severity%22%3A2%2C%22thresholdValue%22%3A1000%2C%22thresholdEnum%22%3A%22ABOVE%22%7D%5D'
test('renders', () => {
const wrapper = shallow(<AlertContainer content={rawContent} closeFlyout={()=> null}/>);
print(wrapper.toString())
expect(wrapper).toMatchSnapshot();
});
});
238 changes: 238 additions & 0 deletions public/dependencies/component/AlertContainer/AlertContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
EuiButton, EuiCallOut,
EuiFlexGroup, EuiFlexItem, EuiSmallButtonEmpty,
EuiSpacer, EuiText, EuiTitle,
} from '@elastic/eui';
import _ from 'lodash';
import { FieldArray, Formik } from 'formik';
import {
getClient,
getNotifications,
getUISettings,
NotificationService,
} from '../../../services';
import {
getInitialValues,
getPlugins,
submit,
} from '../../../pages/CreateMonitor/containers/CreateMonitor/utils/helpers';
import DefineMonitor from '../../../pages/CreateMonitor/containers/DefineMonitor';
import { formikToMonitor } from '../../../pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor';
import MonitorDetails from '../../../pages/CreateMonitor/containers/MonitorDetails';
import ConfigureTriggers from '../../../pages/CreateTrigger/containers/ConfigureTriggers';
import { unitToLabel } from '../../../pages/CreateMonitor/components/Schedule/Frequencies/Interval';
import EnhancedAccordion from '../../../components/FeatureAnywhereContextMenu/EnhancedAccordion';
import './styles.scss';

function AlertContainer(props: {content: string, closeFlyout: () => void}) {
const isDarkMode = getUISettings().get('theme:darkMode') || false;
const setFlyout = () => null;
const httpClient = getClient();
const notifications = getNotifications();
const searchType = '';
const flyoutMode = 'olly';
const history = {
location: { pathname: '/create-monitor', search: props.content, hash: '', state: undefined },
push: () => null,
goBack: null,
};
const initialValues = useMemo(
() =>
getInitialValues({ ...history, searchType }),
[]
);
const [isLoading, setIsLoading] = useState(false);
const [isDisabled, setIsDisabled] = useState(false);
const [plugins, setPlugins] = useState([]);
const notificationService = useMemo(() => new NotificationService(httpClient), []);
const [accordionsOpen, setAccordionsOpen] = useState<{[key: string]: boolean}>({ monitorDetails: false, defineMonitor: true});

const onAccordionToggle = (key: string) => {
if (key in accordionsOpen) {
setAccordionsOpen({ ...accordionsOpen, [key]: !accordionsOpen[key]});
}
};

const onCreate = useCallback( async (values, formikBag) => {
submit({
values,
formikBag,
history,
updateMonitor: () => null,
notifications,
httpClient,
onSuccess: async ({ monitor }) => {
setIsDisabled(true)
setIsLoading(false)
props.closeFlyout()
notifications.toasts.addSuccess(`Monitor "${monitor.name}" successfully created.`);
},
});
}, [history, notifications, httpClient]);
const onSubmit = useCallback( async ({ handleSubmit, validateForm }) => {
setIsLoading(true);
const errors = await validateForm();

if (Object.keys(errors).length > 0) {
// Delay to allow errors to render
setTimeout(() => {
document
.querySelector('.alert-container__scroll')
?.scrollTo({ top: 0, behavior: 'smooth' });
}, 300);
setIsLoading(false);
}

const hasErrorInMonitorDetails = 'name' in errors;
const hasErrorInDefineMonitor = 'aggregations' in errors || 'groupBy' in errors ||
'where' in errors || 'bucketValue' in errors;
if (hasErrorInMonitorDetails != accordionsOpen.monitorDetails ||
hasErrorInDefineMonitor != accordionsOpen.defineMonitor) {
setAccordionsOpen({ ...accordionsOpen,
monitorDetails: hasErrorInMonitorDetails || accordionsOpen.monitorDetails,
defineMonitor: hasErrorInDefineMonitor || accordionsOpen.defineMonitor});
}

handleSubmit();
}, [setIsLoading, accordionsOpen, setAccordionsOpen]);

useEffect(() => {
const updatePlugins = async () => {
const newPlugins = await getPlugins(httpClient);
setPlugins(newPlugins);
};

updatePlugins();
}, []);

return (
<Formik initialValues={initialValues} onSubmit={onCreate} validateOnChange={false} >
{(formikProps) => {
const {values, handleSubmit, errors, touched, validateForm} = formikProps;
const errorList = Object.entries(errors) as [string, string][];

return (
<div className="alert-container">
{errorList.length > 0 && (
<>
<EuiSpacer size="m" />
<EuiCallOut title="Address the following errors:" color="danger" iconType="alert">
<ul>
{
errorList.map(([k, v]) => (<li key={k}>{v}</li>))
}
</ul>
</EuiCallOut>
</>
)}
<EuiSpacer size="m" />
<div className="alert-container__scroll">
<EnhancedAccordion
{...{
id: 'monitorDetails',
isOpen: accordionsOpen.monitorDetails,
onToggle: () => onAccordionToggle('monitorDetails'),
title: values.name,
subTitle: (
<>
<EuiText size="m" className="create-monitor__frequency">
<p>
Runs every {values.period.interval} <span>{unitToLabel[values.period.unit]}</span>
</p>
</EuiText>
</>
),
}}
>
<MonitorDetails
values={values}
errors={errors}
history={history}
httpClient={httpClient}
plugins={plugins}
isAd={false}
flyoutMode={flyoutMode} />
</EnhancedAccordion>
<EuiSpacer size="m" />

<EnhancedAccordion
{...{
id: 'defineMonitor',
isOpen: accordionsOpen.defineMonitor,
onToggle: () => onAccordionToggle('defineMonitor'),
title: 'Advanced data source configuration',
}}
>
<DefineMonitor
values={values}
errors={errors}
touched={touched}
httpClient={httpClient}
location={location}
notifications={notifications}
isDarkMode={isDarkMode}
flyoutMode={flyoutMode}
/>
</EnhancedAccordion>
<EuiSpacer size="m" />

<EuiSpacer size="xl" />
<EuiTitle size="s">
<h3>Triggers</h3>
</EuiTitle>
<EuiSpacer size="m" />
<FieldArray name={'triggerDefinitions'} validateOnChange={true}>
{(triggerArrayHelpers) => (
<ConfigureTriggers
triggerArrayHelpers={triggerArrayHelpers}
monitor={formikToMonitor(values)}
monitorValues={values}
setFlyout={setFlyout}
triggers={_.get(formikToMonitor(values), 'triggers', [])}
triggerValues={values}
isDarkMode={isDarkMode}
httpClient={httpClient}
notifications={notifications}
notificationService={notificationService}
plugins={plugins}
flyoutMode={flyoutMode}
submitCount={formikProps.submitCount}
errors={errors}
/>
)}
</FieldArray>

<EuiSpacer size="l" />
</div>
<div style={{ position: 'relative' }}>
<EuiFlexGroup justifyContent="spaceBetween" >
<EuiFlexItem grow={false}>
<EuiSmallButtonEmpty onClick={props.closeFlyout}>Cancel</EuiSmallButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
onClick={() => onSubmit({ handleSubmit, validateForm })}
fill
isLoading={isLoading}
disabled={isDisabled}
>
{isDisabled ? 'Monitor Created' : 'Create monitor'}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</div>

</div>
);
}}
</Formik>
);
}

export default AlertContainer;
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`AlertContainer renders 1`] = `
<Formik
initialValues={
Object {
"adResultIndex": undefined,
"aggregationType": "count",
"aggregations": Array [
Object {
"aggregationType": "sum",
"fieldName": "FlightDelayMin",
},
],
"associatedMonitors": Object {
"sequence": Object {
"delegates": Array [],
},
},
"associatedMonitorsEditor": "",
"associatedMonitorsList": Array [],
"bucketUnitOfTime": "h",
"bucketValue": 12,
"clusterNames": Array [],
"cronExpression": "0 */1 * * *",
"daily": 0,
"description": "",
"detectorId": "",
"disabled": false,
"fieldName": Array [],
"filters": Array [
Object {
"fieldName": Array [
Object {
"label": "FlightDelayMin",
"type": "integer",
},
],
"fieldValue": 0,
"operator": "is_greater",
},
],
"frequency": "interval",
"groupBy": Array [],
"groupByField": Array [
Object {
"label": "",
},
],
"groupedOverFieldName": "bytes",
"groupedOverTop": 5,
"index": Array [
Object {
"label": "opensearch_dashboards_sample_data_flights",
},
],
"monitor_type": "query_level_monitor",
"monthly": Object {
"day": 1,
"type": "day",
},
"name": "Flight Delay Alert-Monitor",
"overDocuments": "all documents",
"period": Object {
"interval": 1,
"unit": "MINUTES",
},
"preventVisualEditor": false,
"queries": Array [],
"query": "{
\\"size\\": 0,
\\"query\\": {
\\"match_all\\": {}
}
}",
"searchType": "graph",
"timeField": "timestamp",
"timezone": Array [],
"triggerDefinitions": Array [
Object {
"name": "Delayed Time Exceeds 1000 Minutes",
"severity": 2,
"thresholdEnum": "ABOVE",
"thresholdValue": 1000,
},
],
"uri": Object {
"api_type": "",
"clusters": Array [],
"path": "",
"path_params": "",
"url": "",
},
"weekly": Object {
"fri": false,
"mon": false,
"sat": false,
"sun": false,
"thur": false,
"tue": false,
"wed": false,
},
}
}
onSubmit={[Function]}
validateOnChange={false}
>
<Component />
</Formik>
`;
Loading
Loading