change(ui) - popup replacement
This commit is contained in:
parent
f890015e55
commit
c73cb9e60a
76 changed files with 2088 additions and 2373 deletions
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 don’t 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 you’d 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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './ManageUsers';
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 don’t have the permissions to perform this action." disabled={isAdmin}>
|
||||
<Tooltip title="You don’t have the permissions to perform this action." disabled={isAdmin}>
|
||||
<Button variant="primary" onClick={() => editHandler({})}>Add</Button>
|
||||
</Popup>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 don’t 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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 don’t 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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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')}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 session’s 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 session’s 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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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']),
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import Popup from '.';
|
||||
|
||||
storiesOf('Popup', module)
|
||||
.add('Pure', () => (
|
||||
<Popup />
|
||||
))
|
||||
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './Popup';
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue