fix(ui) - integration form
This commit is contained in:
parent
25ddb03b3f
commit
48145e5a70
3 changed files with 267 additions and 188 deletions
|
|
@ -70,29 +70,18 @@ export default class IntegrationForm extends React.PureComponent {
|
||||||
|
|
||||||
remove = () => {
|
remove = () => {
|
||||||
const { name, config, ignoreProject } = this.props;
|
const { name, config, ignoreProject } = this.props;
|
||||||
this.props.remove(name, !ignoreProject ? config.projectId : null).then(
|
this.props.remove(name, !ignoreProject ? config.projectId : null).then(() => {
|
||||||
function () {
|
this.props.onClose();
|
||||||
this.props.onClose();
|
this.fetchList();
|
||||||
this.fetchList();
|
});
|
||||||
}.bind(this)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { config, saving, removing, formFields, name, loading, ignoreProject } = this.props;
|
const { config, saving, removing, formFields, name, loading, integrated } = this.props;
|
||||||
// const { currentSiteId } = this.state;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Loader loading={loading}>
|
<Loader loading={loading}>
|
||||||
<div className="ph-20">
|
<div className="ph-20">
|
||||||
<Form>
|
<Form>
|
||||||
{/* {!ignoreProject && (
|
|
||||||
<Form.Field>
|
|
||||||
<label>{'OpenReplay Project'}</label>
|
|
||||||
<SiteDropdown value={currentSiteId} onChange={this.onChangeSelect} />
|
|
||||||
</Form.Field>
|
|
||||||
)} */}
|
|
||||||
|
|
||||||
{formFields.map(
|
{formFields.map(
|
||||||
({
|
({
|
||||||
key,
|
key,
|
||||||
|
|
@ -140,7 +129,7 @@ export default class IntegrationForm extends React.PureComponent {
|
||||||
{config.exists() ? 'Update' : 'Add'}
|
{config.exists() ? 'Update' : 'Add'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{config.exists() && (
|
{integrated && (
|
||||||
<Button loading={removing} onClick={this.remove}>
|
<Button loading={removing} onClick={this.remove}>
|
||||||
{'Delete'}
|
{'Delete'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -27,159 +27,244 @@ import AssistDoc from './AssistDoc';
|
||||||
import { PageTitle, Tooltip } from 'UI';
|
import { PageTitle, Tooltip } from 'UI';
|
||||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||||
import withPageTitle from 'HOCs/withPageTitle';
|
import withPageTitle from 'HOCs/withPageTitle';
|
||||||
import PiniaDoc from './PiniaDoc'
|
import PiniaDoc from './PiniaDoc';
|
||||||
import ZustandDoc from './ZustandDoc'
|
import ZustandDoc from './ZustandDoc';
|
||||||
import MSTeams from './Teams'
|
import MSTeams from './Teams';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
fetch: (name: string, siteId: string) => void;
|
fetch: (name: string, siteId: string) => void;
|
||||||
init: () => void;
|
init: () => void;
|
||||||
fetchIntegrationList: (siteId: any) => void;
|
fetchIntegrationList: (siteId: any) => void;
|
||||||
integratedList: any;
|
integratedList: any;
|
||||||
initialSiteId: string;
|
initialSiteId: string;
|
||||||
setSiteId: (siteId: string) => void;
|
setSiteId: (siteId: string) => void;
|
||||||
siteId: string;
|
siteId: string;
|
||||||
hideHeader?: boolean;
|
hideHeader?: boolean;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
function Integrations(props: Props) {
|
function Integrations(props: Props) {
|
||||||
const { initialSiteId, hideHeader = false, loading = false } = props;
|
const { initialSiteId, hideHeader = false, loading = false } = props;
|
||||||
const { showModal } = useModal();
|
const { showModal } = useModal();
|
||||||
const [integratedList, setIntegratedList] = React.useState([]);
|
const [integratedList, setIntegratedList] = React.useState<any>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const list = props.integratedList.filter((item: any) => item.integrated).map((item: any) => item.name);
|
const list = props.integratedList
|
||||||
setIntegratedList(list);
|
.filter((item: any) => item.integrated)
|
||||||
}, [props.integratedList]);
|
.map((item: any) => item.name);
|
||||||
|
setIntegratedList(list);
|
||||||
|
}, [props.integratedList]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!props.siteId) {
|
if (!props.siteId) {
|
||||||
props.setSiteId(initialSiteId);
|
props.setSiteId(initialSiteId);
|
||||||
props.fetchIntegrationList(initialSiteId);
|
props.fetchIntegrationList(initialSiteId);
|
||||||
} else {
|
} else {
|
||||||
props.fetchIntegrationList(props.siteId);
|
props.fetchIntegrationList(props.siteId);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onClick = (integration: any, width: number) => {
|
const onClick = (integration: any, width: number) => {
|
||||||
if (integration.slug) {
|
if (integration.slug) {
|
||||||
props.fetch(integration.slug, props.siteId);
|
props.fetch(integration.slug, props.siteId);
|
||||||
}
|
}
|
||||||
showModal(integration.component, { right: true, width });
|
|
||||||
};
|
|
||||||
|
|
||||||
const onChangeSelect = ({ value }: any) => {
|
showModal(
|
||||||
props.setSiteId(value.value);
|
React.cloneElement(integration.component, {
|
||||||
props.fetchIntegrationList(value.value);
|
integrated: integratedList.includes(integration.slug),
|
||||||
};
|
}),
|
||||||
|
{ right: true, width }
|
||||||
return (
|
|
||||||
<div className="mb-4 p-5">
|
|
||||||
{!hideHeader && <PageTitle title={<div>Integrations</div>} />}
|
|
||||||
{integrations.map((cat: any) => (
|
|
||||||
<div className="mb-2 border-b last:border-none py-3" key={cat.key}>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<h2 className="font-medium text-lg">{cat.title}</h2>
|
|
||||||
{cat.isProject && (
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="flex flex-wrap mx-4">
|
|
||||||
<SiteDropdown value={props.siteId} onChange={onChangeSelect} />
|
|
||||||
</div>
|
|
||||||
{loading && cat.isProject && <AnimatedSVG name={ICONS.LOADER} size={20} />}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="">{cat.description}</div>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap mt-4">
|
|
||||||
{cat.integrations.map((integration: any) => (
|
|
||||||
<React.Fragment key={integration.slug}>
|
|
||||||
<Tooltip
|
|
||||||
delay={50}
|
|
||||||
title="Global configuration, available to all team members."
|
|
||||||
disabled={!integration.shared}
|
|
||||||
placement={"bottom"}
|
|
||||||
>
|
|
||||||
<IntegrationItem
|
|
||||||
integrated={integratedList.includes(integration.slug)}
|
|
||||||
integration={integration}
|
|
||||||
onClick={() => onClick(integration, cat.title === "Plugins" ? 500 : 350)}
|
|
||||||
hide={
|
|
||||||
(integration.slug === 'github' && integratedList.includes('jira')) ||
|
|
||||||
(integration.slug === 'jira' && integratedList.includes('github'))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChangeSelect = ({ value }: any) => {
|
||||||
|
props.setSiteId(value.value);
|
||||||
|
props.fetchIntegrationList(value.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-4 p-5">
|
||||||
|
{!hideHeader && <PageTitle title={<div>Integrations</div>} />}
|
||||||
|
{integrations.map((cat: any) => (
|
||||||
|
<div className="mb-2 border-b last:border-none py-3" key={cat.key}>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<h2 className="font-medium text-lg">{cat.title}</h2>
|
||||||
|
{cat.isProject && (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="flex flex-wrap mx-4">
|
||||||
|
<SiteDropdown value={props.siteId} onChange={onChangeSelect} />
|
||||||
|
</div>
|
||||||
|
{loading && cat.isProject && <AnimatedSVG name={ICONS.LOADER} size={20} />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="">{cat.description}</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap mt-4">
|
||||||
|
{cat.integrations.map((integration: any) => (
|
||||||
|
<React.Fragment key={integration.slug}>
|
||||||
|
<Tooltip
|
||||||
|
delay={50}
|
||||||
|
title="Global configuration, available to all team members."
|
||||||
|
disabled={!integration.shared}
|
||||||
|
placement={'bottom'}
|
||||||
|
>
|
||||||
|
<IntegrationItem
|
||||||
|
integrated={integratedList.includes(integration.slug)}
|
||||||
|
integration={integration}
|
||||||
|
onClick={() => onClick(integration, cat.title === 'Plugins' ? 500 : 350)}
|
||||||
|
hide={
|
||||||
|
(integration.slug === 'github' && integratedList.includes('jira')) ||
|
||||||
|
(integration.slug === 'jira' && integratedList.includes('github'))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
(state: any) => ({
|
(state: any) => ({
|
||||||
initialSiteId: state.getIn(['site', 'siteId']),
|
initialSiteId: state.getIn(['site', 'siteId']),
|
||||||
integratedList: state.getIn(['integrations', 'list']) || [],
|
integratedList: state.getIn(['integrations', 'list']) || [],
|
||||||
loading: state.getIn(['integrations', 'fetchRequest', 'loading']),
|
loading: state.getIn(['integrations', 'fetchRequest', 'loading']),
|
||||||
siteId: state.getIn(['integrations', 'siteId']),
|
siteId: state.getIn(['integrations', 'siteId']),
|
||||||
}),
|
}),
|
||||||
{ fetch, init, fetchIntegrationList, setSiteId }
|
{ fetch, init, fetchIntegrationList, setSiteId }
|
||||||
)(withPageTitle('Integrations - OpenReplay Preferences')(Integrations));
|
)(withPageTitle('Integrations - OpenReplay Preferences')(Integrations));
|
||||||
|
|
||||||
const integrations = [
|
const integrations = [
|
||||||
{
|
{
|
||||||
title: 'Issue Reporting and Collaborations',
|
title: 'Issue Reporting and Collaborations',
|
||||||
key: 1,
|
key: 1,
|
||||||
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,
|
isProject: false,
|
||||||
integrations: [
|
integrations: [
|
||||||
{ title: 'Jira', slug: 'jira', category: 'Errors', icon: 'integrations/jira', component: <JiraForm /> },
|
{
|
||||||
{ title: 'Github', slug: 'github', category: 'Errors', icon: 'integrations/github', component: <GithubForm /> },
|
title: 'Jira',
|
||||||
{ title: 'Slack', slug: 'slack', category: 'Errors', icon: 'integrations/slack', component: <SlackForm />, shared: true },
|
slug: 'jira',
|
||||||
{ title: 'MS Teams', slug: 'msteams', category: 'Errors', icon: 'integrations/teams', component: <MSTeams />, shared: true },
|
category: 'Errors',
|
||||||
],
|
icon: 'integrations/jira',
|
||||||
},
|
component: <JiraForm />,
|
||||||
{
|
},
|
||||||
title: 'Backend Logging',
|
{
|
||||||
key: 2,
|
title: 'Github',
|
||||||
isProject: true,
|
slug: 'github',
|
||||||
description: 'Sync your backend errors with sessions replays and see what happened front-to-back.',
|
category: 'Errors',
|
||||||
integrations: [
|
icon: 'integrations/github',
|
||||||
{ title: 'Sentry', slug: 'sentry', icon: 'integrations/sentry', component: <SentryForm /> },
|
component: <GithubForm />,
|
||||||
{ title: 'Bugsnag', slug: 'bugsnag', icon: 'integrations/bugsnag', component: <BugsnagForm /> },
|
},
|
||||||
{ title: 'Rollbar', slug: 'rollbar', icon: 'integrations/rollbar', component: <RollbarForm /> },
|
{
|
||||||
{ title: 'Elasticsearch', slug: 'elasticsearch', icon: 'integrations/elasticsearch', component: <ElasticsearchForm /> },
|
title: 'Slack',
|
||||||
{ title: 'Datadog', slug: 'datadog', icon: 'integrations/datadog', component: <DatadogForm /> },
|
slug: 'slack',
|
||||||
{ title: 'Sumo Logic', slug: 'sumologic', icon: 'integrations/sumologic', component: <SumoLogicForm /> },
|
category: 'Errors',
|
||||||
{
|
icon: 'integrations/slack',
|
||||||
title: 'Stackdriver',
|
component: <SlackForm />,
|
||||||
slug: 'stackdriver',
|
shared: true,
|
||||||
icon: 'integrations/google-cloud',
|
},
|
||||||
component: <StackdriverForm />,
|
{
|
||||||
},
|
title: 'MS Teams',
|
||||||
{ title: 'CloudWatch', slug: 'cloudwatch', icon: 'integrations/aws', component: <CloudwatchForm /> },
|
slug: 'msteams',
|
||||||
{ title: 'Newrelic', slug: 'newrelic', icon: 'integrations/newrelic', component: <NewrelicForm /> },
|
category: 'Errors',
|
||||||
],
|
icon: 'integrations/teams',
|
||||||
},
|
component: <MSTeams />,
|
||||||
{
|
shared: true,
|
||||||
title: 'Plugins',
|
},
|
||||||
key: 3,
|
],
|
||||||
isProject: true,
|
},
|
||||||
description:
|
{
|
||||||
"Reproduce issues as if they happened in your own browser. Plugins help capture your application's store, HTTP requeets, GraphQL queries, and more.",
|
title: 'Backend Logging',
|
||||||
integrations: [
|
key: 2,
|
||||||
{ title: 'Redux', slug: 'redux', icon: 'integrations/redux', component: <ReduxDoc /> },
|
isProject: true,
|
||||||
{ title: 'VueX', slug: 'vuex', icon: 'integrations/vuejs', component: <VueDoc /> },
|
description:
|
||||||
{ title: 'Pinia', slug: 'pinia', icon: 'integrations/pinia', component: <PiniaDoc /> },
|
'Sync your backend errors with sessions replays and see what happened front-to-back.',
|
||||||
{ title: 'GraphQL', slug: 'graphql', icon: 'integrations/graphql', component: <GraphQLDoc /> },
|
integrations: [
|
||||||
{ title: 'NgRx', slug: 'ngrx', icon: 'integrations/ngrx', component: <NgRxDoc /> },
|
{ title: 'Sentry', slug: 'sentry', icon: 'integrations/sentry', component: <SentryForm /> },
|
||||||
{ title: 'MobX', slug: 'mobx', icon: 'integrations/mobx', component: <MobxDoc /> },
|
{
|
||||||
{ title: 'Profiler', slug: 'profiler', icon: 'integrations/openreplay', component: <ProfilerDoc /> },
|
title: 'Bugsnag',
|
||||||
{ title: 'Assist', slug: 'assist', icon: 'integrations/openreplay', component: <AssistDoc /> },
|
slug: 'bugsnag',
|
||||||
{ title: 'Zustand', slug: 'zustand', icon: '', header: '🐻', component: <ZustandDoc /> }
|
icon: 'integrations/bugsnag',
|
||||||
],
|
component: <BugsnagForm />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Rollbar',
|
||||||
|
slug: 'rollbar',
|
||||||
|
icon: 'integrations/rollbar',
|
||||||
|
component: <RollbarForm />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Elasticsearch',
|
||||||
|
slug: 'elasticsearch',
|
||||||
|
icon: 'integrations/elasticsearch',
|
||||||
|
component: <ElasticsearchForm />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Datadog',
|
||||||
|
slug: 'datadog',
|
||||||
|
icon: 'integrations/datadog',
|
||||||
|
component: <DatadogForm />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Sumo Logic',
|
||||||
|
slug: 'sumologic',
|
||||||
|
icon: 'integrations/sumologic',
|
||||||
|
component: <SumoLogicForm />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Stackdriver',
|
||||||
|
slug: 'stackdriver',
|
||||||
|
icon: 'integrations/google-cloud',
|
||||||
|
component: <StackdriverForm />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'CloudWatch',
|
||||||
|
slug: 'cloudwatch',
|
||||||
|
icon: 'integrations/aws',
|
||||||
|
component: <CloudwatchForm />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Newrelic',
|
||||||
|
slug: 'newrelic',
|
||||||
|
icon: 'integrations/newrelic',
|
||||||
|
component: <NewrelicForm />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Plugins',
|
||||||
|
key: 3,
|
||||||
|
isProject: true,
|
||||||
|
description:
|
||||||
|
"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', slug: 'redux', icon: 'integrations/redux', component: <ReduxDoc /> },
|
||||||
|
{ title: 'VueX', slug: 'vuex', icon: 'integrations/vuejs', component: <VueDoc /> },
|
||||||
|
{ title: 'Pinia', slug: 'pinia', icon: 'integrations/pinia', component: <PiniaDoc /> },
|
||||||
|
{
|
||||||
|
title: 'GraphQL',
|
||||||
|
slug: 'graphql',
|
||||||
|
icon: 'integrations/graphql',
|
||||||
|
component: <GraphQLDoc />,
|
||||||
|
},
|
||||||
|
{ title: 'NgRx', slug: 'ngrx', icon: 'integrations/ngrx', component: <NgRxDoc /> },
|
||||||
|
{ title: 'MobX', slug: 'mobx', icon: 'integrations/mobx', component: <MobxDoc /> },
|
||||||
|
{
|
||||||
|
title: 'Profiler',
|
||||||
|
slug: 'profiler',
|
||||||
|
icon: 'integrations/openreplay',
|
||||||
|
component: <ProfilerDoc />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Assist',
|
||||||
|
slug: 'assist',
|
||||||
|
icon: 'integrations/openreplay',
|
||||||
|
component: <AssistDoc />,
|
||||||
|
},
|
||||||
|
{ title: 'Zustand', slug: 'zustand', icon: '', header: '🐻', component: <ZustandDoc /> },
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,45 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import IntegrationForm from '../IntegrationForm';
|
import IntegrationForm from '../IntegrationForm';
|
||||||
import DocLink from 'Shared/DocLink/DocLink';
|
import DocLink from 'Shared/DocLink/DocLink';
|
||||||
|
import { useModal } from 'App/components/Modal';
|
||||||
|
|
||||||
const JiraForm = (props) => (
|
const JiraForm = (props) => {
|
||||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
const { hideModal } = useModal();
|
||||||
<h3 className="p-5 text-2xl">Jira</h3>
|
return (
|
||||||
<div className="p-5 border-b mb-4">
|
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||||
<div>How to integrate Jira Cloud with OpenReplay.</div>
|
<h3 className="p-5 text-2xl">Jira</h3>
|
||||||
<div className="mt-8">
|
<div className="p-5 border-b mb-4">
|
||||||
<DocLink className="mt-4" label="Integrate Jira Cloud" url="https://docs.openreplay.com/integrations/jira" />
|
<div>How to integrate Jira Cloud with OpenReplay.</div>
|
||||||
|
<div className="mt-8">
|
||||||
|
<DocLink className="mt-4" label="Integrate Jira Cloud" url="https://docs.openreplay.com/integrations/jira" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<IntegrationForm
|
||||||
|
{...props}
|
||||||
|
ignoreProject={true}
|
||||||
|
name="jira"
|
||||||
|
customPath="jira"
|
||||||
|
onClose={hideModal}
|
||||||
|
formFields={[
|
||||||
|
{
|
||||||
|
key: 'username',
|
||||||
|
label: 'Username',
|
||||||
|
autoFocus: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'token',
|
||||||
|
label: 'API Token',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'url',
|
||||||
|
label: 'JIRA URL',
|
||||||
|
placeholder: 'E.x. https://myjira.atlassian.net',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<IntegrationForm
|
)
|
||||||
{...props}
|
};
|
||||||
ignoreProject={true}
|
|
||||||
name="jira"
|
|
||||||
customPath="jira"
|
|
||||||
formFields={[
|
|
||||||
{
|
|
||||||
key: 'username',
|
|
||||||
label: 'Username',
|
|
||||||
autoFocus: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'token',
|
|
||||||
label: 'API Token',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'url',
|
|
||||||
label: 'JIRA URL',
|
|
||||||
placeholder: 'E.x. https://myjira.atlassian.net',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
JiraForm.displayName = 'JiraForm';
|
JiraForm.displayName = 'JiraForm';
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue