change(ui) - onboarding (#1124)
* change(ui) - preferences header text change * change(ui) - onboarding - wip * change(ui) - onboarding - wip * change(ui) - onboarding - wip * change(ui) - onboarding - wip * change(ui) - onboarding - wip * change(ui) - onboarding - wip * change(ui) - onboarding - wip * change(ui) - onboarding - wip * change(ui) - onboarding - wip
This commit is contained in:
parent
7ed74ac412
commit
704abbb47a
40 changed files with 1040 additions and 777 deletions
|
|
@ -1,68 +1,61 @@
|
|||
import React from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { edit, save } from 'Duck/customField';
|
||||
import { Form, Input, Button, Message } from 'UI';
|
||||
import { Form, Input, Button } from 'UI';
|
||||
import styles from './customFieldForm.module.css';
|
||||
|
||||
@connect(
|
||||
(state) => ({
|
||||
field: state.getIn(['customFields', 'instance']),
|
||||
saving: state.getIn(['customFields', 'saveRequest', 'loading']),
|
||||
errors: state.getIn(['customFields', 'saveRequest', 'errors']),
|
||||
}),
|
||||
{
|
||||
edit,
|
||||
save,
|
||||
}
|
||||
)
|
||||
class CustomFieldForm extends React.PureComponent {
|
||||
setFocus = () => this.focusElement.focus();
|
||||
onChangeSelect = (event, { name, value }) => this.props.edit({ [name]: value });
|
||||
write = ({ target: { value, name } }) => this.props.edit({ [name]: value });
|
||||
const CustomFieldForm = ({ field, saving, errors, edit, save, onSave, onClose, onDelete }) => {
|
||||
const focusElementRef = useRef(null);
|
||||
|
||||
render() {
|
||||
const { field, errors } = this.props;
|
||||
const exists = field.exists();
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto">
|
||||
<h3 className="p-5 text-2xl">{exists ? 'Update' : 'Add'} Metadata Field</h3>
|
||||
<Form className={styles.wrapper}>
|
||||
<Form.Field>
|
||||
<label>{'Field Name'}</label>
|
||||
<Input
|
||||
ref={(ref) => {
|
||||
this.focusElement = ref;
|
||||
}}
|
||||
name="key"
|
||||
value={field.key}
|
||||
onChange={this.write}
|
||||
placeholder="Field Name"
|
||||
maxLength={50}
|
||||
/>
|
||||
</Form.Field>
|
||||
const setFocus = () => focusElementRef.current.focus();
|
||||
const onChangeSelect = (event, { name, value }) => edit({ [name]: value });
|
||||
const write = ({ target: { value, name } }) => edit({ [name]: value });
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
onClick={() => this.props.onSave(field)}
|
||||
disabled={!field.validate()}
|
||||
loading={this.props.saving}
|
||||
variant="primary"
|
||||
className="float-left mr-2"
|
||||
>
|
||||
{exists ? 'Update' : 'Add'}
|
||||
</Button>
|
||||
<Button data-hidden={!exists} onClick={this.props.onClose}>
|
||||
{'Cancel'}
|
||||
</Button>
|
||||
</div>
|
||||
const exists = field.exists();
|
||||
|
||||
<Button variant="text" icon="trash" data-hidden={!exists} onClick={this.props.onDelete}></Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto">
|
||||
<h3 className="p-5 text-2xl">{exists ? 'Update' : 'Add'} Metadata Field</h3>
|
||||
<Form className={styles.wrapper}>
|
||||
<Form.Field>
|
||||
<label>{'Field Name'}</label>
|
||||
<Input
|
||||
ref={focusElementRef}
|
||||
name="key"
|
||||
value={field.key}
|
||||
onChange={write}
|
||||
placeholder="Field Name"
|
||||
maxLength={50}
|
||||
/>
|
||||
</Form.Field>
|
||||
|
||||
export default CustomFieldForm;
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
onClick={() => onSave(field)}
|
||||
disabled={!field.validate()}
|
||||
loading={saving}
|
||||
variant="primary"
|
||||
className="float-left mr-2"
|
||||
>
|
||||
{exists ? 'Update' : 'Add'}
|
||||
</Button>
|
||||
<Button data-hidden={!exists} onClick={onClose}>
|
||||
{'Cancel'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button variant="text" icon="trash" data-hidden={!exists} onClick={onDelete}></Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
field: state.getIn(['customFields', 'instance']),
|
||||
saving: state.getIn(['customFields', 'saveRequest', 'loading']),
|
||||
errors: state.getIn(['customFields', 'saveRequest', 'errors']),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, { edit, save })(CustomFieldForm);
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ function CustomFields(props) {
|
|||
props.save(currentSite.id, field).then((response) => {
|
||||
if (!response || !response.errors || response.errors.size === 0) {
|
||||
hideModal();
|
||||
toast.success('Metadata added successfully!');
|
||||
} else {
|
||||
toast.error(response.errors[0]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ import withPageTitle from 'HOCs/withPageTitle';
|
|||
import PiniaDoc from './PiniaDoc';
|
||||
import ZustandDoc from './ZustandDoc';
|
||||
import MSTeams from './Teams';
|
||||
import DocCard from 'Shared/DocCard/DocCard';
|
||||
import cn from 'classnames';
|
||||
|
||||
interface Props {
|
||||
fetch: (name: string, siteId: string) => void;
|
||||
|
|
@ -85,42 +87,48 @@ function Integrations(props: Props) {
|
|||
<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 className="grid grid-cols-6 border-b last:border-none">
|
||||
<div
|
||||
className={cn('col-span-4 mb-2 py-3', cat.docs ? 'col-span-4' : 'col-span-6')}
|
||||
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>
|
||||
{loading && cat.isProject && <AnimatedSVG name={ICONS.LOADER} size={20} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="">{cat.description}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="">{cat.description}</div>
|
||||
|
||||
<div className="flex flex-wrap mt-4 gap-3">
|
||||
{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 className="flex flex-wrap mt-4 gap-3">
|
||||
{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>
|
||||
{cat.docs && <div className="col-span-2 py-6">{cat.docs()}</div>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -182,6 +190,16 @@ const integrations = [
|
|||
isProject: true,
|
||||
description:
|
||||
'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"
|
||||
>
|
||||
Sync your backend errors with sessions replays and see what happened front-to-back.
|
||||
</DocCard>
|
||||
),
|
||||
integrations: [
|
||||
{ title: 'Sentry', slug: 'sentry', icon: 'integrations/sentry', component: <SentryForm /> },
|
||||
{
|
||||
|
|
@ -238,6 +256,17 @@ const integrations = [
|
|||
title: 'Plugins',
|
||||
key: 3,
|
||||
isProject: true,
|
||||
docs: () => (
|
||||
<DocCard
|
||||
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.
|
||||
</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.",
|
||||
integrations: [
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Form, Input, Button, Icon } from 'UI';
|
||||
import { save, edit, update, fetchList, remove } from 'Duck/site';
|
||||
|
|
@ -12,121 +12,129 @@ import { clearSearch as clearSearchLive } from 'Duck/liveSearch';
|
|||
import { withStore } from 'App/mstore';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
@connect(
|
||||
(state) => ({
|
||||
site: state.getIn(['site', 'instance']),
|
||||
sites: state.getIn(['site', 'list']),
|
||||
siteList: state.getIn(['site', 'list']),
|
||||
loading: state.getIn(['site', 'save', 'loading']) || state.getIn(['site', 'remove', 'loading']),
|
||||
}),
|
||||
{
|
||||
save,
|
||||
remove,
|
||||
edit,
|
||||
update,
|
||||
pushNewSite,
|
||||
fetchList,
|
||||
setSiteId,
|
||||
clearSearch,
|
||||
clearSearchLive,
|
||||
}
|
||||
)
|
||||
@withRouter
|
||||
@withStore
|
||||
export default class NewSiteForm extends React.PureComponent {
|
||||
state = {
|
||||
existsError: false,
|
||||
};
|
||||
|
||||
const NewSiteForm = ({
|
||||
site,
|
||||
loading,
|
||||
save,
|
||||
remove,
|
||||
edit,
|
||||
update,
|
||||
pushNewSite,
|
||||
fetchList,
|
||||
setSiteId,
|
||||
clearSearch,
|
||||
clearSearchLive,
|
||||
location: { pathname },
|
||||
onClose,
|
||||
mstore,
|
||||
}) => {
|
||||
const [existsError, setExistsError] = useState(false);
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
location: { pathname },
|
||||
match: {
|
||||
params: { siteId },
|
||||
},
|
||||
} = this.props;
|
||||
if (pathname.includes('onboarding')) {
|
||||
this.props.setSiteId(siteId);
|
||||
}
|
||||
useEffect(() => {
|
||||
if (pathname.includes('onboarding')) {
|
||||
setSiteId(site.id);
|
||||
}
|
||||
}, []);
|
||||
|
||||
onSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const {
|
||||
site,
|
||||
siteList,
|
||||
location: { pathname },
|
||||
} = this.props;
|
||||
|
||||
if (site.exists()) {
|
||||
this.props.update(this.props.site, this.props.site.id).then((response) => {
|
||||
if (!response || !response.errors || response.errors.size === 0) {
|
||||
this.props.onClose(null);
|
||||
this.props.fetchList();
|
||||
toast.success('Project updated successfully');
|
||||
} else {
|
||||
toast.error(response.errors[0]);
|
||||
}
|
||||
});
|
||||
const onSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (site.exists()) {
|
||||
update(site, site.id).then((response) => {
|
||||
if (!response || !response.errors || response.errors.size === 0) {
|
||||
onClose(null);
|
||||
fetchList();
|
||||
toast.success('Project updated successfully');
|
||||
} else {
|
||||
this.props.save(this.props.site).then((response) => {
|
||||
if (!response || !response.errors || response.errors.size === 0) {
|
||||
this.props.onClose(null);
|
||||
this.props.clearSearch();
|
||||
this.props.clearSearchLive();
|
||||
this.props.mstore.initClient();
|
||||
toast.success('Project added successfully');
|
||||
} else {
|
||||
toast.error(response.errors[0]);
|
||||
}
|
||||
});
|
||||
toast.error(response.errors[0]);
|
||||
}
|
||||
};
|
||||
|
||||
remove = async (site) => {
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Projects',
|
||||
confirmation: `Are you sure you want to delete this Project? We won't be able to record anymore sessions.`,
|
||||
})
|
||||
) {
|
||||
this.props.remove(site.id).then(() => {
|
||||
this.props.onClose(null);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
save(site).then((response) => {
|
||||
if (!response || !response.errors || response.errors.size === 0) {
|
||||
onClose(null);
|
||||
clearSearch();
|
||||
clearSearchLive();
|
||||
mstore.initClient();
|
||||
toast.success('Project added successfully');
|
||||
} else {
|
||||
toast.error(response.errors[0]);
|
||||
}
|
||||
};
|
||||
|
||||
edit = ({ target: { name, value } }) => {
|
||||
this.setState({ existsError: false });
|
||||
this.props.edit({ [name]: value });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { site, loading } = this.props;
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">{site.exists() ? 'Edit Project' : 'New Project'}</h3>
|
||||
<Form className={styles.formWrapper} onSubmit={site.validate() && this.onSubmit}>
|
||||
<div className={styles.content}>
|
||||
<Form.Field>
|
||||
<label>{'Name'}</label>
|
||||
<Input placeholder="Ex. openreplay" name="name" maxLength={40} value={site.name} onChange={this.edit} className={styles.input} />
|
||||
</Form.Field>
|
||||
<div className="mt-6 flex justify-between">
|
||||
<Button variant="primary" type="submit" className="float-left mr-2" loading={loading} disabled={!site.validate()}>
|
||||
{site.exists() ? 'Update' : 'Add'}
|
||||
</Button>
|
||||
{site.exists() && (
|
||||
<Button variant="text" type="button" onClick={() => this.remove(site)}>
|
||||
<Icon name="trash" size="16" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{this.state.existsError && <div className={styles.errorMessage}>{'Project exists already.'}</div>}
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = async () => {
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Projects',
|
||||
confirmation: `Are you sure you want to delete this Project? We won't be able to record anymore sessions.`,
|
||||
})
|
||||
) {
|
||||
remove(site.id).then(() => {
|
||||
onClose(null);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = ({ target: { name, value } }) => {
|
||||
setExistsError(false);
|
||||
edit({ [name]: value });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">{site.exists() ? 'Edit Project' : 'New Project'}</h3>
|
||||
<Form className={styles.formWrapper} onSubmit={site.validate() && onSubmit}>
|
||||
<div className={styles.content}>
|
||||
<Form.Field>
|
||||
<label>{'Name'}</label>
|
||||
<Input
|
||||
placeholder="Ex. openreplay"
|
||||
name="name"
|
||||
maxLength={40}
|
||||
value={site.name}
|
||||
onChange={handleEdit}
|
||||
className={styles.input}
|
||||
/>
|
||||
</Form.Field>
|
||||
<div className="mt-6 flex justify-between">
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
className="float-left mr-2"
|
||||
loading={loading}
|
||||
disabled={!site.validate()}
|
||||
>
|
||||
{site.exists() ? 'Update' : 'Add'}
|
||||
</Button>
|
||||
{site.exists() && (
|
||||
<Button variant="text" type="button" onClick={handleRemove}>
|
||||
<Icon name="trash" size="16" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{existsError && <div className={styles.errorMessage}>{'Project exists already.'}</div>}
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
site: state.getIn(['site', 'instance']),
|
||||
siteList: state.getIn(['site', 'list']),
|
||||
loading: state.getIn(['site', 'save', 'loading']) || state.getIn(['site', 'remove', 'loading']),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
save,
|
||||
remove,
|
||||
edit,
|
||||
update,
|
||||
pushNewSite,
|
||||
fetchList,
|
||||
setSiteId,
|
||||
clearSearch,
|
||||
clearSearchLive,
|
||||
})(withRouter(withStore(NewSiteForm)));
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ function UserForm(props: Props) {
|
|||
const { hideModal } = useModal();
|
||||
const { userStore, roleStore } = useStore();
|
||||
const isSaving = useObserver(() => userStore.saving);
|
||||
const user: any = useObserver(() => userStore.instance);
|
||||
const user: any = useObserver(() => userStore.instance || userStore.initUser());
|
||||
const roles = useObserver(() => roleStore.list.filter(r => r.isProtected ? user.isSuperAdmin : true).map(r => ({ label: r.name, value: r.roleId })));
|
||||
|
||||
const onChangeCheckbox = (e: any) => {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ function PreferencesView(props: Props) {
|
|||
|
||||
<div className="flex items-center p-3 text-lg">
|
||||
<Icon name="info-circle" size="16" color="gray-dark" />
|
||||
<span className="ml-2">Updates are be applied at organization level.</span>
|
||||
<span className="ml-2">Any changes will be put into effect across your organization.</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
import React from 'react'
|
||||
import SideMenu from './components/SideMenu'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { Switch, Route, Redirect } from 'react-router'
|
||||
import { OB_TABS, onboarding as onboardingRoute } from 'App/routes'
|
||||
import InstallOpenReplayTab from './components/InstallOpenReplayTab'
|
||||
import IdentifyUsersTab from './components/IdentifyUsersTab'
|
||||
import IntegrationsTab from './components/IntegrationsTab'
|
||||
import ManageUsersTab from './components/ManageUsersTab'
|
||||
import OnboardingNavButton from './components/OnboardingNavButton'
|
||||
import * as routes from '../../routes'
|
||||
|
||||
const withSiteId = routes.withSiteId;
|
||||
|
||||
const Onboarding = (props) => {
|
||||
const { match: { params: { activeTab } } } = props;
|
||||
|
||||
const route = path => {
|
||||
return withSiteId(onboardingRoute(path));
|
||||
}
|
||||
|
||||
const renderActiveTab = () => (
|
||||
<Switch>
|
||||
<Route exact strict path={ route(OB_TABS.INSTALLING) } component={ () => <InstallOpenReplayTab /> } />
|
||||
<Route exact strict path={ route(OB_TABS.IDENTIFY_USERS) } component={ () => <IdentifyUsersTab /> } />
|
||||
<Route exact strict path={ route(OB_TABS.MANAGE_USERS) } component={ () => <ManageUsersTab /> } />
|
||||
<Route exact strict path={ route(OB_TABS.INTEGRATIONS) } component={ () => <IntegrationsTab /> } />
|
||||
<Redirect to={ route(OB_TABS.INSTALLING) } />
|
||||
</Switch>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="page flex relative h-full" style={{ minHeight: '100vh', paddingBottom: '75px' }}>
|
||||
<div className="flex w-full">
|
||||
<div className="flex-1 flex bg-ray">
|
||||
<div className="pt-6 px-6" style={{ width: '250px'}}>
|
||||
<SideMenu />
|
||||
</div>
|
||||
<div className="bg-white flex-1 h-full px-6">
|
||||
{ activeTab && renderActiveTab()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="py-6 px-4 w-full flex items-center fixed bottom-0 bg-white border-t z-10">
|
||||
<div className="ml-auto">
|
||||
<OnboardingNavButton />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(Onboarding);
|
||||
63
frontend/app/components/Onboarding/Onboarding.tsx
Normal file
63
frontend/app/components/Onboarding/Onboarding.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import React from 'react';
|
||||
import SideMenu from './components/SideMenu';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { Switch, Route, Redirect, RouteComponentProps } from 'react-router';
|
||||
import { OB_TABS, onboarding as onboardingRoute } from 'App/routes';
|
||||
import InstallOpenReplayTab from './components/InstallOpenReplayTab';
|
||||
import IdentifyUsersTab from './components/IdentifyUsersTab';
|
||||
import IntegrationsTab from './components/IntegrationsTab';
|
||||
import ManageUsersTab from './components/ManageUsersTab';
|
||||
import { withSiteId } from 'App/routes';
|
||||
interface Props {
|
||||
match: {
|
||||
params: {
|
||||
activeTab: string;
|
||||
siteId: string;
|
||||
};
|
||||
};
|
||||
history: RouteComponentProps['history'];
|
||||
}
|
||||
const Onboarding = (props: Props) => {
|
||||
const {
|
||||
match: {
|
||||
params: { activeTab, siteId },
|
||||
},
|
||||
} = props;
|
||||
|
||||
const route = (path: string) => {
|
||||
return withSiteId(onboardingRoute(path));
|
||||
};
|
||||
|
||||
const onMenuItemClick = (tab: string) => {
|
||||
props.history.push(withSiteId(onboardingRoute(tab), siteId));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page-margin container-90 flex relative">
|
||||
<div className="side-menu">
|
||||
<SideMenu activeTab={activeTab} onClick={onMenuItemClick} />
|
||||
</div>
|
||||
<div className="side-menu-margined w-full">
|
||||
<div
|
||||
className="bg-white w-full rounded-lg mx-auto mb-8 border"
|
||||
style={{ maxWidth: '1300px' }}
|
||||
>
|
||||
<Switch>
|
||||
<Route exact strict path={route(OB_TABS.INSTALLING)} component={InstallOpenReplayTab} />
|
||||
<Route exact strict path={route(OB_TABS.IDENTIFY_USERS)} component={IdentifyUsersTab} />
|
||||
<Route exact strict path={route(OB_TABS.MANAGE_USERS)} component={ManageUsersTab} />
|
||||
<Route exact strict path={route(OB_TABS.INTEGRATIONS)} component={IntegrationsTab} />
|
||||
<Redirect to={route(OB_TABS.INSTALLING)} />
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="py-6 px-4 w-full flex items-center fixed bottom-0 bg-white border-t z-10">
|
||||
<div className="ml-auto">
|
||||
<OnboardingNavButton />
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default withRouter(Onboarding);
|
||||
|
|
@ -9,4 +9,5 @@
|
|||
color: white;
|
||||
font-size: 12px;
|
||||
margin-right: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
import React from 'react'
|
||||
import CircleNumber from '../CircleNumber'
|
||||
import MetadataList from '../MetadataList/MetadataList'
|
||||
import { HighlightCode } from 'UI'
|
||||
|
||||
export default function IdentifyUsersTab() {
|
||||
return (
|
||||
<div className="flex pt-8 -mx-4">
|
||||
<div className="w-8/12 px-4">
|
||||
<h1 className="text-3xl font-bold flex items-center mb-4">
|
||||
<span>🕵️♂️</span>
|
||||
<div className="ml-3">Identify Users</div>
|
||||
</h1>
|
||||
<div>
|
||||
<div className="font-bold mb-4 text-lg">By User ID</div>
|
||||
<div className="mb-2">
|
||||
Call <span className="highlight-gray">setUserID</span> to identify your users when recording a session. The identity of the user can be changed, but OpenReplay will only keep the last communicated user ID.
|
||||
</div>
|
||||
<HighlightCode className="js" text={`tracker.setUserID('john@doe.com');`} />
|
||||
</div>
|
||||
|
||||
<div className="my-8" />
|
||||
<div>
|
||||
<div className="font-bold mb-4 text-lg">By adding metadata</div>
|
||||
<div className="flex items-start">
|
||||
<CircleNumber text="1" />
|
||||
<div className="pt-1">
|
||||
<span className="font-bold">Explicitly specify the metadata</span>
|
||||
<div className="my-2">You can add up to 10 keys.</div>
|
||||
<div className="my-2" />
|
||||
|
||||
<MetadataList />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="my-6" />
|
||||
<div className="flex items-start">
|
||||
<CircleNumber text="2" />
|
||||
<div className="pt-1">
|
||||
<span className="font-bold">Inject metadata when recording sessions</span>
|
||||
<div className="my-2">Use the <span className="highlight-gray">setMetadata</span> method in your code to inject custom user data in the form of a key/value pair (string).</div>
|
||||
<HighlightCode className="js" text={`tracker.setMetadata('plan', 'premium');`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="my-8" />
|
||||
<div className="w-4/12 py-6">
|
||||
<div className="p-5 bg-gray-lightest mb-4 rounded">
|
||||
<div className="font-bold mb-2">Why Identify Users?</div>
|
||||
<div className="text-sm">Make it easy to search and filter replays by user id. OpenReplay allows you to associate your internal-user-id with the recording.</div>
|
||||
</div>
|
||||
|
||||
<div className="p-5 bg-gray-lightest mb-4 rounded">
|
||||
<div className="font-bold mb-2">What is Metadata?</div>
|
||||
<div className="text-sm">Additional information about users can be provided with metadata (also known as traits or user variables). They take the form of key/value pairs, and are useful for filtering and searching for specific session replays.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
import React from 'react';
|
||||
import CircleNumber from '../CircleNumber';
|
||||
import MetadataList from '../MetadataList/MetadataList';
|
||||
import { HighlightCode, Icon, Button } from 'UI';
|
||||
import DocCard from 'Shared/DocCard/DocCard';
|
||||
import withOnboarding, { WithOnboardingProps } from '../withOnboarding';
|
||||
import { OB_TABS } from 'App/routes';
|
||||
|
||||
interface Props extends WithOnboardingProps {}
|
||||
|
||||
function IdentifyUsersTab(props: Props) {
|
||||
return (
|
||||
<>
|
||||
<h1 className="flex items-center px-4 py-3 border-b text-2xl">
|
||||
<span>🕵️♂️</span>
|
||||
<div className="ml-3">Identify Users</div>
|
||||
</h1>
|
||||
<div className="grid grid-cols-6 gap-4 w-full p-4">
|
||||
<div className="col-span-4">
|
||||
<div>
|
||||
<div className="font-medium mb-2 text-lg">Identify users by user ID</div>
|
||||
<div className="mb-2">
|
||||
Call <span className="highlight-blue">setUserID</span> to identify your users when
|
||||
recording a session.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center my-2">
|
||||
<Icon name="info-circle" color="gray-darkest" />
|
||||
<span className="ml-2">OpenReplay keeps the last communicated user ID.</span>
|
||||
</div>
|
||||
|
||||
<HighlightCode className="js" text={`tracker.setUserID('john@doe.com');`} />
|
||||
<div className="border-t my-8" />
|
||||
|
||||
<div className="my-8" />
|
||||
<div>
|
||||
<div className="font-medium mb-2 text-lg">Identify users by adding metadata</div>
|
||||
<p>
|
||||
To identify users through metadata, you will have to explicitly specify your user
|
||||
metadata so it can be injected during sessions. Follow the below steps
|
||||
</p>
|
||||
<div className="flex items-start">
|
||||
<CircleNumber text="1" />
|
||||
<MetadataList />
|
||||
</div>
|
||||
|
||||
<div className="my-6" />
|
||||
<div className="flex items-start">
|
||||
<CircleNumber text="2" />
|
||||
<div className="pt-1">
|
||||
<span className="font-bold">Inject metadata when recording sessions</span>
|
||||
<div className="my-2">
|
||||
Use the <span className="highlight-blue">setMetadata</span> method in your code to
|
||||
inject custom user data in the form of a key/value pair (string).
|
||||
</div>
|
||||
<HighlightCode className="js" text={`tracker.setMetadata('plan', 'premium');`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<DocCard
|
||||
title="Why to identify users?"
|
||||
icon="question-lg"
|
||||
iconBgColor="bg-red-lightest"
|
||||
iconColor="red"
|
||||
>
|
||||
Make it easy to search and filter replays by user id. OpenReplay allows you to associate
|
||||
your internal-user-id with the recording.
|
||||
</DocCard>
|
||||
|
||||
<DocCard title="What is Metadata?" icon="lightbulb">
|
||||
Additional information about users can be provided with metadata (also known as traits
|
||||
or user variables). They take the form of key/value pairs, and are useful for filtering
|
||||
and searching for specific session replays.
|
||||
</DocCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t px-4 py-3 flex justify-end gap-4">
|
||||
<Button variant="text-primary" onClick={() => (props.skip ? props.skip() : null)}>
|
||||
Skip
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
className=""
|
||||
onClick={() => (props.navTo ? props.navTo(OB_TABS.MANAGE_USERS) : null)}
|
||||
>
|
||||
Invite Team Members
|
||||
<Icon name="arrow-right-short" color="white" size={20} />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default withOnboarding(IdentifyUsersTab);
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import React from 'react'
|
||||
import OnboardingTabs from '../OnboardingTabs'
|
||||
import ProjectFormButton from '../ProjectFormButton'
|
||||
|
||||
export default function InstallOpenReplayTab() {
|
||||
return (
|
||||
<div className="pt-8">
|
||||
<h1 className="flex items-center mb-4">
|
||||
<span className="text-3xl">👋</span>
|
||||
<div className="ml-3 flex items-end">
|
||||
<span className="text-3xl font-bold">Hey there! Setup</span>
|
||||
<ProjectFormButton />
|
||||
</div>
|
||||
</h1>
|
||||
<div className="mb-6">OpenReplay can be installed via script or NPM package (recommended).</div>
|
||||
<OnboardingTabs />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import React from 'react';
|
||||
import OnboardingTabs from '../OnboardingTabs';
|
||||
import ProjectFormButton from '../ProjectFormButton';
|
||||
import { Button, Icon } from 'UI';
|
||||
import withOnboarding from '../withOnboarding';
|
||||
import { WithOnboardingProps } from '../withOnboarding';
|
||||
import { OB_TABS } from 'App/routes';
|
||||
|
||||
interface Props extends WithOnboardingProps {}
|
||||
|
||||
function InstallOpenReplayTab(props: Props) {
|
||||
const { site } = props;
|
||||
return (
|
||||
<>
|
||||
<h1 className="flex items-center px-4 py-3 border-b justify-between">
|
||||
<div className="flex items-center text-2xl">
|
||||
<span>👋</span>
|
||||
<div className="ml-3 flex items-end">
|
||||
<span>Hey there! Setup</span>
|
||||
<ProjectFormButton />
|
||||
</div>
|
||||
</div>
|
||||
<a className="flex items-center link" href="https://docs.openreplay.com/en/installation/javascript-sdk/" target="_blank">
|
||||
<Icon name="book" color="blue" className="mr-2" size={16} />
|
||||
<span>Setup Guide</span>
|
||||
</a>
|
||||
</h1>
|
||||
<div className="p-4">
|
||||
<div className="mb-6 text-lg font-medium">
|
||||
Setup OpenReplay through NPM package <span className="text-sm">(recommended)</span> or
|
||||
script.
|
||||
</div>
|
||||
<OnboardingTabs site={site} />
|
||||
</div>
|
||||
<div className="border-t px-4 py-3 flex justify-end">
|
||||
<Button
|
||||
variant="primary"
|
||||
className=""
|
||||
onClick={() => (props.navTo ? props.navTo(OB_TABS.IDENTIFY_USERS) : null)}
|
||||
>
|
||||
Identify Users
|
||||
<Icon name="arrow-right-short" color="white" size={20} />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default withOnboarding(InstallOpenReplayTab);
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
import React from 'react'
|
||||
import { Icon } from 'UI'
|
||||
import Integrations from '../../../Client/Integrations'
|
||||
|
||||
function IntegrationItem({ icon, title, onClick= () => null }) {
|
||||
return (
|
||||
<div className="flex flex-col items-center mr-16">
|
||||
<Icon name={icon} size="40" />
|
||||
<div className="mt-1 text-sm">{title}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function IntegrationsTab() {
|
||||
return (
|
||||
<div className="flex pt-8 -mx-4">
|
||||
<div className="w-8/12 px-4">
|
||||
<h1 className="text-3xl font-bold flex items-center mb-4">
|
||||
<span>🔌</span>
|
||||
<div className="ml-3">Integrations</div>
|
||||
</h1>
|
||||
<Integrations hideHeader={true} />
|
||||
|
||||
{/* <div className="my-4"/>
|
||||
<h1 className="text-3xl font-bold flex items-center mb-4">
|
||||
<span>🔌</span>
|
||||
<div className="ml-3">Integrations</div>
|
||||
</h1>
|
||||
|
||||
<Integrations hideHeader /> */}
|
||||
|
||||
{/* <div className="mt-6">
|
||||
<div className="font-bold mb-4">How are you handling store management?</div>
|
||||
<div className="flex">
|
||||
<IntegrationItem icon="vendors/redux" title="Redux" />
|
||||
<IntegrationItem icon="vendors/vuex" title="VueX" />
|
||||
<IntegrationItem icon="vendors/graphql" title="GraphQL" />
|
||||
<IntegrationItem icon="vendors/ngrx" title="NgRx" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="divider" />
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="font-bold mb-4">How are you monitoring errors and crash reporting?</div>
|
||||
<div className="flex">
|
||||
<IntegrationItem icon="integrations/sentry" title="Sentry" />
|
||||
<IntegrationItem icon="integrations/bugsnag" title="Sentry" />
|
||||
<IntegrationItem icon="integrations/rollbar" title="Sentry" />
|
||||
<IntegrationItem icon="integrations/elasticsearch" title="Sentry" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="divider" />
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="font-bold mb-4">How are you logging backend errors?</div>
|
||||
<div className="flex">
|
||||
<IntegrationItem icon="integrations/datadog" title="Datadog" />
|
||||
<IntegrationItem icon="integrations/sumologic" title="Sumo Logic" />
|
||||
<IntegrationItem icon="integrations/stackdriver" title="Stackdriver" />
|
||||
<IntegrationItem icon="integrations/cloudwatch" title="CloudWatch" />
|
||||
<IntegrationItem icon="integrations/newrelic" title="New Relic" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="my-4" /> */}
|
||||
</div>
|
||||
<div className="py-6 w-4/12">
|
||||
<div className="p-5 bg-gray-lightest mb-4">
|
||||
<div className="font-bold mb-2">Why Use Plugins?</div>
|
||||
<div className="text-sm">Reproduce issues as if they happened in your own browser. Plugins help capture your application’s store, HTTP requests, GraphQL queries and more.</div>
|
||||
</div>
|
||||
|
||||
<div className="p-5 bg-gray-lightest mb-4">
|
||||
<div className="font-bold mb-2">Why Use Integrations?</div>
|
||||
<div className="text-sm">Sync your backend errors with sessions replays and see what happened front-to-back.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default IntegrationsTab
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import React from 'react';
|
||||
import { Button } from 'UI';
|
||||
import Integrations from 'App/components/Client/Integrations/Integrations';
|
||||
import withOnboarding, { WithOnboardingProps } from '../withOnboarding';
|
||||
|
||||
interface Props extends WithOnboardingProps {}
|
||||
function IntegrationsTab(props: Props) {
|
||||
return (
|
||||
<>
|
||||
<h1 className="flex items-center px-4 py-3 border-b text-2xl">
|
||||
<span>🔌</span>
|
||||
<div className="ml-3">Integrations</div>
|
||||
</h1>
|
||||
<Integrations hideHeader={true} />
|
||||
{/* <div className="py-6 w-4/12">
|
||||
<div className="p-5 bg-gray-lightest mb-4">
|
||||
<div className="font-bold mb-2">Why Use Plugins?</div>
|
||||
<div className="text-sm">
|
||||
Reproduce issues as if they happened in your own browser. Plugins help capture your
|
||||
application’s store, HTTP requests, GraphQL queries and more.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-5 bg-gray-lightest mb-4">
|
||||
<div className="font-bold mb-2">Why Use Integrations?</div>
|
||||
<div className="text-sm">
|
||||
Sync your backend errors with sessions replays and see what happened front-to-back.
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
<div className="border-t px-4 py-3 flex justify-end">
|
||||
<Button variant="primary" className="" onClick={() => (props.skip ? props.skip() : null)}>
|
||||
Complete Setup
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default withOnboarding(IntegrationsTab);
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import UsersView from 'App/components/Client/Users/UsersView'
|
||||
import React from 'react'
|
||||
|
||||
export default function ManageUsersTab() {
|
||||
return (
|
||||
<div className="flex pt-8 -mx-4">
|
||||
<div className="w-8/12 px-4">
|
||||
<h1 className="text-3xl font-bold flex items-center mb-4">
|
||||
<span>👨💻</span>
|
||||
<div className="ml-3">Invite Collaborators</div>
|
||||
</h1>
|
||||
|
||||
<UsersView isOnboarding={true} />
|
||||
</div>
|
||||
<div className="w-4/12 py-6">
|
||||
<div className="p-5 bg-gray-lightest mb-4 rounded">
|
||||
<div className="font-bold mb-2">Why Invite Collaborators?</div>
|
||||
<div className="text-sm">Session replay is useful for all team members, from developers, testers and product managers to design, support and marketing folks. Invite them all and start improving your app now.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import UsersView from 'App/components/Client/Users/UsersView';
|
||||
import DocCard from 'Shared/DocCard/DocCard';
|
||||
import React from 'react';
|
||||
import { Button, Icon } from 'UI';
|
||||
import withOnboarding, { WithOnboardingProps } from '../withOnboarding';
|
||||
import { OB_TABS } from 'App/routes';
|
||||
|
||||
interface Props extends WithOnboardingProps {}
|
||||
|
||||
function ManageUsersTab(props: Props) {
|
||||
return (
|
||||
<>
|
||||
<h1 className="flex items-center px-4 py-3 border-b text-2xl">
|
||||
<span>👨💻</span>
|
||||
<div className="ml-3">Invite Collaborators</div>
|
||||
</h1>
|
||||
<div className="grid grid-cols-6 gap-4 p-4">
|
||||
<div className="col-span-4">
|
||||
<UsersView isOnboarding={true} />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<DocCard
|
||||
title="Why Invite Collaborators?"
|
||||
icon="question-lg"
|
||||
iconBgColor="bg-red-lightest"
|
||||
iconColor="red"
|
||||
>
|
||||
<p>Come together and unlock the potential collaborative improvements!</p>
|
||||
<p>
|
||||
Session replays are useful to developers, designers, product managers and to everyone
|
||||
on the product team.
|
||||
</p>
|
||||
</DocCard>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t px-4 py-3 flex justify-end">
|
||||
<Button variant="text-primary" onClick={() => (props.skip ? props.skip() : null)}>
|
||||
Skip
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
className=""
|
||||
onClick={() => (props.navTo ? props.navTo(OB_TABS.INTEGRATIONS) : null)}
|
||||
>
|
||||
Configure Integrations
|
||||
<Icon name="arrow-right-short" color="white" size={20} />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default withOnboarding(ManageUsersTab);
|
||||
|
|
@ -1,69 +1,67 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import { Button, SlideModal, TagBadge } from 'UI'
|
||||
import { connect } from 'react-redux'
|
||||
import React, { useEffect } from 'react';
|
||||
import { Button, TagBadge } from 'UI';
|
||||
import { connect } from 'react-redux';
|
||||
import { fetchList, save, remove } from 'Duck/customField';
|
||||
import CustomFieldForm from '../../../Client/CustomFields/CustomFieldForm';
|
||||
import { confirm } from 'UI';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
const MetadataList = (props) => {
|
||||
const { site, fields } = props;
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
|
||||
const { showModal, hideModal } = useModal();
|
||||
|
||||
useEffect(() => {
|
||||
props.fetchList(site.id);
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
const save = (field) => {
|
||||
props.save(site.id, field).then(() => {
|
||||
setShowModal(false)
|
||||
props.save(site.id, field).then((response) => {
|
||||
if (!response || !response.errors || response.errors.size === 0) {
|
||||
hideModal();
|
||||
toast.success('Metadata added successfully!');
|
||||
} else {
|
||||
toast.error(response.errors[0]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const openModal = () => {
|
||||
setShowModal(!showModal);
|
||||
}
|
||||
showModal(<CustomFieldForm onClose={hideModal} onSave={save} />, { right: true });
|
||||
};
|
||||
|
||||
const removeMetadata = async (field) => {
|
||||
if (await confirm({
|
||||
header: 'Metadata',
|
||||
confirmation: `Are you sure you want to remove?`
|
||||
})) {
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Metadata',
|
||||
confirmation: `Are you sure you want to remove?`,
|
||||
})
|
||||
) {
|
||||
props.remove(site.id, field.index);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="py-2 flex">
|
||||
<Button variant="outline" onClick={() => openModal(true)}>Add Metadata</Button>
|
||||
<Button variant="outline" onClick={() => openModal(true)}>
|
||||
Add Metadata
|
||||
</Button>
|
||||
<div className="flex ml-2">
|
||||
{ fields.map((f, index) => (
|
||||
<TagBadge
|
||||
key={index}
|
||||
text={ f.key }
|
||||
onRemove={ () => removeMetadata(f) }
|
||||
outline
|
||||
/>
|
||||
// <div>{f.key}</div>
|
||||
{fields.map((f, index) => (
|
||||
<TagBadge key={index} text={f.key} onRemove={() => removeMetadata(f)} outline />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<SlideModal
|
||||
// title={ `${ (field.exists() ? 'Update' : 'Add') + ' Metadata Field' }` }
|
||||
title={ `Metadata Field` }
|
||||
size="small"
|
||||
isDisplayed={ showModal }
|
||||
content={ showModal && (
|
||||
<CustomFieldForm onClose={ () => setShowModal(false) } onSave={save} />
|
||||
)}
|
||||
onClose={ () => setShowModal(false) }
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(state => ({
|
||||
site: state.getIn([ 'site', 'instance' ]),
|
||||
fields: state.getIn(['customFields', 'list']).sortBy(i => i.index),
|
||||
field: state.getIn(['customFields', 'instance']),
|
||||
loading: state.getIn(['customFields', 'fetchRequest', 'loading']),
|
||||
}), { fetchList, save, remove })(MetadataList)
|
||||
export default connect(
|
||||
(state) => ({
|
||||
site: state.getIn(['site', 'instance']),
|
||||
fields: state.getIn(['customFields', 'list']).sortBy((i) => i.index),
|
||||
field: state.getIn(['customFields', 'instance']),
|
||||
loading: state.getIn(['customFields', 'fetchRequest', 'loading']),
|
||||
}),
|
||||
{ fetchList, save, remove }
|
||||
)(MetadataList);
|
||||
|
|
|
|||
|
|
@ -1,73 +1,96 @@
|
|||
import React from 'react'
|
||||
import { Icon, SideMenuitem } from 'UI'
|
||||
import cn from 'classnames'
|
||||
import stl from './onboardingMenu.module.css'
|
||||
import { OB_TABS, onboarding as onboardingRoute } from 'App/routes'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import * as routes from '../../../../routes'
|
||||
import React from 'react';
|
||||
import { Icon, SideMenuitem } from 'UI';
|
||||
import cn from 'classnames';
|
||||
import stl from './onboardingMenu.module.css';
|
||||
import { OB_TABS, onboarding as onboardingRoute } from 'App/routes';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import * as routes from '../../../../routes';
|
||||
|
||||
const withSiteId = routes.withSiteId;
|
||||
|
||||
const MENU_ITEMS = [OB_TABS.INSTALLING, OB_TABS.IDENTIFY_USERS, OB_TABS.MANAGE_USERS, OB_TABS.INTEGRATIONS]
|
||||
const MENU_ITEMS = [
|
||||
OB_TABS.INSTALLING,
|
||||
OB_TABS.IDENTIFY_USERS,
|
||||
OB_TABS.MANAGE_USERS,
|
||||
OB_TABS.INTEGRATIONS,
|
||||
];
|
||||
|
||||
const Item = ({ icon, text, completed, active, onClick }) => (
|
||||
<div className={
|
||||
cn(
|
||||
'cursor-pointer',
|
||||
stl.stepWrapper,
|
||||
{ [stl.completed]: completed, [stl.active]: active }
|
||||
)}
|
||||
<div
|
||||
className={cn('cursor-pointer', stl.stepWrapper, {
|
||||
[stl.completed]: completed,
|
||||
[stl.active]: active,
|
||||
})}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className={stl.verticleLine }/>
|
||||
<div className={stl.verticleLine} />
|
||||
<div className={cn('flex', stl.step)}>
|
||||
<div className={
|
||||
cn(
|
||||
"h-6 w-6 mr-3 rounded-full flex items-center justify-center",
|
||||
<div
|
||||
className={cn(
|
||||
'h-6 w-6 mr-3 rounded-full flex items-center justify-center',
|
||||
stl.iconWrapper,
|
||||
{'bg-gray-light' : !active || !completed }
|
||||
)}
|
||||
{ 'bg-gray-light': !active || !completed }
|
||||
)}
|
||||
>
|
||||
{ completed &&
|
||||
<Icon
|
||||
name={icon}
|
||||
color={active? 'white' : 'gray-medium' }
|
||||
size="18"
|
||||
/>
|
||||
}
|
||||
{/* {completed && <Icon name={icon} color={active ? 'white' : 'gray-medium'} size="18" />} */}
|
||||
</div>
|
||||
<div className="color-gray-dark">{text}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
const OnboardingMenu = (props) => {
|
||||
const { match: { params: { activeTab, siteId } }, history } = props;
|
||||
const activeIndex = MENU_ITEMS.findIndex(i => i === activeTab);
|
||||
const {
|
||||
match: {
|
||||
params: { activeTab, siteId },
|
||||
},
|
||||
history,
|
||||
} = props;
|
||||
const activeIndex = MENU_ITEMS.findIndex((i) => i === activeTab);
|
||||
|
||||
const setTab = (tab) => {
|
||||
history.push(withSiteId(onboardingRoute(tab), siteId));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ activeIndex === 0 && (
|
||||
<SideMenuitem
|
||||
title="Install OpenReplay"
|
||||
iconName="tools"
|
||||
active
|
||||
/>
|
||||
)}
|
||||
{ activeIndex > 0 && (
|
||||
<SideMenuitem title="Install OpenReplay" iconName="tools" active />
|
||||
<SideMenuitem title="Identify Users" iconName="tools" active />
|
||||
<SideMenuitem title="Invite Collaborators" iconName="tools" active />
|
||||
<SideMenuitem title="Integrations" iconName="tools" active />
|
||||
|
||||
<>
|
||||
<Item icon="check" text="Install OpenReplay" completed={activeIndex >= 0} active={activeIndex === 0} onClick={() => setTab(MENU_ITEMS[0])} />
|
||||
<Item icon="check" text="Identify Users" completed={activeIndex >= 1} active={activeIndex === 1} onClick={() => setTab(MENU_ITEMS[1])} />
|
||||
<Item icon="check" text="Invite Collaborators" completed={activeIndex >= 2} active={activeIndex === 2} onClick={() => setTab(MENU_ITEMS[2])} />
|
||||
<Item icon="check" text="Integrations" completed={activeIndex >= 3} active={activeIndex === 3} onClick={() => setTab(MENU_ITEMS[3])} />
|
||||
<Item
|
||||
icon="check"
|
||||
text="Install OpenReplay"
|
||||
completed={activeIndex >= 0}
|
||||
active={activeIndex === 0}
|
||||
onClick={() => setTab(MENU_ITEMS[0])}
|
||||
/>
|
||||
<Item
|
||||
icon="check"
|
||||
text="Identify Users"
|
||||
completed={activeIndex >= 1}
|
||||
active={activeIndex === 1}
|
||||
onClick={() => setTab(MENU_ITEMS[1])}
|
||||
/>
|
||||
<Item
|
||||
icon="check"
|
||||
text="Invite Collaborators"
|
||||
completed={activeIndex >= 2}
|
||||
active={activeIndex === 2}
|
||||
onClick={() => setTab(MENU_ITEMS[2])}
|
||||
/>
|
||||
<Item
|
||||
icon="check"
|
||||
text="Integrations"
|
||||
completed={activeIndex >= 3}
|
||||
active={activeIndex === 3}
|
||||
onClick={() => setTab(MENU_ITEMS[3])}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default withRouter(OnboardingMenu)
|
||||
export default withRouter(OnboardingMenu);
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ function InstallDocs({ site }) {
|
|||
<CircleNumber text="1" />
|
||||
Install the npm package.
|
||||
</div>
|
||||
<div className={ cn(stl.snippetWrapper, 'ml-10 mr-8') }>
|
||||
<div className={ cn(stl.snippetWrapper, 'ml-10') }>
|
||||
<CopyButton content={installationCommand} className={cn(stl.codeCopy, 'mt-2 mr-2')} />
|
||||
<Highlight className="cli">
|
||||
{installationCommand}
|
||||
|
|
@ -67,7 +67,7 @@ function InstallDocs({ site }) {
|
|||
<div className="flex ml-10 mt-4">
|
||||
<div className="w-full">
|
||||
{isSpa && (
|
||||
<div className="w-6/12">
|
||||
<div>
|
||||
<div className="mb-2 text-sm">If your website is a <strong>Single Page Application (SPA)</strong> use the below code:</div>
|
||||
<div className={ cn(stl.snippetWrapper) }>
|
||||
<CopyButton content={_usageCode} className={cn(stl.codeCopy, 'mt-2 mr-2')} />
|
||||
|
|
@ -79,7 +79,7 @@ function InstallDocs({ site }) {
|
|||
)}
|
||||
|
||||
{!isSpa && (
|
||||
<div className="w-6/12">
|
||||
<div>
|
||||
<div className="mb-2 text-sm">Otherwise, if your web app is <strong>Server-Side-Rendered (SSR)</strong> (i.e. NextJS, NuxtJS) use this snippet:</div>
|
||||
<div className={ cn(stl.snippetWrapper) }>
|
||||
<CopyButton content={_usageCodeSST} className={cn(stl.codeCopy, 'mt-2 mr-2')} />
|
||||
|
|
@ -92,7 +92,6 @@ function InstallDocs({ site }) {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t pt-4 mt-8">See <a href="https://docs.openreplay.com/installation/javascript-sdk" className="color-teal underline" target="_blank">Documentation</a> for the list of available options.</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Tabs } from 'UI';
|
||||
import ProjectCodeSnippet from './ProjectCodeSnippet';
|
||||
import InstallDocs from './InstallDocs';
|
||||
|
||||
const PROJECT = 'SCRIPT';
|
||||
const DOCUMENTATION = 'NPM';
|
||||
// const SEGMENT = 'SEGMENT';
|
||||
// const GOOGLE_TAG = 'GOOGLE TAG';
|
||||
const TABS = [
|
||||
{ key: DOCUMENTATION, text: DOCUMENTATION },
|
||||
{ key: PROJECT, text: PROJECT },
|
||||
// { key: SEGMENT, text: SEGMENT },
|
||||
// { key: GOOGLE_TAG, text: GOOGLE_TAG }
|
||||
];
|
||||
|
||||
class TrackingCodeModal extends React.PureComponent {
|
||||
state = { copied: false, changed: false, activeTab: DOCUMENTATION };
|
||||
|
||||
setActiveTab = (tab) => {
|
||||
this.setState({ activeTab: tab });
|
||||
}
|
||||
|
||||
renderActiveTab = () => {
|
||||
switch (this.state.activeTab) {
|
||||
case PROJECT:
|
||||
return <ProjectCodeSnippet />
|
||||
case DOCUMENTATION:
|
||||
return <InstallDocs />
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { activeTab } = this.state;
|
||||
return (
|
||||
<>
|
||||
<Tabs
|
||||
tabs={ TABS }
|
||||
active={ activeTab } onClick={ this.setActiveTab } />
|
||||
<div className="p-5 py-8">
|
||||
{ this.renderActiveTab() }
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TrackingCodeModal;
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Tabs, Icon, CopyButton } from 'UI';
|
||||
import ProjectCodeSnippet from './ProjectCodeSnippet';
|
||||
import InstallDocs from './InstallDocs';
|
||||
import DocCard from 'Shared/DocCard/DocCard';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import UserForm from 'App/components/Client/Users/components/UserForm/UserForm';
|
||||
|
||||
const PROJECT = 'SCRIPT';
|
||||
const DOCUMENTATION = 'NPM';
|
||||
const TABS = [
|
||||
{ key: DOCUMENTATION, text: DOCUMENTATION },
|
||||
{ key: PROJECT, text: PROJECT },
|
||||
];
|
||||
|
||||
interface Props {
|
||||
site: any;
|
||||
}
|
||||
const TrackingCodeModal = (props: Props) => {
|
||||
const { site } = props;
|
||||
const [activeTab, setActiveTab] = useState(DOCUMENTATION);
|
||||
const { showModal } = useModal();
|
||||
|
||||
const showUserModal = () => {
|
||||
showModal(<UserForm />, { right: true });
|
||||
};
|
||||
|
||||
const renderActiveTab = () => {
|
||||
switch (activeTab) {
|
||||
case PROJECT:
|
||||
return (
|
||||
<div className="grid grid-cols-6 gap-4">
|
||||
<div className="col-span-4">
|
||||
<ProjectCodeSnippet />
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<DocCard title="Need help from team member?">
|
||||
<a className="link" onClick={showUserModal}>
|
||||
Invite and Collaborate
|
||||
</a>
|
||||
</DocCard>
|
||||
<DocCard title="Project Key">
|
||||
<div className="rounded bg-white px-2 py-1 flex items-center justify-between">
|
||||
<span>{site.projectKey}</span>
|
||||
<CopyButton content={''} className="capitalize" />
|
||||
</div>
|
||||
</DocCard>
|
||||
<DocCard title="Other ways to install">
|
||||
<a
|
||||
className="link flex items-center"
|
||||
href="https://docs.openreplay.com/integrations/google-tag-manager"
|
||||
target="_blank"
|
||||
>
|
||||
Google Tag Manager (GTM)
|
||||
<Icon name="external-link-alt" className="ml-1" color="blue" />
|
||||
</a>
|
||||
</DocCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case DOCUMENTATION:
|
||||
return (
|
||||
<div className="grid grid-cols-6 gap-4">
|
||||
<div className="col-span-4">
|
||||
<InstallDocs />
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<DocCard title="Need help from team member?">Invite and Collaborate</DocCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs tabs={TABS} active={activeTab} onClick={setActiveTab} />
|
||||
<div className="p-5 py-8">{renderActiveTab()}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrackingCodeModal;
|
||||
|
|
@ -1,13 +1,12 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { editGDPR, saveGDPR, init } from 'Duck/site';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { Checkbox } from 'UI';
|
||||
import { Checkbox, Toggler } from 'UI';
|
||||
import GDPR from 'Types/site/gdpr';
|
||||
import cn from 'classnames'
|
||||
import stl from './projectCodeSnippet.module.css'
|
||||
import cn from 'classnames';
|
||||
import stl from './projectCodeSnippet.module.css';
|
||||
import CircleNumber from '../../CircleNumber';
|
||||
import Select from 'Shared/Select'
|
||||
import Select from 'Shared/Select';
|
||||
import CodeSnippet from 'Shared/CodeSnippet';
|
||||
|
||||
const inputModeOptions = [
|
||||
|
|
@ -16,117 +15,67 @@ const inputModeOptions = [
|
|||
{ label: 'Obscure all inputs', value: 'hidden' },
|
||||
];
|
||||
|
||||
const inputModeOptionsMap = {}
|
||||
inputModeOptions.forEach((o, i) => inputModeOptionsMap[o.value] = i)
|
||||
const inputModeOptionsMap = {};
|
||||
inputModeOptions.forEach((o, i) => (inputModeOptionsMap[o.value] = i));
|
||||
|
||||
const ProjectCodeSnippet = props => {
|
||||
// const site = props.sites.find(s => s.id === props.siteId);
|
||||
const ProjectCodeSnippet = (props) => {
|
||||
const { site } = props;
|
||||
const { gdpr } = props.site;
|
||||
const [changed, setChanged] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [changed, setChanged] = useState(false);
|
||||
const [isAssistEnabled, setAssistEnabled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const site = props.sites.find(s => s.id === props.siteId);
|
||||
const site = props.sites.find((s) => s.id === props.siteId);
|
||||
if (site) {
|
||||
props.init(site)
|
||||
props.init(site);
|
||||
}
|
||||
}, [])
|
||||
|
||||
const codeSnippet = `<!-- OpenReplay Tracking Code for HOST -->
|
||||
<script>
|
||||
var initOpts = {
|
||||
projectKey: "PROJECT_KEY",
|
||||
ingestPoint: "https://${window.location.hostname}/ingest",
|
||||
defaultInputMode: ${gdpr.defaultInputMode},
|
||||
obscureTextNumbers: ${gdpr.maskNumbers},
|
||||
obscureTextEmails: ${gdpr.maskEmails},
|
||||
};
|
||||
var startOpts = { userID: "" };
|
||||
(function(A,s,a,y,e,r){
|
||||
r=window.OpenReplay=[e,r,y,[s-1, e]];
|
||||
s=document.createElement('script');s.src=A;s.async=!a;
|
||||
document.getElementsByTagName('head')[0].appendChild(s);
|
||||
r.start=function(v){r.push([0])};
|
||||
r.stop=function(v){r.push([1])};
|
||||
r.setUserID=function(id){r.push([2,id])};
|
||||
r.setUserAnonymousID=function(id){r.push([3,id])};
|
||||
r.setMetadata=function(k,v){r.push([4,k,v])};
|
||||
r.event=function(k,p,i){r.push([5,k,p,i])};
|
||||
r.issue=function(k,p){r.push([6,k,p])};
|
||||
r.isActive=function(){return false};
|
||||
r.getSessionToken=function(){};
|
||||
})("${window.env.TRACKER_HOST || '//static.openreplay.com'}/${window.env.TRACKER_VERSION}/openreplay.js",1,0,initOpts,startOpts);
|
||||
</script>`;
|
||||
}, []);
|
||||
|
||||
const saveGDPR = (value) => {
|
||||
setChanged(true)
|
||||
props.saveGDPR(site.id, GDPR({...value}));
|
||||
}
|
||||
setChanged(true);
|
||||
props.saveGDPR(site.id, GDPR({ ...value }));
|
||||
};
|
||||
|
||||
const onChangeSelect = ({ name, value }) => {
|
||||
const _gdpr = { ...gdpr.toData() };
|
||||
_gdpr[name] = value;
|
||||
props.editGDPR({ [ name ]: value });
|
||||
saveGDPR(_gdpr)
|
||||
props.editGDPR({ [name]: value });
|
||||
saveGDPR(_gdpr);
|
||||
};
|
||||
|
||||
const onChangeOption = ({ target: { name, checked } }) => {
|
||||
const _gdpr = { ...gdpr.toData() };
|
||||
_gdpr[name] = checked;
|
||||
props.editGDPR({ [ name ]: checked });
|
||||
saveGDPR(_gdpr)
|
||||
}
|
||||
props.editGDPR({ [name]: checked });
|
||||
saveGDPR(_gdpr);
|
||||
};
|
||||
|
||||
const getOptionValues = () => {
|
||||
// const { gdpr } = site;
|
||||
return (!!gdpr.maskEmails)|(!!gdpr.maskNumbers << 1)|(['plain' , 'obscured', 'hidden'].indexOf(gdpr.defaultInputMode) << 5)|28
|
||||
}
|
||||
|
||||
|
||||
const getCodeSnippet = site => {
|
||||
let snippet = codeSnippet;
|
||||
if (site && site.id) {
|
||||
snippet = snippet.replace('PROJECT_KEY', site.projectKey);
|
||||
}
|
||||
return snippet
|
||||
.replace('XXX', getOptionValues())
|
||||
.replace('HOST', site && site.host);
|
||||
}
|
||||
|
||||
const copyHandler = (code) => {
|
||||
setCopied(true);
|
||||
copy(code);
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const _snippet = getCodeSnippet(site);
|
||||
|
||||
// console.log('gdpr.defaultInputMode', gdpr.defaultInputMode)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<div className="font-semibold mb-2 flex items-center">
|
||||
<CircleNumber text="1" /> Choose data recording options
|
||||
</div>
|
||||
<div className="flex items-center ml-10">
|
||||
|
||||
<div className="ml-10 mb-4" style={{ maxWidth: '50%' }}>
|
||||
<Select
|
||||
name="defaultInputMode"
|
||||
options={ inputModeOptions }
|
||||
onChange={ ({ value }) => onChangeSelect({ name: 'defaultInputMode', value: value.value }) }
|
||||
options={inputModeOptions}
|
||||
onChange={({ value }) =>
|
||||
onChangeSelect({ name: 'defaultInputMode', value: value.value })
|
||||
}
|
||||
placeholder="Default Input Mode"
|
||||
defaultValue={ gdpr.defaultInputMode }
|
||||
defaultValue={gdpr.defaultInputMode}
|
||||
/>
|
||||
<div className="mx-4" />
|
||||
</div>
|
||||
|
||||
<div className="mx-4" />
|
||||
<div className="flex items-center ml-10">
|
||||
<Checkbox
|
||||
name="maskNumbers"
|
||||
type="checkbox"
|
||||
checked={ gdpr.maskNumbers }
|
||||
onChange={ onChangeOption }
|
||||
checked={gdpr.maskNumbers}
|
||||
onChange={onChangeOption}
|
||||
className="mr-2"
|
||||
label="Do not record any numeric text"
|
||||
/>
|
||||
|
|
@ -136,54 +85,71 @@ const ProjectCodeSnippet = props => {
|
|||
<Checkbox
|
||||
name="maskEmails"
|
||||
type="checkbox"
|
||||
checked={ gdpr.maskEmails }
|
||||
onChange={ onChangeOption }
|
||||
checked={gdpr.maskEmails}
|
||||
onChange={onChangeOption}
|
||||
className="mr-2"
|
||||
label="Do not record email addresses"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={ cn(stl.info,'rounded bg-gray mt-2 mb-4', { 'hidden': !changed })}>
|
||||
<div className={cn(stl.info, 'rounded bg-gray mt-2 mb-4 ml-10', { hidden: !changed })}>
|
||||
Below code snippet changes depending on the data recording options chosen.
|
||||
</div>
|
||||
|
||||
<div className={ cn(stl.instructions, 'mt-8') }>
|
||||
|
||||
<div className={cn(stl.instructions, 'mt-8')}>
|
||||
<div className="font-semibold flex items-center">
|
||||
<CircleNumber text="2" />
|
||||
<span>Enable Assist (Optional)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-10">
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<Toggler
|
||||
label="Yes"
|
||||
checked={isAssistEnabled}
|
||||
name="test"
|
||||
className="font-medium mr-2"
|
||||
onChange={() => setAssistEnabled(!isAssistEnabled)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={cn(stl.instructions, 'mt-8')}>
|
||||
<div className="font-semibold flex items-center">
|
||||
<CircleNumber text="3" />
|
||||
<span>Install SDK</span>
|
||||
</div>
|
||||
<div className={ stl.siteId }>{ 'Project Key: ' } <span>{ site.projectKey }</span></div>
|
||||
</div>
|
||||
|
||||
<div className="ml-10 mb-2 text-sm">
|
||||
Paste this snippet <span>{ 'before the ' }</span>
|
||||
<span className={ stl.highLight }> { '</head>' } </span>
|
||||
<span>{ ' tag of your page.' }</span>
|
||||
<div className="ml-10 mb-2">
|
||||
Paste this snippet <span>{'before the '}</span>
|
||||
<span className={stl.highLight}> {'</head>'} </span>
|
||||
<span>{' tag of your page.'}</span>
|
||||
</div>
|
||||
<div className={ cn(stl.snippetsWrapper, 'ml-10') }>
|
||||
<div className={cn(stl.snippetsWrapper, 'ml-10')}>
|
||||
<CodeSnippet
|
||||
host={ site && site.host }
|
||||
projectKey={ site && site.projectKey }
|
||||
isAssistEnabled={isAssistEnabled}
|
||||
host={site && site.host}
|
||||
projectKey={site && site.projectKey}
|
||||
ingestPoint={`"https://${window.location.hostname}/ingest"`}
|
||||
defaultInputMode={ gdpr.defaultInputMode }
|
||||
obscureTextNumbers={ gdpr.maskNumbers }
|
||||
obscureTextEmails={ gdpr.maskEmails }
|
||||
defaultInputMode={gdpr.defaultInputMode}
|
||||
obscureTextNumbers={gdpr.maskNumbers}
|
||||
obscureTextEmails={gdpr.maskEmails}
|
||||
/>
|
||||
{/* <button className={ stl.codeCopy } onClick={ () => copyHandler(_snippet) }>{ copied ? 'copied' : 'copy' }</button>
|
||||
<Highlight className="html">
|
||||
{_snippet}
|
||||
</Highlight> */}
|
||||
</div>
|
||||
{/* TODO Extract for SaaS */}
|
||||
<div className="my-4">You can also setup OpenReplay using <a className="link" href="https://docs.openreplay.com/integrations/google-tag-manager" target="_blank">Google Tag Manager (GTM)</a>.</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(state => ({
|
||||
siteId: state.getIn([ 'site', 'siteId' ]),
|
||||
site: state.getIn([ 'site', 'instance' ]),
|
||||
sites: state.getIn([ 'site', 'list' ]),
|
||||
// gdpr: state.getIn([ 'site', 'instance', 'gdpr' ]),
|
||||
saving: state.getIn([ 'site', 'saveGDPR', 'loading' ])
|
||||
}), { editGDPR, saveGDPR, init })(ProjectCodeSnippet)
|
||||
export default connect(
|
||||
(state) => ({
|
||||
siteId: state.getIn(['site', 'siteId']),
|
||||
site: state.getIn(['site', 'instance']),
|
||||
sites: state.getIn(['site', 'list']),
|
||||
saving: state.getIn(['site', 'saveGDPR', 'loading']),
|
||||
}),
|
||||
{ editGDPR, saveGDPR, init }
|
||||
)(ProjectCodeSnippet);
|
||||
|
|
|
|||
|
|
@ -1,37 +1,35 @@
|
|||
import React, { useState } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { SlideModal } from 'UI'
|
||||
import NewSiteForm from '../../../Client/Sites/NewSiteForm'
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import NewSiteForm from '../../../Client/Sites/NewSiteForm';
|
||||
import { init } from 'Duck/site';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
|
||||
const ProjectFormButton = ({ sites, siteId, init }) => {
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const site = sites.find(({ id }) => id === siteId)
|
||||
|
||||
const closeModal = () => setShowModal(!showModal);
|
||||
const openModal = () => {
|
||||
setShowModal(true)
|
||||
init(site)
|
||||
const site = sites.find(({ id }) => id === siteId);
|
||||
const { showModal, hideModal } = useModal();
|
||||
const openModal = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
init(site);
|
||||
showModal(<NewSiteForm onClose={hideModal} />, { right: true });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
className="text-3xl font-bold ml-2 color-teal underline-dashed cursor-pointer"
|
||||
onClick={ () => openModal()}
|
||||
>{site && site.name}</span>
|
||||
<SlideModal
|
||||
title={ 'Project' }
|
||||
size="small"
|
||||
isDisplayed={ showModal }
|
||||
content={ <NewSiteForm onClose={ closeModal } /> }
|
||||
onClose={ closeModal }
|
||||
/>
|
||||
className="text-2xl font-bold ml-2 color-teal underline-dashed cursor-pointer"
|
||||
onClick={(e) => openModal(e)}
|
||||
>
|
||||
{site && site.name}
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(state => ({
|
||||
siteId: state.getIn([ 'site', 'siteId' ]),
|
||||
sites: state.getIn([ 'site', 'list' ]),
|
||||
}), { init })(ProjectFormButton)
|
||||
export default connect(
|
||||
(state) => ({
|
||||
siteId: state.getIn(['site', 'siteId']),
|
||||
sites: state.getIn(['site', 'list']),
|
||||
}),
|
||||
{ init }
|
||||
)(ProjectFormButton);
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
import React from 'react'
|
||||
import stl from './sideMenu.module.css'
|
||||
import cn from 'classnames'
|
||||
import { SideMenuitem } from 'UI'
|
||||
import OnboardingMenu from './OnboardingMenu/OnboardingMenu'
|
||||
|
||||
export default function SideMenu() {
|
||||
return (
|
||||
<div className={stl.wrapper}>
|
||||
<div className={ cn(stl.header, 'flex items-center') }>
|
||||
<div className={ stl.label }>
|
||||
<span>Setup Project</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<OnboardingMenu />
|
||||
|
||||
<div className={cn(stl.divider, 'my-4')} />
|
||||
|
||||
<div className={ cn(stl.header, 'flex items-center') }>
|
||||
<div className={ stl.label }>
|
||||
<span>Help</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SideMenuitem
|
||||
title="Documentation"
|
||||
iconName="journal-code"
|
||||
onClick={() => window.open('https://docs.openreplay.com', '_blank')}
|
||||
/>
|
||||
|
||||
<SideMenuitem
|
||||
title="Report Issue"
|
||||
iconName="github"
|
||||
onClick={() => window.open('https://github.com/openreplay/openreplay/issues', '_blank')}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
71
frontend/app/components/Onboarding/components/SideMenu.tsx
Normal file
71
frontend/app/components/Onboarding/components/SideMenu.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import React from 'react';
|
||||
import stl from './sideMenu.module.css';
|
||||
import cn from 'classnames';
|
||||
import { SideMenuitem } from 'UI';
|
||||
import { OB_TABS, onboarding as onboardingRoute } from 'App/routes';
|
||||
import OnboardingMenu from './OnboardingMenu/OnboardingMenu';
|
||||
import { withRouter } from 'react-router';
|
||||
|
||||
interface Props {
|
||||
activeTab: string;
|
||||
onClick: (tab: string) => void;
|
||||
}
|
||||
function SideMenu(props: Props) {
|
||||
const { activeTab } = props;
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className={cn(stl.header, 'flex items-center')}>
|
||||
<div className={stl.label}>
|
||||
<span>Setup Project</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SideMenuitem
|
||||
title="Install OpenReplay"
|
||||
iconName="tools"
|
||||
active={activeTab === OB_TABS.INSTALLING}
|
||||
onClick={() => props.onClick(OB_TABS.INSTALLING)}
|
||||
/>
|
||||
<SideMenuitem
|
||||
title="Identify Users"
|
||||
iconName="person-border"
|
||||
active={activeTab === OB_TABS.IDENTIFY_USERS}
|
||||
onClick={() => props.onClick(OB_TABS.IDENTIFY_USERS)}
|
||||
/>
|
||||
<SideMenuitem
|
||||
title="Invite Collaborators"
|
||||
iconName="people"
|
||||
active={activeTab === OB_TABS.MANAGE_USERS}
|
||||
onClick={() => props.onClick(OB_TABS.MANAGE_USERS)}
|
||||
/>
|
||||
<SideMenuitem
|
||||
title="Integrations"
|
||||
iconName="plug"
|
||||
active={activeTab === OB_TABS.INTEGRATIONS}
|
||||
onClick={() => props.onClick(OB_TABS.INTEGRATIONS)}
|
||||
/>
|
||||
|
||||
<div className={cn(stl.divider, 'my-4')} />
|
||||
|
||||
<div className={cn(stl.header, 'flex items-center')}>
|
||||
<div className={stl.label}>
|
||||
<span>Help</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SideMenuitem
|
||||
title="Documentation"
|
||||
iconName="journal-code"
|
||||
onClick={() => window.open('https://docs.openreplay.com', '_blank')}
|
||||
/>
|
||||
|
||||
<SideMenuitem
|
||||
title="Report Issue"
|
||||
iconName="github"
|
||||
onClick={() => window.open('https://github.com/openreplay/openreplay/issues', '_blank')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SideMenu;
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { setOnboarding } from 'Duck/user';
|
||||
import { sessions, withSiteId, onboarding as onboardingRoute } from 'App/routes';
|
||||
|
||||
export interface WithOnboardingProps {
|
||||
history: RouteComponentProps['history'];
|
||||
skip?: () => void;
|
||||
navTo?: (tab: string) => void;
|
||||
site?: any;
|
||||
match: {
|
||||
params: {
|
||||
activeTab: string;
|
||||
siteId: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const connector = connect(
|
||||
(state: any) => ({
|
||||
siteId: state.getIn(['site', 'siteId']),
|
||||
sites: state.getIn(['site', 'list']),
|
||||
}),
|
||||
{ setOnboarding }
|
||||
);
|
||||
|
||||
type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||
|
||||
const withOnboarding = <P extends RouteComponentProps>(
|
||||
Component: React.ComponentType<P & WithOnboardingProps & PropsFromRedux>
|
||||
) => {
|
||||
const WithOnboarding: React.FC<P & WithOnboardingProps & PropsFromRedux> = (props) => {
|
||||
const {
|
||||
sites,
|
||||
match: {
|
||||
params: { siteId },
|
||||
},
|
||||
} = props;
|
||||
const site = useMemo(() => sites.find((s: any) => s.id === siteId), [sites, siteId]);
|
||||
|
||||
const skip = () => {
|
||||
props.setOnboarding(true);
|
||||
props.history.push(sessions());
|
||||
};
|
||||
|
||||
const navTo = (tab: string) => {
|
||||
props.history.push(withSiteId(onboardingRoute(tab), siteId));
|
||||
};
|
||||
|
||||
return <Component skip={skip} navTo={navTo} {...props} site={site} />;
|
||||
};
|
||||
|
||||
return withRouter(connector(WithOnboarding as React.ComponentType<any>));
|
||||
};
|
||||
|
||||
export default withOnboarding;
|
||||
|
|
@ -12,6 +12,7 @@ const inputModeOptionsMap: any = {}
|
|||
inputModeOptions.forEach((o: any, i: any) => inputModeOptionsMap[o.value] = i)
|
||||
|
||||
interface Props {
|
||||
isAssistEnabled: boolean;
|
||||
host: string;
|
||||
projectKey: string;
|
||||
ingestPoint: string;
|
||||
|
|
@ -20,7 +21,7 @@ interface Props {
|
|||
obscureTextEmails: boolean;
|
||||
}
|
||||
function CodeSnippet(props: Props) {
|
||||
const { host, projectKey, ingestPoint, defaultInputMode, obscureTextNumbers, obscureTextEmails } = props;
|
||||
const { host, projectKey, ingestPoint, defaultInputMode, obscureTextNumbers, obscureTextEmails, isAssistEnabled } = props;
|
||||
const codeSnippet = `<!-- OpenReplay Tracking Code for ${host} -->
|
||||
<script>
|
||||
var initOpts = {
|
||||
|
|
@ -44,7 +45,7 @@ function CodeSnippet(props: Props) {
|
|||
r.issue=function(k,p){r.push([6,k,p])};
|
||||
r.isActive=function(){return false};
|
||||
r.getSessionToken=function(){};
|
||||
})("${window.env.TRACKER_HOST || '//static.openreplay.com'}/${window.env.TRACKER_VERSION}/openreplay.js",1,0,initOpts,startOpts);
|
||||
})("${window.env.TRACKER_HOST || '//static.openreplay.com'}/${window.env.TRACKER_VERSION}/openreplay${isAssistEnabled ? '-assist.js' : '.js'}",1,0,initOpts,startOpts);
|
||||
</script>`;
|
||||
|
||||
return (
|
||||
|
|
|
|||
37
frontend/app/components/shared/DocCard/DocCard.tsx
Normal file
37
frontend/app/components/shared/DocCard/DocCard.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import cn from 'classnames';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
icon?: string;
|
||||
iconColor?: string;
|
||||
iconBgColor?: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
function DocCard(props: Props) {
|
||||
const { className = '', iconColor = 'tealx', iconBgColor = 'bg-tealx-light' } = props;
|
||||
|
||||
return (
|
||||
<div className={cn('p-5 bg-gray-lightest mb-4 rounded', className)}>
|
||||
<div className="font-medium mb-2 flex items-center">
|
||||
{props.icon && (
|
||||
<div
|
||||
className={cn(
|
||||
'w-8 h-8 rounded-full flex items-center justify-center shrink-0 mr-2',
|
||||
iconBgColor
|
||||
)}
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
<Icon name={props.icon} size={18} color={iconColor} />
|
||||
</div>
|
||||
)}
|
||||
<span>{props.title}</span>
|
||||
</div>
|
||||
<div className="text-sm">{props.children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DocCard;
|
||||
1
frontend/app/components/shared/DocCard/index.ts
Normal file
1
frontend/app/components/shared/DocCard/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './DocCard';
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import styles from './toggler.module.css';
|
||||
|
||||
export default ({ onChange, name, className = '', checked, label = '', plain = false }) => (
|
||||
<div className={className}>
|
||||
<div className={className + ' w-fit'}>
|
||||
<label className={styles.label}>
|
||||
<div className={plain ? styles.switchPlain : styles.switch}>
|
||||
<input type={styles.checkbox} onClick={onChange} name={name} defaultChecked={checked} />
|
||||
|
|
|
|||
|
|
@ -256,6 +256,12 @@
|
|||
padding: 1px 2px;
|
||||
}
|
||||
|
||||
.highlight-blue {
|
||||
background-color: $active-blue;
|
||||
border-radius: 3px;
|
||||
padding: 1px 3px;
|
||||
}
|
||||
|
||||
.hljs {
|
||||
padding: 10px !important;
|
||||
border-radius: 6px !important;
|
||||
|
|
|
|||
3
frontend/app/svg/icons/people.svg
Normal file
3
frontend/app/svg/icons/people.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-people" viewBox="0 0 16 16">
|
||||
<path d="M15 14s1 0 1-1-1-4-5-4-5 3-5 4 1 1 1 1h8Zm-7.978-1A.261.261 0 0 1 7 12.996c.001-.264.167-1.03.76-1.72C8.312 10.629 9.282 10 11 10c1.717 0 2.687.63 3.24 1.276.593.69.758 1.457.76 1.72l-.008.002a.274.274 0 0 1-.014.002H7.022ZM11 7a2 2 0 1 0 0-4 2 2 0 0 0 0 4Zm3-2a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM6.936 9.28a5.88 5.88 0 0 0-1.23-.247A7.35 7.35 0 0 0 5 9c-4 0-5 3-5 4 0 .667.333 1 1 1h4.216A2.238 2.238 0 0 1 5 13c0-1.01.377-2.042 1.09-2.904.243-.294.526-.569.846-.816ZM4.92 10A5.493 5.493 0 0 0 4 13H1c0-.26.164-1.03.76-1.724.545-.636 1.492-1.256 3.16-1.275ZM1.5 5.5a3 3 0 1 1 6 0 3 3 0 0 1-6 0Zm3-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 745 B |
4
frontend/app/svg/icons/person-border.svg
Normal file
4
frontend/app/svg/icons/person-border.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-person-bounding-box" viewBox="0 0 16 16">
|
||||
<path d="M1.5 1a.5.5 0 0 0-.5.5v3a.5.5 0 0 1-1 0v-3A1.5 1.5 0 0 1 1.5 0h3a.5.5 0 0 1 0 1h-3zM11 .5a.5.5 0 0 1 .5-.5h3A1.5 1.5 0 0 1 16 1.5v3a.5.5 0 0 1-1 0v-3a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 1-.5-.5zM.5 11a.5.5 0 0 1 .5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 1 0 1h-3A1.5 1.5 0 0 1 0 14.5v-3a.5.5 0 0 1 .5-.5zm15 0a.5.5 0 0 1 .5.5v3a1.5 1.5 0 0 1-1.5 1.5h-3a.5.5 0 0 1 0-1h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 1 .5-.5z"/>
|
||||
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4Zm-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664h10Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 784 B |
3
frontend/app/svg/icons/plug.svg
Normal file
3
frontend/app/svg/icons/plug.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-plug" viewBox="0 0 16 16">
|
||||
<path d="M6 0a.5.5 0 0 1 .5.5V3h3V.5a.5.5 0 0 1 1 0V3h1a.5.5 0 0 1 .5.5v3A3.5 3.5 0 0 1 8.5 10c-.002.434-.01.845-.04 1.22-.041.514-.126 1.003-.317 1.424a2.083 2.083 0 0 1-.97 1.028C6.725 13.9 6.169 14 5.5 14c-.998 0-1.61.33-1.974.718A1.922 1.922 0 0 0 3 16H2c0-.616.232-1.367.797-1.968C3.374 13.42 4.261 13 5.5 13c.581 0 .962-.088 1.218-.219.241-.123.4-.3.514-.55.121-.266.193-.621.23-1.09.027-.34.035-.718.037-1.141A3.5 3.5 0 0 1 4 6.5v-3a.5.5 0 0 1 .5-.5h1V.5A.5.5 0 0 1 6 0zM5 4v2.5A2.5 2.5 0 0 0 7.5 9h1A2.5 2.5 0 0 0 11 6.5V4H5z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 645 B |
Loading…
Add table
Reference in a new issue