change(ui): remove webhook reducer

This commit is contained in:
nick-delirium 2023-01-09 17:52:29 +01:00 committed by Delirium
parent ef090aa696
commit 8d7d183041
13 changed files with 297 additions and 283 deletions

View file

@ -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);

View file

@ -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 (
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
<h3 className="p-5 text-2xl">{webhook.exists() ? 'Update' : 'Add'} Webhook</h3>
<Form className={styles.wrapper}>
<Form.Field>
<label>{'Name'}</label>
<Input
ref={(ref) => {
this.focusElement = ref;
}}
name="name"
value={webhook.name}
onChange={this.write}
placeholder="Name"
/>
</Form.Field>
<Form.Field>
<label>{'Endpoint'}</label>
<Input
ref={(ref) => {
this.focusElement = ref;
}}
name="endpoint"
value={webhook.endpoint}
onChange={this.write}
placeholder="Endpoint"
/>
</Form.Field>
return (
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
<h3 className="p-5 text-2xl">{webhook.exists() ? 'Update' : 'Add'} Webhook</h3>
<Form className={styles.wrapper}>
<Form.Field>
<label>{'Name'}</label>
<Input
name="name"
value={webhook.name}
onChange={write}
placeholder="Name"
/>
</Form.Field>
<Form.Field>
<label>{'Auth Header (optional)'}</label>
<Input
ref={(ref) => {
this.focusElement = ref;
}}
name="authHeader"
value={webhook.authHeader}
onChange={this.write}
placeholder="Auth Header"
/>
</Form.Field>
<Form.Field>
<label>{'Endpoint'}</label>
<Input
name="endpoint"
value={webhook.endpoint}
onChange={write}
placeholder="Endpoint"
/>
</Form.Field>
<div className="flex justify-between">
<div className="flex items-center">
<Button
onClick={this.save}
disabled={!webhook.validate()}
loading={loading}
variant="primary"
className="float-left mr-2"
>
{webhook.exists() ? 'Update' : 'Add'}
</Button>
{webhook.exists() && <Button onClick={this.props.onClose}>{'Cancel'}</Button>}
</div>
{webhook.exists() && <Button icon="trash" variant="text" onClick={() => this.props.onDelete(webhook.webhookId)}></Button>}
<Form.Field>
<label>{'Auth Header (optional)'}</label>
<Input
name="authHeader"
value={webhook.authHeader}
onChange={write}
placeholder="Auth Header"
/>
</Form.Field>
<div className="flex justify-between">
<div className="flex items-center">
<Button
onClick={save}
disabled={!webhook.validate()}
loading={loading}
variant="primary"
className="float-left mr-2"
>
{webhook.exists() ? 'Update' : 'Add'}
</Button>
{webhook.exists() && <Button onClick={props.onClose}>{'Cancel'}</Button>}
</div>
</Form>
</div>
);
}
{webhook.exists() &&
<Button icon="trash" variant="text" onClick={() => props.onDelete(webhook.webhookId)}></Button>}
</div>
</Form>
</div>
);
}
export default WebhookForm;
export default observer(WebhookForm);

View file

@ -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(<WebhookForm onClose={hideModal} onDelete={removeWebhook} />);
};
const init = (v) => {
settingsStore.initWebhook(v);
showModal(<WebhookForm onClose={hideModal} onDelete={removeWebhook} />);
};
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 (
<div>
<div className={cn(styles.tabHeader, 'px-5 pt-5')}>
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Webhooks'}</h3>
{/* <Button rounded={true} icon="plus" variant="outline" onClick={() => init()} /> */}
<Button className="ml-auto" variant="primary" onClick={() => init()}>Add Webhook</Button>
</div>
<div className="text-base text-disabled-text flex items-center my-3 px-5">
<Icon name="info-circle-fill" className="mr-2" size={16} />
Leverage webhooks to push OpenReplay data to other systems.
</div>
<Loader loading={loading}>
<NoContent
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.NO_WEBHOOKS} size={80} />
<div className="text-center text-gray-600 my-4">None added yet</div>
return (
<div>
<div className={cn(styles.tabHeader, 'px-5 pt-5')}>
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Webhooks'}</h3>
<Button className="ml-auto" variant="primary" onClick={() => init()}>Add Webhook</Button>
</div>
}
size="small"
show={noSlackWebhooks.size === 0}
>
<div className="cursor-pointer">
{noSlackWebhooks.map((webhook) => (
<ListItem key={webhook.key} webhook={webhook} onEdit={() => init(webhook)} />
))}
</div>
</NoContent>
</Loader>
</div>
);
<div className="text-base text-disabled-text flex items-center my-3 px-5">
<Icon name="info-circle-fill" className="mr-2" size={16} />
Leverage webhooks to push OpenReplay data to other systems.
</div>
<Loader loading={loading}>
<NoContent
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.NO_WEBHOOKS} size={80} />
<div className="text-center text-gray-600 my-4">None added yet</div>
</div>
}
size="small"
show={noSlackWebhooks.length === 0}
>
<div className="cursor-pointer">
{noSlackWebhooks.map((webhook) => (
<ListItem key={webhook.key} webhook={webhook} onEdit={() => init(webhook)} />
))}
</div>
</NoContent>
</Loader>
</div>
);
}
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));

View file

@ -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<any>;
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);

View file

@ -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))

View file

@ -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,

View file

@ -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;

View file

@ -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);
})
}
}

View file

@ -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<any> {
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<any> {
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<IWebhook> | 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<IWebhook>) => {
Object.assign(this.webhookInst, diff)
}
}

View file

@ -0,0 +1,25 @@
import BaseService from './BaseService';
import Webhook, { IWebhook } from "Types/webhook";
export default class WebhookService extends BaseService {
fetchList(): Promise<IWebhook[]> {
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)
}
}

View file

@ -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();
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,
]

View file

@ -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;
},
},
});

View file

@ -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<IWebhook> = {}) {
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
}
}