refactoring integrations reducers etc WIP
This commit is contained in:
parent
b9590f702e
commit
5a011692f8
26 changed files with 1079 additions and 822 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
110
frontend/app/components/Client/Integrations/IntegrationForm.tsx
Normal file
110
frontend/app/components/Client/Integrations/IntegrationForm.tsx
Normal 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));
|
||||
|
|
@ -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 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.
|
||||
</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 />,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
37
frontend/app/mstore/types/integrations/consts.ts
Normal file
37
frontend/app/mstore/types/integrations/consts.ts
Normal 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)',
|
||||
};
|
||||
41
frontend/app/mstore/types/integrations/messengers.ts
Normal file
41
frontend/app/mstore/types/integrations/messengers.ts
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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)',
|
||||
};
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue