change(ui) - player improvements (#1164)

* change(ui) - player - back button spacing

* change(ui) - onboarding - changes

* change(ui) - onboarding - changes

* change(ui) - integrations gap-4

* change(ui) - install script copy button styles

* change(ui) - copy button in account settings

* fix(ui) - error details modal loader position

* change(ui) - share popup styles

* change(ui) - player improvements

* change(ui) - player improvements - playback speed with menu

* change(ui) - player improvements - current timezone

* change(ui) - player improvements - autoplay options
This commit is contained in:
Shekar Siri 2023-04-13 17:53:36 +02:00 committed by GitHub
parent c4cc3ed234
commit d0bcae82f2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 400 additions and 330 deletions

View file

@ -87,7 +87,7 @@ function Integrations(props: Props) {
<div className="mb-4 p-5">
{!hideHeader && <PageTitle title={<div>Integrations</div>} />}
{integrations.map((cat: any) => (
<div className="grid grid-cols-6 border-b last:border-none">
<div className="grid grid-cols-6 border-b last:border-none gap-4">
<div
className={cn('col-span-4 mb-2 py-3', cat.docs ? 'col-span-4' : 'col-span-6')}
key={cat.key}
@ -192,7 +192,7 @@ const integrations = [
'Sync your backend errors with sessions replays and see what happened front-to-back.',
docs: () => (
<DocCard
title="Why use integrations"
title="Why use integrations?"
icon="question-lg"
iconBgColor="bg-red-lightest"
iconColor="red"

View file

@ -2,7 +2,7 @@ import React from 'react';
import copy from 'copy-to-clipboard';
import { connect } from 'react-redux';
import styles from './profileSettings.module.css';
import { Form, Input, Button } from 'UI';
import { Form, Input, Button, CopyButton } from 'UI';
@connect(state => ({
apiKey: state.getIn([ 'user', 'account', 'apiKey' ]),
@ -36,14 +36,7 @@ export default class Api extends React.PureComponent {
readOnly={ true }
value={ apiKey }
leadingButton={
<Button
type="button"
variant="text-primary"
role="button"
onClick={ this.copyHandler }
>
{ copied ? 'copied' : 'copy' }
</Button>
<CopyButton content={ apiKey } />
}
/>
</Form.Field>

View file

@ -11,52 +11,56 @@ import AddUserButton from './components/AddUserButton';
import withPageTitle from 'HOCs/withPageTitle';
interface Props {
isOnboarding?: boolean;
account: any;
isEnterprise: boolean;
isOnboarding?: boolean;
account: any;
isEnterprise: boolean;
}
function UsersView(props: Props) {
const { account, isEnterprise, isOnboarding = false } = props;
const { userStore, roleStore } = useStore();
const userCount = useObserver(() => userStore.list.length);
const roles = useObserver(() => roleStore.list);
const { showModal } = useModal();
const isAdmin = account.admin || account.superAdmin;
const { account, isEnterprise, isOnboarding = false } = props;
const { userStore, roleStore } = useStore();
const userCount = useObserver(() => userStore.list.length);
const roles = useObserver(() => roleStore.list);
const { showModal } = useModal();
const isAdmin = account.admin || account.superAdmin;
const editHandler = (user: any = null) => {
userStore.initUser(user).then(() => {
showModal(<UserForm />, { right: true });
});
};
const editHandler = (user: any = null) => {
userStore.initUser(user).then(() => {
showModal(<UserForm />, { right: true });
});
};
useEffect(() => {
if (roles.length === 0 && isEnterprise) {
roleStore.fetchRoles();
}
}, []);
useEffect(() => {
if (roles.length === 0 && isEnterprise) {
roleStore.fetchRoles();
}
}, []);
return (
<div>
<div className="flex items-center justify-between px-5 pt-5">
<PageTitle
title={
<div>
Team <span className="color-gray-medium">{userCount}</span>
</div>
}
/>
<div className="flex items-center">
<AddUserButton isAdmin={isAdmin} onClick={() => editHandler(null)} />
<div className="mx-2" />
<UserSearch />
</div>
return (
<div>
<div className="flex items-center justify-between px-5 pt-5">
<PageTitle
title={
<div>
Team <span className="color-gray-medium">{userCount}</span>
</div>
<UserList isEnterprise={isEnterprise} isOnboarding={isOnboarding} />
}
/>
<div className="flex items-center">
<AddUserButton
btnVariant={isOnboarding ? 'outline' : 'primary'}
isAdmin={isAdmin}
onClick={() => editHandler(null)}
/>
<div className="mx-2" />
{!isOnboarding && <UserSearch />}
</div>
);
</div>
<UserList isEnterprise={isEnterprise} isOnboarding={isOnboarding} />
</div>
);
}
export default connect((state: any) => ({
account: state.getIn(['user', 'account']),
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee',
account: state.getIn(['user', 'account']),
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee',
}))(withPageTitle('Team - OpenReplay Preferences')(UsersView));

View file

@ -6,7 +6,7 @@ import { useObserver } from 'mobx-react-lite';
const PERMISSION_WARNING = 'You dont have the permissions to perform this action.';
const LIMIT_WARNING = 'You have reached users limit.';
function AddUserButton({ isAdmin = false, onClick }: any) {
function AddUserButton({ isAdmin = false, onClick, btnVariant = 'primary' }: any) {
const { userStore } = useStore();
const limtis = useObserver(() => userStore.limits);
const cannAddUser = useObserver(
@ -17,7 +17,7 @@ function AddUserButton({ isAdmin = false, onClick }: any) {
title={`${!isAdmin ? PERMISSION_WARNING : !cannAddUser ? LIMIT_WARNING : 'Add team member'}`}
disabled={isAdmin || cannAddUser}
>
<Button disabled={!cannAddUser || !isAdmin} variant="primary" onClick={onClick}>
<Button disabled={!cannAddUser || !isAdmin} variant={btnVariant} onClick={onClick}>
Add Team Member
</Button>
</Tooltip>

View file

@ -21,20 +21,17 @@ function UserList(props: Props) {
const searchQuery = useObserver(() => userStore.searchQuery);
const { showModal } = useModal();
// const filterList = (list) => {
// const filterRE = getRE(searchQuery, 'i');
// let _list = list.filter((w) => {
// return filterRE.test(w.email) || filterRE.test(w.roleName);
// });
// return _list;
// };
const getList = (list) => filterList(list, searchQuery, ['email', 'roleName', 'name'])
const getList = (list: any) => filterList(list, searchQuery, ['email', 'roleName', 'name'])
const list: any = searchQuery !== '' ? getList(users) : users;
const length = list.length;
useEffect(() => {
userStore.fetchUsers();
return () => {
userStore.updateKey('page', 1)
}
}, []);
const editHandler = (user: any) => {

View file

@ -77,8 +77,8 @@ export default class ErrorInfo extends React.PureComponent {
subtext="Please try to find existing one."
show={!loading && errorIdInStore == null}
>
<div className="flex">
<Loader loading={loading} className="w-9/12">
<div className="flex w-full">
<Loader loading={loading} className="w-full">
<MainSection className="w-9/12" />
<SideSection className="w-3/12" />
</Loader>

View file

@ -5,15 +5,24 @@ import { HighlightCode, Icon, Button } from 'UI';
import DocCard from 'Shared/DocCard/DocCard';
import withOnboarding, { WithOnboardingProps } from '../withOnboarding';
import { OB_TABS } from 'App/routes';
import withPageTitle from 'App/components/hocs/withPageTitle';
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 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">Identify Users</div>
</div>
<a href="https://docs.openreplay.com/en/v1.10.0/installation/identify-user/" target="_blank">
<Button variant="text-primary" icon="question-circle" className="ml-2">
See Documentation
</Button>
</a>
</h1>
<div className="grid grid-cols-6 gap-4 w-full p-4">
<div className="col-span-4">
@ -31,9 +40,24 @@ function IdentifyUsersTab(props: Props) {
</div>
<HighlightCode className="js" text={`tracker.setUserID('john@doe.com');`} />
<div className="border-t my-8" />
</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>
</div>
</div>
<div className="my-8" />
<div className="border-t my-6" />
<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 adding metadata</div>
<p>
@ -48,7 +72,7 @@ function IdentifyUsersTab(props: Props) {
<div className="my-6" />
<div className="flex items-start">
<CircleNumber text="2" />
<div className="pt-1">
<div className="pt-1 w-full">
<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
@ -59,18 +83,7 @@ function IdentifyUsersTab(props: Props) {
</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
@ -96,4 +109,4 @@ function IdentifyUsersTab(props: Props) {
);
}
export default withOnboarding(IdentifyUsersTab);
export default withOnboarding(withPageTitle("Identify Users - OpenReplay")(IdentifyUsersTab));

View file

@ -5,6 +5,7 @@ import { Button, Icon } from 'UI';
import withOnboarding from '../withOnboarding';
import { WithOnboardingProps } from '../withOnboarding';
import { OB_TABS } from 'App/routes';
import withPageTitle from 'App/components/hocs/withPageTitle';
interface Props extends WithOnboardingProps {}
@ -20,9 +21,10 @@ function InstallOpenReplayTab(props: Props) {
<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 href="https://docs.openreplay.com/en/installation/javascript-sdk/" target="_blank">
<Button variant="text-primary" icon="question-circle" className="ml-2">
See Documentation
</Button>
</a>
</h1>
<div className="p-4">
@ -46,4 +48,4 @@ function InstallOpenReplayTab(props: Props) {
);
}
export default withOnboarding(InstallOpenReplayTab);
export default withOnboarding(withPageTitle("Project Setup - OpenReplay")(InstallOpenReplayTab));

View file

@ -2,32 +2,25 @@ import React from 'react';
import { Button } from 'UI';
import Integrations from 'App/components/Client/Integrations/Integrations';
import withOnboarding, { WithOnboardingProps } from '../withOnboarding';
import withPageTitle from 'App/components/hocs/withPageTitle';
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>
<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">Integrations</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> */}
<a href="https://docs.openreplay.com/en/v1.10.0/integrations/" target="_blank">
<Button variant="text-primary" icon="question-circle" className="ml-2">
See Documentation
</Button>
</a>
</h1>
<Integrations hideHeader={true} />
<div className="border-t px-4 py-3 flex justify-end">
<Button variant="primary" className="" onClick={() => (props.skip ? props.skip() : null)}>
Complete Setup
@ -37,4 +30,4 @@ function IntegrationsTab(props: Props) {
);
}
export default withOnboarding(IntegrationsTab);
export default withOnboarding(withPageTitle("Integrations - OpenReplay")(IntegrationsTab));

View file

@ -4,15 +4,27 @@ import React from 'react';
import { Button, Icon } from 'UI';
import withOnboarding, { WithOnboardingProps } from '../withOnboarding';
import { OB_TABS } from 'App/routes';
import withPageTitle from 'App/components/hocs/withPageTitle';
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 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">Invite Collaborators</div>
</div>
<a
href="https://docs.openreplay.com/en/tutorials/adding-users/"
target="_blank"
>
<Button variant="text-primary" icon="question-circle" className="ml-2">
See Documentation
</Button>
</a>
</h1>
<div className="grid grid-cols-6 gap-4 p-4">
<div className="col-span-4">
@ -50,4 +62,4 @@ function ManageUsersTab(props: Props) {
);
}
export default withOnboarding(ManageUsersTab);
export default withOnboarding(withPageTitle('Invite Collaborators - OpenReplay')(ManageUsersTab));

View file

@ -1,20 +1,20 @@
import React, { useState } from 'react'
import { connect } from 'react-redux'
import stl from './installDocs.module.css'
import cn from 'classnames'
import Highlight from 'react-highlight'
import CircleNumber from '../../CircleNumber'
import { CopyButton } from 'UI'
import React, { useState } from 'react';
import { connect } from 'react-redux';
import stl from './installDocs.module.css';
import cn from 'classnames';
import Highlight from 'react-highlight';
import CircleNumber from '../../CircleNumber';
import { CopyButton } from 'UI';
import { Toggler } from 'UI';
const installationCommand = 'npm i @openreplay/tracker'
const installationCommand = 'npm i @openreplay/tracker';
const usageCode = `import Tracker from '@openreplay/tracker';
const tracker = new Tracker({
projectKey: "PROJECT_KEY",
ingestPoint: "https://${window.location.hostname}/ingest",
});
tracker.start();`
tracker.start();`;
const usageCodeSST = `import Tracker from '@openreplay/tracker/cjs';
const tracker = new Tracker({
@ -28,12 +28,12 @@ function MyApp() {
}, []);
//...
}`
}`;
function InstallDocs({ site }) {
const _usageCode = usageCode.replace('PROJECT_KEY', site.projectKey)
const _usageCodeSST = usageCodeSST.replace('PROJECT_KEY', site.projectKey)
const [isSpa, setIsSpa] = useState(true)
const _usageCode = usageCode.replace('PROJECT_KEY', site.projectKey);
const _usageCodeSST = usageCodeSST.replace('PROJECT_KEY', site.projectKey);
const [isSpa, setIsSpa] = useState(true);
return (
<div>
<div className="mb-8">
@ -41,61 +41,69 @@ function InstallDocs({ site }) {
<CircleNumber text="1" />
Install the npm package.
</div>
<div className={ cn(stl.snippetWrapper, 'ml-10') }>
<CopyButton content={installationCommand} className={cn(stl.codeCopy, 'mt-2 mr-2')} />
<Highlight className="cli">
{installationCommand}
</Highlight>
<div className={cn(stl.snippetWrapper, 'ml-10')}>
<div className="absolute mt-1 mr-2 right-0">
<CopyButton content={installationCommand} />
</div>
<Highlight className="cli">{installationCommand}</Highlight>
</div>
</div>
<div>
<div className="font-semibold mb-2 flex items-center">
<CircleNumber text="2" />
Continue with one of the following options.
Continue with one of the following options.
</div>
<div className="flex items-center ml-10 cursor-pointer">
<div className="mr-2" onClick={() => setIsSpa(!isSpa)}>Server-Side-Rendered (SSR)?</div>
<div className="mr-2" onClick={() => setIsSpa(!isSpa)}>
Server-Side-Rendered (SSR)?
</div>
<Toggler
checked={!isSpa}
name="sessionsLive"
onChange={ () => setIsSpa(!isSpa) }
onChange={() => setIsSpa(!isSpa)}
// style={{ lineHeight: '23px' }}
/>
</div>
<div className="flex ml-10 mt-4">
<div className="w-full">
{isSpa && (
<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')} />
<Highlight className="js">
{_usageCode}
</Highlight>
<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)}>
<div className="absolute mt-1 mr-2 right-0">
<CopyButton content={_usageCode} />
</div>
<Highlight className="js">{_usageCode}</Highlight>
</div>
</div>
)}
{!isSpa && (
<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')} />
<Highlight className="js">
{_usageCodeSST}
</Highlight>
<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)}>
<div className="absolute mt-1 mr-2 right-0">
<CopyButton content={_usageCodeSST} />
</div>
<Highlight className="js">{_usageCodeSST}</Highlight>
</div>
</div>
)}
</div>
</div>
</div>
</div>
</div>
)
);
}
export default connect(state => ({
site: state.getIn([ 'site', 'instance' ]),
}))(InstallDocs)
export default connect((state) => ({
site: state.getIn(['site', 'instance']),
}))(InstallDocs);

View file

@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { editGDPR, saveGDPR, init } from 'Duck/site';
import { Checkbox, Toggler } from 'UI';
import { Checkbox, Loader, Toggler } from 'UI';
import GDPR from 'Types/site/gdpr';
import cn from 'classnames';
import stl from './projectCodeSnippet.module.css';
@ -23,6 +23,7 @@ const ProjectCodeSnippet = (props) => {
const { gdpr } = props.site;
const [changed, setChanged] = useState(false);
const [isAssistEnabled, setAssistEnabled] = useState(false);
const [showLoader, setShowLoader] = useState(false);
useEffect(() => {
const site = props.sites.find((s) => s.id === props.siteId);
@ -50,6 +51,14 @@ const ProjectCodeSnippet = (props) => {
saveGDPR(_gdpr);
};
useEffect(() => {
// show loader for 500 milliseconds
setShowLoader(true);
setTimeout(() => {
setShowLoader(false);
}, 200);
}, [isAssistEnabled]);
return (
<div>
<div className="mb-4">
@ -130,15 +139,21 @@ const ProjectCodeSnippet = (props) => {
<span>{' tag of your page.'}</span>
</div>
<div className={cn(stl.snippetsWrapper, 'ml-10')}>
<CodeSnippet
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}
/>
{showLoader ? (
<div style={{ height: '474px' }}>
<Loader loading={true} />
</div>
) : (
<CodeSnippet
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}
/>
)}
</div>
</div>
);

View file

@ -17,7 +17,7 @@ const ProjectFormButton = ({ sites, siteId, init }) => {
return (
<>
<span
className="text-2xl font-bold ml-2 color-teal underline-dashed cursor-pointer"
className="text-2xl font-bold ml-2 color-teal underline decoration-dotted cursor-pointer"
onClick={(e) => openModal(e)}
>
{site && site.name}

View file

@ -21,7 +21,7 @@ function SideMenu(props: Props) {
</div>
<SideMenuitem
title="Install OpenReplay"
title="Setup OpenReplay"
iconName="tools"
active={activeTab === OB_TABS.INSTALLING}
onClick={() => props.onClick(OB_TABS.INSTALLING)}

View file

@ -77,7 +77,7 @@ function UserCard({ className, request, session, width, height, similarSessions,
<span className="mx-1 font-bold text-xl">&#183;</span>
<Popover
render={() => (
<div className="text-left bg-white">
<div className="text-left bg-white rounded">
<SessionInfoItem
comp={<CountryFlag country={userCountry} height={11} />}
label={countries[userCountry]}

View file

@ -83,7 +83,7 @@ function PlayerBlockHeader(props: any) {
onClick={backHandler}
>
{/* @ts-ignore TODO */}
<BackLink label="Back" className="h-full" />
<BackLink label="Back" className="h-full ml-2" />
<div className={stl.divider} />
</div>
)}

View file

@ -1,7 +1,6 @@
.header {
height: 50px;
border-bottom: solid thin $gray-light;
padding-left: 15px;
padding-right: 0;
background-color: white;
}

View file

@ -163,7 +163,7 @@ function Controls(props: any) {
disabled={disabled}
backTenSeconds={backTenSeconds}
forthTenSeconds={forthTenSeconds}
toggleSpeed={() => player.toggleSpeed()}
toggleSpeed={(speedIndex) => player.toggleSpeed(speedIndex)}
toggleSkip={() => player.toggleSkip()}
playButton={<PlayButton state={state} togglePlay={player.togglePlay} iconSize={36} />}
skipIntervals={SKIP_INTERVALS}

View file

@ -4,7 +4,8 @@ import cn from 'classnames';
import { ReduxTime } from '../Time';
// @ts-ignore
import styles from '../controls.module.css';
import { SkipButton } from 'App/player-ui'
import { SkipButton } from 'App/player-ui';
import { SPEED_OPTIONS } from 'App/player/player/Player';
interface Props {
skip: boolean;
@ -16,7 +17,7 @@ interface Props {
setSkipInterval: (interval: number) => void;
backTenSeconds: () => void;
forthTenSeconds: () => void;
toggleSpeed: () => void;
toggleSpeed: (speedIndex: number) => void;
toggleSkip: () => void;
}
@ -79,7 +80,6 @@ function PlayerControls(props: Props) {
<ReduxTime isCustom name="endTime" format="mm:ss" />
</div>
<div className="rounded ml-4 bg-active-blue border border-active-blue-border flex items-stretch">
{/* @ts-ignore */}
<Tooltip
@ -87,10 +87,7 @@ function PlayerControls(props: Props) {
title={`Rewind ${currentInterval}s`}
placement="top"
>
<button
ref={arrowBackRef}
className="h-full bg-transparent"
>
<button ref={arrowBackRef} className="h-full bg-transparent">
<SkipButton
size={18}
onClick={backTenSeconds}
@ -146,10 +143,7 @@ function PlayerControls(props: Props) {
title={`Rewind ${currentInterval}s`}
placement="top"
>
<button
ref={arrowForwardRef}
className="h-full bg-transparent"
>
<button ref={arrowForwardRef} className="h-full bg-transparent">
<SkipButton
size={18}
onClick={forthTenSeconds}
@ -159,34 +153,65 @@ function PlayerControls(props: Props) {
</Tooltip>
</div>
<div className="flex items-center">
<div className="mx-2" />
{/* @ts-ignore */}
<Tooltip title="Control play back speed (↑↓)" placement="top">
<button
ref={speedRef}
className={cn(styles.speedButton, 'focus:border focus:border-blue')}
onClick={toggleSpeed}
data-disabled={disabled}
>
<div>{speed + 'x'}</div>
</button>
</Tooltip>
<div className="mx-2" />
<button
className={cn(styles.skipIntervalButton, {
[styles.withCheckIcon]: skip,
[styles.active]: skip,
})}
onClick={toggleSkip}
data-disabled={disabled}
>
{skip && <Icon name="check" size="24" className="mr-1" />}
{'Skip Inactivity'}
</button>
</div>
<div className="flex items-center">
<div className="mx-2" />
{/* @ts-ignore */}
<Popover
// @ts-ignore
theme="nopadding"
animation="none"
duration={0}
className="cursor-pointer select-none"
distance={20}
render={({ close }: any) => (
<div className="flex flex-col bg-white border border-borderColor-gray-light-shade text-figmaColors-text-primary rounded">
<div className="font-semibold py-2 px-4 w-full text-left">
Playback speed
</div>
{Object.keys(SPEED_OPTIONS).map((index: any) => (
<div
key={SPEED_OPTIONS[index]}
onClick={() => {
close();
toggleSpeed(index);
}}
className={cn(
'py-2 px-4 cursor-pointer w-full text-left font-semibold',
'hover:bg-active-blue border-t border-borderColor-gray-light-shade'
)}
>
{SPEED_OPTIONS[index]}
<span className="text-disabled-text">x</span>
</div>
))}
</div>
)}
>
<div onClick={toggleTooltip} ref={skipRef} className="cursor-pointer select-none">
<Tooltip disabled={showTooltip} title="Set default skip duration">
<button
ref={speedRef}
className={cn(styles.speedButton, 'focus:border focus:border-blue')}
data-disabled={disabled}
>
<div>{speed + 'x'}</div>
</button>
</Tooltip>
</div>
</Popover>
<div className="mx-2" />
<button
className={cn(styles.skipIntervalButton, {
[styles.withCheckIcon]: skip,
[styles.active]: skip,
})}
onClick={toggleSkip}
data-disabled={disabled}
>
{skip && <Icon name="check" size="24" className="mr-1" />}
{'Skip Inactivity'}
</button>
</div>
</div>
);
}

View file

@ -6,6 +6,7 @@ import { Button, Link, Icon } from 'UI';
import { session as sessionRoute, withSiteId } from 'App/routes';
import stl from './AutoplayTimer.module.css';
import clsOv from './overlay.module.css';
import AutoplayToggle from 'Shared/AutoplayToggle';
interface IProps extends RouteComponentProps {
nextId: number;
@ -40,20 +41,28 @@ function AutoplayTimer({ nextId, siteId, history }: IProps) {
return (
<div className={cn(clsOv.overlay, stl.overlayBg)}>
<div className="border p-6 shadow-lg bg-white rounded">
<div className="py-4">Next recording will be played in {counter}s</div>
<div className="flex items-center">
<Button primary="outline" onClick={cancel}>
Cancel
</Button>
<div className="px-3" />
<Link to={sessionRoute(nextId)} disabled={!nextId}>
<Button variant="primary">Play Now</Button>
</Link>
<div className="border p-5 shadow-lg bg-white rounded">
<div className="mb-5">
Autoplaying next session in <span className="font-medium">{counter}</span> seconds
</div>
<div className="mt-2 flex items-center color-gray-dark">
<div className="flex items-center justify-between">
<div className="mr-10">
<AutoplayToggle />
</div>
<div className="flex items-center">
<Button variant="text-primary" onClick={cancel}>
Cancel
</Button>
<div className="px-2" />
<Link to={sessionRoute(nextId)} disabled={!nextId}>
<Button variant="outline">Play Now</Button>
</Link>
</div>
</div>
{/* <div className="mt-2 flex items-center color-gray-dark">
Turn on/off auto-replay in <Icon name="ellipsis-v" className="mx-1" /> More options
</div>
</div> */}
</div>
</div>
);

View file

@ -107,7 +107,7 @@ function SubHeader(props) {
{location && (
<>
<div
className="flex items-center cursor-pointer color-gray-medium text-sm p-1 hover:bg-gray-light-shade rounded-md"
className="flex items-center cursor-pointer color-gray-medium text-sm p-1 hover:bg-active-blue rounded-md"
onClick={() => {
copy(currentLocation);
setCopied(true);

View file

@ -1,5 +1,5 @@
import React from 'react';
import { Button } from 'UI';
import { Button, Icon } from 'UI';
import styles from './menu.module.css';
import cn from 'classnames';
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
@ -42,8 +42,11 @@ export default class ItemMenu extends React.PureComponent<Props> {
return (
<div className={styles.wrapper}>
<OutsideClickDetectingDiv onClickOutside={this.closeMenu}>
<Button variant="text" icon="ellipsis-v" onClick={this.toggleMenu}>
More
<Button variant="text" onClick={this.toggleMenu}>
<div className="flex items-center">
<Icon name="ellipsis-v" size={18} className="mr-1" />
<span>More</span>
</div>
</Button>
<div className={cn(styles.menu, styles.menuDim)} data-displayed={displayed}>
{items.map((item) =>

View file

@ -2,7 +2,6 @@ import React from 'react';
import { Button } from 'UI';
import { connect } from 'react-redux';
import { setCreateNoteTooltip } from 'Duck/sessions';
import GuidePopup from 'Shared/GuidePopup';
import { PlayerContext } from 'App/components/Session/playerContext';
function NotePopup({
@ -12,7 +11,7 @@ function NotePopup({
setCreateNoteTooltip: (args: any) => void;
tooltipActive: boolean;
}) {
const { player, store } = React.useContext(PlayerContext)
const { player, store } = React.useContext(PlayerContext);
const toggleNotePopup = () => {
if (tooltipActive) return;
@ -25,14 +24,9 @@ function NotePopup({
}, []);
return (
<GuidePopup
title="Introducing Notes"
description={'Annotate session replays and share your feedback with the rest of your team.'}
>
<Button icon="quotes" variant="text" disabled={tooltipActive} onClick={toggleNotePopup}>
Add Note
</Button>
</GuidePopup>
<Button icon="quotes" variant="text" disabled={tooltipActive} onClick={toggleNotePopup}>
Add Note
</Button>
);
}

View file

@ -39,7 +39,7 @@
}
white-space: nowrap;
z-index: 20;
z-index: 9999;
position: absolute;
right: 0px;
top: 37px;

View file

@ -3,20 +3,16 @@ import { Toggler } from 'UI';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
interface Props {
toggleAutoplay: () => void;
autoplay: boolean;
}
function AutoplayToggle(props: Props) {
function AutoplayToggle() {
const { player, store } = React.useContext(PlayerContext)
const { autoplay } = store.get()
return (
<div
onClick={() => player.toggleAutoplay()}
className="cursor-pointer flex items-center mr-2 hover:bg-gray-light-shade rounded-md p-2"
>
<Toggler name="sessionsLive" onChange={props.toggleAutoplay} checked={autoplay} />
<Toggler name="sessionsLive" onChange={() => player.toggleAutoplay()} checked={autoplay} />
<span className="ml-2 whitespace-nowrap">Auto-Play</span>
</div>
);

View file

@ -6,47 +6,78 @@ import { Timezone } from 'App/mstore/types/sessionSettings';
import { useObserver } from 'mobx-react-lite';
import { toast } from 'react-toastify';
type TimezonesDropdown = Timezone[]
type TimezonesDropdown = Timezone[];
function DefaultTimezone() {
const [changed, setChanged] = React.useState(false);
const { settingsStore } = useStore();
const timezoneOptions: TimezonesDropdown = settingsStore.sessionSettings.defaultTimezones;
const [timezone, setTimezone] = React.useState(settingsStore.sessionSettings.timezone);
const sessionSettings = useObserver(() => settingsStore.sessionSettings);
const [changed, setChanged] = React.useState(false);
const { settingsStore } = useStore();
const timezoneOptions: TimezonesDropdown = settingsStore.sessionSettings.defaultTimezones;
const [timezone, setTimezone] = React.useState(settingsStore.sessionSettings.timezone);
const sessionSettings = useObserver(() => settingsStore.sessionSettings);
useEffect(() => {
if (!timezone) setTimezone({ label: 'Local Timezone', value: 'system' });
}, []);
useEffect(() => {
if (!timezone) setTimezone({ label: 'Local Timezone', value: 'system' });
}, []);
const onSelectChange = ({ value }: { value: Timezone }) => {
setTimezone(value);
setChanged(true);
}
const onTimezoneSave = () => {
setChanged(false);
sessionSettings.updateKey('timezone', timezone);
toast.success("Default timezone saved successfully");
}
return (
<>
<h3 className="text-lg">Default Timezone</h3>
<div className="my-1">Session Time</div>
<div className="mt-2 flex items-center" style={{ width: "265px" }}>
<Select
options={timezoneOptions}
defaultValue={timezone.value}
className="w-full"
onChange={onSelectChange}
/>
<div className="col-span-3 ml-3">
<Button disabled={!changed} variant="outline" size="medium" onClick={onTimezoneSave}>Update</Button>
</div>
</div>
<div className="text-sm mt-3">This change will impact the timestamp on session card and player.</div>
</>
const getCurrentTimezone = () => {
const currentTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const timezoneOffset = new Date().getTimezoneOffset() / -60;
const timezoneValue = `UTC${
(timezoneOffset >= 0 ? '+' : '-') + timezoneOffset.toString().padStart(2, '0')
}`;
const selectedTimezone = timezoneOptions.find(
(option) => option.label.includes(currentTimezone) || option.value === timezoneValue
);
return selectedTimezone ? selectedTimezone : null;
};
const setCurrentTimezone = () => {
const selectedTimezone = getCurrentTimezone();
console.log('selectedTimezone', selectedTimezone);
if (selectedTimezone) {
setTimezone(selectedTimezone);
sessionSettings.updateKey('timezone', selectedTimezone);
toast.success('Default timezone saved successfully');
}
};
const onSelectChange = ({ value }: { value: Timezone }) => {
setTimezone(value);
setChanged(true);
};
const onTimezoneSave = () => {
setChanged(false);
sessionSettings.updateKey('timezone', timezone);
toast.success('Default timezone saved successfully');
};
return (
<>
<h3 className="text-lg">Default Timezone</h3>
<div className="my-1">
Set the timezone for this project. All Sessions, Charts will be referenced to this.
</div>
<div className="mt-2 flex items-center" style={{ width: '265px' }}>
<Select
options={timezoneOptions}
defaultValue={timezone.value}
className="w-full"
value={timezoneOptions.find((option) => option.value === timezone.value)}
onChange={onSelectChange}
/>
<div className="col-span-3 ml-3">
<Button disabled={!changed} variant="outline" size="medium" onClick={onTimezoneSave}>
Update
</Button>
</div>
</div>
<div onClick={setCurrentTimezone} className="mt-3 link">
Apply my current timezone
</div>
</>
);
}
export default DefaultTimezone;

View file

@ -78,10 +78,7 @@ export default class SharePopup extends React.PureComponent {
};
handleSuccess = (endpoint) => {
const obj =
endpoint === 'Slack'
? { loadingSlack: false }
: { loadingTeams: false };
const obj = endpoint === 'Slack' ? { loadingSlack: false } : { loadingTeams: false };
this.setState(obj);
toast.success(`Sent to ${endpoint}.`);
};
@ -114,7 +111,7 @@ export default class SharePopup extends React.PureComponent {
<div className={styles.wrapper}>
{this.state.loadingTeams || this.state.loadingSlack ? (
<Loader loading />
) :(
) : (
<>
<div className={styles.header}>
<div className={cn(styles.title, 'text-lg')}>
@ -139,24 +136,21 @@ export default class SharePopup extends React.PureComponent {
{slackOptions.length > 0 && (
<>
<span>Share to slack</span>
<div className="flex items-center justify-between mb-2">
<div className="grid grid-cols-6 gap-4">
<Select
options={slackOptions}
defaultValue={channelId}
onChange={this.changeSlackChannel}
className="mr-4"
className="col-span-4"
/>
{this.state.channelId && (
<Button onClick={this.shareToSlack} variant="primary">
<div className="flex items-center">
<Icon
name="integrations/slack-bw"
color="white"
size="18"
marginRight="10"
/>
{loadingSlack ? 'Sending...' : 'Send'}
</div>
<Button
onClick={this.shareToSlack}
icon="integrations/slack-bw"
variant="outline"
className="col-span-2"
>
{loadingSlack ? 'Sending...' : 'Send'}
</Button>
)}
</div>
@ -164,25 +158,22 @@ export default class SharePopup extends React.PureComponent {
)}
{msTeamsOptions.length > 0 && (
<>
<span>Share to MS Teams</span>
<div className="flex items-center justify-between">
<div className="mt-4">Share to MS Teams</div>
<div className="grid grid-cols-6 gap-4">
<Select
options={msTeamsOptions}
defaultValue={teamsChannel}
onChange={this.changeTeamsChannel}
className="mr-4"
className="col-span-4"
/>
{this.state.teamsChannel && (
<Button onClick={this.shareToMSTeams} variant="primary">
<div className="flex items-center">
<Icon
name="integrations/teams-white"
color="white"
size="18"
marginRight="10"
/>
{loadingTeams ? 'Sending...' : 'Send'}
</div>
<Button
onClick={this.shareToMSTeams}
icon="integrations/teams-white"
variant="outline"
className="col-span-2"
>
{loadingTeams ? 'Sending...' : 'Send'}
</Button>
)}
</div>

View file

@ -46,7 +46,7 @@ function UserSessionsModal(props: Props) {
useEffect(fetchData, [filter.page, filter.startDate, filter.endDate]);
return (
<div className="h-screen overflow-y-auto bg-white">
<div className="bg-white pb-6 h-screen">
<div className="flex items-center justify-between w-full px-5 py-3">
<div className="text-lg flex items-center">
<Avatar isActive={false} seed={hash} isAssist={false} className={''} />
@ -66,7 +66,7 @@ function UserSessionsModal(props: Props) {
<div className="text-center text-gray-600">No recordings found.</div>
</div>
}>
<div className="border rounded m-5">
<div className="border rounded m-5 overflow-y-auto" style={{ maxHeight: 'calc(100vh - 85px)'}}>
<Loader loading={loading}>
{data.sessions.map((session: any) => (
<div className="border-b last:border-none" key={session.sessionId}>

View file

@ -17,7 +17,7 @@ function CopyButton({ content, variant="text-primary", className = '', btnText
return (
<Button
variant={variant}
className={ className }
className={ className + ' capitalize' }
onClick={ copyHandler }
>
{ copied ? 'copied' : btnText }

View file

@ -1,13 +1,14 @@
import React from 'react'
import Highlight from 'react-highlight'
import stl from './highlightCode.module.css'
import cn from 'classnames'
import { CopyButton } from 'UI'
function HighlightCode({ className = 'js', text = ''}) {
return (
<div className={stl.snippetWrapper}>
<CopyButton content={text} className={cn(stl.codeCopy, 'mt-2 mr-2')} />
<div className="absolute mt-1 mr-2 right-0">
<CopyButton content={text} />
</div>
<Highlight className={className}>
{text}
</Highlight>

View file

@ -2,24 +2,6 @@
.snippetWrapper {
position: relative;
& .codeCopy {
position: absolute;
right: 0px;
top: -3px;
z-index: $codeSnippet;
padding: 5px 10px;
color: $teal;
text-transform: uppercase;
cursor: pointer;
border-radius: 3px;
transition: all 0.4s;
user-select: none;
&:hover {
background-color: $gray-light;
transition: all 0.2s;
}
}
& .snippet {
overflow: hidden;
line-height: 20px;

View file

@ -3,6 +3,7 @@ import * as typedLocalStorage from './localStorage';
import type { Moveable, Cleanable, Store } from '../common/types';
import Animator from './Animator';
import type { GetState as AnimatorGetState } from './Animator';
export const SPEED_OPTIONS = [0.5, 1, 2, 4, 8, 16]
/* == separate this == */
@ -13,7 +14,7 @@ const SKIP_TO_ISSUE_STORAGE_KEY = "__$session-skipToIssue$__"
const AUTOPLAY_STORAGE_KEY = "__$player-autoplay$__"
const SHOW_EVENTS_STORAGE_KEY = "__$player-show-events$__"
const storedSpeed: number = typedLocalStorage.number(SPEED_STORAGE_KEY)
const initialSpeed = [0.5, 1, 2, 4, 8, 16].includes(storedSpeed) ? storedSpeed : 1
const initialSpeed = SPEED_OPTIONS.includes(storedSpeed) ? storedSpeed : 1
const initialSkip = typedLocalStorage.boolean(SKIP_STORAGE_KEY)
const initialSkipToIssue = typedLocalStorage.boolean(SKIP_TO_ISSUE_STORAGE_KEY)
const initialAutoplay = typedLocalStorage.boolean(AUTOPLAY_STORAGE_KEY)
@ -89,9 +90,10 @@ export default class Player extends Animator {
this.pState.update({ speed })
}
toggleSpeed() {
const { speed } = this.pState.get()
this.updateSpeed(speed < HIGHEST_SPEED ? speed * 2 : 0.5)
toggleSpeed(index: number | null) {
const { speed } = this.pState.get();
const newSpeedIndex = index === null ? null : Math.max(0, Math.min(SPEED_OPTIONS.length - 1, index));
this.updateSpeed(newSpeedIndex === null ? speed * 2 : SPEED_OPTIONS[newSpeedIndex]);
}
speedUp() {

View file

@ -263,7 +263,7 @@
}
.hljs {
padding: 10px !important;
padding: 12px !important;
border-radius: 6px !important;
background-color: $gray-lightest !important;
font-size: 12px !important;

View file

@ -62,7 +62,7 @@
}
}
.side-menu-margined {
margin-left: 220px;
margin-left: 250px;
}
.top-header {