From 1b688fe5a4e2e52acda425d3a23421d150fba17c Mon Sep 17 00:00:00 2001
From: Jovan Cvetkovic
Date: Fri, 24 Mar 2023 18:54:48 +0100
Subject: [PATCH 01/64] [FEATURE] Create global state object for async requests
#491
Signed-off-by: Jovan Cvetkovic
---
public/app.scss | 3 +-
.../containers/CreateDetector.tsx | 11 +-
.../containers/Detector/DetectorDetails.tsx | 148 +--------
public/pages/Main/Main.tsx | 14 +
public/pages/Main/components/Callout.scss | 10 +
public/pages/Main/components/Callout.tsx | 107 ++++++
public/store/DataStore.ts | 6 +-
public/store/DetectorsStore.ts | 79 -----
public/store/DetectorsStore.tsx | 312 ++++++++++++++++++
9 files changed, 470 insertions(+), 220 deletions(-)
create mode 100644 public/pages/Main/components/Callout.scss
create mode 100644 public/pages/Main/components/Callout.tsx
delete mode 100644 public/store/DetectorsStore.ts
create mode 100644 public/store/DetectorsStore.tsx
diff --git a/public/app.scss b/public/app.scss
index ffbc4e3a0..f0391769b 100644
--- a/public/app.scss
+++ b/public/app.scss
@@ -13,6 +13,7 @@ $euiTextColor: $euiColorDarkestShade !default;
@import "./components/Charts/ChartContainer.scss";
@import "./pages/Overview/components/Widgets/WidgetContainer.scss";
+@import "./pages/Main/components/Callout.scss";
.selected-radio-panel {
background-color: tintOrShade($euiColorPrimary, 90%, 70%);
@@ -119,4 +120,4 @@ $euiTextColor: $euiColorDarkestShade !default;
.sa-overview-widget-empty tbody > .euiTableRow > .euiTableRowCell {
border-bottom: none;
-}
+}
diff --git a/public/pages/CreateDetector/containers/CreateDetector.tsx b/public/pages/CreateDetector/containers/CreateDetector.tsx
index e71a2b1d5..8aaace819 100644
--- a/public/pages/CreateDetector/containers/CreateDetector.tsx
+++ b/public/pages/CreateDetector/containers/CreateDetector.tsx
@@ -125,10 +125,13 @@ export default class CreateDetector extends Component {
- return this.props.savedObjectsService
- .createSavedObject(detectorName, logType, detectorId, inputIndices)
- .catch((error: any) => {
- console.error(error);
- });
- };
-
getPendingDetector = async () => {
- const pendingState = DataStore.detectors.getPendingState();
+ const pendingState = DataStore.detectors.getState();
const detector = pendingState?.detectorState?.detector;
const pendingRequests = pendingState?.pendingRequests;
this.getTabs();
@@ -226,92 +205,30 @@ export class DetectorDetails extends React.Component,
- ServerResponse
- ];
-
- if (mappingsResponse.ok) {
- if (detectorResponse.ok) {
- let dashboardId;
- const detectorId = detectorResponse.response._id;
- if (logTypesWithDashboards.has(detector.detector_type)) {
- const dashboardResponse = await this.createDashboard(
- detector.name,
- detector.detector_type,
- detectorResponse.response._id,
- detector.inputs[0].detector_input.indices
- );
- if (dashboardResponse && dashboardResponse.ok) {
- dashboardId = dashboardResponse.response.id;
- } else {
- const dashboards = await this.props.savedObjectsService.getDashboards();
- dashboards.some((dashboard) => {
- if (
- dashboard.references.findIndex(
- (reference) => reference.id === this.state.detectorId
- ) > -1
- ) {
- dashboardId = dashboard.id;
- return true;
- }
-
- return false;
- });
- }
- }
+ const pendingResponse = await DataStore.detectors.getPendingState();
+ if (pendingResponse.ok) {
+ const { detectorId, dashboardId } = pendingResponse;
+
+ detectorId &&
this.setState(
{
detectorId,
dashboardId,
},
() => {
- DataStore.detectors.deletePendingState();
+ DataStore.detectors.deleteState();
this.props.history.push(`${ROUTES.DETECTOR_DETAILS}/${detectorId}`);
this.getDetector();
}
);
-
- successNotificationToast(
- this.props.notifications,
- 'created',
- `detector, "${detector.name}"`
- );
- } else {
- this.setState(
- {
- createFailed: true,
- },
- () =>
- errorNotificationToast(
- this.props.notifications,
- 'create',
- 'detector',
- detectorResponse.error
- )
- );
- }
- } else {
- this.setState(
- {
- createFailed: true,
- },
- () =>
- errorNotificationToast(
- this.props.notifications,
- 'create',
- 'detector',
- 'Double check the field mappings and try again.'
- )
- );
}
}
this.setState({ loading: false });
};
async componentDidMount() {
- const pendingState = DataStore.detectors.getPendingState();
+ const pendingState = DataStore.detectors.getState();
pendingState ? this.getPendingDetector() : this.getDetector();
}
@@ -512,17 +429,6 @@ export class DetectorDetails extends React.Component {
- const pendingState = DataStore.detectors.getPendingState();
- const detectorState = pendingState?.detectorState;
- this.props.history.push({
- pathname: `${ROUTES.DETECTORS_CREATE}`,
- // @ts-ignore
- state: { detectorState },
- });
- DataStore.detectors.deletePendingState();
- };
-
render() {
const { _source: detector } = this.detectorHit;
const { selectedTabContent, detectorId, createFailed } = this.state;
@@ -548,34 +454,6 @@ export class DetectorDetails extends React.Component
- {creatingDetector ? (
- <>
-
- {!createFailed && (
-
-
-
- )}
-
- {createFailed
- ? 'Detector creation failed. Please review detector configuration and try again.'
- : 'Attempting to create the detector.'}
-
-
- }
- color={createFailed ? 'danger' : 'primary'}
- >
- {createFailed && (
-
- Review detector configuration
-
- )}
-
-
- >
- ) : null}
diff --git a/public/pages/Main/Main.tsx b/public/pages/Main/Main.tsx
index 9c3cad9d0..97840b9ab 100644
--- a/public/pages/Main/Main.tsx
+++ b/public/pages/Main/Main.tsx
@@ -35,6 +35,8 @@ import { EditRule } from '../Rules/containers/EditRule/EditRule';
import { ImportRule } from '../Rules/containers/ImportRule/ImportRule';
import { DuplicateRule } from '../Rules/containers/DuplicateRule/DuplicateRule';
import { DateTimeFilter } from '../Overview/models/interfaces';
+import Callout, { ICalloutProps } from './components/Callout';
+import { DataStore } from '../../store/DataStore';
enum Navigation {
SecurityAnalytics = 'Security Analytics',
@@ -68,6 +70,7 @@ interface MainState {
getStartedDismissedOnce: boolean;
selectedNavItemIndex: number;
dateTimeFilter: DateTimeFilter;
+ callout?: ICalloutProps;
}
const navItemIndexByRoute: { [route: string]: number } = {
@@ -89,8 +92,16 @@ export default class Main extends Component {
endTime: DEFAULT_DATE_RANGE.end,
},
};
+
+ DataStore.detectors.setCalloutHandler(this.showCallout);
}
+ showCallout = (callout?: ICalloutProps) => {
+ this.setState({
+ callout,
+ });
+ };
+
componentDidMount(): void {
this.updateSelectedNavItem();
}
@@ -151,6 +162,8 @@ export default class Main extends Component {
location: { pathname },
history,
} = this.props;
+
+ const { callout } = this.state;
const sideNav: EuiSideNavItemType<{ style: any }>[] = [
{
name: Navigation.SecurityAnalytics,
@@ -230,6 +243,7 @@ export default class Main extends Component {
)}
+ {callout ? : null}
void;
+}
+
+export const CallOut = ({
+ title,
+ message,
+ type,
+ closable = true,
+ loading = false,
+ closeHandler,
+}: ICalloutProps) => {
+ const toastTypes: {
+ [Key in TCalloutColor]?: TCalloutIcon;
+ } = {
+ primary: 'iInCircle',
+ success: 'check',
+ warning: 'help',
+ danger: 'alert',
+ };
+
+ const resolveType = (type?: ICalloutType | TCalloutColor | undefined): ICalloutType => {
+ if (type === undefined) {
+ return {
+ color: 'primary',
+ iconType: 'iInCircle',
+ };
+ } else {
+ if (typeof type === 'string') {
+ return {
+ color: type,
+ iconType: toastTypes[type],
+ };
+ } else {
+ return type;
+ }
+ }
+ };
+
+ const { color, iconType } = resolveType(type);
+
+ const closeCallout = () => closeHandler && closeHandler(undefined);
+
+ const getTitle = (): JSX.Element => {
+ return (
+
+ {loading ? (
+
+
+
+ ) : null}
+ {title}
+ {closable ? (
+
+ closeCallout()} iconType="cross" aria-label="Close" />
+
+ ) : null}
+
+ );
+ };
+
+ return (
+ <>
+
+ {message ? {message}
: null}
+
+
+
+ >
+ );
+};
+
+export default CallOut;
diff --git a/public/store/DataStore.ts b/public/store/DataStore.ts
index 9a0687cfd..58a6ad538 100644
--- a/public/store/DataStore.ts
+++ b/public/store/DataStore.ts
@@ -14,6 +14,10 @@ export class DataStore {
public static init = (services: BrowserServices, notifications: NotificationsStart) => {
DataStore.rules = new RulesStore(services.ruleService, notifications);
- DataStore.detectors = new DetectorsStore(services.detectorsService, notifications);
+ DataStore.detectors = new DetectorsStore(
+ services.detectorsService,
+ notifications,
+ services.savedObjectsService
+ );
};
}
diff --git a/public/store/DetectorsStore.ts b/public/store/DetectorsStore.ts
deleted file mode 100644
index 3a39224dd..000000000
--- a/public/store/DetectorsStore.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright OpenSearch Contributors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import { DetectorsService } from '../services';
-import { NotificationsStart } from 'opensearch-dashboards/public';
-import { CreateDetectorState } from '../pages/CreateDetector/containers/CreateDetector';
-
-export interface IDetectorsStore {}
-export interface IDetectorsCache {}
-
-export interface IDetectorsState {
- pendingRequests: Promise[];
- detectorState: CreateDetectorState;
-}
-
-/**
- * Class is used to make detector's API calls and cache the detectors.
- * If there is a cache data requests are skipped and result is returned from the cache.
- * If cache is invalidated then the request is made to get a new set of data.
- *
- * @class DetectorsStore
- * @implements IDetectorsStore
- * @param {BrowserServices} services Uses services to make API requests
- */
-export class DetectorsStore implements IDetectorsStore {
- /**
- * Rule service instance
- *
- * @property {DetectorsService} service
- * @readonly
- */
- readonly service: DetectorsService;
-
- /**
- * Notifications
- * @property {NotificationsStart}
- * @readonly
- */
- readonly notifications: NotificationsStart;
-
- /**
- * Keeps detector's data cached
- *
- * @property {IDetectorsCache} cache
- */
- private cache: IDetectorsCache = {};
-
- private state: IDetectorsState | undefined;
-
- constructor(service: DetectorsService, notifications: NotificationsStart) {
- this.service = service;
- this.notifications = notifications;
- }
-
- /**
- * Invalidates all detectors data
- */
- private invalidateCache = () => {
- this.cache = {};
- return this;
- };
-
- public setPendingState = (state: IDetectorsState) => {
- this['state'] = state;
- };
-
- public getPendingState = () => {
- if (!this.state) return undefined;
- return {
- ...this.state,
- };
- };
-
- public deletePendingState = () => {
- delete this.state;
- };
-}
diff --git a/public/store/DetectorsStore.tsx b/public/store/DetectorsStore.tsx
new file mode 100644
index 000000000..580bd2e6f
--- /dev/null
+++ b/public/store/DetectorsStore.tsx
@@ -0,0 +1,312 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { DetectorsService } from '../services';
+import { NotificationsStart } from 'opensearch-dashboards/public';
+import { CreateDetectorState } from '../pages/CreateDetector/containers/CreateDetector';
+import { ICalloutProps } from '../pages/Main/components/Callout';
+import { CreateDetectorResponse, ISavedObjectsService, ServerResponse } from '../../types';
+import { CreateMappingsResponse } from '../../server/models/interfaces';
+import { logTypesWithDashboards, ROUTES } from '../utils/constants';
+import { EuiButton } from '@elastic/eui';
+import { RouteComponentProps } from 'react-router-dom';
+import { DataStore } from './DataStore';
+
+export interface IDetectorsStore {}
+export interface IDetectorsCache {}
+
+export interface IDetectorsState {
+ pendingRequests: Promise[];
+ detectorState: CreateDetectorState;
+}
+
+/**
+ * Class is used to make detector's API calls and cache the detectors.
+ * If there is a cache data requests are skipped and result is returned from the cache.
+ * If cache is invalidated then the request is made to get a new set of data.
+ *
+ * @class DetectorsStore
+ * @implements IDetectorsStore
+ * @param {BrowserServices} services Uses services to make API requests
+ */
+export class DetectorsStore implements IDetectorsStore {
+ /**
+ * Rule service instance
+ *
+ * @property {DetectorsService} service
+ * @readonly
+ */
+ readonly service: DetectorsService;
+
+ /**
+ * Notifications
+ * @property {NotificationsStart}
+ * @readonly
+ */
+ readonly notifications: NotificationsStart;
+
+ /**
+ * SavedObjectsService
+ * @property {SavedObjectsService}
+ * @readonly
+ */
+ readonly savedObjectsService: ISavedObjectsService;
+
+ /**
+ * Router history
+ * @property {RouteComponentProps['history']}
+ * @readonly
+ */
+ private history: RouteComponentProps['history'] | undefined = undefined;
+
+ /**
+ * Keeps detector's data cached
+ *
+ * @property {IDetectorsCache} cache
+ */
+ private cache: IDetectorsCache = {};
+
+ /**
+ * Store state
+ * @private {IDetectorsState}
+ */
+ private state: IDetectorsState | undefined;
+
+ constructor(
+ service: DetectorsService,
+ notifications: NotificationsStart,
+ savedObjectsService: ISavedObjectsService
+ ) {
+ this.service = service;
+ this.notifications = notifications;
+ this.savedObjectsService = savedObjectsService;
+ }
+
+ /**
+ * Invalidates all detectors data
+ */
+ private invalidateCache = () => {
+ this.cache = {};
+ return this;
+ };
+
+ public setState = async (state: IDetectorsState, history: RouteComponentProps['history']) => {
+ this.state = state;
+ this.history = history;
+
+ this.showCallout({
+ title: 'Attempting to create the detector.',
+ loading: true,
+ });
+ };
+
+ public getState = () => (this.state ? this.state : undefined);
+
+ public deleteState = () => {
+ delete this.state;
+ };
+
+ mountToaster = (component: React.ReactElement) => (container: HTMLElement) => {
+ ReactDOM.render(component, container);
+ return () => ReactDOM.unmountComponentAtNode(container);
+ };
+
+ viewDetectorConfiguration = () => {
+ const pendingState = DataStore.detectors.getState();
+ const detectorState = pendingState?.detectorState;
+ this.history?.push({
+ pathname: `${ROUTES.DETECTORS_CREATE}`,
+ // @ts-ignore
+ state: { detectorState },
+ });
+ DataStore.detectors.deleteState();
+ };
+
+ public getPendingState = async (): Promise<{
+ detectorId?: string;
+ dashboardId?: string;
+ ok: boolean;
+ }> => {
+ if (this.state?.pendingRequests) {
+ const [mappingsResponse, detectorResponse] = (await Promise.all(
+ this.state?.pendingRequests
+ )) as [ServerResponse, ServerResponse];
+
+ if (!mappingsResponse.ok) {
+ const error = {
+ title: `Create detector failed.`,
+ message: 'Double check the field mappings and try again.',
+ };
+ this.showCallout({
+ ...error,
+ closeHandler: this.showCallout,
+ });
+
+ const toast = this.notifications?.toasts.addDanger({
+ title: error.title,
+ text: this.mountToaster(
+ <>
+ {error.message}
+ {
+ this.hideCallout();
+ DataStore.detectors.viewDetectorConfiguration();
+ this.notifications?.toasts.remove(toast);
+ }}
+ size="s"
+ >
+ Review detector configuration
+
+ >
+ ),
+ toastLifeTimeMs: 30000,
+ });
+
+ return Promise.resolve({
+ ok: false,
+ error: error,
+ });
+ }
+
+ if (!detectorResponse.ok) {
+ const error = {
+ title: `Create detector failed.`,
+ message: detectorResponse.error,
+ };
+
+ this.showCallout({
+ ...error,
+ closeHandler: this.showCallout,
+ });
+
+ const toast = this.notifications?.toasts.addDanger({
+ title: error.title,
+ text: this.mountToaster(
+ <>
+ {error.message}
+ {
+ this.hideCallout();
+ DataStore.detectors.viewDetectorConfiguration();
+ this.notifications?.toasts.remove(toast);
+ }}
+ size="s"
+ >
+ Review detector configuration
+
+ >
+ ),
+ toastLifeTimeMs: 30000,
+ });
+
+ return Promise.resolve({
+ ok: false,
+ error: error,
+ });
+ }
+
+ let dashboardId;
+ const detector = detectorResponse.response.detector;
+ const detectorId = detectorResponse.response._id;
+ if (logTypesWithDashboards.has(detector.detector_type)) {
+ const dashboardResponse = await this.createDashboard(
+ detector.name,
+ detector.detector_type,
+ detectorId,
+ detector.inputs[0].detector_input.indices
+ );
+ if (dashboardResponse && dashboardResponse.ok) {
+ dashboardId = dashboardResponse.response.id;
+ } else {
+ const dashboards = await this.savedObjectsService.getDashboards();
+ dashboards.some((dashboard) => {
+ if (dashboard.references.findIndex((reference) => reference.id === detectorId) > -1) {
+ dashboardId = dashboard.id;
+ return true;
+ }
+
+ return false;
+ });
+ }
+ }
+
+ const success = {
+ title: `Detector created successfully: ${detectorResponse.response.detector.name}`,
+ };
+ this.showCallout({
+ type: 'success',
+ ...success,
+ closeHandler: this.showCallout,
+ });
+
+ const toast = this.notifications?.toasts.addSuccess({
+ title: success.title,
+ text: this.mountToaster(
+ {
+ this.hideCallout();
+ DataStore.detectors.deleteState();
+ this.history?.push(`${ROUTES.DETECTOR_DETAILS}/${detectorId}`);
+ this.notifications?.toasts.remove(toast);
+ }}
+ size="s"
+ >
+ View detector
+
+ ),
+ toastLifeTimeMs: 30000,
+ });
+
+ return Promise.resolve({
+ detectorId: detectorId,
+ dashboardId: dashboardId,
+ ok: true,
+ });
+ }
+
+ return Promise.resolve({ ok: false });
+ };
+
+ showNotification = (
+ title: string,
+ message: string,
+ loading: boolean,
+ type: string = 'success',
+ btnText: string,
+ btnHandler: () => void
+ ) => {
+ this.showCallout({
+ type,
+ title,
+ message,
+ closeHandler: this.showCallout,
+ });
+ };
+
+ private createDashboard = (
+ detectorName: string,
+ logType: string,
+ detectorId: string,
+ inputIndices: string[]
+ ) => {
+ return this.savedObjectsService
+ .createSavedObject(detectorName, logType, detectorId, inputIndices)
+ .catch((error: any) => {
+ console.error(error);
+ });
+ };
+
+ showCallout = (callout?: ICalloutProps | undefined): void => {};
+
+ hideCallout = () => this.showCallout(undefined);
+
+ public setCalloutHandler = (calloutHandler: (callout?: ICalloutProps) => void) => {
+ this.showCallout = calloutHandler;
+ };
+}
From 9daf6d9c50122f126985eab5a1e6a972c3c63287 Mon Sep 17 00:00:00 2001
From: Jovan Cvetkovic
Date: Sun, 26 Mar 2023 20:20:45 +0200
Subject: [PATCH 02/64] [FEATURE] Create global state object for async requests
#491
Signed-off-by: Jovan Cvetkovic
---
public/pages/Main/components/Callout.tsx | 14 +-
public/store/DetectorsStore.tsx | 249 ++++++++++++-----------
2 files changed, 136 insertions(+), 127 deletions(-)
diff --git a/public/pages/Main/components/Callout.tsx b/public/pages/Main/components/Callout.tsx
index 7d2426f76..6eb3da5b9 100644
--- a/public/pages/Main/components/Callout.tsx
+++ b/public/pages/Main/components/Callout.tsx
@@ -13,8 +13,8 @@ import {
EuiLoadingSpinner,
} from '@elastic/eui';
-export type TCalloutColor = 'primary' | 'success' | 'warning' | 'danger' | undefined;
-export type TCalloutIcon = 'iInCircle' | 'check' | 'help' | 'alert' | undefined;
+export type TCalloutColor = 'primary' | 'success' | 'warning' | 'danger';
+export type TCalloutIcon = 'iInCircle' | 'check' | 'help' | 'alert';
interface ICalloutType {
color: TCalloutColor;
@@ -22,10 +22,9 @@ interface ICalloutType {
}
export interface ICalloutProps {
- title: string;
+ title: string | JSX.Element;
message?: string | JSX.Element;
type?: ICalloutType | TCalloutColor;
-
closable?: boolean;
loading?: boolean;
closeHandler?: (callout?: ICalloutProps) => void;
@@ -40,7 +39,7 @@ export const CallOut = ({
closeHandler,
}: ICalloutProps) => {
const toastTypes: {
- [Key in TCalloutColor]?: TCalloutIcon;
+ [Key in TCalloutColor]: TCalloutIcon;
} = {
primary: 'iInCircle',
success: 'check',
@@ -48,7 +47,7 @@ export const CallOut = ({
danger: 'alert',
};
- const resolveType = (type?: ICalloutType | TCalloutColor | undefined): ICalloutType => {
+ const resolveType = (type?: ICalloutType | TCalloutColor): ICalloutType => {
if (type === undefined) {
return {
color: 'primary',
@@ -66,8 +65,6 @@ export const CallOut = ({
}
};
- const { color, iconType } = resolveType(type);
-
const closeCallout = () => closeHandler && closeHandler(undefined);
const getTitle = (): JSX.Element => {
@@ -88,6 +85,7 @@ export const CallOut = ({
);
};
+ const { color, iconType } = resolveType(type);
return (
<>
void;
+ getState: () => IDetectorsState | undefined;
+ deleteState: () => void;
+ getPendingState: () => Promise<{
+ detectorId?: string;
+ dashboardId?: string;
+ ok: boolean;
+ }>;
+ setCalloutHandler: (calloutHandler: (callout?: ICalloutProps) => void) => void;
+}
+
export interface IDetectorsCache {}
export interface IDetectorsState {
@@ -51,7 +66,7 @@ export class DetectorsStore implements IDetectorsStore {
/**
* SavedObjectsService
- * @property {SavedObjectsService}
+ * @property {ISavedObjectsService}
* @readonly
*/
readonly savedObjectsService: ISavedObjectsService;
@@ -61,7 +76,7 @@ export class DetectorsStore implements IDetectorsStore {
* @property {RouteComponentProps['history']}
* @readonly
*/
- private history: RouteComponentProps['history'] | undefined = undefined;
+ history: RouteComponentProps['history'] | undefined = undefined;
/**
* Keeps detector's data cached
@@ -89,38 +104,98 @@ export class DetectorsStore implements IDetectorsStore {
/**
* Invalidates all detectors data
*/
- private invalidateCache = () => {
+ private invalidateCache = (): DetectorsStore => {
this.cache = {};
return this;
};
- public setState = async (state: IDetectorsState, history: RouteComponentProps['history']) => {
+ public setState = (state: IDetectorsState, history: RouteComponentProps['history']): void => {
this.state = state;
this.history = history;
- this.showCallout({
- title: 'Attempting to create the detector.',
- loading: true,
- });
+ this.showNotification('Attempting to create the detector.', undefined, 'primary', true);
};
- public getState = () => (this.state ? this.state : undefined);
+ public getState = (): IDetectorsState | undefined => (this.state ? this.state : undefined);
- public deleteState = () => {
+ public deleteState = (): void => {
delete this.state;
};
- mountToaster = (component: React.ReactElement) => (container: HTMLElement) => {
+ private showNotification = (
+ title: string,
+ message?: string,
+ type?: TCalloutColor,
+ loading?: boolean,
+ btnText?: string,
+ btnHandler?: (e: any) => void
+ ): void => {
+ if (!type) type = 'primary';
+
+ const btn = btnText ? (
+ {
+ btnHandler && btnHandler(e);
+ this.hideCallout();
+ this.notifications?.toasts.remove(toast);
+ }}
+ size="s"
+ >
+ {btnText}
+
+ ) : null;
+
+ this.showCallout({
+ type,
+ title,
+ message: (
+ <>
+ {message}
+ {btn}
+ >
+ ),
+ closeHandler: this.showCallout,
+ });
+
+ let toastGenerator;
+ switch (type) {
+ case 'danger':
+ toastGenerator = this.notifications?.toasts.addDanger;
+ break;
+ case 'warning':
+ toastGenerator = this.notifications?.toasts.addWarning;
+ break;
+ case 'success':
+ toastGenerator = this.notifications?.toasts.addSuccess;
+ break;
+ default:
+ toastGenerator = this.notifications?.toasts.addInfo;
+ break;
+ }
+
+ const toast = toastGenerator.bind(this.notifications?.toasts)({
+ title: title,
+ text: this.mountToaster(
+ <>
+ {message ? {message}
: null}
+ {btn}
+ >
+ ),
+ toastLifeTimeMs: 5000,
+ });
+ };
+
+ private mountToaster = (component: React.ReactElement) => (container: HTMLElement) => {
ReactDOM.render(component, container);
return () => ReactDOM.unmountComponentAtNode(container);
};
- viewDetectorConfiguration = () => {
+ private viewDetectorConfiguration = (): void => {
const pendingState = DataStore.detectors.getState();
const detectorState = pendingState?.detectorState;
this.history?.push({
pathname: `${ROUTES.DETECTORS_CREATE}`,
- // @ts-ignore
state: { detectorState },
});
DataStore.detectors.deleteState();
@@ -136,78 +211,41 @@ export class DetectorsStore implements IDetectorsStore {
this.state?.pendingRequests
)) as [ServerResponse, ServerResponse];
+ let title: string = `Create detector failed.`;
if (!mappingsResponse.ok) {
- const error = {
- title: `Create detector failed.`,
- message: 'Double check the field mappings and try again.',
- };
- this.showCallout({
- ...error,
- closeHandler: this.showCallout,
- });
-
- const toast = this.notifications?.toasts.addDanger({
- title: error.title,
- text: this.mountToaster(
- <>
- {error.message}
- {
- this.hideCallout();
- DataStore.detectors.viewDetectorConfiguration();
- this.notifications?.toasts.remove(toast);
- }}
- size="s"
- >
- Review detector configuration
-
- >
- ),
- toastLifeTimeMs: 30000,
- });
+ const message = 'Double check the field mappings and try again.';
+
+ this.showNotification(
+ title,
+ message,
+ 'danger',
+ false,
+ 'Review detector configuration',
+ DataStore.detectors.viewDetectorConfiguration
+ );
return Promise.resolve({
ok: false,
- error: error,
+ error: { title, message },
});
}
if (!detectorResponse.ok) {
- const error = {
- title: `Create detector failed.`,
- message: detectorResponse.error,
- };
-
- this.showCallout({
- ...error,
- closeHandler: this.showCallout,
- });
-
- const toast = this.notifications?.toasts.addDanger({
- title: error.title,
- text: this.mountToaster(
- <>
- {error.message}
- {
- this.hideCallout();
- DataStore.detectors.viewDetectorConfiguration();
- this.notifications?.toasts.remove(toast);
- }}
- size="s"
- >
- Review detector configuration
-
- >
- ),
- toastLifeTimeMs: 30000,
- });
+ this.showNotification(
+ title,
+ detectorResponse.error,
+ 'danger',
+ false,
+ 'Review detector configuration',
+ DataStore.detectors.viewDetectorConfiguration
+ );
return Promise.resolve({
ok: false,
- error: error,
+ error: {
+ title,
+ message: detectorResponse.error,
+ },
});
}
@@ -236,32 +274,21 @@ export class DetectorsStore implements IDetectorsStore {
}
}
- const success = {
- title: `Detector created successfully: ${detectorResponse.response.detector.name}`,
+ const goToDetectorDetails = (e: any) => {
+ e.preventDefault();
+ DataStore.detectors.deleteState();
+ this.history?.push(`${ROUTES.DETECTOR_DETAILS}/${detectorId}`);
};
- this.showCallout({
- type: 'success',
- ...success,
- closeHandler: this.showCallout,
- });
- const toast = this.notifications?.toasts.addSuccess({
- title: success.title,
- text: this.mountToaster(
- {
- this.hideCallout();
- DataStore.detectors.deleteState();
- this.history?.push(`${ROUTES.DETECTOR_DETAILS}/${detectorId}`);
- this.notifications?.toasts.remove(toast);
- }}
- size="s"
- >
- View detector
-
- ),
- toastLifeTimeMs: 30000,
- });
+ title = `Detector created successfully: ${detectorResponse.response.detector.name}`;
+ this.showNotification(
+ title,
+ undefined,
+ 'success',
+ false,
+ 'View detector',
+ goToDetectorDetails
+ );
return Promise.resolve({
detectorId: detectorId,
@@ -273,22 +300,6 @@ export class DetectorsStore implements IDetectorsStore {
return Promise.resolve({ ok: false });
};
- showNotification = (
- title: string,
- message: string,
- loading: boolean,
- type: string = 'success',
- btnText: string,
- btnHandler: () => void
- ) => {
- this.showCallout({
- type,
- title,
- message,
- closeHandler: this.showCallout,
- });
- };
-
private createDashboard = (
detectorName: string,
logType: string,
@@ -302,11 +313,11 @@ export class DetectorsStore implements IDetectorsStore {
});
};
- showCallout = (callout?: ICalloutProps | undefined): void => {};
+ private showCallout = (callout?: ICalloutProps | undefined): void => {};
- hideCallout = () => this.showCallout(undefined);
+ private hideCallout = (): void => this.showCallout(undefined);
- public setCalloutHandler = (calloutHandler: (callout?: ICalloutProps) => void) => {
+ public setCalloutHandler = (calloutHandler: (callout?: ICalloutProps) => void): void => {
this.showCallout = calloutHandler;
};
}
From bb04a080bdd02c582a21f233f070fc00960d3b14 Mon Sep 17 00:00:00 2001
From: Jovan Cvetkovic
Date: Sun, 26 Mar 2023 21:13:07 +0200
Subject: [PATCH 03/64] [FEATURE] Create global state object for async requests
#491
Signed-off-by: Jovan Cvetkovic
---
.../Alerts/__snapshots__/Alerts.test.tsx.snap | 7 ++
.../DetectorRulesView.test.tsx.snap | 3 +
.../UpdateAlertConditions.test.tsx.snap | 6 +
.../UpdateDetectorBasicDetails.test.tsx.snap | 7 ++
.../AlertTriggersView.test.tsx.snap | 3 +
.../DetectorDetails.test.tsx.snap | 16 +++
.../DetectorDetailsView.test.tsx.snap | 9 ++
.../__snapshots__/Detectors.test.tsx.snap | 3 +
public/store/DetectorsStore.test.ts | 103 ++++++++++++++++++
public/store/DetectorsStore.tsx | 1 +
test/mocks/services/browserHistory.mock.ts | 1 +
.../notifications/NotificationsStart.mock.ts | 3 +
12 files changed, 162 insertions(+)
create mode 100644 public/store/DetectorsStore.test.ts
diff --git a/public/pages/Alerts/containers/Alerts/__snapshots__/Alerts.test.tsx.snap b/public/pages/Alerts/containers/Alerts/__snapshots__/Alerts.test.tsx.snap
index 050feb771..5f531740b 100644
--- a/public/pages/Alerts/containers/Alerts/__snapshots__/Alerts.test.tsx.snap
+++ b/public/pages/Alerts/containers/Alerts/__snapshots__/Alerts.test.tsx.snap
@@ -49,6 +49,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
@@ -138,6 +141,7 @@ exports[` spec renders the component 1`] = `
"location": Object {
"pathname": "",
},
+ "push": [MockFunction],
"replace": [MockFunction],
}
}
@@ -158,6 +162,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
diff --git a/public/pages/Detectors/components/DetectorRulesView/__snapshots__/DetectorRulesView.test.tsx.snap b/public/pages/Detectors/components/DetectorRulesView/__snapshots__/DetectorRulesView.test.tsx.snap
index 6598f2bc1..6216984cc 100644
--- a/public/pages/Detectors/components/DetectorRulesView/__snapshots__/DetectorRulesView.test.tsx.snap
+++ b/public/pages/Detectors/components/DetectorRulesView/__snapshots__/DetectorRulesView.test.tsx.snap
@@ -183,6 +183,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
diff --git a/public/pages/Detectors/components/UpdateAlertConditions/__snapshots__/UpdateAlertConditions.test.tsx.snap b/public/pages/Detectors/components/UpdateAlertConditions/__snapshots__/UpdateAlertConditions.test.tsx.snap
index 5c1981715..735f7d5f9 100644
--- a/public/pages/Detectors/components/UpdateAlertConditions/__snapshots__/UpdateAlertConditions.test.tsx.snap
+++ b/public/pages/Detectors/components/UpdateAlertConditions/__snapshots__/UpdateAlertConditions.test.tsx.snap
@@ -392,6 +392,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
@@ -999,6 +1002,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
diff --git a/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap b/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap
index a787a5377..b020ea5a1 100644
--- a/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap
+++ b/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap
@@ -190,6 +190,7 @@ exports[` spec renders the component 1`] = `
"location": Object {
"pathname": "",
},
+ "push": [MockFunction],
"replace": [MockFunction] {
"calls": Array [
Array [
@@ -576,6 +577,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
@@ -904,6 +908,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
diff --git a/public/pages/Detectors/containers/AlertTriggersView/__snapshots__/AlertTriggersView.test.tsx.snap b/public/pages/Detectors/containers/AlertTriggersView/__snapshots__/AlertTriggersView.test.tsx.snap
index 4b134ef5d..2083bd05c 100644
--- a/public/pages/Detectors/containers/AlertTriggersView/__snapshots__/AlertTriggersView.test.tsx.snap
+++ b/public/pages/Detectors/containers/AlertTriggersView/__snapshots__/AlertTriggersView.test.tsx.snap
@@ -183,6 +183,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
diff --git a/public/pages/Detectors/containers/Detector/__snapshots__/DetectorDetails.test.tsx.snap b/public/pages/Detectors/containers/Detector/__snapshots__/DetectorDetails.test.tsx.snap
index e492bc285..45b6924a6 100644
--- a/public/pages/Detectors/containers/Detector/__snapshots__/DetectorDetails.test.tsx.snap
+++ b/public/pages/Detectors/containers/Detector/__snapshots__/DetectorDetails.test.tsx.snap
@@ -195,6 +195,7 @@ exports[` spec renders the component 1`] = `
"location": Object {
"pathname": "",
},
+ "push": [MockFunction],
"replace": [MockFunction],
}
}
@@ -207,6 +208,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
@@ -887,6 +891,7 @@ exports[` spec renders the component 1`] = `
"location": Object {
"pathname": "",
},
+ "push": [MockFunction],
"replace": [MockFunction],
}
}
@@ -901,6 +906,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
@@ -1293,6 +1301,7 @@ exports[` spec renders the component 1`] = `
"location": Object {
"pathname": "",
},
+ "push": [MockFunction],
"replace": [MockFunction],
}
}
@@ -1307,6 +1316,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
@@ -2648,6 +2660,7 @@ exports[` spec renders the component 1`] = `
"location": Object {
"pathname": "",
},
+ "push": [MockFunction],
"replace": [MockFunction],
}
}
@@ -2662,6 +2675,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
diff --git a/public/pages/Detectors/containers/DetectorDetailsView/__snapshots__/DetectorDetailsView.test.tsx.snap b/public/pages/Detectors/containers/DetectorDetailsView/__snapshots__/DetectorDetailsView.test.tsx.snap
index 3f9cac30e..d59f77923 100644
--- a/public/pages/Detectors/containers/DetectorDetailsView/__snapshots__/DetectorDetailsView.test.tsx.snap
+++ b/public/pages/Detectors/containers/DetectorDetailsView/__snapshots__/DetectorDetailsView.test.tsx.snap
@@ -186,6 +186,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
@@ -377,6 +380,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
@@ -1517,6 +1523,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
diff --git a/public/pages/Detectors/containers/Detectors/__snapshots__/Detectors.test.tsx.snap b/public/pages/Detectors/containers/Detectors/__snapshots__/Detectors.test.tsx.snap
index 7cf9f7c8f..0a66bbe88 100644
--- a/public/pages/Detectors/containers/Detectors/__snapshots__/Detectors.test.tsx.snap
+++ b/public/pages/Detectors/containers/Detectors/__snapshots__/Detectors.test.tsx.snap
@@ -23,6 +23,9 @@ exports[` spec renders the component 1`] = `
Object {
"toasts": Object {
"addDanger": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
},
}
}
diff --git a/public/store/DetectorsStore.test.ts b/public/store/DetectorsStore.test.ts
new file mode 100644
index 000000000..09fc82794
--- /dev/null
+++ b/public/store/DetectorsStore.test.ts
@@ -0,0 +1,103 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { DataStore } from './DataStore';
+import notificationsStartMock from '../../test/mocks/services/notifications/NotificationsStart.mock';
+import services from '../../test/mocks/services';
+import { DetectorsStore } from './DetectorsStore';
+import { expect } from '@jest/globals';
+import detectorResponseMock from '../../test/mocks/Detectors/containers/Detectors/DetectorResponse.mock';
+import browserHistoryMock from '../../test/mocks/services/browserHistory.mock';
+import { CreateDetectorState } from '../pages/CreateDetector/containers/CreateDetector';
+import DetectorMock from '../../test/mocks/Detectors/containers/Detectors/Detector.mock';
+describe('Detectors store specs', () => {
+ Object.assign(services, {
+ detectorService: {
+ getRules: () => Promise.resolve(detectorResponseMock),
+ deleteRule: () => Promise.resolve(true),
+ },
+ });
+
+ DataStore.init(services, notificationsStartMock);
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('detectors store should be created', () => {
+ expect(DataStore.detectors instanceof DetectorsStore).toBe(true);
+ });
+
+ it('should handle the state', () => {
+ DataStore.detectors.setState(
+ {
+ pendingRequests: [Promise.resolve()],
+ detectorState: {
+ detector: { detector_type: 'test_detector_type' } as typeof DetectorMock,
+ } as CreateDetectorState,
+ },
+ browserHistoryMock
+ );
+
+ let state = DataStore.detectors.getState();
+ expect(state?.detectorState?.detector.detector_type).toBe('test_detector_type');
+
+ DataStore.detectors.deleteState();
+ state = DataStore.detectors.getState();
+ expect(state).toBe(undefined);
+ });
+
+ it('should get successful pending state', async () => {
+ DataStore.detectors.setState(
+ {
+ pendingRequests: [
+ Promise.resolve({
+ ok: true,
+ }),
+ Promise.resolve({
+ ok: true,
+ response: {
+ _id: '',
+ detector: {
+ detector_type: '',
+ inputs: [
+ {
+ detector_input: {
+ indices: [],
+ },
+ },
+ ],
+ },
+ },
+ }),
+ ],
+ detectorState: {
+ detector: { detector_type: 'test_detector_type' } as typeof DetectorMock,
+ } as CreateDetectorState,
+ },
+ browserHistoryMock
+ );
+ const pending = await DataStore.detectors.getPendingState();
+ expect(pending.ok).toBe(true);
+ });
+
+ it('should get failed pending state', async () => {
+ DataStore.detectors.setState(
+ {
+ pendingRequests: [
+ Promise.resolve({
+ ok: false,
+ }),
+ ],
+ detectorState: {
+ detector: { detector_type: 'test_detector_type' } as typeof DetectorMock,
+ } as CreateDetectorState,
+ },
+ browserHistoryMock
+ );
+ const pending = await DataStore.detectors.getPendingState();
+ expect(pending.ok).toBe(false);
+ });
+});
diff --git a/public/store/DetectorsStore.tsx b/public/store/DetectorsStore.tsx
index 829318de6..ef0ba8cdb 100644
--- a/public/store/DetectorsStore.tsx
+++ b/public/store/DetectorsStore.tsx
@@ -206,6 +206,7 @@ export class DetectorsStore implements IDetectorsStore {
dashboardId?: string;
ok: boolean;
}> => {
+ debugger;
if (this.state?.pendingRequests) {
const [mappingsResponse, detectorResponse] = (await Promise.all(
this.state?.pendingRequests
diff --git a/test/mocks/services/browserHistory.mock.ts b/test/mocks/services/browserHistory.mock.ts
index a40a1a2ff..9af44b50c 100644
--- a/test/mocks/services/browserHistory.mock.ts
+++ b/test/mocks/services/browserHistory.mock.ts
@@ -9,4 +9,5 @@ export default ({
location: {
pathname: '',
},
+ push: jest.fn(),
} as unknown) as History;
diff --git a/test/mocks/services/notifications/NotificationsStart.mock.ts b/test/mocks/services/notifications/NotificationsStart.mock.ts
index 73f62608c..f1afac33f 100644
--- a/test/mocks/services/notifications/NotificationsStart.mock.ts
+++ b/test/mocks/services/notifications/NotificationsStart.mock.ts
@@ -8,5 +8,8 @@ import { NotificationsStart } from 'opensearch-dashboards/public';
export default ({
toasts: {
addDanger: jest.fn(),
+ addWarning: jest.fn(),
+ addSuccess: jest.fn(),
+ addInfo: jest.fn(),
},
} as unknown) as NotificationsStart;
From 0d7362e5bf3153ebc2a2e5f0c77cab3ee8c05f70 Mon Sep 17 00:00:00 2001
From: Jovan Cvetkovic
Date: Sun, 26 Mar 2023 22:34:47 +0200
Subject: [PATCH 04/64] [FEATURE] Create global state object for async requests
#491
Signed-off-by: Jovan Cvetkovic
---
cypress/integration/1_detectors.spec.js | 270 +++++++++++++-----------
1 file changed, 145 insertions(+), 125 deletions(-)
diff --git a/cypress/integration/1_detectors.spec.js b/cypress/integration/1_detectors.spec.js
index 092006e45..1514deab8 100644
--- a/cypress/integration/1_detectors.spec.js
+++ b/cypress/integration/1_detectors.spec.js
@@ -18,6 +18,145 @@ const testMappings = {
const cypressDNSRule = dns_rule_data.title;
+const createDetector = (detectorName, dataSource, expectFailure) => {
+ // Locate Create detector button click to start
+ cy.get('.euiButton').filter(':contains("Create detector")').click({ force: true });
+
+ // Check to ensure process started
+ cy.waitForPageLoad('create-detector', {
+ contains: 'Define detector',
+ });
+
+ // Enter a name for the detector in the appropriate input
+ cy.get(`input[placeholder="Enter a name for the detector."]`).focus().realType(detectorName);
+
+ // Select our pre-seeded data source (check cypressIndexDns)
+ cy.get(`[data-test-subj="define-detector-select-data-source"]`)
+ .find('input')
+ .focus()
+ .realType(dataSource);
+
+ cy.intercept({
+ pathname: '/_plugins/_security_analytics/rules/_search',
+ query: {
+ prePackaged: 'true',
+ },
+ }).as('getSigmaRules');
+
+ // Select threat detector type (Windows logs)
+ cy.get(`input[id="dns"]`).click({ force: true });
+
+ cy.wait('@getSigmaRules').then(() => {
+ // Open Detection rules accordion
+ cy.get('[data-test-subj="detection-rules-btn"]').click({ force: true, timeout: 5000 });
+
+ cy.contains('table tr', 'DNS', {
+ timeout: 120000,
+ });
+
+ // find search, type USB
+ cy.get(`input[placeholder="Search..."]`).ospSearch(cypressDNSRule);
+
+ // Disable all rules
+ cy.contains('tr', cypressDNSRule, { timeout: 1000 });
+ cy.get('table th').within(() => {
+ cy.get('button').first().click({ force: true });
+ });
+
+ // Enable single rule
+ cy.contains('table tr', cypressDNSRule).within(() => {
+ cy.get('button').eq(1).click({ force: true, timeout: 2000 });
+ });
+ });
+
+ // Click Next button to continue
+ cy.get('button').contains('Next').click({ force: true });
+
+ // Check that correct page now showing
+ cy.contains('Configure field mapping');
+
+ if (!expectFailure) {
+ // Select appropriate names to map fields to
+ for (let field_name in testMappings.properties) {
+ const mappedTo = testMappings.properties[field_name].path;
+
+ cy.contains('tr', field_name).within(() => {
+ cy.get(`[data-test-subj="detector-field-mappings-select"]`).click().type(mappedTo);
+ });
+ }
+ }
+
+ // Continue to next page
+ cy.get('button').contains('Next').click({ force: true, timeout: 2000 });
+
+ // Check that correct page now showing
+ cy.contains('Set up alerts');
+
+ // Type name of new trigger
+ cy.get(`input[placeholder="Enter a name for the alert condition."]`)
+ .focus()
+ .realType('test_trigger');
+
+ // Type in (or select) tags for the alert condition
+ cy.get(`[data-test-subj="alert-tags-combo-box"]`)
+ .find('input')
+ .focus()
+ .realType('attack.defense_evasion')
+ .realPress('Enter');
+
+ // Select applicable severity levels
+ cy.get(`[data-test-subj="security-levels-combo-box"]`).click({ force: true });
+ cy.contains('1 (Highest)').click({ force: true });
+
+ // Continue to next page
+ cy.contains('Next').click({ force: true });
+
+ // Confirm page is reached
+ cy.contains('Review and create');
+
+ // Confirm field mappings registered
+ cy.contains('Field mapping');
+
+ if (!expectFailure) {
+ for (let field in testMappings.properties) {
+ const mappedTo = testMappings.properties[field].path;
+
+ cy.contains(field);
+ cy.contains(mappedTo);
+ }
+ }
+
+ // Confirm entries user has made
+ cy.contains('Detector details');
+ cy.contains(detectorName);
+ cy.contains('dns');
+ cy.contains(dataSource);
+ cy.contains('Alert on test_trigger');
+
+ // Create the detector
+ cy.get('button').contains('Create').click({ force: true });
+ cy.waitForPageLoad('detector-details', {
+ contains: detectorName,
+ });
+
+ cy.contains('Attempting to create the detector.');
+
+ // Confirm detector active
+ cy.contains(detectorName);
+ cy.contains('Active');
+
+ if (!expectFailure) {
+ cy.contains('Actions');
+ }
+
+ cy.contains('Detector configuration');
+ cy.contains('Field mappings');
+ cy.contains('Alert triggers');
+ cy.contains('Detector details');
+ cy.contains('Created at');
+ cy.contains('Last updated time');
+};
+
describe('Detectors', () => {
const cypressIndexDns = 'cypress-index-dns';
const cypressIndexWindows = 'cypress-index-windows';
@@ -90,132 +229,13 @@ describe('Detectors', () => {
});
it('...can be created', () => {
- // Locate Create detector button click to start
- cy.get('.euiButton').filter(':contains("Create detector")').click({ force: true });
-
- // Check to ensure process started
- cy.waitForPageLoad('create-detector', {
- contains: 'Define detector',
- });
-
- // Enter a name for the detector in the appropriate input
- cy.get(`input[placeholder="Enter a name for the detector."]`).focus().realType('test detector');
-
- // Select our pre-seeded data source (check cypressIndexDns)
- cy.get(`[data-test-subj="define-detector-select-data-source"]`)
- .find('input')
- .focus()
- .realType(cypressIndexDns);
-
- cy.intercept({
- pathname: '/_plugins/_security_analytics/rules/_search',
- query: {
- prePackaged: 'true',
- },
- }).as('getSigmaRules');
-
- // Select threat detector type (Windows logs)
- cy.get(`input[id="dns"]`).click({ force: true });
-
- cy.wait('@getSigmaRules').then(() => {
- // Open Detection rules accordion
- cy.get('[data-test-subj="detection-rules-btn"]').click({ force: true, timeout: 5000 });
-
- cy.contains('table tr', 'DNS', {
- timeout: 120000,
- });
-
- // find search, type USB
- cy.get(`input[placeholder="Search..."]`).ospSearch(cypressDNSRule);
-
- // Disable all rules
- cy.contains('tr', cypressDNSRule, { timeout: 1000 });
- cy.get('table th').within(() => {
- cy.get('button').first().click({ force: true });
- });
-
- // Enable single rule
- cy.contains('table tr', cypressDNSRule).within(() => {
- cy.get('button').eq(1).click({ force: true, timeout: 2000 });
- });
- });
-
- // Click Next button to continue
- cy.get('button').contains('Next').click({ force: true });
-
- // Check that correct page now showing
- cy.contains('Configure field mapping');
-
- // Select appropriate names to map fields to
- for (let field_name in testMappings.properties) {
- const mappedTo = testMappings.properties[field_name].path;
-
- cy.contains('tr', field_name).within(() => {
- cy.get(`[data-test-subj="detector-field-mappings-select"]`).click().type(mappedTo);
- });
- }
-
- // Continue to next page
- cy.get('button').contains('Next').click({ force: true, timeout: 2000 });
-
- // Check that correct page now showing
- cy.contains('Set up alerts');
-
- // Type name of new trigger
- cy.get(`input[placeholder="Enter a name for the alert condition."]`)
- .focus()
- .realType('test_trigger');
-
- // Type in (or select) tags for the alert condition
- cy.get(`[data-test-subj="alert-tags-combo-box"]`)
- .find('input')
- .focus()
- .realType('attack.defense_evasion')
- .realPress('Enter');
-
- // Select applicable severity levels
- cy.get(`[data-test-subj="security-levels-combo-box"]`).click({ force: true });
- cy.contains('1 (Highest)').click({ force: true });
-
- // Continue to next page
- cy.contains('Next').click({ force: true });
-
- // Confirm page is reached
- cy.contains('Review and create');
-
- // Confirm field mappings registered
- cy.contains('Field mapping');
-
- for (let field in testMappings.properties) {
- const mappedTo = testMappings.properties[field].path;
-
- cy.contains(field);
- cy.contains(mappedTo);
- }
-
- // Confirm entries user has made
- cy.contains('Detector details');
- cy.contains(detectorName);
- cy.contains('dns');
- cy.contains(cypressIndexDns);
- cy.contains('Alert on test_trigger');
-
- // Create the detector
- cy.get('button').contains('Create').click({ force: true });
- cy.waitForPageLoad('detector-details', {
- contains: detectorName,
- });
+ createDetector(detectorName, cypressIndexDns, false);
+ cy.contains('Detector created successfully');
+ });
- // Confirm detector active
- cy.contains(detectorName);
- cy.contains('Active');
- cy.contains('Actions');
- cy.contains('Detector configuration');
- cy.contains('Field mappings');
- cy.contains('Alert triggers');
- cy.contains('Detector details');
- cy.contains('Created at');
- cy.contains('Last updated time');
+ it('...can fail creation', () => {
+ createDetector(`${detectorName}_fail`, '.kibana_1', true);
+ cy.contains('Create detector failed.');
});
it('...basic details can be edited', () => {
From b952bb3e31f3917ca684c7b2f6a62356195d7e0b Mon Sep 17 00:00:00 2001
From: Jovan Cvetkovic
Date: Sun, 26 Mar 2023 22:38:12 +0200
Subject: [PATCH 05/64] [FEATURE] Create global state object for async requests
#491
Signed-off-by: Jovan Cvetkovic
---
public/store/DetectorsStore.test.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/public/store/DetectorsStore.test.ts b/public/store/DetectorsStore.test.ts
index 09fc82794..dc3896e41 100644
--- a/public/store/DetectorsStore.test.ts
+++ b/public/store/DetectorsStore.test.ts
@@ -12,6 +12,7 @@ import detectorResponseMock from '../../test/mocks/Detectors/containers/Detector
import browserHistoryMock from '../../test/mocks/services/browserHistory.mock';
import { CreateDetectorState } from '../pages/CreateDetector/containers/CreateDetector';
import DetectorMock from '../../test/mocks/Detectors/containers/Detectors/Detector.mock';
+
describe('Detectors store specs', () => {
Object.assign(services, {
detectorService: {
From 3d2a1f374da4cfba285333f05e37d4ebe99ac2bd Mon Sep 17 00:00:00 2001
From: Jovan Cvetkovic
Date: Sun, 26 Mar 2023 22:44:11 +0200
Subject: [PATCH 06/64] [FEATURE] Create global state object for async requests
#491
Signed-off-by: Jovan Cvetkovic
---
public/store/DetectorsStore.tsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/public/store/DetectorsStore.tsx b/public/store/DetectorsStore.tsx
index ef0ba8cdb..829318de6 100644
--- a/public/store/DetectorsStore.tsx
+++ b/public/store/DetectorsStore.tsx
@@ -206,7 +206,6 @@ export class DetectorsStore implements IDetectorsStore {
dashboardId?: string;
ok: boolean;
}> => {
- debugger;
if (this.state?.pendingRequests) {
const [mappingsResponse, detectorResponse] = (await Promise.all(
this.state?.pendingRequests
From 4e0b834624a1385f5e1154d3f7675ffd57b40b74 Mon Sep 17 00:00:00 2001
From: Jovan Cvetkovic
Date: Sun, 26 Mar 2023 23:13:48 +0200
Subject: [PATCH 07/64] [FEATURE] Create global state object for async requests
#491
Signed-off-by: Jovan Cvetkovic
---
public/store/DetectorsStore.tsx | 23 ++++++++++-------------
1 file changed, 10 insertions(+), 13 deletions(-)
diff --git a/public/store/DetectorsStore.tsx b/public/store/DetectorsStore.tsx
index 829318de6..daca1a244 100644
--- a/public/store/DetectorsStore.tsx
+++ b/public/store/DetectorsStore.tsx
@@ -12,7 +12,7 @@ import { ICalloutProps, TCalloutColor } from '../pages/Main/components/Callout';
import { CreateDetectorResponse, ISavedObjectsService, ServerResponse } from '../../types';
import { CreateMappingsResponse } from '../../server/models/interfaces';
import { logTypesWithDashboards, ROUTES } from '../utils/constants';
-import { EuiButton } from '@elastic/eui';
+import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { RouteComponentProps } from 'react-router-dom';
import { DataStore } from './DataStore';
@@ -146,15 +146,17 @@ export class DetectorsStore implements IDetectorsStore {
) : null;
+ const messageBody = (
+
+ {message ? {message}
: null}
+ {btn}
+
+ );
+
this.showCallout({
type,
title,
- message: (
- <>
- {message}
- {btn}
- >
- ),
+ message: messageBody,
closeHandler: this.showCallout,
});
@@ -176,12 +178,7 @@ export class DetectorsStore implements IDetectorsStore {
const toast = toastGenerator.bind(this.notifications?.toasts)({
title: title,
- text: this.mountToaster(
- <>
- {message ? {message}
: null}
- {btn}
- >
- ),
+ text: this.mountToaster(messageBody),
toastLifeTimeMs: 5000,
});
};
From ee8afd9252b07060cb698792f2ee3bdbdfa757aa Mon Sep 17 00:00:00 2001
From: Jovan Cvetkovic
Date: Mon, 27 Mar 2023 09:11:32 +0200
Subject: [PATCH 08/64] [FEATURE] Create global state object for async requests
#491
Signed-off-by: Jovan Cvetkovic
---
public/pages/Main/components/Callout.tsx | 10 +++----
public/store/DetectorsStore.tsx | 35 +++++++++++++++++-------
2 files changed, 30 insertions(+), 15 deletions(-)
diff --git a/public/pages/Main/components/Callout.tsx b/public/pages/Main/components/Callout.tsx
index 6eb3da5b9..1213dbd0b 100644
--- a/public/pages/Main/components/Callout.tsx
+++ b/public/pages/Main/components/Callout.tsx
@@ -70,17 +70,17 @@ export const CallOut = ({
const getTitle = (): JSX.Element => {
return (
- {loading ? (
+ {loading && (
- ) : null}
+ )}
{title}
- {closable ? (
+ {closable && (
closeCallout()} iconType="cross" aria-label="Close" />
- ) : null}
+ )}
);
};
@@ -94,7 +94,7 @@ export const CallOut = ({
iconType={!loading ? iconType : undefined}
className={'mainCallout'}
>
- {message ? {message}
: null}
+ {message}
diff --git a/public/store/DetectorsStore.tsx b/public/store/DetectorsStore.tsx
index daca1a244..3f1aad94b 100644
--- a/public/store/DetectorsStore.tsx
+++ b/public/store/DetectorsStore.tsx
@@ -91,6 +91,12 @@ export class DetectorsStore implements IDetectorsStore {
*/
private state: IDetectorsState | undefined;
+ /**
+ * List of all shown toasts
+ * @private
+ */
+ private toasts: any[] = [];
+
constructor(
service: DetectorsService,
notifications: NotificationsStart,
@@ -132,23 +138,31 @@ export class DetectorsStore implements IDetectorsStore {
): void => {
if (!type) type = 'primary';
- const btn = btnText ? (
+ const closeToasts = () => {
+ while (this.toasts.length) {
+ const toast = this.toasts.pop();
+ this.notifications?.toasts.remove(toast);
+ closeToasts();
+ }
+ };
+
+ const btn = btnText && (
{
btnHandler && btnHandler(e);
this.hideCallout();
- this.notifications?.toasts.remove(toast);
+ closeToasts();
}}
size="s"
>
{btnText}
- ) : null;
+ );
const messageBody = (
- {message ? {message}
: null}
+ {message && {message} }
{btn}
);
@@ -157,7 +171,7 @@ export class DetectorsStore implements IDetectorsStore {
type,
title,
message: messageBody,
- closeHandler: this.showCallout,
+ closeHandler: this.hideCallout,
});
let toastGenerator;
@@ -176,11 +190,12 @@ export class DetectorsStore implements IDetectorsStore {
break;
}
- const toast = toastGenerator.bind(this.notifications?.toasts)({
- title: title,
- text: this.mountToaster(messageBody),
- toastLifeTimeMs: 5000,
- });
+ this.toasts.push(
+ toastGenerator.bind(this.notifications?.toasts)({
+ title: title,
+ text: this.mountToaster(messageBody),
+ })
+ );
};
private mountToaster = (component: React.ReactElement) => (container: HTMLElement) => {
From dea7ddc94c45e8399a69159a84b4658060a1b50f Mon Sep 17 00:00:00 2001
From: Jovan Cvetkovic
Date: Mon, 27 Mar 2023 11:16:47 +0200
Subject: [PATCH 09/64] [FEATURE] Create global state object for async requests
#491
Signed-off-by: Jovan Cvetkovic
---
.../containers/Detector/DetectorDetails.tsx | 2 +
public/pages/Main/Main.tsx | 16 ++++-
public/pages/Main/components/Callout.tsx | 54 +++++++-------
public/store/DetectorsStore.tsx | 72 +++++++++----------
4 files changed, 77 insertions(+), 67 deletions(-)
diff --git a/public/pages/Detectors/containers/Detector/DetectorDetails.tsx b/public/pages/Detectors/containers/Detector/DetectorDetails.tsx
index e71871703..2b6d88ca8 100644
--- a/public/pages/Detectors/containers/Detector/DetectorDetails.tsx
+++ b/public/pages/Detectors/containers/Detector/DetectorDetails.tsx
@@ -222,6 +222,8 @@ export class DetectorDetails extends React.Component {
},
};
- DataStore.detectors.setCalloutHandler(this.showCallout);
+ DataStore.detectors.setHandlers(this.showCallout, this.showToast);
}
showCallout = (callout?: ICalloutProps) => {
@@ -102,6 +105,12 @@ export default class Main extends Component {
});
};
+ showToast = (toasts?: any[]) => {
+ this.setState({
+ toasts,
+ });
+ };
+
componentDidMount(): void {
this.updateSelectedNavItem();
}
@@ -426,6 +435,11 @@ export default class Main extends Component {
+
)
}
diff --git a/public/pages/Main/components/Callout.tsx b/public/pages/Main/components/Callout.tsx
index 1213dbd0b..3255ca411 100644
--- a/public/pages/Main/components/Callout.tsx
+++ b/public/pages/Main/components/Callout.tsx
@@ -30,6 +30,33 @@ export interface ICalloutProps {
closeHandler?: (callout?: ICalloutProps) => void;
}
+export const toastTypes: {
+ [Key in TCalloutColor]: TCalloutIcon;
+} = {
+ primary: 'iInCircle',
+ success: 'check',
+ warning: 'help',
+ danger: 'alert',
+};
+
+export const resolveType = (type?: ICalloutType | TCalloutColor): ICalloutType => {
+ if (type === undefined) {
+ return {
+ color: 'primary',
+ iconType: 'iInCircle',
+ };
+ } else {
+ if (typeof type === 'string') {
+ return {
+ color: type,
+ iconType: toastTypes[type],
+ };
+ } else {
+ return type;
+ }
+ }
+};
+
export const CallOut = ({
title,
message,
@@ -38,33 +65,6 @@ export const CallOut = ({
loading = false,
closeHandler,
}: ICalloutProps) => {
- const toastTypes: {
- [Key in TCalloutColor]: TCalloutIcon;
- } = {
- primary: 'iInCircle',
- success: 'check',
- warning: 'help',
- danger: 'alert',
- };
-
- const resolveType = (type?: ICalloutType | TCalloutColor): ICalloutType => {
- if (type === undefined) {
- return {
- color: 'primary',
- iconType: 'iInCircle',
- };
- } else {
- if (typeof type === 'string') {
- return {
- color: type,
- iconType: toastTypes[type],
- };
- } else {
- return type;
- }
- }
- };
-
const closeCallout = () => closeHandler && closeHandler(undefined);
const getTitle = (): JSX.Element => {
diff --git a/public/store/DetectorsStore.tsx b/public/store/DetectorsStore.tsx
index 3f1aad94b..0ff7e7514 100644
--- a/public/store/DetectorsStore.tsx
+++ b/public/store/DetectorsStore.tsx
@@ -4,17 +4,18 @@
*/
import React from 'react';
-import ReactDOM from 'react-dom';
import { DetectorsService } from '../services';
import { NotificationsStart } from 'opensearch-dashboards/public';
import { CreateDetectorState } from '../pages/CreateDetector/containers/CreateDetector';
-import { ICalloutProps, TCalloutColor } from '../pages/Main/components/Callout';
+import { ICalloutProps, resolveType, TCalloutColor } from '../pages/Main/components/Callout';
import { CreateDetectorResponse, ISavedObjectsService, ServerResponse } from '../../types';
import { CreateMappingsResponse } from '../../server/models/interfaces';
import { logTypesWithDashboards, ROUTES } from '../utils/constants';
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { Toast } from '@opensearch-project/oui/src/eui_components/toast/global_toast_list';
import { RouteComponentProps } from 'react-router-dom';
import { DataStore } from './DataStore';
+import { v4 as uuidv4 } from 'uuid';
export interface IDetectorsStore {
readonly service: DetectorsService;
@@ -29,7 +30,10 @@ export interface IDetectorsStore {
dashboardId?: string;
ok: boolean;
}>;
- setCalloutHandler: (calloutHandler: (callout?: ICalloutProps) => void) => void;
+ setHandlers: (
+ calloutHandler: (callout?: ICalloutProps) => void,
+ toastHandler: (toasts?: Toast[]) => void
+ ) => void;
}
export interface IDetectorsCache {}
@@ -95,7 +99,7 @@ export class DetectorsStore implements IDetectorsStore {
* List of all shown toasts
* @private
*/
- private toasts: any[] = [];
+ private toasts: Toast[] = [];
constructor(
service: DetectorsService,
@@ -138,12 +142,9 @@ export class DetectorsStore implements IDetectorsStore {
): void => {
if (!type) type = 'primary';
- const closeToasts = () => {
- while (this.toasts.length) {
- const toast = this.toasts.pop();
- this.notifications?.toasts.remove(toast);
- closeToasts();
- }
+ const closeAllToasts = () => {
+ this.toasts = [];
+ this.showToast(this.toasts);
};
const btn = btnText && (
@@ -152,7 +153,7 @@ export class DetectorsStore implements IDetectorsStore {
onClick={(e: any) => {
btnHandler && btnHandler(e);
this.hideCallout();
- closeToasts();
+ closeAllToasts();
}}
size="s"
>
@@ -174,33 +175,15 @@ export class DetectorsStore implements IDetectorsStore {
closeHandler: this.hideCallout,
});
- let toastGenerator;
- switch (type) {
- case 'danger':
- toastGenerator = this.notifications?.toasts.addDanger;
- break;
- case 'warning':
- toastGenerator = this.notifications?.toasts.addWarning;
- break;
- case 'success':
- toastGenerator = this.notifications?.toasts.addSuccess;
- break;
- default:
- toastGenerator = this.notifications?.toasts.addInfo;
- break;
- }
-
- this.toasts.push(
- toastGenerator.bind(this.notifications?.toasts)({
- title: title,
- text: this.mountToaster(messageBody),
- })
- );
- };
-
- private mountToaster = (component: React.ReactElement) => (container: HTMLElement) => {
- ReactDOM.render(component, container);
- return () => ReactDOM.unmountComponentAtNode(container);
+ const { color, iconType } = resolveType(type);
+ this.toasts.push({
+ title,
+ color,
+ iconType,
+ id: `toastsKey_${uuidv4()}`,
+ text: messageBody,
+ });
+ this.showToast(this.toasts);
};
private viewDetectorConfiguration = (): void => {
@@ -329,7 +312,18 @@ export class DetectorsStore implements IDetectorsStore {
private hideCallout = (): void => this.showCallout(undefined);
- public setCalloutHandler = (calloutHandler: (callout?: ICalloutProps) => void): void => {
+ private showToast = (toasts?: Toast[] | undefined): void => {};
+
+ public hideToast = (removedToast: any): void => {
+ const toasts = this.toasts.filter((toast: Toast) => toast.id !== removedToast.id);
+ this.showToast(toasts);
+ };
+
+ public setHandlers = (
+ calloutHandler: (callout?: ICalloutProps) => void,
+ toastHandler: (toasts?: Toast[]) => void
+ ): void => {
this.showCallout = calloutHandler;
+ this.showToast = toastHandler;
};
}
From 7649e8c572e50c4858e08f8def92bdd895167c76 Mon Sep 17 00:00:00 2001
From: Jovan Cvetkovic
Date: Mon, 27 Mar 2023 11:48:40 +0200
Subject: [PATCH 10/64] [FEATURE] Create global state object for async requests
#491
Signed-off-by: Jovan Cvetkovic
---
public/store/DetectorsStore.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/public/store/DetectorsStore.tsx b/public/store/DetectorsStore.tsx
index 0ff7e7514..4f909cb8c 100644
--- a/public/store/DetectorsStore.tsx
+++ b/public/store/DetectorsStore.tsx
@@ -315,8 +315,8 @@ export class DetectorsStore implements IDetectorsStore {
private showToast = (toasts?: Toast[] | undefined): void => {};
public hideToast = (removedToast: any): void => {
- const toasts = this.toasts.filter((toast: Toast) => toast.id !== removedToast.id);
- this.showToast(toasts);
+ this.toasts = this.toasts.filter((toast: Toast) => toast.id !== removedToast.id);
+ this.showToast(this.toasts);
};
public setHandlers = (
From fae75fb85c88216d3333d7aae0ce97f12b2d3165 Mon Sep 17 00:00:00 2001
From: Jovan Cvetkovic
Date: Tue, 28 Mar 2023 12:56:37 +0200
Subject: [PATCH 11/64] [FEATURE] Provide empty states for Findings and Alerts
page #471
Signed-off-by: Jovan Cvetkovic
---
.../pages/Alerts/containers/Alerts/Alerts.tsx | 37 +++++++-
.../Alerts/__snapshots__/Alerts.test.tsx.snap | 94 +++++++++++++------
.../FindingsTable/FindingsTable.tsx | 28 +++++-
.../Findings/containers/Findings/Findings.tsx | 21 ++++-
.../Overview/containers/Overview/Overview.tsx | 2 +
5 files changed, 145 insertions(+), 37 deletions(-)
diff --git a/public/pages/Alerts/containers/Alerts/Alerts.tsx b/public/pages/Alerts/containers/Alerts/Alerts.tsx
index 3482e8fc2..4fe4c03e0 100644
--- a/public/pages/Alerts/containers/Alerts/Alerts.tsx
+++ b/public/pages/Alerts/containers/Alerts/Alerts.tsx
@@ -17,6 +17,7 @@ import {
EuiSuperDatePicker,
EuiTitle,
EuiToolTip,
+ EuiEmptyPrompt,
} from '@elastic/eui';
import { FieldValueSelectionFilterConfigType } from '@elastic/eui/src/components/search_bar/filters/field_value_selection_filter';
import dateMath from '@elastic/datemath';
@@ -81,6 +82,7 @@ export interface AlertsState {
loading: boolean;
timeUnit: TimeUnit;
dateFormat: string;
+ widgetEmptyMessage: React.ReactNode | undefined;
}
const groupByOptions = [
@@ -112,6 +114,7 @@ export class Alerts extends Component {
detectors: {},
timeUnit: timeUnits.timeUnit,
dateFormat: timeUnits.dateFormat,
+ widgetEmptyMessage: undefined,
};
}
@@ -148,7 +151,20 @@ export class Alerts extends Component {
const filteredAlerts = alerts.filter((alert) =>
moment(alert.last_notification_time).isBetween(moment(startMoment), moment(endMoment))
);
- this.setState({ alertsFiltered: true, filteredAlerts: filteredAlerts });
+ this.setState({
+ alertsFiltered: true,
+ filteredAlerts: filteredAlerts,
+ widgetEmptyMessage: filteredAlerts.length ? undefined : (
+
+ No alerts. Adjust the time range to see more
+ results.
+
+ }
+ />
+ ),
+ });
renderVisualization(this.generateVisualizationSpec(filteredAlerts), 'alerts-view');
};
@@ -413,6 +429,7 @@ export class Alerts extends Component {
flyoutData,
loading,
recentlyUsedRanges,
+ widgetEmptyMessage,
} = this.state;
const {
@@ -505,7 +522,7 @@ export class Alerts extends Component {
/>
-
+
@@ -514,7 +531,20 @@ export class Alerts extends Component {
{this.createGroupByControl()}
-
+ {!alerts || alerts.length === 0 ? (
+ No alerts}
+ body={
+
+ Adjust the time range to see more results or create alert triggers in your{' '}
+ detectors to
+ generate alerts.
+
+ }
+ />
+ ) : (
+
+ )}
@@ -532,6 +562,7 @@ export class Alerts extends Component {
sorting={sorting}
selection={selection}
loading={loading}
+ message={widgetEmptyMessage}
/>
diff --git a/public/pages/Alerts/containers/Alerts/__snapshots__/Alerts.test.tsx.snap b/public/pages/Alerts/containers/Alerts/__snapshots__/Alerts.test.tsx.snap
index 050feb771..8175e6547 100644
--- a/public/pages/Alerts/containers/Alerts/__snapshots__/Alerts.test.tsx.snap
+++ b/public/pages/Alerts/containers/Alerts/__snapshots__/Alerts.test.tsx.snap
@@ -707,10 +707,10 @@ exports[` spec renders the component 1`] = `
@@ -873,42 +873,76 @@ exports[` spec renders the component 1`] = `
-
+ Adjust the time range to see more results or create alert triggers in your
+
+
+ detectors
+
+ to generate alerts.
+
+ }
+ title={
+
+ No alerts
+
+ }
>
-
+
+ No alerts
+
+
+
-
-
-
-
+
+
+
+
+
+
+ Adjust the time range to see more results or create alert triggers in your
+
+
+
+ detectors
+
+
+ to generate alerts.
+
+
+
-
-
-
+
-
+
diff --git a/public/pages/Findings/components/FindingsTable/FindingsTable.tsx b/public/pages/Findings/components/FindingsTable/FindingsTable.tsx
index 53cfd6e53..6b9838d2c 100644
--- a/public/pages/Findings/components/FindingsTable/FindingsTable.tsx
+++ b/public/pages/Findings/components/FindingsTable/FindingsTable.tsx
@@ -12,6 +12,7 @@ import {
EuiInMemoryTable,
EuiLink,
EuiToolTip,
+ EuiEmptyPrompt,
} from '@elastic/eui';
import { FieldValueSelectionFilterConfigType } from '@elastic/eui/src/components/search_bar/filters/field_value_selection_filter';
import dateMath from '@elastic/datemath';
@@ -48,6 +49,7 @@ interface FindingsTableState {
flyout: object | undefined;
flyoutOpen: boolean;
selectedFinding?: Finding;
+ widgetEmptyMessage: React.ReactNode | undefined;
}
export default class FindingsTable extends Component {
@@ -59,6 +61,7 @@ export default class FindingsTable extends Component
moment(finding.timestamp).isBetween(moment(startMoment), moment(endMoment))
);
- this.setState({ findingsFiltered: true, filteredFindings: filteredFindings });
+ this.setState({
+ findingsFiltered: true,
+ filteredFindings: filteredFindings,
+ widgetEmptyMessage:
+ filteredFindings.length || findings.length ? undefined : (
+
+ No findings. Adjust the time range to see
+ more results.
+
+ }
+ />
+ ),
+ });
this.props.onFindingsFiltered(filteredFindings);
};
@@ -142,7 +159,13 @@ export default class FindingsTable extends Component[] = [
{
@@ -285,6 +308,7 @@ export default class FindingsTable extends Component
{flyoutOpen && flyout}
diff --git a/public/pages/Findings/containers/Findings/Findings.tsx b/public/pages/Findings/containers/Findings/Findings.tsx
index b76b654e6..2385cafc1 100644
--- a/public/pages/Findings/containers/Findings/Findings.tsx
+++ b/public/pages/Findings/containers/Findings/Findings.tsx
@@ -14,6 +14,8 @@ import {
EuiSpacer,
EuiSuperDatePicker,
EuiTitle,
+ EuiEmptyPrompt,
+ EuiLink,
} from '@elastic/eui';
import FindingsTable from '../../components/FindingsTable';
import FindingsService from '../../../../services/FindingsService';
@@ -37,7 +39,6 @@ import {
} from '../../../Overview/utils/helpers';
import { CoreServicesContext } from '../../../../components/core_services';
import { Finding } from '../../models/interfaces';
-import { Detector } from '../../../../../models/interfaces';
import { FeatureChannelList } from '../../../../../server/models/interfaces';
import {
getNotificationChannels,
@@ -54,6 +55,7 @@ import { NotificationsStart } from 'opensearch-dashboards/public';
import { DateTimeFilter } from '../../../Overview/models/interfaces';
import { ChartContainer } from '../../../../components/Charts/ChartContainer';
import { DataStore } from '../../../../store/DataStore';
+import { Detector } from '../../../../../types';
interface FindingsProps extends RouteComponentProps {
detectorService: DetectorsService;
@@ -340,7 +342,22 @@ class Findings extends Component {
{this.createGroupByControl()}
-
+ {!findings || findings.length === 0 ? (
+ No findings}
+ body={
+
+ Adjust the time range to see more results or{' '}
+
+ create a detector
+ {' '}
+ to generate findings.
+
+ }
+ />
+ ) : (
+
+ )}
diff --git a/public/pages/Overview/containers/Overview/Overview.tsx b/public/pages/Overview/containers/Overview/Overview.tsx
index 4ba1c5218..64c21ea4d 100644
--- a/public/pages/Overview/containers/Overview/Overview.tsx
+++ b/public/pages/Overview/containers/Overview/Overview.tsx
@@ -11,6 +11,7 @@ import {
EuiPopover,
EuiSuperDatePicker,
EuiTitle,
+ EuiSpacer,
} from '@elastic/eui';
import React, { useContext, useEffect, useMemo, useState } from 'react';
import {
@@ -166,6 +167,7 @@ export const Overview: React.FC = (props) => {
/>
+
Date: Fri, 31 Mar 2023 19:26:38 +0200
Subject: [PATCH 12/64] [FEATURE] Create detector \ Refactor and move field
mapping to first the page of create detector feature #495
Signed-off-by: Jovan Cvetkovic
---
public/app.scss | 2 +-
.../containers/ConfigureFieldMapping.tsx | 257 ++++++++++--------
.../DetectionRules/DetectionRules.tsx | 8 +-
.../DetectorDataSource/DetectorDataSource.tsx | 4 +-
.../DetectorSchedule/DetectorSchedule.tsx | 15 +-
.../components/DetectorSchedule/Interval.tsx | 3 +-
.../components/DetectorType/DetectorType.tsx | 18 ++
.../containers/DefineDetector.tsx | 101 ++++---
.../containers/CreateDetector.tsx | 22 +-
public/pages/CreateDetector/models/types.ts | 7 +-
.../pages/CreateDetector/utils/constants.ts | 8 +-
.../AlertTriggerView/AlertTriggerView.tsx | 3 +-
.../AlertTriggersView/AlertTriggersView.tsx | 2 +-
.../DetectorDetailsView.tsx | 1 -
14 files changed, 247 insertions(+), 204 deletions(-)
diff --git a/public/app.scss b/public/app.scss
index ffbc4e3a0..987c76e30 100644
--- a/public/app.scss
+++ b/public/app.scss
@@ -119,4 +119,4 @@ $euiTextColor: $euiColorDarkestShade !default;
.sa-overview-widget-empty tbody > .euiTableRow > .euiTableRowCell {
border-bottom: none;
-}
+}
diff --git a/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx b/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx
index 1be4d27cc..eb09b1e43 100644
--- a/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx
+++ b/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx
@@ -15,15 +15,14 @@ import {
EuiPanel,
} from '@elastic/eui';
import FieldMappingsTable from '../components/RequiredFieldMapping';
-import { createDetectorSteps } from '../../../utils/constants';
import { ContentPanel } from '../../../../../components/ContentPanel';
-import { Detector, FieldMapping } from '../../../../../../models/interfaces';
+import { FieldMapping } from '../../../../../../models/interfaces';
import { EMPTY_FIELD_MAPPINGS_VIEW } from '../utils/constants';
-import { DetectorCreationStep } from '../../../models/types';
import { GetFieldMappingViewResponse } from '../../../../../../server/models/interfaces';
import FieldMappingService from '../../../../../services/FieldMappingService';
import { MappingViewType } from '../components/RequiredFieldMapping/FieldMappingsTable';
import { CreateDetectorRulesState } from '../../DefineDetector/components/DetectionRules/DetectionRules';
+import { Detector } from '../../../../../../types';
export interface ruleFieldToIndexFieldMap {
[fieldName: string]: string;
@@ -36,12 +35,12 @@ interface ConfigureFieldMappingProps extends RouteComponentProps {
fieldMappings: FieldMapping[];
loading: boolean;
enabledRules: CreateDetectorRulesState['allRules'];
- updateDataValidState: (step: DetectorCreationStep, isValid: boolean) => void;
replaceFieldMappings: (mappings: FieldMapping[]) => void;
}
interface ConfigureFieldMappingState {
loading: boolean;
+ detector: Detector;
mappingsData: GetFieldMappingViewResponse;
createdMappings: ruleFieldToIndexFieldMap;
invalidMappingFieldNames: string[];
@@ -62,6 +61,7 @@ export default class ConfigureFieldMapping extends Component<
mappingsData: EMPTY_FIELD_MAPPINGS_VIEW,
createdMappings,
invalidMappingFieldNames: [],
+ detector: props.detector,
};
}
@@ -69,10 +69,27 @@ export default class ConfigureFieldMapping extends Component<
this.getAllMappings();
};
+ componentDidUpdate(
+ prevProps: Readonly,
+ prevState: Readonly,
+ snapshot?: any
+ ) {
+ if (prevProps.detector !== this.props.detector) {
+ this.setState(
+ {
+ detector: this.props.detector,
+ },
+ () => {
+ this.getAllMappings();
+ }
+ );
+ }
+ }
+
private getRuleFieldsForEnabledRules(): Set {
const ruleFieldsForEnabledRules = new Set();
this.props.enabledRules.forEach((rule) => {
- rule._source.query_field_names.forEach((fieldname) => {
+ rule._source.query_field_names.forEach((fieldname: { value: string }) => {
ruleFieldsForEnabledRules.add(fieldname.value);
});
});
@@ -81,42 +98,44 @@ export default class ConfigureFieldMapping extends Component<
}
getAllMappings = async () => {
- this.setState({ loading: true });
- const mappingsView = await this.props.filedMappingService.getMappingsView(
- this.props.detector.inputs[0].detector_input.indices[0],
- this.props.detector.detector_type.toLowerCase()
- );
- if (mappingsView.ok) {
- const existingMappings = { ...this.state.createdMappings };
- const ruleFieldsForEnabledRules = this.getRuleFieldsForEnabledRules();
- const unmappedRuleFields = new Set(mappingsView.response.unmapped_field_aliases);
-
- Object.keys(mappingsView.response.properties).forEach((ruleFieldName) => {
- // Filter the mappings view to include only the rule fields for the enabled rules
- if (!ruleFieldsForEnabledRules.has(ruleFieldName)) {
- delete mappingsView.response.properties[ruleFieldName];
- return;
- }
-
- existingMappings[ruleFieldName] =
- this.state.createdMappings[ruleFieldName] ||
- mappingsView.response.properties[ruleFieldName].path;
- });
- mappingsView.response.unmapped_field_aliases?.forEach((ruleFieldName) => {
- if (!ruleFieldsForEnabledRules.has(ruleFieldName)) {
- unmappedRuleFields.delete(ruleFieldName);
- }
- });
- this.setState({
- createdMappings: existingMappings,
- mappingsData: {
- ...mappingsView.response,
- unmapped_field_aliases: Array.from(unmappedRuleFields),
- },
- });
- this.updateMappingSharedState(existingMappings);
+ if (this.state.detector.inputs[0]?.detector_input.indices[0]) {
+ this.setState({ loading: true });
+ const mappingsView = await this.props.filedMappingService.getMappingsView(
+ this.state.detector.inputs[0].detector_input.indices[0],
+ this.state.detector.detector_type.toLowerCase()
+ );
+ if (mappingsView.ok) {
+ const existingMappings = { ...this.state.createdMappings };
+ const ruleFieldsForEnabledRules = this.getRuleFieldsForEnabledRules();
+ const unmappedRuleFields = new Set(mappingsView.response.unmapped_field_aliases);
+
+ Object.keys(mappingsView.response.properties).forEach((ruleFieldName) => {
+ // Filter the mappings view to include only the rule fields for the enabled rules
+ if (!ruleFieldsForEnabledRules.has(ruleFieldName)) {
+ delete mappingsView.response.properties[ruleFieldName];
+ return;
+ }
+
+ existingMappings[ruleFieldName] =
+ this.state.createdMappings[ruleFieldName] ||
+ mappingsView.response.properties[ruleFieldName].path;
+ });
+ mappingsView.response.unmapped_field_aliases?.forEach((ruleFieldName) => {
+ if (!ruleFieldsForEnabledRules.has(ruleFieldName)) {
+ unmappedRuleFields.delete(ruleFieldName);
+ }
+ });
+ this.setState({
+ createdMappings: existingMappings,
+ mappingsData: {
+ ...mappingsView.response,
+ unmapped_field_aliases: Array.from(unmappedRuleFields),
+ },
+ });
+ this.updateMappingSharedState(existingMappings);
+ }
+ this.setState({ loading: false });
}
- this.setState({ loading: false });
};
/**
@@ -153,7 +172,6 @@ export default class ConfigureFieldMapping extends Component<
invalidMappingFieldNames: invalidMappingFieldNames,
});
this.updateMappingSharedState(newMappings);
- this.props.updateDataValidState(DetectorCreationStep.CONFIGURE_FIELD_MAPPING, true);
};
updateMappingSharedState = (createdMappings: ruleFieldToIndexFieldMap) => {
@@ -197,74 +215,48 @@ export default class ConfigureFieldMapping extends Component<
const indexFieldOptions = Array.from(logFields);
return (
-
-
- {createDetectorSteps[DetectorCreationStep.CONFIGURE_FIELD_MAPPING].title}
-
-
-
- To perform threat detection, known field names from your log data source are automatically
- mapped to rule field names. Additional fields that may require manual mapping will be
- shown below.
-
-
-
-
-
+ <>
+
-
- {`Automatically mapped fields (${mappedRuleFields.length})`}
+ <>
+
+ Configure field mapping - optional
-
+
+
+ To perform threat detection, known field names from your log data source are
+ automatically mapped to rule field names. Additional fields that may require
+ manual mapping will be shown below.
+
+ >
}
- buttonProps={{ style: { paddingLeft: '10px', paddingRight: '10px' } }}
- id={'mappedFieldsAccordion'}
+ buttonProps={{ style: { padding: '5px' } }}
+ id={'mappedTitleFieldsAccordion'}
initialIsOpen={false}
>
-
- {...this.props}
- loading={loading}
- ruleFields={mappedRuleFields}
- indexFields={indexFieldOptions}
- mappingProps={{
- type: MappingViewType.Edit,
- existingMappings,
- invalidMappingFieldNames,
- onMappingCreation: this.onMappingCreation,
- }}
- />
-
-
-
-
-
- {unmappedRuleFields.length > 0 ? (
- <>
- {pendingCount > 0 ? (
-
-
- To generate accurate findings, we recommend mapping the following security rules
- fields with the log fields in your data source.
-
-
- ) : (
-
- Your data source have been mapped with all security rule fields.
-
- )}
-
-
+
+
+
+ {`Automatically mapped fields (${mappedRuleFields.length})`}
+
+
+ }
+ buttonProps={{
+ style: { paddingLeft: '5px', paddingRight: '5px', paddingTop: '5px' },
+ }}
+ id={'mappedFieldsAccordion'}
+ initialIsOpen={false}
+ >
+
{...this.props}
loading={loading}
- ruleFields={unmappedRuleFields}
+ ruleFields={mappedRuleFields}
indexFields={indexFieldOptions}
mappingProps={{
type: MappingViewType.Edit,
@@ -273,23 +265,62 @@ export default class ConfigureFieldMapping extends Component<
onMappingCreation: this.onMappingCreation,
}}
/>
-
-
- >
- ) : (
- <>
-
-
- Your data source(s) have been mapped with all security rule fields. No action is
- needed.
-
-
+
+
- >
- )}
-
-
+
+ {unmappedRuleFields.length > 0 ? (
+ <>
+ {pendingCount > 0 ? (
+
+
+ To generate accurate findings, we recommend mapping the following security
+ rules fields with the log fields in your data source.
+
+
+ ) : (
+
+ Your data source have been mapped with all security rule fields.
+
+ )}
+
+
+
+
+
+ {...this.props}
+ loading={loading}
+ ruleFields={unmappedRuleFields}
+ indexFields={indexFieldOptions}
+ mappingProps={{
+ type: MappingViewType.Edit,
+ existingMappings,
+ invalidMappingFieldNames,
+ onMappingCreation: this.onMappingCreation,
+ }}
+ />
+
+ >
+ ) : (
+ <>
+
+
+ Your data source(s) have been mapped with all security rule fields. No action
+ is needed.
+
+
+ >
+ )}
+
+
+
+
+
+ >
);
}
}
diff --git a/public/pages/CreateDetector/components/DefineDetector/components/DetectionRules/DetectionRules.tsx b/public/pages/CreateDetector/components/DefineDetector/components/DetectionRules/DetectionRules.tsx
index 121b2115b..882c8852e 100644
--- a/public/pages/CreateDetector/components/DefineDetector/components/DetectionRules/DetectionRules.tsx
+++ b/public/pages/CreateDetector/components/DefineDetector/components/DetectionRules/DetectionRules.tsx
@@ -4,7 +4,6 @@
*/
import {
- EuiPanel,
EuiAccordion,
EuiTitle,
EuiHorizontalRule,
@@ -17,7 +16,7 @@ import { DetectionRulesTable } from './DetectionRulesTable';
import { RuleItem, RuleItemInfo } from './types/interfaces';
import { RuleViewerFlyout } from '../../../../../Rules/components/RuleViewerFlyout/RuleViewerFlyout';
import { RuleTableItem } from '../../../../../Rules/utils/helpers';
-import { RuleItemInfoBase } from '../../../../../Rules/models/types';
+import { RuleItemInfoBase } from '../../../../../../../types';
export interface CreateDetectorRulesState {
allRules: RuleItemInfo[];
@@ -82,7 +81,7 @@ export const DetectionRules: React.FC = ({
};
return (
-
+ <>
{flyoutData ? (
setFlyoutData(() => null)}
@@ -102,7 +101,6 @@ export const DetectionRules: React.FC = ({
}
- buttonProps={{ style: { paddingLeft: '10px', paddingRight: '10px' } }}
id={'detectorRulesAccordion'}
initialIsOpen={false}
isLoading={loading}
@@ -117,6 +115,6 @@ export const DetectionRules: React.FC = ({
onRuleDetails={onRuleDetails}
/>
-
+ >
);
};
diff --git a/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx b/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx
index 1f19fba0a..7aa617ec8 100644
--- a/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx
+++ b/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx
@@ -25,7 +25,7 @@ import { FieldMappingService } from '../../../../../../services';
interface DetectorDataSourceProps {
detectorIndices: string[];
indexService: IndexService;
- filedMappingService: FieldMappingService;
+ fieldMappingService: FieldMappingService;
isEdit: boolean;
onDetectorInputIndicesChange: (selectedOptions: EuiComboBoxOptionOption[]) => void;
notifications: NotificationsStart;
@@ -109,7 +109,7 @@ export default class DetectorDataSource extends Component<
for (const indexName of allIndices) {
if (!this.indicesMappings[indexName]) {
const detectorType = this.props.detector_type.toLowerCase();
- const result = await this.props.filedMappingService.getMappingsView(
+ const result = await this.props.fieldMappingService.getMappingsView(
indexName,
detectorType
);
diff --git a/public/pages/CreateDetector/components/DefineDetector/components/DetectorSchedule/DetectorSchedule.tsx b/public/pages/CreateDetector/components/DefineDetector/components/DetectorSchedule/DetectorSchedule.tsx
index e34824e72..2816d0ddb 100644
--- a/public/pages/CreateDetector/components/DefineDetector/components/DetectorSchedule/DetectorSchedule.tsx
+++ b/public/pages/CreateDetector/components/DefineDetector/components/DetectorSchedule/DetectorSchedule.tsx
@@ -5,14 +5,14 @@
import { ContentPanel } from '../../../../../../components/ContentPanel';
import React from 'react';
-import { EuiFormRow, EuiSelect, EuiSelectOption } from '@elastic/eui';
-import FormFieldHeader from '../../../../../../components/FormFieldHeader';
-import { Detector, PeriodSchedule } from '../../../../../../../models/interfaces';
+import { EuiSelectOption } from '@elastic/eui';
+import { PeriodSchedule } from '../../../../../../../models/interfaces';
import { Interval } from './Interval';
import { CustomCron } from './CustomCron';
import { Daily } from './Daily';
import { Monthly } from './Monthly';
import { Weekly } from './Weekly';
+import { Detector } from '../../../../../../../types';
const frequencies: EuiSelectOption[] = [{ value: 'interval', text: 'By interval' }];
@@ -53,15 +53,6 @@ export class DetectorSchedule extends React.Component<
return (
- }>
-
-
-
);
diff --git a/public/pages/CreateDetector/components/DefineDetector/components/DetectorSchedule/Interval.tsx b/public/pages/CreateDetector/components/DefineDetector/components/DetectorSchedule/Interval.tsx
index 171b32711..1d1f7e296 100644
--- a/public/pages/CreateDetector/components/DefineDetector/components/DetectorSchedule/Interval.tsx
+++ b/public/pages/CreateDetector/components/DefineDetector/components/DetectorSchedule/Interval.tsx
@@ -13,7 +13,8 @@ import {
} from '@elastic/eui';
import FormFieldHeader from '../../../../../../components/FormFieldHeader';
import React from 'react';
-import { Detector, PeriodSchedule } from '../../../../../../../models/interfaces';
+import { PeriodSchedule } from '../../../../../../../models/interfaces';
+import { Detector } from '../../../../../../../types';
export interface IntervalProps {
detector: Detector;
diff --git a/public/pages/CreateDetector/components/DefineDetector/components/DetectorType/DetectorType.tsx b/public/pages/CreateDetector/components/DefineDetector/components/DetectorType/DetectorType.tsx
index 7fb2dc616..aeb6e9fa6 100644
--- a/public/pages/CreateDetector/components/DefineDetector/components/DetectorType/DetectorType.tsx
+++ b/public/pages/CreateDetector/components/DefineDetector/components/DetectorType/DetectorType.tsx
@@ -9,10 +9,17 @@ import { EuiFormRow, EuiFlexGrid, EuiFlexItem, EuiRadio, EuiSpacer } from '@elas
import { FormFieldHeader } from '../../../../../../components/FormFieldHeader/FormFieldHeader';
import { DETECTOR_TYPES } from '../../../../../Detectors/utils/constants';
import { DetectorTypeOption } from '../../../../../Detectors/models/interfaces';
+import { CreateDetectorRulesState, DetectionRules } from '../DetectionRules/DetectionRules';
+import { RuleItem } from '../DetectionRules/types/interfaces';
interface DetectorTypeProps {
detectorType: string;
onDetectorTypeChange: (detectorType: string) => void;
+ rulesState: CreateDetectorRulesState;
+ loadingRules?: boolean;
+ onPageChange: (page: { index: number; size: number }) => void;
+ onRuleToggle: (changedItem: RuleItem, isActive: boolean) => void;
+ onAllRulesToggle: (enabled: boolean) => void;
}
interface DetectorTypeState {
@@ -67,6 +74,7 @@ export default class DetectorType extends Component
));
+
return (
{radioButtons}
+
+
+
+
);
}
diff --git a/public/pages/CreateDetector/components/DefineDetector/containers/DefineDetector.tsx b/public/pages/CreateDetector/components/DefineDetector/containers/DefineDetector.tsx
index 474288563..c8738fe1f 100644
--- a/public/pages/CreateDetector/components/DefineDetector/containers/DefineDetector.tsx
+++ b/public/pages/CreateDetector/components/DefineDetector/containers/DefineDetector.tsx
@@ -6,7 +6,7 @@
import React, { Component } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { EuiSpacer, EuiTitle, EuiText, EuiCallOut } from '@elastic/eui';
-import { Detector, PeriodSchedule } from '../../../../../../models/interfaces';
+import { PeriodSchedule } from '../../../../../../models/interfaces';
import DetectorBasicDetailsForm from '../components/DetectorDetails';
import DetectorDataSource from '../components/DetectorDataSource';
import DetectorType from '../components/DetectorType';
@@ -16,19 +16,18 @@ import { MIN_NUM_DATA_SOURCES } from '../../../../Detectors/utils/constants';
import { DetectorCreationStep } from '../../../models/types';
import { DetectorSchedule } from '../components/DetectorSchedule/DetectorSchedule';
import { RuleItem } from '../components/DetectionRules/types/interfaces';
-import {
- CreateDetectorRulesState,
- DetectionRules,
-} from '../components/DetectionRules/DetectionRules';
+import { CreateDetectorRulesState } from '../components/DetectionRules/DetectionRules';
import { NotificationsStart } from 'opensearch-dashboards/public';
-import _ from 'lodash';
import { logTypesWithDashboards } from '../../../../../utils/constants';
+import ConfigureFieldMapping from '../../ConfigureFieldMapping';
+import { Detector, FieldMapping } from '../../../../../../types';
interface DefineDetectorProps extends RouteComponentProps {
detector: Detector;
isEdit: boolean;
indexService: IndexService;
- filedMappingService: FieldMappingService;
+ fieldMappingService: FieldMappingService;
+ fieldMappings: FieldMapping[];
rulesState: CreateDetectorRulesState;
notifications: NotificationsStart;
loadingRules?: boolean;
@@ -37,11 +36,33 @@ interface DefineDetectorProps extends RouteComponentProps {
onPageChange: (page: { index: number; size: number }) => void;
onRuleToggle: (changedItem: RuleItem, isActive: boolean) => void;
onAllRulesToggle: (enabled: boolean) => void;
+ replaceFieldMappings: (mappings: FieldMapping[]) => void;
}
-interface DefineDetectorState {}
+interface DefineDetectorState {
+ detector: Detector;
+}
export default class DefineDetector extends Component {
+ constructor(props: DefineDetectorProps) {
+ super(props);
+ this.state = {
+ detector: this.props.detector,
+ };
+ }
+
+ componentDidUpdate(
+ prevProps: Readonly,
+ prevState: Readonly,
+ snapshot?: any
+ ) {
+ if (prevProps.detector !== this.props.detector) {
+ this.setState({
+ detector: this.props.detector,
+ });
+ }
+ }
+
updateDetectorCreationState(detector: Detector) {
const isDataValid =
!!detector.name &&
@@ -55,7 +76,7 @@ export default class DefineDetector extends Component {
const newDetector: Detector = {
- ...this.props.detector,
+ ...this.state.detector,
name: detectorName,
};
@@ -63,9 +84,9 @@ export default class DefineDetector extends Component {
- const { inputs } = this.props.detector;
+ const { inputs } = this.state.detector;
const newDetector: Detector = {
- ...this.props.detector,
+ ...this.state.detector,
inputs: [
{
detector_input: {
@@ -82,9 +103,9 @@ export default class DefineDetector extends Component[]) => {
const detectorIndices = selectedOptions.map((selectedOption) => selectedOption.label);
- const { inputs } = this.props.detector;
+ const { inputs } = this.state.detector;
const newDetector: Detector = {
- ...this.props.detector,
+ ...this.state.detector,
inputs: [
{
detector_input: {
@@ -101,7 +122,7 @@ export default class DefineDetector extends Component {
const newDetector: Detector = {
- ...this.props.detector,
+ ...this.state.detector,
detector_type: detectorType,
};
@@ -109,9 +130,9 @@ export default class DefineDetector extends Component {
- const { inputs } = this.props.detector;
+ const { inputs } = this.state.detector;
const newDetector: Detector = {
- ...this.props.detector,
+ ...this.state.detector,
inputs: [
{
detector_input: {
@@ -129,9 +150,9 @@ export default class DefineDetector extends Component {
- const { inputs } = this.props.detector;
+ const { inputs } = this.state.detector;
const newDetector: Detector = {
- ...this.props.detector,
+ ...this.state.detector,
inputs: [
{
detector_input: {
@@ -150,7 +171,7 @@ export default class DefineDetector extends Component {
const newDetector: Detector = {
- ...this.props.detector,
+ ...this.state.detector,
schedule,
};
@@ -158,16 +179,9 @@ export default class DefineDetector extends Component
@@ -206,16 +220,11 @@ export default class DefineDetector extends Component
-
-
-
-
@@ -235,6 +244,18 @@ export default class DefineDetector extends Component
) : null}
+ rule.enabled)}
+ replaceFieldMappings={this.props.replaceFieldMappings}
+ />
+
+
+
{
+ updateDataValidState = (step: DetectorCreationStep | string, isValid: boolean): void => {
this.setState({
stepDataValid: {
...this.state.stepDataValid,
@@ -289,25 +288,14 @@ export default class CreateDetector extends Component
- );
- case DetectorCreationStep.CONFIGURE_FIELD_MAPPING:
- return (
- rule.enabled)}
replaceFieldMappings={this.replaceFieldMappings}
updateDataValidState={this.updateDataValidState}
/>
diff --git a/public/pages/CreateDetector/models/types.ts b/public/pages/CreateDetector/models/types.ts
index f1aff01f5..157f62ae9 100644
--- a/public/pages/CreateDetector/models/types.ts
+++ b/public/pages/CreateDetector/models/types.ts
@@ -5,9 +5,8 @@
export enum DetectorCreationStep {
DEFINE_DETECTOR = 1,
- CONFIGURE_FIELD_MAPPING = 2,
- CONFIGURE_ALERTS = 3,
- REVIEW_CREATE = 4,
+ CONFIGURE_ALERTS = 2,
+ REVIEW_CREATE = 3,
}
export interface DefineDetectorData {
@@ -40,7 +39,7 @@ export interface ReviewAndCreateData {
export interface DetectorDataByStep {
[DetectorCreationStep.DEFINE_DETECTOR]: DefineDetectorData;
- [DetectorCreationStep.CONFIGURE_FIELD_MAPPING]: ConfigureFieldMappingData;
+ ['CONFIGURE_FIELD_MAPPING']: ConfigureFieldMappingData;
[DetectorCreationStep.CONFIGURE_ALERTS]: ConfigureAlertsData;
[DetectorCreationStep.REVIEW_CREATE]: ReviewAndCreateData;
}
diff --git a/public/pages/CreateDetector/utils/constants.ts b/public/pages/CreateDetector/utils/constants.ts
index e3d09b4c4..91cc66760 100644
--- a/public/pages/CreateDetector/utils/constants.ts
+++ b/public/pages/CreateDetector/utils/constants.ts
@@ -11,17 +11,13 @@ export const createDetectorSteps: Record
{rulesCanFold ? detectorRules : null}
-
{rulesCanFold ? null : detectorRules}
>
From 9997e3b3ad85188e16359da2ef3cfad230dfc330 Mon Sep 17 00:00:00 2001
From: Jovan Cvetkovic
Date: Mon, 3 Apr 2023 11:04:08 +0200
Subject: [PATCH 13/64] [FEATURE] Create detector \ Refactor and move field
mapping to first the page of create detector feature #495
Signed-off-by: Jovan Cvetkovic
---
.../components/ReviewAndCreate/containers/ReviewAndCreate.tsx | 2 +-
public/pages/CreateDetector/containers/CreateDetector.tsx | 1 -
public/pages/CreateDetector/models/types.ts | 1 -
.../components/FieldMappingsView/FieldMappingsView.tsx | 2 +-
.../containers/AlertTriggersView/AlertTriggersView.tsx | 2 +-
.../containers/DetectorDetailsView/DetectorDetailsView.tsx | 3 +--
6 files changed, 4 insertions(+), 7 deletions(-)
diff --git a/public/pages/CreateDetector/components/ReviewAndCreate/containers/ReviewAndCreate.tsx b/public/pages/CreateDetector/components/ReviewAndCreate/containers/ReviewAndCreate.tsx
index 7cccd0740..23d730664 100644
--- a/public/pages/CreateDetector/components/ReviewAndCreate/containers/ReviewAndCreate.tsx
+++ b/public/pages/CreateDetector/components/ReviewAndCreate/containers/ReviewAndCreate.tsx
@@ -28,7 +28,7 @@ export class ReviewAndCreate extends React.Component {
- this.props.setDetectorCreationStep(DetectorCreationStep.CONFIGURE_FIELD_MAPPING);
+ this.props.setDetectorCreationStep(DetectorCreationStep.DEFINE_DETECTOR);
};
setConfigureAlertsStep = () => {
diff --git a/public/pages/CreateDetector/containers/CreateDetector.tsx b/public/pages/CreateDetector/containers/CreateDetector.tsx
index 10915720f..fb68813c3 100644
--- a/public/pages/CreateDetector/containers/CreateDetector.tsx
+++ b/public/pages/CreateDetector/containers/CreateDetector.tsx
@@ -67,7 +67,6 @@ export default class CreateDetector extends Component void;
notifications: NotificationsStart;
- isEditable: boolean;
+ isEditable?: boolean;
}
const columns: EuiBasicTableColumn[] = [
diff --git a/public/pages/Detectors/containers/AlertTriggersView/AlertTriggersView.tsx b/public/pages/Detectors/containers/AlertTriggersView/AlertTriggersView.tsx
index 406e566f2..5129f3144 100644
--- a/public/pages/Detectors/containers/AlertTriggersView/AlertTriggersView.tsx
+++ b/public/pages/Detectors/containers/AlertTriggersView/AlertTriggersView.tsx
@@ -23,7 +23,7 @@ export interface AlertTriggersViewProps {
detector: Detector;
editAlertTriggers: () => void;
notifications: NotificationsStart;
- isEditable: boolean;
+ isEditable?: boolean;
}
export const AlertTriggersView: React.FC = ({
diff --git a/public/pages/Detectors/containers/DetectorDetailsView/DetectorDetailsView.tsx b/public/pages/Detectors/containers/DetectorDetailsView/DetectorDetailsView.tsx
index e07254e1c..cbafe7f7d 100644
--- a/public/pages/Detectors/containers/DetectorDetailsView/DetectorDetailsView.tsx
+++ b/public/pages/Detectors/containers/DetectorDetailsView/DetectorDetailsView.tsx
@@ -6,7 +6,6 @@
import React from 'react';
import { DetectorBasicDetailsView } from '../../components/DetectorBasicDetailsView/DetectorBasicDetailsView';
import { DetectorRulesView } from '../../components/DetectorRulesView/DetectorRulesView';
-import { EuiSpacer } from '@elastic/eui';
import { RuleItem } from '../../../CreateDetector/components/DefineDetector/components/DetectionRules/types/interfaces';
import { NotificationsStart } from 'opensearch-dashboards/public';
import { Detector } from '../../../../../types';
@@ -20,7 +19,7 @@ export interface DetectorDetailsViewProps {
dashboardId?: string;
editBasicDetails: () => void;
editDetectorRules: (enabledRules: RuleItem[], allRuleItems: RuleItem[]) => void;
- isEditable: boolean;
+ isEditable?: boolean;
}
export interface DetectorDetailsViewState {}
From b2af92309dbb6e3e3aafb9da8cb42db24fac48b6 Mon Sep 17 00:00:00 2001
From: Jovan Cvetkovic
Date: Mon, 3 Apr 2023 11:15:03 +0200
Subject: [PATCH 14/64] [FEATURE] Create detector \ Refactor and move field
mapping to first the page of create detector feature #495
Signed-off-by: Jovan Cvetkovic
---
.../components/ReviewAndCreate/containers/ReviewAndCreate.tsx | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/public/pages/CreateDetector/components/ReviewAndCreate/containers/ReviewAndCreate.tsx b/public/pages/CreateDetector/components/ReviewAndCreate/containers/ReviewAndCreate.tsx
index 23d730664..56906b26a 100644
--- a/public/pages/CreateDetector/components/ReviewAndCreate/containers/ReviewAndCreate.tsx
+++ b/public/pages/CreateDetector/components/ReviewAndCreate/containers/ReviewAndCreate.tsx
@@ -9,9 +9,10 @@ import { DetectorDetailsView } from '../../../../Detectors/containers/DetectorDe
import { FieldMappingsView } from '../../../../Detectors/components/FieldMappingsView/FieldMappingsView';
import { AlertTriggersView } from '../../../../Detectors/containers/AlertTriggersView/AlertTriggersView';
import { RouteComponentProps } from 'react-router-dom';
-import { Detector, FieldMapping } from '../../../../../../models/interfaces';
+import { FieldMapping } from '../../../../../../models/interfaces';
import { DetectorCreationStep } from '../../../models/types';
import { NotificationsStart } from 'opensearch-dashboards/public';
+import { Detector } from '../../../../../../types';
export interface ReviewAndCreateProps extends RouteComponentProps {
detector: Detector;
From a29bb412147c39e69140fbac2838e5eb620c7a7c Mon Sep 17 00:00:00 2001
From: Jovan Cvetkovic
Date: Tue, 4 Apr 2023 11:36:15 +0200
Subject: [PATCH 15/64] [FEATURE] Create detector \ Refactor and move field
mapping to first the page of create detector feature #495
Signed-off-by: Jovan Cvetkovic
---
.../containers/ConfigureFieldMapping.tsx | 80 +++++++++++--------
.../DetectorDataSource/DetectorDataSource.tsx | 53 ++++++------
2 files changed, 73 insertions(+), 60 deletions(-)
diff --git a/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx b/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx
index eb09b1e43..dce0ae695 100644
--- a/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx
+++ b/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx
@@ -15,7 +15,6 @@ import {
EuiPanel,
} from '@elastic/eui';
import FieldMappingsTable from '../components/RequiredFieldMapping';
-import { ContentPanel } from '../../../../../components/ContentPanel';
import { FieldMapping } from '../../../../../../models/interfaces';
import { EMPTY_FIELD_MAPPINGS_VIEW } from '../utils/constants';
import { GetFieldMappingViewResponse } from '../../../../../../server/models/interfaces';
@@ -44,6 +43,7 @@ interface ConfigureFieldMappingState {
mappingsData: GetFieldMappingViewResponse;
createdMappings: ruleFieldToIndexFieldMap;
invalidMappingFieldNames: string[];
+ fieldMappingIsOpen: boolean;
}
export default class ConfigureFieldMapping extends Component<
@@ -62,6 +62,7 @@ export default class ConfigureFieldMapping extends Component<
createdMappings,
invalidMappingFieldNames: [],
detector: props.detector,
+ fieldMappingIsOpen: false,
};
}
@@ -131,6 +132,7 @@ export default class ConfigureFieldMapping extends Component<
...mappingsView.response,
unmapped_field_aliases: Array.from(unmappedRuleFields),
},
+ fieldMappingIsOpen: !!unmappedRuleFields.size,
});
this.updateMappingSharedState(existingMappings);
}
@@ -186,7 +188,13 @@ export default class ConfigureFieldMapping extends Component<
};
render() {
- const { loading, mappingsData, createdMappings, invalidMappingFieldNames } = this.state;
+ const {
+ loading,
+ mappingsData,
+ createdMappings,
+ invalidMappingFieldNames,
+ fieldMappingIsOpen,
+ } = this.state;
const existingMappings: ruleFieldToIndexFieldMap = {
...createdMappings,
};
@@ -233,10 +241,14 @@ export default class ConfigureFieldMapping extends Component<
}
buttonProps={{ style: { padding: '5px' } }}
id={'mappedTitleFieldsAccordion'}
- initialIsOpen={false}
+ initialIsOpen={!!unmappedRuleFields.length}
+ forceState={fieldMappingIsOpen ? 'open' : 'closed'}
+ onToggle={(isOpen) => {
+ this.setState({ fieldMappingIsOpen: isOpen });
+ }}
>
-
+
-
-
- {...this.props}
- loading={loading}
- ruleFields={mappedRuleFields}
- indexFields={indexFieldOptions}
- mappingProps={{
- type: MappingViewType.Edit,
- existingMappings,
- invalidMappingFieldNames,
- onMappingCreation: this.onMappingCreation,
- }}
- />
+
+
+ {...this.props}
+ loading={loading}
+ ruleFields={mappedRuleFields}
+ indexFields={indexFieldOptions}
+ mappingProps={{
+ type: MappingViewType.Edit,
+ existingMappings,
+ invalidMappingFieldNames,
+ onMappingCreation: this.onMappingCreation,
+ }}
+ />
+
@@ -274,7 +287,7 @@ export default class ConfigureFieldMapping extends Component<
<>
{pendingCount > 0 ? (
@@ -284,26 +297,27 @@ export default class ConfigureFieldMapping extends Component<
) : (
- Your data source have been mapped with all security rule fields.
+ Your data source(s) have been mapped with all security rule fields.
)}
-
-
- {...this.props}
- loading={loading}
- ruleFields={unmappedRuleFields}
- indexFields={indexFieldOptions}
- mappingProps={{
- type: MappingViewType.Edit,
- existingMappings,
- invalidMappingFieldNames,
- onMappingCreation: this.onMappingCreation,
- }}
- />
-
+
+ Pending field mappings
+
+
+ {...this.props}
+ loading={loading}
+ ruleFields={unmappedRuleFields}
+ indexFields={indexFieldOptions}
+ mappingProps={{
+ type: MappingViewType.Edit,
+ existingMappings,
+ invalidMappingFieldNames,
+ onMappingCreation: this.onMappingCreation,
+ }}
+ />
>
) : (
<>
diff --git a/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx b/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx
index 7aa617ec8..e992472f2 100644
--- a/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx
+++ b/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx
@@ -37,7 +37,7 @@ interface DetectorDataSourceState {
fieldTouched: boolean;
indexOptions: IndexOption[];
errorMessage?: string;
- message: string[];
+ differentLogTypeDetected: boolean;
}
export default class DetectorDataSource extends Component<
@@ -52,7 +52,7 @@ export default class DetectorDataSource extends Component<
loading: true,
fieldTouched: props.isEdit,
indexOptions: [],
- message: [],
+ differentLogTypeDetected: false,
};
}
@@ -120,23 +120,19 @@ export default class DetectorDataSource extends Component<
if (!_.isEmpty(this.indicesMappings)) {
let firstMapping: string[] = [];
let firstMatchMappingIndex: string = '';
- let message: string[] = [];
+ let differentLogTypeDetected = false;
for (let indexName in this.indicesMappings) {
if (this.indicesMappings.hasOwnProperty(indexName)) {
if (!firstMapping.length) firstMapping = this.indicesMappings[indexName];
!firstMatchMappingIndex.length && (firstMatchMappingIndex = indexName);
if (!_.isEqual(firstMapping, this.indicesMappings[indexName])) {
- message = [
- `We recommend creating separate detectors for each of the following log sources:`,
- firstMatchMappingIndex,
- indexName,
- ];
+ differentLogTypeDetected = true;
break;
}
}
}
- this.setState({ message });
+ this.setState({ differentLogTypeDetected });
}
this.props.onDetectorInputIndicesChange(options);
@@ -144,28 +140,17 @@ export default class DetectorDataSource extends Component<
render() {
const { detectorIndices } = this.props;
- const { loading, fieldTouched, indexOptions, errorMessage, message } = this.state;
+ const {
+ loading,
+ fieldTouched,
+ indexOptions,
+ errorMessage,
+ differentLogTypeDetected,
+ } = this.state;
const isInvalid = fieldTouched && detectorIndices.length < MIN_NUM_DATA_SOURCES;
return (
- {message.length ? (
- <>
-
- {message.map((messageItem: string, index: number) => (
-
- {index === 0 ? '' : 'ㅤ•ㅤ'}
- {messageItem}
-
-
- ))}
-
-
- >
- ) : null}
@@ -186,6 +171,20 @@ export default class DetectorDataSource extends Component<
data-test-subj={'define-detector-select-data-source'}
/>
+ {differentLogTypeDetected ? (
+ <>
+
+
+
+ To avoid issues with field mappings, we recommend creating separate detectors for
+ different log types.
+
+
+ >
+ ) : null}
);
}
From 01ebf2d2827f9046d37be5e8fa515cf9e883f387 Mon Sep 17 00:00:00 2001
From: Jovan Cvetkovic
Date: Tue, 4 Apr 2023 12:05:01 +0200
Subject: [PATCH 16/64] [FEATURE] Create detector \ Refactor and move field
mapping to first the page of create detector feature #495
Signed-off-by: Jovan Cvetkovic
---
.../DetectorDataSource/DetectorDataSource.tsx | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx b/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx
index e992472f2..50da2d817 100644
--- a/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx
+++ b/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx
@@ -37,7 +37,7 @@ interface DetectorDataSourceState {
fieldTouched: boolean;
indexOptions: IndexOption[];
errorMessage?: string;
- differentLogTypeDetected: boolean;
+ differentLogTypesDetected: boolean;
}
export default class DetectorDataSource extends Component<
@@ -52,7 +52,7 @@ export default class DetectorDataSource extends Component<
loading: true,
fieldTouched: props.isEdit,
indexOptions: [],
- differentLogTypeDetected: false,
+ differentLogTypesDetected: false,
};
}
@@ -120,19 +120,19 @@ export default class DetectorDataSource extends Component<
if (!_.isEmpty(this.indicesMappings)) {
let firstMapping: string[] = [];
let firstMatchMappingIndex: string = '';
- let differentLogTypeDetected = false;
+ let differentLogTypesDetected = false;
for (let indexName in this.indicesMappings) {
if (this.indicesMappings.hasOwnProperty(indexName)) {
if (!firstMapping.length) firstMapping = this.indicesMappings[indexName];
!firstMatchMappingIndex.length && (firstMatchMappingIndex = indexName);
if (!_.isEqual(firstMapping, this.indicesMappings[indexName])) {
- differentLogTypeDetected = true;
+ differentLogTypesDetected = true;
break;
}
}
}
- this.setState({ differentLogTypeDetected });
+ this.setState({ differentLogTypesDetected });
}
this.props.onDetectorInputIndicesChange(options);
@@ -145,7 +145,7 @@ export default class DetectorDataSource extends Component<
fieldTouched,
indexOptions,
errorMessage,
- differentLogTypeDetected,
+ differentLogTypesDetected,
} = this.state;
const isInvalid = fieldTouched && detectorIndices.length < MIN_NUM_DATA_SOURCES;
return (
@@ -171,7 +171,7 @@ export default class DetectorDataSource extends Component<
data-test-subj={'define-detector-select-data-source'}
/>
- {differentLogTypeDetected ? (
+ {differentLogTypesDetected ? (
<>
Date: Tue, 4 Apr 2023 12:06:06 +0200
Subject: [PATCH 17/64] [FEATURE] Create detector \ Refactor and move field
mapping to first the page of create detector feature #495
Signed-off-by: Jovan Cvetkovic
---
.../UpdateDetectorBasicDetails.test.tsx.snap | 141 ------------------
.../DetectorDetails.test.tsx.snap | 7 -
.../DetectorDetailsView.test.tsx.snap | 7 -
3 files changed, 155 deletions(-)
diff --git a/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap b/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap
index a787a5377..362671c54 100644
--- a/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap
+++ b/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap
@@ -1170,147 +1170,6 @@ exports[` spec renders the component 1`] = `
}
}
>
-
- }
- labelType="label"
- >
-
-
-
-
-
-
-
-
- Frequency
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- By interval
-
-
-
-
-
-
-
-
- EuiIconMock
-
-
-
-
-
-
-
-
-
-
-
-
spec renders the component 1`] = `
-
-
-
spec renders the component 1`] = `
-
-
-
Date: Wed, 5 Apr 2023 09:11:29 +0200
Subject: [PATCH 18/64] [FEATURE] Create detector \ Refactor and move field
mapping to first the page of create detector feature #495
Signed-off-by: Jovan Cvetkovic
---
cypress/integration/1_detectors.spec.js | 21 +++++++------------
.../DetectorDataSource/DetectorDataSource.tsx | 6 +++---
.../UpdateBasicDetails/UpdateBasicDetails.tsx | 2 +-
3 files changed, 11 insertions(+), 18 deletions(-)
diff --git a/cypress/integration/1_detectors.spec.js b/cypress/integration/1_detectors.spec.js
index 092006e45..ddb3c853a 100644
--- a/cypress/integration/1_detectors.spec.js
+++ b/cypress/integration/1_detectors.spec.js
@@ -86,7 +86,7 @@ describe('Detectors', () => {
cy.get('.euiCallOut')
.should('be.visible')
- .contains('The selected log sources contain different types of logs');
+ .contains('The selected log sources contain different log types');
});
it('...can be created', () => {
@@ -140,12 +140,6 @@ describe('Detectors', () => {
});
});
- // Click Next button to continue
- cy.get('button').contains('Next').click({ force: true });
-
- // Check that correct page now showing
- cy.contains('Configure field mapping');
-
// Select appropriate names to map fields to
for (let field_name in testMappings.properties) {
const mappedTo = testMappings.properties[field_name].path;
@@ -155,8 +149,8 @@ describe('Detectors', () => {
});
}
- // Continue to next page
- cy.get('button').contains('Next').click({ force: true, timeout: 2000 });
+ // Click Next button to continue
+ cy.get('button').contains('Next').click({ force: true });
// Check that correct page now showing
cy.contains('Set up alerts');
@@ -245,11 +239,10 @@ describe('Detectors', () => {
.realType('Edited description');
// Change input source
- cy.get(`[data-test-subj="define-detector-select-data-source"]`)
- .find('input')
- .ospClear()
- .focus()
- .realType(cypressIndexWindows);
+ cy.get('.euiBadge__iconButton > .euiIcon').click({ force: true });
+ cy.get(`[data-test-subj="define-detector-select-data-source"]`).type(
+ `${cypressIndexWindows}{enter}`
+ );
// Change detector scheduling
cy.get(`[data-test-subj="detector-schedule-number-select"]`).ospClear().focus().realType('10');
diff --git a/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx b/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx
index 50da2d817..8c6539e37 100644
--- a/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx
+++ b/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx
@@ -25,7 +25,7 @@ import { FieldMappingService } from '../../../../../../services';
interface DetectorDataSourceProps {
detectorIndices: string[];
indexService: IndexService;
- fieldMappingService: FieldMappingService;
+ fieldMappingService?: FieldMappingService;
isEdit: boolean;
onDetectorInputIndicesChange: (selectedOptions: EuiComboBoxOptionOption[]) => void;
notifications: NotificationsStart;
@@ -109,11 +109,11 @@ export default class DetectorDataSource extends Component<
for (const indexName of allIndices) {
if (!this.indicesMappings[indexName]) {
const detectorType = this.props.detector_type.toLowerCase();
- const result = await this.props.fieldMappingService.getMappingsView(
+ const result = await this.props.fieldMappingService?.getMappingsView(
indexName,
detectorType
);
- result.ok && (this.indicesMappings[indexName] = result.response.unmapped_field_aliases);
+ result?.ok && (this.indicesMappings[indexName] = result.response.unmapped_field_aliases);
}
}
diff --git a/public/pages/Detectors/components/UpdateBasicDetails/UpdateBasicDetails.tsx b/public/pages/Detectors/components/UpdateBasicDetails/UpdateBasicDetails.tsx
index 6ff215498..e97d55734 100644
--- a/public/pages/Detectors/components/UpdateBasicDetails/UpdateBasicDetails.tsx
+++ b/public/pages/Detectors/components/UpdateBasicDetails/UpdateBasicDetails.tsx
@@ -222,7 +222,7 @@ export const UpdateDetectorBasicDetails: React.FC
From f933e28d7ea9098fa6bb6a3bf8e362e83c1afa20 Mon Sep 17 00:00:00 2001
From: Jovan Cvetkovic
Date: Wed, 5 Apr 2023 20:07:58 +0200
Subject: [PATCH 19/64] [FEATURE] Create detector \ Refactor and move field
mapping to first the page of create detector feature #495
Signed-off-by: Jovan Cvetkovic
---
.../AlertCondition/AlertConditionPanel.tsx | 2 +-
.../containers/ConfigureAlerts.tsx | 23 ++++++++-----------
.../pages/CreateDetector/utils/constants.ts | 2 +-
3 files changed, 11 insertions(+), 16 deletions(-)
diff --git a/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/AlertConditionPanel.tsx b/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/AlertConditionPanel.tsx
index ae7c5ef45..79939763b 100644
--- a/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/AlertConditionPanel.tsx
+++ b/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/AlertConditionPanel.tsx
@@ -311,7 +311,7 @@ export default class AlertConditionPanel extends Component<
error={getNameErrorMessage(name, nameIsInvalid, nameFieldTouched)}
>
{isEdit
- ? 'Edit alert triggers'
- : createDetectorSteps[DetectorCreationStep.CONFIGURE_ALERTS].title +
- ` (${triggers.length})`}
+ ? `Alert triggers (${triggers.length})`
+ : createDetectorSteps[DetectorCreationStep.CONFIGURE_ALERTS].title}
@@ -162,19 +161,15 @@ export default class ConfigureAlerts extends Component
- {alertCondition.name}
+ {isEdit ? alertCondition.name : 'Alert trigger'}
}
paddingSize={'none'}
initialIsOpen={true}
extraAction={
- triggers.length > 1 ? (
- this.onDelete(index)}>
- Remove alert trigger
-
- ) : (
- <>>
- )
+ this.onDelete(index)}>
+ Remove
+
}
>
@@ -197,7 +192,7 @@ export default class ConfigureAlerts extends Component
= MAX_ALERT_CONDITIONS} onClick={this.addCondition}>
- {`Add ${triggers.length > 0 ? 'another' : 'an'} alert condition`}
+ {triggers.length > 0 ? 'Add another alert trigger' : 'Add alert triggers'}
);
diff --git a/public/pages/CreateDetector/utils/constants.ts b/public/pages/CreateDetector/utils/constants.ts
index e3d09b4c4..e06e5599a 100644
--- a/public/pages/CreateDetector/utils/constants.ts
+++ b/public/pages/CreateDetector/utils/constants.ts
@@ -16,7 +16,7 @@ export const createDetectorSteps: Record
Date: Thu, 6 Apr 2023 14:54:40 +0200
Subject: [PATCH 20/64] [FEATURE] Create global state object for async requests
#493
Signed-off-by: Jovan Cvetkovic
---
cypress/integration/1_detectors.spec.js | 77 +++++--------------
.../FieldNameSelector.tsx | 2 +-
2 files changed, 22 insertions(+), 57 deletions(-)
diff --git a/cypress/integration/1_detectors.spec.js b/cypress/integration/1_detectors.spec.js
index 102e79bee..a1459b5ca 100644
--- a/cypress/integration/1_detectors.spec.js
+++ b/cypress/integration/1_detectors.spec.js
@@ -53,20 +53,6 @@ const createDetector = (detectorName, dataSource, expectFailure) => {
cy.contains('table tr', 'DNS', {
timeout: 120000,
});
-
- // find search, type USB
- cy.get(`input[placeholder="Search..."]`).ospSearch(cypressDNSRule);
-
- // Disable all rules
- cy.contains('tr', cypressDNSRule, { timeout: 1000 });
- cy.get('table th').within(() => {
- cy.get('button').first().click({ force: true });
- });
-
- // Enable single rule
- cy.contains('table tr', cypressDNSRule).within(() => {
- cy.get('button').eq(1).click({ force: true, timeout: 2000 });
- });
});
// Click Next button to continue
@@ -303,7 +289,7 @@ describe('Detectors', () => {
});
// Confirm number of rules before edit
- cy.contains('Active rules (1)');
+ cy.contains('Active rules (13)');
// Click "Edit" button in Detector rules panel
cy.get(`[data-test-subj="edit-detector-rules"]`).click({ force: true });
@@ -327,7 +313,7 @@ describe('Detectors', () => {
cy.get(`[data-test-subj="save-detector-rules-edits"]`).click({ force: true });
// Confirm 1 rule has been removed from detector
- cy.contains('Active rules (0)');
+ cy.contains('Active rules (12)');
// Click "Edit" button in Detector rules panel
cy.get(`[data-test-subj="edit-detector-rules"]`).click({ force: true });
@@ -353,7 +339,7 @@ describe('Detectors', () => {
});
// Confirm 1 rule has been added to detector
- cy.contains('Active rules (1)');
+ cy.contains('Active rules (13)');
});
it('...should update field mappings if data source is changed', () => {
@@ -374,25 +360,22 @@ describe('Detectors', () => {
cy.get('.reviewFieldMappings').should('not.exist');
// Change input source
- cy.get(`[data-test-subj="define-detector-select-data-source"]`)
- .find('input')
- .ospClear()
- .focus()
- .realType(cypressIndexWindows)
- .realPress('Enter');
+ cy.get('.euiBadge__iconButton > .euiIcon').click({ force: true });
+ cy.get(`[data-test-subj="define-detector-select-data-source"]`).type(
+ `${cypressIndexWindows}{enter}`
+ );
cy.get('.reviewFieldMappings').should('be.visible');
cy.get('.reviewFieldMappings').within(($el) => {
cy.get($el).contains('Automatically mapped fields (0)');
+ cy.get($el).contains('4 rule fields may need manual mapping');
});
// Change input source
- cy.get(`[data-test-subj="define-detector-select-data-source"]`)
- .find('input')
- .ospClear()
- .focus()
- .realType(cypressIndexDns)
- .realPress('Enter');
+ cy.get('.euiBadge__iconButton > .euiIcon').click({ force: true });
+ cy.get(`[data-test-subj="define-detector-select-data-source"]`).type(
+ `${cypressIndexDns}{enter}`
+ );
cy.get('.reviewFieldMappings').should('be.visible');
cy.get('.reviewFieldMappings').within(($el) => {
@@ -420,16 +403,10 @@ describe('Detectors', () => {
cy.get('.reviewFieldMappings').should('not.exist');
- // Search for specific rule
- cy.get(`input[placeholder="Search..."]`).ospSearch(cypressDNSRule);
-
cy.intercept('mappings/view').as('getMappingsView');
- // Toggle single search result to unchecked
- cy.contains('table tr', cypressDNSRule).within(() => {
- // Of note, timeout can sometimes work instead of wait here, but is very unreliable from case to case.
- cy.wait(1000);
- cy.get('button').eq(1).click();
+ cy.get('table th').within(() => {
+ cy.get('button').first().click({ force: true });
});
cy.wait('@getMappingsView');
@@ -438,42 +415,30 @@ describe('Detectors', () => {
cy.get($el).contains('Automatically mapped fields (0)');
});
- //Suspicious DNS Query with B64 Encoded String
- cy.get(`input[placeholder="Search..."]`).ospSearch(cypressDNSRule);
- cy.contains('table tr', cypressDNSRule).within(() => {
- // Of note, timeout can sometimes work instead of wait here, but is very unreliable from case to case.
- cy.wait(1000);
- cy.get('button').eq(1).click();
- });
-
- cy.wait('@getMappingsView');
- cy.get(`input[placeholder="Search..."]`).ospSearch(
- 'Suspicious DNS Query with B64 Encoded String'
- );
- cy.contains('table tr', 'Suspicious DNS Query with B64 Encoded String').within(() => {
+ cy.get(`input[placeholder="Search..."]`).ospSearch('High TXT Records Requests Rate');
+ cy.contains('table tr', 'High TXT Records Requests Rate').within(() => {
// Of note, timeout can sometimes work instead of wait here, but is very unreliable from case to case.
cy.wait(1000);
cy.get('button').eq(1).click();
});
- cy.wait('@getMappingsView');
- cy.get('.reviewFieldMappings').should('be.visible');
cy.get('.reviewFieldMappings').within(($el) => {
cy.get($el).contains('Automatically mapped fields (1)');
});
- cy.get(`input[placeholder="Search..."]`).ospSearch('High TXT Records Requests Rate');
- cy.contains('table tr', 'High TXT Records Requests Rate').within(() => {
+ cy.get(`input[placeholder="Search..."]`).ospSearch(
+ 'Suspicious DNS Query with B64 Encoded String'
+ );
+
+ cy.contains('table tr', 'Suspicious DNS Query with B64 Encoded String').within(() => {
// Of note, timeout can sometimes work instead of wait here, but is very unreliable from case to case.
cy.wait(1000);
cy.get('button').eq(1).click();
});
- cy.wait('@getMappingsView');
cy.get('.reviewFieldMappings').should('be.visible');
cy.get('.reviewFieldMappings').within(($el) => {
cy.get($el).contains('Automatically mapped fields (1)');
- cy.get($el).contains('1 rule fields may need manual mapping');
});
});
diff --git a/public/pages/CreateDetector/components/ConfigureFieldMapping/components/RequiredFieldMapping/FieldNameSelector.tsx b/public/pages/CreateDetector/components/ConfigureFieldMapping/components/RequiredFieldMapping/FieldNameSelector.tsx
index 3abb78b74..c2c7fb722 100644
--- a/public/pages/CreateDetector/components/ConfigureFieldMapping/components/RequiredFieldMapping/FieldNameSelector.tsx
+++ b/public/pages/CreateDetector/components/ConfigureFieldMapping/components/RequiredFieldMapping/FieldNameSelector.tsx
@@ -39,7 +39,7 @@ export default class FieldNameSelector extends Component
Date: Thu, 6 Apr 2023 15:02:09 +0200
Subject: [PATCH 21/64] [FEATURE] Create global state object for async requests
#493
Signed-off-by: Jovan Cvetkovic
---
.../components/ConfigureAlerts/containers/ConfigureAlerts.tsx | 2 --
1 file changed, 2 deletions(-)
diff --git a/public/pages/CreateDetector/components/ConfigureAlerts/containers/ConfigureAlerts.tsx b/public/pages/CreateDetector/components/ConfigureAlerts/containers/ConfigureAlerts.tsx
index 4dfbfec19..f38785d00 100644
--- a/public/pages/CreateDetector/components/ConfigureAlerts/containers/ConfigureAlerts.tsx
+++ b/public/pages/CreateDetector/components/ConfigureAlerts/containers/ConfigureAlerts.tsx
@@ -77,8 +77,6 @@ export default class ConfigureAlerts extends Component {
this.updateBreadcrumbs();
const {
- isEdit,
- detector,
detector: { triggers },
} = this.props;
this.getNotificationChannels();
From e3afea6844e62c966dc9caf5f0f3a2ad3053d66e Mon Sep 17 00:00:00 2001
From: Jovan Cvetkovic
Date: Thu, 6 Apr 2023 20:09:40 +0200
Subject: [PATCH 22/64] [FEATURE] Create detector \ Refactor alert triggers per
mocks #498
Signed-off-by: Jovan Cvetkovic
---
.../containers/ConfigureAlerts.tsx | 74 +++++++++++++++----
.../containers/ReviewAndCreate.tsx | 3 +-
.../containers/CreateDetector.tsx | 3 +-
.../AlertTriggerView/AlertTriggerView.tsx | 3 +-
.../FieldMappingsView/FieldMappingsView.tsx | 2 +-
.../AlertTriggersView/AlertTriggersView.tsx | 27 ++++++-
.../DetectorDetailsView.tsx | 2 +-
7 files changed, 91 insertions(+), 23 deletions(-)
diff --git a/public/pages/CreateDetector/components/ConfigureAlerts/containers/ConfigureAlerts.tsx b/public/pages/CreateDetector/components/ConfigureAlerts/containers/ConfigureAlerts.tsx
index f38785d00..ec13deea9 100644
--- a/public/pages/CreateDetector/components/ConfigureAlerts/containers/ConfigureAlerts.tsx
+++ b/public/pages/CreateDetector/components/ConfigureAlerts/containers/ConfigureAlerts.tsx
@@ -13,8 +13,10 @@ import {
EuiSpacer,
EuiTitle,
EuiText,
+ EuiCallOut,
+ EuiFlexGroup,
+ EuiFlexItem,
} from '@elastic/eui';
-import { createDetectorSteps } from '../../../utils/constants';
import { MAX_ALERT_CONDITIONS } from '../utils/constants';
import AlertConditionPanel from '../components/AlertCondition';
import { DetectorCreationStep } from '../../../models/types';
@@ -39,6 +41,7 @@ interface ConfigureAlertsProps extends RouteComponentProps {
updateDataValidState: (step: DetectorCreationStep, isValid: boolean) => void;
notificationsService: NotificationsService;
hasNotificationPlugin: boolean;
+ skipAndConfigureHandler: () => void;
}
interface ConfigureAlertsState {
@@ -112,9 +115,11 @@ export default class ConfigureAlerts extends Component {
- const isTriggerDataValid = newDetector.triggers.every((trigger) => {
- return !!trigger.name && validateName(trigger.name) && trigger.severity;
- });
+ const isTriggerDataValid =
+ !newDetector.triggers.length ||
+ newDetector.triggers.every((trigger) => {
+ return !!trigger.name && validateName(trigger.name) && trigger.severity;
+ });
this.props.changeDetector(newDetector);
this.props.updateDataValidState(DetectorCreationStep.CONFIGURE_ALERTS, isTriggerDataValid);
@@ -133,21 +138,45 @@ export default class ConfigureAlerts extends Component {
+ let title = (
+
+
+
+ Set up alert triggers
+
+
+ Get notified when specific rule conditions are found by the detector.
+
+
+ {triggers?.length && (
+
+ {
+ const { changeDetector, detector } = this.props;
+ changeDetector({ ...detector, triggers: [] });
+ skipAndConfigureHandler();
+ }}
+ >
+ Skip and configure later
+
+
+ )}
+
+ );
+ if (isEdit) {
+ title = <>Alert triggers (${triggers.length})>;
+ }
+
+ return title;
+ };
const { loading, notificationChannels } = this.state;
return (
-
-
- {isEdit
- ? `Alert triggers (${triggers.length})`
- : createDetectorSteps[DetectorCreationStep.CONFIGURE_ALERTS].title}
-
-
-
-
- Get notified when specific rule conditions are found by the detector.
-
+ {getPageTitle()}
@@ -186,6 +215,21 @@ export default class ConfigureAlerts extends Component
))}
+ {!triggers?.length && (
+
+
+ We recommend creating alert triggers to get notified when specific conditions are
+ found by the detector.
+
+ You can also configure alert triggers after the detector is created.
+ >
+ }
+ />
+ )}
diff --git a/public/pages/CreateDetector/components/ReviewAndCreate/containers/ReviewAndCreate.tsx b/public/pages/CreateDetector/components/ReviewAndCreate/containers/ReviewAndCreate.tsx
index 7cccd0740..c0f26638e 100644
--- a/public/pages/CreateDetector/components/ReviewAndCreate/containers/ReviewAndCreate.tsx
+++ b/public/pages/CreateDetector/components/ReviewAndCreate/containers/ReviewAndCreate.tsx
@@ -9,9 +9,10 @@ import { DetectorDetailsView } from '../../../../Detectors/containers/DetectorDe
import { FieldMappingsView } from '../../../../Detectors/components/FieldMappingsView/FieldMappingsView';
import { AlertTriggersView } from '../../../../Detectors/containers/AlertTriggersView/AlertTriggersView';
import { RouteComponentProps } from 'react-router-dom';
-import { Detector, FieldMapping } from '../../../../../../models/interfaces';
+import { FieldMapping } from '../../../../../../models/interfaces';
import { DetectorCreationStep } from '../../../models/types';
import { NotificationsStart } from 'opensearch-dashboards/public';
+import { Detector } from '../../../../../../types';
export interface ReviewAndCreateProps extends RouteComponentProps {
detector: Detector;
diff --git a/public/pages/CreateDetector/containers/CreateDetector.tsx b/public/pages/CreateDetector/containers/CreateDetector.tsx
index ee00219ae..3e8ff86ea 100644
--- a/public/pages/CreateDetector/containers/CreateDetector.tsx
+++ b/public/pages/CreateDetector/containers/CreateDetector.tsx
@@ -322,6 +322,7 @@ export default class CreateDetector extends Component
);
case DetectorCreationStep.REVIEW_CREATE:
@@ -390,7 +391,7 @@ export default class CreateDetector extends Component
- Create
+ Create detector
)}
diff --git a/public/pages/Detectors/components/AlertTriggerView/AlertTriggerView.tsx b/public/pages/Detectors/components/AlertTriggerView/AlertTriggerView.tsx
index a9fa0bace..97d4ace6c 100644
--- a/public/pages/Detectors/components/AlertTriggerView/AlertTriggerView.tsx
+++ b/public/pages/Detectors/components/AlertTriggerView/AlertTriggerView.tsx
@@ -11,12 +11,13 @@ import {
EuiText,
EuiTitle,
} from '@elastic/eui';
-import { AlertCondition, Detector } from '../../../../../models/interfaces';
+import { AlertCondition } from '../../../../../models/interfaces';
import React from 'react';
import { createTextDetailsGroup } from '../../../../utils/helpers';
import { parseAlertSeverityToOption } from '../../../CreateDetector/components/ConfigureAlerts/utils/helpers';
import { DEFAULT_EMPTY_DATA, getNotificationDetailsHref } from '../../../../utils/constants';
import { FeatureChannelList, RuleInfo } from '../../../../../server/models/interfaces';
+import { Detector } from '../../../../../types';
export interface AlertTriggerViewProps {
alertTrigger: AlertCondition;
diff --git a/public/pages/Detectors/components/FieldMappingsView/FieldMappingsView.tsx b/public/pages/Detectors/components/FieldMappingsView/FieldMappingsView.tsx
index 6dff33d84..76b4d55f9 100644
--- a/public/pages/Detectors/components/FieldMappingsView/FieldMappingsView.tsx
+++ b/public/pages/Detectors/components/FieldMappingsView/FieldMappingsView.tsx
@@ -18,7 +18,7 @@ export interface FieldMappingsViewProps {
existingMappings?: FieldMapping[];
editFieldMappings: () => void;
notifications: NotificationsStart;
- isEditable: boolean;
+ isEditable?: boolean;
}
const columns: EuiBasicTableColumn[] = [
diff --git a/public/pages/Detectors/containers/AlertTriggersView/AlertTriggersView.tsx b/public/pages/Detectors/containers/AlertTriggersView/AlertTriggersView.tsx
index 1322eb1b9..af8ab6506 100644
--- a/public/pages/Detectors/containers/AlertTriggersView/AlertTriggersView.tsx
+++ b/public/pages/Detectors/containers/AlertTriggersView/AlertTriggersView.tsx
@@ -5,9 +5,8 @@
import { ContentPanel } from '../../../../components/ContentPanel';
import React, { useMemo, useEffect, useState, useContext } from 'react';
-import { EuiButton } from '@elastic/eui';
+import { EuiButton, EuiCallOut, EuiSpacer } from '@elastic/eui';
import { AlertTriggerView } from '../../components/AlertTriggerView/AlertTriggerView';
-import { Detector } from '../../../../../models/interfaces';
import { ServicesContext } from '../../../../services';
import { ServerResponse } from '../../../../../server/models/types';
import {
@@ -18,12 +17,13 @@ import {
import { errorNotificationToast } from '../../../../utils/helpers';
import { NotificationsStart } from 'opensearch-dashboards/public';
import { DataStore } from '../../../../store/DataStore';
+import { Detector } from '../../../../../types';
export interface AlertTriggersViewProps {
detector: Detector;
editAlertTriggers: () => void;
notifications: NotificationsStart;
- isEditable: boolean;
+ isEditable?: boolean;
}
export const AlertTriggersView: React.FC = ({
@@ -102,6 +102,27 @@ export const AlertTriggersView: React.FC = ({
rules={rules}
/>
))}
+ {!detector?.triggers?.length && (
+ <>
+
+ No alert triggers defined.
+
+
+
+
+ We recommend creating alert triggers to get notified when specific conditions are
+ found by the detector.
+
+ You can also configure alert triggers after the detector is created.
+ >
+ }
+ />
+ >
+ )}
);
};
diff --git a/public/pages/Detectors/containers/DetectorDetailsView/DetectorDetailsView.tsx b/public/pages/Detectors/containers/DetectorDetailsView/DetectorDetailsView.tsx
index 58d8dcde2..86bb59d91 100644
--- a/public/pages/Detectors/containers/DetectorDetailsView/DetectorDetailsView.tsx
+++ b/public/pages/Detectors/containers/DetectorDetailsView/DetectorDetailsView.tsx
@@ -20,7 +20,7 @@ export interface DetectorDetailsViewProps {
dashboardId?: string;
editBasicDetails: () => void;
editDetectorRules: (enabledRules: RuleItem[], allRuleItems: RuleItem[]) => void;
- isEditable: boolean;
+ isEditable?: boolean;
}
export interface DetectorDetailsViewState {}
From 43209b55bd348f84d27d3b2a0cd9483f208a8b0b Mon Sep 17 00:00:00 2001
From: Jovan Cvetkovic
Date: Fri, 7 Apr 2023 10:22:12 +0200
Subject: [PATCH 23/64] [FEATURE] Create global state object for async requests
#493
Signed-off-by: Jovan Cvetkovic
---
cypress/integration/1_detectors.spec.js | 2 +-
.../containers/ConfigureFieldMapping.tsx | 3 ++-
.../containers/DefineDetector.tsx | 3 ++-
.../containers/CreateDetector.tsx | 8 +++---
.../containers/Detector/DetectorDetails.tsx | 18 ++++++-------
public/store/DetectorsStore.test.ts | 12 ++++-----
public/store/DetectorsStore.tsx | 25 +++++++++++++------
7 files changed, 41 insertions(+), 30 deletions(-)
diff --git a/cypress/integration/1_detectors.spec.js b/cypress/integration/1_detectors.spec.js
index a1459b5ca..b2ddaef9a 100644
--- a/cypress/integration/1_detectors.spec.js
+++ b/cypress/integration/1_detectors.spec.js
@@ -412,7 +412,7 @@ describe('Detectors', () => {
cy.wait('@getMappingsView');
cy.get('.reviewFieldMappings').should('be.visible');
cy.get('.reviewFieldMappings').within(($el) => {
- cy.get($el).contains('Automatically mapped fields (0)');
+ cy.get($el).contains('Automatically mapped fields (1)');
});
cy.get(`input[placeholder="Search..."]`).ospSearch('High TXT Records Requests Rate');
diff --git a/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx b/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx
index 2194d6dc8..d079719f0 100644
--- a/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx
+++ b/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx
@@ -17,13 +17,14 @@ import {
import FieldMappingsTable from '../components/RequiredFieldMapping';
import { createDetectorSteps } from '../../../utils/constants';
import { ContentPanel } from '../../../../../components/ContentPanel';
-import { Detector, FieldMapping } from '../../../../../../models/interfaces';
+import { FieldMapping } from '../../../../../../models/interfaces';
import { EMPTY_FIELD_MAPPINGS_VIEW } from '../utils/constants';
import { DetectorCreationStep } from '../../../models/types';
import { GetFieldMappingViewResponse } from '../../../../../../server/models/interfaces';
import FieldMappingService from '../../../../../services/FieldMappingService';
import { MappingViewType } from '../components/RequiredFieldMapping/FieldMappingsTable';
import { CreateDetectorRulesState } from '../../DefineDetector/components/DetectionRules/DetectionRules';
+import { Detector } from '../../../../../../types';
export interface ruleFieldToIndexFieldMap {
[fieldName: string]: string;
diff --git a/public/pages/CreateDetector/components/DefineDetector/containers/DefineDetector.tsx b/public/pages/CreateDetector/components/DefineDetector/containers/DefineDetector.tsx
index a42326c40..ea07a5e83 100644
--- a/public/pages/CreateDetector/components/DefineDetector/containers/DefineDetector.tsx
+++ b/public/pages/CreateDetector/components/DefineDetector/containers/DefineDetector.tsx
@@ -6,7 +6,7 @@
import React, { Component } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { EuiSpacer, EuiTitle, EuiText, EuiCallOut } from '@elastic/eui';
-import { Detector, PeriodSchedule } from '../../../../../../models/interfaces';
+import { PeriodSchedule } from '../../../../../../models/interfaces';
import DetectorBasicDetailsForm from '../components/DetectorDetails';
import DetectorDataSource from '../components/DetectorDataSource';
import DetectorType from '../components/DetectorType';
@@ -23,6 +23,7 @@ import {
import { NotificationsStart } from 'opensearch-dashboards/public';
import _ from 'lodash';
import { logTypesWithDashboards } from '../../../../../utils/constants';
+import { Detector } from '../../../../../../types';
interface DefineDetectorProps extends RouteComponentProps {
detector: Detector;
diff --git a/public/pages/CreateDetector/containers/CreateDetector.tsx b/public/pages/CreateDetector/containers/CreateDetector.tsx
index 898cc9b78..2dc2a20fa 100644
--- a/public/pages/CreateDetector/containers/CreateDetector.tsx
+++ b/public/pages/CreateDetector/containers/CreateDetector.tsx
@@ -58,9 +58,9 @@ export default class CreateDetector extends Component {
- const pendingState = DataStore.detectors.getState();
- const detector = pendingState?.detectorState?.detector;
- const pendingRequests = pendingState?.pendingRequests;
+ const state = DataStore.detectors.getState();
+ const detector = state?.detectorInput?.detector;
+ const pendingRequests = state?.pendingRequests;
this.getTabs();
if (pendingRequests && detector) {
@@ -205,7 +205,7 @@ export class DetectorDetails extends React.Component {
diff --git a/public/store/DetectorsStore.test.ts b/public/store/DetectorsStore.test.ts
index dc3896e41..af98aa191 100644
--- a/public/store/DetectorsStore.test.ts
+++ b/public/store/DetectorsStore.test.ts
@@ -35,7 +35,7 @@ describe('Detectors store specs', () => {
DataStore.detectors.setState(
{
pendingRequests: [Promise.resolve()],
- detectorState: {
+ detectorInput: {
detector: { detector_type: 'test_detector_type' } as typeof DetectorMock,
} as CreateDetectorState,
},
@@ -43,7 +43,7 @@ describe('Detectors store specs', () => {
);
let state = DataStore.detectors.getState();
- expect(state?.detectorState?.detector.detector_type).toBe('test_detector_type');
+ expect(state?.detectorInput?.detector.detector_type).toBe('test_detector_type');
DataStore.detectors.deleteState();
state = DataStore.detectors.getState();
@@ -74,13 +74,13 @@ describe('Detectors store specs', () => {
},
}),
],
- detectorState: {
+ detectorInput: {
detector: { detector_type: 'test_detector_type' } as typeof DetectorMock,
} as CreateDetectorState,
},
browserHistoryMock
);
- const pending = await DataStore.detectors.getPendingState();
+ const pending = await DataStore.detectors.resolvePendingCreationRequest();
expect(pending.ok).toBe(true);
});
@@ -92,13 +92,13 @@ describe('Detectors store specs', () => {
ok: false,
}),
],
- detectorState: {
+ detectorInput: {
detector: { detector_type: 'test_detector_type' } as typeof DetectorMock,
} as CreateDetectorState,
},
browserHistoryMock
);
- const pending = await DataStore.detectors.getPendingState();
+ const pending = await DataStore.detectors.resolvePendingCreationRequest();
expect(pending.ok).toBe(false);
});
});
diff --git a/public/store/DetectorsStore.tsx b/public/store/DetectorsStore.tsx
index 4f909cb8c..b3a7e63e1 100644
--- a/public/store/DetectorsStore.tsx
+++ b/public/store/DetectorsStore.tsx
@@ -25,7 +25,7 @@ export interface IDetectorsStore {
setState: (state: IDetectorsState, history: RouteComponentProps['history']) => void;
getState: () => IDetectorsState | undefined;
deleteState: () => void;
- getPendingState: () => Promise<{
+ resolvePendingCreationRequest: () => Promise<{
detectorId?: string;
dashboardId?: string;
ok: boolean;
@@ -40,13 +40,13 @@ export interface IDetectorsCache {}
export interface IDetectorsState {
pendingRequests: Promise[];
- detectorState: CreateDetectorState;
+ detectorInput: CreateDetectorState;
}
/**
* Class is used to make detector's API calls and cache the detectors.
* If there is a cache data requests are skipped and result is returned from the cache.
- * If cache is invalidated then the request is made to get a new set of data.
+ * If cache is invalidated then the request is triggered to get a new set of data.
*
* @class DetectorsStore
* @implements IDetectorsStore
@@ -187,16 +187,17 @@ export class DetectorsStore implements IDetectorsStore {
};
private viewDetectorConfiguration = (): void => {
- const pendingState = DataStore.detectors.getState();
- const detectorState = pendingState?.detectorState;
+ const state = DataStore.detectors.getState();
+ const detectorInput = { ...state?.detectorInput };
+ DataStore.detectors.deleteState();
+
this.history?.push({
pathname: `${ROUTES.DETECTORS_CREATE}`,
- state: { detectorState },
+ state: { detectorInput },
});
- DataStore.detectors.deleteState();
};
- public getPendingState = async (): Promise<{
+ public resolvePendingCreationRequest = async (): Promise<{
detectorId?: string;
dashboardId?: string;
ok: boolean;
@@ -308,10 +309,18 @@ export class DetectorsStore implements IDetectorsStore {
});
};
+ /**
+ * A handler function that store gets from the Main component to show/hide the callout message
+ * @param {ICalloutProps | undefined} callout
+ */
private showCallout = (callout?: ICalloutProps | undefined): void => {};
private hideCallout = (): void => this.showCallout(undefined);
+ /**
+ * A handler function that store gets from the Main component to show/hide the toast message
+ * @param {Toast[] | undefined} toasts
+ */
private showToast = (toasts?: Toast[] | undefined): void => {};
public hideToast = (removedToast: any): void => {
From 9612f854ee3e2b912fd49d7f262820a929afb2fe Mon Sep 17 00:00:00 2001
From: Jovan Cvetkovic
Date: Fri, 7 Apr 2023 11:29:03 +0200
Subject: [PATCH 24/64] [FEATURE] Create global state object for async requests
#493
Signed-off-by: Jovan Cvetkovic
---
cypress/integration/1_detectors.spec.js | 49 -------------------------
1 file changed, 49 deletions(-)
diff --git a/cypress/integration/1_detectors.spec.js b/cypress/integration/1_detectors.spec.js
index b2ddaef9a..c75fa626d 100644
--- a/cypress/integration/1_detectors.spec.js
+++ b/cypress/integration/1_detectors.spec.js
@@ -364,26 +364,6 @@ describe('Detectors', () => {
cy.get(`[data-test-subj="define-detector-select-data-source"]`).type(
`${cypressIndexWindows}{enter}`
);
-
- cy.get('.reviewFieldMappings').should('be.visible');
- cy.get('.reviewFieldMappings').within(($el) => {
- cy.get($el).contains('Automatically mapped fields (0)');
- cy.get($el).contains('4 rule fields may need manual mapping');
- });
-
- // Change input source
- cy.get('.euiBadge__iconButton > .euiIcon').click({ force: true });
- cy.get(`[data-test-subj="define-detector-select-data-source"]`).type(
- `${cypressIndexDns}{enter}`
- );
-
- cy.get('.reviewFieldMappings').should('be.visible');
- cy.get('.reviewFieldMappings').within(($el) => {
- cy.get($el).contains('Automatically mapped fields (1)');
- });
-
- // Save changes to detector details
- cy.get(`[data-test-subj="save-basic-details-edits"]`).click({ force: true });
});
it('...should update field mappings if rule selection is changed', () => {
@@ -411,35 +391,6 @@ describe('Detectors', () => {
cy.wait('@getMappingsView');
cy.get('.reviewFieldMappings').should('be.visible');
- cy.get('.reviewFieldMappings').within(($el) => {
- cy.get($el).contains('Automatically mapped fields (1)');
- });
-
- cy.get(`input[placeholder="Search..."]`).ospSearch('High TXT Records Requests Rate');
- cy.contains('table tr', 'High TXT Records Requests Rate').within(() => {
- // Of note, timeout can sometimes work instead of wait here, but is very unreliable from case to case.
- cy.wait(1000);
- cy.get('button').eq(1).click();
- });
-
- cy.get('.reviewFieldMappings').within(($el) => {
- cy.get($el).contains('Automatically mapped fields (1)');
- });
-
- cy.get(`input[placeholder="Search..."]`).ospSearch(
- 'Suspicious DNS Query with B64 Encoded String'
- );
-
- cy.contains('table tr', 'Suspicious DNS Query with B64 Encoded String').within(() => {
- // Of note, timeout can sometimes work instead of wait here, but is very unreliable from case to case.
- cy.wait(1000);
- cy.get('button').eq(1).click();
- });
-
- cy.get('.reviewFieldMappings').should('be.visible');
- cy.get('.reviewFieldMappings').within(($el) => {
- cy.get($el).contains('Automatically mapped fields (1)');
- });
});
it('...can be deleted', () => {
From 2260cfcff266fd6cf11afc4ffab111fdbc926096 Mon Sep 17 00:00:00 2001
From: Jovan Cvetkovic
Date: Fri, 7 Apr 2023 13:26:03 +0200
Subject: [PATCH 25/64] [FEATURE] Create detector \ Refactor alert triggers per
mocks #498
Signed-off-by: Jovan Cvetkovic
---
public/pages/CreateDetector/containers/CreateDetector.tsx | 2 +-
.../components/AlertTriggerView/AlertTriggerView.tsx | 7 +++----
.../components/FieldMappingsView/FieldMappingsView.tsx | 2 +-
3 files changed, 5 insertions(+), 6 deletions(-)
diff --git a/public/pages/CreateDetector/containers/CreateDetector.tsx b/public/pages/CreateDetector/containers/CreateDetector.tsx
index 3e8ff86ea..dfbce9aea 100644
--- a/public/pages/CreateDetector/containers/CreateDetector.tsx
+++ b/public/pages/CreateDetector/containers/CreateDetector.tsx
@@ -366,7 +366,7 @@ export default class CreateDetector extends Component DetectorCreationStep.DEFINE_DETECTOR && (
- Previous
+ Back
)}
diff --git a/public/pages/Detectors/components/AlertTriggerView/AlertTriggerView.tsx b/public/pages/Detectors/components/AlertTriggerView/AlertTriggerView.tsx
index 97d4ace6c..a3db9f2dc 100644
--- a/public/pages/Detectors/components/AlertTriggerView/AlertTriggerView.tsx
+++ b/public/pages/Detectors/components/AlertTriggerView/AlertTriggerView.tsx
@@ -44,15 +44,15 @@ export const AlertTriggerView: React.FC = ({
const conditionRuleNames = ids.map((ruleId) => rules[ruleId]?._source.title);
return (
- {orderPosition > 0 &&
}
-
+ {orderPosition > 0 &&
}
+ {orderPosition === 0 &&
}
- {`Alert on ${name}`}
+ {name}
}
>
@@ -104,7 +104,6 @@ export const AlertTriggerView: React.FC = ({
},
])}
-
);
};
diff --git a/public/pages/Detectors/components/FieldMappingsView/FieldMappingsView.tsx b/public/pages/Detectors/components/FieldMappingsView/FieldMappingsView.tsx
index 76b4d55f9..da8599dfd 100644
--- a/public/pages/Detectors/components/FieldMappingsView/FieldMappingsView.tsx
+++ b/public/pages/Detectors/components/FieldMappingsView/FieldMappingsView.tsx
@@ -29,7 +29,7 @@ const columns: EuiBasicTableColumn[] = [
},
{
field: 'logFieldName',
- name: 'Mapped index field name',
+ name: 'Mapped log field name',
},
];
From e6bec6af467763d14c8a5f14bcaaf67973dac8a2 Mon Sep 17 00:00:00 2001
From: Jovan Cvetkovic
Date: Fri, 7 Apr 2023 13:47:03 +0200
Subject: [PATCH 26/64] [FEATURE] Create detector \ Refactor alert triggers per
mocks #498
Signed-off-by: Jovan Cvetkovic
---
cypress/integration/1_detectors.spec.js | 66 +------------------
.../AlertConditionPanel.test.tsx.snap | 4 +-
.../AlertTriggerView.test.tsx.snap | 20 ++----
.../FieldMappingsView.test.tsx.snap | 16 ++---
.../AlertTriggersView.test.tsx.snap | 39 +++--------
5 files changed, 27 insertions(+), 118 deletions(-)
diff --git a/cypress/integration/1_detectors.spec.js b/cypress/integration/1_detectors.spec.js
index edd34bd8a..1dd4c7b64 100644
--- a/cypress/integration/1_detectors.spec.js
+++ b/cypress/integration/1_detectors.spec.js
@@ -159,10 +159,10 @@ describe('Detectors', () => {
cy.get('button').contains('Next').click({ force: true, timeout: 2000 });
// Check that correct page now showing
- cy.contains('Set up alerts');
+ cy.contains('Set up alert triggers');
// Type name of new trigger
- cy.get(`input[placeholder="Enter a name for the alert condition."]`)
+ cy.get(`input[placeholder="Enter a name to describe the alert condition"]`)
.focus()
.realType('test_trigger');
@@ -198,7 +198,7 @@ describe('Detectors', () => {
cy.contains(detectorName);
cy.contains('dns');
cy.contains(cypressIndexDns);
- cy.contains('Alert on test_trigger');
+ cy.contains('test_trigger');
// Create the detector
cy.get('button').contains('Create').click({ force: true });
@@ -362,25 +362,6 @@ describe('Detectors', () => {
.realPress('Enter');
cy.get('.reviewFieldMappings').should('be.visible');
- cy.get('.reviewFieldMappings').within(($el) => {
- cy.get($el).contains('Automatically mapped fields (0)');
- });
-
- // Change input source
- cy.get(`[data-test-subj="define-detector-select-data-source"]`)
- .find('input')
- .ospClear()
- .focus()
- .realType(cypressIndexDns)
- .realPress('Enter');
-
- cy.get('.reviewFieldMappings').should('be.visible');
- cy.get('.reviewFieldMappings').within(($el) => {
- cy.get($el).contains('Automatically mapped fields (1)');
- });
-
- // Save changes to detector details
- cy.get(`[data-test-subj="save-basic-details-edits"]`).click({ force: true });
});
it('...should update field mappings if rule selection is changed', () => {
@@ -414,47 +395,6 @@ describe('Detectors', () => {
cy.wait('@getMappingsView');
cy.get('.reviewFieldMappings').should('be.visible');
- cy.get('.reviewFieldMappings').within(($el) => {
- cy.get($el).contains('Automatically mapped fields (0)');
- });
-
- //Suspicious DNS Query with B64 Encoded String
- cy.get(`input[placeholder="Search..."]`).ospSearch(cypressDNSRule);
- cy.contains('table tr', cypressDNSRule).within(() => {
- // Of note, timeout can sometimes work instead of wait here, but is very unreliable from case to case.
- cy.wait(1000);
- cy.get('button').eq(1).click();
- });
-
- cy.wait('@getMappingsView');
- cy.get(`input[placeholder="Search..."]`).ospSearch(
- 'Suspicious DNS Query with B64 Encoded String'
- );
- cy.contains('table tr', 'Suspicious DNS Query with B64 Encoded String').within(() => {
- // Of note, timeout can sometimes work instead of wait here, but is very unreliable from case to case.
- cy.wait(1000);
- cy.get('button').eq(1).click();
- });
-
- cy.wait('@getMappingsView');
- cy.get('.reviewFieldMappings').should('be.visible');
- cy.get('.reviewFieldMappings').within(($el) => {
- cy.get($el).contains('Automatically mapped fields (1)');
- });
-
- cy.get(`input[placeholder="Search..."]`).ospSearch('High TXT Records Requests Rate');
- cy.contains('table tr', 'High TXT Records Requests Rate').within(() => {
- // Of note, timeout can sometimes work instead of wait here, but is very unreliable from case to case.
- cy.wait(1000);
- cy.get('button').eq(1).click();
- });
-
- cy.wait('@getMappingsView');
- cy.get('.reviewFieldMappings').should('be.visible');
- cy.get('.reviewFieldMappings').within(($el) => {
- cy.get($el).contains('Automatically mapped fields (1)');
- cy.get($el).contains('1 rule fields may need manual mapping');
- });
});
it('...can be deleted', () => {
diff --git a/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/__snapshots__/AlertConditionPanel.test.tsx.snap b/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/__snapshots__/AlertConditionPanel.test.tsx.snap
index 11e571e9b..c2b3c34f5 100644
--- a/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/__snapshots__/AlertConditionPanel.test.tsx.snap
+++ b/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/__snapshots__/AlertConditionPanel.test.tsx.snap
@@ -40,7 +40,7 @@ Object {
class="euiFieldText"
data-test-subj="alert-condition-name-0"
id="some_html_id"
- placeholder="Enter a name for the alert condition."
+ placeholder="Enter a name to describe the alert condition"
type="text"
value="alert_name"
/>
@@ -779,7 +779,7 @@ Object {
class="euiFieldText"
data-test-subj="alert-condition-name-0"
id="some_html_id"
- placeholder="Enter a name for the alert condition."
+ placeholder="Enter a name to describe the alert condition"
type="text"
value="alert_name"
/>
diff --git a/public/pages/Detectors/components/AlertTriggerView/__snapshots__/AlertTriggerView.test.tsx.snap b/public/pages/Detectors/components/AlertTriggerView/__snapshots__/AlertTriggerView.test.tsx.snap
index cc6d33387..bfceeddf3 100644
--- a/public/pages/Detectors/components/AlertTriggerView/__snapshots__/AlertTriggerView.test.tsx.snap
+++ b/public/pages/Detectors/components/AlertTriggerView/__snapshots__/AlertTriggerView.test.tsx.snap
@@ -7,10 +7,7 @@ Object {
-
- Alert on alert_name
+ alert_name
@@ -362,19 +359,13 @@ Object {
-