diff --git a/frontend/app/components/Alerts/AlertFormModal/AlertFormModal.tsx b/frontend/app/components/Alerts/AlertFormModal/AlertFormModal.tsx index 48a46a3a0..93999281a 100644 --- a/frontend/app/components/Alerts/AlertFormModal/AlertFormModal.tsx +++ b/frontend/app/components/Alerts/AlertFormModal/AlertFormModal.tsx @@ -2,9 +2,7 @@ import React, { useEffect, useState } from 'react'; 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 { SLACK, TEAMS, WEBHOOK } from 'App/constants/schedule'; import { confirm } from 'UI'; @@ -18,16 +16,14 @@ interface Props { showModal?: boolean; metricId?: number; onClose?: () => void; - webhooks: any; - fetchWebhooks: Function; } function AlertFormModal(props: Props) { - const { alertsStore } = useStore() - const { metricId = null, showModal = false, webhooks } = props; + const { alertsStore, settingsStore } = useStore() + const { metricId = null, showModal = false } = props; const [showForm, setShowForm] = useState(false); - + const webhooks = settingsStore.webhooks useEffect(() => { - props.fetchWebhooks(); + settingsStore.fetchWebhooks(); }, []); @@ -110,9 +106,4 @@ function AlertFormModal(props: Props) { ); } -export default connect( - (state) => ({ - webhooks: state.getIn(['webhooks', 'list']), - }), - { fetchWebhooks } -)(observer(AlertFormModal)); +export default observer(AlertFormModal); diff --git a/frontend/app/components/Client/Webhooks/WebhookForm.js b/frontend/app/components/Client/Webhooks/WebhookForm.js index b64a63af8..c918dcb93 100644 --- a/frontend/app/components/Client/Webhooks/WebhookForm.js +++ b/frontend/app/components/Client/Webhooks/WebhookForm.js @@ -1,94 +1,74 @@ import React from 'react'; -import { connect } from 'react-redux'; -import { edit, save } from 'Duck/webhook'; import { Form, Button, Input } from 'UI'; import styles from './webhookForm.module.css'; +import { useStore } from 'App/mstore' +import { observer } from 'mobx-react-lite' -@connect( - (state) => ({ - webhook: state.getIn(['webhooks', 'instance']), - loading: state.getIn(['webhooks', 'saveRequest', 'loading']), - }), - { - edit, - save, - } -) -class WebhookForm extends React.PureComponent { - setFocus = () => this.focusElement.focus(); - onChangeSelect = (event, { name, value }) => this.props.edit({ [name]: value }); - write = ({ target: { value, name } }) => this.props.edit({ [name]: value }); +function WebhookForm(props) { + const { settingsStore } = useStore() + const { webhookInst: webhook, hooksLoading: loading, saveWebhook, editWebhook } = settingsStore + const write = ({ target: { value, name } }) => editWebhook({ [name]: value }); - save = () => { - this.props.save(this.props.webhook).then(() => { - this.props.onClose(); + const save = () => { + saveWebhook(webhook).then(() => { + props.onClose(); }); }; - render() { - const { webhook, loading } = this.props; - return ( -
-

{webhook.exists() ? 'Update' : 'Add'} Webhook

-
- - - { - this.focusElement = ref; - }} - name="name" - value={webhook.name} - onChange={this.write} - placeholder="Name" - /> - - - - { - this.focusElement = ref; - }} - name="endpoint" - value={webhook.endpoint} - onChange={this.write} - placeholder="Endpoint" - /> - + return ( +
+

{webhook.exists() ? 'Update' : 'Add'} Webhook

+ + + + + - - - { - this.focusElement = ref; - }} - name="authHeader" - value={webhook.authHeader} - onChange={this.write} - placeholder="Auth Header" - /> - + + + + -
-
- - {webhook.exists() && } -
- {webhook.exists() && } + + + + + +
+
+ + {webhook.exists() && }
- -
- ); - } + {webhook.exists() && + } +
+ +
+ ); } -export default WebhookForm; +export default observer(WebhookForm); diff --git a/frontend/app/components/Client/Webhooks/Webhooks.js b/frontend/app/components/Client/Webhooks/Webhooks.js index a87ac2298..0e1a0214c 100644 --- a/frontend/app/components/Client/Webhooks/Webhooks.js +++ b/frontend/app/components/Client/Webhooks/Webhooks.js @@ -1,9 +1,7 @@ import React, { useEffect } from 'react'; -import { connect } from 'react-redux'; import cn from 'classnames'; import withPageTitle from 'HOCs/withPageTitle'; import { Button, Loader, NoContent, Icon } from 'UI'; -import { init, fetchList, remove } from 'Duck/webhook'; import WebhookForm from './WebhookForm'; import ListItem from './ListItem'; import styles from './webhooks.module.css'; @@ -11,79 +9,71 @@ import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; import { confirm } from 'UI'; import { toast } from 'react-toastify'; import { useModal } from 'App/components/Modal'; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite' -function Webhooks(props) { - const { webhooks, loading } = props; - const { showModal, hideModal } = useModal(); +function Webhooks() { + const { settingsStore } = useStore() + const { webhooks, hooksLoading: loading } = settingsStore; + const { showModal, hideModal } = useModal(); - const noSlackWebhooks = webhooks.filter((hook) => hook.type === 'webhook'); - useEffect(() => { - props.fetchList(); - }, []); + const noSlackWebhooks = webhooks.filter((hook) => hook.type === 'webhook'); + useEffect(() => { + void settingsStore.fetchWebhooks(); + }, []); - const init = (v) => { - props.init(v); - showModal(); - }; + const init = (v) => { + settingsStore.initWebhook(v); + showModal(); + }; - const removeWebhook = async (id) => { - if ( - await confirm({ - header: 'Confirm', - confirmButton: 'Yes, delete', - confirmation: `Are you sure you want to remove this webhook?`, - }) - ) { - props.remove(id).then(() => { - toast.success('Webhook removed successfully'); - }); - hideModal(); - } - }; + const removeWebhook = async (id) => { + if ( + await confirm({ + header: 'Confirm', + confirmButton: 'Yes, delete', + confirmation: `Are you sure you want to remove this webhook?`, + }) + ) { + settingsStore.removeWebhook(id).then(() => { + toast.success('Webhook removed successfully'); + }); + hideModal(); + } + }; - return ( -
-
-

{'Webhooks'}

- {/* -
- -
- - Leverage webhooks to push OpenReplay data to other systems. -
- - - - -
None added yet
+ return ( +
+
+

{'Webhooks'}

+
- } - size="small" - show={noSlackWebhooks.size === 0} - > -
- {noSlackWebhooks.map((webhook) => ( - init(webhook)} /> - ))} -
- - -
- ); + +
+ + Leverage webhooks to push OpenReplay data to other systems. +
+ + + + +
None added yet
+
+ } + size="small" + show={noSlackWebhooks.length === 0} + > +
+ {noSlackWebhooks.map((webhook) => ( + init(webhook)} /> + ))} +
+ + +
+ ); } -export default connect( - (state) => ({ - webhooks: state.getIn(['webhooks', 'list']), - loading: state.getIn(['webhooks', 'loading']), - }), - { - init, - fetchList, - remove, - } -)(withPageTitle('Webhooks - OpenReplay Preferences')(Webhooks)); +export default withPageTitle('Webhooks - OpenReplay Preferences')(observer(Webhooks)); diff --git a/frontend/app/components/Dashboard/components/Alerts/AlertsList.tsx b/frontend/app/components/Dashboard/components/Alerts/AlertsList.tsx index a6f20b449..e4005098e 100644 --- a/frontend/app/components/Dashboard/components/Alerts/AlertsList.tsx +++ b/frontend/app/components/Dashboard/components/Alerts/AlertsList.tsx @@ -1,25 +1,21 @@ import React from 'react'; -import { NoContent, Pagination, Icon } from 'UI'; +import { NoContent, Pagination } from 'UI'; import { filterList } from 'App/utils'; import { sliceListPerPage } from 'App/utils'; -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 { siteId: string; - webhooks: Array; - fetchWebhooks: () => void; } -function AlertsList({ siteId, fetchWebhooks, webhooks }: Props) { - const { alertsStore } = useStore(); +function AlertsList({ siteId }: Props) { + const { alertsStore, settingsStore } = useStore(); + const { fetchWebhooks, webhooks } = settingsStore const { alerts: alertsList, alertsSearch, fetchList, init } = alertsStore React.useEffect(() => { fetchList(); fetchWebhooks() }, []); @@ -72,10 +68,4 @@ function AlertsList({ siteId, fetchWebhooks, webhooks }: Props) { ); } -export default connect( - (state) => ({ - // @ts-ignore - webhooks: state.getIn(['webhooks', 'list']), - }), - { fetchWebhooks } -)(observer(AlertsList)); +export default observer(AlertsList); diff --git a/frontend/app/components/Dashboard/components/Alerts/NewAlert.tsx b/frontend/app/components/Dashboard/components/Alerts/NewAlert.tsx index 4ad94e8a0..aa6c18714 100644 --- a/frontend/app/components/Dashboard/components/Alerts/NewAlert.tsx +++ b/frontend/app/components/Dashboard/components/Alerts/NewAlert.tsx @@ -5,7 +5,6 @@ import { validateEmail } from 'App/validate'; import { confirm } from 'UI'; import { toast } from 'react-toastify'; import { SLACK, WEBHOOK, TEAMS } from 'App/constants/schedule'; -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'; @@ -57,17 +56,15 @@ interface Select { interface IProps extends RouteComponentProps { siteId: string; slackChannels: any[]; - webhooks: any[]; loading: boolean; deleting: boolean; triggerOptions: any[]; list: any; onSubmit: (instance: Alert) => void; - fetchWebhooks: () => void; } const NewAlert = (props: IProps) => { - const { alertsStore } = useStore(); + const { alertsStore, settingsStore } = useStore(); const { fetchTriggerOptions, init, @@ -81,11 +78,10 @@ const NewAlert = (props: IProps) => { loading, } = alertsStore const deleting = loading - + const webhooks = settingsStore.webhooks + const fetchWebhooks = settingsStore.fetchWebhooks const { siteId, - webhooks, - fetchWebhooks, } = props; useEffect(() => { @@ -288,12 +284,4 @@ const NewAlert = (props: IProps) => { ); }; -export default withRouter( - connect( - (state) => ({ - // @ts-ignore - webhooks: state.getIn(['webhooks', 'list']), - }), - { fetchWebhooks } - )(observer(NewAlert)) -); +export default withRouter(observer(NewAlert)) diff --git a/frontend/app/duck/index.ts b/frontend/app/duck/index.ts index 5133d2a10..d9412eedc 100644 --- a/frontend/app/duck/index.ts +++ b/frontend/app/duck/index.ts @@ -13,7 +13,6 @@ import sources from './sources'; import members from './member'; import site from './site'; import customFields from './customField'; -import webhooks from './webhook'; import integrations from './integrations'; import rehydrate from './rehydrate'; import errors from './errors'; @@ -36,7 +35,6 @@ const rootReducer = combineReducers({ members, site, customFields, - webhooks, rehydrate, errors, funnels, diff --git a/frontend/app/duck/webhook.js b/frontend/app/duck/webhook.js deleted file mode 100644 index 8dc323a75..000000000 --- a/frontend/app/duck/webhook.js +++ /dev/null @@ -1,7 +0,0 @@ -import Webhook from 'Types/webhook'; -import crudDuckGenerator from './tools/crudDuck'; - -const crudDuck = crudDuckGenerator('webhook', Webhook, { idKey: 'webhookId' }); -export const { fetchList, init, edit, save, remove } = crudDuck.actions; - -export default crudDuck.reducer; diff --git a/frontend/app/mstore/index.tsx b/frontend/app/mstore/index.tsx index b9567c817..e177d86d0 100644 --- a/frontend/app/mstore/index.tsx +++ b/frontend/app/mstore/index.tsx @@ -6,17 +6,7 @@ import RoleStore from './roleStore'; import APIClient from 'App/api_client'; import FunnelStore from './funnelStore'; import { - dashboardService, - metricService, - sessionService, - userService, - auditService, - funnelService, - errorService, - notesService, - recordingsService, - configService, - alertsService, + services } from 'App/services'; import SettingsStore from './settingsStore'; import AuditStore from './auditStore'; @@ -69,17 +59,9 @@ export class RootStore { initClient() { const client = new APIClient(); - dashboardService.initClient(client); - metricService.initClient(client); - funnelService.initClient(client); - sessionService.initClient(client); - userService.initClient(client); - auditService.initClient(client); - errorService.initClient(client); - notesService.initClient(client) - recordingsService.initClient(client); - configService.initClient(client); - alertsService.initClient(client) + services.forEach(service => { + service.initClient(client); + }) } } diff --git a/frontend/app/mstore/settingsStore.ts b/frontend/app/mstore/settingsStore.ts index 45cb9610c..dc4f6baa4 100644 --- a/frontend/app/mstore/settingsStore.ts +++ b/frontend/app/mstore/settingsStore.ts @@ -2,44 +2,89 @@ import { makeAutoObservable, observable, action } from "mobx" import SessionSettings from "./types/sessionSettings" import { sessionService } from "App/services" import { toast } from 'react-toastify'; +import Webhook, { IWebhook } from 'Types/webhook'; +import { + webhookService +} from 'App/services'; +import Alert, { IAlert } from "Types/alert"; export default class SettingsStore { - loadingCaptureRate: boolean = false; - sessionSettings: SessionSettings = new SessionSettings() - captureRateFetched: boolean = false; - limits: any = null; + loadingCaptureRate: boolean = false; + sessionSettings: SessionSettings = new SessionSettings() + captureRateFetched: boolean = false; + limits: any = null; - constructor() { - makeAutoObservable(this, { - sessionSettings: observable, + webhooks: Webhook[] = [] + webhookInst = new Webhook() + + hooksLoading = false + + constructor() { + makeAutoObservable(this, { + sessionSettings: observable, + }) + } + + saveCaptureRate(data: any) { + return sessionService.saveCaptureRate(data) + .then(data => data.json()) + .then(({ data }) => { + this.sessionSettings.merge({ + captureRate: data.rate, + captureAll: data.captureAll }) - } + toast.success("Settings updated successfully"); + }).catch(err => { + toast.error("Error saving capture rate"); + }) + } - saveCaptureRate(data: any) { - return sessionService.saveCaptureRate(data) - .then(data => data.json()) - .then(({ data }) => { - this.sessionSettings.merge({ - captureRate: data.rate, - captureAll: data.captureAll - }) - toast.success("Settings updated successfully"); - }).catch(err => { - toast.error("Error saving capture rate"); - }) - } + fetchCaptureRate(): Promise { + this.loadingCaptureRate = true; + return sessionService.fetchCaptureRate() + .then(data => { + this.sessionSettings.merge({ + captureRate: data.rate, + captureAll: data.captureAll + }) + this.captureRateFetched = true; + }).finally(() => { + this.loadingCaptureRate = false; + }) + } - fetchCaptureRate(): Promise { - this.loadingCaptureRate = true; - return sessionService.fetchCaptureRate() - .then(data => { - this.sessionSettings.merge({ - captureRate: data.rate, - captureAll: data.captureAll - }) - this.captureRateFetched = true; - }).finally(() => { - this.loadingCaptureRate = false; - }) - } + fetchWebhooks = () => { + this.hooksLoading = true + return webhookService.fetchList() + .then(data => { + this.webhooks = data.map(hook => new Webhook(hook)) + this.hooksLoading = false + }) + } + + initWebhook = (inst: Partial | Webhook) => { + this.webhookInst = inst instanceof Webhook ? inst : new Webhook(inst) + } + + saveWebhook = (inst: Webhook) => { + this.hooksLoading = true + return webhookService.saveWebhook(inst) + .then(data => { + this.webhookInst = new Webhook(data) + this.hooksLoading = false + }) + } + + removeWebhook = (hookId: string) => { + this.hooksLoading = true + return webhookService.removeWebhook(hookId) + .then(() => { + this.webhooks = this.webhooks.filter(hook => hook.webhookId!== hookId) + this.hooksLoading = false + }) + } + + editWebhook = (diff: Partial) => { + Object.assign(this.webhookInst, diff) + } } diff --git a/frontend/app/services/WebhookService.ts b/frontend/app/services/WebhookService.ts new file mode 100644 index 000000000..2bcefa619 --- /dev/null +++ b/frontend/app/services/WebhookService.ts @@ -0,0 +1,25 @@ +import BaseService from './BaseService'; +import Webhook, { IWebhook } from "Types/webhook"; + +export default class WebhookService extends BaseService { + fetchList(): Promise { + return this.client.get('/webhooks') + .then(r => r.json()) + .then(j => j.data || []) + .catch(Promise.reject) + } + + saveWebhook(inst: Webhook) { + return this.client.put('/webhooks', inst) + .then(r => r.json()) + .then(j => j.data || {}) + .catch(Promise.reject) + } + + removeWebhook(id: Webhook["webhookId"]) { + return this.client.delete('/webhooks/' + 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 72d0f4d92..816113e68 100644 --- a/frontend/app/services/index.ts +++ b/frontend/app/services/index.ts @@ -9,6 +9,8 @@ import NotesService from "./NotesService"; import RecordingsService from "./RecordingsService"; import ConfigService from './ConfigService' import AlertsService from './AlertsService' +import WebhookService from './WebhookService' + export const dashboardService = new DashboardService(); export const metricService = new MetricService(); export const sessionService = new SessionSerivce(); @@ -19,4 +21,20 @@ 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(); \ No newline at end of file +export const alertsService = new AlertsService(); +export const webhookService = new WebhookService(); + +export const services = [ + dashboardService, + metricService, + sessionService, + userService, + funnelService, + auditService, + errorService, + notesService, + recordingsService, + configService, + alertsService, + webhookService, +] \ No newline at end of file diff --git a/frontend/app/types/webhook.js b/frontend/app/types/webhook.js deleted file mode 100644 index 5024411f4..000000000 --- a/frontend/app/types/webhook.js +++ /dev/null @@ -1,22 +0,0 @@ -import Record from 'Types/Record'; -import { validateName, validateURL } from 'App/validate'; - -export default Record({ - webhookId: undefined, - type: undefined, - name: '', - endpoint: '', - authHeader: '', -}, { - idKey: 'webhookId', - methods: { - validate() { - return !!this.name && validateName(this.name) && !!this.endpoint && validateURL(this.endpoint); - }, - toData() { - const js = this.toJS(); - delete js.key; - return js; - }, - }, -}); diff --git a/frontend/app/types/webhook.ts b/frontend/app/types/webhook.ts new file mode 100644 index 000000000..a8771a623 --- /dev/null +++ b/frontend/app/types/webhook.ts @@ -0,0 +1,36 @@ +import { validateName, validateURL } from 'App/validate'; +import { makeAutoObservable } from 'mobx' + +export interface IWebhook { + webhookId: string + type: string + name: string + endpoint: string + authHeader: string +} + +export default class Webhook { + webhookId: IWebhook["webhookId"] + type: IWebhook["type"] + name: IWebhook["name"] = '' + endpoint: IWebhook["endpoint"] = '' + authHeader: IWebhook["authHeader"] = '' + + constructor(data: Partial = {}) { + Object.assign(this, data) + + makeAutoObservable(this) + } + + toData() { + return { ...this }; + } + + validate() { + return !!this.name && validateName(this.name) && !!this.endpoint && validateURL(this.endpoint); + } + + exists() { + return !!this.webhookId + } +} \ No newline at end of file