refactoring integrations reducers etc WIP

This commit is contained in:
nick-delirium 2024-09-17 16:52:56 +02:00
parent b9590f702e
commit 5a011692f8
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
26 changed files with 1079 additions and 822 deletions

View file

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

View file

@ -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) => (
<div className='bg-white h-screen overflow-y-auto' style={{ width: '350px' }}>
<IntegrationModalCard title='Cloud Watch' icon='integrations/aws'
description='Integrate CloudWatch to see backend logs and errors alongside session replay.' />
<div className='p-5 border-b mb-4'>
<div className='font-medium mb-1'>How it works?</div>
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
<IntegrationModalCard
title="Cloud Watch"
icon="integrations/aws"
description="Integrate CloudWatch to see backend logs and errors alongside session replay."
/>
<div className="p-5 border-b mb-4">
<div className="font-medium mb-1">How it works?</div>
<ol className="list-decimal list-inside">
<li>Create a Service Account</li>
<li>Enter the details below</li>
<li>Propagate openReplaySessionToken</li>
</ol>
<DocLink className='mt-4' label='Integrate CloudWatch'
url='https://docs.openreplay.com/integrations/cloudwatch' />
<DocLink
className="mt-4"
label="Integrate CloudWatch"
url="https://docs.openreplay.com/integrations/cloudwatch"
/>
</div>
<IntegrationForm
{...props}
name='cloudwatch'
name="cloudwatch"
formFields={[
{
key: 'awsAccessKeyId',
label: 'AWS Access Key ID'
label: 'AWS Access Key ID',
},
{
key: 'awsSecretAccessKey',
label: 'AWS Secret Access Key'
label: 'AWS Secret Access Key',
},
{
key: 'region',
label: 'Region',
component: RegionDropdown
component: RegionDropdown,
},
{
key: 'logGroupName',
@ -44,8 +56,8 @@ const CloudwatchForm = (props) => (
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,
},
]}
/>
</div>

View file

@ -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 (
<div className='bg-white h-screen overflow-y-auto' style={{ width: '350px' }}>
<IntegrationModalCard title='Elasticsearch' icon='integrations/elasticsearch'
description='Integrate Elasticsearch with session replays to seamlessly observe backend errors.' />
const ElasticsearchForm = (props) => {
return (
<div
className="bg-white h-screen overflow-y-auto"
style={{ width: '350px' }}
>
<IntegrationModalCard
title="Elasticsearch"
icon="integrations/elasticsearch"
description="Integrate Elasticsearch with session replays to seamlessly observe backend errors."
/>
<div className='p-5 border-b mb-4'>
<div className='font-medium mb-1'>How it works?</div>
<ol className="list-decimal list-inside">
<li>Create a new Elastic API key</li>
<li>Enter the API key below</li>
<li>Propagate openReplaySessionToken</li>
</ol>
<DocLink className='mt-4' label='Integrate Elasticsearch'
url='https://docs.openreplay.com/integrations/elastic' />
</div>
<IntegrationForm
{...props}
name='elasticsearch'
formFields={[
{
key: 'host',
label: 'Host'
},
{
key: 'apiKeyId',
label: 'API Key ID'
},
{
key: 'apiKey',
label: 'API Key'
},
{
key: 'indexes',
label: 'Indexes'
},
{
key: 'port',
label: 'Port',
type: 'number'
}
]}
<div className="p-5 border-b mb-4">
<div className="font-medium mb-1">How it works?</div>
<ol className="list-decimal list-inside">
<li>Create a new Elastic API key</li>
<li>Enter the API key below</li>
<li>Propagate openReplaySessionToken</li>
</ol>
<DocLink
className="mt-4"
label="Integrate Elasticsearch"
url="https://docs.openreplay.com/integrations/elastic"
/>
</div>
);
}
}
<IntegrationForm
{...props}
name="elasticsearch"
formFields={[
{
key: 'host',
label: 'Host',
},
{
key: 'apiKeyId',
label: 'API Key ID',
},
{
key: 'apiKey',
label: 'API Key',
},
{
key: 'indexes',
label: 'Indexes',
},
{
key: 'port',
label: 'Port',
type: 'number',
},
]}
/>
</div>
);
};
export default ElasticsearchForm;

View file

@ -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 (
<Loader loading={loading}>
<div className="ph-20">
<Form>
{formFields.map(
({
key,
label,
placeholder = label,
component: Component = 'input',
type = 'text',
checkIfDisplayed,
autoFocus = false,
}) =>
(typeof checkIfDisplayed !== 'function' || checkIfDisplayed(config)) &&
(type === 'checkbox' ? (
<Form.Field key={key}>
<Checkbox
label={label}
name={key}
value={config[key]}
onChange={this.write}
placeholder={placeholder}
type={Component === 'input' ? type : null}
/>
</Form.Field>
) : (
<Form.Field key={key}>
<label>{label}</label>
<Input
name={key}
value={config[key]}
onChange={this.write}
placeholder={placeholder}
type={Component === 'input' ? type : null}
autoFocus={autoFocus}
/>
</Form.Field>
))
)}
<Button
onClick={this.save}
disabled={!config.validate()}
loading={saving || loading}
variant="primary"
className="float-left mr-2"
>
{config.exists() ? 'Update' : 'Add'}
</Button>
{integrated && (
<Button loading={removing} onClick={this.remove}>
{'Delete'}
</Button>
)}
</Form>
</div>
</Loader>
);
}
}

View file

@ -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 (
<Loader loading={loading}>
<div className="ph-20">
<Form>
{formFields.map(
({
key,
label,
placeholder = label,
component: Component = 'input',
type = 'text',
checkIfDisplayed,
autoFocus = false,
}) =>
(typeof checkIfDisplayed !== 'function' ||
checkIfDisplayed(config)) &&
(type === 'checkbox' ? (
<Form.Field key={key}>
<Checkbox
label={label}
name={key}
value={config[key]}
onChange={write}
placeholder={placeholder}
type={Component === 'input' ? type : null}
/>
</Form.Field>
) : (
<Form.Field key={key}>
<label>{label}</label>
<Input
name={key}
value={config[key]}
onChange={write}
placeholder={placeholder}
type={Component === 'input' ? type : null}
autoFocus={autoFocus}
/>
</Form.Field>
))
)}
<Button
onClick={save}
disabled={!config?.validate()}
loading={loading}
variant="primary"
className="float-left mr-2"
>
{config?.exists() ? 'Update' : 'Add'}
</Button>
{integrated && (
<Button loading={loading} onClick={remove}>
{'Delete'}
</Button>
)}
</Form>
</div>
</Loader>
);
}
export default connect((state: any) => ({
sites: state.getIn(['site', 'list']),
initialSiteId: state.getIn(['site', 'siteId']),
}))(observer(IntegrationForm));

View file

