change(ui) - popup replacement

This commit is contained in:
Shekar Siri 2022-11-14 18:14:13 +01:00
parent f890015e55
commit c73cb9e60a
76 changed files with 2088 additions and 2373 deletions

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { Popup, Button, Tooltip } from 'UI';
import { Button, Tooltip } from 'UI';
import { connect } from 'react-redux';
import cn from 'classnames';
import { toggleChatWindow } from 'Duck/sessions';
@ -195,8 +195,8 @@ function AssistActions({
</Tooltip>
<div className={stl.divider} />
<Popup
content={
<Tooltip
title={
cannotCall
? `You don't have the permissions to perform this action.`
: `Call ${userId ? userId : 'User'}`
@ -218,7 +218,7 @@ function AssistActions({
{onCall ? 'End' : isPrestart ? 'Join Call' : 'Call'}
</Button>
</div>
</Popup>
</Tooltip>
<div className="fixed ml-3 left-0 top-0" style={{ zIndex: 999 }}>
{onCall && callObject && (

View file

@ -1,40 +0,0 @@
import React from 'react';
import { Popup } from 'UI';
export default class Tooltip extends React.PureComponent {
state = {
open: false,
}
mouseOver = false
onMouseEnter = () => {
this.mouseOver = true;
setTimeout(() => {
if (this.mouseOver) this.setState({ open: true });
}, 1000)
}
onMouseLeave = () => {
this.mouseOver = false;
this.setState({
open: false,
});
}
render() {
const { trigger, tooltip } = this.props;
const { open } = this.state;
return (
<Popup
open={ open }
content={ tooltip }
inverted
>
<span
onMouseEnter={ this.onMouseEnter }
onMouseLeave={ this.onMouseLeave }
>
{ trigger }
</span>
</Popup>
);
}
}

View file

@ -1,7 +1,7 @@
import React from 'react'
import { connect } from 'react-redux';
import cn from 'classnames';
import { SideMenuitem, Popup } from 'UI'
import { SideMenuitem, Tooltip } from 'UI'
import stl from './sessionMenu.module.css';
import { clearEvents } from 'Duck/filters';
import { issues_types } from 'Types/session/issue'
@ -24,12 +24,11 @@ function SessionsMenu(props) {
<span>Sessions</span>
</div>
<span className={ cn(stl.manageButton, 'mr-2') } onClick={() => showModal(<SessionSettings />, { right: true })}>
<Popup
hideOnClick={true}
content={<span>Configure the percentage of sessions <br /> to be captured, timezone and more.</span>}
<Tooltip
title={<span>Configure the percentage of sessions <br /> to be captured, timezone and more.</span>}
>
Settings
</Popup>
</Tooltip>
</span>
</div>

View file

@ -1,273 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import cn from 'classnames';
import withPageTitle from 'HOCs/withPageTitle';
import {
Form, IconButton, SlideModal, Input, Button, Loader,
NoContent, Popup, CopyButton } from 'UI';
import Select from 'Shared/Select';
import { init, save, edit, remove as deleteMember, fetchList, generateInviteLink } from 'Duck/member';
import { fetchList as fetchRoles } from 'Duck/roles';
import styles from './manageUsers.module.css';
import UserItem from './UserItem';
import { confirm } from 'UI';
import { toast } from 'react-toastify';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
const PERMISSION_WARNING = 'You dont have the permissions to perform this action.';
const LIMIT_WARNING = 'You have reached users limit.';
@connect(state => ({
account: state.getIn([ 'user', 'account' ]),
members: state.getIn([ 'members', 'list' ]).filter(u => u.id),
member: state.getIn([ 'members', 'instance' ]),
errors: state.getIn([ 'members', 'saveRequest', 'errors' ]),
loading: state.getIn([ 'members', 'loading' ]),
saving: state.getIn([ 'members', 'saveRequest', 'loading' ]),
roles: state.getIn(['roles', 'list']).filter(r => !r.protected).map(r => ({ label: r.name, value: r.roleId })).toJS(),
isEnterprise: state.getIn([ 'user', 'account', 'edition' ]) === 'ee',
}), {
init,
save,
edit,
deleteMember,
fetchList,
generateInviteLink,
fetchRoles
})
@withPageTitle('Team - OpenReplay Preferences')
class ManageUsers extends React.PureComponent {
state = { showModal: false, remaining: this.props.account.limits.teamMember.remaining, invited: false }
// writeOption = (e, { name, value }) => this.props.edit({ [ name ]: value });
onChange = ({ name, value }) => this.props.edit({ [ name ]: value.value });
onChangeCheckbox = ({ target: { checked, name } }) => this.props.edit({ [ name ]: checked });
setFocus = () => this.focusElement && this.focusElement.focus();
closeModal = () => this.setState({ showModal: false });
componentWillMount = () => {
this.props.fetchList();
if (this.props.isEnterprise) {
this.props.fetchRoles();
}
}
adminLabel = (user) => {
if (user.superAdmin) return null;
return user.admin ? 'Admin' : '';
};
editHandler = user => {
this.init(user)
}
deleteHandler = async (user) => {
if (await confirm({
header: 'Users',
confirmation: `Are you sure you want to remove this user?`
})) {
this.props.deleteMember(user.id).then(() => {
const { remaining } = this.state;
if (remaining <= 0) return;
this.setState({ remaining: remaining - 1 })
});
}
}
save = (e) => {
e.preventDefault();
this.props.save(this.props.member)
.then(() => {
const { errors } = this.props;
if (errors && errors.size > 0) {
errors.forEach(e => {
toast.error(e);
})
}
this.setState({ invited: true })
// this.closeModal()
});
}
formContent = () => {
const { member, account, isEnterprise, roles } = this.props;
return (
<div className={ styles.form }>
<Form onSubmit={ this.save } >
<div className={ styles.formGroup }>
<label>{ 'Full Name' }</label>
<Input
ref={ (ref) => { this.focusElement = ref; } }
name="name"
value={ member.name }
onChange={ this.onChange }
className={ styles.input }
id="name-field"
/>
</div>
<Form.Field>
<label>{ 'Email Address' }</label>
<Input
disabled={member.exists()}
name="email"
value={ member.email }
onChange={ this.onChange }
className={ styles.input }
/>
</Form.Field>
{ !account.smtp &&
<div className={cn("mb-4 p-2", styles.smtpMessage)}>
SMTP is not configured (see <a className="link" href="https://docs.openreplay.com/configuration/configure-smtp" target="_blank">here</a> how to set it up). You can still add new users, but youd have to manually copy then send them the invitation link.
</div>
}
<Form.Field>
<label className={ styles.checkbox }>
<input
name="admin"
type="checkbox"
value={ member.admin }
checked={ !!member.admin }
onChange={ this.onChangeCheckbox }
disabled={member.superAdmin}
/>
<span>{ 'Admin Privileges' }</span>
</label>
<div className={ styles.adminInfo }>{ 'Can manage Projects and team members.' }</div>
</Form.Field>
{ isEnterprise && (
<Form.Field>
<label htmlFor="role">{ 'Role' }</label>
<Select
placeholder="Role"
selection
options={ roles }
name="roleId"
value={ roles.find(r => r.value === member.roleId) }
onChange={ this.onChange }
/>
</Form.Field>
)}
</Form>
<div className="flex items-center">
<div className="flex items-center mr-auto">
<Button
onClick={ this.save }
disabled={ !member.validate() }
loading={ this.props.saving }
variant="primary"
className="float-left mr-2"
>
{ member.exists() ? 'Update' : 'Invite' }
</Button>
{member.exists() && (
<Button
onClick={ this.closeModal }
>
{ 'Cancel' }
</Button>
)}
</div>
{ !member.joined && member.invitationLink &&
<CopyButton
content={member.invitationLink}
className="link"
btnText="Copy invite link"
/>
}
</div>
</div>
)
}
init = (v) => {
const { roles } = this.props;
this.props.init(v ? v : { roleId: roles[0] ? roles[0].value : null });
this.setState({ showModal: true });
setTimeout(this.setFocus, 100);
}
render() {
const {
members, loading, account, hideHeader = false
} = this.props;
const { showModal, remaining, invited } = this.state;
const isAdmin = account.admin || account.superAdmin;
const canAddUsers = isAdmin && remaining !== 0;
return (
<React.Fragment>
<Loader loading={ loading }>
<SlideModal
title="Invite People"
size="small"
isDisplayed={ showModal }
content={ showModal && this.formContent() }
onClose={ this.closeModal }
/>
<div className={ styles.wrapper }>
<div className={ cn(styles.tabHeader, 'flex items-center') }>
<div className="flex items-center mr-auto">
{ !hideHeader && <h3 className={ cn(styles.tabTitle, "text-2xl") }>{ (isAdmin ? 'Manage ' : '') + `Users (${members.size})` }</h3> }
{ hideHeader && <h3 className={ cn(styles.tabTitle, "text-xl") }>{ `Users (${members.size})` }</h3>}
<Popup
disabled={ canAddUsers }
content={ `${ !canAddUsers ? (!isAdmin ? PERMISSION_WARNING : LIMIT_WARNING) : 'Add team member' }` }
>
<div>
<IconButton
id="add-button"
disabled={ !canAddUsers }
circle
icon="plus"
outline
onClick={ () => this.init() }
/>
</div>
</Popup>
</div>
</div>
<NoContent
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.EMPTY_STATE} size="170" />
<div className="mt-6 text-2xl">No data available.</div>
</div>
}
size="small"
show={ members.size === 0 }
// animatedIcon="empty-state"
>
<div className={ styles.list }>
{
members.map(user => (
<UserItem
generateInviteLink={this.props.generateInviteLink}
key={ user.id }
user={ user }
adminLabel={ this.adminLabel(user) }
deleteHandler={ isAdmin && account.email !== user.email
? this.deleteHandler
: null
}
editHandler={ isAdmin ? this.editHandler : null }
/>
))
}
{ !members.size > 0 &&
<div>{ 'No Data.' }</div>
}
</div>
</NoContent>
</div>
</Loader>
</React.Fragment>
);
}
}
export default ManageUsers;

View file

@ -1,49 +0,0 @@
import React from 'react';
import { Icon, CopyButton, Popup } from 'UI';
import styles from './userItem.module.css';
const UserItem = ({ user, adminLabel, deleteHandler, editHandler, generateInviteLink }) => (
<div className={ styles.wrapper } id="user-row">
<Icon name="user-alt" size="16" marginRight="10" />
<div id="user-name">{ user.name || user.email }</div>
<div className="px-2"/>
{ adminLabel && <div className={ styles.adminLabel }>{ adminLabel }</div>}
{ user.roleName && <div className={ styles.adminLabel }>{ user.roleName }</div>}
<div className={ styles.actions }>
{ user.expiredInvitation && !user.joined &&
<Popup
content={ `Generate Invitation Link` }
>
<div className={ styles.button } onClick={ () => generateInviteLink(user) } id="trash">
<Icon name="link-45deg" size="16" color="red"/>
</div>
</Popup>
}
{ !user.expiredInvitation && !user.joined && user.invitationLink &&
<Popup
content={ `Copy Invitation Link` }
>
<div className={ styles.button }>
<CopyButton
content={user.invitationLink}
className="link"
btnText={<Icon name="link-45deg" size="16" color="teal"/>}
/>
</div>
</Popup>
}
{ !!deleteHandler &&
<div className={ styles.button } onClick={ () => deleteHandler(user) } id="trash">
<Icon name="trash" size="16" color="teal"/>
</div>
}
{ !!editHandler &&
<div className={ styles.button } onClick={ () => editHandler(user) }>
<Icon name="edit" size="16" color="teal"/>
</div>
}
</div>
</div>
);
export default UserItem;

View file

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

View file

@ -1,42 +0,0 @@
.tabHeader {
display: flex;
align-items: center;
margin-bottom: 25px;
& .tabTitle {
margin: 0 15px 0 0;
font-weight: 400 !important;
}
}
.form {
padding: 0 20px;
& .formGroup {
margin-bottom: 15px;
}
& label {
display: block;
margin-bottom: 5px;
font-weight: 500;
}
& .input {
width: 100%;
}
& input[type=checkbox] {
margin-right: 10px;
height: 13px;
}
}
.adminInfo {
font-size: 12px;
color: $gray-medium;
}
.smtpMessage {
background-color: #faf6e0;
border-radius: 3px;
}

View file

@ -1,47 +0,0 @@
@import 'mixins.css';
.wrapper {
padding: 15px 10px;
display: flex;
align-items: center;
border-bottom: solid thin $gray-light-shade;
&:hover {
background-color: $active-blue;
transition: all 0.2s;
& .actions {
opacity: 1;
transition: all 0.4s;
}
}
& .adminLabel {
margin-left: 5px;
padding: 0 10px;
border-radius: 3px;
background-color: $gray-lightest;
font-size: 10px;
border: solid thin $gray-light;
}
}
.actions {
margin-left: auto;
/* opacity: 0; */
transition: all 0.4s;
display: flex;
align-items: center;
& .button {
padding: 5px;
cursor: pointer;
margin-left: 10px;
display: flex;
align-items: center;
justify-content: center;
&:hover {
& svg {
fill: $teal-dark;
}
}
}
}

View file

@ -1,6 +1,6 @@
import React, { useEffect } from 'react';
import cn from 'classnames';
import { Loader, Popup, NoContent, Button } from 'UI';
import { Loader, NoContent, Button, Tooltip } from 'UI';
import { connect } from 'react-redux';
import stl from './roles.module.css';
import RoleForm from './components/RoleForm';
@ -69,9 +69,9 @@ function Roles(props: Props) {
<div className={cn(stl.tabHeader, 'flex items-center')}>
<div className="flex items-center mr-auto px-5 pt-5">
<h3 className={cn(stl.tabTitle, 'text-2xl')}>Roles and Access</h3>
<Popup content="You dont have the permissions to perform this action." disabled={isAdmin}>
<Tooltip title="You dont have the permissions to perform this action." disabled={isAdmin}>
<Button variant="primary" onClick={() => editHandler({})}>Add</Button>
</Popup>
</Tooltip>
</div>
</div>

View file

@ -1,5 +1,5 @@
import React from 'react';
import { Popup, Button, IconButton } from 'UI';
import { Tooltip, Button, IconButton } from 'UI';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
import { init, remove, fetchGDPR } from 'Duck/site';
@ -11,20 +11,27 @@ const PERMISSION_WARNING = 'You dont have the permissions to perform this act
const LIMIT_WARNING = 'You have reached site limit.';
function AddProjectButton({ isAdmin = false, init = () => {} }: any) {
const { userStore } = useStore();
const { showModal, hideModal } = useModal();
const limtis = useObserver(() => userStore.limits);
const canAddProject = useObserver(() => isAdmin && (limtis.projects === -1 || limtis.projects > 0));
const { userStore } = useStore();
const { showModal, hideModal } = useModal();
const limtis = useObserver(() => userStore.limits);
const canAddProject = useObserver(
() => isAdmin && (limtis.projects === -1 || limtis.projects > 0)
);
const onClick = () => {
init();
showModal(<NewSiteForm onClose={hideModal} />, { right: true });
};
return (
<Popup content={`${!isAdmin ? PERMISSION_WARNING : !canAddProject ? LIMIT_WARNING : 'Add a Project'}`}>
<Button variant="primary" onClick={onClick} disabled={!canAddProject || !isAdmin}>Add Project</Button>
</Popup>
);
const onClick = () => {
init();
showModal(<NewSiteForm onClose={hideModal} />, { right: true });
};
return (
<Tooltip
title={`${!isAdmin ? PERMISSION_WARNING : !canAddProject ? LIMIT_WARNING : 'Add a Project'}`}
disabled={isAdmin || canAddProject}
>
<Button variant="primary" onClick={onClick} disabled={!canAddProject || !isAdmin}>
Add Project
</Button>
</Tooltip>
);
}
export default connect(null, { init, remove, fetchGDPR })(AddProjectButton);

View file

@ -1,8 +1,8 @@
import { withCopy } from 'HOCs';
import React from 'react';
function ProjectKey({ value, tooltip }: any) {
return <div className="rounded border bg-gray-lightest w-fit px-2">{value}</div>;
function ProjectKey({ value }: any) {
return <div className="rounded border bg-gray-lightest w-fit px-2">{value}</div>;
}
export default withCopy(ProjectKey);

View file

@ -1,7 +1,7 @@
import React from 'react';
import { connect } from 'react-redux';
import withPageTitle from 'HOCs/withPageTitle';
import { Loader, Button, Popup, TextLink, NoContent } from 'UI';
import { Loader, Button, Tooltip, TextLink, NoContent } from 'UI';
import { init, remove, fetchGDPR } from 'Duck/site';
import { RED, YELLOW, GREEN, STATUS_COLOR_MAP } from 'Types/site';
import stl from './sites.module.css';
@ -101,7 +101,7 @@ class Sites extends React.PureComponent {
>
<div className="col-span-4">
<div className="flex items-center">
<Popup content={STATUS_MESSAGE_MAP[_site.status]} inverted>
<Tooltip title={STATUS_MESSAGE_MAP[_site.status]}>
<div className="relative flex items-center justify-center w-10 h-10">
<div
className="absolute left-0 right-0 top-0 bottom-0 mx-auto w-10 h-10 rounded-full opacity-10"
@ -111,7 +111,7 @@ class Sites extends React.PureComponent {
{getInitials(_site.name)}
</div>
</div>
</Popup>
</Tooltip>
<span className="ml-2">{_site.host}</span>
</div>
</div>

View file

@ -1,22 +1,27 @@
import React from 'react';
import { Popup, IconButton, Button } from 'UI';
import { Tooltip, Button } from 'UI';
import { useStore } from 'App/mstore';
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 ) {
const { userStore } = useStore();
const limtis = useObserver(() => userStore.limits);
const cannAddUser = useObserver(() => isAdmin && (limtis.teamMember === -1 || limtis.teamMember > 0));
return (
<Popup
content={ `${ !isAdmin ? PERMISSION_WARNING : (!cannAddUser ? LIMIT_WARNING : 'Add team member') }` }
>
<Button disabled={ !cannAddUser || !isAdmin } variant="primary" onClick={ onClick }>Add Team Member</Button>
</Popup>
);
function AddUserButton({ isAdmin = false, onClick }: any) {
const { userStore } = useStore();
const limtis = useObserver(() => userStore.limits);
const cannAddUser = useObserver(
() => isAdmin && (limtis.teamMember === -1 || limtis.teamMember > 0)
);
return (
<Tooltip
title={`${!isAdmin ? PERMISSION_WARNING : !cannAddUser ? LIMIT_WARNING : 'Add team member'}`}
disabled={isAdmin || cannAddUser}
>
<Button disabled={!cannAddUser || !isAdmin} variant="primary" onClick={onClick}>
Add Team Member
</Button>
</Tooltip>
);
}
export default AddUserButton;
export default AddUserButton;

View file

@ -1,68 +1,108 @@
//@ts-nocheck
import React from 'react';
import { Button, Popup } from 'UI';
import { Button, Tooltip } from 'UI';
import { checkForRecent } from 'App/date';
import cn from 'classnames';
const AdminPrivilegeLabel = ({ user }) => {
return (
<>
{user.isAdmin && <span className="px-2 py-1 bg-gray-lightest rounded border text-sm capitalize">Admin</span>}
{user.isSuperAdmin && <span className="px-2 py-1 bg-gray-lightest rounded border text-sm capitalize">Owner</span>}
{!user.isAdmin && !user.isSuperAdmin && <span className="px-2 py-1 bg-gray-lightest rounded border text-sm capitalize">Member</span>}
</>
);
return (
<>
{user.isAdmin && (
<span className="px-2 py-1 bg-gray-lightest rounded border text-sm capitalize">Admin</span>
)}
{user.isSuperAdmin && (
<span className="px-2 py-1 bg-gray-lightest rounded border text-sm capitalize">Owner</span>
)}
{!user.isAdmin && !user.isSuperAdmin && (
<span className="px-2 py-1 bg-gray-lightest rounded border text-sm capitalize">Member</span>
)}
</>
);
};
interface Props {
isOnboarding?: boolean;
user: any;
editHandler?: any;
generateInvite?: any;
copyInviteCode?: any;
isEnterprise?: boolean;
isOnboarding?: boolean;
user: any;
editHandler?: any;
generateInvite?: any;
copyInviteCode?: any;
isEnterprise?: boolean;
}
function UserListItem(props: Props) {
const { user, editHandler = () => {}, generateInvite = () => {}, copyInviteCode = () => {}, isEnterprise = false, isOnboarding = false } = props;
return (
<div className="grid grid-cols-12 py-4 px-5 border-t items-center select-none hover:bg-active-blue group cursor-pointer" onClick={editHandler}>
<div className="col-span-5">
<span className="mr-2">{user.name}</span>
{/* {isEnterprise && <AdminPrivilegeLabel user={user} />} */}
</div>
<div className="col-span-3">
{!isEnterprise && <AdminPrivilegeLabel user={user} />}
{isEnterprise && (
<>
<span className="px-2 py-1 bg-gray-lightest rounded border text-sm capitalize">{user.roleName}</span>
{ user.isSuperAdmin || user.isAdmin && <><span className="ml-2" /><AdminPrivilegeLabel user={user} /></> }
</>)}
</div>
{!isOnboarding && (
<div className="col-span-2">
<span>{user.createdAt && checkForRecent(user.createdAt, 'LLL dd, yyyy, hh:mm a')}</span>
</div>
const {
user,
editHandler = () => {},
generateInvite = () => {},
copyInviteCode = () => {},
isEnterprise = false,
isOnboarding = false,
} = props;
return (
<div
className="grid grid-cols-12 py-4 px-5 border-t items-center select-none hover:bg-active-blue group cursor-pointer"
onClick={editHandler}
>
<div className="col-span-5">
<span className="mr-2">{user.name}</span>
{/* {isEnterprise && <AdminPrivilegeLabel user={user} />} */}
</div>
<div className="col-span-3">
{!isEnterprise && <AdminPrivilegeLabel user={user} />}
{isEnterprise && (
<>
<span className="px-2 py-1 bg-gray-lightest rounded border text-sm capitalize">
{user.roleName}
</span>
{user.isSuperAdmin ||
(user.isAdmin && (
<>
<span className="ml-2" />
<AdminPrivilegeLabel user={user} />
</>
))}
</>
)}
</div>
{!isOnboarding && (
<div className="col-span-2">
<span>{user.createdAt && checkForRecent(user.createdAt, 'LLL dd, yyyy, hh:mm a')}</span>
</div>
)}
<div
className={cn('justify-self-end invisible group-hover:visible', {
'col-span-2': !isOnboarding,
'col-span-4': isOnboarding,
})}
>
<div className="grid grid-cols-2 gap-3 items-center justify-end">
<div>
{!user.isJoined && user.invitationLink && !user.isExpiredInvite && (
<Tooltip title="Copy Invite Code" hideOnClick={true}>
<Button
variant="text-primary"
icon="link-45deg"
className=""
onClick={copyInviteCode}
/>
</Tooltip>
)}
<div className={cn('justify-self-end invisible group-hover:visible', { 'col-span-2': !isOnboarding, 'col-span-4': isOnboarding })}>
<div className="grid grid-cols-2 gap-3 items-center justify-end">
<div>
{!user.isJoined && user.invitationLink && !user.isExpiredInvite && (
<Popup delay={500} content="Copy Invite Code" hideOnClick={true}>
<Button variant="text-primary" icon="link-45deg" className="" onClick={copyInviteCode} />
</Popup>
)}
{!user.isJoined && user.isExpiredInvite && (
<Popup delay={500} arrow content="Generate Invite" hideOnClick={true}>
<Button icon="link-45deg" variant="text-primary" className="" onClick={generateInvite} />
</Popup>
)}
</div>
<Button variant="text-primary" icon="pencil" />
</div>
</div>
{!user.isJoined && user.isExpiredInvite && (
<Tooltip title="Generate Invite" hideOnClick={true}>
<Button
icon="link-45deg"
variant="text-primary"
className=""
onClick={generateInvite}
/>
</Tooltip>
)}
</div>
<Button variant="text-primary" icon="pencil" />
</div>
);
</div>
</div>
);
}
export default UserListItem;

View file

@ -1,20 +1,26 @@
import { Popup, Icon } from 'UI';
import { Tooltip, Icon } from 'UI';
import styles from './imageInfo.module.css';
const ImageInfo = ({ data }) => (
<div className={ styles.name }>
<Popup
className={ styles.popup }
content={ <img src={ `//${ data.url }` } className={ styles.imagePreview } alt="One of the slowest images" /> }
<div className={styles.name}>
<Tooltip
className={styles.Tooltip}
title={
<img
src={`//${data.url}`}
className={styles.imagePreview}
alt="One of the slowest images"
/>
}
>
<div className={ styles.imageWrapper }>
<Icon name="camera-alt" size="18" color="gray-light" />
<div className={ styles.label }>{ 'Preview' }</div>
</div>
</Popup>
<Popup content={ data.url } >
<span>{ data.name }</span>
</Popup>
<div className={styles.imageWrapper}>
<Icon name="camera-alt" size="18" color="gray-light" />
<div className={styles.label}>{'Preview'}</div>
</div>
</Tooltip>
<Tooltip title={data.url}>
<span>{data.name}</span>
</Tooltip>
</div>
);

View file

@ -1,28 +1,35 @@
import React, { useState } from 'react';
import { connect } from 'react-redux';
import { Loader, NoContent, Icon, Popup } from 'UI';
import { Loader, NoContent, Icon, Tooltip } from 'UI';
import { Styles } from '../../common';
import { ResponsiveContainer } from 'recharts';
import stl from './CustomMetricWidget.module.css';
import { getStartAndEndTimestampsByDensity } from 'Types/dashboard/helper';
import { init, edit, remove, setAlertMetricId, setActiveWidget, updateActiveState } from 'Duck/customMetrics';
import {
init,
edit,
remove,
setAlertMetricId,
setActiveWidget,
updateActiveState,
} from 'Duck/customMetrics';
import { setShowAlerts } from 'Duck/dashboard';
import CustomMetriLineChart from '../CustomMetriLineChart';
import CustomMetricPieChart from '../CustomMetricPieChart';
import CustomMetricPercentage from '../CustomMetricPercentage';
import CustomMetricTable from '../CustomMetricTable';
import { NO_METRIC_DATA } from 'App/constants/messages'
import { NO_METRIC_DATA } from 'App/constants/messages';
const customParams = rangeName => {
const params = { density: 70 }
const customParams = (rangeName) => {
const params = { density: 70 };
// if (rangeName === LAST_24_HOURS) params.density = 70
// if (rangeName === LAST_30_MINUTES) params.density = 70
// if (rangeName === YESTERDAY) params.density = 70
// if (rangeName === LAST_7_DAYS) params.density = 70
return params
}
return params;
};
interface Props {
metric: any;
@ -43,12 +50,12 @@ interface Props {
}
function CustomMetricWidget(props: Props) {
const { metric, period, isTemplate } = props;
const [loading, setLoading] = useState(false)
const [loading, setLoading] = useState(false);
const [data, setData] = useState<any>([]);
// const [seriesMap, setSeriesMap] = useState<any>([]);
const colors = Styles.customMetricColors;
const params = customParams(period.rangeName)
const params = customParams(period.rangeName);
// const metricParams = { ...params, metricId: metric.metricId, viewType: 'lineChart', startDate: period.start, endDate: period.end }
const isLineChart = metric.viewType === 'lineChart';
const isProgress = metric.viewType === 'progress';
@ -61,17 +68,18 @@ function CustomMetricWidget(props: Props) {
period: period,
...period.toTimestamps(),
filters,
}
};
props.setActiveWidget(activeWidget);
}
};
const clickHandler = (event, index) => {
if (event) {
const payload = event.activePayload[0].payload;
const timestamp = payload.timestamp;
const periodTimestamps = metric.metricType === 'timeseries' ?
getStartAndEndTimestampsByDensity(timestamp, period.start, period.end, params.density) :
period.toTimestamps();
const periodTimestamps =
metric.metricType === 'timeseries'
? getStartAndEndTimestampsByDensity(timestamp, period.start, period.end, params.density)
: period.toTimestamps();
const activeWidget = {
widget: metric,
@ -79,68 +87,81 @@ function CustomMetricWidget(props: Props) {
...periodTimestamps,
timestamp: payload.timestamp,
index,
}
};
props.setActiveWidget(activeWidget);
}
}
};
const updateActiveState = (metricId, state) => {
props.updateActiveState(metricId, state);
}
};
return (
<div className={stl.wrapper}>
<div className="flex items-center p-2">
<div className="font-medium">{metric.name}</div>
<div className="ml-auto flex items-center">
{!isTable && !isPieChart && <WidgetIcon className="cursor-pointer mr-6" icon="bell-plus" tooltip="Set Alert" onClick={props.onAlertClick} /> }
<WidgetIcon className="cursor-pointer mr-6" icon="pencil" tooltip="Edit Metric" onClick={() => props.init(metric)} />
<WidgetIcon className="cursor-pointer" icon="close" tooltip="Hide Metric" onClick={() => updateActiveState(metric.metricId, false)} />
{!isTable && !isPieChart && (
<WidgetIcon
className="cursor-pointer mr-6"
icon="bell-plus"
tooltip="Set Alert"
onClick={props.onAlertClick}
/>
)}
<WidgetIcon
className="cursor-pointer mr-6"
icon="pencil"
tooltip="Edit Metric"
onClick={() => props.init(metric)}
/>
<WidgetIcon
className="cursor-pointer"
icon="close"
tooltip="Hide Metric"
onClick={() => updateActiveState(metric.metricId, false)}
/>
</div>
</div>
<div className="px-3">
<Loader loading={ loading } size="small">
<NoContent
size="small"
title={NO_METRIC_DATA}
show={ data.length === 0 }
>
<ResponsiveContainer height={ 240 } width="100%">
<Loader loading={loading} size="small">
<NoContent size="small" title={NO_METRIC_DATA} show={data.length === 0}>
<ResponsiveContainer height={240} width="100%">
<>
{isLineChart && (
<CustomMetriLineChart
data={ data }
params={ params }
// seriesMap={ seriesMap }
colors={ colors }
onClick={ clickHandler }
data={data}
params={params}
// seriesMap={ seriesMap }
colors={colors}
onClick={clickHandler}
/>
)}
{isPieChart && (
<CustomMetricPieChart
metric={metric}
data={ data[0] }
colors={ colors }
onClick={ clickHandlerTable }
data={data[0]}
colors={colors}
onClick={clickHandlerTable}
/>
)}
{isProgress && (
<CustomMetricPercentage
data={ data[0] }
params={ params }
colors={ colors }
onClick={ clickHandler }
data={data[0]}
params={params}
colors={colors}
onClick={clickHandler}
/>
)}
{isTable && (
<CustomMetricTable
metric={ metric }
data={ data[0] }
onClick={ clickHandlerTable }
metric={metric}
data={data[0]}
onClick={clickHandlerTable}
isTemplate={isTemplate}
/>
)}
@ -153,26 +174,36 @@ function CustomMetricWidget(props: Props) {
);
}
export default connect(state => ({
period: state.getIn(['dashboard', 'period']),
}), {
remove,
setShowAlerts,
setAlertMetricId,
edit,
setActiveWidget,
updateActiveState,
init,
})(CustomMetricWidget);
export default connect(
(state) => ({
period: state.getIn(['dashboard', 'period']),
}),
{
remove,
setShowAlerts,
setAlertMetricId,
edit,
setActiveWidget,
updateActiveState,
init,
}
)(CustomMetricWidget);
const WidgetIcon = ({ className = '', tooltip = '', icon, onClick }) => (
<Popup
size="small"
content={tooltip}
>
const WidgetIcon = ({
className = '',
tooltip = '',
icon,
onClick,
}: {
className: string;
tooltip: string;
icon: string;
onClick: any;
}) => (
<Tooltip title={tooltip}>
<div className={className} onClick={onClick}>
<Icon name={icon} size="14" />
{/* @ts-ignore */}
<Icon name={icon} size="14" />
</div>
</Popup>
)
</Tooltip>
);

View file

@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import { Map } from 'immutable';
import cn from 'classnames';
import { ResponsiveContainer, AreaChart, XAxis, YAxis, CartesianGrid, Area, Legend } from 'recharts';
import { Loader, TextEllipsis, Popup, Icon } from 'UI';
import { Loader, TextEllipsis, Tooltip } from 'UI';
import { TYPES } from 'Types/resource';
import { LAST_24_HOURS, LAST_30_MINUTES, LAST_7_DAYS, LAST_30_DAYS } from 'Types/app/period';
import { fetchPerformanseSearch } from 'Duck/dashboard';
@ -101,7 +101,7 @@ export default class Performance extends React.PureComponent {
compare = () => this.setState({ comparing: true })
legendPopup = (component, trigger) => <Popup size="mini" content={ component }>{trigger}</Popup>
legendPopup = (component, trigger) => <Tooltip size="mini" content={ component }>{trigger}</Tooltip>
legendFormatter = (value, entry, index) => {
const { opacity } = this.state;
@ -113,16 +113,15 @@ export default class Performance extends React.PureComponent {
if (value.includes(BASE_KEY)) {
const resourceIndex = Number.parseInt(value.substr(BASE_KEY.length));
return (
<Popup
wide
content={ this.state.resources.getIn([ resourceIndex, 'value' ]) }
<Tooltip
title={ this.state.resources.getIn([ resourceIndex, 'value' ]) }
>
<TextEllipsis
maxWidth="200px"
style={ { verticalAlign: 'middle' } }
text={ this.state.resources.getIn([ resourceIndex, 'value' ]) }
/>
</Popup>
</Tooltip>
);
}
}

View file

@ -1,5 +1,5 @@
import React from 'react';
import { Popup } from 'UI';
import { Tooltip } from 'UI';
import cn from 'classnames';
import styles from './imageInfo.module.css';
@ -8,18 +8,24 @@ const supportedTypes = ['png', 'jpg', 'jpeg', 'svg'];
const ImageInfo = ({ data }) => {
const canPreview = supportedTypes.includes(data.type);
return (
<div className={ styles.name }>
<Popup
className={ styles.popup }
<div className={styles.name}>
<Tooltip
className={styles.popup}
disabled={!canPreview}
content={ <img src={ `${ data.url }` } className={ styles.imagePreview } alt="One of the slowest images" /> }
title={
<img
src={`${data.url}`}
className={styles.imagePreview}
alt="One of the slowest images"
/>
}
>
<div className={cn({ [styles.hasPreview]: canPreview})}>
<div className={ styles.label }>{data.name}</div>
<div className={cn({ [styles.hasPreview]: canPreview })}>
<div className={styles.label}>{data.name}</div>
</div>
</Popup>
</Tooltip>
</div>
)
);
};
ImageInfo.displayName = 'ImageInfo';

View file

@ -1,24 +1,27 @@
import React from 'react';
import { Popup, Icon } from 'UI';
import { Tooltip, Icon } from 'UI';
import styles from './imageInfo.module.css';
const ImageInfo = ({ data }) => (
<div className={ styles.name }>
<Popup
className={ styles.popup }
content={ <img src={ `//${ data.url }` } className={ styles.imagePreview } alt="One of the slowest images" /> }
<div className={styles.name}>
<Tooltip
className={styles.popup}
title={
<img
src={`//${data.url}`}
className={styles.imagePreview}
alt="One of the slowest images"
/>
}
>
<div className={ styles.imageWrapper }>
<div className={styles.imageWrapper}>
<Icon name="camera-alt" size="18" color="gray-light" />
<div className={ styles.label }>{ 'Preview' }</div>
<div className={styles.label}>{'Preview'}</div>
</div>
</Popup>
<Popup
disabled
content={ data.url }
>
<span>{ data.name }</span>
</Popup>
</Tooltip>
<Tooltip disabled content={data.url}>
<span>{data.name}</span>
</Tooltip>
</div>
);

View file

@ -1,5 +1,5 @@
import React from 'react';
import { Popup } from 'UI';
import { Tooltip } from 'UI';
import cn from 'classnames';
import styles from './imageInfo.module.css';
@ -8,18 +8,24 @@ const supportedTypes = ['png', 'jpg', 'jpeg', 'svg'];
const ImageInfo = ({ data }) => {
const canPreview = supportedTypes.includes(data.type);
return (
<div className={ styles.name }>
<Popup
className={ styles.popup }
<div className={styles.name}>
<Tooltip
className={styles.popup}
disabled={!canPreview}
content={ <img src={ `${ data.url }` } className={ styles.imagePreview } alt="One of the slowest images" /> }
title={
<img
src={`${data.url}`}
className={styles.imagePreview}
alt="One of the slowest images"
/>
}
>
<div className={cn({ [styles.hasPreview]: canPreview})}>
<div className={ styles.label }>{data.name}</div>
<div className={cn({ [styles.hasPreview]: canPreview })}>
<div className={styles.label}>{data.name}</div>
</div>
</Popup>
</Tooltip>
</div>
)
);
};
ImageInfo.displayName = 'ImageInfo';

View file

@ -1,50 +1,59 @@
import React from 'react';
import { Popup } from 'UI';
import { Tooltip } from 'UI';
const MIN_WIDTH = '20px';
interface Props {
issue: any
issue: any;
}
function FunnelIssueGraph(props: Props) {
const { issue } = props;
const { issue } = props;
return (
<div className="flex rounded-sm" style={{ width: '600px' }}>
<div style={{ width: issue.unaffectedSessionsPer + '%', minWidth: MIN_WIDTH }} className="relative">
<Popup
content={ `Unaffected sessions` }
size="tiny"
inverted
position="top center"
>
<div className="w-full relative rounded-tl-sm rounded-bl-sm" style={{ height: '18px', backgroundColor: 'rgba(217, 219, 238, 0.7)' }} />
<div className="absolute ml-2 font-bold top-0 bottom-0 text-sm">{issue.unaffectedSessions}</div>
</Popup>
</div>
<div style={{ width: issue.affectedSessionsPer + '%', minWidth: MIN_WIDTH}} className="border-l relative">
<Popup
content={ `Affected sessions` }
size="tiny"
inverted
position="top center"
>
<div className="w-full relative" style={{ height: '18px', backgroundColor: 'rgba(238, 238, 238, 0.7)' }} />
<div className="absolute ml-2 font-bold top-0 bottom-0 text-sm">{issue.affectedSessions}</div>
</Popup>
</div>
<div style={{ width: issue.lostConversionsPer + '%', minWidth: MIN_WIDTH}} className="border-l relative">
<Popup
content={ `Conversion lost` }
size="tiny"
inverted
position="top center"
>
<div className="w-full relative rounded-tr-sm rounded-br-sm" style={{ height: '18px', backgroundColor: 'rgba(204, 0, 0, 0.26)' }} />
<div className="absolute ml-2 font-bold top-0 bottom-0 text-sm color-red">{issue.lostConversions}</div>
</Popup>
</div>
</div>
);
return (
<div className="flex rounded-sm" style={{ width: '600px' }}>
<div
style={{ width: issue.unaffectedSessionsPer + '%', minWidth: MIN_WIDTH }}
className="relative"
>
<Tooltip title={`Unaffected sessions`} placement="top">
<div
className="w-full relative rounded-tl-sm rounded-bl-sm"
style={{ height: '18px', backgroundColor: 'rgba(217, 219, 238, 0.7)' }}
/>
<div className="absolute ml-2 font-bold top-0 bottom-0 text-sm">
{issue.unaffectedSessions}
</div>
</Tooltip>
</div>
<div
style={{ width: issue.affectedSessionsPer + '%', minWidth: MIN_WIDTH }}
className="border-l relative"
>
<Tooltip title={`Affected sessions`} placement="top">
<div
className="w-full relative"
style={{ height: '18px', backgroundColor: 'rgba(238, 238, 238, 0.7)' }}
/>
<div className="absolute ml-2 font-bold top-0 bottom-0 text-sm">
{issue.affectedSessions}
</div>
</Tooltip>
</div>
<div
style={{ width: issue.lostConversionsPer + '%', minWidth: MIN_WIDTH }}
className="border-l relative"
>
<Tooltip title={`Conversion lost`} placement="top">
<div
className="w-full relative rounded-tr-sm rounded-br-sm"
style={{ height: '18px', backgroundColor: 'rgba(204, 0, 0, 0.26)' }}
/>
<div className="absolute ml-2 font-bold top-0 bottom-0 text-sm color-red">
{issue.lostConversions}
</div>
</Tooltip>
</div>
</div>
);
}
export default FunnelIssueGraph;
export default FunnelIssueGraph;

View file

@ -5,7 +5,7 @@ import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
import { Button, Icon, SegmentSelection } from 'UI'
import FilterSeries from '../FilterSeries';
import { confirm, Popup } from 'UI';
import { confirm, Tooltip } from 'UI';
import Select from 'Shared/Select'
import { withSiteId, dashboardMetricDetails, metricDetails } from 'App/routes'
@ -200,8 +200,8 @@ function WidgetForm(props: Props) {
</div>
<div className="form-groups flex items-center justify-between">
<Popup
content="Cannot save funnel metric without at least 2 events"
<Tooltip
title="Cannot save funnel metric without at least 2 events"
disabled={!cannotSaveFunnel}
>
<Button
@ -211,7 +211,7 @@ function WidgetForm(props: Props) {
>
{metric.exists() ? 'Update' : 'Create'}
</Button>
</Popup>
</Tooltip>
<div className="flex items-center">
{metric.exists() && (
<Button variant="text-primary" onClick={onDelete}>

View file

@ -1,21 +1,22 @@
import React from 'react';
import { Icon, Popup } from 'UI';
import { Icon, Tooltip } from 'UI';
interface Props {
className: string
onClick: () => void
icon: string
tooltip: string
className: string;
onClick: () => void;
icon: string;
tooltip: string;
}
function WidgetIcon(props: Props) {
const { className, onClick, icon, tooltip } = props;
return (
<Popup title={tooltip} >
<div className={className} onClick={onClick}>
<Icon name={icon} size="14" />
</div>
</Popup>
);
const { className, onClick, icon, tooltip } = props;
return (
<Tooltip title={tooltip}>
<div className={className} onClick={onClick}>
{/* @ts-ignore */}
<Icon name={icon} size="14" />
</div>
</Tooltip>
);
}
export default WidgetIcon;

View file

@ -1,6 +1,6 @@
import React, { useRef } from 'react';
import cn from 'classnames';
import { ItemMenu, Popup } from 'UI';
import { ItemMenu, Tooltip } from 'UI';
import { useDrag, useDrop } from 'react-dnd';
import WidgetChart from '../WidgetChart';
import { observer } from 'mobx-react-lite';
@ -14,150 +14,161 @@ import { FilterKey } from 'App/types/filter/filterType';
import LazyLoad from 'react-lazyload';
interface Props {
className?: string;
widget?: any;
index?: number;
moveListItem?: any;
isPreview?: boolean;
isTemplate?: boolean
dashboardId?: string;
siteId?: string,
active?: boolean;
history?: any
onClick?: () => void;
isWidget?: boolean;
hideName?: boolean;
grid?: string;
className?: string;
widget?: any;
index?: number;
moveListItem?: any;
isPreview?: boolean;
isTemplate?: boolean;
dashboardId?: string;
siteId?: string;
active?: boolean;
history?: any;
onClick?: () => void;
isWidget?: boolean;
hideName?: boolean;
grid?: string;
}
function WidgetWrapper(props: Props & RouteComponentProps) {
const { dashboardStore } = useStore();
const { isWidget = false, active = false, index = 0, moveListItem = null, isPreview = false, isTemplate = false, siteId, grid = "" } = props;
const widget: any = props.widget;
const isTimeSeries = widget.metricType === 'timeseries';
const isPredefined = widget.metricType === 'predefined';
const dashboard = dashboardStore.selectedDashboard;
const { dashboardStore } = useStore();
const {
isWidget = false,
active = false,
index = 0,
moveListItem = null,
isPreview = false,
isTemplate = false,
siteId,
grid = '',
} = props;
const widget: any = props.widget;
const isTimeSeries = widget.metricType === 'timeseries';
const isPredefined = widget.metricType === 'predefined';
const dashboard = dashboardStore.selectedDashboard;
const [{ isDragging }, dragRef] = useDrag({
type: 'item',
item: { index, grid },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
opacity: monitor.isDragging() ? 0.5 : 1,
}),
});
const [{ isDragging }, dragRef] = useDrag({
type: 'item',
item: { index, grid },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
opacity: monitor.isDragging() ? 0.5 : 1,
}),
});
const [{ isOver, canDrop }, dropRef] = useDrop({
accept: 'item',
drop: (item: any) => {
if (item.index === index || (item.grid !== grid)) return;
moveListItem(item.index, index);
},
canDrop(item) {
return item.grid === grid
},
collect: (monitor: any) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
})
const [{ isOver, canDrop }, dropRef] = useDrop({
accept: 'item',
drop: (item: any) => {
if (item.index === index || item.grid !== grid) return;
moveListItem(item.index, index);
},
canDrop(item) {
return item.grid === grid;
},
collect: (monitor: any) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
});
const onDelete = async () => {
dashboardStore.deleteDashboardWidget(dashboard?.dashboardId, widget.widgetId);
}
const onDelete = async () => {
dashboardStore.deleteDashboardWidget(dashboard?.dashboardId, widget.widgetId);
};
const onChartClick = () => {
if (!isWidget || isPredefined) return;
props.history.push(withSiteId(dashboardMetricDetails(dashboard?.dashboardId, widget.metricId),siteId));
}
const ref: any = useRef(null)
const dragDropRef: any = dragRef(dropRef(ref))
const addOverlay = isTemplate || (!isPredefined && isWidget && widget.metricOf !== FilterKey.ERRORS && widget.metricOf !== FilterKey.SESSIONS)
return (
<div
className={
cn(
"relative rounded bg-white border group",
'col-span-' + widget.config.col,
{ "hover:shadow-border-gray": !isTemplate && isWidget },
{ "hover:shadow-border-main": isTemplate }
)
}
style={{
userSelect: 'none',
opacity: isDragging ? 0.5 : 1,
borderColor: (canDrop && isOver) || active ? '#394EFF' : (isPreview ? 'transparent' : '#EEEEEE'),
}}
ref={dragDropRef}
onClick={props.onClick ? props.onClick : () => {}}
id={`widget-${widget.widgetId}`}
>
{!isTemplate && isWidget && isPredefined &&
<div
className={cn(
stl.drillDownMessage,
'disabled text-gray text-sm invisible group-hover:visible')}
>
{'Cannot drill down system provided metrics'}
</div>
}
{/* @ts-ignore */}
<Popup
hideOnClick={true}
position="bottom"
delay={300}
followCursor
disabled={!isTemplate}
boundary="viewport"
flip={["top"]}
content={<span>Click to select</span>}
>
{addOverlay && <TemplateOverlay onClick={onChartClick} isTemplate={isTemplate} />}
<div
className={cn("p-3 pb-4 flex items-center justify-between", { "cursor-move" : !isTemplate && isWidget })}
>
{!props.hideName ? <div className="capitalize-first w-full font-medium">{widget.name}</div> : null}
{isWidget && (
<div className="flex items-center" id="no-print">
{!isPredefined && isTimeSeries && (
<>
<AlertButton seriesId={widget.series[0] && widget.series[0].seriesId} />
<div className='mx-2'/>
</>
)}
{!isTemplate && (
<ItemMenu
items={[
{
text: widget.metricType === 'predefined' ? 'Cannot edit system generated metrics' : 'Edit',
onClick: onChartClick,
disabled: widget.metricType === 'predefined',
},
{
text: 'Hide',
onClick: onDelete
},
]}
/>
)}
</div>
)}
</div>
{/* <LazyLoad height={!isTemplate ? 300 : 10} offset={!isTemplate ? 100 : 10} > */}
<LazyLoad offset={!isTemplate ? 100 : 10} >
<div className="px-4" onClick={onChartClick}>
<WidgetChart metric={widget} isTemplate={isTemplate} isWidget={isWidget} />
</div>
</LazyLoad>
</Popup>
</div>
const onChartClick = () => {
if (!isWidget || isPredefined) return;
props.history.push(
withSiteId(dashboardMetricDetails(dashboard?.dashboardId, widget.metricId), siteId)
);
};
const ref: any = useRef(null);
const dragDropRef: any = dragRef(dropRef(ref));
const addOverlay =
isTemplate ||
(!isPredefined &&
isWidget &&
widget.metricOf !== FilterKey.ERRORS &&
widget.metricOf !== FilterKey.SESSIONS);
return (
<div
className={cn(
'relative rounded bg-white border group',
'col-span-' + widget.config.col,
{ 'hover:shadow-border-gray': !isTemplate && isWidget },
{ 'hover:shadow-border-main': isTemplate }
)}
style={{
userSelect: 'none',
opacity: isDragging ? 0.5 : 1,
borderColor:
(canDrop && isOver) || active ? '#394EFF' : isPreview ? 'transparent' : '#EEEEEE',
}}
ref={dragDropRef}
onClick={props.onClick ? props.onClick : () => {}}
id={`widget-${widget.widgetId}`}
>
{!isTemplate && isWidget && isPredefined && (
<div
className={cn(
stl.drillDownMessage,
'disabled text-gray text-sm invisible group-hover:visible'
)}
>
{'Cannot drill down system provided metrics'}
</div>
)}
<Tooltip disabled={!isTemplate} title={<span>Click to select</span>} className="w-full">
{addOverlay && <TemplateOverlay onClick={onChartClick} isTemplate={isTemplate} />}
<div
className={cn('p-3 pb-4 flex items-center justify-between', {
'cursor-move': !isTemplate && isWidget,
})}
>
{!props.hideName ? (
<div className="capitalize-first w-full font-medium">{widget.name}</div>
) : null}
{isWidget && (
<div className="flex items-center" id="no-print">
{!isPredefined && isTimeSeries && (
<>
<AlertButton seriesId={widget.series[0] && widget.series[0].seriesId} />
<div className="mx-2" />
</>
)}
{!isTemplate && (
<ItemMenu
items={[
{
text:
widget.metricType === 'predefined'
? 'Cannot edit system generated metrics'
: 'Edit',
onClick: onChartClick,
disabled: widget.metricType === 'predefined',
},
{
text: 'Hide',
onClick: onDelete,
},
]}
/>
)}
</div>
)}
</div>
{/* <LazyLoad height={!isTemplate ? 300 : 10} offset={!isTemplate ? 100 : 10} > */}
<LazyLoad offset={!isTemplate ? 100 : 0}>
<div className="px-4" onClick={onChartClick}>
<WidgetChart metric={widget} isTemplate={isTemplate} isWidget={isWidget} />
</div>
</LazyLoad>
</Tooltip>
</div>
);
}
export default withRouter(observer(WidgetWrapper));

View file

@ -1,59 +1,59 @@
import React from 'react';
import cn from 'classnames';
import { Popup, TextEllipsis } from 'UI';
import { Tooltip, TextEllipsis } from 'UI';
import { Styles } from '../../Dashboard/Widgets/common';
import cls from './distributionBar.module.css';
import { colorScale } from 'App/utils';
function DistributionBar({ className, title, partitions }) {
if (partitions.length === 0) {
return null;
}
if (partitions.length === 0) {
return null;
}
const values = Array(partitions.length)
.fill()
.map((element, index) => index + 0);
const colors = colorScale(values, Styles.colors);
const values = Array(partitions.length)
.fill()
.map((element, index) => index + 0);
const colors = colorScale(values, Styles.colors);
return (
<div className={className}>
<div className="flex justify-between text-sm mb-1">
<div className="capitalize">{title}</div>
<div className="flex items-center">
<div className="font-thin capitalize" style={{ maxWidth: '80px', height: '19px' }}>
<TextEllipsis text={partitions[0].label} />
</div>
<div className="ml-2">{`${Math.round(partitions[0].prc)}% `}</div>
</div>
</div>
<div className={cn('border-radius-3 overflow-hidden flex', cls.bar)}>
{partitions.map((p, index) => (
<Popup
key={p.label}
content={
<div className="text-center">
<span className="capitalize">{p.label}</span>
<br />
{`${Math.round(p.prc)}%`}
</div>
}
style={{
marginLeft: '1px',
width: `${p.prc}%`,
backgroundColor: colors(index),
}}
>
<div
className="h-full bg-tealx"
style={{
backgroundColor: colors(index),
}}
/>
</Popup>
))}
</div>
return (
<div className={className}>
<div className="flex justify-between text-sm mb-1">
<div className="capitalize">{title}</div>
<div className="flex items-center">
<div className="font-thin capitalize" style={{ maxWidth: '80px', height: '19px' }}>
<TextEllipsis text={partitions[0].label} />
</div>
<div className="ml-2">{`${Math.round(partitions[0].prc)}% `}</div>
</div>
);
</div>
<div className={cn('border-radius-3 overflow-hidden flex', cls.bar)}>
{partitions.map((p, index) => (
<Tooltip
key={p.label}
title={
<div className="text-center">
<span className="capitalize">{p.label}</span>
<br />
{`${Math.round(p.prc)}%`}
</div>
}
style={{
marginLeft: '1px',
width: `${p.prc}%`,
backgroundColor: colors(index),
}}
>
<div
className="h-full bg-tealx"
style={{
backgroundColor: colors(index),
}}
/>
</Tooltip>
))}
</div>
</div>
);
}
DistributionBar.displayName = 'DistributionBar';

View file

@ -1,119 +1,87 @@
import React from 'react';
import { connect } from 'react-redux';
import withSiteIdRouter from 'HOCs/withSiteIdRouter';
import { errors as errorsRoute, error as errorRoute } from 'App/routes';
import { NoContent, Loader, IconButton, Icon, Popup, BackLink } from 'UI';
import { error as errorRoute } from 'App/routes';
import { NoContent, Loader } from 'UI';
import { fetch, fetchTrace } from 'Duck/errors';
import MainSection from './MainSection';
import SideSection from './SideSection';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
@connect(
(state) => ({
errorIdInStore: state.getIn(['errors', 'instance']).errorId,
list: state.getIn(['errors', 'instanceTrace']),
loading: state.getIn(['errors', 'fetch', 'loading']) || state.getIn(['errors', 'fetchTrace', 'loading']),
errorOnFetch: state.getIn(['errors', 'fetch', 'errors']) || state.getIn(['errors', 'fetchTrace', 'errors']),
}),
{
fetch,
fetchTrace,
}
(state) => ({
errorIdInStore: state.getIn(['errors', 'instance']).errorId,
list: state.getIn(['errors', 'instanceTrace']),
loading:
state.getIn(['errors', 'fetch', 'loading']) ||
state.getIn(['errors', 'fetchTrace', 'loading']),
errorOnFetch:
state.getIn(['errors', 'fetch', 'errors']) || state.getIn(['errors', 'fetchTrace', 'errors']),
}),
{
fetch,
fetchTrace,
}
)
@withSiteIdRouter
export default class ErrorInfo extends React.PureComponent {
ensureInstance() {
const { errorId, loading, errorOnFetch } = this.props;
if (!loading && this.props.errorIdInStore !== errorId && errorId != null) {
this.props.fetch(errorId);
this.props.fetchTrace(errorId);
}
ensureInstance() {
const { errorId, loading, errorOnFetch } = this.props;
if (!loading && this.props.errorIdInStore !== errorId && errorId != null) {
this.props.fetch(errorId);
this.props.fetchTrace(errorId);
}
componentDidMount() {
this.ensureInstance();
}
componentDidMount() {
this.ensureInstance();
}
componentDidUpdate() {
this.ensureInstance();
}
next = () => {
const { list, errorId } = this.props;
const curIndex = list.findIndex((e) => e.errorId === errorId);
const next = list.get(curIndex + 1);
if (next != null) {
this.props.history.push(errorRoute(next.errorId));
}
componentDidUpdate() {
this.ensureInstance();
};
prev = () => {
const { list, errorId } = this.props;
const curIndex = list.findIndex((e) => e.errorId === errorId);
const prev = list.get(curIndex - 1);
if (prev != null) {
this.props.history.push(errorRoute(prev.errorId));
}
next = () => {
const { list, errorId } = this.props;
const curIndex = list.findIndex((e) => e.errorId === errorId);
const next = list.get(curIndex + 1);
if (next != null) {
this.props.history.push(errorRoute(next.errorId));
}
};
prev = () => {
const { list, errorId } = this.props;
const curIndex = list.findIndex((e) => e.errorId === errorId);
const prev = list.get(curIndex - 1);
if (prev != null) {
this.props.history.push(errorRoute(prev.errorId));
}
};
render() {
const { loading, errorIdInStore, list, errorId } = this.props;
};
render() {
const { loading, errorIdInStore, list, errorId } = this.props;
let nextDisabled = true,
prevDisabled = true;
if (list.size > 0) {
nextDisabled = loading || list.last().errorId === errorId;
prevDisabled = loading || list.first().errorId === errorId;
}
return (
<NoContent
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.EMPTY_STATE} size="170" />
<div className="mt-6 text-2xl">No Error Found!</div>
</div>
}
subtext="Please try to find existing one."
// animatedIcon="no-results"
show={!loading && errorIdInStore == null}
>
{/* <div className="w-9/12 mb-4 flex justify-between">
<BackLink to={ errorsRoute() } label="Back" />
<div />
<div className="flex items-center">
<Popup
pinned
content="Prev Error"
>
<IconButton
outline
compact
size="small"
icon="prev1"
disabled={ prevDisabled }
onClick={this.prev}
/>
</Popup>
<div className="mr-3" />
<Popup
pinned
content="Next Error"
>
<IconButton
outline
compact
size="small"
icon="next1"
disabled={ nextDisabled }
onClick={this.next}
/>
</Popup>
</div>
</div> */}
<div className="flex">
<Loader loading={loading} className="w-9/12">
<MainSection className="w-9/12" />
<SideSection className="w-3/12" />
</Loader>
</div>
</NoContent>
);
let nextDisabled = true,
prevDisabled = true;
if (list.size > 0) {
nextDisabled = loading || list.last().errorId === errorId;
prevDisabled = loading || list.first().errorId === errorId;
}
return (
<NoContent
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.EMPTY_STATE} size="170" />
<div className="mt-6 text-2xl">No Error Found!</div>
</div>
}
subtext="Please try to find existing one."
show={!loading && errorIdInStore == null}
>
<div className="flex">
<Loader loading={loading} className="w-9/12">
<MainSection className="w-9/12" />
<SideSection className="w-3/12" />
</Loader>
</div>
</NoContent>
);
}
}

View file

@ -1,12 +1,23 @@
import React, { useState } from 'react'
import { Icon, Popup } from 'UI'
import React, { useState } from 'react';
import { Icon, Tooltip as AppTooltip } from 'UI';
import { numberCompact } from 'App/utils';
import { BarChart, Bar, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, LabelList, Label } from 'recharts';
import {
BarChart,
Bar,
Cell,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
LabelList,
Label,
} from 'recharts';
import { connect } from 'react-redux';
import { setActiveStages } from 'Duck/funnels';
import { Styles } from '../../Dashboard/Widgets/common';
import { numberWithCommas } from 'App/utils'
import { truncate } from 'App/utils'
import { numberWithCommas } from 'App/utils';
import { truncate } from 'App/utils';
const MIN_BAR_HEIGHT = 20;
@ -14,27 +25,32 @@ function CustomTick(props) {
const { x, y, payload } = props;
return (
<g transform={`translate(${x},${y})`}>
<text x={0} y={0} dy={16} fontSize={12} textAnchor="middle" fill="#666">{payload.value}</text>
<text x={0} y={0} dy={16} fontSize={12} textAnchor="middle" fill="#666">
{payload.value}
</text>
</g>
);
}
function FunnelGraph(props) {
const { data, activeStages, funnelId, liveFilters } = props;
const [activeIndex, setActiveIndex] = useState(activeStages)
const { data, activeStages, funnelId, liveFilters } = props;
const [activeIndex, setActiveIndex] = useState(activeStages);
const renderPercentage = (props) => {
const {
x, y, width, height, value,
} = props;
const { x, y, width, height, value } = props;
const radius = 10;
const _x = (x + width / 2) + 45;
const _x = x + width / 2 + 45;
return (
<g>
<svg width="46px" height="21px" version="1.1">
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M37.2387001,0.5 L45.3588127,10.5034561 L37.4215407,20.5 L0.5,20.5 L0.5,0.5 L37.2387001,0.5 Z" id="Rectangle" stroke="#AFACAC" fill="#FFFFFF"></path>
<path
d="M37.2387001,0.5 L45.3588127,10.5034561 L37.4215407,20.5 L0.5,20.5 L0.5,0.5 L37.2387001,0.5 Z"
id="Rectangle"
stroke="#AFACAC"
fill="#FFFFFF"
></path>
</g>
</svg>
<text x={x} y={70} fill="#000" textAnchor="middle" dominantBaseline="middle">
@ -45,31 +61,38 @@ function FunnelGraph(props) {
};
const renderCustomizedLabel = (props) => {
const {
x, y, width, height, value, textColor = '#fff'
} = props;
const { x, y, width, height, value, textColor = '#fff' } = props;
const radius = 10;
if (value === 0) return;
return (
<g>
<text x={x + width / 2} y={(y - radius) + 20} fill={ textColor } font-size="12" textAnchor="middle" dominantBaseline="middle">
<g>
<text
x={x + width / 2}
y={y - radius + 20}
fill={textColor}
font-size="12"
textAnchor="middle"
dominantBaseline="middle"
>
{numberCompact(value)}
</text>
</g>
);
};
const handleClick= (data, index) => {
if (activeStages.length === 1 && activeStages.includes(index)) { // selecting the same bar
const handleClick = (data, index) => {
if (activeStages.length === 1 && activeStages.includes(index)) {
// selecting the same bar
props.setActiveStages([], null);
return;
}
if (activeStages.length === 2) { // already having two bars
return;
}
if (activeStages.length === 2) {
// already having two bars
return;
}
// new selection
const arr = activeStages.concat([index]);
@ -78,167 +101,180 @@ function FunnelGraph(props) {
const resetActiveSatges = () => {
props.setActiveStages([], liveFilters, funnelId, true);
}
};
const renderDropLabel = ({ x, y, width, value }) => {
if (value === 0) return;
return (
<text fill="#cc0000" x={x + width / 2} y={y - 5} textAnchor="middle" fontSize="12">{value}</text>
)
}
<text fill="#cc0000" x={x + width / 2} y={y - 5} textAnchor="middle" fontSize="12">
{value}
</text>
);
};
const renderMainLabel = ({ x, y, width, value }) => {
const renderMainLabel = ({ x, y, width, value }) => {
return (
<text fill="#FFFFFF" x={x + width / 2} y={y + 14} textAnchor="middle" fontSize="12">{numberWithCommas(value)}</text>
)
}
<text fill="#FFFFFF" x={x + width / 2} y={y + 14} textAnchor="middle" fontSize="12">
{numberWithCommas(value)}
</text>
);
};
const CustomBar = (props) => {
const { fill, x, y, width, height, sessionsCount, index, dropDueToIssues } = props;
const yp = sessionsCount < MIN_BAR_HEIGHT ? (MIN_BAR_HEIGHT - 1): dropDueToIssues
const CustomBar = (props) => {
const { fill, x, y, width, height, sessionsCount, index, dropDueToIssues } = props;
const yp = sessionsCount < MIN_BAR_HEIGHT ? MIN_BAR_HEIGHT - 1 : dropDueToIssues;
const tmp = (height <= 20 ? 20 : height) - (TEMP[index].height > 20 ? 0 : TEMP[index].height);
return (
<svg >
<svg>
<rect x={x} y={y} width={width} height={tmp} fill={fill} />
</svg>
)
}
const MainBar = (props) => {
const { fill, x, y, width, height, sessionsCount, index, dropDueToIssues, hasSelection = false } = props;
const yp = sessionsCount < MIN_BAR_HEIGHT ? (MIN_BAR_HEIGHT - 1): dropDueToIssues
);
};
const MainBar = (props) => {
const {
fill,
x,
y,
width,
height,
sessionsCount,
index,
dropDueToIssues,
hasSelection = false,
} = props;
const yp = sessionsCount < MIN_BAR_HEIGHT ? MIN_BAR_HEIGHT - 1 : dropDueToIssues;
TEMP[index] = {height,y};
TEMP[index] = { height, y };
return (
<svg style={{ cursor: hasSelection ? '' : 'pointer' }}>
<rect x={x} y={y} width={width} height={height} fill={fill} />
</svg>
)
}
);
};
const renderDropPct = (props) => { // TODO
const renderDropPct = (props) => {
// TODO
const { fill, x, y, width, height, value, totalBars } = props;
const barW = x + ((730 / totalBars) / 2);
const barW = x + 730 / totalBars / 2;
return (
<svg >
<rect x={barW} y={80} width={width} height={20} fill='red' />
<svg>
<rect x={barW} y={80} width={width} height={20} fill="red" />
</svg>
)
}
);
};
const CustomTooltip = (props) => {
const { payload } = props;
if (payload.length === 0) return null;
const { value, headerText } = payload[0].payload;
// const value = payload[0].payload.value;
if (!value) return null;
return (
<div className="rounded border bg-white p-2">
<div>{headerText}</div>
{value.map(i => (
{value.map((i) => (
<div className="text-sm ml-2">{truncate(i, 30)}</div>
))}
</div>
)
);
};
// const CustomTooltip = ({ active, payload, msg = '' }) => {
// const CustomTooltip = ({ active, payload, msg = '' }) => {
// return (
// <div className="rounded border bg-white p-2">
// <div className="rounded border bg-white p-2">
// <p className="text-sm">{msg}</p>
// </div>
// );
// };
const TEMP = {}
const TEMP = {};
return (
<div className="relative">
{ activeStages.length === 2 && (
{activeStages.length === 2 && (
<div
className="absolute right-0 top-0 cursor-pointer z-10"
style={{marginRight: '60px', marginTop: '0' }}
className="absolute right-0 top-0 cursor-pointer z-10"
style={{ marginRight: '60px', marginTop: '0' }}
onClick={resetActiveSatges}
>
<Popup
content={ `Reset Selection` }
>
<AppTooltip title={`Reset Selection`}>
<Icon name="sync-alt" size="15" color="teal" />
</Popup>
</AppTooltip>
</div>
)}
<BarChart width={800} height={190} data={data}
margin={{top: 20, right: 20, left: 0, bottom: 0}}
)}
<BarChart
width={800}
height={190}
data={data}
margin={{ top: 20, right: 20, left: 0, bottom: 0 }}
background={'transparent'}
>
<CartesianGrid strokeDasharray="1 3" stroke="#BBB" vertical={false} />
{/* {activeStages.length < 2 && <Tooltip cursor={{ fill: 'transparent' }} content={<CustomTooltip msg={activeStages.length > 0 ? 'Select one more event.' : 'Select any two events to analyze in depth.'} />} />} */}
<Tooltip cursor={{ fill: 'transparent' }} content={CustomTooltip} />
<CartesianGrid strokeDasharray="1 3" stroke="#BBB" vertical={false} />
{/* {activeStages.length < 2 && <Tooltip cursor={{ fill: 'transparent' }} content={<CustomTooltip msg={activeStages.length > 0 ? 'Select one more event.' : 'Select any two events to analyze in depth.'} />} />} */}
<Tooltip cursor={{ fill: 'transparent' }} content={CustomTooltip} />
<Bar
dataKey="sessionsCount"
onClick={handleClick}
maxBarSize={80}
stackId="a"
shape={<MainBar hasSelection={activeStages.length === 2} />}
cursor="pointer"
minPointSize={MIN_BAR_HEIGHT}
background={false}
>
<LabelList dataKey="sessionsCount" content={renderMainLabel} />
{
data.map((entry, index) => {
const selected = activeStages.includes(index) || (index > activeStages[0] && index < activeStages[1]);
const opacity = activeStages.length > 0 && !selected ? 0.4 : 1;
return (
<Cell
cursor="pointer"
fill={selected ? '#394EFF' : (opacity === 1 ? '#3EAAAF' : '#CCC') }
key={`cell-${index}`}
/>
)
})
}
</Bar>
<Bar
hide={activeStages.length !== 2}
dataKey="dropDueToIssues"
onClick={handleClick}
maxBarSize={80}
stackId="a"
shape={<CustomBar />}
minPointSize={MIN_BAR_HEIGHT}
>
<LabelList dataKey="dropDueToIssues" content={renderDropLabel} />
{
data.map((entry, index) => {
const selected = activeStages.includes(index) || (index > activeStages[0] && index < activeStages[1]);
const opacity = activeStages.length > 0 && !selected ? 0.4 : 1;
return (
<Cell
opacity={opacity}
cursor="pointer"
fill={ activeStages[1] === index ? '#cc000040' : 'transparent' }
key={`cell-${index}`}
/>
)
})
}
</Bar>
<XAxis
stroke={0}
dataKey="label"
strokeWidth={0}
interval={0}
// tick ={{ fill: '#666', fontSize: 12 }}
tick={<CustomTick />}
xAxisId={0}
/>
{/* <XAxis
<Bar
dataKey="sessionsCount"
onClick={handleClick}
maxBarSize={80}
stackId="a"
shape={<MainBar hasSelection={activeStages.length === 2} />}
cursor="pointer"
minPointSize={MIN_BAR_HEIGHT}
background={false}
>
<LabelList dataKey="sessionsCount" content={renderMainLabel} />
{data.map((entry, index) => {
const selected =
activeStages.includes(index) || (index > activeStages[0] && index < activeStages[1]);
const opacity = activeStages.length > 0 && !selected ? 0.4 : 1;
return (
<Cell
cursor="pointer"
fill={selected ? '#394EFF' : opacity === 1 ? '#3EAAAF' : '#CCC'}
key={`cell-${index}`}
/>
);
})}
</Bar>
<Bar
hide={activeStages.length !== 2}
dataKey="dropDueToIssues"
onClick={handleClick}
maxBarSize={80}
stackId="a"
shape={<CustomBar />}
minPointSize={MIN_BAR_HEIGHT}
>
<LabelList dataKey="dropDueToIssues" content={renderDropLabel} />
{data.map((entry, index) => {
const selected =
activeStages.includes(index) || (index > activeStages[0] && index < activeStages[1]);
const opacity = activeStages.length > 0 && !selected ? 0.4 : 1;
return (
<Cell
opacity={opacity}
cursor="pointer"
fill={activeStages[1] === index ? '#cc000040' : 'transparent'}
key={`cell-${index}`}
/>
);
})}
</Bar>
<XAxis
stroke={0}
dataKey="label"
strokeWidth={0}
interval={0}
// tick ={{ fill: '#666', fontSize: 12 }}
tick={<CustomTick />}
xAxisId={0}
/>
{/* <XAxis
stroke={0}
xAxisId={1}
dataKey="value"
@ -248,13 +284,21 @@ function FunnelGraph(props) {
tick ={{ fill: '#666', fontSize: 12 }}
tickFormatter={val => '"' + val + '"'}
/> */}
<YAxis interval={ 0 } strokeWidth={0} tick ={{ fill: '#999999', fontSize: 11 }} tickFormatter={val => Styles.tickFormatter(val)} />
</BarChart>
</div>
)
<YAxis
interval={0}
strokeWidth={0}
tick={{ fill: '#999999', fontSize: 11 }}
tickFormatter={(val) => Styles.tickFormatter(val)}
/>
</BarChart>
</div>
);
}
export default connect(state => ({
activeStages: state.getIn(['funnels', 'activeStages']).toJS(),
liveFilters: state.getIn(['funnelFilters', 'appliedFilter']),
}), { setActiveStages })(FunnelGraph)
export default connect(
(state) => ({
activeStages: state.getIn(['funnels', 'activeStages']).toJS(),
liveFilters: state.getIn(['funnelFilters', 'appliedFilter']),
}),
{ setActiveStages }
)(FunnelGraph);

View file

@ -1,7 +1,13 @@
import React, { useEffect, useState } from 'react';
import { Icon, BackLink, IconButton, Dropdown, Popup, TextEllipsis, Button } from 'UI';
import { remove as deleteFunnel, fetch, fetchInsights, fetchIssuesFiltered, fetchSessionsFiltered } from 'Duck/funnels';
import { editFilter, editFunnelFilter, refresh, addFilter } from 'Duck/funnels';
import React, { useState } from 'react';
import { Icon, BackLink, IconButton, Dropdown, Tooltip, TextEllipsis, Button } from 'UI';
import {
remove as deleteFunnel,
fetch,
fetchInsights,
fetchIssuesFiltered,
fetchSessionsFiltered,
} from 'Duck/funnels';
import { editFilter, editFunnelFilter, refresh } from 'Duck/funnels';
import DateRange from 'Shared/DateRange';
import { connect } from 'react-redux';
import { confirm } from 'UI';
@ -10,84 +16,107 @@ import stl from './funnelHeader.module.css';
const Info = ({ label = '', value = '', className = 'mx-4' }) => {
return (
<div className={className}>
<div className={className}>
<span className="color-gray-medium">{label}</span>
<span className="font-medium ml-2">{value}</span>
<span className="font-medium ml-2">{value}</span>
</div>
)
}
);
};
const FunnelHeader = (props) => {
const { funnel, insights, funnels, onBack, funnelId, showFilters = false, funnelFilters, renameHandler } = props;
const [showSaveModal, setShowSaveModal] = useState(false)
const {
funnel,
insights,
funnels,
onBack,
funnelId,
showFilters = false,
funnelFilters,
renameHandler,
} = props;
const [showSaveModal, setShowSaveModal] = useState(false);
const writeOption = (e, { name, value }) => {
props.redirect(value)
props.fetch(value).then(() => props.refresh(value))
}
props.redirect(value);
props.fetch(value).then(() => props.refresh(value));
};
const deleteFunnel = async (e, funnel) => {
e.preventDefault();
e.stopPropagation();
if (await confirm({
if (
await confirm({
header: 'Delete Funnel',
confirmButton: 'Delete',
confirmation: `Are you sure you want to permanently delete "${funnel.name}"?`
})) {
props.deleteFunnel(funnel.funnelId).then(props.onBack);
} else {}
}
confirmation: `Are you sure you want to permanently delete "${funnel.name}"?`,
})
) {
props.deleteFunnel(funnel.funnelId).then(props.onBack);
} else {
}
};
const onDateChange = (e) => {
props.editFunnelFilter(e, funnelId);
}
};
const options = funnels.map(({ funnelId, name }) => ({ text: name, value: funnelId })).toJS();
const selectedFunnel = funnels.filter(i => i.funnelId === parseInt(funnelId)).first() || {};
const eventsCount = funnel.filter.filters.filter(i => i.isEvent).size;
const selectedFunnel = funnels.filter((i) => i.funnelId === parseInt(funnelId)).first() || {};
const eventsCount = funnel.filter.filters.filter((i) => i.isEvent).size;
return (
<div>
<div className="bg-white border rounded flex items-center w-full relative group pr-2">
<BackLink onClick={onBack} vertical className="absolute" style={{ left: '-50px', top: '8px' }} />
<FunnelSaveModal
show={showSaveModal}
closeHandler={() => setShowSaveModal(false)}
/>
<BackLink
onClick={onBack}
vertical
className="absolute"
style={{ left: '-50px', top: '8px' }}
/>
<FunnelSaveModal show={showSaveModal} closeHandler={() => setShowSaveModal(false)} />
<div className="flex items-center mr-auto relative">
<Dropdown
<Dropdown
scrolling
trigger={
<div className="text-xl capitalize font-medium" style={{ maxWidth: '300px', overflow: 'hidden'}}>
<div
className="text-xl capitalize font-medium"
style={{ maxWidth: '300px', overflow: 'hidden' }}
>
<TextEllipsis text={selectedFunnel.name} />
</div>
}
}
options={options}
className={ stl.dropdown }
className={stl.dropdown}
name="funnel"
value={ parseInt(funnelId) }
value={parseInt(funnelId)}
// icon={null}
onChange={ writeOption }
onChange={writeOption}
selectOnBlur={false}
icon={ <Icon name="chevron-down" color="gray-dark" size="14" className={stl.dropdownIcon} /> }
icon={
<Icon name="chevron-down" color="gray-dark" size="14" className={stl.dropdownIcon} />
}
/>
<Info label="Events" value={eventsCount} />
<span>-</span>
<Button variant="text-primary" onClick={props.toggleFilters}>{ showFilters ? 'HIDE' : 'EDIT FUNNEL' }</Button>
<Info label="Sessions" value={insights.sessionsCount} />
<Button variant="text-primary" onClick={props.toggleFilters}>
{showFilters ? 'HIDE' : 'EDIT FUNNEL'}
</Button>
<Info label="Sessions" value={insights.sessionsCount} />
<Info label="Conversion" value={`${insights.conversions}%`} />
</div>
<div className="flex items-center">
<div className="flex items-center">
<div className="flex items-center invisible group-hover:visible">
<Popup
content={ `Edit Funnel` }
>
<IconButton icon="edit" onClick={() => setShowSaveModal(true)} />
</Popup>
<Popup content={ `Remove Funnel` } >
<IconButton icon="trash" onClick={(e) => deleteFunnel(e, funnel)} className="ml-2 mr-2" />
</Popup>
<Tooltip title={`Edit Funnel`}>
<IconButton icon="edit" onClick={() => setShowSaveModal(true)} />
</Tooltip>
<Tooltip title={`Remove Funnel`}>
<IconButton
icon="trash"
onClick={(e) => deleteFunnel(e, funnel)}
className="ml-2 mr-2"
/>
</Tooltip>
</div>
<DateRange
rangeValue={funnelFilters.rangeValue}
@ -99,10 +128,22 @@ const FunnelHeader = (props) => {
</div>
</div>
</div>
)
}
);
};
export default connect(state => ({
funnelFilters: state.getIn([ 'funnels', 'funnelFilters' ]).toJS(),
funnel: state.getIn([ 'funnels', 'instance' ]),
}), { editFilter, editFunnelFilter, deleteFunnel, fetch, fetchInsights, fetchIssuesFiltered, fetchSessionsFiltered, refresh })(FunnelHeader)
export default connect(
(state) => ({
funnelFilters: state.getIn(['funnels', 'funnelFilters']).toJS(),
funnel: state.getIn(['funnels', 'instance']),
}),
{
editFilter,
editFunnelFilter,
deleteFunnel,
fetch,
fetchInsights,
fetchIssuesFiltered,
fetchSessionsFiltered,
refresh,
}
)(FunnelHeader);

View file

@ -1,7 +1,7 @@
import React from 'react'
import cn from 'classnames'
import stl from './funnelMenuItem.module.css'
import { Icon, ItemMenu, Popup } from 'UI'
import React from 'react';
import cn from 'classnames';
import stl from './funnelMenuItem.module.css';
import { Icon, Tooltip } from 'UI';
function FunnelMenuItem({
iconName = 'info',
@ -14,31 +14,34 @@ function FunnelMenuItem({
}) {
return (
<div
className={ cn(
className,
stl.menuItem,
"flex items-center py-1 justify-between group",
{ [stl.active] : active }
)}
className={cn(className, stl.menuItem, 'flex items-center py-1 justify-between group', {
[stl.active]: active,
})}
onClick={disabled ? null : onClick}
>
<div className={ cn(stl.iconLabel, 'flex items-center', { [stl.disabled] : disabled })}>
<div className={cn(stl.iconLabel, 'flex items-center', { [stl.disabled]: disabled })}>
<div className="flex items-center justify-center w-8 h-8 flex-shrink-0">
<Icon name={ iconName } size={ 16 } color={'gray-dark'} className="absolute" />
<Icon name={iconName} size={16} color={'gray-dark'} className="absolute" />
</div>
<span className={cn(stl.title, 'cap-first')}>{ title }</span>
<span className={cn(stl.title, 'cap-first')}>{title}</span>
</div>
<div className="flex items-center">
<div className={cn("mx-2", { 'invisible': !isPublic })}>
<Popup content={ `Shared with team` } >
<div className={cn("bg-gray-light h-8 w-8 rounded-full flex items-center justify-center", stl.teamIcon)} style={{ opacity: '0.3'}}>
<Icon name="user-friends" color="gray-dark" size={16} />
</div>
</Popup>
</div>
</div>
<div className={cn('mx-2', { invisible: !isPublic })}>
<Tooltip title={`Shared with team`}>
<div
className={cn(
'bg-gray-light h-8 w-8 rounded-full flex items-center justify-center',
stl.teamIcon
)}
style={{ opacity: '0.3' }}
>
<Icon name="user-friends" color="gray-dark" size={16} />
</div>
</Tooltip>
</div>
</div>
</div>
)
);
}
export default FunnelMenuItem
export default FunnelMenuItem;

View file

@ -1,30 +1,45 @@
import React from 'react'
import { Popup } from 'UI'
import React from 'react';
import { Tooltip } from 'UI';
function IssueGraph({ issue }) {
return (
<div className="flex rounded-sm" style={{ width: '600px' }}>
<Popup content={ `Unaffected sessions` } >
<div style={{ width: issue.unaffectedSessionsPer + '%' }} className="relative">
<div className="w-full relative rounded-tl-sm rounded-bl-sm" style={{ height: '18px', backgroundColor: 'rgba(217, 219, 238, 0.7)' }} />
<div className="absolute ml-2 font-bold top-0 bottom-0 text-sm">{issue.unaffectedSessions}</div>
<div className="flex rounded-sm" style={{ width: '600px' }}>
<Tooltip title={`Unaffected sessions`}>
<div style={{ width: issue.unaffectedSessionsPer + '%' }} className="relative">
<div
className="w-full relative rounded-tl-sm rounded-bl-sm"
style={{ height: '18px', backgroundColor: 'rgba(217, 219, 238, 0.7)' }}
/>
<div className="absolute ml-2 font-bold top-0 bottom-0 text-sm">
{issue.unaffectedSessions}
</div>
</Popup>
<Popup content={ `Affected sessions` } >
<div style={{ width: issue.affectedSessionsPer + '%'}} className="border-l relative">
<div className="w-full relative" style={{ height: '18px', backgroundColor: 'rgba(238, 238, 238, 0.7)' }} />
<div className="absolute ml-2 font-bold top-0 bottom-0 text-sm">{issue.affectedSessions}</div>
{/* <div className="absolute left-0 ml-1 text-xs">{issue.affectedSessionsPer}</div> */}
</div>
</Tooltip>
<Tooltip title={`Affected sessions`}>
<div style={{ width: issue.affectedSessionsPer + '%' }} className="border-l relative">
<div
className="w-full relative"
style={{ height: '18px', backgroundColor: 'rgba(238, 238, 238, 0.7)' }}
/>
<div className="absolute ml-2 font-bold top-0 bottom-0 text-sm">
{issue.affectedSessions}
</div>
</Popup>
<Popup content={ `Conversion lost` } >
<div style={{ width: issue.lostConversionsPer + '%'}} className="border-l relative">
<div className="w-full relative rounded-tr-sm rounded-br-sm" style={{ height: '18px', backgroundColor: 'rgba(204, 0, 0, 0.26)' }} />
<div className="absolute ml-2 font-bold top-0 bottom-0 text-sm color-red">{issue.lostConversions}</div>
{/* <div className="absolute left-0 ml-1 text-xs">{issue.affectedSessionsPer}</div> */}
</div>
</Tooltip>
<Tooltip title={`Conversion lost`}>
<div style={{ width: issue.lostConversionsPer + '%' }} className="border-l relative">
<div
className="w-full relative rounded-tr-sm rounded-br-sm"
style={{ height: '18px', backgroundColor: 'rgba(204, 0, 0, 0.26)' }}
/>
<div className="absolute ml-2 font-bold top-0 bottom-0 text-sm color-red">
{issue.lostConversions}
</div>
</Popup>
</div>
</Tooltip>
</div>
)
);
}
export default IssueGraph
export default IssueGraph;

View file

@ -4,7 +4,7 @@ import { NavLink, withRouter } from 'react-router-dom';
import cn from 'classnames';
import { client, CLIENT_DEFAULT_TAB } from 'App/routes';
import { logout } from 'Duck/user';
import { Icon, Popup } from 'UI';
import { Icon, Tooltip } from 'UI';
import styles from './header.module.css';
import OnboardingExplore from './OnboardingExplore/OnboardingExplore';
import Notifications from '../Alerts/Notifications';
@ -68,7 +68,7 @@ const Header = (props) => {
<Notifications />
<div className={cn(styles.userDetails, 'group cursor-pointer')}>
<Popup content={`Preferences`} disabled>
<Tooltip title={`Preferences`} disabled>
<div className="flex items-center">
<NavLink to={CLIENT_PATH}>
<Icon name="gear" size="20" color="gray-dark" />
@ -76,7 +76,7 @@ const Header = (props) => {
<SettingsMenu className="invisible group-hover:visible" account={account} />
</div>
</Popup>
</Tooltip>
</div>
<div className={cn(styles.userDetails, 'group cursor-pointer')}>

View file

@ -1,6 +1,6 @@
import { observer } from 'mobx-react-lite';
import { useState, useCallback } from 'react';
import { Popup, SlideModal } from 'UI';
import { Tooltip, SlideModal } from 'UI';
import { NETWORK } from 'Player/ios/state';
@ -24,13 +24,13 @@ const COLUMNS = [
label: 'url',
width: 130,
render: (r) => (
<Popup
content={<div className={cls.popupNameContent}>{r.url}</div>}
<Tooltip
title={<div className={cls.popupNameContent}>{r.url}</div>}
size="mini"
position="right center"
>
<div className={cls.popupNameTrigger}>{r.url}</div>
</Popup>
</Tooltip>
),
},
{

View file

@ -1,7 +1,7 @@
import React from 'react';
import { useCallback } from 'react';
import cn from 'classnames';
import { Popup } from 'UI';
import { Tooltip } from 'UI';
import { CRASHES, EVENTS } from 'Player/ios/state';
import TimeTracker from './TimeTracker';
import PlayerTime from './PlayerTime';
@ -28,12 +28,10 @@ export default function Timeline({ player }) {
<div key={e.key} className={cls.event} style={{ left: `${e.time * scale}%` }} />
))}
{player.lists[CRASHES].list.map((e) => (
<Popup
<Tooltip
key={e.key}
offset="-19"
pinned
className="error"
content={
title={
<div className={cls.popup}>
<b>{`Crash ${e.name}:`}</b>
<br />
@ -46,7 +44,7 @@ export default function Timeline({ player }) {
className={cn(cls.markup, cls.error)}
style={{ left: `${e.time * scale}%` }}
/>
</Popup>
</Tooltip>
))}
</div>
<PlayerTime player={player} timeKey="endTime" />

View file

@ -1,29 +1,32 @@
import React from 'react';
import cn from 'classnames';
import { Popup } from 'UI';
import { Tooltip } from 'UI';
import stl from './issueListItem.module.css';
const IssueListItem = ({ issue, onClick, icon, user, active }) => {
return (
<div
onClick={ () => onClick(issue) }
className={ cn(stl.wrapper, active ? 'active-bg' : '', 'flex flex-col justify-between cursor-pointer text-base text-gray-800')}
onClick={() => onClick(issue)}
className={cn(
stl.wrapper,
active ? 'active-bg' : '',
'flex flex-col justify-between cursor-pointer text-base text-gray-800'
)}
>
<div className="flex items-center justify-between">
<div className="flex items-center">
{ icon }
{/* <img src={ icon } width="16" height="16" className="mr-3" /> */}
<span>{ issue.id }</span>
{icon}
<span>{issue.id}</span>
</div>
<div className="flex items-center">
{ user &&
<Popup content={ 'Assignee ' + user.name } >
<img src={ user.avatarUrls['24x24'] } width="24" height="24" />
</Popup>
}
{user && (
<Tooltip title={'Assignee ' + user.name}>
<img src={user.avatarUrls['24x24']} width="24" height="24" />
</Tooltip>
)}
</div>
</div>
<div className={ stl.title }>{ issue.title }</div>
<div className={stl.title}>{issue.title}</div>
</div>
);
};

View file

@ -1,7 +1,7 @@
import React from 'react';
import cn from 'classnames';
import { connectPlayer, jump, pause } from 'Player';
import { Popup, Button, TextEllipsis } from 'UI';
import { Tooltip, Button, TextEllipsis } from 'UI';
import { getRE } from 'App/utils';
import { TYPES } from 'Types/session/resource';
import stl from './network.module.css';
@ -29,12 +29,12 @@ const TAB_TO_TYPE_MAP = {
export function renderName(r) {
return (
<div className="flex justify-between items-center grow-0 w-full">
<Popup
<Tooltip
style={{ maxWidth: '75%' }}
content={<div className={stl.popupNameContent}>{r.url}</div>}
title={<div className={stl.popupNameContent}>{r.url}</div>}
>
<TextEllipsis>{r.name}</TextEllipsis>
</Popup>
</Tooltip>
</div>
);
}
@ -56,9 +56,9 @@ export function renderDuration(r) {
}
return (
<Popup content={tooltipText}>
<Tooltip title={tooltipText}>
<div className={cn(className, stl.duration)}> {text} </div>
</Popup>
</Tooltip>
);
}

View file

@ -1,7 +1,7 @@
import React from 'react';
import cn from 'classnames';
// import { connectPlayer } from 'Player';
import { QuestionMarkHint, Popup, Tabs, Input, NoContent, Icon, Toggler, Button } from 'UI';
import { QuestionMarkHint, Tooltip, Tabs, Input, NoContent, Icon, Toggler, Button } from 'UI';
import { getRE } from 'App/utils';
import { TYPES } from 'Types/session/resource';
import { formatBytes } from 'App/utils';
@ -40,17 +40,17 @@ const LOAD_TIME_COLOR = 'red';
export function renderType(r) {
return (
<Popup style={{ width: '100%' }} content={<div className={stl.popupNameContent}>{r.type}</div>}>
<Tooltip style={{ width: '100%' }} title={<div className={stl.popupNameContent}>{r.type}</div>}>
<div className={stl.popupNameTrigger}>{r.type}</div>
</Popup>
</Tooltip>
);
}
export function renderName(r) {
return (
<Popup style={{ width: '100%' }} content={<div className={stl.popupNameContent}>{r.url}</div>}>
<Tooltip style={{ width: '100%' }} title={<div className={stl.popupNameContent}>{r.url}</div>}>
<div className={stl.popupNameTrigger}>{r.name}</div>
</Popup>
</Tooltip>
);
}
@ -76,7 +76,6 @@ const renderXHRText = () => (
<span className="flex items-center">
{XHR}
<QuestionMarkHint
onHover={true}
content={
<>
Use our{' '}
@ -129,9 +128,9 @@ function renderSize(r) {
}
return (
<Popup style={{ width: '100%' }} content={content}>
<Tooltip style={{ width: '100%' }} content={content}>
<div>{triggerText}</div>
</Popup>
</Tooltip>
);
}
@ -152,9 +151,9 @@ export function renderDuration(r) {
}
return (
<Popup style={{ width: '100%' }} content={tooltipText}>
<Tooltip style={{ width: '100%' }} content={tooltipText}>
<div className={cn(className, stl.duration)}> {text} </div>
</Popup>
</Tooltip>
);
}
@ -244,7 +243,7 @@ export default class NetworkContent extends React.PureComponent {
<BottomBlock.Content>
<div className="flex items-center justify-between px-4">
<div>
<Toggler checked={true} name="test" onChange={() => {}} label="4xx-5xx Only" />
<Toggler checked={true} name="test" onChange={() => {}} label="4xx-5xx Only" />
</div>
<InfoLine>
<InfoLine.Point label={filtered.length} value=" requests" />

View file

@ -1,66 +1,87 @@
import React from 'react';
import cn from 'classnames';
import { getTimelinePosition } from 'App/utils';
import { Icon, Popup } from 'UI';
import { Icon, Tooltip } from 'UI';
import PerformanceGraph from '../PerformanceGraph';
interface Props {
list?: any[];
title: string;
message?: string;
className?: string;
endTime?: number;
renderElement?: (item: any) => React.ReactNode;
isGraph?: boolean;
zIndex?: number;
noMargin?: boolean;
list?: any[];
title: string;
message?: string;
className?: string;
endTime?: number;
renderElement?: (item: any) => React.ReactNode;
isGraph?: boolean;
zIndex?: number;
noMargin?: boolean;
}
const EventRow = React.memo((props: Props) => {
const { title, className, list = [], endTime = 0, isGraph = false, message = '' } = props;
const scale = 100 / endTime;
const _list =
!isGraph &&
React.useMemo(() => {
return list.map((item: any, _index: number) => {
const spread = item.toJS ? { ...item.toJS() } : { ...item }
return {
...spread,
left: getTimelinePosition(item.time, scale),
};
});
}, [list]);
const { title, className, list = [], endTime = 0, isGraph = false, message = '' } = props;
const scale = 100 / endTime;
const _list =
!isGraph &&
React.useMemo(() => {
return list.map((item: any, _index: number) => {
const spread = item.toJS ? { ...item.toJS() } : { ...item };
return {
...spread,
left: getTimelinePosition(item.time, scale),
};
});
}, [list]);
return (
<div className={cn('w-full flex flex-col py-2', className)} style={{ height: isGraph ? 60 : 50 }}>
<div className={cn("uppercase color-gray-medium text-sm flex items-center py-1", props.noMargin ? '' : 'ml-4' )}>
<div style={{ zIndex: props.zIndex ? props.zIndex : undefined }} className="mr-2 leading-none">{title}</div>
{message ? <RowInfo zIndex={props.zIndex} message={message} /> : null}
</div>
<div className="relative w-full" style={{ zIndex: props.zIndex ? props.zIndex : undefined }}>
{isGraph ? (
<PerformanceGraph list={list} />
) : (
_list.length > 0 ? _list.map((item: any, index: number) => {
return (
<div key={index} className="absolute" style={{ left: `clamp(0%, calc(${item.left}% - 7px), calc(100% - 14px))`, zIndex: props.zIndex ? props.zIndex : undefined }}>
{props.renderElement ? props.renderElement(item) : null}
</div>
);
}) : (
<div className={cn("color-gray-medium text-sm pt-2", props.noMargin ? '' : 'ml-4')}>None captured.</div>
)
)}
</div>
return (
<div
className={cn('w-full flex flex-col py-2', className)}
style={{ height: isGraph ? 60 : 50 }}
>
<div
className={cn(
'uppercase color-gray-medium text-sm flex items-center py-1',
props.noMargin ? '' : 'ml-4'
)}
>
<div
style={{ zIndex: props.zIndex ? props.zIndex : undefined }}
className="mr-2 leading-none"
>
{title}
</div>
);
{message ? <RowInfo zIndex={props.zIndex} message={message} /> : null}
</div>
<div className="relative w-full" style={{ zIndex: props.zIndex ? props.zIndex : undefined }}>
{isGraph ? (
<PerformanceGraph list={list} />
) : _list.length > 0 ? (
_list.map((item: any, index: number) => {
return (
<div
key={index}
className="absolute"
style={{
left: `clamp(0%, calc(${item.left}% - 7px), calc(100% - 14px))`,
zIndex: props.zIndex ? props.zIndex : undefined,
}}
>
{props.renderElement ? props.renderElement(item) : null}
</div>
);
})
) : (
<div className={cn('color-gray-medium text-sm pt-2', props.noMargin ? '' : 'ml-4')}>
None captured.
</div>
)}
</div>
</div>
);
});
export default EventRow;
function RowInfo({ message, zIndex } : any) {
return (
<Popup content={message} delay={0} style={{ zIndex: zIndex ? zIndex : undefined }}>
<Icon name="info-circle" color="gray-medium"/>
</Popup>
)
function RowInfo({ message, zIndex }: any) {
return (
<Tooltip title={message} delay={0} style={{ zIndex: zIndex ? zIndex : undefined }}>
<Icon name="info-circle" color="gray-medium" />
</Tooltip>
);
}

View file

@ -1,5 +1,5 @@
import React from 'react';
import { Checkbox, Popup } from 'UI';
import { Checkbox, Tooltip } from 'UI';
const NETWORK = 'NETWORK';
const ERRORS = 'ERRORS';
@ -8,48 +8,48 @@ const CLICKRAGE = 'CLICKRAGE';
const PERFORMANCE = 'PERFORMANCE';
export const HELP_MESSAGE: any = {
NETWORK: 'Network requests made in this session',
EVENTS: 'Visualizes the events that takes place in the DOM',
ERRORS: 'Visualizes native JS errors like Type, URI, Syntax etc.',
CLICKRAGE: 'Indicates user frustration when repeated clicks are recorded',
PERFORMANCE: 'Summary of this sessions memory, and CPU consumption on the timeline',
}
NETWORK: 'Network requests made in this session',
EVENTS: 'Visualizes the events that takes place in the DOM',
ERRORS: 'Visualizes native JS errors like Type, URI, Syntax etc.',
CLICKRAGE: 'Indicates user frustration when repeated clicks are recorded',
PERFORMANCE: 'Summary of this sessions memory, and CPU consumption on the timeline',
};
interface Props {
list: any[];
updateList: any;
list: any[];
updateList: any;
}
function FeatureSelection(props: Props) {
const { list } = props;
const features = [NETWORK, ERRORS, EVENTS, CLICKRAGE, PERFORMANCE];
const disabled = list.length >= 3;
const { list } = props;
const features = [NETWORK, ERRORS, EVENTS, CLICKRAGE, PERFORMANCE];
const disabled = list.length >= 3;
return (
<React.Fragment>
{features.map((feature, index) => {
const checked = list.includes(feature);
const _disabled = disabled && !checked;
return (
<Popup content="X-RAY supports up to 3 views" disabled={!_disabled} delay={0}>
<Checkbox
key={index}
label={feature}
checked={checked}
className="mx-4"
disabled={_disabled}
onClick={() => {
if (checked) {
props.updateList(list.filter((item: any) => item !== feature));
} else {
props.updateList([...list, feature]);
}
}}
/>
</Popup>
);
})}
</React.Fragment>
);
return (
<React.Fragment>
{features.map((feature, index) => {
const checked = list.includes(feature);
const _disabled = disabled && !checked;
return (
<Tooltip title="X-RAY supports up to 3 views" disabled={!_disabled} delay={0}>
<Checkbox
key={index}
label={feature}
checked={checked}
className="mx-4"
disabled={_disabled}
onClick={() => {
if (checked) {
props.updateList(list.filter((item: any) => item !== feature));
} else {
props.updateList([...list, feature]);
}
}}
/>
</Tooltip>
);
})}
</React.Fragment>
);
}
export default FeatureSelection;

View file

@ -1,10 +1,8 @@
import React from 'react';
import { connectPlayer, Controls } from 'App/player';
import { toggleBottomBlock, NETWORK, EXCEPTIONS, PERFORMANCE } from 'Duck/components/player';
import { Controls } from 'App/player';
import { NETWORK, EXCEPTIONS } from 'Duck/components/player';
import { useModal } from 'App/components/Modal';
import { Icon, ErrorDetails, Popup } from 'UI';
import { Tooltip } from 'react-tippy';
import { TYPES as EVENT_TYPES } from 'Types/session/event';
import { Icon, Tooltip } from 'UI';
import StackEventModal from '../StackEventModal';
import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorDetailsModal';
import FetchDetails from 'Shared/FetchDetailsModal';
@ -46,54 +44,58 @@ const TimelinePointer = React.memo((props: Props) => {
const renderNetworkElement = (item: any) => {
const name = item.name || '';
return (
<Popup
content={
<Tooltip
title={
<div className="">
<b>{item.success ? 'Slow resource: ' : 'Missing resource:'}</b>
<br />
{name.length > 200 ? name.slice(0, 100) + ' ... ' + name.slice(-50) : name.length > 200 ? (item.name.slice(0, 100) + ' ... ' + item.name.slice(-50)) : item.name}
{name.length > 200
? name.slice(0, 100) + ' ... ' + name.slice(-50)
: name.length > 200
? item.name.slice(0, 100) + ' ... ' + item.name.slice(-50)
: item.name}
</div>
}
delay={0}
position="top"
placement="top"
>
<div onClick={createEventClickHandler(item, NETWORK)} className="cursor-pointer">
<div className="h-4 w-4 rounded-full bg-red text-white font-bold flex items-center justify-center text-sm">
<span>!</span>
</div>
</div>
</Popup>
</Tooltip>
);
};
const renderClickRageElement = (item: any) => {
return (
<Popup
content={
<Tooltip
title={
<div className="">
<b>{'Click Rage'}</b>
</div>
}
delay={0}
position="top"
placement="top"
>
<div onClick={createEventClickHandler(item, null)} className="cursor-pointer">
<Icon className="bg-white" name="funnel/emoji-angry" color="red" size="16" />
</div>
</Popup>
</Tooltip>
);
};
const renderStackEventElement = (item: any) => {
return (
<Popup
content={
<Tooltip
title={
<div className="">
<b>{'Stack Event'}</b>
</div>
}
delay={0}
position="top"
placement="top"
>
<div
onClick={createEventClickHandler(item, 'EVENT')}
@ -101,20 +103,20 @@ const TimelinePointer = React.memo((props: Props) => {
>
{/* <Icon className="rounded-full bg-white" name="funnel/exclamation-circle-fill" color="red" size="16" /> */}
</div>
</Popup>
</Tooltip>
);
};
const renderPerformanceElement = (item: any) => {
return (
<Popup
content={
<Tooltip
title={
<div className="">
<b>{item.type}</b>
</div>
}
delay={0}
position="top"
placement="top"
>
<div
onClick={createEventClickHandler(item, EXCEPTIONS)}
@ -122,14 +124,14 @@ const TimelinePointer = React.memo((props: Props) => {
>
{/* <Icon className="rounded-full bg-white" name="funnel/exclamation-circle-fill" color="red" size="16" /> */}
</div>
</Popup>
</Tooltip>
);
};
const renderExceptionElement = (item: any) => {
return (
<Popup
content={
<Tooltip
title={
<div className="">
<b>{'Exception'}</b>
<br />
@ -137,14 +139,14 @@ const TimelinePointer = React.memo((props: Props) => {
</div>
}
delay={0}
position="top"
placement="top"
>
<div onClick={createEventClickHandler(item, 'ERRORS')} className="cursor-pointer">
<div className="h-4 w-4 rounded-full bg-red text-white font-bold flex items-center justify-center text-sm">
<span>!</span>
</div>
<span>!</span>
</div>
</div>
</Popup>
</Tooltip>
);
};

View file

@ -1,25 +1,30 @@
import { Popup } from 'UI';
import { Tooltip } from 'UI';
import { percentOf } from 'App/utils';
import styles from './barRow.module.css'
import styles from './barRow.module.css';
import tableStyles from './timeTable.module.css';
import React from 'react';
const formatTime = time => time < 1000 ? `${time.toFixed(2)}ms` : `${time / 1000}s`;
const formatTime = (time) => (time < 1000 ? `${time.toFixed(2)}ms` : `${time / 1000}s`);
interface Props {
resource: {
time: number
ttfb?: number
duration?: number
key: string
}
popup?: boolean
timestart: number
timewidth: number
time: number;
ttfb?: number;
duration?: number;
key: string;
};
popup?: boolean;
timestart: number;
timewidth: number;
}
// TODO: If request has no duration, set duration to 0.2s. Enforce existence of duration in the future.
const BarRow = ({ resource: { time, ttfb = 0, duration = 200, key }, popup = false, timestart = 0, timewidth }: Props) => {
const BarRow = ({
resource: { time, ttfb = 0, duration = 200, key },
popup = false,
timestart = 0,
timewidth,
}: Props) => {
const timeOffset = time - timestart;
ttfb = ttfb || 0;
const trigger = (
@ -28,7 +33,7 @@ const BarRow = ({ resource: { time, ttfb = 0, duration = 200, key }, popup = fal
style={{
left: `${percentOf(timeOffset, timewidth)}%`,
right: `${100 - percentOf(timeOffset + duration, timewidth)}%`,
minWidth: '5px'
minWidth: '5px',
}}
>
<div
@ -41,23 +46,28 @@ const BarRow = ({ resource: { time, ttfb = 0, duration = 200, key }, popup = fal
className={styles.downloadBar}
style={{
width: `${percentOf(duration - ttfb, duration)}%`,
minWidth: '5px'
minWidth: '5px',
}}
/>
</div>
);
if (!popup) return <div key={key} className={tableStyles.row} > {trigger} </div>;
if (!popup)
return (
<div key={key} className={tableStyles.row}>
{' '}
{trigger}{' '}
</div>
);
return (
<div key={key} className={tableStyles.row} >
<Popup
basic
content={
<div key={key} className={tableStyles.row}>
<Tooltip
title={
<React.Fragment>
{ttfb != null &&
{ttfb != null && (
<div className={styles.popupRow}>
<div className={styles.title}>{'Waiting (TTFB)'}</div>
<div className={styles.popupBarWrapper} >
<div className={styles.popupBarWrapper}>
<div
className={styles.ttfbBar}
style={{
@ -66,11 +76,11 @@ const BarRow = ({ resource: { time, ttfb = 0, duration = 200, key }, popup = fal
}}
/>
</div>
<div className={styles.time} >{formatTime(ttfb)}</div>
<div className={styles.time}>{formatTime(ttfb)}</div>
</div>
}
)}
<div className={styles.popupRow}>
<div className={styles.title} >{'Content Download'}</div>
<div className={styles.title}>{'Content Download'}</div>
<div className={styles.popupBarWrapper}>
<div
className={styles.downloadBar}
@ -86,11 +96,13 @@ const BarRow = ({ resource: { time, ttfb = 0, duration = 200, key }, popup = fal
}
size="mini"
position="top center"
/>
>
{trigger}
</Tooltip>
</div>
);
}
};
BarRow.displayName = "BarRow";
BarRow.displayName = 'BarRow';
export default BarRow;
export default BarRow;

View file

@ -3,25 +3,25 @@ import copy from 'copy-to-clipboard';
import { Tooltip } from 'UI';
const withCopy = (WrappedComponent: React.ComponentType) => {
const ComponentWithCopy = (props: any) => {
const [copied, setCopied] = React.useState(false);
const { value, tooltip } = props;
const copyToClipboard = (text: string) => {
copy(text);
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 1000);
};
return (
<div onClick={() => copyToClipboard(value)} className="w-fit">
<Tooltip title={copied ? tooltip : 'Click to copy'}>
<WrappedComponent {...props} copyToClipboard={copyToClipboard} />
</Tooltip>
</div>
);
const ComponentWithCopy = (props: any) => {
const [copied, setCopied] = React.useState(false);
const { value, tooltip } = props;
const copyToClipboard = (text: string) => {
copy(text);
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 1000);
};
return ComponentWithCopy;
return (
<div onClick={() => copyToClipboard(value)} className="w-fit cursor-pointer">
<Tooltip title={copied ? tooltip : 'Click to copy'} delay={0}>
<WrappedComponent {...props} copyToClipboard={copyToClipboard} />
</Tooltip>
</div>
);
};
return ComponentWithCopy;
};
export default withCopy;

View file

@ -1,14 +1,14 @@
import React, { useEffect, useState } from 'react';
import { Popup, Button, Icon } from 'UI';
import { Tooltip, Button, Icon } from 'UI';
import { toggleFavorite } from 'Duck/sessions';
import { connect } from 'react-redux';
import { toast } from 'react-toastify';
interface Props {
toggleFavorite: (sessionId: string) => Promise<void>;
favorite: Boolean;
favorite: boolean;
sessionId: any;
isEnterprise: Boolean;
isEnterprise: boolean;
noMargin?: boolean;
}
function Bookmark(props: Props) {
@ -37,12 +37,7 @@ function Bookmark(props: Props) {
return (
<div onClick={toggleFavorite} className="w-full">
<Popup
delay={500}
content={isFavorite ? TOOLTIP_TEXT_REMOVE : TOOLTIP_TEXT_ADD}
hideOnClick={true}
distance={20}
>
<Tooltip title={isFavorite ? TOOLTIP_TEXT_REMOVE : TOOLTIP_TEXT_ADD}>
{noMargin ? (
<div className="flex items-center cursor-pointer h-full w-full p-3">
<Icon
@ -62,13 +57,13 @@ function Bookmark(props: Props) {
<span className="ml-2">{isEnterprise ? 'Vault' : 'Bookmark'}</span>
</Button>
)}
</Popup>
</Tooltip>
</div>
);
}
export default connect(
(state) => ({
(state: any) => ({
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee',
favorite: state.getIn(['sessions', 'current', 'favorite']),
}),

View file

@ -1,5 +1,5 @@
import React from 'react';
import { Icon, Popup } from 'UI';
import { Icon, Tooltip } from 'UI';
interface Props {
onClick: any;
@ -8,7 +8,7 @@ interface Props {
function JumpButton(props: Props) {
const { tooltip = '' } = props;
return (
<Popup content={tooltip} disabled={!!tooltip}>
<Tooltip title={tooltip} disabled={!!tooltip}>
<div
className="mr-2 border cursor-pointer invisible group-hover:visible rounded-lg bg-active-blue text-xs flex items-center px-2 py-1 color-teal absolute right-0 top-0 bottom-0 hover:shadow h-6 my-auto"
onClick={(e: any) => {
@ -19,7 +19,7 @@ function JumpButton(props: Props) {
<Icon name="caret-right-fill" size="12" color="teal" />
<span>JUMP</span>
</div>
</Popup>
</Tooltip>
);
}

View file

@ -1,7 +1,5 @@
import React, { useState } from 'react';
import cn from 'classnames';
// import { connectPlayer } from 'Player';
import { QuestionMarkHint, Popup, Tabs, Input, NoContent, Icon, Toggler, Button } from 'UI';
import { QuestionMarkHint, Tooltip, Tabs, Input, NoContent, Icon, Toggler, Button } from 'UI';
import { getRE } from 'App/utils';
import Resource, { TYPES } from 'Types/session/resource';
import { formatBytes } from 'App/utils';
@ -10,12 +8,10 @@ import { formatMs } from 'App/date';
import TimeTable from '../TimeTable';
import BottomBlock from '../BottomBlock';
import InfoLine from '../BottomBlock/InfoLine';
// import stl from './network.module.css';
import { Duration } from 'luxon';
import { connectPlayer, jump, pause } from 'Player';
import { connectPlayer, jump } from 'Player';
import { useModal } from 'App/components/Modal';
import FetchDetailsModal from 'Shared/FetchDetailsModal';
import { sort } from 'App/duck/sessions';
const ALL = 'ALL';
const XHR = 'xhr';
@ -49,17 +45,17 @@ function compare(a: any, b: any, key: string) {
export function renderType(r: any) {
return (
<Popup style={{ width: '100%' }} content={<div>{r.type}</div>}>
<Tooltip style={{ width: '100%' }} title={<div>{r.type}</div>}>
<div>{r.type}</div>
</Popup>
</Tooltip>
);
}
export function renderName(r: any) {
return (
<Popup style={{ width: '100%' }} content={<div>{r.url}</div>}>
<Tooltip style={{ width: '100%' }} title={<div>{r.url}</div>}>
<div>{r.name}</div>
</Popup>
</Tooltip>
);
}
@ -75,7 +71,6 @@ const renderXHRText = () => (
<span className="flex items-center">
{XHR}
<QuestionMarkHint
onHover={true}
content={
<>
Use our{' '}
@ -128,9 +123,9 @@ function renderSize(r: any) {
}
return (
<Popup style={{ width: '100%' }} content={content}>
<Tooltip style={{ width: '100%' }} title={content}>
<div>{triggerText}</div>
</Popup>
</Tooltip>
);
}
@ -151,9 +146,9 @@ export function renderDuration(r: any) {
}
return (
<Popup style={{ width: '100%' }} content={tooltipText}>
<Tooltip style={{ width: '100%' }} title={tooltipText}>
<div> {text} </div>
</Popup>
</Tooltip>
);
}
@ -221,7 +216,7 @@ function NetworkPanel(props: Props) {
({ type, name, status, success }: any) =>
(!!filter ? filterRE.test(status) || filterRE.test(name) || filterRE.test(type) : true) &&
(activeTab === ALL || type === TAB_TO_TYPE_MAP[activeTab]) &&
(showOnlyErrors ? (parseInt(status) >= 400 || !success) : true)
(showOnlyErrors ? parseInt(status) >= 400 || !success : true)
);
return list;
}, [filter, sortBy, sortAscending, showOnlyErrors, activeTab]);
@ -374,7 +369,7 @@ function NetworkPanel(props: Props) {
dataKey: 'decodedBodySize',
render: renderSize,
onClick: handleSort,
hidden: activeTab === XHR
hidden: activeTab === XHR,
},
{
label: 'Time',

View file

@ -1,25 +1,30 @@
import { Popup } from 'UI';
import { Tooltip } from 'UI';
import { percentOf } from 'App/utils';
import styles from './barRow.module.css'
import styles from './barRow.module.css';
import tableStyles from './timeTable.module.css';
import React from 'react';
const formatTime = time => time < 1000 ? `${time.toFixed(2)}ms` : `${time / 1000}s`;
const formatTime = (time) => (time < 1000 ? `${time.toFixed(2)}ms` : `${time / 1000}s`);
interface Props {
resource: {
time: number
ttfb?: number
duration?: number
key: string
}
popup?: boolean
timestart: number
timewidth: number
time: number;
ttfb?: number;
duration?: number;
key: string;
};
popup?: boolean;
timestart: number;
timewidth: number;
}
// TODO: If request has no duration, set duration to 0.2s. Enforce existence of duration in the future.
const BarRow = ({ resource: { time, ttfb = 0, duration = 200, key }, popup = false, timestart = 0, timewidth }: Props) => {
const BarRow = ({
resource: { time, ttfb = 0, duration = 200, key },
popup = false,
timestart = 0,
timewidth,
}: Props) => {
const timeOffset = time - timestart;
ttfb = ttfb || 0;
const trigger = (
@ -28,7 +33,7 @@ const BarRow = ({ resource: { time, ttfb = 0, duration = 200, key }, popup = fal
style={{
left: `${percentOf(timeOffset, timewidth)}%`,
right: `${100 - percentOf(timeOffset + duration, timewidth)}%`,
minWidth: '5px'
minWidth: '5px',
}}
>
<div
@ -41,23 +46,28 @@ const BarRow = ({ resource: { time, ttfb = 0, duration = 200, key }, popup = fal
className={styles.downloadBar}
style={{
width: `${percentOf(duration - ttfb, duration)}%`,
minWidth: '5px'
minWidth: '5px',
}}
/>
</div>
);
if (!popup) return <div key={key} className={tableStyles.row} > {trigger} </div>;
if (!popup)
return (
<div key={key} className={tableStyles.row}>
{' '}
{trigger}{' '}
</div>
);
return (
<div key={key} className={tableStyles.row} >
<Popup
basic
content={
<div key={key} className={tableStyles.row}>
<Tooltip
title={
<React.Fragment>
{ttfb != null &&
{ttfb != null && (
<div className={styles.popupRow}>
<div className={styles.title}>{'Waiting (TTFB)'}</div>
<div className={styles.popupBarWrapper} >
<div className={styles.popupBarWrapper}>
<div
className={styles.ttfbBar}
style={{
@ -66,11 +76,11 @@ const BarRow = ({ resource: { time, ttfb = 0, duration = 200, key }, popup = fal
}}
/>
</div>
<div className={styles.time} >{formatTime(ttfb)}</div>
<div className={styles.time}>{formatTime(ttfb)}</div>
</div>
}
)}
<div className={styles.popupRow}>
<div className={styles.title} >{'Content Download'}</div>
<div className={styles.title}>{'Content Download'}</div>
<div className={styles.popupBarWrapper}>
<div
className={styles.downloadBar}
@ -84,13 +94,14 @@ const BarRow = ({ resource: { time, ttfb = 0, duration = 200, key }, popup = fal
</div>
</React.Fragment>
}
size="mini"
position="top center"
/>
placement="top"
>
{trigger}
</Tooltip>
</div>
);
}
};
BarRow.displayName = "BarRow";
BarRow.displayName = 'BarRow';
export default BarRow;
export default BarRow;

View file

@ -1,30 +1,40 @@
import React from 'react'
import React, { useState } from 'react';
import { connect } from 'react-redux';
import { Popup } from 'UI'
import { resendEmailVerification } from 'Duck/user'
import { Tooltip } from 'UI';
import { resendEmailVerification } from 'Duck/user';
import { toast } from 'react-toastify';
function EmailVerificationMessage(props) {
const [sent, setSent] = useState(false);
const { email } = props;
const send = () => {
props.resendEmailVerification(email).then(function() {
props.resendEmailVerification(email).then(function () {
toast.success(`Verification email sent to ${email}`);
})
}
return (
<Popup
content={
`We've sent a verification email to "${email}" please follow the instructions in it to use OpenReplay uninterruptedly.`
}
setSent(true);
});
};
return !sent ? (
<Tooltip
title={`We've sent a verification email to "${email}" please follow the instructions in it to use OpenReplay uninterruptedly.`}
>
<div
className="mt-3 px-3 rounded-2xl font-medium"
style={{ paddingTop: '3px', height: '28px', backgroundColor: 'rgba(255, 239, 239, 1)', border: 'solid thin rgba(221, 181, 181, 1)' }}
className="mt-3 px-3 rounded-2xl font-medium"
style={{
paddingTop: '3px',
height: '28px',
backgroundColor: 'rgba(255, 239, 239, 1)',
border: 'solid thin rgba(221, 181, 181, 1)',
}}
>
<span>Please, verify your email.</span> <a href="#" className="link" onClick={send}>Resend</a>
<span>Please, verify your email.</span>{' '}
<a href="#" className="link" onClick={send}>
Resend
</a>
</div>
</Popup>
)
</Tooltip>
) : (
<></>
);
}
export default connect(null, { resendEmailVerification })(EmailVerificationMessage)
export default connect(null, { resendEmailVerification })(EmailVerificationMessage);

View file

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import FilterItem from '../FilterItem';
import { SegmentSelection, Popup } from 'UI';
import { SegmentSelection, Tooltip } from 'UI';
import { List } from 'immutable';
import { useObserver } from 'mobx-react-lite';
@ -24,31 +24,34 @@ function FilterList(props: Props) {
const onRemoveFilter = (filterIndex: any) => {
props.onRemoveFilter(filterIndex);
}
};
return useObserver(() => (
<div className="flex flex-col">
{ hasEvents && (
{hasEvents && (
<>
<div className="flex items-center mb-2">
<div className="text-sm color-gray-medium mr-auto">EVENTS</div>
{ !hideEventsOrder && (
{!hideEventsOrder && (
<div className="flex items-center">
<div className="mr-2 color-gray-medium text-sm" style={{ textDecoration: 'underline dotted'}}>
<Popup
content={ `Select the operator to be applied between events in your search.` }
<div
className="mr-2 color-gray-medium text-sm"
style={{ textDecoration: 'underline dotted' }}
>
<Tooltip
title={`Select the operator to be applied between events in your search.`}
>
<div>Events Order</div>
</Popup>
</Tooltip>
</div>
<SegmentSelection
primary
name="eventsOrder"
extraSmall={true}
onSelect={props.onChangeEventsOrder}
value={{ value: filter.eventsOrder }}
list={ [
list={[
{ name: 'THEN', value: 'then' },
{ name: 'AND', value: 'and' },
{ name: 'OR', value: 'or' },
@ -57,38 +60,42 @@ function FilterList(props: Props) {
</div>
)}
</div>
{filters.map((filter: any, filterIndex: any) => filter.isEvent ? (
<FilterItem
key={`${filter.key}-${filterIndex}`}
filterIndex={rowIndex++}
filter={filter}
onUpdate={(filter) => props.onUpdateFilter(filterIndex, filter)}
onRemoveFilter={() => onRemoveFilter(filterIndex) }
saveRequestPayloads={saveRequestPayloads}
/>
): null)}
<div className='mb-2' />
{filters.map((filter: any, filterIndex: any) =>
filter.isEvent ? (
<FilterItem
key={`${filter.key}-${filterIndex}`}
filterIndex={rowIndex++}
filter={filter}
onUpdate={(filter) => props.onUpdateFilter(filterIndex, filter)}
onRemoveFilter={() => onRemoveFilter(filterIndex)}
saveRequestPayloads={saveRequestPayloads}
/>
) : null
)}
<div className="mb-2" />
</>
)}
{hasFilters && (
<>
{hasEvents && <div className='border-t -mx-5 mb-4' />}
{hasEvents && <div className="border-t -mx-5 mb-4" />}
<div className="mb-2 text-sm color-gray-medium mr-auto">FILTERS</div>
{filters.map((filter: any, filterIndex: any) => !filter.isEvent ? (
<FilterItem
key={filterIndex}
isFilter={true}
filterIndex={filterIndex}
filter={filter}
onUpdate={(filter) => props.onUpdateFilter(filterIndex, filter)}
onRemoveFilter={() => onRemoveFilter(filterIndex) }
/>
): null)}
{filters.map((filter: any, filterIndex: any) =>
!filter.isEvent ? (
<FilterItem
key={filterIndex}
isFilter={true}
filterIndex={filterIndex}
filter={filter}
onUpdate={(filter) => props.onUpdateFilter(filterIndex, filter)}
onRemoveFilter={() => onRemoveFilter(filterIndex)}
/>
) : null
)}
</>
)}
</div>
));
}
export default FilterList;
export default FilterList;

View file

@ -1,36 +1,39 @@
import React from 'react';
import LiveSessionSearchField from 'Shared/LiveSessionSearchField';
import { Button, Popup } from 'UI';
import { Button, Tooltip } from 'UI';
import { clearSearch } from 'Duck/liveSearch';
import { connect } from 'react-redux';
interface Props {
clearSearch: () => void;
appliedFilter: any;
clearSearch: () => void;
appliedFilter: any;
}
const LiveSearchBar = (props: Props) => {
const { appliedFilter } = props;
const hasFilters = appliedFilter && appliedFilter.filters && appliedFilter.filters.size > 0;
return (
<div className="flex items-center">
<div style={{ width: "60%", marginRight: "10px"}}>
<LiveSessionSearchField />
</div>
<div className="flex items-center" style={{ width: "40%"}}>
<Popup content={'Clear Steps'} >
<Button
variant="text-primary"
disabled={!hasFilters}
className="ml-auto font-medium"
onClick={() => props.clearSearch()}
>
Clear
</Button>
</Popup>
</div>
<div style={{ width: '60%', marginRight: '10px' }}>
<LiveSessionSearchField />
</div>
<div className="flex items-center" style={{ width: '40%' }}>
<Tooltip title={'Clear Steps'}>
<Button
variant="text-primary"
disabled={!hasFilters}
className="ml-auto font-medium"
onClick={() => props.clearSearch()}
>
Clear
</Button>
</Tooltip>
</div>
</div>
)
}
export default connect(state => ({
);
};
export default connect(
(state) => ({
appliedFilter: state.getIn(['liveSearch', 'instance']),
}), { clearSearch })(LiveSearchBar);
}),
{ clearSearch }
)(LiveSearchBar);

View file

@ -1,5 +1,5 @@
import React from 'react';
import { CircularLoader, Icon, Popup } from 'UI';
import { CircularLoader, Icon, Tooltip } from 'UI';
import cn from 'classnames';
interface Props {
@ -12,13 +12,11 @@ interface Props {
export default function ReloadButton(props: Props) {
const { loading, onClick, iconSize = '20', iconName = 'arrow-repeat', className = '' } = props;
return (
<Popup content="Refresh">
<div
className={cn('h-5 w-6 flex items-center justify-center', className)}
onClick={onClick}
>
<Tooltip title="Refresh">
<div className={cn('h-5 w-6 flex items-center justify-center', className)} onClick={onClick}>
{/* @ts-ignore */}
{loading ? <CircularLoader className="ml-1" /> : <Icon name={iconName} size={iconSize} />}
</div>
</Popup>
</Tooltip>
);
}

View file

@ -1,32 +1,30 @@
import React from 'react'
import { Popup } from 'UI'
import MetaItem from '../MetaItem'
import React from 'react';
import { Popover, Button } from 'UI';
import MetaItem from '../MetaItem';
interface Props {
list: any[],
maxLength: number,
list: any[];
maxLength: number;
}
export default function MetaMoreButton(props: Props) {
const { list, maxLength } = props
return (
<Popup
className="p-0"
theme="light"
content={
<div className="text-sm grid grid-col p-4 gap-3" style={{ maxHeight: '200px', overflowY: 'auto'}}>
{list.slice(maxLength).map(({ label, value }, index) => (
<MetaItem key={index} label={label} value={value} />
))}
</div>
}
on="click"
position="center center"
const { list, maxLength } = props;
return (
<Popover
render={() => (
<div
className="text-sm grid grid-col p-4 gap-3 bg-white"
style={{ maxHeight: '200px', overflowY: 'auto' }}
>
<div className=" flex items-center">
<span className="rounded bg-active-blue color-teal p-2 color-gray-dark cursor-pointer whitespace-nowrap">
+{list.length - maxLength} More
</span>
</div>
</Popup>
)
{list.slice(maxLength).map(({ label, value }, index) => (
<MetaItem key={index} label={label} value={value} />
))}
</div>
)}
placement="bottom"
>
<div className="flex items-center">
<Button variant="text-primary">+{list.length - maxLength} More</Button>
</div>
</Popover>
);
}

View file

@ -1,27 +1,25 @@
import React from 'react'
import { Popup } from 'UI'
import cn from 'classnames'
import React from 'react';
import { Popup } from 'UI';
import cn from 'classnames';
import MetaItem from '../MetaItem';
import MetaMoreButton from '../MetaMoreButton';
interface Props {
className?: string,
metaList: any[],
maxLength?: number,
className?: string;
metaList: any[];
maxLength?: number;
}
export default function SessionMetaList(props: Props) {
const { className = '', metaList, maxLength = 4 } = props
const { className = '', metaList, maxLength = 4 } = props;
return (
<div className={cn("text-sm flex items-center", className)}>
<div className={cn('text-sm flex items-center', className)}>
{metaList.slice(0, maxLength).map(({ label, value }, index) => (
<MetaItem key={index} label={label} value={''+value} className="mr-3" />
<MetaItem key={index} label={label} value={'' + value} className="mr-3" />
))}
{metaList.length > maxLength && (
<MetaMoreButton list={metaList} maxLength={maxLength} />
)}
{metaList.length > maxLength && <MetaMoreButton list={metaList} maxLength={maxLength} />}
</div>
)
);
}

View file

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Icon, Toggler, Button, Input, Loader, Popup } from 'UI';
import { Icon, Toggler, Button, Input, Loader, Tooltip } from 'UI';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import { connect } from 'react-redux';
@ -43,17 +43,17 @@ function CaptureRate({ isAdmin = false }) {
<Loader loading={loading}>
<h3 className="text-lg">Capture Rate</h3>
<div className="my-1">The percentage of session you want to capture</div>
<Popup content="You don't have permission to change." disabled={isAdmin} delay={0}>
<Tooltip title="You don't have permission to change." disabled={isAdmin} delay={0}>
<div className={cn('mt-2 mb-4 mr-1 flex items-center', { disabled: !isAdmin })}>
<Toggler checked={captureAll} name="test" onChange={toggleRate} />
<span className="ml-2" style={{ color: captureAll ? '#000000' : '#999' }}>
100%
</span>
</div>
</Popup>
</Tooltip>
{!captureAll && (
<div className="flex items-center">
<Popup content="You don't have permission to change." disabled={isAdmin} delay={0}>
<Tooltip title="You don't have permission to change." disabled={isAdmin} delay={0}>
<div className={cn("relative", { 'disabled' : !isAdmin })}>
<Input
type="number"
@ -66,7 +66,7 @@ function CaptureRate({ isAdmin = false }) {
/>
<Icon className="absolute right-0 mr-6 top-0 bottom-0 m-auto" name="percent" color="gray-medium" size="18" />
</div>
</Popup>
</Tooltip>
<span className="mx-3">of the sessions</span>
<Button
disabled={!changed}

View file

@ -1,34 +1,40 @@
import React from 'react'
import { Icon, Popup } from 'UI'
import cn from 'classnames'
import React from 'react';
import { Icon, Tooltip } from 'UI';
import cn from 'classnames';
interface Props {
sortOrder: string,
onChange?: (sortOrder: string) => void,
sortOrder: string;
onChange?: (sortOrder: string) => void;
}
export default React.memo(function SortOrderButton(props: Props) {
const { sortOrder, onChange = () => null } = props
const isAscending = sortOrder === 'asc'
const { sortOrder, onChange = () => null } = props;
const isAscending = sortOrder === 'asc';
return (
<div className="flex items-center border">
<Popup content={'Ascending'} >
<div
className={cn("p-2 hover:bg-active-blue", { 'cursor-pointer bg-white' : !isAscending, 'bg-active-blue pointer-events-none' : isAscending })}
onClick={() => onChange('asc')}
>
<Icon name="arrow-up" size="14" color={isAscending ? 'teal' : 'gray-medium'} />
</div>
</Popup>
<Popup content={'Descending'} >
<div
className={cn("p-2 hover:bg-active-blue border-l", { 'cursor-pointer bg-white' : isAscending, 'bg-active-blue pointer-events-none' : !isAscending })}
onClick={() => onChange('desc')}
>
<Icon name="arrow-down" size="14" color={!isAscending ? 'teal' : 'gray-medium'} />
</div>
</Popup>
return (
<div className="flex items-center border">
<Tooltip title={'Ascending'}>
<div
className={cn('p-2 hover:bg-active-blue', {
'cursor-pointer bg-white': !isAscending,
'bg-active-blue pointer-events-none': isAscending,
})}
onClick={() => onChange('asc')}
>
<Icon name="arrow-up" size="14" color={isAscending ? 'teal' : 'gray-medium'} />
</div>
)
})
</Tooltip>
<Tooltip title={'Descending'}>
<div
className={cn('p-2 hover:bg-active-blue border-l', {
'cursor-pointer bg-white': isAscending,
'bg-active-blue pointer-events-none': !isAscending,
})}
onClick={() => onChange('desc')}
>
<Icon name="arrow-down" size="14" color={!isAscending ? 'teal' : 'gray-medium'} />
</div>
</Tooltip>
</div>
);
});

View file

@ -1,6 +1,5 @@
import React from 'react';
// import SlackIcon from '../../../svg/slack-help.svg';
import { Popup, Icon } from 'UI';
import { Icon } from 'UI';
import SupportList from './components/SupportList';
function SupportCallout() {
@ -10,11 +9,9 @@ function SupportCallout() {
<SupportList />
</div>
<div className="fixed z-50 left-0 bottom-0 m-4">
{/* <Popup content="OpenReplay community" delay={0}> */}
<div className="w-12 h-12 cursor-pointer bg-white border rounded-full flex items-center justify-center group-hover:shadow-lg group-hover:!bg-active-blue">
<Icon name="question-lg" size={30} color="teal" />
</div>
{/* </Popup> */}
</div>
</div>
);

View file

@ -2,23 +2,39 @@ import React from 'react';
import cn from 'classnames';
import { avatarIconName } from 'App/iconNames';
import stl from './avatar.module.css';
import { Icon, Popup } from 'UI';
import { Icon, Tooltip } from 'UI';
const Avatar = ({ isActive = false, isAssist = false, width = '38px', height = '38px', iconSize = 26, seed }) => {
var iconName = avatarIconName(seed);
return (
<Popup content={isActive ? 'Active user' : 'User might be inactive'} disabled={!isAssist}>
<div className={cn(stl.wrapper, 'p-2 border flex items-center justify-center rounded-full relative')} style={{ width, height }}>
<Icon name={iconName} size={iconSize} color="tealx" />
{isAssist && (
<div
className={cn('w-2 h-2 rounded-full absolute right-0 bottom-0', { 'bg-green': isActive, 'bg-orange': !isActive })}
style={{ marginRight: '3px', marginBottom: '3px' }}
/>
)}
</div>
</Popup>
);
const Avatar = ({
isActive = false,
isAssist = false,
width = '38px',
height = '38px',
iconSize = 26,
seed,
}) => {
var iconName = avatarIconName(seed);
return (
<Tooltip title={isActive ? 'Active user' : 'User might be inactive'} disabled={!isAssist}>
<div
className={cn(
stl.wrapper,
'p-2 border flex items-center justify-center rounded-full relative'
)}
style={{ width, height }}
>
<Icon name={iconName} size={iconSize} color="tealx" />
{isAssist && (
<div
className={cn('w-2 h-2 rounded-full absolute right-0 bottom-0', {
'bg-green': isActive,
'bg-orange': !isActive,
})}
style={{ marginRight: '3px', marginBottom: '3px' }}
/>
)}
</div>
</Tooltip>
);
};
export default Avatar;

View file

@ -1,95 +1,104 @@
import React from 'react';
import cn from 'classnames';
import { CircularLoader, Icon, Popup } from 'UI';
import { CircularLoader, Icon, Tooltip } from 'UI';
interface Props {
className?: string;
children?: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
type?: 'button' | 'submit' | 'reset';
variant?: 'default' | 'primary' | 'text' | 'text-primary' | 'text-red' | 'outline' | 'green'
loading?: boolean;
icon?: string;
iconSize?: number;
rounded?: boolean;
tooltip?: any;
[x: string]: any;
className?: string;
children?: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
type?: 'button' | 'submit' | 'reset';
variant?: 'default' | 'primary' | 'text' | 'text-primary' | 'text-red' | 'outline' | 'green';
loading?: boolean;
icon?: string;
iconSize?: number;
rounded?: boolean;
tooltip?: any;
[x: string]: any;
}
export default (props: Props) => {
const {
icon = '',
iconSize = 18,
className = '',
variant = 'default', // 'default|primary|text|text-primary|text-red|outline',
type = 'button',
size = '',
disabled = false,
children,
loading = false,
rounded = false,
tooltip = null,
...rest
} = props;
const {
icon = '',
iconSize = 18,
className = '',
variant = 'default', // 'default|primary|text|text-primary|text-red|outline',
type = 'button',
size = '',
disabled = false,
children,
loading = false,
rounded = false,
tooltip = null,
...rest
} = props;
let classes = ['relative flex items-center h-10 px-3 rounded tracking-wide whitespace-nowrap'];
let iconColor = variant === 'text' || variant === 'default' ? 'gray-dark' : 'teal';
let classes = ['relative flex items-center h-10 px-3 rounded tracking-wide whitespace-nowrap'];
let iconColor = variant === 'text' || variant === 'default' ? 'gray-dark' : 'teal';
if (variant === 'default') {
classes.push('bg-white hover:bg-gray-light border border-gray-light');
}
if (variant === 'default') {
classes.push('bg-white hover:bg-gray-light border border-gray-light');
}
if (variant === 'primary') {
classes.push('bg-teal color-white hover:bg-teal-dark');
}
if (variant === 'primary') {
classes.push('bg-teal color-white hover:bg-teal-dark');
}
if (variant === 'green') {
classes.push('bg-green color-white hover:bg-green-dark');
iconColor = 'white';
}
if (variant === 'green') {
classes.push('bg-green color-white hover:bg-green-dark');
iconColor = 'white';
}
if (variant === 'text') {
classes.push('bg-transparent color-gray-dark hover:bg-gray-light-shade');
}
if (variant === 'text') {
classes.push('bg-transparent color-gray-dark hover:bg-gray-light-shade');
}
if (variant === 'text-primary') {
classes.push('bg-transparent color-teal hover:bg-teal-light hover:color-teal-dark');
}
if (variant === 'text-primary') {
classes.push('bg-transparent color-teal hover:bg-teal-light hover:color-teal-dark');
}
if (variant === 'text-red') {
classes.push('bg-transparent color-red hover:bg-teal-light');
}
if (variant === 'text-red') {
classes.push('bg-transparent color-red hover:bg-teal-light');
}
if (variant === 'outline') {
classes.push('bg-white color-teal border border-teal hover:bg-teal-light');
}
if (variant === 'outline') {
classes.push('bg-white color-teal border border-teal hover:bg-teal-light');
}
if (disabled) {
classes.push('opacity-40 pointer-events-none');
}
if (disabled) {
classes.push('opacity-40 pointer-events-none');
}
if (variant === 'primary') {
iconColor = 'white';
}
if (variant === 'text-red') {
iconColor = 'red';
}
if (variant === 'primary') {
iconColor = 'white';
}
if (variant === 'text-red') {
iconColor = 'red';
}
if (rounded) {
classes = classes.map((c) => c.replace('rounded', 'rounded-full h-10 w-10 justify-center'));
}
if (rounded) {
classes = classes.map((c) => c.replace('rounded', 'rounded-full h-10 w-10 justify-center'));
}
const render = () => (
<button {...rest} type={type} className={cn(classes, className)}>
{icon && <Icon className={cn({ 'mr-2': children })} name={icon} color={iconColor} size={iconSize} />}
{loading && (
<div className="absolute flex items-center justify-center inset-0 z-1 rounded">
<CircularLoader />
</div>
)}
<div className={cn({ 'opacity-0': loading }, 'flex items-center')}>{children}</div>
</button>
);
const render = () => (
<button {...rest} type={type} className={cn(classes, className)}>
{icon && (
// @ts-ignore
<Icon className={cn({ 'mr-2': children })} name={icon} color={iconColor} size={iconSize} />
)}
{loading && (
<div className="absolute flex items-center justify-center inset-0 z-1 rounded">
<CircularLoader />
</div>
)}
<div className={cn({ 'opacity-0': loading }, 'flex items-center')}>{children}</div>
</button>
);
return tooltip ? <Popup content={tooltip.title} {...tooltip}>{render()}</Popup> : render();
return tooltip ? (
<Tooltip title={tooltip.title} {...tooltip}>
{render()}
</Tooltip>
) : (
render()
);
};

View file

@ -1,19 +1,20 @@
import React from 'react'
import { Icon, Popup } from 'UI'
import React from 'react';
import { Icon, Tooltip } from 'UI';
interface Props {
text: string,
className?: string,
position?: string,
text: string;
className?: string;
position?: string;
}
export default function HelpText(props: Props) {
const { text, className = '', position = 'top center' } = props
return (
<div>
<Popup content={text} >
<div className={className}><Icon name="question-circle" size={16} /></div>
</Popup>
const { text, className = '', position = 'top center' } = props;
return (
<div>
<Tooltip title={text}>
<div className={className}>
<Icon name="question-circle" size={16} />
</div>
)
</Tooltip>
</div>
);
}

View file

@ -1,79 +1,81 @@
import React from 'react';
import cn from 'classnames';
import { CircularLoader, Icon, Popup } from 'UI';
import { CircularLoader, Icon, Tooltip } from 'UI';
import stl from './iconButton.module.css';
const IconButton = React.forwardRef(({
icon,
label = false,
active,
onClick,
plain = false,
shadow = false,
red = false,
primary = false,
primaryText = false,
redText = false,
outline = false,
loading = false,
roundedOutline = false,
hideLoader = false,
circle = false,
size = 'default',
marginRight,
buttonSmall,
className = '',
style,
name,
disabled = false,
tooltip = false,
tooltipPosition = 'top center',
compact = false,
...rest
}, ref) => (
<Popup
content={tooltip}
position={tooltipPosition}
>
<button
ref={ ref }
name={ name }
className={ cn(stl.button, className, {
[ stl.plain ]: plain,
[ stl.active ]: active,
[ stl.shadow ]: shadow,
[ stl.primary ]: primary,
[ stl.red ]: red,
[ stl.primaryText ]: primaryText,
[ stl.redText ]: redText,
[ stl.outline ]: outline,
[ stl.circle ]: circle,
[ stl.roundedOutline ]: roundedOutline,
[ stl.buttonSmall ]: buttonSmall,
[ stl.small ]: size === 'small',
[ stl.tiny ]: size === 'tiny',
[ stl.marginRight ]: marginRight,
[ stl.compact ]: compact,
[ stl.hasLabel]: !!label
}) }
onClick={ onClick }
disabled={ disabled || loading }
const IconButton = React.forwardRef(
(
{
icon,
label = false,
active,
onClick,
plain = false,
shadow = false,
red = false,
primary = false,
primaryText = false,
redText = false,
outline = false,
loading = false,
roundedOutline = false,
hideLoader = false,
circle = false,
size = 'default',
marginRight,
buttonSmall,
className = '',
style,
name,
disabled = false,
tooltip = false,
tooltipPosition = 'top center',
compact = false,
...rest
},
ref
) => (
<Tooltip title={tooltip} position={tooltipPosition}>
<button
ref={ref}
name={name}
className={cn(stl.button, className, {
[stl.plain]: plain,
[stl.active]: active,
[stl.shadow]: shadow,
[stl.primary]: primary,
[stl.red]: red,
[stl.primaryText]: primaryText,
[stl.redText]: redText,
[stl.outline]: outline,
[stl.circle]: circle,
[stl.roundedOutline]: roundedOutline,
[stl.buttonSmall]: buttonSmall,
[stl.small]: size === 'small',
[stl.tiny]: size === 'tiny',
[stl.marginRight]: marginRight,
[stl.compact]: compact,
[stl.hasLabel]: !!label,
})}
onClick={onClick}
disabled={disabled || loading}
style={style}
{ ...rest }
{...rest}
>
{ !hideLoader && <CircularLoader loading={ loading } /> }
{ icon &&
{!hideLoader && <CircularLoader loading={loading} />}
{icon && (
<Icon
color="teal"
name={ icon }
data-hidden={ loading }
size={ size === 'tiny' || size === 'small' || buttonSmall ? '14' : '16' }
name={icon}
data-hidden={loading}
size={size === 'tiny' || size === 'small' || buttonSmall ? '14' : '16'}
/>
}
{ label && <span className={ cn(stl.label, icon || loading ? 'ml-2' : '') }>{ label }</span> }
)}
{label && <span className={cn(stl.label, icon || loading ? 'ml-2' : '')}>{label}</span>}
</button>
</Popup>
));
</Tooltip>
)
);
IconButton.displayName = "IconButton";
IconButton.displayName = 'IconButton';
export default IconButton;

View file

@ -4,7 +4,7 @@ import styles from './itemMenu.module.css';
import cn from 'classnames';
interface Item {
icon: string;
icon?: string;
text: string;
onClick: (args: any) => void;
hidden?: boolean;
@ -15,7 +15,7 @@ interface Props {
bold?: boolean;
flat?: boolean;
items: Item[];
label: React.ReactNode;
label?: React.ReactNode;
onToggle?: (args: any) => void;
}

View file

@ -1,76 +1,64 @@
//@ts-nocheck
import React from 'react'
import { Icon, Popup } from 'UI'
import cn from 'classnames'
import React from 'react';
import { Icon, Tooltip } from 'UI';
import cn from 'classnames';
import { debounce } from 'App/utils';
import { numberWithCommas } from 'App/utils';
interface Props {
page: number
totalPages: number
onPageChange: (page: number) => void
limit?: number
debounceRequest?: number
page: number;
totalPages: number;
onPageChange: (page: number) => void;
limit?: number;
debounceRequest?: number;
}
export default function Pagination(props: Props) {
const { page, totalPages, onPageChange, limit = 5, debounceRequest = 0 } = props;
const [currentPage, setCurrentPage] = React.useState(page);
React.useMemo(
() => setCurrentPage(page),
[page],
);
React.useMemo(() => setCurrentPage(page), [page]);
const debounceChange = React.useCallback(debounce(onPageChange, debounceRequest), []);
const changePage = (page: number) => {
if (page > 0 && page <= totalPages) {
setCurrentPage(page);
debounceChange(page);
}
const changePage = (page: number) => {
if (page > 0 && page <= totalPages) {
setCurrentPage(page);
debounceChange(page);
}
const isFirstPage = currentPage === 1;
const isLastPage = currentPage === totalPages;
return (
<div className="flex items-center">
<Popup
content="Previous Page"
// hideOnClick={true}
animation="none"
delay={1500}
>
<button
className={cn("py-2 px-3", { "opacity-50 cursor-default": isFirstPage })}
disabled={isFirstPage}
onClick={() => changePage(currentPage - 1)}
>
<Icon name="chevron-left" size="18" color={isFirstPage ? 'gray-medium' : 'teal'} />
</button>
</Popup>
<span className="mr-2 color-gray-medium">Page</span>
<input
type="number"
className={cn("py-1 px-2 bg-white border border-gray-light rounded w-16", { "opacity-50 cursor-default": totalPages === 1 })}
value={currentPage}
min={1}
max={totalPages ? totalPages : 1}
onChange={(e) => changePage(parseInt(e.target.value))}
/>
<span className="mx-3 color-gray-medium">of</span>
<span >{numberWithCommas(totalPages)}</span>
<Popup
content="Next Page"
// hideOnClick={true}
animation="none"
delay={1500}
>
<button
className={cn("py-2 px-3", { "opacity-50 cursor-default": isLastPage })}
disabled={isLastPage}
onClick={() => changePage(currentPage + 1)}
>
<Icon name="chevron-right" size="18" color={isLastPage ? 'gray-medium' : 'teal'} />
</button>
</Popup>
</div>
)
};
const isFirstPage = currentPage === 1;
const isLastPage = currentPage === totalPages;
return (
<div className="flex items-center">
<Tooltip title="Previous Page">
<button
className={cn('py-2 px-3', { 'opacity-50 cursor-default': isFirstPage })}
disabled={isFirstPage}
onClick={() => changePage(currentPage - 1)}
>
<Icon name="chevron-left" size="18" color={isFirstPage ? 'gray-medium' : 'teal'} />
</button>
</Tooltip>
<span className="mr-2 color-gray-medium">Page</span>
<input
type="number"
className={cn('py-1 px-2 bg-white border border-gray-light rounded w-16', {
'opacity-50 cursor-default': totalPages === 1,
})}
value={currentPage}
min={1}
max={totalPages ? totalPages : 1}
onChange={(e) => changePage(parseInt(e.target.value))}
/>
<span className="mx-3 color-gray-medium">of</span>
<span>{numberWithCommas(totalPages)}</span>
<Tooltip title="Next Page">
<button
className={cn('py-2 px-3', { 'opacity-50 cursor-default': isLastPage })}
disabled={isLastPage}
onClick={() => changePage(currentPage + 1)}
>
<Icon name="chevron-right" size="18" color={isLastPage ? 'gray-medium' : 'teal'} />
</button>
</Tooltip>
</div>
);
}

View file

@ -1,56 +1,43 @@
import React, { useState, useCallback } from 'react';
import React, { useState } from 'react';
import cn from 'classnames';
import { Icon, Popup } from 'UI';
import { Icon, Tooltip } from 'UI';
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
import cls from './popMenu.module.css';
export default React.memo(function PopMenu({ items }) {
const [ open, setOpen ] = useState(false);
const [open, setOpen] = useState(false);
return (
<OutsideClickDetectingDiv
className={ cls.wrapper }
onClickOutside={() => setOpen(false)}
>
{ open &&
<div className={ cls.menuItems } >
{ items.map(item => (
<OutsideClickDetectingDiv className={cls.wrapper} onClickOutside={() => setOpen(false)}>
{open && (
<div className={cls.menuItems}>
{items.map((item) => (
<button
key={ item.label }
onClick={ (e) => {
item.onClick(e);
key={item.label}
onClick={(e) => {
item.onClick(e);
setOpen(false);
}}
className={ cn("flex items-center justify-end color-green bg-white uppercase overflow-hidden", cls.menuItemButton) }
className={cn(
'flex items-center justify-end color-green bg-white uppercase overflow-hidden',
cls.menuItemButton
)}
>
<div className={ cls.buttonLabel }>{ item.label }</div>
<Icon
name={ item.icon }
size="18"
className={ cls.icon }
color="teal"
/>
<div className={cls.buttonLabel}>{item.label}</div>
<Icon name={item.icon} size="18" className={cls.icon} color="teal" />
</button>
))}
</div>
}
<Popup
content={ `Add Step` }
size="tiny"
inverted
position="top center"
>
)}
<Tooltip title="Add Step">
<button
onClick={ () => setOpen(o => !o) }
className={ cn("bg-teal flex items-center justify-center", cls.addStepButton, {[cls.openMenuBtn] : open }) }
onClick={() => setOpen((o) => !o)}
className={cn('bg-teal flex items-center justify-center', cls.addStepButton, {
[cls.openMenuBtn]: open,
})}
>
<Icon
name="plus"
size="18"
className={ cls.plusIcon }
color="white"
/>
<Icon name="plus" size="18" className={cls.plusIcon} color="white" />
</button>
</Popup>
</Tooltip>
</OutsideClickDetectingDiv>
);
});

View file

@ -63,7 +63,7 @@ const Popover = ({ children, render, placement }: Props) => {
position: strategy,
top: y ?? 0,
left: x ?? 0,
zIndex: INDEXES.TOOLTIP
zIndex: INDEXES.TOOLTIP,
}}
aria-labelledby={labelId}
aria-describedby={descriptionId}

View file

@ -1,8 +0,0 @@
import { storiesOf } from '@storybook/react';
import Popup from '.';
storiesOf('Popup', module)
.add('Pure', () => (
<Popup />
))

View file

@ -1,57 +0,0 @@
import React from 'react';
import { Tooltip, Theme, Trigger, Position, Animation } from 'react-tippy';
interface Props {
content?: any;
title?: any;
trigger?: Trigger;
position?: Position;
className?: string;
delay?: number;
hideDelay?: number;
disabled?: boolean;
arrow?: boolean;
style?: any;
theme?: Theme;
interactive?: boolean;
children?: any;
animation?: Animation;
// [x:string]: any;
}
export default ({
position = 'top',
title = '',
className = '',
trigger = 'mouseenter',
delay = 0,
hideDelay = 0,
content = '',
disabled = false,
arrow = false,
theme = 'dark',
style = {},
interactive = false,
children,
animation = 'fade',
}: // ...props
Props) => (
// @ts-ignore
<Tooltip
animation={animation}
position={position}
className={className}
trigger={trigger}
html={content || title}
disabled={disabled}
arrow={arrow}
delay={delay}
hideOnClick={true}
hideOnScroll={true}
theme={theme}
style={style}
interactive={interactive}
hideDelay={hideDelay}
>
{children}
</Tooltip>
);

View file

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

View file

@ -1,16 +1,11 @@
import React from 'react';
import cn from "classnames";
import { Icon, Popup } from 'UI';
import cn from 'classnames';
import { Icon, Tooltip } from 'UI';
export default function QuestionMarkHint({ onHover = false, content, ...props }) {
return (
<Popup
trigger={ onHover ? 'mouseenter' : 'click'}
content={ content }
interactive
{ ...props }
>
<Icon name="question-circle" size="18" className={ cn("cursor-pointer") }/>
</Popup>
);
}
export default function QuestionMarkHint({ content, ...props }) {
return (
<Tooltip title={content} {...props}>
<Icon name="question-circle" size="18" className={cn('cursor-pointer')} />
</Tooltip>
);
}

View file

@ -1,44 +1,62 @@
import React from 'react';
import { Icon, Popup } from 'UI';
import { Icon, Tooltip } from 'UI';
import cn from 'classnames';
import styles from './segmentSelection.module.css';
class SegmentSelection extends React.Component {
setActiveItem = (item) => {
this.props.onSelect(null, { name: this.props.name, value: item.value });
}
};
render() {
const { className, list, small = false, extraSmall = false, primary = false, size = "normal", icons = false, disabled = false, disabledMessage = 'Not Allowed', outline } = this.props;
const {
className,
list,
small = false,
extraSmall = false,
primary = false,
size = 'normal',
icons = false,
disabled = false,
disabledMessage = 'Not Allowed',
outline,
} = this.props;
return (
<Popup
content={disabledMessage}
disabled={!disabled}
>
<div className={ cn(styles.wrapper, {
[styles.primary] : primary,
[styles.small] : size === 'small' || small,
[styles.extraSmall] : size === 'extraSmall' || extraSmall,
[styles.icons] : icons === true,
[styles.disabled] : disabled,
[styles.outline]: outline,
}, className) }
<Tooltip title={disabledMessage} disabled={!disabled}>
<div
className={cn(
styles.wrapper,
{
[styles.primary]: primary,
[styles.small]: size === 'small' || small,
[styles.extraSmall]: size === 'extraSmall' || extraSmall,
[styles.icons]: icons === true,
[styles.disabled]: disabled,
[styles.outline]: outline,
},
className
)}
>
{ list.map(item => (
<div
key={ item.name }
className={ cn(styles.item, 'w-full', { 'opacity-25 cursor-default' : item.disabled }) }
data-active={ this.props.value && this.props.value.value === item.value }
onClick={ () => !item.disabled && this.setActiveItem(item) }
>
{ item.icon && <Icon name={ item.icon } size={(size === "extraSmall" || icons) ? 14 : 20} marginRight={ item.name ? "6" : "" } /> }
<div className="leading-none">{ item.name }</div>
</div>
))
}
{list.map((item) => (
<div
key={item.name}
className={cn(styles.item, 'w-full', { 'opacity-25 cursor-default': item.disabled })}
data-active={this.props.value && this.props.value.value === item.value}
onClick={() => !item.disabled && this.setActiveItem(item)}
>
{item.icon && (
<Icon
name={item.icon}
size={size === 'extraSmall' || icons ? 14 : 20}
marginRight={item.name ? '6' : ''}
/>
)}
<div className="leading-none">{item.name}</div>
</div>
))}
</div>
</Popup>
</Tooltip>
);
}
}

View file

@ -1,5 +1,5 @@
import React from 'react';
import { Icon, Popup } from 'UI';
import { Icon, Tooltip } from 'UI';
import cn from 'classnames';
import stl from './sideMenuItem.module.css';
@ -18,12 +18,10 @@ function SideMenuitem({
...props
}) {
return (
<Popup
<Tooltip
disabled={ !disabled }
content={ 'No recordings' }
size="tiny"
inverted
position="left center"
title={ 'No recordings' }
placement="left"
>
<div
className={ cn(
@ -51,7 +49,7 @@ function SideMenuitem({
<div onClick={deleteHandler} className={stl.actions}><Icon name="trash" size="14" /></div>
}
</div>
</Popup>
</Tooltip>
)
}

View file

@ -1,10 +1,10 @@
import React from 'react';
import { useState, useRef, useEffect, forwardRef } from 'react';
import cn from 'classnames';
import { Popup } from 'UI';
import { Tooltip } from 'UI';
import styles from './textEllipsis.module.css';
/** calculates text width in pixels
/** calculates text width in pixels
* by creating a hidden element with
* text and counting its width
* @param text String - text string
@ -12,96 +12,100 @@ import styles from './textEllipsis.module.css';
* @returns width number
*/
function findTextWidth(text, fontProp) {
const tag = document.createElement('div')
const tag = document.createElement('div');
tag.style.position = 'absolute'
tag.style.left = '-99in'
tag.style.whiteSpace = 'nowrap'
tag.style.font = fontProp
tag.innerHTML = text
tag.style.position = 'absolute';
tag.style.left = '-99in';
tag.style.whiteSpace = 'nowrap';
tag.style.font = fontProp;
tag.innerHTML = text;
document.body.appendChild(tag)
const result = tag.clientWidth
document.body.removeChild(tag)
document.body.appendChild(tag);
const result = tag.clientWidth;
document.body.removeChild(tag);
return result;
return result;
}
const Trigger = forwardRef(({ textOrChildren, maxWidth, style, className, ...rest }, ref) => (
<div
className={ cn(styles.textEllipsis, className) }
style={{ maxWidth, ...style }}
ref={ref}
{ ...rest }
>
{ textOrChildren }
</div>
))
<div
className={cn(styles.textEllipsis, className)}
style={{ maxWidth, ...style }}
ref={ref}
{...rest}
>
{textOrChildren}
</div>
));
const TextEllipsis = ({
text,
hintText = text,
children = null,
maxWidth="auto",
style = {},
className="",
noHint=false,
popupProps={},
hintProps={},
...props
const TextEllipsis = ({
text,
hintText = text,
children = null,
maxWidth = 'auto',
style = {},
className = '',
noHint = false,
popupProps = {},
hintProps = {},
...props
}) => {
const [showPopup, setShowPopup] = useState(false)
const [computed, setComputed] = useState(false)
const textRef = useRef(null);
const [showPopup, setShowPopup] = useState(false);
const [computed, setComputed] = useState(false);
const textRef = useRef(null);
const textOrChildren = text || children;
const popupId = (Math.random() + 1).toString(36).substring(2);
const textOrChildren = text || children;
useEffect(() => {
if (computed) return;
if (textRef.current) {
const element = textRef.current;
const popupId = (Math.random() + 1).toString(36).substring(2);
const fontSize = window.getComputedStyle(element, null).getPropertyValue('font-size');
const textWidth = findTextWidth(element.innerText, fontSize)
if (textWidth > element.clientWidth) setShowPopup(true)
else setShowPopup(false)
setComputed(true)
}
}, [textRef.current, computed])
useEffect(() => {
if (computed) return;
if (textRef.current) {
const element = textRef.current;
if (noHint || !showPopup) return (
<Trigger
className={className}
maxWidth={maxWidth}
style={style}
textOrChildren={textOrChildren}
ref={textRef}
{...props}
/>
)
const fontSize = window.getComputedStyle(element, null).getPropertyValue('font-size');
return (
<Popup
content={ <div className="customPopupText" { ...hintProps } >{ hintText || textOrChildren }</div> }
{ ...popupProps }
>
<Trigger
className={className}
maxWidth={maxWidth}
style={style}
textOrChildren={textOrChildren}
id={popupId}
ref={textRef}
{...props}
/>
</Popup>
);
const textWidth = findTextWidth(element.innerText, fontSize);
if (textWidth > element.clientWidth) setShowPopup(true);
else setShowPopup(false);
setComputed(true);
}
}, [textRef.current, computed]);
if (noHint || !showPopup)
return (
<Trigger
className={className}
maxWidth={maxWidth}
style={style}
textOrChildren={textOrChildren}
ref={textRef}
{...props}
/>
);
return (
<Tooltip
title={
<div className="customPopupText" {...hintProps}>
{hintText || textOrChildren}
</div>
}
{...popupProps}
>
<Trigger
className={className}
maxWidth={maxWidth}
style={style}
textOrChildren={textOrChildren}
id={popupId}
ref={textRef}
{...props}
/>
</Tooltip>
);
};
TextEllipsis.displayName ="TextEllipsis";
TextEllipsis.displayName = 'TextEllipsis';
export default TextEllipsis;

View file

@ -1,6 +1,6 @@
import React from 'react';
import cn from 'classnames';
import { Icon, Popup } from 'UI';
import { Icon, Tooltip } from 'UI';
import styles from './textLabel.module.css';
export default function TextLabel({
@ -12,26 +12,27 @@ export default function TextLabel({
textTransform = '',
color = 'gray-medium',
iconColor = color,
}) {
}) {
return (
<div className={ cn("flex items-center", styles.sessionLabel) } style={ { minWidth: `${ minWidth }` } }>
<Icon name={ icon } size="16" color={ iconColor } />
{ popupLabel ?
<Popup
content={ popupLabel }
size="mini"
inverted
>
<div style={ { maxWidth: `${ maxWidth }px` } } className={textTransform}>{ label }</div>
</Popup>
:
<div
className={cn('flex items-center', styles.sessionLabel)}
style={{ minWidth: `${minWidth}` }}
>
<Icon name={icon} size="16" color={iconColor} />
{popupLabel ? (
<Tooltip title={popupLabel}>
<div style={{ maxWidth: `${maxWidth}px` }} className={textTransform}>
{label}
</div>
</Tooltip>
) : (
<div
style={ { maxWidth: `${ maxWidth }px`, lineHeight: '16px' } }
style={{ maxWidth: `${maxWidth}px`, lineHeight: '16px' }}
className={cn(`color-${color}`, textTransform)} // textTransform by tailwind
>
{ label }
{label}
</div>
}
)}
</div>
);
}

View file

@ -1,24 +1,19 @@
import React from 'react'
import { Icon, Popup } from 'UI'
import stl from './timelinePointer.module.css'
import cn from 'classnames'
import React from 'react';
import { Icon, Tooltip } from 'UI';
import stl from './timelinePointer.module.css';
import cn from 'classnames';
function TimelinePointer({ icon, content }) {
return (
<Popup
// offset={20}
// pinned
distance={15}
content={content}
>
<div className={cn(stl.wrapper, 'flex items-center justify-center relative')}>
<Tooltip title={content}>
<div className={cn(stl.wrapper, 'flex items-center justify-center relative')}>
<div className={stl.pin} />
<div style={{ top: '3px' }} className={stl.icon} >
<div style={{ top: '3px' }} className={stl.icon}>
<Icon name={icon} size="18" style={{ fill: '#D3545F' }} />
</div>
</div>
</Popup>
)
</Tooltip>
);
}
export default TimelinePointer
export default TimelinePointer;

View file

@ -87,9 +87,9 @@ export const TooltipAnchor = React.forwardRef<
}
return (
<button ref={ref} {...state.getReferenceProps(props)}>
<div ref={ref} {...state.getReferenceProps(props)}>
{children}
</button>
</div>
);
});

View file

@ -11,9 +11,18 @@ interface Props {
placement?: Placement;
className?: string;
delay?: number;
style?: any;
}
function Tooltip(props: Props) {
const { title, disabled = false, open = false, placement, className = '', delay = 500 } = props;
const {
title,
disabled = false,
open = false,
placement,
className = '',
delay = 500,
style = {},
} = props;
const state = useTooltipState({ disabled: disabled, placement, delay });
return (
<>

View file

@ -1,47 +0,0 @@
import React from 'react';
import { Popup } from 'UI';
import { useTooltipState, TooltipAnchor, FloatingTooltip } from './FloatingTooltip';
interface Props {
timeout: number;
position: string;
tooltip: string;
trigger: React.ReactNode;
}
export default class Tooltip extends React.PureComponent<Props> {
static defaultProps = {
timeout: 500,
};
state = {
open: false,
};
mouseOver = false;
onMouseEnter = () => {
this.mouseOver = true;
setTimeout(() => {
if (this.mouseOver) this.setState({ open: true });
}, this.props.timeout);
};
onMouseLeave = () => {
this.mouseOver = false;
this.setState({
open: false,
});
};
render() {
const { trigger, tooltip, position } = this.props;
const { open } = this.state;
return (
<Popup open={open} content={tooltip} disabled={!tooltip} position={position}>
<span //TODO: no wrap component around
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
>
{trigger}
</span>
</Popup>
);
}
}

View file

@ -3,7 +3,6 @@ export { default as Link } from './Link';
// export { default as Dropdown } from './Dropdown';
export { default as Button } from './Button';
export { default as Label } from './Label';
export { default as Popup } from './Popup';
export { default as Progress } from './Progress';
export { default as SlideModal } from './SlideModal';
export { default as NoContent } from './NoContent';

View file

@ -15,166 +15,177 @@ function hashString(s: string): number {
let hash = 0;
for (let i = 0; i < s.length; i++) {
hash = (hash + s.charCodeAt(i) * mul) % HASH_MOD;
mul = (mul*HASH_P) % HASH_MOD;
mul = (mul * HASH_P) % HASH_MOD;
}
return hash;
}
export default Record({
sessionId: '',
pageTitle: '',
active: false,
siteId: '',
projectKey: '',
peerId: '',
live: false,
startedAt: 0,
duration: 0,
events: List(),
logs: List(),
stackEvents: List(),
resources: List(),
missedResources: List(),
metadata: Map(),
favorite: false,
filterId: '',
messagesUrl: '',
domURL: [],
devtoolsURL: [],
mobsUrl: [], // @depricated
userBrowser: '',
userBrowserVersion: '?',
userCountry: '',
userDevice: '',
userDeviceType: '',
isMobile: false,
userOs: '',
userOsVersion: '',
userId: '',
userAnonymousId: '',
userUuid: undefined,
userDisplayName: "",
userNumericHash: 0,
viewed: false,
consoleLogCount: '?',
eventsCount: '?',
pagesCount: '?',
clickRage: undefined,
clickRageTime: undefined,
resourcesScore: 0,
consoleError: undefined,
resourceError: undefined,
returningLocation: undefined,
returningLocationTime: undefined,
errorsCount: 0,
watchdogs: [],
issueTypes: [],
issues: [],
userDeviceHeapSize: 0,
userDeviceMemorySize: 0,
errors: List(),
crashes: [],
socket: null,
isIOS: false,
revId: '',
userSessionsCount: 0,
agentIds: [],
isCallActive: false,
agentToken: '',
notes: [],
notesWithEvents: [],
fileKey: '',
}, {
fromJS:({
startTs=0,
timestamp = 0,
backendErrors=0,
consoleErrors=0,
projectId,
errors,
stackEvents = [],
issues = [],
sessionId,
sessionID,
domURL = [],
devtoolsURL= [],
mobsUrl = [],
notes = [],
...session
}) => {
const duration = Duration.fromMillis(session.duration < 1000 ? 1000 : session.duration);
const durationSeconds = duration.valueOf();
const startedAt = +startTs || +timestamp;
const userDevice = session.userDevice || session.userDeviceType || 'Other';
const userDeviceType = session.userDeviceType || 'other';
const isMobile = [ 'console', 'mobile', 'tablet' ].includes(userDeviceType)
const events = List(session.events)
.map(e => SessionEvent({ ...e, time: e.timestamp - startedAt }))
.filter(({ type, time }) => type !== TYPES.CONSOLE && time <= durationSeconds);
let resources = List(session.resources)
.map(Resource);
// this code shoud die.
const firstResourceTime = resources.map(r => r.time).reduce((a,b)=>Math.min(a,b), Number.MAX_SAFE_INTEGER);
resources = resources
.map(r => r.set("time", r.time - firstResourceTime))
.sort((r1, r2) => r1.time - r2.time);
const missedResources = resources.filter(({ success }) => !success);
const logs = List(session.logs).map(Log);
const stackEventsList = List(stackEvents)
.concat(List(session.userEvents))
.sortBy(se => se.timestamp)
.map(se => StackEvent({ ...se, time: se.timestamp - startedAt }));
const exceptions = List(errors)
.map(SessionError)
const issuesList = List(issues)
.map(e => Issue({ ...e, time: e.timestamp - startedAt }))
const rawEvents = !session.events
? []
// @ts-ignore
: session.events.map(evt => ({ ...evt, time: evt.timestamp - startedAt })).filter(({ type, time }) => type !== TYPES.CONSOLE && time <= durationSeconds) || []
const rawNotes = notes
const notesWithEvents = [...rawEvents, ...rawNotes].sort((a, b) => {
const aTs = a.time || a.timestamp
const bTs = b.time || b.timestamp
return aTs - bTs
})
return {
...session,
isIOS: session.platform === "ios",
watchdogs: session.watchdogs || [],
errors: exceptions,
siteId: projectId,
events,
logs,
stackEvents: stackEventsList,
resources,
missedResources,
userDevice,
userDeviceType,
isMobile,
startedAt,
duration,
userNumericHash: hashString(session.userId || session.userAnonymousId || session.userUuid || session.userID || session.userUUID || ""),
userDisplayName: session.userId || session.userAnonymousId || session.userID || 'Anonymous User',
firstResourceTime,
issues: issuesList,
sessionId: sessionId || sessionID,
userId: session.userId || session.userID,
mobsUrl: Array.isArray(mobsUrl) ? mobsUrl : [ mobsUrl ],
domURL,
devtoolsURL,
notes,
notesWithEvents: List(notesWithEvents),
};
export default Record(
{
sessionId: '',
pageTitle: '',
active: false,
siteId: '',
projectKey: '',
peerId: '',
live: false,
startedAt: 0,
duration: 0,
events: List(),
logs: List(),
stackEvents: List(),
resources: List(),
missedResources: List(),
metadata: Map(),
favorite: false,
filterId: '',
messagesUrl: '',
domURL: [],
devtoolsURL: [],
mobsUrl: [], // @depricated
userBrowser: '',
userBrowserVersion: '?',
userCountry: '',
userDevice: '',
userDeviceType: '',
isMobile: false,
userOs: '',
userOsVersion: '',
userId: '',
userAnonymousId: '',
userUuid: undefined,
userDisplayName: '',
userNumericHash: 0,
viewed: false,
consoleLogCount: '?',
eventsCount: '?',
pagesCount: '?',
clickRage: undefined,
clickRageTime: undefined,
resourcesScore: 0,
consoleError: undefined,
resourceError: undefined,
returningLocation: undefined,
returningLocationTime: undefined,
errorsCount: 0,
watchdogs: [],
issueTypes: [],
issues: [],
userDeviceHeapSize: 0,
userDeviceMemorySize: 0,
errors: List(),
crashes: [],
socket: null,
isIOS: false,
revId: '',
userSessionsCount: 0,
agentIds: [],
isCallActive: false,
agentToken: '',
notes: [],
notesWithEvents: [],
fileKey: '',
},
idKey: "sessionId",
});
{
fromJS: ({
startTs = 0,
timestamp = 0,
backendErrors = 0,
consoleErrors = 0,
projectId,
errors,
stackEvents = [],
issues = [],
sessionId,
sessionID,
domURL = [],
devtoolsURL = [],
mobsUrl = [],
notes = [],
...session
}) => {
const duration = Duration.fromMillis(session.duration < 1000 ? 1000 : session.duration);
const durationSeconds = duration.valueOf();
const startedAt = +startTs || +timestamp;
const userDevice = session.userDevice || session.userDeviceType || 'Other';
const userDeviceType = session.userDeviceType || 'other';
const isMobile = ['console', 'mobile', 'tablet'].includes(userDeviceType);
const events = List(session.events)
.map((e) => SessionEvent({ ...e, time: e.timestamp - startedAt }))
.filter(({ type, time }) => type !== TYPES.CONSOLE && time <= durationSeconds);
let resources = List(session.resources).map(Resource);
// this code shoud die.
const firstResourceTime = resources
.map((r) => r.time)
.reduce((a, b) => Math.min(a, b), Number.MAX_SAFE_INTEGER);
resources = resources
.map((r) => r.set('time', r.time - firstResourceTime))
.sort((r1, r2) => r1.time - r2.time);
const missedResources = resources.filter(({ success }) => !success);
const logs = List(session.logs).map(Log);
const stackEventsList = List(stackEvents)
.concat(List(session.userEvents))
.sortBy((se) => se.timestamp)
.map((se) => StackEvent({ ...se, time: se.timestamp - startedAt }));
const exceptions = List(errors).map(SessionError);
const issuesList = List(issues).map((e) => Issue({ ...e, time: e.timestamp - startedAt }));
const rawEvents = !session.events
? []
: // @ts-ignore
session.events
.map((evt) => ({ ...evt, time: evt.timestamp - startedAt }))
.filter(({ type, time }) => type !== TYPES.CONSOLE && time <= durationSeconds) || [];
const rawNotes = notes;
const notesWithEvents = [...rawEvents, ...rawNotes].sort((a, b) => {
const aTs = a.time || a.timestamp;
const bTs = b.time || b.timestamp;
return aTs - bTs;
});
return {
...session,
isIOS: session.platform === 'ios',
watchdogs: session.watchdogs || [],
errors: exceptions,
siteId: projectId,
events,
logs,
stackEvents: stackEventsList,
resources,
missedResources,
userDevice,
userDeviceType,
isMobile,
startedAt,
duration,
userNumericHash: hashString(
session.userId ||
session.userAnonymousId ||
session.userUuid ||
session.userID ||
session.userUUID ||
''
),
userDisplayName:
session.userId || session.userAnonymousId || session.userID || 'Anonymous User',
firstResourceTime,
issues: issuesList,
sessionId: sessionId || sessionID,
userId: session.userId || session.userID,
mobsUrl: Array.isArray(mobsUrl) ? mobsUrl : [mobsUrl],
domURL,
devtoolsURL,
notes,
notesWithEvents: List(notesWithEvents),
};
},
idKey: 'sessionId',
}
);