change(ui): refactor alerts?

This commit is contained in:
sylenien 2023-01-06 13:14:39 +01:00 committed by Delirium
parent 0ad417d0dc
commit bf1fb4f680
27 changed files with 475 additions and 287 deletions

View file

@ -8,7 +8,6 @@ import { fetchUserInfo } from 'Duck/user';
import withSiteIdUpdater from 'HOCs/withSiteIdUpdater';
import Header from 'Components/Header/Header';
import { fetchList as fetchSiteList } from 'Duck/site';
import { fetchList as fetchAlerts } from 'Duck/alerts';
import { withStore } from 'App/mstore';
import APIClient from './api_client';
@ -114,7 +113,6 @@ const MULTIVIEW_INDEX_PATH = routes.multiviewIndex();
fetchTenants,
setSessionPath,
fetchSiteList,
fetchAlerts,
}
)
class Router extends React.Component {

View file

@ -1,13 +1,12 @@
import React, { useEffect } from 'react';
import { Button, Form, Input, SegmentSelection, Checkbox, Icon } from 'UI';
import { alertConditions as conditions } from 'App/constants';
import { client, CLIENT_TABS } from 'App/routes';
import { connect } from 'react-redux';
import stl from './alertForm.module.css';
import DropdownChips from './DropdownChips';
import { validateEmail } from 'App/validate';
import cn from 'classnames';
import { fetchTriggerOptions } from 'Duck/alerts';
import { useStore } from 'App/mstore'
import { observer } from 'mobx-react-lite'
import Select from 'Shared/Select';
const thresholdOptions = [
@ -44,26 +43,28 @@ const Section = ({ index, title, description, content }) => (
</div>
);
const integrationsRoute = client(CLIENT_TABS.INTEGRATIONS);
const AlertForm = (props) => {
const {
instance,
slackChannels,
msTeamsChannels,
webhooks,
loading,
onDelete,
deleting,
triggerOptions,
style = { width: '580px', height: '100vh' },
} = props;
const { alertsStore } = useStore()
const {
instance,
triggerOptions,
loading,
} = alertsStore
const deleting = loading
const write = ({ target: { value, name } }) => props.edit({ [name]: value });
const writeOption = (e, { name, value }) => props.edit({ [name]: value.value });
const onChangeCheck = ({ target: { checked, name } }) => props.edit({ [name]: checked });
useEffect(() => {
props.fetchTriggerOptions();
alertsStore.fetchTriggerOptions();
}, []);
const writeQueryOption = (e, { name, value }) => {
@ -378,12 +379,4 @@ const AlertForm = (props) => {
);
};
export default connect(
(state) => ({
instance: state.getIn(['alerts', 'instance']),
triggerOptions: state.getIn(['alerts', 'triggerOptions']),
loading: state.getIn(['alerts', 'saveRequest', 'loading']),
deleting: state.getIn(['alerts', 'removeRequest', 'loading']),
}),
{ fetchTriggerOptions }
)(AlertForm);
export default observer(AlertForm);

View file

@ -1,11 +1,12 @@
import React, { useEffect, useState } from 'react';
import { SlideModal, IconButton } from 'UI';
import { init, edit, save, remove } from 'Duck/alerts';
import { SlideModal } from 'UI';
import { useStore } from 'App/mstore'
import { observer } from 'mobx-react-lite'
import { fetchList as fetchWebhooks } from 'Duck/webhook';
import AlertForm from '../AlertForm';
import { connect } from 'react-redux';
import { setShowAlerts } from 'Duck/dashboard';
import { EMAIL, SLACK, WEBHOOK } from 'App/constants/schedule';
import { SLACK, WEBHOOK } from 'App/constants/schedule';
import { confirm } from 'UI';
interface Props {
@ -14,12 +15,9 @@ interface Props {
onClose?: () => void;
webhooks: any;
fetchWebhooks: Function;
save: Function;
remove: Function;
init: Function;
edit: Function;
}
function AlertFormModal(props: Props) {
const { alertsStore } = useStore()
const { metricId = null, showModal = false, webhooks } = props;
const [showForm, setShowForm] = useState(false);
@ -38,7 +36,7 @@ function AlertFormModal(props: Props) {
const saveAlert = (instance) => {
const wasUpdating = instance.exists();
props.save(instance).then(() => {
alertsStore.save(instance).then(() => {
if (!wasUpdating) {
toggleForm(null, false);
}
@ -56,7 +54,7 @@ function AlertFormModal(props: Props) {
confirmation: `Are you sure you want to permanently delete this alert?`,
})
) {
props.remove(instance.alertId).then(() => {
alertsStore.remove(instance.alertId).then(() => {
toggleForm(null, false);
});
}
@ -64,7 +62,7 @@ function AlertFormModal(props: Props) {
const toggleForm = (instance, state) => {
if (instance) {
props.init(instance);
alertsStore.init(instance);
}
return setShowForm(state ? state : !showForm);
};
@ -83,7 +81,7 @@ function AlertFormModal(props: Props) {
showModal && (
<AlertForm
metricId={metricId}
edit={props.edit}
edit={alertsStore.edit}
slackChannels={slackChannels}
webhooks={hooks}
onSubmit={saveAlert}
@ -100,7 +98,6 @@ function AlertFormModal(props: Props) {
export default connect(
(state) => ({
webhooks: state.getIn(['webhooks', 'list']),
instance: state.getIn(['alerts', 'instance']),
}),
{ init, edit, save, remove, fetchWebhooks, setShowAlerts }
)(AlertFormModal);
{ fetchWebhooks, setShowAlerts }
)(observer(AlertFormModal));

View file

@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import { Loader, NoContent, Input, Button } from 'UI';
import AlertItem from './AlertItem';
import { fetchList, init } from 'Duck/alerts';
import { fetchList } from 'Duck/alerts';
import { connect } from 'react-redux';
import { getRE } from 'App/utils';
@ -54,5 +54,5 @@ export default connect(
instance: state.getIn(['alerts', 'instance']),
loading: state.getIn(['alerts', 'loading']),
}),
{ fetchList, init }
{ fetchList }
)(AlertsList);

View file

@ -0,0 +1,18 @@
import React from 'react'
import stl from './Bar.module.css'
const Bar = ({ className = '', width = 0, avg, domain, color }) => {
return (
<div className={className}>
<div className="flex items-center">
<div className={stl.bar} style={{ width: `${width > 0 ? width : 5 }%`, backgroundColor: color }}></div>
<div className="ml-2">
<span className="font-medium">{`${avg}`}</span>
</div>
</div>
<div className="text-sm leading-3 color-gray-medium">{domain}</div>
</div>
)
}
export default Bar

View file

@ -2,7 +2,7 @@ import React from 'react';
import { NoContent } from 'UI';
import { Styles } from '../../common';
import { numberWithCommas } from 'App/utils';
import Bar from 'App/components/Dashboard/Widgets/ErrorsPerDomain/Bar';
import Bar from './Bar';
import { NO_METRIC_DATA } from 'App/constants/messages'
interface Props {

View file

@ -0,0 +1,6 @@
.bar {
height: 5px;
background-color: red;
width: 100%;
border-radius: 3px;
}

View file

@ -0,0 +1,6 @@
.bar {
height: 10px;
background-color: red;
width: 100%;
border-radius: 3px;
}

View file

@ -0,0 +1,19 @@
import React from 'react'
import stl from './Bar.module.css'
const Bar = ({ className = '', width = 0, avg, domain, color }) => {
return (
<div className={className}>
<div className="flex items-center">
<div className={stl.bar} style={{ width: `${width < 5 ? 5 : width }%`, backgroundColor: color }}></div>
<div className="ml-2 shrink-0">
<span className="font-medium">{avg}</span>
<span> ms</span>
</div>
</div>
<div className="text-sm leading-3">{domain}</div>
</div>
)
}
export default Bar

View file

@ -2,7 +2,7 @@ import React from 'react';
import { NoContent } from 'UI';
import { Styles } from '../../common';
import { numberWithCommas } from 'App/utils';
import Bar from 'App/components/Dashboard/Widgets/SlowestDomains/Bar';
import Bar from './Bar';
import { NO_METRIC_DATA } from 'App/constants/messages'
interface Props {

View file

@ -7,6 +7,7 @@ import { numberWithCommas } from 'App/utils';
import { DateTime } from 'luxon';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import cn from 'classnames';
import Alert from 'Types/alert';
const getThreshold = (threshold: number) => {
if (threshold === 15) return '15 Minutes';
@ -75,7 +76,7 @@ const getNotifyChannel = (alert: Record<string, any>, webhooks: Array<any>) => {
interface Props extends RouteComponentProps {
alert: Alert;
siteId: string;
init: (alert?: Alert) => void;
init: (alert: Alert) => void;
demo?: boolean;
webhooks: Array<any>;
}
@ -90,7 +91,7 @@ function AlertListItem(props: Props) {
const onItemClick = () => {
if (demo) return;
const path = withSiteId(alertEdit(alert.alertId), siteId);
init(alert);
init(alert || {});
history.push(path);
};
@ -117,9 +118,9 @@ function AlertListItem(props: Props) {
{demo
? DateTime.fromMillis(+new Date()).toFormat('LLL dd, yyyy, hh:mm a')
: checkForRecent(
DateTime.fromMillis(alert.createdAt || +new Date()),
'LLL dd, yyyy, hh:mm a'
)}
DateTime.fromMillis(alert.createdAt || +new Date()),
'LLL dd, yyyy, hh:mm a'
)}
</div>
</div>
<div className="color-gray-medium px-2 pb-2">
@ -133,11 +134,13 @@ function AlertListItem(props: Props) {
{numberWithCommas(alert.query.right)} {alert.metric.unit}
</span>
{' over the past '}
<span className="font-semibold" style={{ fontFamily: 'Menlo, Monaco, Consolas' }}>{getThreshold(alert.currentPeriod)}</span>
<span className="font-semibold" style={{ fontFamily: 'Menlo, Monaco, Consolas' }}>{getThreshold(
alert.currentPeriod)}</span>
{alert.detectionMethod === 'change' ? (
<>
{' compared to the previous '}
<span className="font-semibold" style={{ fontFamily: 'Menlo, Monaco, Consolas ' }}>{getThreshold(alert.previousPeriod)}</span>
<span className="font-semibold" style={{ fontFamily: 'Menlo, Monaco, Consolas ' }}>{getThreshold(
alert.previousPeriod)}</span>
</>
) : null}
{', notify me on '}

View file

@ -2,37 +2,36 @@ import React from 'react';
import { NoContent, Pagination, Icon } from 'UI';
import { filterList } from 'App/utils';
import { sliceListPerPage } from 'App/utils';
import { fetchList } from 'Duck/alerts';
import { connect } from 'react-redux';
import { fetchList as fetchWebhooks } from 'Duck/webhook';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import AlertListItem from './AlertListItem'
import { useStore } from 'App/mstore'
import { observer } from 'mobx-react-lite'
import Alert from 'Types/alert'
const pageSize = 10;
interface Props {
fetchList: () => void;
list: any;
alertsSearch: any;
siteId: string;
webhooks: Array<any>;
init: (instance?: Alert) => void
fetchWebhooks: () => void;
}
function AlertsList({ fetchList, list: alertsList, alertsSearch, siteId, init, fetchWebhooks, webhooks }: Props) {
React.useEffect(() => { fetchList(); fetchWebhooks() }, []);
function AlertsList({ siteId, fetchWebhooks, webhooks }: Props) {
const { alertsStore } = useStore();
const { alerts: alertsList, alertsSearch, fetchList, init } = alertsStore
const alertsArray = alertsList.toJS();
React.useEffect(() => { fetchList(); fetchWebhooks() }, []);
const alertsArray = alertsList
const [page, setPage] = React.useState(1);
const filteredAlerts = filterList(alertsArray, alertsSearch, ['name'], (item, query) => query.test(item.query.left))
const list = alertsSearch !== '' ? filteredAlerts : alertsArray;
const lenth = list.length;
return (
<NoContent
show={lenth === 0}
show={list.length === 0}
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.NO_ALERTS} size={180} />
@ -63,7 +62,7 @@ function AlertsList({ fetchList, list: alertsList, alertsSearch, siteId, init, f
</div>
<Pagination
page={page}
totalPages={Math.ceil(lenth / pageSize)}
totalPages={Math.ceil(list.length / pageSize)}
onPageChange={(page) => setPage(page)}
limit={pageSize}
debounceRequest={100}
@ -75,12 +74,8 @@ function AlertsList({ fetchList, list: alertsList, alertsSearch, siteId, init, f
export default connect(
(state) => ({
// @ts-ignore
list: state.getIn(['alerts', 'list']).sort((a, b) => b.createdAt - a.createdAt),
// @ts-ignore
alertsSearch: state.getIn(['alerts', 'alertsSearch']),
// @ts-ignore
webhooks: state.getIn(['webhooks', 'list']),
}),
{ fetchList, fetchWebhooks }
)(AlertsList);
{ fetchWebhooks }
)(observer(AlertsList));

View file

@ -1,20 +1,17 @@
import React, { useEffect, useState } from 'react';
import { Icon } from 'UI';
import { debounce } from 'App/utils';
import { changeSearch } from 'Duck/alerts';
import { connect } from 'react-redux';
import { useStore } from 'App/mstore'
import { observer } from 'mobx-react-lite'
let debounceUpdate: any = () => {};
interface Props {
changeSearch: (value: string) => void;
}
function AlertsSearch({ changeSearch }: Props) {
const [inputValue, setInputValue] = useState('');
function AlertsSearch() {
const { alertsStore } = useStore();
const [inputValue, setInputValue] = useState(alertsStore.alertsSearch);
useEffect(() => {
debounceUpdate = debounce((value: string) => changeSearch(value), 500);
debounceUpdate = debounce((value: string) => alertsStore.changeSearch(value), 500);
}, []);
const write = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
@ -36,10 +33,4 @@ function AlertsSearch({ changeSearch }: Props) {
);
}
export default connect(
(state) => ({
// @ts-ignore
alertsSearch: state.getIn(['alerts', 'alertsSearch']),
}),
{ changeSearch }
)(AlertsSearch);
export default observer(AlertsSearch);

View file

@ -1,8 +1,6 @@
import React from 'react';
import { Button, PageTitle, Icon, Link } from 'UI';
import withPageTitle from 'HOCs/withPageTitle';
import { connect } from 'react-redux';
import { init } from 'Duck/alerts';
import { withSiteId, alertCreate } from 'App/routes';
import AlertsList from './AlertsList';
@ -10,10 +8,9 @@ import AlertsSearch from './AlertsSearch';
interface IAlertsView {
siteId: string;
init: (instance?: Alert) => any;
}
function AlertsView({ siteId, init }: IAlertsView) {
function AlertsView({ siteId }: IAlertsView) {
return (
<div style={{ maxWidth: '1300px', margin: 'auto'}} className="bg-white rounded py-4 border">
<div className="flex items-center mb-4 justify-between px-6">
@ -21,7 +18,7 @@ function AlertsView({ siteId, init }: IAlertsView) {
<PageTitle title="Alerts" />
</div>
<div className="ml-auto flex items-center">
<Link to={withSiteId(alertCreate(), siteId)}><Button variant="primary" onClick={null}>Create Alert</Button></Link>
<Link to={withSiteId(alertCreate(), siteId)}><Button variant="primary">Create Alert</Button></Link>
<div className="ml-4 w-1/4" style={{ minWidth: 300 }}>
<AlertsSearch />
</div>
@ -31,12 +28,9 @@ function AlertsView({ siteId, init }: IAlertsView) {
<Icon name="info-circle-fill" className="mr-2" size={16} />
Alerts helps your team stay up to date with the activity on your app.
</div>
<AlertsList siteId={siteId} init={init} />
<AlertsList siteId={siteId} />
</div>
);
}
// @ts-ignore
const Container = connect(null, { init })(AlertsView);
export default withPageTitle('Alerts - OpenReplay')(Container);
export default withPageTitle('Alerts - OpenReplay')(AlertsView);

View file

@ -1,8 +1,7 @@
import React, { useEffect } from 'react';
import { Form, SegmentSelection, Icon } from 'UI';
import { Form, SegmentSelection } from 'UI';
import { connect } from 'react-redux';
import { validateEmail } from 'App/validate';
import { fetchTriggerOptions, init, edit, save, remove, fetchList } from 'Duck/alerts';
import { confirm } from 'UI';
import { toast } from 'react-toastify';
import { SLACK, WEBHOOK, TEAMS } from 'App/constants/schedule';
@ -10,7 +9,9 @@ import { fetchList as fetchWebhooks } from 'Duck/webhook';
import Breadcrumb from 'Shared/Breadcrumb';
import { withSiteId, alerts } from 'App/routes';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { useStore } from 'App/mstore'
import { observer } from 'mobx-react-lite'
import Alert from 'Types/alert'
import cn from 'classnames';
import WidgetName from '../WidgetName';
import BottomButtons from './AlertForm/BottomButtons';
@ -55,67 +56,63 @@ interface Select {
interface IProps extends RouteComponentProps {
siteId: string;
instance: Alert;
slackChannels: any[];
webhooks: any[];
loading: boolean;
deleting: boolean;
triggerOptions: any[];
list: any;
fetchTriggerOptions: () => void;
edit: (query: any) => void;
init: (alert?: Alert) => any;
save: (alert: Alert) => Promise<any>;
remove: (alertId: string) => Promise<any>;
onSubmit: (instance: Alert) => void;
fetchWebhooks: () => void;
fetchList: () => void;
}
const NewAlert = (props: IProps) => {
const { alertsStore } = useStore();
const {
instance,
siteId,
webhooks,
loading,
deleting,
triggerOptions,
fetchTriggerOptions,
init,
edit,
save,
remove,
fetchWebhooks,
fetchList,
list,
instance,
alerts: list,
triggerOptions,
loading,
} = alertsStore
const deleting = loading
const {
siteId,
webhooks,
fetchWebhooks,
} = props;
useEffect(() => {
init({});
if (list.size === 0) fetchList();
props.fetchTriggerOptions();
if (list.length === 0) fetchList();
fetchTriggerOptions();
fetchWebhooks();
}, []);
useEffect(() => {
if (list.size > 0) {
if (list.length > 0) {
const alertId = location.pathname.split('/').pop();
const currentAlert = list
.toJS()
.find((alert: Alert) => alert.alertId === parseInt(alertId, 10));
init(currentAlert);
.find((alert: Alert) => alert.alertId === String(alertId));
init(currentAlert || {});
}
}, [list]);
const write = ({ target: { value, name } }: React.ChangeEvent<HTMLInputElement>) =>
props.edit({ [name]: value });
edit({ [name]: value });
const writeOption = (
_: React.ChangeEvent,
{ name, value }: { name: string; value: Record<string, any> }
) => props.edit({ [name]: value.value });
) => edit({ [name]: value.value });
const onChangeCheck = ({ target: { checked, name } }: React.ChangeEvent<HTMLInputElement>) =>
props.edit({ [name]: checked });
const onChangeCheck = ({ target: { checked, name } }: React.ChangeEvent<HTMLInputElement>) => edit({ [name]: checked });
const onDelete = async (instance: Alert) => {
if (
@ -170,12 +167,12 @@ const NewAlert = (props: IProps) => {
{ name, value }: { name: string; value: string }
) => {
const { query } = instance;
props.edit({ query: { ...query, [name]: value } });
edit({ query: { ...query, [name]: value } });
};
const writeQuery = ({ target: { value, name } }: React.ChangeEvent<HTMLInputElement>) => {
const { query } = instance;
props.edit({ query: { ...query, [name]: value } });
edit({ query: { ...query, [name]: value } });
};
const metric =
@ -222,7 +219,7 @@ const NewAlert = (props: IProps) => {
outline
name="detectionMethod"
className="my-3 w-1/4"
onSelect={(e: any, { name, value }: any) => props.edit({ [name]: value })}
onSelect={(e: any, { name, value }: any) => edit({ [name]: value })}
value={{ value: instance.detectionMethod }}
list={[
{ name: 'Threshold', value: 'threshold' },
@ -294,20 +291,9 @@ const NewAlert = (props: IProps) => {
export default withRouter(
connect(
(state) => ({
// @ts-ignore
instance: state.getIn(['alerts', 'instance']),
//@ts-ignore
list: state.getIn(['alerts', 'list']),
// @ts-ignore
triggerOptions: state.getIn(['alerts', 'triggerOptions']),
// @ts-ignore
loading: state.getIn(['alerts', 'saveRequest', 'loading']),
// @ts-ignore
deleting: state.getIn(['alerts', 'removeRequest', 'loading']),
// @ts-ignore
webhooks: state.getIn(['webhooks', 'list']),
}),
{ fetchTriggerOptions, init, edit, save, remove, fetchWebhooks, fetchList }
// @ts-ignore
)(NewAlert)
{ fetchWebhooks }
)(observer(NewAlert))
);

View file

@ -1,2 +0,0 @@
// TODO burn the immutable and make typing this possible
type Alert = Record<string, any>

View file

@ -3,4 +3,4 @@ export default [
{ value: '>=', label: 'above or equal to' },
{ value: '<', label: 'below' },
{ value: '<=', label: 'below or equal to' },
];
] as const;

View file

@ -18,4 +18,4 @@ export default [
{ value: 'performance.crashes.count', label: 'performance.crashes.count', unit: '' },
{ value: 'errors.javascript.count', label: 'errors.javascript.count', unit: '' },
{ value: 'errors.backend.count', label: 'errors.backend.count', unit: '' },
];
] as const;

View file

@ -1,16 +1,16 @@
import Alert from 'Types/alert';
import { Map } from 'immutable';
import crudDuckGenerator from './tools/crudDuck';
import withRequestState, { RequestTypes } from 'Duck/requestStateCreator';
import { RequestTypes } from 'Duck/requestStateCreator';
import { reduceDucks } from 'Duck/tools';
const name = 'alert'
const idKey = 'alertId';
const crudDuck = crudDuckGenerator(name, Alert, { idKey: idKey });
const crudDuck = crudDuckGenerator(name, (d) => new Alert(d), { idKey: idKey });
export const { fetchList, init, edit, remove } = crudDuck.actions;
const FETCH_TRIGGER_OPTIONS = new RequestTypes(`${name}/FETCH_TRIGGER_OPTIONS`);
const CHANGE_SEARCH = `${name}/CHANGE_SEARCH`
console.log(fetchList(), init(), edit(), remove())
const initialState = Map({
definedPercent: 0,
triggerOptions: [],

View file

@ -0,0 +1,75 @@
import { makeAutoObservable } from 'mobx'
import Alert, { IAlert } from 'Types/alert'
import { alertsService } from 'App/services'
export default class AlertsStore {
alerts: Alert[] = [];
triggerOptions: { label: string, value: string | number, unit?: string }[] = [];
alertsSearch = '';
// @ts-ignore
instance: Alert = new Alert({}, false);
loading = false
constructor() {
makeAutoObservable(this);
}
changeSearch(value: string) {
this.alertsSearch = value;
}
async fetchList() {
this.loading = true
try {
const list = await alertsService.fetchList();
this.alerts = list.map(alert => new Alert(alert, true));
} catch (e) {
console.error(e)
} finally {
this.loading = false
}
}
async save(inst: Alert) {
this.loading = true
try {
await alertsService.save(inst ? inst : this.instance)
this.instance.isExists = true
} catch (e) {
console.error(e)
} finally {
this.loading = false
}
}
async remove(id: string) {
this.loading = true
try {
await alertsService.remove(id)
} catch (e) {
console.error(e)
} finally {
this.loading = false
}
}
async fetchTriggerOptions() {
this.loading = true
try {
const options = await alertsService.fetchTriggerOptions();
this.triggerOptions = options.map(({ name, value }) => ({ label: name, value }))
} catch (e) {
console.error(e)
} finally {
this.loading = false
}
}
init(inst: Partial<IAlert> | Alert) {
this.instance = inst instanceof Alert ? inst : new Alert(inst, false)
}
edit(diff: Partial<Alert>) {
Object.assign(this.instance, diff)
}
}

View file

@ -16,6 +16,7 @@ import {
notesService,
recordingsService,
configService,
alertsService,
} from 'App/services';
import SettingsStore from './settingsStore';
import AuditStore from './auditStore';
@ -27,6 +28,7 @@ import BugReportStore from './bugReportStore'
import RecordingsStore from './recordingsStore'
import AssistMultiviewStore from './assistMultiviewStore';
import WeeklyReportStore from './weeklyReportConfigStore'
import AlertStore from './alertsStore'
export class RootStore {
dashboardStore: DashboardStore;
@ -44,6 +46,7 @@ export class RootStore {
recordingsStore: RecordingsStore;
assistMultiviewStore: AssistMultiviewStore;
weeklyReportStore: WeeklyReportStore
alertsStore: AlertStore
constructor() {
this.dashboardStore = new DashboardStore();
@ -61,6 +64,7 @@ export class RootStore {
this.recordingsStore = new RecordingsStore();
this.assistMultiviewStore = new AssistMultiviewStore();
this.weeklyReportStore = new WeeklyReportStore();
this.alertsStore = new AlertStore();
}
initClient() {
@ -75,6 +79,7 @@ export class RootStore {
notesService.initClient(client)
recordingsService.initClient(client);
configService.initClient(client);
alertsService.initClient(client)
}
}

View file

@ -1,4 +1,4 @@
import { makeAutoObservable, observable, action, runInAction } from "mobx"
import { makeAutoObservable, runInAction } from "mobx"
import Widget from "./widget"
import { dashboardService } from "App/services"
import { toast } from 'react-toastify';
@ -6,7 +6,7 @@ import { DateTime } from 'luxon';
export default class Dashboard {
public static get ID_KEY():string { return "dashboardId" }
dashboardId: any = undefined
dashboardId?: string = undefined
name: string = "Untitled Dashboard"
description: string = ""
isPublic: boolean = true

View file

@ -15,7 +15,6 @@ export default class Widget {
widgetId: any = undefined
category?: string = undefined
name: string = "Untitled Card"
// metricType: string = "timeseries"
metricType: string = "timeseries"
metricOf: string = "sessionCount"
metricValue: string = ""
@ -37,8 +36,6 @@ export default class Widget {
period: Record<string, any> = Period({ rangeName: LAST_24_HOURS }) // temp value in detail view
hasChanged: boolean = false
sessionsLoading: boolean = false
position: number = 0
data: any = {
sessions: [],
@ -51,7 +48,6 @@ export default class Widget {
isLoading: boolean = false
isValid: boolean = false
dashboardId: any = undefined
colSpan: number = 2
predefinedKey: string = ''
constructor() {
@ -103,10 +99,6 @@ export default class Widget {
return this
}
setPeriod(period: any) {
this.period = new Period({ start: period.startDate, end: period.endDate, rangeName: period.rangeName })
}
toWidget(): any {
return {
config: {
@ -117,10 +109,6 @@ export default class Widget {
}
}
toJsonDrilldown() {
return this.series.map((series: any) => series.toJson())
}
toJson() {
return {
metricId: this.metricId,
@ -171,18 +159,6 @@ export default class Widget {
})
}
fetchIssues(filter: any): Promise<any> {
return new Promise((resolve) => {
metricService.fetchIssues(filter).then((response: any) => {
const significantIssues = response.issues.significant ? response.issues.significant.map((issue: any) => new Funnelissue().fromJSON(issue)) : []
const insignificantIssues = response.issues.insignificant ? response.issues.insignificant.map((issue: any) => new Funnelissue().fromJSON(issue)) : []
resolve({
issues: significantIssues.length > 0 ? significantIssues : insignificantIssues,
})
})
})
}
fetchIssue(funnelId: any, issueId: any, params: any): Promise<any> {
return new Promise((resolve, reject) => {
metricService.fetchIssue(funnelId, issueId, params).then((response: any) => {

View file

@ -0,0 +1,49 @@
import APIClient from 'App/api_client';
import Alert, { IAlert } from "Types/alert";
export default class AlertsService {
private client: APIClient;
constructor(client?: APIClient) {
this.client = client ? client : new APIClient();
}
initClient(client?: APIClient) {
this.client = client || new APIClient();
}
save(instance: Alert): Promise<IAlert> {
return this.client.post(instance['alertId'] ? `/alerts/${instance['alertId']}` : '/alerts', instance.toData())
.then(response => response.json())
.then(response => response.data || {})
.catch(Promise.reject)
}
fetchTriggerOptions(): Promise<{ name: string, value: string | number }[]> {
return this.client.get('/alerts/triggers')
.then(r => r.json())
.then(j => j.data || [])
.catch(Promise.reject)
}
fetchList(): Promise<IAlert[]> {
return this.client.get('/alerts')
.then(r => r.json())
.then(j => j.data || [])
.catch(Promise.reject)
}
fetch(id: string): Promise<IAlert> {
return this.client.get(`/alerts/${id}`)
.then(r => r.json())
.then(j => j.data || {})
.catch(Promise.reject)
}
remove(id: string): Promise<IAlert> {
return this.client.delete(`/alerts/${id}`)
.then(r => r.json())
.then(j => j.data || {})
.catch(Promise.reject)
}
}

View file

@ -8,7 +8,7 @@ import ErrorService from "./ErrorService";
import NotesService from "./NotesService";
import RecordingsService from "./RecordingsService";
import ConfigService from './ConfigService'
import AlertsService from './AlertsService'
export const dashboardService = new DashboardService();
export const metricService = new MetricService();
export const sessionService = new SessionSerivce();
@ -19,3 +19,4 @@ export const errorService = new ErrorService();
export const notesService = new NotesService();
export const recordingsService = new RecordingsService();
export const configService = new ConfigService();
export const alertsService = new AlertsService();

View file

@ -1,119 +0,0 @@
import Record from 'Types/Record';
import { notEmptyString, validateName, validateNumber, validateEmail } from 'App/validate';
import { List, Map } from 'immutable';
import { alertMetrics as metrics, alertConditions as conditions } from 'App/constants';
// import Filter from './filter';
const metricsMap = {}
const conditionsMap = {}
metrics.forEach(m => { metricsMap[m.value] = m });
conditions.forEach(c => { conditionsMap[c.value] = c });
export default Record({
alertId: '',
projectId: undefined,
name: 'Untitled Alert',
description: '',
active: true,
currentPeriod: 15,
previousPeriod: 15,
detectionMethod: 'threshold',
change: 'change',
query: Map({ left: '', operator: '', right: ''}),
options: Map({ currentPeriod: 15, previousPeriod: 15 }),
createdAt: undefined,
slack: false,
slackInput: [],
webhook: false,
webhookInput: [],
email: false,
emailInput: [],
msteams: false,
msteamsInput: [],
hasNotification: false,
metric: '',
condition: '',
}, {
idKey: 'alertId',
methods: {
validate() {
return notEmptyString(this.name) &&
this.query.left && this.query.right && validateNumber(this.query.right) && this.query.right > 0 && this.query.operator &&
(this.slack ? this.slackInput.length > 0 : true) &&
(this.email ? this.emailInput.length > 0 : true) &&
(this.msteams ? this.msteamsInput.length > 0 : true) &&
(this.webhook ? this.webhookInput.length > 0 : true);
},
toData() {
const js = this.toJS();
const options = { message: [] }
if (js.slack && js.slackInput)
options.message = options.message.concat(js.slackInput.map(i => ({ type: 'slack', value: i })))
// options.message.push({ type: 'slack', value: js.slackInput })
if (js.email && js.emailInput)
options.message = options.message.concat(js.emailInput.map(i => ({ type: 'email', value: i })))
// options.message.push({ type: 'email', value: js.emailInput })
if (js.webhook && js.webhookInput)
options.message = options.message.concat(js.webhookInput.map(i => ({ type: 'webhook', value: i })))
// options.message.push({ type: 'webhook', value: js.webhookInput })
if (js.msteams && js.msteamsInput)
options.message = options.message.concat(js.msteamsInput.map(i => ({ type: 'msteams', value: i })))
options.previousPeriod = js.previousPeriod
options.currentPeriod = js.currentPeriod
js.detection_method = js.detectionMethod;
delete js.slack;
delete js.webhook;
delete js.email;
delete js.slackInput;
delete js.webhookInput;
delete js.emailInput;
delete js.msteams;
delete js.msteamsInput;
delete js.hasNotification;
delete js.metric;
delete js.condition;
delete js.currentPeriod;
delete js.previousPeriod;
return { ...js, options: options };
},
},
fromJS: (item) => {
const options = item.options || { currentPeriod: 15, previousPeriod: 15, message: [] };
const query = item.query || { left: '', operator: '', right: ''};
const slack = List(options.message).filter(i => i.type === 'slack');
const email = List(options.message).filter(i => i.type === 'email');
const webhook = List(options.message).filter(i => i.type === 'webhook');
const msteams = List(options.message).filter(i => i.type === 'msteams');
return {
...item,
metric: metricsMap[query.left],
condition: item.query ? conditionsMap[item.query.operator] : {},
detectionMethod: item.detectionMethod || item.detection_method,
query: query,
options: options,
previousPeriod: options.previousPeriod,
currentPeriod: options.currentPeriod,
slack: slack.size > 0,
slackInput: slack.map(i => parseInt(i.value)).toJS(),
msteams: msteams.size > 0,
msteamsInput: msteams.map(i => parseInt(i.value)).toJS(),
email: email.size > 0,
emailInput: email.map(i => i.value).toJS(),
webhook: webhook.size > 0,
webhookInput: webhook.map(i => parseInt(i.value)).toJS(),
hasNotification: !!slack || !!email || !!webhook
}
},
});

197
frontend/app/types/alert.ts Normal file
View file

@ -0,0 +1,197 @@
import { notEmptyString, validateNumber } from 'App/validate';
import { alertMetrics as metrics, alertConditions as conditions } from 'App/constants';
const metricsMap = {}
const conditionsMap = {}
// @ts-ignore
metrics.forEach(m => { metricsMap[m.value] = m });
// @ts-ignore
conditions.forEach(c => { conditionsMap[c.value] = c });
export interface IAlert {
alertId: string;
projectId?: string;
name: string;
description: string;
active: boolean;
currentPeriod: number;
previousPeriod: number;
detectionMethod: string;
detection_method?: string;
change: string;
query: { left: string, operator: string, right: string };
options: { currentPeriod: number, previousPeriod: number, message: {type: string, value: string}[] };
createdAt?: number;
slack: boolean;
slackInput: string[];
webhook: boolean;
webhookInput: string[];
email: boolean;
emailInput: string[];
msteams: boolean;
msteamsInput: string[];
hasNotification: boolean;
metric: { unit: any };
condition: string;
}
const defaults = {
alertId: '',
projectId: undefined,
name: 'Untitled Alert',
description: '',
active: true,
currentPeriod: 15,
previousPeriod: 15,
detectionMethod: 'threshold',
change: 'change',
query: { left: '', operator: '', right: '' },
options: { currentPeriod: 15, previousPeriod: 15 },
createdAt: undefined,
slack: false,
slackInput: [],
webhook: false,
webhookInput: [],
email: false,
emailInput: [],
msteams: false,
msteamsInput: [],
hasNotification: false,
metric: '',
condition: '',
} as unknown as IAlert
export default class Alert {
alertId: IAlert["alertId"]
projectId?: IAlert["projectId"]
name: IAlert["name"]
description: IAlert["description"]
active: IAlert["active"]
currentPeriod: IAlert["currentPeriod"]
previousPeriod: IAlert["previousPeriod"]
detectionMethod: IAlert["detectionMethod"]
detection_method: IAlert["detection_method"]
change: IAlert["change"]
query:IAlert["query"]
options: IAlert["options"]
createdAt?: IAlert["createdAt"]
slack: IAlert["slack"]
slackInput: IAlert["slackInput"]
webhook: IAlert["webhook"]
webhookInput: IAlert["webhookInput"]
email: IAlert["email"]
emailInput: IAlert["emailInput"]
msteams: IAlert["msteams"]
msteamsInput: IAlert["msteamsInput"]
hasNotification: IAlert["hasNotification"]
metric: IAlert["metric"]
condition: IAlert["condition"]
isExists = false
constructor(item: Partial<IAlert> = defaults, isExists: boolean) {
Object.assign(defaults, item)
const options = defaults.options || { currentPeriod: 15, previousPeriod: 15, message: [] };
const query = defaults.query || { left: '', operator: '', right: ''};
const slack = options.message?.filter(i => i.type === 'slack') || [];
const email = options.message?.filter(i => i.type === 'email') || [];
const webhook = options.message?.filter(i => i.type === 'webhook') || [];
const msteams = options.message?.filter(i => i.type === 'msteams') || [];
Object.assign(this, {
...defaults,
// @ts-ignore
metric: metricsMap[query.left],
alertId: String(defaults.alertId),
// @ts-ignore TODO
condition: defaults.query ? conditionsMap[defaults.query.operator] : {},
detectionMethod: defaults.detectionMethod || defaults.detection_method,
query: query,
options: options,
previousPeriod: options.previousPeriod,
currentPeriod: options.currentPeriod,
slack: slack.length > 0,
slackInput: slack.map(i => parseInt(i.value)),
msteams: msteams.length > 0,
msteamsInput: msteams.map(i => parseInt(i.value)),
email: email.length > 0,
emailInput: email.map(i => i.value),
webhook: webhook.length > 0,
webhookInput: webhook.map(i => parseInt(i.value)),
hasNotification: !!slack || !!email || !!webhook,
isExists,
})
}
validate() {
return notEmptyString(this.name) &&
this.query.left && this.query.right && validateNumber(this.query.right) && parseInt(this.query.right, 10) > 0 && this.query.operator &&
(this.slack ? this.slackInput.length > 0 : true) &&
(this.email ? this.emailInput.length > 0 : true) &&
(this.msteams ? this.msteamsInput.length > 0 : true) &&
(this.webhook ? this.webhookInput.length > 0 : true);
}
toData() {
const js = { ...this };
const options = { message: [], previousPeriod: 0, currentPeriod: 0 }
if (js.slack && js.slackInput)
// @ts-ignore
options.message = options.message.concat(js.slackInput.map(i => ({ type: 'slack', value: i })))
if (js.email && js.emailInput)
// @ts-ignore
options.message = options.message.concat(js.emailInput.map(i => ({ type: 'email', value: i })))
if (js.webhook && js.webhookInput)
// @ts-ignore
options.message = options.message.concat(js.webhookInput.map(i => ({ type: 'webhook', value: i })))
if (js.msteams && js.msteamsInput)
// @ts-ignore
options.message = options.message.concat(js.msteamsInput.map(i => ({ type: 'msteams', value: i })))
options.previousPeriod = js.previousPeriod
options.currentPeriod = js.currentPeriod
js.detection_method = js.detectionMethod;
// @ts-ignore
delete js.slack;
// @ts-ignore
delete js.webhook;
// @ts-ignore
delete js.email;
// @ts-ignore
delete js.slackInput;
// @ts-ignore
delete js.webhookInput;
// @ts-ignore
delete js.emailInput;
// @ts-ignore
delete js.msteams;
// @ts-ignore
delete js.msteamsInput;
// @ts-ignore
delete js.hasNotification;
// @ts-ignore
delete js.metric;
// @ts-ignore
delete js.condition;
// @ts-ignore
delete js.currentPeriod;
// @ts-ignore
delete js.previousPeriod;
return { ...js, options: options };
}
exists() {
return this.isExists
}
}