diff --git a/frontend/app/Router.js b/frontend/app/Router.js index 584fb59e5..662a7a7a7 100644 --- a/frontend/app/Router.js +++ b/frontend/app/Router.js @@ -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 { diff --git a/frontend/app/components/Alerts/AlertForm.js b/frontend/app/components/Alerts/AlertForm.js index fd6fe3d77..18ae1ac01 100644 --- a/frontend/app/components/Alerts/AlertForm.js +++ b/frontend/app/components/Alerts/AlertForm.js @@ -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 }) => ( ); -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); diff --git a/frontend/app/components/Alerts/AlertFormModal/AlertFormModal.tsx b/frontend/app/components/Alerts/AlertFormModal/AlertFormModal.tsx index dc4c9db15..f03c479be 100644 --- a/frontend/app/components/Alerts/AlertFormModal/AlertFormModal.tsx +++ b/frontend/app/components/Alerts/AlertFormModal/AlertFormModal.tsx @@ -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 && ( ({ webhooks: state.getIn(['webhooks', 'list']), - instance: state.getIn(['alerts', 'instance']), }), - { init, edit, save, remove, fetchWebhooks, setShowAlerts } -)(AlertFormModal); + { fetchWebhooks, setShowAlerts } +)(observer(AlertFormModal)); diff --git a/frontend/app/components/Alerts/AlertsList.js b/frontend/app/components/Alerts/AlertsList.js index 5a874e0fa..a64094a0d 100644 --- a/frontend/app/components/Alerts/AlertsList.js +++ b/frontend/app/components/Alerts/AlertsList.js @@ -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); diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/Bar.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/Bar.tsx new file mode 100644 index 000000000..a6ca35923 --- /dev/null +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/Bar.tsx @@ -0,0 +1,18 @@ +import React from 'react' +import stl from './Bar.module.css' + +const Bar = ({ className = '', width = 0, avg, domain, color }) => { + return ( +
+
+
0 ? width : 5 }%`, backgroundColor: color }}>
+
+ {`${avg}`} +
+
+
{domain}
+
+ ) +} + +export default Bar \ No newline at end of file diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/ErrorsPerDomain.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/ErrorsPerDomain.tsx index 13643c769..d9e773948 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/ErrorsPerDomain.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/ErrorsPerDomain.tsx @@ -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 { diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/bar.module.css b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/bar.module.css new file mode 100644 index 000000000..6dfde11a5 --- /dev/null +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/bar.module.css @@ -0,0 +1,6 @@ +.bar { + height: 5px; + background-color: red; + width: 100%; + border-radius: 3px; +} \ No newline at end of file diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/Bar.module.css b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/Bar.module.css new file mode 100644 index 000000000..8037424f2 --- /dev/null +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/Bar.module.css @@ -0,0 +1,6 @@ +.bar { + height: 10px; + background-color: red; + width: 100%; + border-radius: 3px; +} \ No newline at end of file diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/Bar.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/Bar.tsx new file mode 100644 index 000000000..179bca846 --- /dev/null +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/Bar.tsx @@ -0,0 +1,19 @@ +import React from 'react' +import stl from './Bar.module.css' + +const Bar = ({ className = '', width = 0, avg, domain, color }) => { + return ( +
+
+
+
+ {avg} + ms +
+
+
{domain}
+
+ ) +} + +export default Bar \ No newline at end of file diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/SlowestDomains.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/SlowestDomains.tsx index fa4b703f2..758a6575a 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/SlowestDomains.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/SlowestDomains.tsx @@ -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 { diff --git a/frontend/app/components/Dashboard/components/Alerts/AlertListItem.tsx b/frontend/app/components/Dashboard/components/Alerts/AlertListItem.tsx index 202bb7c21..1022842c9 100644 --- a/frontend/app/components/Dashboard/components/Alerts/AlertListItem.tsx +++ b/frontend/app/components/Dashboard/components/Alerts/AlertListItem.tsx @@ -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, webhooks: Array) => { interface Props extends RouteComponentProps { alert: Alert; siteId: string; - init: (alert?: Alert) => void; + init: (alert: Alert) => void; demo?: boolean; webhooks: Array; } @@ -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' + )}
@@ -133,11 +134,13 @@ function AlertListItem(props: Props) { {numberWithCommas(alert.query.right)} {alert.metric.unit} {' over the past '} - {getThreshold(alert.currentPeriod)} + {getThreshold( + alert.currentPeriod)} {alert.detectionMethod === 'change' ? ( <> {' compared to the previous '} - {getThreshold(alert.previousPeriod)} + {getThreshold( + alert.previousPeriod)} ) : null} {', notify me on '} diff --git a/frontend/app/components/Dashboard/components/Alerts/AlertsList.tsx b/frontend/app/components/Dashboard/components/Alerts/AlertsList.tsx index 57af6efc2..a6f20b449 100644 --- a/frontend/app/components/Dashboard/components/Alerts/AlertsList.tsx +++ b/frontend/app/components/Dashboard/components/Alerts/AlertsList.tsx @@ -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; - 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 ( @@ -63,7 +62,7 @@ function AlertsList({ fetchList, list: alertsList, alertsSearch, siteId, init, f
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)); diff --git a/frontend/app/components/Dashboard/components/Alerts/AlertsSearch.tsx b/frontend/app/components/Dashboard/components/Alerts/AlertsSearch.tsx index 0e4ffc5ef..0928f3a46 100644 --- a/frontend/app/components/Dashboard/components/Alerts/AlertsSearch.tsx +++ b/frontend/app/components/Dashboard/components/Alerts/AlertsSearch.tsx @@ -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) => { @@ -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); diff --git a/frontend/app/components/Dashboard/components/Alerts/AlertsView.tsx b/frontend/app/components/Dashboard/components/Alerts/AlertsView.tsx index 07b77961a..631df8e43 100644 --- a/frontend/app/components/Dashboard/components/Alerts/AlertsView.tsx +++ b/frontend/app/components/Dashboard/components/Alerts/AlertsView.tsx @@ -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 (
@@ -21,7 +18,7 @@ function AlertsView({ siteId, init }: IAlertsView) {
- +
@@ -31,12 +28,9 @@ function AlertsView({ siteId, init }: IAlertsView) { Alerts helps your team stay up to date with the activity on your app.
- +
); } -// @ts-ignore -const Container = connect(null, { init })(AlertsView); - -export default withPageTitle('Alerts - OpenReplay')(Container); +export default withPageTitle('Alerts - OpenReplay')(AlertsView); diff --git a/frontend/app/components/Dashboard/components/Alerts/NewAlert.tsx b/frontend/app/components/Dashboard/components/Alerts/NewAlert.tsx index 335bf8295..4ad94e8a0 100644 --- a/frontend/app/components/Dashboard/components/Alerts/NewAlert.tsx +++ b/frontend/app/components/Dashboard/components/Alerts/NewAlert.tsx @@ -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; - remove: (alertId: string) => Promise; 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) => - props.edit({ [name]: value }); + edit({ [name]: value }); const writeOption = ( _: React.ChangeEvent, { name, value }: { name: string; value: Record } - ) => props.edit({ [name]: value.value }); + ) => edit({ [name]: value.value }); - const onChangeCheck = ({ target: { checked, name } }: React.ChangeEvent) => - props.edit({ [name]: checked }); + const onChangeCheck = ({ target: { checked, name } }: React.ChangeEvent) => 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) => { 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)) ); diff --git a/frontend/app/components/Dashboard/components/Alerts/type.d.ts b/frontend/app/components/Dashboard/components/Alerts/type.d.ts deleted file mode 100644 index 6ac1a8f34..000000000 --- a/frontend/app/components/Dashboard/components/Alerts/type.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -// TODO burn the immutable and make typing this possible -type Alert = Record diff --git a/frontend/app/constants/alertConditions.js b/frontend/app/constants/alertConditions.ts similarity index 93% rename from frontend/app/constants/alertConditions.js rename to frontend/app/constants/alertConditions.ts index 09dfefc58..0c19fd51e 100644 --- a/frontend/app/constants/alertConditions.js +++ b/frontend/app/constants/alertConditions.ts @@ -3,4 +3,4 @@ export default [ { value: '>=', label: 'above or equal to' }, { value: '<', label: 'below' }, { value: '<=', label: 'below or equal to' }, -]; +] as const; diff --git a/frontend/app/constants/alertMetrics.js b/frontend/app/constants/alertMetrics.ts similarity index 99% rename from frontend/app/constants/alertMetrics.js rename to frontend/app/constants/alertMetrics.ts index 01fc24acb..b7ee3ce4f 100644 --- a/frontend/app/constants/alertMetrics.js +++ b/frontend/app/constants/alertMetrics.ts @@ -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; diff --git a/frontend/app/duck/alerts.js b/frontend/app/duck/alerts.js index 6591ae7e7..31721ff45 100644 --- a/frontend/app/duck/alerts.js +++ b/frontend/app/duck/alerts.js @@ -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: [], diff --git a/frontend/app/mstore/alertsStore.ts b/frontend/app/mstore/alertsStore.ts new file mode 100644 index 000000000..25f90644d --- /dev/null +++ b/frontend/app/mstore/alertsStore.ts @@ -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 | Alert) { + this.instance = inst instanceof Alert ? inst : new Alert(inst, false) + } + + edit(diff: Partial) { + Object.assign(this.instance, diff) + } +} \ No newline at end of file diff --git a/frontend/app/mstore/index.tsx b/frontend/app/mstore/index.tsx index 707fb175a..b9567c817 100644 --- a/frontend/app/mstore/index.tsx +++ b/frontend/app/mstore/index.tsx @@ -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) } } diff --git a/frontend/app/mstore/types/dashboard.ts b/frontend/app/mstore/types/dashboard.ts index 0213f9b0d..19faffe7b 100644 --- a/frontend/app/mstore/types/dashboard.ts +++ b/frontend/app/mstore/types/dashboard.ts @@ -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 diff --git a/frontend/app/mstore/types/widget.ts b/frontend/app/mstore/types/widget.ts index 4b6760bc1..9d8f5a151 100644 --- a/frontend/app/mstore/types/widget.ts +++ b/frontend/app/mstore/types/widget.ts @@ -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 = 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 { - 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 { return new Promise((resolve, reject) => { metricService.fetchIssue(funnelId, issueId, params).then((response: any) => { diff --git a/frontend/app/services/AlertsService.ts b/frontend/app/services/AlertsService.ts new file mode 100644 index 000000000..7ba1702e9 --- /dev/null +++ b/frontend/app/services/AlertsService.ts @@ -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 { + 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 { + return this.client.get('/alerts') + .then(r => r.json()) + .then(j => j.data || []) + .catch(Promise.reject) + } + + fetch(id: string): Promise { + return this.client.get(`/alerts/${id}`) + .then(r => r.json()) + .then(j => j.data || {}) + .catch(Promise.reject) + } + + remove(id: string): Promise { + return this.client.delete(`/alerts/${id}`) + .then(r => r.json()) + .then(j => j.data || {}) + .catch(Promise.reject) + } +} \ No newline at end of file diff --git a/frontend/app/services/index.ts b/frontend/app/services/index.ts index 5033c4ed3..72d0f4d92 100644 --- a/frontend/app/services/index.ts +++ b/frontend/app/services/index.ts @@ -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(); @@ -18,4 +18,5 @@ export const auditService = new AuditService(); export const errorService = new ErrorService(); export const notesService = new NotesService(); export const recordingsService = new RecordingsService(); -export const configService = new ConfigService(); \ No newline at end of file +export const configService = new ConfigService(); +export const alertsService = new AlertsService(); \ No newline at end of file diff --git a/frontend/app/types/alert.js b/frontend/app/types/alert.js deleted file mode 100644 index 3e46a2e06..000000000 --- a/frontend/app/types/alert.js +++ /dev/null @@ -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 - } - }, -}); diff --git a/frontend/app/types/alert.ts b/frontend/app/types/alert.ts new file mode 100644 index 000000000..ff127a240 --- /dev/null +++ b/frontend/app/types/alert.ts @@ -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 = 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 + } +}