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:
Shekar Siri 2023-04-06 18:35:26 +02:00 committed by GitHub
parent 7ed74ac412
commit 704abbb47a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 1040 additions and 777 deletions

View file

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

View file

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

View file

@ -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 applications store, monitor queries, track performance issues and even
assist your end user through live sessions.
</DocCard>
),
description:
"Reproduce issues as if they happened in your own browser. Plugins help capture your application's store, HTTP requeets, GraphQL queries, and more.",
integrations: [

View file

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

View file

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

View file

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

View file

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

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

View file

@ -9,4 +9,5 @@
color: white;
font-size: 12px;
margin-right: 10px;
flex-shrink: 0;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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;

View file

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

View file

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

View 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;

View file

@ -0,0 +1 @@
export { default } from './DocCard';

File diff suppressed because one or more lines are too long

View file

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

View file

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

View 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

View 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

View 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