@ -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<string[]>([]);
const [activeFilter, setActiveFilter] = useState<string>('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 (
<>
<div className='bg-white rounded-lg border shadow-sm p-5 mb-4'>
<div className="bg-white rounded-lg border shadow-sm p-5 mb-4">
{!hideHeader && <PageTitle title={<div>Integrations</div>} />}
<IntegrationFilters onChange={onChange} activeItem={activeFilter} filters={filters} />
<IntegrationFilters
onChange={onChange}
activeItem={activeFilter}
filters={filters}
/>
</div>
<div className='mb-4' />
<div className="mb-4" />
<div className={cn(`
<div
className={cn(`
mt-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3
`)}>
`)}
>
{allIntegrations.map((integration: any) => (
<IntegrationItem
integrated={integratedList.includes(integration.slug)}
integration={integration}
onClick={() =>
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'))
}
/>
))}
</div>
</>
);
}
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: <JiraForm />
component: <JiraForm />,
},
{
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: <GithubForm />
}
]
component: <GithubForm />,
},
],
},
{
title: 'Backend Logging',
@ -186,106 +201,119 @@ const integrations = [
'Sync your backend errors with sessions replays and see what happened front-to-back.',
docs: () => (
<DocCard
title='Why use integrations?'
icon='question-lg'
iconBgColor='bg-red-lightest'
iconColor='red'
title="Why use integrations?"
icon="question-lg"
iconBgColor="bg-red-lightest"
iconColor="red"
>
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.
</DocCard>
),
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: <SentryForm />
component: <SentryForm />,
},
{
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: <BugsnagForm />
component: <BugsnagForm />,
},
{
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: <RollbarForm />
component: <RollbarForm />,
},
{
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: <ElasticsearchForm />
component: <ElasticsearchForm />,
},
{
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: <DatadogForm />
component: <DatadogForm />,
},
{
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: <SumoLogicForm />
component: <SumoLogicForm />,
},
{
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: <StackdriverForm />
component: <StackdriverForm />,
},
{
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: <CloudwatchForm />
component: <CloudwatchForm />,
},
{
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: <NewrelicForm />
}
]
component: <NewrelicForm />,
},
],
},
{
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: <SlackForm />,
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: <MSTeams />,
shared: true
}
]
shared: true,
},
],
},
// {
// title: 'State Management',
@ -302,72 +330,82 @@ const integrations = [
icon: 'chat-left-text',
docs: () => (
<DocCard
title='What are plugins?'
icon='question-lg'
iconBgColor='bg-red-lightest'
iconColor='red'
title="What are plugins?"
icon="question-lg"
iconBgColor="bg-red-lightest"
iconColor="red"
>
Plugins capture your applications store, monitor queries, track performance issues and even
assist your end user through live sessions.
Plugins capture your applications store, monitor queries, track
performance issues and even assist your end user through live sessions.
</DocCard>
),
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: <ReduxDoc />
subtitle:
'Capture Redux actions/state and inspect them later on while replaying session recordings.',
icon: 'integrations/redux',
component: <ReduxDoc />,
},
{
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: <VueDoc />
component: <VueDoc />,
},
{
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: <PiniaDoc />
component: <PiniaDoc />,
},
{
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: <GraphQLDoc />
component: <GraphQLDoc />,
},
{
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: <NgRxDoc />
component: <NgRxDoc />,
},
{
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: <MobxDoc />
component: <MobxDoc />,
},
{
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: <ProfilerDoc />
component: <ProfilerDoc />,
},
{
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: <AssistDoc />
component: <AssistDoc />,
},
{
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: <ZustandDoc />
}
]
}
component: <ZustandDoc />,
},
],
},
];

View file

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

View file

