diff --git a/frontend/app/components/Client/Integrations/BugsnagForm/ProjectListDropdown.js b/frontend/app/components/Client/Integrations/BugsnagForm/ProjectListDropdown.js index c30b57953..8fb9d5df2 100644 --- a/frontend/app/components/Client/Integrations/BugsnagForm/ProjectListDropdown.js +++ b/frontend/app/components/Client/Integrations/BugsnagForm/ProjectListDropdown.js @@ -1,17 +1,16 @@ import React from 'react'; import { connect } from 'react-redux'; import { tokenRE } from 'Types/integrations/bugsnagConfig'; -import { edit } from 'Duck/integrations/actions'; import Select from 'Shared/Select'; import { withRequest } from 'HOCs'; @connect(state => ({ token: state.getIn([ 'bugsnag', 'instance', 'authorizationToken' ]) -}), { edit }) +})) @withRequest({ dataName: "projects", initialData: [], - dataWrapper: (data = [], prevData) => { + dataWrapper: (data = []) => { if (!Array.isArray(data)) throw new Error('Wrong responce format.'); const withOrgName = data.length > 1; return data.reduce((accum, { name: orgName, projects }) => { @@ -35,15 +34,7 @@ export default class ProjectListDropdown extends React.PureComponent { if (!tokenRE.test(token)) return; this.props.fetchProjectList({ authorizationToken: token, - }).then(() => { - const { value, projects } = this.props; - const values = projects.map(p => p.id); - if (!values.includes(value) && values.length > 0) { - this.props.edit("bugsnag", { - projectId: values[0], - }); - } - }); + }) } componentDidUpdate(prevProps) { if (prevProps.token !== this.props.token) { diff --git a/frontend/app/components/Client/Integrations/CloudwatchForm/CloudwatchForm.js b/frontend/app/components/Client/Integrations/CloudwatchForm/CloudwatchForm.js index ca4e6ae3b..003545e23 100644 --- a/frontend/app/components/Client/Integrations/CloudwatchForm/CloudwatchForm.js +++ b/frontend/app/components/Client/Integrations/CloudwatchForm/CloudwatchForm.js @@ -1,41 +1,53 @@ +import { + ACCESS_KEY_ID_LENGTH, + SECRET_ACCESS_KEY_LENGTH, +} from 'Types/integrations/cloudwatchConfig'; import React from 'react'; -import { ACCESS_KEY_ID_LENGTH, SECRET_ACCESS_KEY_LENGTH } from 'Types/integrations/cloudwatchConfig'; + +import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModalCard'; + +import DocLink from 'Shared/DocLink/DocLink'; + import IntegrationForm from '../IntegrationForm'; import LogGroupDropdown from './LogGroupDropdown'; import RegionDropdown from './RegionDropdown'; -import DocLink from 'Shared/DocLink/DocLink'; -import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModalCard'; const CloudwatchForm = (props) => ( -
- -
-
How it works?
+
+ +
+
How it works?
  1. Create a Service Account
  2. Enter the details below
  3. Propagate openReplaySessionToken
- +
( checkIfDisplayed: (config) => config.awsSecretAccessKey.length === SECRET_ACCESS_KEY_LENGTH && config.region !== '' && - config.awsAccessKeyId.length === ACCESS_KEY_ID_LENGTH - } + config.awsAccessKeyId.length === ACCESS_KEY_ID_LENGTH, + }, ]} />
diff --git a/frontend/app/components/Client/Integrations/ElasticsearchForm.js b/frontend/app/components/Client/Integrations/ElasticsearchForm.js index 2c30cea47..8c5c3d7f6 100644 --- a/frontend/app/components/Client/Integrations/ElasticsearchForm.js +++ b/frontend/app/components/Client/Integrations/ElasticsearchForm.js @@ -1,97 +1,64 @@ import React from 'react'; -import { connect } from 'react-redux'; -import IntegrationForm from './IntegrationForm'; -import { withRequest } from 'HOCs'; -import { edit } from 'Duck/integrations/actions'; -import DocLink from 'Shared/DocLink/DocLink'; import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModalCard'; -@connect( - (state) => ({ - config: state.getIn(['elasticsearch', 'instance']) - }), - { edit } -) -@withRequest({ - dataName: 'isValid', - initialData: false, - dataWrapper: (data) => data.state, - requestName: 'validateConfig', - endpoint: '/integrations/elasticsearch/test', - method: 'POST' -}) -export default class ElasticsearchForm extends React.PureComponent { - componentWillReceiveProps(newProps) { - const { - config: { host, port, apiKeyId, apiKey } - } = this.props; - const { loading, config } = newProps; - const valuesChanged = host !== config.host || port !== config.port || apiKeyId !== config.apiKeyId || apiKey !== config.apiKey; - if (!loading && valuesChanged && newProps.config.validateKeys() && newProps) { - this.validateConfig(newProps); - } - } +import DocLink from 'Shared/DocLink/DocLink'; - validateConfig = (newProps) => { - const { config } = newProps; - this.props - .validateConfig({ - host: config.host, - port: config.port, - apiKeyId: config.apiKeyId, - apiKey: config.apiKey - }) - .then((res) => { - const { isValid } = this.props; - this.props.edit('elasticsearch', { isValid: isValid }); - }); - }; +import IntegrationForm from './IntegrationForm'; - render() { - const props = this.props; - return ( -
- +const ElasticsearchForm = (props) => { + return ( +
+ -
-
How it works?
-
    -
  1. Create a new Elastic API key
  2. -
  3. Enter the API key below
  4. -
  5. Propagate openReplaySessionToken
  6. -
- -
- +
How it works?
+
    +
  1. Create a new Elastic API key
  2. +
  3. Enter the API key below
  4. +
  5. Propagate openReplaySessionToken
  6. +
+
- ); - } -} + +
+ ); +}; + +export default ElasticsearchForm; diff --git a/frontend/app/components/Client/Integrations/IntegrationForm.js b/frontend/app/components/Client/Integrations/IntegrationForm.js deleted file mode 100644 index c4d634562..000000000 --- a/frontend/app/components/Client/Integrations/IntegrationForm.js +++ /dev/null @@ -1,142 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { Input, Form, Button, Checkbox, Loader } from 'UI'; -import { save, init, edit, remove } from 'Duck/integrations/actions'; -import { fetchIntegrationList } from 'Duck/integrations/integrations'; - -@connect( - (state, { name, customPath }) => ({ - sites: state.getIn(['site', 'list']), - initialSiteId: state.getIn(['site', 'siteId']), - list: state.getIn([name, 'list']), - config: state.getIn([name, 'instance']), - loading: state.getIn([name, 'fetchRequest', 'loading']), - saving: state.getIn([customPath || name, 'saveRequest', 'loading']), - removing: state.getIn([name, 'removeRequest', 'loading']), - siteId: state.getIn(['integrations', 'siteId']), - }), - { - save, - init, - edit, - remove, - // fetchList, - fetchIntegrationList, - } -) -export default class IntegrationForm extends React.PureComponent { - constructor(props) { - super(props); - } - - fetchList = () => { - const { siteId, initialSiteId } = this.props; - if (!siteId) { - this.props.fetchIntegrationList(initialSiteId); - } else { - this.props.fetchIntegrationList(siteId); - } - } - - write = ({ target: { value, name: key, type, checked } }) => { - if (type === 'checkbox') this.props.edit(this.props.name, { [key]: checked }); - else this.props.edit(this.props.name, { [key]: value }); - }; - - // onChangeSelect = ({ value }) => { - // const { sites, list, name } = this.props; - // const site = sites.find((s) => s.id === value.value); - // this.setState({ currentSiteId: site.id }); - // this.init(value.value); - // }; - - // init = (siteId) => { - // const { list, name } = this.props; - // const config = parseInt(siteId) > 0 ? list.find((s) => s.projectId === siteId) : undefined; - // this.props.init(name, config ? config : list.first()); - // }; - - save = () => { - const { config, name, customPath, ignoreProject } = this.props; - const isExists = config.exists(); - // const { currentSiteId } = this.state; - this.props.save(customPath || name, !ignoreProject ? this.props.siteId : null, config).then(() => { - // this.props.fetchList(name); - this.fetchList(); - this.props.onClose(); - if (isExists) return; - }); - }; - - remove = () => { - const { name, config, ignoreProject } = this.props; - this.props.remove(name, !ignoreProject ? config.projectId : null).then(() => { - this.props.onClose(); - this.fetchList(); - }); - }; - - render() { - const { config, saving, removing, formFields, name, loading, integrated } = this.props; - return ( - -
-
- {formFields.map( - ({ - key, - label, - placeholder = label, - component: Component = 'input', - type = 'text', - checkIfDisplayed, - autoFocus = false, - }) => - (typeof checkIfDisplayed !== 'function' || checkIfDisplayed(config)) && - (type === 'checkbox' ? ( - - - - ) : ( - - - - - )) - )} - - - - {integrated && ( - - )} -
-
-
- ); - } -} diff --git a/frontend/app/components/Client/Integrations/IntegrationForm.tsx b/frontend/app/components/Client/Integrations/IntegrationForm.tsx new file mode 100644 index 000000000..905b41d70 --- /dev/null +++ b/frontend/app/components/Client/Integrations/IntegrationForm.tsx @@ -0,0 +1,110 @@ +import { observer } from 'mobx-react-lite'; +import React from 'react'; +import { connect } from 'react-redux'; + +import { useStore } from 'App/mstore'; +import { namedStore } from 'App/mstore/integrationsStore'; +import { Button, Checkbox, Form, Input, Loader } from 'UI'; + +function IntegrationForm(props: any) { + const { formFields, name, integrated } = props; + const { integrationsStore } = useStore(); + const integrationStore = integrationsStore[name as unknown as namedStore]; + const config = integrationStore.instance; + const loading = integrationStore.loading; + const onSave = integrationStore.saveIntegration; + const onRemove = integrationStore.deleteIntegration; + const edit = integrationStore.edit; + const fetchIntegrationList = integrationsStore.integrations.fetchIntegrations; + + const fetchList = () => { + void fetchIntegrationList(props.initialSiteId); + }; + + const write = ({ target: { value, name: key, type, checked } }) => { + if (type === 'checkbox') edit({ [key]: checked }); + else edit({ [key]: value }); + }; + + const save = () => { + const { name, customPath } = props; + onSave(customPath || name).then(() => { + fetchList(); + props.onClose(); + }); + }; + + const remove = () => { + onRemove().then(() => { + props.onClose(); + fetchList(); + }); + }; + + return ( + +
+
+ {formFields.map( + ({ + key, + label, + placeholder = label, + component: Component = 'input', + type = 'text', + checkIfDisplayed, + autoFocus = false, + }) => + (typeof checkIfDisplayed !== 'function' || + checkIfDisplayed(config)) && + (type === 'checkbox' ? ( + + + + ) : ( + + + + + )) + )} + + + + {integrated && ( + + )} +
+
+
+ ); +} + +export default connect((state: any) => ({ + sites: state.getIn(['site', 'list']), + initialSiteId: state.getIn(['site', 'siteId']), +}))(observer(IntegrationForm)); diff --git a/frontend/app/components/Client/Integrations/Integrations.tsx b/frontend/app/components/Client/Integrations/Integrations.tsx index e81a7038a..31cd2e46d 100644 --- a/frontend/app/components/Client/Integrations/Integrations.tsx +++ b/frontend/app/components/Client/Integrations/Integrations.tsx @@ -1,88 +1,95 @@ +import withPageTitle from 'HOCs/withPageTitle'; +import cn from 'classnames'; +import { observer } from 'mobx-react-lite'; import React, { useEffect, useState } from 'react'; import { connect } from 'react-redux'; + import { useModal } from 'App/components/Modal'; -import cn from 'classnames'; +import { useStore } from 'App/mstore'; +import IntegrationFilters from 'Components/Client/Integrations/IntegrationFilters'; +import { PageTitle } from 'UI'; -import { fetch, init } from 'Duck/integrations/actions'; -import { fetchIntegrationList, setSiteId } from 'Duck/integrations/integrations'; -import SiteDropdown from 'Shared/SiteDropdown'; -import ReduxDoc from './ReduxDoc'; -import VueDoc from './VueDoc'; -import GraphQLDoc from './GraphQLDoc'; -import NgRxDoc from './NgRxDoc'; -import MobxDoc from './MobxDoc'; -import ProfilerDoc from './ProfilerDoc'; -import AssistDoc from './AssistDoc'; -import PiniaDoc from './PiniaDoc'; -import ZustandDoc from './ZustandDoc'; -import MSTeams from './Teams'; import DocCard from 'Shared/DocCard/DocCard'; -import { PageTitle, Tooltip } from 'UI'; -import withPageTitle from 'HOCs/withPageTitle'; +import AssistDoc from './AssistDoc'; import BugsnagForm from './BugsnagForm'; import CloudwatchForm from './CloudwatchForm'; import DatadogForm from './DatadogForm'; import ElasticsearchForm from './ElasticsearchForm'; import GithubForm from './GithubForm'; +import GraphQLDoc from './GraphQLDoc'; import IntegrationItem from './IntegrationItem'; import JiraForm from './JiraForm'; +import MobxDoc from './MobxDoc'; import NewrelicForm from './NewrelicForm'; +import NgRxDoc from './NgRxDoc'; +import PiniaDoc from './PiniaDoc'; +import ProfilerDoc from './ProfilerDoc'; +import ReduxDoc from './ReduxDoc'; import RollbarForm from './RollbarForm'; import SentryForm from './SentryForm'; import SlackForm from './SlackForm'; import StackdriverForm from './StackdriverForm'; import SumoLogicForm from './SumoLogicForm'; -import IntegrationFilters from 'Components/Client/Integrations/IntegrationFilters'; +import MSTeams from './Teams'; +import VueDoc from './VueDoc'; +import ZustandDoc from './ZustandDoc'; interface Props { - fetch: (name: string, siteId: string) => void; - init: () => void; - fetchIntegrationList: (siteId: any) => void; - integratedList: any; - initialSiteId: string; - setSiteId: (siteId: string) => void; siteId: string; hideHeader?: boolean; - loading?: boolean; } function Integrations(props: Props) { - const { initialSiteId, hideHeader = false, loading = false } = props; + const { integrationsStore } = useStore(); + + const fetchIntegrationList = integrationsStore.integrations.fetchIntegrations; + const storeIntegratedList = integrationsStore.integrations.list; + const { siteId, hideHeader = false } = props; const { showModal } = useModal(); const [integratedList, setIntegratedList] = useState([]); const [activeFilter, setActiveFilter] = useState('all'); useEffect(() => { - const list = props.integratedList + const list = storeIntegratedList .filter((item: any) => item.integrated) .map((item: any) => item.name); setIntegratedList(list); - }, [props.integratedList]); + }, [storeIntegratedList]); useEffect(() => { - props.fetchIntegrationList(initialSiteId); - props.setSiteId(initialSiteId); - }, []); + void fetchIntegrationList(siteId); + }, [siteId]); const onClick = (integration: any, width: number) => { - if (integration.slug && integration.slug !== 'slack' && integration.slug !== 'msteams') { - props.fetch(integration.slug, props.siteId); + if ( + integration.slug && + integration.slug !== 'slack' && + integration.slug !== 'msteams' + ) { + const intName = integration.slug as + | 'sentry' + | 'bugsnag' + | 'rollbar' + | 'elasticsearch' + | 'datadog' + | 'sumologic' + | 'stackdriver' + | 'cloudwatch' + | 'newrelic'; + if (integrationsStore[intName]) { + void integrationsStore[intName].fetchIntegration(siteId); + } } showModal( React.cloneElement(integration.component, { - integrated: integratedList.includes(integration.slug) + integrated: integratedList.includes(integration.slug), }), { right: true, width } ); }; - const onChangeSelect = ({ value }: any) => { - props.setSiteId(value.value); - props.fetchIntegrationList(value.value); - }; - const onChange = (key: string) => { setActiveFilter(key); }; @@ -99,83 +106,91 @@ function Integrations(props: Props) { key: cat.key, title: cat.title, label: cat.title, - icon: cat.icon - })) - - - const allIntegrations = filteredIntegrations.flatMap(cat => cat.integrations); - + icon: cat.icon, + })); + const allIntegrations = filteredIntegrations.flatMap( + (cat) => cat.integrations + ); return ( <> -
+
{!hideHeader && Integrations
} />} - +
-
+
-
+`)} + > {allIntegrations.map((integration: any) => ( - onClick(integration, filteredIntegrations.find(cat => cat.integrations.includes(integration)).title === 'Plugins' ? 500 : 350) + onClick( + integration, + filteredIntegrations.find((cat) => + cat.integrations.includes(integration) + ).title === 'Plugins' + ? 500 + : 350 + ) } hide={ (integration.slug === 'github' && integratedList.includes('jira')) || - (integration.slug === 'jira' && - integratedList.includes('github')) + (integration.slug === 'jira' && integratedList.includes('github')) } /> ))}
- ); } -export default connect( - (state: any) => ({ - initialSiteId: state.getIn(['site', 'siteId']), - integratedList: state.getIn(['integrations', 'list']) || [], - loading: state.getIn(['integrations', 'fetchRequest', 'loading']), - siteId: state.getIn(['integrations', 'siteId']) - }), - { fetch, init, fetchIntegrationList, setSiteId } -)(withPageTitle('Integrations - OpenReplay Preferences')(Integrations)); - +export default connect((state: any) => ({ + siteId: state.getIn(['site', 'siteId']), +}))( + withPageTitle('Integrations - OpenReplay Preferences')(observer(Integrations)) +); const integrations = [ { title: 'Issue Reporting', key: 'issue-reporting', - description: 'Seamlessly report issues or share issues with your team right from OpenReplay.', + description: + 'Seamlessly report issues or share issues with your team right from OpenReplay.', isProject: false, icon: 'exclamation-triangle', integrations: [ { title: 'Jira', - subtitle: 'Integrate Jira with OpenReplay to enable the creation of a new ticket directly from a session.', + subtitle: + 'Integrate Jira with OpenReplay to enable the creation of a new ticket directly from a session.', slug: 'jira', category: 'Errors', icon: 'integrations/jira', - component: + component: , }, { title: 'Github', - subtitle: 'Integrate GitHub with OpenReplay to enable the direct creation of a new issue from a session.', + subtitle: + 'Integrate GitHub with OpenReplay to enable the direct creation of a new issue from a session.', slug: 'github', category: 'Errors', icon: 'integrations/github', - component: - } - ] + component: , + }, + ], }, { title: 'Backend Logging', @@ -186,106 +201,119 @@ const integrations = [ 'Sync your backend errors with sessions replays and see what happened front-to-back.', docs: () => ( - Sync your backend errors with sessions replays and see what happened front-to-back. + Sync your backend errors with sessions replays and see what happened + front-to-back. ), integrations: [ { title: 'Sentry', - subtitle: 'Integrate Sentry with session replays to seamlessly observe backend errors.', + subtitle: + 'Integrate Sentry with session replays to seamlessly observe backend errors.', slug: 'sentry', icon: 'integrations/sentry', - component: + component: , }, { title: 'Bugsnag', - subtitle: 'Integrate Bugsnag to access the OpenReplay session linked to the JS exception within its interface.', + subtitle: + 'Integrate Bugsnag to access the OpenReplay session linked to the JS exception within its interface.', slug: 'bugsnag', icon: 'integrations/bugsnag', - component: + component: , }, { title: 'Rollbar', - subtitle: 'Integrate Rollbar with session replays to seamlessly observe backend errors.', + subtitle: + 'Integrate Rollbar with session replays to seamlessly observe backend errors.', slug: 'rollbar', icon: 'integrations/rollbar', - component: + component: , }, { title: 'Elasticsearch', - subtitle: 'Integrate Elasticsearch with session replays to seamlessly observe backend errors.', + subtitle: + 'Integrate Elasticsearch with session replays to seamlessly observe backend errors.', slug: 'elasticsearch', icon: 'integrations/elasticsearch', - component: + component: , }, { title: 'Datadog', - subtitle: 'Incorporate DataDog to visualize backend errors alongside session replay, for easy troubleshooting.', + subtitle: + 'Incorporate DataDog to visualize backend errors alongside session replay, for easy troubleshooting.', slug: 'datadog', icon: 'integrations/datadog', - component: + component: , }, { title: 'Sumo Logic', - subtitle: 'Integrate Sumo Logic with session replays to seamlessly observe backend errors.', + subtitle: + 'Integrate Sumo Logic with session replays to seamlessly observe backend errors.', slug: 'sumologic', icon: 'integrations/sumologic', - component: + component: , }, { title: 'Google Cloud', - subtitle: 'Integrate Google Cloud to view backend logs and errors in conjunction with session replay', + subtitle: + 'Integrate Google Cloud to view backend logs and errors in conjunction with session replay', slug: 'stackdriver', icon: 'integrations/google-cloud', - component: + component: , }, { title: 'CloudWatch', - subtitle: 'Integrate CloudWatch to see backend logs and errors alongside session replay.', + subtitle: + 'Integrate CloudWatch to see backend logs and errors alongside session replay.', slug: 'cloudwatch', icon: 'integrations/aws', - component: + component: , }, { title: 'Newrelic', - subtitle: 'Integrate NewRelic with session replays to seamlessly observe backend errors.', + subtitle: + 'Integrate NewRelic with session replays to seamlessly observe backend errors.', slug: 'newrelic', icon: 'integrations/newrelic', - component: - } - ] + component: , + }, + ], }, { title: 'Collaboration', key: 'collaboration', isProject: false, icon: 'file-code', - description: 'Share your sessions with your team and collaborate on issues.', + description: + 'Share your sessions with your team and collaborate on issues.', integrations: [ { title: 'Slack', - subtitle: 'Integrate Slack to empower every user in your org with the ability to send sessions to any Slack channel.', + subtitle: + 'Integrate Slack to empower every user in your org with the ability to send sessions to any Slack channel.', slug: 'slack', category: 'Errors', icon: 'integrations/slack', component: , - shared: true + shared: true, }, { title: 'MS Teams', - subtitle: 'Integrate MS Teams to empower every user in your org with the ability to send sessions to any MS Teams channel.', + subtitle: + 'Integrate MS Teams to empower every user in your org with the ability to send sessions to any MS Teams channel.', slug: 'msteams', category: 'Errors', icon: 'integrations/teams', component: , - shared: true - } - ] + shared: true, + }, + ], }, // { // title: 'State Management', @@ -302,72 +330,82 @@ const integrations = [ icon: 'chat-left-text', docs: () => ( - Plugins capture your application’s store, monitor queries, track performance issues and even - assist your end user through live sessions. + Plugins capture your application’s store, monitor queries, track + performance issues and even assist your end user through live sessions. ), description: - 'Reproduce issues as if they happened in your own browser. Plugins help capture your application\'s store, HTTP requeets, GraphQL queries, and more.', + "Reproduce issues as if they happened in your own browser. Plugins help capture your application's store, HTTP requeets, GraphQL queries, and more.", integrations: [ { title: 'Redux', - subtitle: 'Capture Redux actions/state and inspect them later on while replaying session recordings.', - icon: 'integrations/redux', component: + subtitle: + 'Capture Redux actions/state and inspect them later on while replaying session recordings.', + icon: 'integrations/redux', + component: , }, { title: 'VueX', - subtitle: 'Capture VueX mutations/state and inspect them later on while replaying session recordings.', + subtitle: + 'Capture VueX mutations/state and inspect them later on while replaying session recordings.', icon: 'integrations/vuejs', - component: + component: , }, { title: 'Pinia', - subtitle: 'Capture Pinia mutations/state and inspect them later on while replaying session recordings.', + subtitle: + 'Capture Pinia mutations/state and inspect them later on while replaying session recordings.', icon: 'integrations/pinia', - component: + component: , }, { title: 'GraphQL', - subtitle: 'Capture GraphQL requests and inspect them later on while replaying session recordings. This plugin is compatible with Apollo and Relay implementations.', + subtitle: + 'Capture GraphQL requests and inspect them later on while replaying session recordings. This plugin is compatible with Apollo and Relay implementations.', icon: 'integrations/graphql', - component: + component: , }, { title: 'NgRx', - subtitle: 'Capture NgRx actions/state and inspect them later on while replaying session recordings.\n', + subtitle: + 'Capture NgRx actions/state and inspect them later on while replaying session recordings.\n', icon: 'integrations/ngrx', - component: + component: , }, { title: 'MobX', - subtitle: 'Capture MobX mutations and inspect them later on while replaying session recordings.', + subtitle: + 'Capture MobX mutations and inspect them later on while replaying session recordings.', icon: 'integrations/mobx', - component: + component: , }, { title: 'Profiler', - subtitle: 'Plugin allows you to measure your JS functions performance and capture both arguments and result for each call.', + subtitle: + 'Plugin allows you to measure your JS functions performance and capture both arguments and result for each call.', icon: 'integrations/openreplay', - component: + component: , }, { title: 'Assist', - subtitle: 'OpenReplay Assist allows you to support your users by seeing their live screen and instantly hopping on call (WebRTC) with them without requiring any 3rd-party screen sharing software.\n', + subtitle: + 'OpenReplay Assist allows you to support your users by seeing their live screen and instantly hopping on call (WebRTC) with them without requiring any 3rd-party screen sharing software.\n', icon: 'integrations/openreplay', - component: + component: , }, { title: 'Zustand', - subtitle: 'Capture Zustand mutations/state and inspect them later on while replaying session recordings.', + subtitle: + 'Capture Zustand mutations/state and inspect them later on while replaying session recordings.', icon: 'integrations/zustand', // header: '🐻', - component: - } - ] - } + component: , + }, + ], + }, ]; diff --git a/frontend/app/components/Client/Integrations/MobxDoc/MobxDoc.js b/frontend/app/components/Client/Integrations/MobxDoc/MobxDoc.js index 65705b1e0..fb1c1ec3a 100644 --- a/frontend/app/components/Client/Integrations/MobxDoc/MobxDoc.js +++ b/frontend/app/components/Client/Integrations/MobxDoc/MobxDoc.js @@ -3,9 +3,14 @@ import ToggleContent from 'Shared/ToggleContent'; import DocLink from 'Shared/DocLink/DocLink'; import { connect } from 'react-redux'; import { CodeBlock } from "UI"; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; const MobxDoc = (props) => { - const { projectKey } = props; + const { integrationsStore } = useStore(); + const sites = props.sites ? props.sites.toJS() : [] + const siteId = integrationsStore.integrations.siteId + const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey const mobxUsage = `import OpenReplay from '@openreplay/tracker'; import trackerMobX from '@openreplay/tracker-mobx'; @@ -68,9 +73,8 @@ function SomeFunctionalComponent() { MobxDoc.displayName = 'MobxDoc'; export default connect((state) => { - const siteId = state.getIn(['integrations', 'siteId']); const sites = state.getIn(['site', 'list']); return { - projectKey: sites.find((site) => site.get('id') === siteId).get('projectKey'), + sites, }; -})(MobxDoc); +})(observer(MobxDoc)) diff --git a/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js b/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js index 26cfc9520..60edc363d 100644 --- a/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js +++ b/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js @@ -1,25 +1,36 @@ import React from 'react'; import { connect } from 'react-redux'; -import { edit, save, init, update } from 'Duck/integrations/slack'; import { Form, Input, Button, Message } from 'UI'; import { confirm } from 'UI'; -import { remove } from 'Duck/integrations/slack'; +import { observer } from 'mobx-react-lite' +import { useStore } from 'App/mstore' -class SlackAddForm extends React.PureComponent { - componentWillUnmount() { - this.props.init({}); - } +function SlackAddForm(props) { + const { onClose } = props; + const { integrationsStore } = useStore(); + const instance = integrationsStore.slack.instance; + const saving = integrationsStore.slack.loading; + const errors = integrationsStore.slack.errors; + const edit = integrationsStore.slack.edit; + const onSave = integrationsStore.slack.saveIntegration; + const update = integrationsStore.slack.update; + const init = integrationsStore.slack.init; + const onRemove = integrationsStore.slack.removeInt; + + React.useEffect(() => { + return () => init({}) + }, []) - save = () => { - const instance = this.props.instance; + + const save = () => { if (instance.exists()) { - this.props.update(this.props.instance); + void update(instance); } else { - this.props.save(this.props.instance); + void onSave(instance); } }; - remove = async (id) => { + const remove = async (id) => { if ( await confirm({ header: 'Confirm', @@ -27,79 +38,68 @@ class SlackAddForm extends React.PureComponent { confirmation: `Are you sure you want to permanently delete this channel?`, }) ) { - this.props.remove(id); + await onRemove(id); + onClose(); } }; - write = ({ target: { name, value } }) => this.props.edit({ [name]: value }); - - render() { - const { instance, saving, errors, onClose } = this.props; - return ( -
-
- - - - - - - - -
-
- - - -
- - -
-
- {errors && ( -
- {errors.map((error) => ( - - {error} - - ))} +
- )} -
- ); - } + + +
+ + + {errors && ( +
+ {errors.map((error) => ( + + {error} + + ))} +
+ )} +
+ ); } -export default connect( - (state) => ({ - instance: state.getIn(['slack', 'instance']), - saving: - state.getIn(['slack', 'saveRequest', 'loading']) || - state.getIn(['slack', 'updateRequest', 'loading']), - errors: state.getIn(['slack', 'saveRequest', 'errors']), - }), - { edit, save, init, remove, update } -)(SlackAddForm); +export default observer(SlackAddForm); diff --git a/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js b/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js index 8d25b4454..db53d3100 100644 --- a/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js +++ b/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js @@ -1,14 +1,16 @@ import React from 'react'; -import { connect } from 'react-redux'; import { NoContent } from 'UI'; -import { remove, edit, init } from 'Duck/integrations/slack'; import DocLink from 'Shared/DocLink/DocLink'; +import { observer } from 'mobx-react-lite' +import { useStore } from 'App/mstore' function SlackChannelList(props) { - const { list } = props; + const { integrationsStore } = useStore(); + const list = integrationsStore.slack.list; + const edit = integrationsStore.slack.edit; const onEdit = (instance) => { - props.edit(instance); + edit(instance.toData()); props.onEdit(); }; @@ -24,7 +26,7 @@ function SlackChannelList(props) {
} size="small" - show={list.size === 0} + show={list.length === 0} > {list.map((c) => (
({ - list: state.getIn(['slack', 'list']), - }), - { remove, edit, init } -)(SlackChannelList); +export default observer(SlackChannelList); diff --git a/frontend/app/components/Client/Integrations/SlackForm.tsx b/frontend/app/components/Client/Integrations/SlackForm.tsx index 018dbe885..43a720da4 100644 --- a/frontend/app/components/Client/Integrations/SlackForm.tsx +++ b/frontend/app/components/Client/Integrations/SlackForm.tsx @@ -1,17 +1,14 @@ import React, { useEffect } from 'react'; import SlackChannelList from './SlackChannelList/SlackChannelList'; -import { fetchList, init } from 'Duck/integrations/slack'; -import { connect } from 'react-redux'; import SlackAddForm from './SlackAddForm'; import { Button } from 'UI'; +import { observer } from 'mobx-react-lite' +import { useStore } from 'App/mstore' -interface Props { - onEdit?: (integration: any) => void; - istance: any; - fetchList: any; - init: any; -} -const SlackForm = (props: Props) => { +const SlackForm = () => { + const { integrationsStore } = useStore(); + const init = integrationsStore.slack.init; + const fetchList = integrationsStore.slack.fetchIntegrations; const [active, setActive] = React.useState(false); const onEdit = () => { @@ -20,11 +17,11 @@ const SlackForm = (props: Props) => { const onNew = () => { setActive(true); - props.init({}); + init({}); } useEffect(() => { - props.fetchList(); + void fetchList(); }, []); return ( @@ -47,9 +44,4 @@ const SlackForm = (props: Props) => { SlackForm.displayName = 'SlackForm'; -export default connect( - (state: any) => ({ - istance: state.getIn(['slack', 'instance']), - }), - { fetchList, init } -)(SlackForm); +export default observer(SlackForm); \ No newline at end of file diff --git a/frontend/app/components/Client/Integrations/Teams/TeamsAddForm.tsx b/frontend/app/components/Client/Integrations/Teams/TeamsAddForm.tsx index f13efc535..e45d6d7b1 100644 --- a/frontend/app/components/Client/Integrations/Teams/TeamsAddForm.tsx +++ b/frontend/app/components/Client/Integrations/Teams/TeamsAddForm.tsx @@ -1,36 +1,38 @@ +import { observer } from 'mobx-react-lite'; import React from 'react'; -import { connect } from 'react-redux'; -import { edit, save, init, update, remove } from 'Duck/integrations/teams'; -import { Form, Input, Button, Message } from 'UI'; + +import { useStore } from 'App/mstore'; +import { Button, Form, Input, Message } from 'UI'; import { confirm } from 'UI'; interface Props { - edit: (inst: any) => void; - save: (inst: any) => void; - init: (inst: any) => void; - update: (inst: any) => void; - remove: (id: string) => void; onClose: () => void; - instance: any; - saving: boolean; - errors: any; } -class TeamsAddForm extends React.PureComponent { - componentWillUnmount() { - this.props.init({}); - } +function TeamsAddForm({ onClose }: Props) { + const { integrationsStore } = useStore(); + const instance = integrationsStore.msteams.instance; + const saving = integrationsStore.msteams.loading; + const errors = integrationsStore.msteams.errors; + const edit = integrationsStore.msteams.edit; + const onSave = integrationsStore.msteams.saveIntegration; + const init = integrationsStore.msteams.init; + const onRemove = integrationsStore.msteams.removeInt; + const update = integrationsStore.msteams.update; - save = () => { - const instance = this.props.instance; - if (instance.exists()) { - this.props.update(this.props.instance); + React.useEffect(() => { + return () => init({}); + }, []); + + const save = () => { + if (instance?.exists()) { + void update(); } else { - this.props.save(this.props.instance); + void onSave(); } }; - remove = async (id: string) => { + const remove = async (id: string) => { if ( await confirm({ header: 'Confirm', @@ -38,80 +40,74 @@ class TeamsAddForm extends React.PureComponent { confirmation: `Are you sure you want to permanently delete this channel?`, }) ) { - this.props.remove(id); + void onRemove(id); } }; - write = ({ target: { name, value } }: { target: { name: string; value: string } }) => - this.props.edit({ [name]: value }); + const write = ({ + target: { name, value }, + }: { + target: { name: string; value: string }; + }) => edit({ [name]: value }); - render() { - const { instance, saving, errors, onClose } = this.props; - return ( -
-
- - - - - - - - -
-
- - - -
- - -
-
- {errors && ( -
- {errors.map((error: any) => ( - - {error} - - ))} +
- )} -
- ); - } + + +
+ + + {errors && ( +
+ {errors.map((error: any) => ( + + {error} + + ))} +
+ )} +
+ ); } -export default connect( - (state: any) => ({ - instance: state.getIn(['teams', 'instance']), - saving: - state.getIn(['teams', 'saveRequest', 'loading']) || - state.getIn(['teams', 'updateRequest', 'loading']), - errors: state.getIn(['teams', 'saveRequest', 'errors']), - }), - { edit, save, init, remove, update } -)(TeamsAddForm); +export default observer(TeamsAddForm); diff --git a/frontend/app/components/Client/Integrations/Teams/TeamsChannelList.tsx b/frontend/app/components/Client/Integrations/Teams/TeamsChannelList.tsx index 942e1e32c..131a404c8 100644 --- a/frontend/app/components/Client/Integrations/Teams/TeamsChannelList.tsx +++ b/frontend/app/components/Client/Integrations/Teams/TeamsChannelList.tsx @@ -1,51 +1,57 @@ +import { observer } from 'mobx-react-lite'; import React from 'react'; -import { connect } from 'react-redux'; + +import { useStore } from 'App/mstore'; import { NoContent } from 'UI'; -import { remove, edit, init } from 'Duck/integrations/teams'; + import DocLink from 'Shared/DocLink/DocLink'; -function TeamsChannelList(props: { list: any, edit: (inst: any) => any, onEdit: () => void }) { - const { list } = props; +function TeamsChannelList(props: { onEdit: () => void }) { + const { integrationsStore } = useStore(); + const list = integrationsStore.msteams.list; + const edit = integrationsStore.msteams.edit; - const onEdit = (instance: Record) => { - props.edit(instance); - props.onEdit(); - }; + const onEdit = (instance: Record) => { + edit(instance); + props.onEdit(); + }; - return ( -
- -
- Integrate MS Teams with OpenReplay and share insights with the rest of the team, directly from the recording page. -
- -
- } - size="small" - show={list.size === 0} - > - {list.map((c: any) => ( -
onEdit(c)} - > -
-
{c.name}
-
{c.endpoint}
-
-
- ))} - - - ); + return ( +
+ +
+ Integrate MS Teams with OpenReplay and share insights with the + rest of the team, directly from the recording page. +
+ +
+ } + size="small" + show={list.length === 0} + > + {list.map((c: any) => ( +
onEdit(c)} + > +
+
{c.name}
+
+ {c.endpoint} +
+
+
+ ))} + + + ); } -export default connect( - (state: any) => ({ - list: state.getIn(['teams', 'list']), - }), - { remove, edit, init } -)(TeamsChannelList); +export default observer(TeamsChannelList); diff --git a/frontend/app/components/Client/Integrations/Teams/index.tsx b/frontend/app/components/Client/Integrations/Teams/index.tsx index e51bd64b1..b85a4f010 100644 --- a/frontend/app/components/Client/Integrations/Teams/index.tsx +++ b/frontend/app/components/Client/Integrations/Teams/index.tsx @@ -1,17 +1,15 @@ import React, { useEffect } from 'react'; import TeamsChannelList from './TeamsChannelList'; -import { fetchList, init } from 'Duck/integrations/teams'; -import { connect } from 'react-redux'; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; + import TeamsAddForm from './TeamsAddForm'; import { Button } from 'UI'; -interface Props { - onEdit?: (integration: any) => void; - istance: any; - fetchList: any; - init: any; -} -const MSTeams = (props: Props) => { +const MSTeams = () => { + const { integrationsStore } = useStore(); + const fetchList = integrationsStore.msteams.fetchIntegrations; + const init = integrationsStore.msteams.init; const [active, setActive] = React.useState(false); const onEdit = () => { @@ -20,11 +18,11 @@ const MSTeams = (props: Props) => { const onNew = () => { setActive(true); - props.init({}); + init({}); } useEffect(() => { - props.fetchList(); + void fetchList(); }, []); return ( @@ -47,9 +45,4 @@ const MSTeams = (props: Props) => { MSTeams.displayName = 'MSTeams'; -export default connect( - (state: any) => ({ - istance: state.getIn(['teams', 'instance']), - }), - { fetchList, init } -)(MSTeams); +export default observer(MSTeams); diff --git a/frontend/app/components/Session/LiveSession.js b/frontend/app/components/Session/LiveSession.js index a9bba11b9..eec861e04 100644 --- a/frontend/app/components/Session/LiveSession.js +++ b/frontend/app/components/Session/LiveSession.js @@ -3,28 +3,29 @@ import { useEffect } from 'react'; import { connect } from 'react-redux'; import usePageTitle from 'App/hooks/usePageTitle'; import { fetch as fetchSession, clearCurrentSession } from 'Duck/sessions'; -import { fetchList as fetchSlackList } from 'Duck/integrations/slack'; import { Loader } from 'UI'; import withPermissions from 'HOCs/withPermissions'; import LivePlayer from './LivePlayer'; import { clearLogs } from 'App/dev/console'; import { toast } from 'react-toastify'; +import { useStore } from 'App/mstore' function LiveSession({ sessionId, fetchSession, - fetchSlackList, hasSessionsPath, session, fetchFailed, clearCurrentSession, }) { + const { integrationsStore } = useStore(); + const fetchSlackList = integrationsStore.slack.fetchIntegrations; const [initialLoading, setInitialLoading] = React.useState(true); usePageTitle('OpenReplay Assist'); useEffect(() => { clearLogs(); - fetchSlackList(); + void fetchSlackList(); return () => { clearCurrentSession() @@ -77,7 +78,6 @@ export default withPermissions(['ASSIST_LIVE', 'SERVICE_ASSIST_LIVE'], '', true, }, { fetchSession, - fetchSlackList, clearCurrentSession, } )(LiveSession) diff --git a/frontend/app/components/Session/MobilePlayer.tsx b/frontend/app/components/Session/MobilePlayer.tsx index 16f4a9178..c03efefa9 100644 --- a/frontend/app/components/Session/MobilePlayer.tsx +++ b/frontend/app/components/Session/MobilePlayer.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useState } from 'react'; import { connect } from 'react-redux'; import { Modal, Loader } from 'UI'; -import { fetchList } from 'Duck/integrations'; import { createIOSPlayer } from 'Player'; import { makeAutoObservable } from 'mobx'; import withLocationHandlers from 'HOCs/withLocationHandlers'; @@ -24,7 +23,7 @@ let playerInst: IOSPlayerContext['player'] | undefined; function MobilePlayer(props: any) { const { session, fetchList } = props; - const { notesStore, sessionStore, uiPlayerStore } = useStore(); + const { notesStore, sessionStore, uiPlayerStore, integrationsStore } = useStore(); const [activeTab, setActiveTab] = useState(''); const [noteItem, setNoteItem] = useState(undefined); // @ts-ignore @@ -37,7 +36,7 @@ function MobilePlayer(props: any) { useEffect(() => { playerInst = undefined; if (!session.sessionId || contextValue.player !== undefined) return; - fetchList('issues'); + void integrationsStore.issues.fetchIntegrations(); sessionStore.setUserTimezone(session.timezone); const [IOSPlayerInst, PlayerStore] = createIOSPlayer( session, diff --git a/frontend/app/components/Session/Session.tsx b/frontend/app/components/Session/Session.tsx index 026ae0c7b..02ab206a5 100644 --- a/frontend/app/components/Session/Session.tsx +++ b/frontend/app/components/Session/Session.tsx @@ -8,7 +8,6 @@ import usePageTitle from 'App/hooks/usePageTitle'; import { useStore } from 'App/mstore'; import { sessions as sessionsRoute } from 'App/routes'; import MobilePlayer from 'Components/Session/MobilePlayer'; -import { fetchList as fetchSlackList } from 'Duck/integrations/slack'; import { clearCurrentSession, fetchV2 } from 'Duck/sessions'; import { Link, Loader, NoContent } from 'UI'; @@ -89,7 +88,6 @@ export default withPermissions( }; }, { - fetchSlackList, fetchV2, clearCurrentSession, } diff --git a/frontend/app/components/Session/WebPlayer.tsx b/frontend/app/components/Session/WebPlayer.tsx index 93970d515..8ccd9f49e 100644 --- a/frontend/app/components/Session/WebPlayer.tsx +++ b/frontend/app/components/Session/WebPlayer.tsx @@ -9,7 +9,6 @@ import { toast } from 'react-toastify'; import { useStore } from 'App/mstore'; import { Note } from 'App/services/NotesService'; -import { fetchList } from 'Duck/integrations'; import { Loader, Modal } from 'UI'; import ReadNote from '../Session_/Player/Controls/components/ReadNote'; @@ -36,10 +35,9 @@ let playerInst: IPlayerContext['player'] | undefined; function WebPlayer(props: any) { const { session, - fetchList, startedAt, } = props; - const { notesStore, sessionStore, uxtestingStore, uiPlayerStore } = useStore(); + const { notesStore, sessionStore, uxtestingStore, uiPlayerStore, integrationsStore } = useStore(); const fullscreen = uiPlayerStore.fullscreen; const toggleFullscreen = uiPlayerStore.toggleFullscreen; const closeBottomBlock = uiPlayerStore.closeBottomBlock; @@ -72,7 +70,7 @@ function WebPlayer(props: any) { | Record | undefined; const usePrefetched = props.prefetched && mobData?.data; - fetchList('issues'); + void integrationsStore.issues.fetchIntegrations(); sessionStore.setUserTimezone(session.timezone); const [WebPlayerInst, PlayerStore] = createWebPlayer( session, @@ -256,7 +254,5 @@ export default connect( jwt: state.getIn(['user', 'jwt']), startedAt: state.getIn(['sessions', 'current']).startedAt || 0, }), - { - fetchList, - } + )(withLocationHandlers()(observer(WebPlayer))); diff --git a/frontend/app/components/Session_/Player/Controls/components/CreateNote.tsx b/frontend/app/components/Session_/Player/Controls/components/CreateNote.tsx index 3da4a31d7..4d4eb49b6 100644 --- a/frontend/app/components/Session_/Player/Controls/components/CreateNote.tsx +++ b/frontend/app/components/Session_/Player/Controls/components/CreateNote.tsx @@ -1,10 +1,9 @@ import { Tag } from 'antd'; -import { List } from 'immutable'; import { Duration } from 'luxon'; import React from 'react'; import { connect } from 'react-redux'; import { toast } from 'react-toastify'; - +import { observer } from 'mobx-react-lite'; import { useStore } from 'App/mstore'; import { Note, @@ -13,8 +12,6 @@ import { iTag, tagProps, } from 'App/services/NotesService'; -import { fetchList as fetchSlack } from 'Duck/integrations/slack'; -import { fetchList as fetchTeams } from 'Duck/integrations/teams'; import { addNote, updateNote } from 'Duck/sessions'; import { Button, Checkbox, Icon } from 'UI'; @@ -27,10 +24,6 @@ interface Props { sessionId: string; isEdit?: boolean; editNote?: WriteNote; - slackChannels: List>; - teamsChannels: List>; - fetchSlack: () => void; - fetchTeams: () => void; hideModal: () => void; } @@ -40,12 +33,13 @@ function CreateNote({ isEdit, editNote, updateNote, - slackChannels, - fetchSlack, - teamsChannels, - fetchTeams, hideModal, }: Props) { + const { notesStore, integrationsStore } = useStore(); + const slackChannels = integrationsStore.slack.list; + const fetchSlack = integrationsStore.slack.fetchIntegrations; + const teamsChannels = integrationsStore.msteams.list; + const fetchTeams = integrationsStore.msteams.fetchIntegrations; const [text, setText] = React.useState(''); const [slackChannel, setSlackChannel] = React.useState(''); const [teamsChannel, setTeamsChannel] = React.useState(''); @@ -56,7 +50,6 @@ function CreateNote({ const [useTeams, setTeams] = React.useState(false); const inputRef = React.createRef(); - const { notesStore } = useStore(); React.useEffect(() => { if (isEdit && editNote) { @@ -151,14 +144,12 @@ function CreateNote({ .map(({ webhookId, name }) => ({ value: webhookId, label: name, - })) - .toJS() as unknown as { value: string; label: string }[]; + })) as unknown as { value: string; label: string }[]; const teamsChannelsOptions = teamsChannels .map(({ webhookId, name }) => ({ value: webhookId, label: name, - })) - .toJS() as unknown as { value: string; label: string }[]; + })) as unknown as { value: string; label: string }[]; slackChannelsOptions.unshift({ // @ts-ignore @@ -334,10 +325,8 @@ function CreateNote({ export default connect( (state: any) => { - const slackChannels = state.getIn(['slack', 'list']); - const teamsChannels = state.getIn(['teams', 'list']); const sessionId = state.getIn(['sessions', 'current']).sessionId; - return { sessionId, slackChannels, teamsChannels }; + return { sessionId }; }, - { addNote, updateNote, fetchSlack, fetchTeams } -)(CreateNote); + { addNote, updateNote,} +)(observer(CreateNote)); diff --git a/frontend/app/components/shared/SharePopup/SharePopup.tsx b/frontend/app/components/shared/SharePopup/SharePopup.tsx index ce7955924..66b61ef81 100644 --- a/frontend/app/components/shared/SharePopup/SharePopup.tsx +++ b/frontend/app/components/shared/SharePopup/SharePopup.tsx @@ -7,11 +7,10 @@ import styles from './sharePopup.module.css'; import IntegrateSlackButton from '../IntegrateSlackButton/IntegrateSlackButton'; import SessionCopyLink from './SessionCopyLink'; import Select from 'Shared/Select'; -import { fetchList as fetchSlack, sendSlackMsg } from 'Duck/integrations/slack'; -import { fetchList as fetchTeams, sendMsTeamsMsg } from 'Duck/integrations/teams'; import { Button, Segmented } from 'antd'; import { PlayerContext } from 'App/components/Session/playerContext'; import { observer } from 'mobx-react-lite'; +import { useStore } from 'App/mstore'; interface Msg { integrationId: string; @@ -51,15 +50,7 @@ const SharePopup = ({ interface Props { sessionId: string; - channels: { webhookId: string; name: string }[]; - slackLoaded: boolean; - msTeamsChannels: { webhookId: string; name: string }[]; - msTeamsLoaded: boolean; tenantId: string; - fetchSlack: () => void; - fetchTeams: () => void; - sendSlackMsg: (msg: Msg) => any; - sendMsTeamsMsg: (msg: Msg) => any; showCopyLink?: boolean; hideModal: () => void; time: number; @@ -67,18 +58,20 @@ interface Props { function ShareModalComp({ sessionId, - sendSlackMsg, - sendMsTeamsMsg, showCopyLink, - channels, - slackLoaded, - msTeamsChannels, - msTeamsLoaded, - fetchSlack, - fetchTeams, hideModal, time, }: Props) { + const { integrationsStore } = useStore(); + const channels = integrationsStore.slack.list; + const slackLoaded = integrationsStore.slack.loaded; + const msTeamsChannels = integrationsStore.msteams.list; + const msTeamsLoaded = integrationsStore.msteams.loaded; + const fetchSlack = integrationsStore.slack.fetchIntegrations; + const fetchTeams = integrationsStore.msteams.fetchIntegrations; + const sendSlackMsg = integrationsStore.slack.sendMessage; + const sendMsTeamsMsg = integrationsStore.msteams.sendMessage; + const [shareTo, setShareTo] = useState('slack'); const [comment, setComment] = useState(''); // @ts-ignore @@ -104,7 +97,7 @@ function ShareModalComp({ const editMessage = (e: React.ChangeEvent) => setComment(e.target.value); const shareToSlack = () => { setLoadingSlack(true); - sendSlackMsg({ + void sendSlackMsg({ integrationId: channelId, entity: 'sessions', entityId: sessionId, @@ -140,16 +133,12 @@ function ShareModalComp({ value: webhookId, label: name, })) - // @ts-ignore - .toJS(); const msTeamsOptions = msTeamsChannels .map(({ webhookId, name }) => ({ value: webhookId, label: name, })) - // @ts-ignore - .toJS(); const sendMsg = () => { if (shareTo === 'slack') { @@ -279,18 +268,9 @@ function ShareModalComp({ const mapStateToProps = (state: Record) => ({ sessionId: state.getIn(['sessions', 'current']).sessionId, - channels: state.getIn(['slack', 'list']), - slackLoaded: state.getIn(['slack', 'loaded']), - msTeamsChannels: state.getIn(['teams', 'list']), - msTeamsLoaded: state.getIn(['teams', 'loaded']), tenantId: state.getIn(['user', 'account', 'tenantId']), }); -const ShareModal = connect(mapStateToProps, { - fetchSlack, - fetchTeams, - sendSlackMsg, - sendMsTeamsMsg, -})(ShareModalComp); +const ShareModal = connect(mapStateToProps)(ShareModalComp); export default observer(SharePopup); diff --git a/frontend/app/duck/integrations/teams.js b/frontend/app/duck/integrations/teams.js index e77835a37..29fac4710 100644 --- a/frontend/app/duck/integrations/teams.js +++ b/frontend/app/duck/integrations/teams.js @@ -93,8 +93,6 @@ export function remove(id) { }; } -// https://api.openreplay.com/5587/integrations/msteams/notify/315/sessions/7856803626558104 -// export function sendMsTeamsMsg({ integrationId, entity, entityId, data }) { return { types: SEND_MSG.toArray(), diff --git a/frontend/app/mstore/index.tsx b/frontend/app/mstore/index.tsx index 723655adb..b127fa585 100644 --- a/frontend/app/mstore/index.tsx +++ b/frontend/app/mstore/index.tsx @@ -27,6 +27,7 @@ import FilterStore from './filterStore'; import UiPlayerStore from './uiPlayerStore'; import IssueReportingStore from './issueReportingStore'; import CustomFieldStore from './customFieldStore'; +import { IntegrationsStore } from "./integrationsStore"; export class RootStore { dashboardStore: DashboardStore; @@ -55,6 +56,7 @@ export class RootStore { uiPlayerStore: UiPlayerStore; issueReportingStore: IssueReportingStore; customFieldStore: CustomFieldStore; + integrationsStore: IntegrationsStore constructor() { this.dashboardStore = new DashboardStore(); @@ -83,6 +85,7 @@ export class RootStore { this.uiPlayerStore = new UiPlayerStore(); this.issueReportingStore = new IssueReportingStore(); this.customFieldStore = new CustomFieldStore(); + this.integrationsStore = new IntegrationsStore(); } initClient() { diff --git a/frontend/app/mstore/integrationsStore.ts b/frontend/app/mstore/integrationsStore.ts index e10e23158..fd3b7fc8c 100644 --- a/frontend/app/mstore/integrationsStore.ts +++ b/frontend/app/mstore/integrationsStore.ts @@ -1,7 +1,9 @@ import { makeAutoObservable } from 'mobx'; import { integrationsService } from 'App/services'; +import ElasticsearchForm from "../components/Client/Integrations/ElasticsearchForm"; +import { MessengerConfig } from './types/integrations/messengers'; import { Bugsnag, Cloudwatch, @@ -16,11 +18,12 @@ import { SentryInt, StackDriverInt, SumoLogic, -} from './types/integrations'; +} from './types/integrations/services'; class GenericIntegrationsStore { list: any[] = []; - siteId: string | null = null; + isLoading: boolean = false; + siteId: string = ''; constructor() { makeAutoObservable(this); } @@ -33,9 +36,20 @@ class GenericIntegrationsStore { this.list = list; } - fetchIntegrations = async () => { - //client.get(`/${siteID}/integrations`) - // this.setList() + setLoading(loading: boolean) { + this.isLoading = loading; + } + + fetchIntegrations = async (siteId?: string) => { + this.setLoading(true); + try { + const { data } = await integrationsService.fetchList(siteId); + this.setList(data); + } catch (e) { + console.log(e); + } finally { + this.setLoading(false); + } }; } @@ -44,83 +58,235 @@ class NamedIntegrationStore { list: T[] = []; fetched: boolean = false; issuesFetched: boolean = false; + loading = false; constructor( private readonly name: string, - private readonly NamedType: new (config: Record) => T + private readonly namedTypeCreator: (config: Record) => T ) { + this.instance = namedTypeCreator({}); makeAutoObservable(this); } + setLoading(loading: boolean): void { + this.loading = loading; + } + setInstance(instance: T): void { this.instance = instance; } - setList(list: T[]): void { + setList = (list: T[]): void => { this.list = list; } - setFetched(fetched: boolean): void { + setFetched = (fetched: boolean): void => { this.fetched = fetched; } - setIssuesFetched(issuesFetched: boolean): void { + setIssuesFetched = (issuesFetched: boolean): void => { this.issuesFetched = issuesFetched; } fetchIntegrations = async (): Promise => { - const { data } = await integrationsService.fetchList(this.name); - this.setList( - data.map((config: Record) => new this.NamedType(config)) - ); + this.setLoading(true); + try { + const { data } = await integrationsService.fetchList(this.name); + this.setList( + data.map((config: Record) => this.namedTypeCreator(config)) + ); + } catch (e) { + console.log(e); + } finally { + this.setFetched(true); + this.setLoading(false); + } }; - fetchIntegration = async (siteId: string): void => { - const { data } = await integrationsService.fetchIntegration( - this.name, - siteId - ); - this.setInstance(new this.NamedType(data)); + fetchIntegration = async (siteId: string): Promise => { + this.setLoading(true); + try { + const { data } = await integrationsService.fetchIntegration( + this.name, + siteId + ); + this.setInstance(this.namedTypeCreator(data)); + } catch (e) { + console.log(e); + } finally { + this.setLoading(false); + } }; - saveIntegration(name: string, siteId: string): void { + saveIntegration = async (name: string, siteId?: string): Promise => { if (!this.instance) return; - const response = integrationsService.saveIntegration( - name, - siteId, - this.instance.toData() + await integrationsService.saveIntegration( + this.name ?? name, + this.instance.toData(), + siteId ); return; } - edit(data: T): void { - this.setInstance(data); + edit = (data: T): void => { + if (!this.instance) { + this.instance = this.namedTypeCreator({}); + } + this.instance.edit(data); } - deleteIntegration(siteId: string) { + deleteIntegration = async (siteId?: string) => { if (!this.instance) return; return integrationsService.removeIntegration(this.name, siteId); } - init(config: Record): void { - this.instance = new this.NamedType(config); + init = (config: Record): void => { + this.instance = this.namedTypeCreator(config); } } -export class IntegrationsStore { - sentry = new NamedIntegrationStore('sentry', SentryInt); - datadog = new NamedIntegrationStore('datadog', DatadogInt); - stackdriver = new NamedIntegrationStore('stackdriver', StackDriverInt); - rollbar = new NamedIntegrationStore('rollbar', RollbarInt); - newrelic = new NamedIntegrationStore('newrelic', NewRelicInt); - bugsnag = new NamedIntegrationStore('bugsnag', Bugsnag); - cloudwatch = new NamedIntegrationStore('cloudwatch', Cloudwatch); - elasticsearch = new NamedIntegrationStore('elasticsearch', ElasticSearchInt); - sumologic = new NamedIntegrationStore('sumologic', SumoLogic); - jira = new NamedIntegrationStore('jira', JiraInt); - github = new NamedIntegrationStore('github', GithubInt); - issues = new NamedIntegrationStore('issues', IssueTracker); - integrations = new GenericIntegrationsStore(); - // + slack - // + teams +class MessengerIntegrationStore { + list: MessengerConfig[] = []; + instance: MessengerConfig | null = null; + loaded: boolean = false; + loading: boolean = false; + errors: any[] = []; + + constructor(private readonly mName: 'slack' | 'msteams') { + makeAutoObservable(this); + } + + setList(list: MessengerConfig[]): void { + this.list = list; + } + + setLoading(loading: boolean): void { + this.loading = loading; + } + + setInstance(instance: MessengerConfig): void { + this.instance = instance; + } + + setLoaded(loaded: boolean): void { + this.loaded = loaded; + } + + setErrors = (errors: any[]) => { + this.errors = errors; + }; + + saveIntegration = async (): Promise => { + // redux todo: errors + if (!this.instance) return; + this.setLoading(true); + try { + await integrationsService.saveIntegration( + this.mName, + this.instance.toData(), + undefined + ); + this.setList([...this.list, this.instance]); + } catch (e) { + console.log(e); + this.setErrors(["Couldn't process the request: check your data."]); + } finally { + this.setLoading(false); + } + }; + + fetchIntegrations = async (): Promise => { + const { data } = await integrationsService.fetchMessengerChannels( + this.mName + ); + this.setList( + data.map((config: Record) => new MessengerConfig(config)) + ); + this.setLoaded(true); + }; + + sendMessage = ({ + integrationId, + entity, + entityId, + data, + }: { + integrationId: string; + entity: string; + entityId: string; + data: any; + }) => { + return integrationsService.sendMsg( + integrationId, + entity, + entityId, + this.mName, + data + ); + }; + + init = (config: Record): void => { + this.instance = new MessengerConfig(config); + }; + + removeInt = async (intId: string) => { + await integrationsService.removeMessengerInt(this.mName, intId); + this.setList(this.list.filter((int) => int.webhookId !== intId)); + }; + + edit = (data: Record): void => { + if (!this.instance) { + this.instance = new MessengerConfig({}); + } + this.instance.edit(data); + }; + + update = async () => { + // redux todo: errors + if (!this.instance) return; + this.setLoading(true); + await integrationsService.updateMessengerInt( + this.mName, + this.instance.toData() + ); + this.setList( + this.list.map((int) => + int.webhookId === this.instance?.webhookId ? this.instance : int + ) + ); + this.setLoading(false); + }; +} +export type namedStore = 'sentry' + | 'datadog' + | 'stackdriver' + | 'rollbar' + | 'newrelic' + | 'bugsnag' + | 'cloudwatch' + | 'elasticsearch' + | 'sumologic' + | 'jira' + | 'github' + | 'issues' +export class IntegrationsStore { + sentry = new NamedIntegrationStore('sentry', (d) => new SentryInt(d)); + datadog = new NamedIntegrationStore('datadog', (d) => new DatadogInt(d)); + stackdriver = new NamedIntegrationStore('stackdriver', (d) => new StackDriverInt(d)); + rollbar = new NamedIntegrationStore('rollbar', (d) => new RollbarInt(d)); + newrelic = new NamedIntegrationStore('newrelic', (d) => new NewRelicInt(d)); + bugsnag = new NamedIntegrationStore('bugsnag', (d) => new Bugsnag(d)); + cloudwatch = new NamedIntegrationStore('cloudwatch', (d) => new Cloudwatch(d)); + elasticsearch = new NamedIntegrationStore('elasticsearch', (d) => new ElasticSearchInt(d)); + sumologic = new NamedIntegrationStore('sumologic', (d) => new SumoLogic(d)); + jira = new NamedIntegrationStore('jira', (d) => new JiraInt(d)); + github = new NamedIntegrationStore('github', (d) => new GithubInt(d)); + issues = new NamedIntegrationStore('issues', (d) => new IssueTracker(d)); + integrations = new GenericIntegrationsStore(); + slack = new MessengerIntegrationStore('slack'); + msteams = new MessengerIntegrationStore('msteams'); + + constructor() { + makeAutoObservable(this); + } } diff --git a/frontend/app/mstore/types/integrations/consts.ts b/frontend/app/mstore/types/integrations/consts.ts new file mode 100644 index 000000000..6e9561657 --- /dev/null +++ b/frontend/app/mstore/types/integrations/consts.ts @@ -0,0 +1,37 @@ +export const sumoRegionLabels = { + au: 'Asia Pacific (Sydney)', + ca: 'Canada (Central)', + de: 'EU (Frankfurt)', + eu: 'EU (Ireland)', + fed: 'US East (N. Virginia)', + in: 'Asia Pacific (Mumbai)', + jp: 'Asia Pacific (Tokyo)', + us1: 'US East (N. Virginia)', + us2: 'US West (Oregon)', +}; +export const API_KEY_ID_LENGTH = 5; +export const API_KEY_LENGTH = 5; +export const SECRET_ACCESS_KEY_LENGTH = 40; +export const ACCESS_KEY_ID_LENGTH = 20; +export const tokenRE = + /^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$/i; +export const awsRegionLabels = { + 'us-east-1': 'US East (N. Virginia)', + 'us-east-2': 'US East (Ohio)', + 'us-west-1': 'US West (N. California)', + 'us-west-2': 'US West (Oregon)', + 'ap-east-1': 'Asia Pacific (Hong Kong)', + 'ap-south-1': 'Asia Pacific (Mumbai)', + 'ap-northeast-2': 'Asia Pacific (Seoul)', + 'ap-southeast-1': 'Asia Pacific (Singapore)', + 'ap-southeast-2': 'Asia Pacific (Sydney)', + 'ap-northeast-1': 'Asia Pacific (Tokyo)', + 'ca-central-1': 'Canada (Central)', + 'eu-central-1': 'EU (Frankfurt)', + 'eu-west-1': 'EU (Ireland)', + 'eu-west-2': 'EU (London)', + 'eu-west-3': 'EU (Paris)', + 'eu-north-1': 'EU (Stockholm)', + 'me-south-1': 'Middle East (Bahrain)', + 'sa-east-1': 'South America (São Paulo)', +}; diff --git a/frontend/app/mstore/types/integrations/messengers.ts b/frontend/app/mstore/types/integrations/messengers.ts new file mode 100644 index 000000000..1e6ae9300 --- /dev/null +++ b/frontend/app/mstore/types/integrations/messengers.ts @@ -0,0 +1,41 @@ +import { validateURL } from "App/validate"; +import { makeAutoObservable } from "mobx"; + +export class MessengerConfig { + endpoint: string = ""; + name: string = ""; + webhookId: string = ""; + + constructor(config: any) { + Object.assign(this, { + endpoint: config.endpoint, + name: config.name, + webhookId: config.webhookId + }); + makeAutoObservable(this); + } + + edit = (data: any): void => { + Object.keys(data).forEach((key) => { + // @ts-ignore + this[key] = data[key]; + }) + } + + validate(): boolean { + return this.endpoint !== '' && this.name != '' && validateURL(this.endpoint); + } + + exists(): boolean { + return !!this.webhookId; + } + + toData(): { endpoint: string, url: string, name: string, webhookId: string } { + return { + endpoint: this.endpoint, + url: this.endpoint, + name: this.name, + webhookId: this.webhookId + }; + } +} diff --git a/frontend/app/mstore/types/integrations.ts b/frontend/app/mstore/types/integrations/services.ts similarity index 73% rename from frontend/app/mstore/types/integrations.ts rename to frontend/app/mstore/types/integrations/services.ts index 3b3eec493..0acbdc687 100644 --- a/frontend/app/mstore/types/integrations.ts +++ b/frontend/app/mstore/types/integrations/services.ts @@ -2,10 +2,21 @@ import { makeAutoObservable } from 'mobx'; import { validateURL } from 'App/validate'; +import { + ACCESS_KEY_ID_LENGTH, + API_KEY_ID_LENGTH, + API_KEY_LENGTH, + SECRET_ACCESS_KEY_LENGTH, + awsRegionLabels, + sumoRegionLabels, + tokenRE, +} from './consts'; + export interface Integration { validate(): boolean; exists(): boolean; toData(): Record; + edit(data: Record): void; } export class SentryInt implements Integration { @@ -17,10 +28,17 @@ export class SentryInt implements Integration { constructor(config: any) { Object.assign(this, { ...config, - projectId: config.projectId || -1, + projectId: config?.projectId ?? -1, }); + makeAutoObservable(this); } + edit = (data: Record) => { + Object.keys(data).forEach((key) => { + this[key] = data[key]; + }); + }; + validate() { return Boolean(this.organizationSlug && this.projectSlug && this.token); } @@ -47,10 +65,17 @@ export class DatadogInt implements Integration { constructor(config: any) { Object.assign(this, { ...config, - projectId: config.projectId || -1, + projectId: config?.projectId ?? -1, }); + makeAutoObservable(this); } + edit = (data: Record) => { + Object.keys(data).forEach((key) => { + this[key] = data[key]; + }); + }; + validate() { return Boolean(this.apiKey && this.applicationKey); } @@ -76,10 +101,17 @@ export class StackDriverInt implements Integration { constructor(config: any) { Object.assign(this, { ...config, - projectId: config.projectId || -1, + projectId: config?.projectId ?? -1, }); + makeAutoObservable(this); } + edit = (data: Record) => { + Object.keys(data).forEach((key) => { + this[key] = data[key]; + }); + }; + validate() { return Boolean( this.serviceAccountCredentials !== '' && this.logName !== '' @@ -106,10 +138,17 @@ export class RollbarInt implements Integration { constructor(config: any) { Object.assign(this, { ...config, - projectId: config.projectId || -1, + projectId: config?.projectId ?? -1, }); + makeAutoObservable(this); } + edit = (data: Record) => { + Object.keys(data).forEach((key) => { + this[key] = data[key]; + }); + }; + validate() { return Boolean(this.accessToken); } @@ -135,10 +174,17 @@ export class NewRelicInt implements Integration { constructor(config: any) { Object.assign(this, { ...config, - projectId: config.projectId || -1, + projectId: config?.projectId ?? -1, }); + makeAutoObservable(this); } + edit = (data: Record) => { + Object.keys(data).forEach((key) => { + this[key] = data[key]; + }); + }; + validate() { return Boolean(this.applicationId && this.xQueryKey); } @@ -165,10 +211,17 @@ export class Bugsnag implements Integration { constructor(config: any) { Object.assign(this, { ...config, - projectId: config.projectId || -1, + projectId: config?.projectId ?? -1, }); + makeAutoObservable(this); } + edit = (data: Record) => { + Object.keys(data).forEach((key) => { + this[key] = data[key]; + }); + }; + validate() { return Boolean( this.bugsnagProjectId !== '' && tokenRE.test(this.authorizationToken) @@ -198,10 +251,17 @@ export class Cloudwatch implements Integration { constructor(config: any) { Object.assign(this, { ...config, - projectId: config.projectId || -1, + projectId: config?.projectId ?? -1, }); + makeAutoObservable(this); } + edit = (data: Record) => { + Object.keys(data).forEach((key) => { + this[key] = data[key]; + }); + }; + validate() { return Boolean( this.awsAccessKeyId !== '' && @@ -237,10 +297,17 @@ export class ElasticSearchInt implements Integration { constructor(config: any) { Object.assign(this, { ...config, - projectId: config.projectId || -1, + projectId: config?.projectId ?? -1, }); + makeAutoObservable(this); } + edit = (data: Record) => { + Object.keys(data).forEach((key) => { + this[key] = data[key]; + }); + }; + private validateKeys() { return Boolean( this.apiKeyId.length > API_KEY_ID_LENGTH && @@ -285,10 +352,17 @@ export class SumoLogic implements Integration { constructor(config: any) { Object.assign(this, { ...config, - projectId: config.projectId || -1, + projectId: config?.projectId ?? -1, }); + makeAutoObservable(this); } + edit = (data: Record) => { + Object.keys(data).forEach((key) => { + this[key] = data[key]; + }); + }; + validate() { return Boolean(this.accessKey && this.accessId); } @@ -316,10 +390,17 @@ export class JiraInt implements Integration { constructor(config: any) { Object.assign(this, { ...config, - projectId: config.projectId || -1, + projectId: config?.projectId ?? -1, }); + makeAutoObservable(this); } + edit = (data: Record) => { + Object.keys(data).forEach((key) => { + this[key] = data[key]; + }); + }; + validateFetchProjects() { return this.username !== '' && this.token !== '' && validateURL(this.url); } @@ -350,10 +431,17 @@ export class GithubInt implements Integration { constructor(config: any) { Object.assign(this, { ...config, - projectId: config.projectId || -1, + projectId: config?.projectId ?? -1, }); + makeAutoObservable(this); } + edit = (data: Record) => { + Object.keys(data).forEach((key) => { + this[key] = data[key]; + }); + }; + validate() { return this.token !== ''; } @@ -381,8 +469,15 @@ export class IssueTracker implements Integration { Object.assign(this, { ...config, }); + makeAutoObservable(this); } + edit = (data: Record) => { + Object.keys(data).forEach((key) => { + this[key] = data[key]; + }); + }; + validateFetchProjects() { return this.username !== '' && this.token !== '' && validateURL(this.url); } @@ -404,41 +499,3 @@ export class IssueTracker implements Integration { }; } } - -export const sumoRegionLabels = { - au: 'Asia Pacific (Sydney)', - ca: 'Canada (Central)', - de: 'EU (Frankfurt)', - eu: 'EU (Ireland)', - fed: 'US East (N. Virginia)', - in: 'Asia Pacific (Mumbai)', - jp: 'Asia Pacific (Tokyo)', - us1: 'US East (N. Virginia)', - us2: 'US West (Oregon)', -}; -export const API_KEY_ID_LENGTH = 5; -export const API_KEY_LENGTH = 5; -export const SECRET_ACCESS_KEY_LENGTH = 40; -export const ACCESS_KEY_ID_LENGTH = 20; -export const tokenRE = - /^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$/i; -export const awsRegionLabels = { - 'us-east-1': 'US East (N. Virginia)', - 'us-east-2': 'US East (Ohio)', - 'us-west-1': 'US West (N. California)', - 'us-west-2': 'US West (Oregon)', - 'ap-east-1': 'Asia Pacific (Hong Kong)', - 'ap-south-1': 'Asia Pacific (Mumbai)', - 'ap-northeast-2': 'Asia Pacific (Seoul)', - 'ap-southeast-1': 'Asia Pacific (Singapore)', - 'ap-southeast-2': 'Asia Pacific (Sydney)', - 'ap-northeast-1': 'Asia Pacific (Tokyo)', - 'ca-central-1': 'Canada (Central)', - 'eu-central-1': 'EU (Frankfurt)', - 'eu-west-1': 'EU (Ireland)', - 'eu-west-2': 'EU (London)', - 'eu-west-3': 'EU (Paris)', - 'eu-north-1': 'EU (Stockholm)', - 'me-south-1': 'Middle East (Bahrain)', - 'sa-east-1': 'South America (São Paulo)', -}; diff --git a/frontend/app/services/IntegrationsService.ts b/frontend/app/services/IntegrationsService.ts index d54f90aad..c39595a2a 100644 --- a/frontend/app/services/IntegrationsService.ts +++ b/frontend/app/services/IntegrationsService.ts @@ -1,8 +1,8 @@ import BaseService from "./BaseService"; export default class IntegrationsService extends BaseService { - fetchList = async (name: string) => { - const r = await this.client.get(`/integrations/${name}`) + fetchList = async (name?: string, siteId?: string) => { + const r = await this.client.get(`${siteId ? `/${siteId}` : ''}/integrations/${name}`) const data = await r.json() return data @@ -16,7 +16,7 @@ export default class IntegrationsService extends BaseService { return data } - saveIntegration = async (name: string, siteId: string, data: any) => { + saveIntegration = async (name: string, data: any, siteId?: string) => { const url = (siteId ? `/${siteId}` : '') + `/integrations/${name}` const r = await this.client.post(url, data) const res = await r.json() @@ -24,11 +24,40 @@ export default class IntegrationsService extends BaseService { return res } - removeIntegration = async (name: string, siteId: string) => { + removeIntegration = async (name: string, siteId?: string) => { const url = (siteId ? `/${siteId}` : '') + `/integrations/${name}` const r = await this.client.delete(url) - const res = await r.json() - return res + return await r.json() + } + + fetchMessengerChannels = async (name: string) => { + const r = await this.client.get(`/integrations/${name}/channels`) + + return await r.json() + } + + updateMessengerInt = async (name: string, data: any) => { + const r = await this.client.put(`/integrations/${name}/${data.webhookId}`, data) + + return await r.json() + } + + removeMessengerInt = async (name: string, webhookId: string) => { + const r = await this.client.delete(`/integrations/${name}/${webhookId}`) + + return await r.json() + } + + sendMsg = async (integrationId, entity, entityId, name, data) => { + const r = await this.client.post(`/integrations/${name}/notify/${integrationId}/${entity}/${entityId}`, data) + + return await r.json() + } + + testElastic = async (data: any) => { + const r = await this.client.post('/integrations/elasticsearch/test', data) + + return r.json(); } }