@ -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 (
<div className="p-5" style={{ minWidth: '300px' }}>
<Form>
<Form.Field>
<label>Name</label>
<Input
name="name"
value={instance.name}
onChange={this.write}
placeholder="Enter any name"
type="text"
/>
</Form.Field>
<Form.Field>
<label>URL</label>
<Input
name="endpoint"
value={instance.endpoint}
onChange={this.write}
placeholder="Slack webhook URL"
type="text"
/>
</Form.Field>
<div className="flex justify-between">
<div className="flex">
<Button
onClick={this.save}
disabled={!instance.validate()}
loading={saving}
variant="primary"
className="float-left mr-2"
>
{instance.exists() ? 'Update' : 'Add'}
</Button>
<Button onClick={onClose}>{'Cancel'}</Button>
</div>
<Button onClick={() => this.remove(instance.webhookId)} disabled={!instance.exists()}>
{'Delete'}
const write = ({ target: { name, value } }) => edit({ [name]: value });
return (
<div className="p-5" style={{ minWidth: '300px' }}>
<Form>
<Form.Field>
<label>Name</label>
<Input
name="name"
value={instance.name}
onChange={write}
placeholder="Enter any name"
type="text"
/>
</Form.Field>
<Form.Field>
<label>URL</label>
<Input
name="endpoint"
value={instance.endpoint}
onChange={write}
placeholder="Slack webhook URL"
type="text"
/>
</Form.Field>
<div className="flex justify-between">
<div className="flex">
<Button
onClick={save}
disabled={!instance.validate()}
loading={saving}
variant="primary"
className="float-left mr-2"
>
{instance.exists() ? 'Update' : 'Add'}
</Button>
</div>
</Form>
{errors && (
<div className="my-3">
{errors.map((error) => (
<Message visible={errors} size="mini" error key={error}>
{error}
</Message>
))}
<Button onClick={onClose}>{'Cancel'}</Button>
</div>
)}
</div>
);
}
<Button onClick={() => remove(instance.webhookId)} disabled={!instance.exists()}>
{'Delete'}
</Button>
</div>
</Form>
{errors && (
<div className="my-3">
{errors.map((error) => (
<Message visible={errors} size="mini" error key={error}>
{error}
</Message>
))}
</div>
)}
</div>
);
}
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);

View file

@ -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) {
</div>
}
size="small"
show={list.size === 0}
show={list.length === 0}
>
{list.map((c) => (
<div
@ -43,9 +45,4 @@ function SlackChannelList(props) {
);
}
export default connect(
(state) => ({
list: state.getIn(['slack', 'list']),
}),
{ remove, edit, init }
)(SlackChannelList);
export default observer(SlackChannelList);

View file

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

View file

@ -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<Props> {
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<Props> {
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 (
<div className="p-5" style={{ minWidth: '300px' }}>
<Form>
<Form.Field>
<label>Name</label>
<Input
name="name"
value={instance.name}
onChange={this.write}
placeholder="Enter any name"
type="text"
/>
</Form.Field>
<Form.Field>
<label>URL</label>
<Input
name="endpoint"
value={instance.endpoint}
onChange={this.write}
placeholder="Teams webhook URL"
type="text"
/>
</Form.Field>
<div className="flex justify-between">
<div className="flex">
<Button
onClick={this.save}
disabled={!instance.validate()}
loading={saving}
variant="primary"
className="float-left mr-2"
>
{instance.exists() ? 'Update' : 'Add'}
</Button>
<Button onClick={onClose}>{'Cancel'}</Button>
</div>
<Button onClick={() => this.remove(instance.webhookId)} disabled={!instance.exists()}>
{'Delete'}
return (
<div className="p-5" style={{ minWidth: '300px' }}>
<Form>
<Form.Field>
<label>Name</label>
<Input
name="name"
value={instance?.name}
onChange={write}
placeholder="Enter any name"
type="text"
/>
</Form.Field>
<Form.Field>
<label>URL</label>
<Input
name="endpoint"
value={instance?.endpoint}
onChange={write}
placeholder="Teams webhook URL"
type="text"
/>
</Form.Field>
<div className="flex justify-between">
<div className="flex">
<Button
onClick={save}
disabled={!instance?.validate()}
loading={saving}
variant="primary"
className="float-left mr-2"
>
{instance?.exists() ? 'Update' : 'Add'}
</Button>
</div>
</Form>
{errors && (
<div className="my-3">
{errors.map((error: any) => (
<Message visible={errors} key={error}>
{error}
</Message>
))}
<Button onClick={onClose}>{'Cancel'}</Button>
</div>
)}
</div>
);
}
<Button
onClick={() => remove(instance?.webhookId)}
disabled={!instance.exists()}
>
{'Delete'}
</Button>
</div>
</Form>
{errors && (
<div className="my-3">
{errors.map((error: any) => (
<Message visible={errors} key={error}>
{error}
</Message>
))}
</div>
)}
</div>
);
}
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);

View file

@ -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<string, any>) => {
props.edit(instance);
props.onEdit();
};
const onEdit = (instance: Record<string, any>) => {
edit(instance);
props.onEdit();
};
return (
<div className="mt-6">
<NoContent
title={
<div className="p-5 mb-4">
<div className="text-base text-left">
Integrate MS Teams with OpenReplay and share insights with the rest of the team, directly from the recording page.
</div>
<DocLink className="mt-4 text-base" label="Integrate MS Teams" url="https://docs.openreplay.com/integrations/msteams" />
</div>
}
size="small"
show={list.size === 0}
>
{list.map((c: any) => (
<div
key={c.webhookId}
className="border-t px-5 py-2 flex items-center justify-between cursor-pointer hover:bg-active-blue"
onClick={() => onEdit(c)}
>
<div className="flex-grow-0" style={{ maxWidth: '90%' }}>
<div>{c.name}</div>
<div className="truncate test-xs color-gray-medium">{c.endpoint}</div>
</div>
</div>
))}
</NoContent>
</div>
);
return (
<div className="mt-6">
<NoContent
title={
<div className="p-5 mb-4">
<div className="text-base text-left">
Integrate MS Teams with OpenReplay and share insights with the
rest of the team, directly from the recording page.
</div>
<DocLink
className="mt-4 text-base"
label="Integrate MS Teams"
url="https://docs.openreplay.com/integrations/msteams"
/>
</div>
}
size="small"
show={list.length === 0}
>
{list.map((c: any) => (
<div
key={c.webhookId}
className="border-t px-5 py-2 flex items-center justify-between cursor-pointer hover:bg-active-blue"
onClick={() => onEdit(c)}
>
<div className="flex-grow-0" style={{ maxWidth: '90%' }}>
<div>{c.name}</div>
<div className="truncate test-xs color-gray-medium">
{c.endpoint}
</div>
</div>
</div>
))}
</NoContent>
</div>
);
}
export default connect(
(state: any) => ({
list: state.getIn(['teams', 'list']),
}),
{ remove, edit, init }
)(TeamsChannelList);
export default observer(TeamsChannelList);

View file

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

View file

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

View file

@ -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<Note | undefined>(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,

View file

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

View file

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

View file

@ -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<Record<string, any>>;
teamsChannels: List<Record<string, any>>;
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<HTMLTextAreaElement>();
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));

View file

@ -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<HTMLTextAreaElement>) => 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<string, any>) => ({
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);

View file

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

View file

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

View file

@ -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<T extends Integration> {
list: T[] = [];
fetched: boolean = false;
issuesFetched: boolean = false;
loading = false;
constructor(
private readonly name: string,
private readonly NamedType: new (config: Record<string, any>) => T
private readonly namedTypeCreator: (config: Record<string, any>) => 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<void> => {
const { data } = await integrationsService.fetchList(this.name);
this.setList(
data.map((config: Record<string, any>) => new this.NamedType(config))
);
this.setLoading(true);
try {
const { data } = await integrationsService.fetchList(this.name);
this.setList(
data.map((config: Record<string, any>) => 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<void> => {
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<void> => {
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<string, any>): void {
this.instance = new this.NamedType(config);
init = (config: Record<string, any>): 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<void> => {
// 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<void> => {
const { data } = await integrationsService.fetchMessengerChannels(
this.mName
);
this.setList(
data.map((config: Record<string, any>) => 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<string, any>): 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<string, any>): 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);
}
}

View file

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

View file

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

View file

@ -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<string, any>;
edit(data: Record<string, any>): 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<string, any>) => {
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<string, any>) => {
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<string, any>) => {
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<string, any>) => {
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<string, any>) => {
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<string, any>) => {
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<string, any>) => {
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<string, any>) => {
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<string, any>) => {
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<string, any>) => {
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<string, any>) => {
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<string, any>) => {
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)',
};

View file

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