change(ui) - merge dev-tools and resovle conflicts

This commit is contained in:
Shekar Siri 2022-10-13 14:27:37 +02:00
commit 08ec8a67e1
86 changed files with 2616 additions and 707 deletions

View file

@ -2,27 +2,27 @@ import logger from 'App/logger';
import APIClient from './api_client';
import { UPDATE, DELETE } from './duck/jwt';
export default store => next => (action) => {
export default (store) => (next) => (action) => {
const { types, call, ...rest } = action;
if (!call) {
return next(action);
}
const [ REQUEST, SUCCESS, FAILURE ] = types;
const [REQUEST, SUCCESS, FAILURE] = types;
next({ ...rest, type: REQUEST });
const client = new APIClient();
return call(client)
.then(async response => {
.then(async (response) => {
if (response.status === 403) {
next({ type: DELETE });
}
if (!response.ok) {
const text = await response.text()
const text = await response.text();
return Promise.reject(text);
}
return response.json()
return response.json();
})
.then(json => json || {}) // TEMP TODO on server: no empty responces
.then((json) => json || {}) // TEMP TODO on server: no empty responces
.then(({ jwt, errors, data }) => {
if (errors) {
next({ type: FAILURE, errors, data });
@ -34,14 +34,22 @@ export default store => next => (action) => {
}
})
.catch((e) => {
logger.error("Error during API request. ", e)
return next({ type: FAILURE, errors: JSON.parse(e).errors || [] });
logger.error('Error during API request. ', e);
return next({ type: FAILURE, errors: parseError(e) });
});
};
function parseError(e) {
try {
return JSON.parse(e).errors || [];
} catch {
return e;
}
}
function jwtExpired(token) {
try {
const base64Url = token.split('.')[ 1 ];
const base64Url = token.split('.')[1];
const base64 = base64Url.replace('-', '+').replace('_', '/');
const tokenObj = JSON.parse(window.atob(base64));
return tokenObj.exp * 1000 < Date.now(); // exp in Unix time (sec)

View file

@ -34,7 +34,7 @@ function Notifications(props: Props) {
<div className={ stl.counter } data-hidden={ count === 0 }>
{ count }
</div>
<Icon name="bell" size="18" />
<Icon name="bell-fill" size="18" />
</div>
</Popup>
));

View file

@ -0,0 +1,71 @@
import React from 'react';
import { NavLink, withRouter } from 'react-router-dom';
import {
sessions,
metrics,
assist,
client,
dashboard,
withSiteId,
CLIENT_DEFAULT_TAB,
} from 'App/routes';
import SiteDropdown from '../SiteDropdown';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import styles from '../header.module.css';
const DASHBOARD_PATH = dashboard();
const METRICS_PATH = metrics();
const SESSIONS_PATH = sessions();
const ASSIST_PATH = assist();
interface Props {
siteId: any;
}
function DefaultMenuView(props: Props) {
const { siteId } = props;
return (
<div className="flex items-center">
<NavLink to={withSiteId(SESSIONS_PATH, props.siteId)}>
<div className="relative select-none">
<div className="px-4 py-2">
<AnimatedSVG name={ICONS.LOGO_SMALL} size="30" />
</div>
<div className="absolute bottom-0" style={{ fontSize: '7px', right: '5px' }}>
v{window.env.VERSION}
</div>
</div>
</NavLink>
<SiteDropdown />
{/* <div className={ styles.divider } /> */}
<NavLink
to={withSiteId(SESSIONS_PATH, siteId)}
className={styles.nav}
activeClassName={styles.active}
>
{'Sessions'}
</NavLink>
<NavLink
to={withSiteId(ASSIST_PATH, siteId)}
className={styles.nav}
activeClassName={styles.active}
>
{'Assist'}
</NavLink>
<NavLink
to={withSiteId(DASHBOARD_PATH, siteId)}
className={styles.nav}
activeClassName={styles.active}
isActive={(_, location) => {
return (
location.pathname.includes(DASHBOARD_PATH) || location.pathname.includes(METRICS_PATH)
);
}}
>
<span>{'Dashboards'}</span>
</NavLink>
</div>
);
}
export default DefaultMenuView;

View file

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

View file

@ -16,10 +16,11 @@ import { logout } from 'Duck/user';
import { Icon, Popup } from 'UI';
import SiteDropdown from './SiteDropdown';
import styles from './header.module.css';
import OnboardingExplore from './OnboardingExplore/OnboardingExplore'
import OnboardingExplore from './OnboardingExplore/OnboardingExplore';
import Announcements from '../Announcements';
import Notifications from '../Alerts/Notifications';
import { init as initSite } from 'Duck/site';
import { getInitials } from 'App/utils';
import ErrorGenPanel from 'App/dev/components';
import Alerts from '../Alerts/Alerts';
@ -27,6 +28,10 @@ import AnimatedSVG, { ICONS } from '../shared/AnimatedSVG/AnimatedSVG';
import { fetchListActive as fetchMetadata } from 'Duck/customField';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
import UserMenu from './UserMenu';
import SettingsMenu from './SettingsMenu';
import DefaultMenuView from './DefaultMenuView';
import PreferencesView from './PreferencesView';
const DASHBOARD_PATH = dashboard();
const ALERTS_PATH = alerts();
@ -37,20 +42,25 @@ const CLIENT_PATH = client(CLIENT_DEFAULT_TAB);
const Header = (props) => {
const {
sites, location, account,
onLogoutClick, siteId,
boardingCompletion = 100, showAlerts = false,
sites,
location,
account,
onLogoutClick,
siteId,
boardingCompletion = 100,
showAlerts = false,
} = props;
const name = account.get('name').split(" ")[0];
const [hideDiscover, setHideDiscover] = useState(false)
const name = account.get('name');
const [hideDiscover, setHideDiscover] = useState(false);
const { userStore, notificationStore } = useStore();
const initialDataFetched = useObserver(() => userStore.initialDataFetched);
let activeSite = null;
const isPreferences = window.location.pathname.includes('/client/');
const onAccountClick = () => {
props.history.push(CLIENT_PATH);
}
};
useEffect(() => {
if (!account.id || initialDataFetched) return;
@ -67,92 +77,62 @@ const Header = (props) => {
}, [account]);
useEffect(() => {
activeSite = sites.find(s => s.id == siteId);
activeSite = sites.find((s) => s.id == siteId);
props.initSite(activeSite);
}, [siteId])
}, [siteId]);
return (
<div className={ cn(styles.header) } style={{ height: '50px'}}>
<NavLink to={ withSiteId(SESSIONS_PATH, siteId) }>
<div className="relative select-none">
<div className="px-4 py-2">
<AnimatedSVG name={ICONS.LOGO_SMALL} size="30" />
</div>
<div className="absolute bottom-0" style={{ fontSize: '7px', right: '5px' }}>v{window.env.VERSION}</div>
</div>
</NavLink>
<SiteDropdown />
<div className={ styles.divider } />
<NavLink
to={ withSiteId(SESSIONS_PATH, siteId) }
className={ styles.nav }
activeClassName={ styles.active }
>
{ 'Sessions' }
</NavLink>
<NavLink
to={ withSiteId(ASSIST_PATH, siteId) }
className={ styles.nav }
activeClassName={ styles.active }
>
{ 'Assist' }
</NavLink>
<NavLink
to={ withSiteId(DASHBOARD_PATH, siteId) }
className={ styles.nav }
activeClassName={ styles.active }
isActive={ (_, location) => {
return location.pathname.includes(DASHBOARD_PATH)
|| location.pathname.includes(METRICS_PATH)
|| location.pathname.includes(ALERTS_PATH)
}}
>
<span>{ 'Dashboards' }</span>
</NavLink>
<div className={ styles.right }>
<Announcements />
<div className={ styles.divider } />
{ (boardingCompletion < 100 && !hideDiscover) && (
<div
className={cn(styles.header, 'fixed w-full bg-white flex justify-between')}
style={{ height: '50px' }}
>
{!isPreferences && <DefaultMenuView siteId={siteId} />}
{isPreferences && <PreferencesView />}
<div className={styles.right}>
{boardingCompletion < 100 && !hideDiscover && (
<React.Fragment>
<OnboardingExplore onComplete={() => setHideDiscover(true)} />
<div className={ styles.divider } />
</React.Fragment>
)}
<Notifications />
<div className={ styles.divider } />
<Popup content={ `Preferences` } >
<NavLink to={ CLIENT_PATH } className={ styles.headerIcon }><Icon name="cog" size="20" /></NavLink>
<Popup content={`Preferences`} disabled>
<div className="group relative">
<NavLink to={CLIENT_PATH} className={styles.headerIcon}>
<Icon name="gear-fill" size="20" />
</NavLink>
<SettingsMenu className="invisible group-hover:visible" account={account} />
</div>
</Popup>
<div className={ styles.divider } />
<div className={ styles.userDetails }>
<div className={cn(styles.userDetails, 'group cursor-pointer')}>
<div className="flex items-center">
<div className="mr-5">{ name }</div>
<Icon color="gray-medium" name="ellipsis-v" size="24" />
<div className="w-10 h-10 bg-tealx rounded-full flex items-center justify-center color-white">
{getInitials(name)}
</div>
</div>
<ul>
<li><button onClick={ onAccountClick }>{ 'Account' }</button></li>
<li><button onClick={ onLogoutClick }>{ 'Logout' }</button></li>
</ul>
<UserMenu className="invisible group-hover:visible" />
</div>
{<ErrorGenPanel />}
</div>
{ <ErrorGenPanel/> }
{showAlerts && <Alerts />}
</div>
);
};
export default withRouter(connect(
state => ({
account: state.getIn([ 'user', 'account' ]),
siteId: state.getIn([ 'site', 'siteId' ]),
sites: state.getIn([ 'site', 'list' ]),
showAlerts: state.getIn([ 'dashboard', 'showAlerts' ]),
boardingCompletion: state.getIn([ 'dashboard', 'boardingCompletion' ])
}),
{ onLogoutClick: logout, initSite, fetchMetadata },
)(Header));
export default withRouter(
connect(
(state) => ({
account: state.getIn(['user', 'account']),
siteId: state.getIn(['site', 'siteId']),
sites: state.getIn(['site', 'list']),
showAlerts: state.getIn(['dashboard', 'showAlerts']),
boardingCompletion: state.getIn(['dashboard', 'boardingCompletion']),
}),
{ onLogoutClick: logout, initSite, fetchMetadata }
)(Header)
);

View file

@ -24,13 +24,17 @@ function NewProjectButton(props: Props) {
};
return (
<div
className={cn('flex items-center justify-center py-3 cursor-pointer hover:bg-active-blue ', { disabled: !canAddProject })}
onClick={onClick}
>
<Icon name="plus" size={12} className="mr-2" color="teal" />
<span className="color-teal">Add New Project</span>
</div>
<li onClick={onClick}>
<Icon name="folder-plus" size="16" color="teal" />
<span className="ml-3 color-teal">Add Project</span>
</li>
// <div
// className={cn('flex items-center justify-center py-3 cursor-pointer hover:bg-active-blue ', { disabled: !canAddProject })}
// onClick={onClick}
// >
// <Icon name="plus" size={12} className="mr-2" color="teal" />
// <span className="color-teal">Add New Project</span>
// </div>
);
}

View file

@ -0,0 +1,28 @@
import React from 'react';
import { Icon } from 'UI';
import { withRouter } from 'react-router-dom';
import ProjectCodeSnippet from 'App/components/Onboarding/components/OnboardingTabs/ProjectCodeSnippet';
interface Props {
history: any;
}
function PreferencesView(props: Props) {
const onExit = () => {
props.history.push('/');
};
return (
<>
<div className="flex items-center p-3 cursor-pointer text-lg ml-1" onClick={onExit}>
<Icon name="arrow-bar-left" color="teal" size="18" />
<span className="color-teal ml-2">Exit Preferences</span>
</div>
<div className="flex items-center p-3">
<Icon name="info-circle" size="16" color="gray-dark" />
<span className="ml-2">Changes applied at organization level</span>
</div>
</>
);
}
export default withRouter(PreferencesView);

View file

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

View file

@ -0,0 +1,66 @@
import React from 'react';
import cn from 'classnames';
import { Icon } from 'UI';
import { CLIENT_TABS, client as clientRoute } from 'App/routes';
import { withRouter, RouteComponentProps } from 'react-router';
interface Props {
history: any;
className: string;
account: any;
}
function SettingsMenu(props: RouteComponentProps<Props>) {
const { history, account, className }: any = props;
const isAdmin = account.admin || account.superAdmin;
const navigateTo = (path: any) => {
switch (path) {
case 'projects':
return history.push(clientRoute(CLIENT_TABS.SITES));
case 'team':
return history.push(clientRoute(CLIENT_TABS.MANAGE_USERS));
case 'metadata':
return history.push(clientRoute(CLIENT_TABS.CUSTOM_FIELDS));
case 'webhooks':
return history.push(clientRoute(CLIENT_TABS.WEBHOOKS));
case 'integrations':
return history.push(clientRoute(CLIENT_TABS.INTEGRATIONS));
case 'notifications':
return history.push(clientRoute(CLIENT_TABS.NOTIFICATIONS));
}
};
return (
<div
style={{ width: '150px' }}
className={cn(className, 'absolute right-0 top-0 bg-white border mt-14')}
>
{isAdmin && (
<>
<MenuItem onClick={() => navigateTo('projects')} label="Projects" icon="folder2" />
<MenuItem onClick={() => navigateTo('team')} label="Team" icon="users" />
</>
)}
<MenuItem onClick={() => navigateTo('metadata')} label="Metadata" icon="tags" />
<MenuItem onClick={() => navigateTo('webhooks')} label="Webhooks" icon="link-45deg" />
<MenuItem onClick={() => navigateTo('integrations')} label="Integrations" icon="puzzle" />
<MenuItem
onClick={() => navigateTo('notifications')}
label="Notifications"
icon="bell-slash"
/>
</div>
);
}
export default withRouter(SettingsMenu);
function MenuItem({ onClick, label, icon }: any) {
return (
<div
className="border-t p-3 cursor-pointer flex items-center hover:bg-active-blue"
onClick={onClick}
>
<Icon name={icon} size="16" />
<button className="ml-2">{label}</button>
</div>
);
}

View file

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

View file

@ -3,7 +3,6 @@ import { connect } from 'react-redux';
import { setSiteId } from 'Duck/site';
import { withRouter } from 'react-router-dom';
import { hasSiteId, siteChangeAvaliable } from 'App/routes';
import { STATUS_COLOR_MAP, GREEN } from 'Types/site';
import { Icon } from 'UI';
import { pushNewSite } from 'Duck/user';
import { init } from 'Duck/site';
@ -13,7 +12,6 @@ import { clearSearch } from 'Duck/search';
import { clearSearch as clearSearchLive } from 'Duck/liveSearch';
import { fetchListActive as fetchIntegrationVariables } from 'Duck/customField';
import { withStore } from 'App/mstore';
import AnimatedSVG, { ICONS } from '../shared/AnimatedSVG/AnimatedSVG';
import NewProjectButton from './NewProjectButton';
@withStore
@ -63,37 +61,27 @@ export default class SiteDropdown extends React.PureComponent {
account,
location: { pathname },
} = this.props;
const { showProductModal } = this.state;
const isAdmin = account.admin || account.superAdmin;
const activeSite = sites.find((s) => s.id == siteId);
const disabled = !siteChangeAvaliable(pathname);
const showCurrent = hasSiteId(pathname) || siteChangeAvaliable(pathname);
// const canAddSites = isAdmin && account.limits.projects && account.limits.projects.remaining !== 0;
return (
<div className={styles.wrapper}>
{showCurrent ? (
activeSite && activeSite.status === GREEN ? (
<AnimatedSVG name={ICONS.SIGNAL_GREEN} size="10" />
) : (
<AnimatedSVG name={ICONS.SIGNAL_RED} size="10" />
)
) : (
<Icon name="window-alt" size="14" marginRight="10" />
)}
<div className={cn(styles.currentSite, 'ml-2')}>{showCurrent && activeSite ? activeSite.host : 'All Projects'}</div>
<Icon className={styles.drodownIcon} color="gray-light" name="chevron-down" size="16" />
<div className={styles.menu}>
<ul data-can-disable={disabled}>
{!showCurrent && <li>{'Project selection is not applicable.'}</li>}
{isAdmin && (
<NewProjectButton onClick={this.newSite} isAdmin={isAdmin} />
)}
{sites.map((site) => (
<li key={site.id} onClick={() => this.switchSite(site.id)}>
<div className="w-2 h-2 rounded-full mr-3" style={{ backgroundColor: STATUS_COLOR_MAP[site.status] }} />
{site.host}
<Icon name="folder2" size="16" />
<span className="ml-3">{site.host}</span>
</li>
))}
</ul>
<NewProjectButton onClick={this.newSite} isAdmin={isAdmin} />
</div>
</div>
);

View file

@ -0,0 +1,57 @@
import React from 'react';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { connect } from 'react-redux';
import { logout } from 'Duck/user';
import { client, CLIENT_DEFAULT_TAB } from 'App/routes';
import { Icon } from 'UI';
import cn from 'classnames';
import { getInitials } from 'App/utils';
const CLIENT_PATH = client(CLIENT_DEFAULT_TAB);
interface Props {
history: any;
onLogoutClick: any;
className: string;
account: any;
}
function UserMenu(props: RouteComponentProps<Props>) {
const { account, history, className, onLogoutClick }: any = props;
const onAccountClick = () => {
history.push(CLIENT_PATH);
};
return (
<div
style={{ width: '250px' }}
className={cn(className, 'absolute right-0 top-0 bg-white border mt-14')}
>
<div className="flex items-center p-3">
<div className="w-10 h-10 bg-tealx rounded-full flex items-center justify-center mr-2 color-white">
{getInitials(account.name)}
</div>
<div>
<div className="color-teal font-medium leading-none">{account.name}</div>
<div className="color-gray-medium">{account.superAdmin ? 'Super Admin' : (account.admin ? 'Admin' : 'Member') } - {account.email}</div>
</div>
</div>
<div className="border-t flex items-center hover:bg-active-blue p-3" onClick={onAccountClick}>
<Icon name="user-circle" size="16" />
<button className="ml-2">{'Account'}</button>
</div>
<div className="border-t flex items-center hover:bg-active-blue p-3" onClick={onLogoutClick}>
<Icon name="door-closed" size="16" />
<button className="ml-2">{'Logout'}</button>
</div>
</div>
);
}
export default connect(
(state: any) => ({
account: state.getIn(['user', 'account']),
}),
{ onLogoutClick: logout }
)(withRouter(UserMenu)) as React.FunctionComponent<RouteComponentProps<Props>>;
// export default UserMenu;

View file

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

View file

@ -4,13 +4,13 @@
$height: 50px;
.header {
position: fixed;
width: 100%;
display: flex;
justify-content: space-between;
/* position: fixed; */
/* width: 100%; */
/* display: flex; */
/* justify-content: space-between; */
border-bottom: solid thin $gray-light;
/* padding: 0 15px; */
background: $white;
/* background: $white; */
z-index: $header;
}
@ -45,7 +45,7 @@ $height: 50px;
}
.right {
margin-left: auto;
/* margin-left: auto; */
position: relative;
cursor: default;
display: flex;
@ -72,11 +72,10 @@ $height: 50px;
.userDetails {
display: flex;
align-items: center;
justify-content: flex-end;
justify-content: center;
position: relative;
padding: 0 5px 0 15px;
padding: 0 10px;
transition: all 0.2s;
min-width: 100px;
&:hover {
background-color: $gray-lightest;
@ -102,7 +101,7 @@ $height: 50px;
border-top: 1px solid $gray-light;
}
}
& a, & button {
/* & a, & button {
color: $gray-darkest;
display: block;
cursor: pointer;
@ -113,7 +112,7 @@ $height: 50px;
&:hover {
background-color: $gray-lightest;
}
}
} */
}
.userIcon {

View file

@ -1,15 +1,20 @@
.wrapper {
display: flex;
align-items: center;
border-left: solid thin $gray-light !important;
/* border-left: solid thin $gray-light !important; */
padding: 10px 10px;
min-width: 180px;
justify-content: flex-start;
position: relative;
user-select: none;
border: solid thin transparent;
height: 30px;
border-radius: 3px;
margin: 10px;
&:hover {
background-color: $gray-lightest;
background-color: $active-blue;
/* border: solid thin $active-blue-border; */
& .drodownIcon {
transform: rotate(180deg);
transition: all 0.2s;
@ -39,11 +44,12 @@
& .menu {
display: none;
position: absolute;
top: 50px;
top: 28px;
left: -1px;
background-color: white;
min-width: 200px;
z-index: 2;
border-radius: 3px;
border: 1px solid $gray-light;
}
@ -68,9 +74,13 @@
&:hover {
background-color: $gray-lightest;
transition: all 0.2s;
color: $teal;
svg {
fill: $teal;
}
}
&:first-child {
border-top: 1px solid $gray-light;
/* border-top: 1px solid $gray-light; */
}
}
}

View file

@ -112,15 +112,15 @@ export default class Autoscroll extends React.PureComponent<Props, {
{children}
</div>
<div className={stl.navButtons}>
{/* <label><input type={'checkbox'} checked={this.state.autoScroll} onChange={(e) => this.setState({ autoScroll: !this.state.autoScroll })} /> Autoscroll</label> */}
{/* <div className={stl.navButtons}>
<label><input type={'checkbox'} checked={this.state.autoScroll} onChange={(e) => this.setState({ autoScroll: !this.state.autoScroll })} /> Autoscroll</label>
{navigation && (
<>
<IconButton size="small" icon="chevron-up" onClick={this.onPrevClick} />
<IconButton size="small" icon="chevron-down" onClick={this.onNextClick} className="mt-5" />
</>
)}
</div>
</div> */}
</div>
);

View file

@ -6,7 +6,7 @@
display: flex;
align-items: center;
& >.infoPoint {
font-size: 12px;
font-size: 14px;
display: flex;
align-items: center;
&:not(:last-child):after {

View file

@ -7,7 +7,8 @@ import { LEVEL } from 'Types/session/log';
import Autoscroll from '../Autoscroll';
import BottomBlock from '../BottomBlock';
import stl from './console.module.css';
import { Duration } from 'luxon';
import ConsoleRow from './ConsoleRow';
// import { Duration } from 'luxon';
const ALL = 'ALL';
const INFO = 'INFO';
@ -83,44 +84,34 @@ export default class ConsoleContent extends React.PureComponent {
<Tabs tabs={TABS} active={activeTab} onClick={this.onTabClick} border={false} />
</div>
<Input
className="input-small"
className="input-small h-8"
placeholder="Filter by keyword"
icon="search"
iconPosition="left"
name="filter"
height={28}
onChange={this.onFilterChange}
/>
</BottomBlock.Header>
<BottomBlock.Content>
<NoContent title={
<div className="capitalize flex items-center mt-16">
<NoContent
title={
<div className="capitalize flex items-center mt-16">
<Icon name="info-circle" className="mr-2" size="18" />
No Data
</div>
} size="small" show={filtered.length === 0}>
</div>
}
size="small"
show={filtered.length === 0}
>
<Autoscroll autoScrollTo={Math.max(lastIndex, 0)}>
{filtered.map((l, index) => (
<div
className={cn('flex py-2 px-4', {
info: !l.isYellow() && !l.isRed(),
warn: l.isYellow(),
error: l.isRed(),
[stl.activeRow]: lastIndex === index,
// [stl.inactiveRow]: index > lastIndex,
'cursor-pointer': !isResult,
})}
onClick={() => !isResult && jump(l.time)}
>
<div className={cn(stl.timestamp)}>
<Icon size="14" className={stl.icon} {...getIconProps(l.level)} />
</div>
<div className={cn(stl.timestamp, {})}>
{Duration.fromMillis(l.time).toFormat('mm:ss.SSS')}
</div>
<div key={l.key} className={cn(stl.line)} data-scroll-item={l.isRed()}>
<div className={stl.message}>{renderWithNL(l.value)}</div>
</div>
</div>
{filtered.map((l) => (
<ConsoleRow
log={l}
jump={jump}
iconProps={getIconProps(l.level)}
renderWithNL={renderWithNL}
/>
))}
</Autoscroll>
</NoContent>

View file

@ -0,0 +1,48 @@
import React, { useState } from 'react';
import cn from 'classnames';
import stl from '../console.module.css';
import { Icon } from 'UI';
import JumpButton from 'Shared/DevTools/JumpButton';
interface Props {
log: any;
iconProps: any;
jump?: any;
renderWithNL?: any;
}
function ConsoleRow(props: Props) {
const { log, iconProps, jump, renderWithNL } = props;
const [expanded, setExpanded] = useState(false);
const lines = log.value.split('\n').filter((l: any) => !!l);
const canExpand = lines.length > 1;
return (
<div
className={cn(stl.line, 'flex py-2 px-4 overflow-hidden group relative select-none', {
info: !log.isYellow() && !log.isRed(),
warn: log.isYellow(),
error: log.isRed(),
'cursor-pointer': canExpand,
})}
onClick={() => setExpanded(!expanded)}
>
<div className={cn(stl.timestamp)}>
<Icon size="14" className={stl.icon} {...iconProps} />
</div>
{/* <div className={cn(stl.timestamp, {})}>
{Duration.fromMillis(log.time).toFormat('mm:ss.SSS')}
</div> */}
<div key={log.key} className={cn('')} data-scroll-item={log.isRed()}>
<div className={cn(stl.message, 'flex items-center')}>
{canExpand && (
<Icon name={expanded ? 'caret-down-fill' : 'caret-right-fill'} className="mr-2" />
)}
<span>{renderWithNL(lines.pop())}</span>
</div>
{canExpand && expanded && lines.map((l: any) => <div className="ml-4 mb-1">{l}</div>)}
</div>
<JumpButton onClick={() => jump(log.time)} />
</div>
);
}
export default ConsoleRow;

View file

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

View file

@ -15,6 +15,9 @@
display: flex;
align-items: flex-start;
border-bottom: solid thin $gray-light-shade;
&:hover {
background-coor: $active-blue !important;
}
}
.timestamp {

View file

@ -22,7 +22,7 @@ import BottomBlock from '../BottomBlock';
@connectPlayer((state) => ({
logs: state.logListNow,
exceptions: state.exceptionsList,
exceptionsNow: state.exceptionsListNow,
// exceptionsNow: state.exceptionsListNow,
}))
@connect(
(state) => ({
@ -55,15 +55,15 @@ export default class Exceptions extends React.PureComponent {
const filtered = exceptions.filter((e) => filterRE.test(e.name) || filterRE.test(e.message));
let lastIndex = -1;
filtered.forEach((item, index) => {
if (
this.props.exceptionsNow.length > 0 &&
item.time <= this.props.exceptionsNow[this.props.exceptionsNow.length - 1].time
) {
lastIndex = index;
}
});
// let lastIndex = -1;
// filtered.forEach((item, index) => {
// if (
// this.props.exceptionsNow.length > 0 &&
// item.time <= this.props.exceptionsNow[this.props.exceptionsNow.length - 1].time
// ) {
// lastIndex = index;
// }
// });
return (
<>
@ -113,6 +113,7 @@ export default class Exceptions extends React.PureComponent {
iconPosition="left"
name="filter"
onChange={this.onFilterChange}
height={28}
/>
<QuestionMarkHint
className={'mx-4'}
@ -133,13 +134,13 @@ export default class Exceptions extends React.PureComponent {
</BottomBlock.Header>
<BottomBlock.Content>
<NoContent size="small" show={filtered.length === 0} title="No recordings found">
<Autoscroll autoScrollTo={Math.max(lastIndex, 0)}>
<Autoscroll>
{filtered.map((e, index) => (
<ErrorItem
onJump={() => jump(e.time)}
error={e}
key={e.key}
selected={lastIndex === index}
// selected={lastIndex === index}
// inactive={index > lastIndex}
onErrorClick={(jsEvent) => {
jsEvent.stopPropagation();

View file

@ -1,7 +1,7 @@
import React from 'react';
import cn from 'classnames';
// import { connectPlayer } from 'Player';
import { QuestionMarkHint, Popup, Tabs, Input, NoContent, Icon, Button } from 'UI';
import { QuestionMarkHint, Popup, Tabs, Input, NoContent, Icon, Toggler, Button } from 'UI';
import { getRE } from 'App/utils';
import { TYPES } from 'Types/session/resource';
import { formatBytes } from 'App/utils';
@ -48,22 +48,17 @@ export function renderType(r) {
export function renderName(r) {
return (
<Popup
style={{ width: '100%' }}
content={<div className={stl.popupNameContent}>{r.url}</div>}
>
<div className={stl.popupNameTrigger}>{r.name}</div>
</Popup>
<Popup style={{ width: '100%' }} content={<div className={stl.popupNameContent}>{r.url}</div>}>
<div className={stl.popupNameTrigger}>{r.name}</div>
</Popup>
);
}
export function renderStart(r) {
return (
<div className="flex justify-between items-center grow-0 w-full">
<span>
{Duration.fromMillis(r.time).toFormat('mm:ss.SSS')}
</span>
<Button
<span>{Duration.fromMillis(r.time).toFormat('mm:ss.SSS')}</span>
{/* <Button
variant="text"
className="right-0 text-xs uppercase p-2 color-gray-500 hover:color-teal"
onClick={(e) => {
@ -72,9 +67,9 @@ export function renderStart(r) {
}}
>
Jump
</Button>
</div>
)
</Button> */}
</div>
);
}
const renderXHRText = () => (
@ -243,39 +238,45 @@ export default class NetworkContent extends React.PureComponent {
iconPosition="left"
name="filter"
onChange={this.onFilterChange}
height={28}
/>
</BottomBlock.Header>
<BottomBlock.Content>
<InfoLine>
<InfoLine.Point label={filtered.length} value=" requests" />
<InfoLine.Point
label={formatBytes(transferredSize)}
value="transferred"
display={transferredSize > 0}
/>
<InfoLine.Point
label={formatBytes(resourcesSize)}
value="resources"
display={resourcesSize > 0}
/>
<InfoLine.Point
label={formatMs(domBuildingTime)}
value="DOM Building Time"
display={domBuildingTime != null}
/>
<InfoLine.Point
label={domContentLoadedTime && formatMs(domContentLoadedTime.value)}
value="DOMContentLoaded"
display={domContentLoadedTime != null}
dotColor={DOM_LOADED_TIME_COLOR}
/>
<InfoLine.Point
label={loadTime && formatMs(loadTime.value)}
value="Load"
display={loadTime != null}
dotColor={LOAD_TIME_COLOR}
/>
</InfoLine>
<div className="flex items-center justify-between px-4">
<div>
<Toggler checked={true} name="test" onChange={() => {}} label="4xx-5xx Only" />
</div>
<InfoLine>
<InfoLine.Point label={filtered.length} value=" requests" />
<InfoLine.Point
label={formatBytes(transferredSize)}
value="transferred"
display={transferredSize > 0}
/>
<InfoLine.Point
label={formatBytes(resourcesSize)}
value="resources"
display={resourcesSize > 0}
/>
<InfoLine.Point
label={formatMs(domBuildingTime)}
value="DOM Building Time"
display={domBuildingTime != null}
/>
<InfoLine.Point
label={domContentLoadedTime && formatMs(domContentLoadedTime.value)}
value="DOMContentLoaded"
display={domContentLoadedTime != null}
dotColor={DOM_LOADED_TIME_COLOR}
/>
<InfoLine.Point
label={loadTime && formatMs(loadTime.value)}
value="Load"
display={loadTime != null}
dotColor={LOAD_TIME_COLOR}
/>
</InfoLine>
</div>
<NoContent
title={
<div className="capitalize flex items-center mt-16">
@ -296,11 +297,11 @@ export default class NetworkContent extends React.PureComponent {
activeIndex={lastIndex}
>
{[
{
label: 'Start',
width: 120,
render: renderStart,
},
// {
// label: 'Start',
// width: 120,
// render: renderStart,
// },
{
label: 'Status',
dataKey: 'status',

View file

@ -2,13 +2,13 @@ import React from 'react';
import { connect } from 'react-redux';
import { Controls as PlayerControls, connectPlayer } from 'Player';
import {
AreaChart,
AreaChart,
Area,
ComposedChart,
Line,
XAxis,
XAxis,
YAxis,
Tooltip,
Tooltip,
ResponsiveContainer,
ReferenceLine,
CartesianGrid,
@ -23,40 +23,35 @@ import stl from './performance.module.css';
import BottomBlock from '../BottomBlock';
import InfoLine from '../BottomBlock/InfoLine';
const CPU_VISUAL_OFFSET = 10;
const FPS_COLOR = '#C5E5E7';
const FPS_STROKE_COLOR = "#92C7CA";
const FPS_LOW_COLOR = "pink";
const FPS_VERY_LOW_COLOR = "red";
const CPU_COLOR = "#A8D1DE";
const CPU_STROKE_COLOR = "#69A5B8";
const FPS_STROKE_COLOR = '#92C7CA';
const FPS_LOW_COLOR = 'pink';
const FPS_VERY_LOW_COLOR = 'red';
const CPU_COLOR = '#A8D1DE';
const CPU_STROKE_COLOR = '#69A5B8';
const USED_HEAP_COLOR = '#A9ABDC';
const USED_HEAP_STROKE_COLOR = "#8588CF";
const USED_HEAP_STROKE_COLOR = '#8588CF';
const TOTAL_HEAP_STROKE_COLOR = '#4A4EB7';
const NODES_COUNT_COLOR = "#C6A9DC";
const NODES_COUNT_STROKE_COLOR = "#7360AC";
const HIDDEN_SCREEN_COLOR = "#CCC";
const NODES_COUNT_COLOR = '#C6A9DC';
const NODES_COUNT_STROKE_COLOR = '#7360AC';
const HIDDEN_SCREEN_COLOR = '#CCC';
const CURSOR_COLOR = "#394EFF";
const CURSOR_COLOR = '#394EFF';
const Gradient = ({ color, id }) => (
<linearGradient id={ id } x1="-1" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={ color } stopOpacity={ 0.7 } />
<stop offset="95%" stopColor={ color } stopOpacity={ 0.2 } />
<linearGradient id={id} x1="-1" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={color} stopOpacity={0.7} />
<stop offset="95%" stopColor={color} stopOpacity={0.2} />
</linearGradient>
);
const TOTAL_HEAP = "Allocated Heap";
const USED_HEAP = "JS Heap";
const FPS = "Framerate";
const CPU = "CPU Load";
const NODES_COUNT = "Nodes Сount";
const TOTAL_HEAP = 'Allocated Heap';
const USED_HEAP = 'JS Heap';
const FPS = 'Framerate';
const CPU = 'CPU Load';
const NODES_COUNT = 'Nodes Сount';
const FPSTooltip = ({ active, payload }) => {
if (!active || !payload || payload.length < 3) {
@ -64,8 +59,8 @@ const FPSTooltip = ({ active, payload }) => {
}
if (payload[0].value === null) {
return (
<div className={ stl.tooltipWrapper } style={{ color: HIDDEN_SCREEN_COLOR }}>
{"Page is not active. User switched the tab or hid the window."}
<div className={stl.tooltipWrapper} style={{ color: HIDDEN_SCREEN_COLOR }}>
{'Page is not active. User switched the tab or hid the window.'}
</div>
);
}
@ -79,9 +74,9 @@ const FPSTooltip = ({ active, payload }) => {
}
return (
<div className={ stl.tooltipWrapper } style={ style }>
<span className="font-medium">{`${ FPS }: `}</span>
{ Math.trunc(payload[0].value) }
<div className={stl.tooltipWrapper} style={style}>
<span className="font-medium">{`${FPS}: `}</span>
{Math.trunc(payload[0].value)}
</div>
);
};
@ -91,47 +86,47 @@ const CPUTooltip = ({ active, payload }) => {
return null;
}
return (
<div className={ stl.tooltipWrapper } >
<span className="font-medium">{`${ CPU }: `}</span>
{ payload[0].value - CPU_VISUAL_OFFSET }
{"%"}
<div className={stl.tooltipWrapper}>
<span className="font-medium">{`${CPU}: `}</span>
{payload[0].value - CPU_VISUAL_OFFSET}
{'%'}
</div>
);
};
const HeapTooltip = ({ active, payload}) => {
const HeapTooltip = ({ active, payload }) => {
if (!active || payload.length < 2) return null;
return (
<div className={ stl.tooltipWrapper } >
<div className={stl.tooltipWrapper}>
<p>
<span className="font-medium">{`${ TOTAL_HEAP }: `}</span>
{ formatBytes(payload[0].value)}
<span className="font-medium">{`${TOTAL_HEAP}: `}</span>
{formatBytes(payload[0].value)}
</p>
<p>
<span className="font-medium">{`${ USED_HEAP }: `}</span>
{ formatBytes(payload[1].value)}
<span className="font-medium">{`${USED_HEAP}: `}</span>
{formatBytes(payload[1].value)}
</p>
</div>
);
}
};
const NodesCountTooltip = ({ active, payload} ) => {
const NodesCountTooltip = ({ active, payload }) => {
if (!active || !payload || payload.length === 0) return null;
return (
<div className={ stl.tooltipWrapper } >
<div className={stl.tooltipWrapper}>
<p>
<span className="font-medium">{`${ NODES_COUNT }: `}</span>
{ payload[0].value }
<span className="font-medium">{`${NODES_COUNT}: `}</span>
{payload[0].value}
</p>
</div>
);
}
};
const TICKS_COUNT = 10;
function generateTicks(data: Array<Timed>): Array<number> {
if (data.length === 0) return [];
const minTime = data[0].time;
const maxTime = data[data.length-1].time;
const maxTime = data[data.length - 1].time;
const ticks = [];
const tickGap = (maxTime - minTime) / (TICKS_COUNT + 1);
@ -159,8 +154,9 @@ function addFpsMetadata(data) {
} else if (point.fps < LOW_FPS) {
fpsLowMarker = LOW_FPS_MARKER_VALUE;
}
}
if (point.fps == null ||
}
if (
point.fps == null ||
(i > 0 && data[i - 1].fps == null) //||
//(i < data.length-1 && data[i + 1].fps == null)
) {
@ -174,17 +170,17 @@ function addFpsMetadata(data) {
fpsLowMarker,
fpsVeryLowMarker,
hiddenScreenMarker,
}
};
});
}
@connect(state => ({
userDeviceHeapSize: state.getIn([ "sessions", "current", "userDeviceHeapSize" ]),
userDeviceMemorySize: state.getIn([ "sessions", "current", "userDeviceMemorySize" ]),
@connect((state) => ({
userDeviceHeapSize: state.getIn(['sessions', 'current', 'userDeviceHeapSize']),
userDeviceMemorySize: state.getIn(['sessions', 'current', 'userDeviceMemorySize']),
}))
export default class Performance extends React.PureComponent {
_timeTicks = generateTicks(this.props.performanceChartData)
_data = addFpsMetadata(this.props.performanceChartData)
_timeTicks = generateTicks(this.props.performanceChartData);
_data = addFpsMetadata(this.props.performanceChartData);
// state = {
// totalHeap: false,
// usedHeap: true,
@ -197,7 +193,7 @@ export default class Performance extends React.PureComponent {
if (!!point) {
PlayerControls.jump(point.time);
}
}
};
onChartClick = (e) => {
if (e === null) return;
@ -206,10 +202,10 @@ export default class Performance extends React.PureComponent {
if (!!point) {
PlayerControls.jump(point.time);
}
}
};
render() {
const {
const {
userDeviceHeapSize,
userDeviceMemorySize,
connType,
@ -218,19 +214,19 @@ export default class Performance extends React.PureComponent {
avaliability = {},
} = this.props;
const { fps, cpu, heap, nodes } = avaliability;
const avaliableCount = [ fps, cpu, heap, nodes ].reduce((c, av) => av ? c + 1 : c, 0);
const height = avaliableCount === 0 ? "0" : `${100 / avaliableCount}%`;
const avaliableCount = [fps, cpu, heap, nodes].reduce((c, av) => (av ? c + 1 : c), 0);
const height = avaliableCount === 0 ? '0' : `${100 / avaliableCount}%`;
return (
<BottomBlock>
<BottomBlock.Header>
<div className="flex items-center">
<span className="font-semibold color-gray-medium mr-4">Performance</span>
<div className="flex items-center w-full">
<div className="font-semibold color-gray-medium mr-auto">Performance</div>
<InfoLine>
<InfoLine.Point
label="Device Heap Size"
value={ formatBytes(userDeviceHeapSize) }
display={ userDeviceHeapSize != null }
value={formatBytes(userDeviceHeapSize)}
display={true}
/>
{/* <InfoLine.Point */}
{/* label="Device Memory Size" */}
@ -238,55 +234,52 @@ export default class Performance extends React.PureComponent {
{/* /> */}
<InfoLine.Point
label="Connection Type"
value={ connType }
display={ connType != null && connType !== "unknown" }
value={connType}
display={connType != null && connType !== 'unknown'}
/>
<InfoLine.Point
label="Connection Speed"
value={ connBandwidth >= 1000
? `${ connBandwidth / 1000 } Mbps`
: `${ connBandwidth } Kbps`
value={
connBandwidth >= 1000 ? `${connBandwidth / 1000} Mbps` : `${connBandwidth} Kbps`
}
display={ connBandwidth != null }
display={connBandwidth != null}
/>
</InfoLine>
</div>
</BottomBlock.Header>
<BottomBlock.Content>
{ fps &&
<ResponsiveContainer height={ height }>
{fps && (
<ResponsiveContainer height={height}>
<AreaChart
onClick={ this.onChartClick }
onClick={this.onChartClick}
data={this._data}
syncId="s"
margin={{
top: 0, right: 0, left: 0, bottom: 0,
top: 0,
right: 0,
left: 0,
bottom: 0,
}}
>
<defs>
<Gradient id="fpsGradient" color={ FPS_COLOR } />
<Gradient id="fpsGradient" color={FPS_COLOR} />
</defs>
{/* <CartesianGrid strokeDasharray="1 1" stroke="#ddd" horizontal={ false } /> */}
{/* <CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" /> */}
<XAxis
<XAxis
dataKey="time"
type="number"
mirror
orientation="top"
tickLine={ false }
tickFormatter={ durationFromMsFormatted }
tick={{ fontSize: "12px" }}
tickLine={false}
tickFormatter={durationFromMsFormatted}
tick={{ fontSize: '12px', fill: '#333' }}
domain={[0, 'dataMax']}
ticks={this._timeTicks}
>
<Label value="FPS" position="insideTopRight" className="fill-gray-medium" />
<Label value="FPS" position="insideTopRight" className="fill-gray-darkest" />
</XAxis>
<YAxis
axisLine={ false }
tick={ false }
mirror
domain={[0, 85]}
/>
<YAxis axisLine={false} tick={false} mirror domain={[0, 85]} />
{/* <YAxis */}
{/* yAxisId="r" */}
{/* axisLine={ false } */}
@ -295,41 +288,41 @@ export default class Performance extends React.PureComponent {
{/* domain={[0, 120]} */}
{/* orientation="right" */}
{/* /> */}
<Area
dataKey="fps"
type="stepBefore"
<Area
dataKey="fps"
type="stepBefore"
stroke={FPS_STROKE_COLOR}
fill="url(#fpsGradient)"
dot={false}
activeDot={{
activeDot={{
onClick: this.onDotClick,
style: { cursor: "pointer" },
}}
isAnimationActive={ false }
/>
style: { cursor: 'pointer' },
}}
isAnimationActive={false}
/>
<Area
dataKey="fpsLowMarker"
dataKey="fpsLowMarker"
type="stepBefore"
stroke="none"
fill={ FPS_LOW_COLOR }
fill={FPS_LOW_COLOR}
activeDot={false}
isAnimationActive={ false }
isAnimationActive={false}
/>
<Area
<Area
dataKey="fpsVeryLowMarker"
type="stepBefore"
stroke="none"
fill={ FPS_VERY_LOW_COLOR }
fill={FPS_VERY_LOW_COLOR}
activeDot={false}
isAnimationActive={ false }
isAnimationActive={false}
/>
<Area
dataKey="hiddenScreenMarker"
type="stepBefore"
stroke="none"
fill={ HIDDEN_SCREEN_COLOR }
fill={HIDDEN_SCREEN_COLOR}
activeDot={false}
isAnimationActive={ false }
isAnimationActive={false}
/>
{/* <Area */}
{/* yAxisId="r" */}
@ -346,101 +339,95 @@ export default class Performance extends React.PureComponent {
{/* isAnimationActive={ false } */}
{/* /> */}
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
<Tooltip
content={FPSTooltip}
filterNull={ false }
/>
<Tooltip content={FPSTooltip} filterNull={false} />
</AreaChart>
</ResponsiveContainer>
}
{ cpu &&
<ResponsiveContainer height={ height }>
)}
{cpu && (
<ResponsiveContainer height={height}>
<AreaChart
onClick={ this.onChartClick }
onClick={this.onChartClick}
data={this._data}
syncId="s"
margin={{
top: 0, right: 0, left: 0, bottom: 0,
top: 0,
right: 0,
left: 0,
bottom: 0,
}}
>
<defs>
<Gradient id="cpuGradient" color={ CPU_COLOR } />
<Gradient id="cpuGradient" color={CPU_COLOR} />
</defs>
{/* <CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" /> */}
<XAxis
dataKey="time"
type="number"
mirror
orientation="top"
tickLine={false}
tickFormatter={()=> ""}
domain={[0, 'dataMax']}
ticks={this._timeTicks}
>
<Label value="CPU" position="insideTopRight" className="fill-gray-medium" />
</XAxis>
<YAxis
axisLine={ false }
tick={ false }
mirror
domain={[ 0, 120]}
orientation="right"
/>
<Area
dataKey="cpu"
type="monotone"
stroke={CPU_STROKE_COLOR}
// fill="none"
fill="url(#cpuGradient)"
dot={false}
activeDot={{
onClick: this.onDotClick,
style: { cursor: "pointer" },
}}
isAnimationActive={ false }
/>
<Area
dataKey="hiddenScreenMarker"
type="stepBefore"
stroke="none"
fill={ HIDDEN_SCREEN_COLOR }
activeDot={false}
isAnimationActive={ false }
/>
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
<Tooltip
content={CPUTooltip}
filterNull={ false }
/>
</AreaChart>
</ResponsiveContainer>
}
{ heap &&
<ResponsiveContainer height={ height }>
<ComposedChart
onClick={ this.onChartClick }
data={this._data}
margin={{
top: 0, right: 0, left: 0, bottom: 0,
}}
syncId="s"
>
<defs>
<Gradient id="usedHeapGradient" color={ USED_HEAP_COLOR } />
</defs>
{/* <CartesianGrid strokeDasharray="1 1" stroke="#ddd" horizontal={ false } /> */}
<XAxis
<XAxis
dataKey="time"
type="number"
mirror
orientation="top"
tickLine={false}
tickFormatter={()=> ""} // tick={false} + this._timeTicks to cartesian array
tickFormatter={() => ''}
domain={[0, 'dataMax']}
ticks={this._timeTicks}
>
<Label value="HEAP" position="insideTopRight" className="fill-gray-medium" />
<Label value="CPU" position="insideTopRight" className="fill-gray-darkest" />
</XAxis>
<YAxis axisLine={false} tick={false} mirror domain={[0, 120]} orientation="right" />
<Area
dataKey="cpu"
type="monotone"
stroke={CPU_STROKE_COLOR}
// fill="none"
fill="url(#cpuGradient)"
dot={false}
activeDot={{
onClick: this.onDotClick,
style: { cursor: 'pointer' },
}}
isAnimationActive={false}
/>
<Area
dataKey="hiddenScreenMarker"
type="stepBefore"
stroke="none"
fill={HIDDEN_SCREEN_COLOR}
activeDot={false}
isAnimationActive={false}
/>
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
<Tooltip content={CPUTooltip} filterNull={false} />
</AreaChart>
</ResponsiveContainer>
)}
{heap && (
<ResponsiveContainer height={height}>
<ComposedChart
onClick={this.onChartClick}
data={this._data}
margin={{
top: 0,
right: 0,
left: 0,
bottom: 0,
}}
syncId="s"
>
<defs>
<Gradient id="usedHeapGradient" color={USED_HEAP_COLOR} />
</defs>
{/* <CartesianGrid strokeDasharray="1 1" stroke="#ddd" horizontal={ false } /> */}
<XAxis
dataKey="time"
type="number"
mirror
orientation="top"
tickLine={false}
tickFormatter={() => ''} // tick={false} + this._timeTicks to cartesian array
domain={[0, 'dataMax']}
ticks={this._timeTicks}
>
<Label value="HEAP" position="insideTopRight" className="fill-gray-darkest" />
</XAxis>
<YAxis
axisLine={false}
@ -448,19 +435,19 @@ export default class Performance extends React.PureComponent {
mirror
// Hack to keep only end tick
minTickGap={Number.MAX_SAFE_INTEGER}
domain={[0, max => max*1.2]}
domain={[0, (max) => max * 1.2]}
/>
<Line
<Line
type="monotone"
dataKey="totalHeap"
// fill="url(#totalHeapGradient)"
stroke={TOTAL_HEAP_STROKE_COLOR}
dot={false}
activeDot={{
activeDot={{
onClick: this.onDotClick,
style: { cursor: "pointer" },
style: { cursor: 'pointer' },
}}
isAnimationActive={ false }
isAnimationActive={false}
/>
<Area
dataKey="usedHeap"
@ -468,81 +455,78 @@ export default class Performance extends React.PureComponent {
fill="url(#usedHeapGradient)"
stroke={USED_HEAP_STROKE_COLOR}
dot={false}
activeDot={{
activeDot={{
onClick: this.onDotClick,
style: { cursor: "pointer" },
}}
isAnimationActive={ false }
style: { cursor: 'pointer' },
}}
isAnimationActive={false}
/>
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
<Tooltip
content={HeapTooltip}
filterNull={ false }
/>
<Tooltip content={HeapTooltip} filterNull={false} />
</ComposedChart>
</ResponsiveContainer>
}
{ nodes &&
<ResponsiveContainer height={ height }>
)}
{nodes && (
<ResponsiveContainer height={height}>
<AreaChart
onClick={ this.onChartClick }
onClick={this.onChartClick}
data={this._data}
syncId="s"
margin={{
top: 0, right: 0, left: 0, bottom: 0,
top: 0,
right: 0,
left: 0,
bottom: 0,
}}
>
<defs>
<Gradient id="nodesGradient" color={ NODES_COUNT_COLOR } />
<Gradient id="nodesGradient" color={NODES_COUNT_COLOR} />
</defs>
{/* <CartesianGrid strokeDasharray="1 1" stroke="#ddd" horizontal={ false } /> */}
<XAxis
dataKey="time"
type="number"
mirror
orientation="top"
tickLine={false}
tickFormatter={()=> ""}
domain={[0, 'dataMax']}
ticks={this._timeTicks}
>
<Label value="NODES" position="insideTopRight" className="fill-gray-medium" />
<XAxis
dataKey="time"
type="number"
mirror
orientation="top"
tickLine={false}
tickFormatter={() => ''}
domain={[0, 'dataMax']}
ticks={this._timeTicks}
>
<Label value="NODES" position="insideTopRight" className="fill-gray-darkest" />
</XAxis>
<YAxis
axisLine={ false }
tick={ false }
axisLine={false}
tick={false}
mirror
orientation="right"
domain={[0, max => max*1.2]}
domain={[0, (max) => max * 1.2]}
/>
<Area
dataKey="nodesCount"
type="monotone"
<Area
dataKey="nodesCount"
type="monotone"
stroke={NODES_COUNT_STROKE_COLOR}
// fill="none"
fill="url(#nodesGradient)"
dot={false}
activeDot={{
activeDot={{
onClick: this.onDotClick,
style: { cursor: "pointer" },
}}
isAnimationActive={ false }
/>
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
<Tooltip
content={NodesCountTooltip}
filterNull={ false }
style: { cursor: 'pointer' },
}}
isAnimationActive={false}
/>
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
<Tooltip content={NodesCountTooltip} filterNull={false} />
</AreaChart>
</ResponsiveContainer>
}
)}
</BottomBlock.Content>
</BottomBlock>
);
}
}
export const ConnectedPerformance = connectPlayer(state => ({
export const ConnectedPerformance = connectPlayer((state) => ({
performanceChartTime: state.performanceChartTime,
performanceChartData: state.performanceChartData,
connType: state.connType,

View file

@ -3,4 +3,5 @@
padding: 2px 5px;
border-radius: 3px;
border: 1px solid #ccc;
color: $gray-dark !important;
}

View file

@ -8,7 +8,7 @@ const ControlButton = ({
icon = '',
disabled = false,
onClick,
count = 0,
// count = 0,
hasErrors = false,
active = false,
size = 20,
@ -31,7 +31,7 @@ const ControlButton = ({
>
<div className={stl.labels}>
{hasErrors && <div className={stl.errorSymbol} />}
{count > 0 && <div className={stl.countLabel}>{count}</div>}
{/* {count > 0 && <div className={stl.countLabel}>{count}</div>} */}
</div>
{!noIcon && <Icon name={icon} size={size} color="gray-dark" />}
{!noLabel && (

View file

@ -8,7 +8,7 @@ import {
selectStorageListNow,
} from 'Player/store';
import LiveTag from 'Shared/LiveTag';
import { toggleTimetravel, jumpToLive } from 'Player';
import { jumpToLive } from 'Player';
import { Icon } from 'UI';
import { toggleInspectorMode } from 'Player';
@ -25,8 +25,6 @@ import {
PROFILER,
PERFORMANCE,
GRAPHQL,
FETCH,
EXCEPTIONS,
INSPECTOR,
} from 'Duck/components/player';
import { AssistDuration } from './Time';
@ -38,23 +36,6 @@ import styles from './controls.module.css';
import { Tooltip } from 'react-tippy';
import XRayButton from 'Shared/XRayButton';
function getStorageIconName(type) {
switch (type) {
case STORAGE_TYPES.REDUX:
return 'vendors/redux';
case STORAGE_TYPES.MOBX:
return 'vendors/mobx';
case STORAGE_TYPES.VUEX:
return 'vendors/vuex';
case STORAGE_TYPES.NGRX:
return 'vendors/ngrx';
case STORAGE_TYPES.ZUSTAND:
return 'vendors/zustand';
case STORAGE_TYPES.NONE:
return 'store';
}
}
const SKIP_INTERVALS = {
2: 2e3,
5: 5e3,
@ -95,24 +76,23 @@ function getStorageName(type) {
disabled: state.cssLoading || state.messagesLoading || state.inspectorMode || state.markedTargets,
inspectorMode: state.inspectorMode,
fullscreenDisabled: state.messagesLoading,
logCount: state.logListNow.length,
logRedCount: state.logRedCountNow,
resourceRedCount: state.resourceRedCountNow,
fetchRedCount: state.fetchRedCountNow,
// logCount: state.logList.length,
logRedCount: state.logRedCount,
resourceRedCount: state.resourceRedCount,
fetchRedCount: state.fetchRedCount,
showStack: state.stackList.length > 0,
stackCount: state.stackListNow.length,
stackRedCount: state.stackRedCountNow,
profilesCount: state.profilesListNow.length,
stackCount: state.stackList.length,
stackRedCount: state.stackRedCount,
profilesCount: state.profilesList.length,
storageCount: selectStorageListNow(state).length,
storageType: selectStorageType(state),
showStorage: selectStorageType(state) !== STORAGE_TYPES.NONE,
showProfiler: state.profilesList.length > 0,
showGraphql: state.graphqlList.length > 0,
showFetch: state.fetchCount > 0,
fetchCount: state.fetchCountNow,
graphqlCount: state.graphqlListNow.length,
exceptionsCount: state.exceptionsListNow.length,
showExceptions: state.exceptionsList.length > 0,
fetchCount: state.fetchCount,
graphqlCount: state.graphqlList.length,
showLongtasks: state.longtasksList.length > 0,
liveTimeTravel: state.liveTimeTravel,
}))
@connect(
@ -162,7 +142,7 @@ export default class Controls extends React.Component {
nextProps.disabled !== this.props.disabled ||
nextProps.fullscreenDisabled !== this.props.fullscreenDisabled ||
// nextProps.inspectorMode !== this.props.inspectorMode ||
nextProps.logCount !== this.props.logCount ||
// nextProps.logCount !== this.props.logCount ||
nextProps.logRedCount !== this.props.logRedCount ||
nextProps.resourceRedCount !== this.props.resourceRedCount ||
nextProps.fetchRedCount !== this.props.fetchRedCount ||
@ -178,8 +158,7 @@ export default class Controls extends React.Component {
nextProps.showFetch !== this.props.showFetch ||
nextProps.fetchCount !== this.props.fetchCount ||
nextProps.graphqlCount !== this.props.graphqlCount ||
nextProps.showExceptions !== this.props.showExceptions ||
nextProps.exceptionsCount !== this.props.exceptionsCount ||
nextProps.showLongtasks !== this.props.showLongtasks ||
nextProps.liveTimeTravel !== this.props.liveTimeTravel ||
nextProps.skipInterval !== this.props.skipInterval
)
@ -284,24 +263,14 @@ export default class Controls extends React.Component {
skip,
speed,
disabled,
logCount,
logRedCount,
resourceRedCount,
fetchRedCount,
showStack,
stackCount,
stackRedCount,
profilesCount,
storageCount,
showStorage,
storageType,
showProfiler,
showGraphql,
showFetch,
fetchCount,
graphqlCount,
exceptionsCount,
showExceptions,
fullscreen,
inspectorMode,
closedLive,
@ -378,7 +347,6 @@ export default class Controls extends React.Component {
label="CONSOLE"
noIcon
labelClassName="!text-base font-semibold"
count={logCount}
hasErrors={logRedCount > 0}
containerClassName="mx-2"
/>
@ -405,25 +373,11 @@ export default class Controls extends React.Component {
containerClassName="mx-2"
/>
)}
{showFetch && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(FETCH)}
active={bottomBlock === FETCH && !inspectorMode}
hasErrors={fetchRedCount > 0}
count={fetchCount}
label="FETCH"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && showGraphql && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(GRAPHQL)}
active={bottomBlock === GRAPHQL && !inspectorMode}
count={graphqlCount}
label="GRAPHQL"
noIcon
labelClassName="!text-base font-semibold"
@ -435,26 +389,12 @@ export default class Controls extends React.Component {
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(STORAGE)}
active={bottomBlock === STORAGE && !inspectorMode}
count={storageCount}
label={getStorageName(storageType)}
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{showExceptions && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(EXCEPTIONS)}
active={bottomBlock === EXCEPTIONS && !inspectorMode}
label="EXCEPTIONS"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
count={exceptionsCount}
hasErrors={exceptionsCount > 0}
/>
)}
{!live && showStack && (
<ControlButton
disabled={disabled && !inspectorMode}
@ -464,7 +404,6 @@ export default class Controls extends React.Component {
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
count={stackCount}
hasErrors={stackRedCount > 0}
/>
)}
@ -473,7 +412,6 @@ export default class Controls extends React.Component {
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(PROFILER)}
active={bottomBlock === PROFILER && !inspectorMode}
count={profilesCount}
label="PROFILER"
noIcon
labelClassName="!text-base font-semibold"

View file

@ -14,20 +14,18 @@ import {
PROFILER,
PERFORMANCE,
GRAPHQL,
FETCH,
EXCEPTIONS,
LONGTASKS,
INSPECTOR,
OVERVIEW,
} from 'Duck/components/player';
import Network from '../Network';
import NetworkPanel from 'Shared/DevTools/NetworkPanel';
import Console from '../Console/Console';
import StackEvents from '../StackEvents/StackEvents';
import Storage from '../Storage';
import Profiler from '../Profiler';
import { ConnectedPerformance } from '../Performance';
import GraphQL from '../GraphQL';
import Fetch from '../Fetch';
import Exceptions from '../Exceptions/Exceptions';
import LongTasks from '../LongTasks';
import Inspector from '../Inspector';
@ -42,29 +40,38 @@ import Overlay from './Overlay';
import stl from './player.module.css';
import { updateLastPlayedSession } from 'Duck/sessions';
import OverviewPanel from '../OverviewPanel';
import ConsolePanel from 'Shared/DevTools/ConsolePanel';
import ProfilerPanel from 'Shared/DevTools/ProfilerPanel';
@connectPlayer(state => ({
@connectPlayer((state) => ({
live: state.live,
}))
@connect(state => {
const isAssist = window.location.pathname.includes('/assist/');
return {
fullscreen: state.getIn([ 'components', 'player', 'fullscreen' ]),
nextId: state.getIn([ 'sessions', 'nextId' ]),
sessionId: state.getIn([ 'sessions', 'current', 'sessionId' ]),
closedLive: !!state.getIn([ 'sessions', 'errors' ]) || (isAssist && !state.getIn([ 'sessions', 'current', 'live' ])),
@connect(
(state) => {
const isAssist = window.location.pathname.includes('/assist/');
return {
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
nextId: state.getIn(['sessions', 'nextId']),
sessionId: state.getIn(['sessions', 'current', 'sessionId']),
closedLive:
!!state.getIn(['sessions', 'errors']) ||
(isAssist && !state.getIn(['sessions', 'current', 'live'])),
};
},
{
hideTargetDefiner,
fullscreenOff,
updateLastPlayedSession,
}
}, {
hideTargetDefiner,
fullscreenOff,
updateLastPlayedSession,
})
)
export default class Player extends React.PureComponent {
screenWrapper = React.createRef();
componentDidUpdate(prevProps) {
if ([ prevProps.bottomBlock, this.props.bottomBlock ].includes(NONE) ||
prevProps.fullscreen !== this.props.fullscreen) {
if (
[prevProps.bottomBlock, this.props.bottomBlock].includes(NONE) ||
prevProps.fullscreen !== this.props.fullscreen
) {
scalePlayerScreen();
}
}
@ -73,7 +80,7 @@ export default class Player extends React.PureComponent {
this.props.updateLastPlayedSession(this.props.sessionId);
if (this.props.closedLive) return;
const parentElement = findDOMNode(this.screenWrapper.current); //TODO: good architecture
const parentElement = findDOMNode(this.screenWrapper.current); //TODO: good architecture
attachPlayer(parentElement);
}
@ -86,66 +93,39 @@ export default class Player extends React.PureComponent {
nextId,
closedLive,
bottomBlock,
activeTab
activeTab,
} = this.props;
const maxWidth = activeTab ? 'calc(100vw - 270px)' : '100vw'
const maxWidth = activeTab ? 'calc(100vw - 270px)' : '100vw';
return (
<div
className={ cn(className, stl.playerBody, "flex flex-col relative", fullscreen && 'pb-2') }
data-bottom-block={ bottomBlockIsActive }
className={cn(className, stl.playerBody, 'flex flex-col relative', fullscreen && 'pb-2')}
data-bottom-block={bottomBlockIsActive}
>
{fullscreen && <EscapeButton onClose={ fullscreenOff } />}
{fullscreen && <EscapeButton onClose={fullscreenOff} />}
<div className="relative flex-1 overflow-hidden">
<Overlay nextId={nextId} togglePlay={PlayerControls.togglePlay} closedLive={closedLive} />
<div
className={ stl.screenWrapper }
ref={ this.screenWrapper }
/>
<div className={stl.screenWrapper} ref={this.screenWrapper} />
</div>
{ !fullscreen && !!bottomBlock &&
{!fullscreen && !!bottomBlock && (
<div style={{ maxWidth, width: '100%' }}>
{ bottomBlock === OVERVIEW &&
<OverviewPanel />
}
{ bottomBlock === CONSOLE &&
<Console />
}
{ bottomBlock === NETWORK &&
<Network />
}
{ bottomBlock === STACKEVENTS &&
<StackEvents />
}
{ bottomBlock === STORAGE &&
<Storage />
}
{ bottomBlock === PROFILER &&
<Profiler />
}
{ bottomBlock === PERFORMANCE &&
<ConnectedPerformance />
}
{ bottomBlock === GRAPHQL &&
<GraphQL />
}
{ bottomBlock === FETCH &&
<Fetch />
}
{ bottomBlock === EXCEPTIONS &&
<Exceptions />
}
{ bottomBlock === LONGTASKS &&
<LongTasks />
}
{ bottomBlock === INSPECTOR &&
<Inspector />
}
{bottomBlock === OVERVIEW && <OverviewPanel />}
{bottomBlock === CONSOLE && <ConsolePanel />}
{bottomBlock === NETWORK && (
// <Network />
<NetworkPanel />
)}
{bottomBlock === STACKEVENTS && <StackEvents />}
{bottomBlock === STORAGE && <Storage />}
{bottomBlock === PROFILER && <ProfilerPanel />}
{bottomBlock === PERFORMANCE && <ConnectedPerformance />}
{bottomBlock === GRAPHQL && <GraphQL />}
{bottomBlock === EXCEPTIONS && <Exceptions />}
{bottomBlock === LONGTASKS && <LongTasks />}
{bottomBlock === INSPECTOR && <Inspector />}
</div>
}
<Controls
{ ...PlayerControls }
/>
)}
<Controls {...PlayerControls} />
</div>
);
}

View file

@ -51,6 +51,7 @@ export default class Profiler extends React.PureComponent {
icon="search"
name="filter"
onChange={ this.onFilterChange }
height={28}
/>
</BottomBlock.Header>
<BottomBlock.Content>

View file

@ -6,6 +6,7 @@ import withEnumToggle from 'HOCs/withEnumToggle';
import { connectPlayer, jump } from 'Player';
import React from 'react';
import { connect } from 'react-redux';
import StackEventRow from 'Shared/DevTools/StackEventRow';
import { DATADOG, SENTRY, STACKDRIVER, typeList } from 'Types/session/stackEvent';
import { NoContent, SlideModal, Tabs, Link } from 'UI';
import Autoscroll from '../Autoscroll';
@ -19,7 +20,7 @@ const TABS = [ALL, ...typeList].map((tab) => ({ text: tab, key: tab }));
@withEnumToggle('activeTab', 'setActiveTab', ALL)
@connectPlayer((state) => ({
stackEvents: state.stackList,
stackEventsNow: state.stackListNow,
// stackEventsNow: state.stackListNow,
}))
@connect(
(state) => ({
@ -69,20 +70,20 @@ export default class StackEvents extends React.PureComponent {
({ key }) => key === ALL || stackEvents.some(({ source }) => key === source)
);
const filteredStackEvents = stackEvents
// .filter(({ data }) => data.includes(filter))
.filter(({ source }) => activeTab === ALL || activeTab === source);
const filteredStackEvents = stackEvents.filter(
({ source }) => activeTab === ALL || activeTab === source
);
let lastIndex = -1;
// TODO: Need to do filtering in store, or preferably in a selector
filteredStackEvents.forEach((item, index) => {
if (
this.props.stackEventsNow.length > 0 &&
item.time <= this.props.stackEventsNow[this.props.stackEventsNow.length - 1].time
) {
lastIndex = index;
}
});
// let lastIndex = -1;
// // TODO: Need to do filtering in store, or preferably in a selector
// filteredStackEvents.forEach((item, index) => {
// if (
// this.props.stackEventsNow.length > 0 &&
// item.time <= this.props.stackEventsNow[this.props.stackEventsNow.length - 1].time
// ) {
// lastIndex = index;
// }
// });
return (
<>
@ -154,16 +155,21 @@ export default class StackEvents extends React.PureComponent {
size="small"
show={filteredStackEvents.length === 0}
>
<Autoscroll autoScrollTo={Math.max(lastIndex, 0)}>
<Autoscroll>
{filteredStackEvents.map((userEvent, index) => (
<UserEvent
<StackEventRow
key={userEvent.key}
onDetailsClick={this.onDetailsClick.bind(this)}
inactive={index > lastIndex}
selected={lastIndex === index}
userEvent={userEvent}
event={userEvent}
onJump={() => jump(userEvent.time)}
/>
// <UserEvent
// key={userEvent.key}
// onDetailsClick={this.onDetailsClick.bind(this)}
// // inactive={index > lastIndex}
// // selected={lastIndex === index}
// userEvent={userEvent}
// onJump={() => jump(userEvent.time)}
// />
))}
</Autoscroll>
</NoContent>

View file

@ -18,7 +18,9 @@ export default class JsonViewer extends React.PureComponent {
{isObjectData && <JSONTree src={data} collapsed={false} />}
{!isObjectData && Array.isArray(data) && (
<div>
<div className="text-lg">{data[0]}</div>
<div className="code-font mb-2">
{typeof data[0] === 'string' ? data[0] : JSON.stringify(data[0])}
</div>
<JSONTree src={data[1]} collapsed={false} />
</div>
)}

View file

@ -1,12 +1,13 @@
import React from 'react';
import cn from 'classnames';
import { OPENREPLAY, SENTRY, DATADOG, STACKDRIVER } from 'Types/session/stackEvent';
import { Icon, IconButton } from 'UI';
import { Icon } from 'UI';
import withToggle from 'HOCs/withToggle';
import Sentry from './Sentry';
import JsonViewer from './JsonViewer';
import stl from './userEvent.module.css';
import { Duration } from 'luxon';
import JumpButton from 'Shared/DevTools/JumpButton';
// const modalSources = [ SENTRY, DATADOG ];
@ -34,34 +35,33 @@ export default class UserEvent extends React.PureComponent {
render() {
const { userEvent, inactive, selected } = this.props;
//const message = this.getEventMessage();
let message = userEvent.payload[0] || '';
message = typeof message === 'string' ? message : JSON.stringify(message);
return (
<div
data-scroll-item={userEvent.isRed()}
// onClick={ this.props.switchOpen } //
onClick={this.props.onJump} //
className={cn('group flex py-2 px-4 ', stl.userEvent, this.getLevelClassname(), {
// [stl.inactive]: inactive,
[stl.selected]: selected,
})}
onClick={this.onClickDetails}
className={cn(
'group flex items-center py-2 px-4 border-b cursor-pointer relative',
// stl.userEvent,
// this.getLevelClassname(),
// {
// [stl.selected]: selected,
// },
'hover:bg-active-blue'
)}
>
<div className={'self-start pr-4'}>
{/* <div className={'self-start pr-4'}>
{Duration.fromMillis(userEvent.time).toFormat('mm:ss.SSS')}
</div>
<div className={cn('mr-auto', stl.infoWrapper)}>
<div className={stl.title}>
<Icon {...this.getIconProps()} />
{userEvent.name}
</div> */}
<div className={cn('mr-auto flex items-start')}>
<Icon {...this.getIconProps()} />
<div>
<div className="capitalize font-medium mb-1">{userEvent.name}</div>
<div className="code-font text-xs">{message}</div>
</div>
</div>
<div className="self-center">
<IconButton
outline={!userEvent.isRed()}
red={userEvent.isRed()}
onClick={this.onClickDetails}
label="DETAILS"
/>
</div>
<JumpButton onClick={this.props.onJump} />
</div>
);
}

View file

@ -9,7 +9,7 @@ $offset: 10px;
.headers {
box-shadow: 0 1px 2px 0 $gray-light;
background-color: $gray-lightest;
color: $gray-medium;
color: $gray-darkest;
font-size: 12px;
overflow-x: hidden;
white-space: nowrap;
@ -47,6 +47,10 @@ $offset: 10px;
.row {
display: flex;
padding: 0 $offset;
&:hover {
background-color: $active-blue;
}
/*align-items: center;
cursor: pointer;
*/

View file

@ -0,0 +1,18 @@
import React from 'react';
import cn from 'classnames';
import stl from './bottomBlock.module.css';
const BottomBlock = ({
children = null,
className = '',
additionalHeight = 0,
...props
}) => (
<div className={ cn(stl.wrapper, "flex flex-col mb-2") } { ...props } >
{ children }
</div>
);
BottomBlock.displayName = 'BottomBlock';
export default BottomBlock;

View file

@ -0,0 +1,17 @@
import React from 'react';
import cn from 'classnames';
import stl from './content.module.css';
const Content = ({
children,
className,
...props
}) => (
<div className={ cn(className, stl.content) } { ...props } >
{ children }
</div>
);
Content.displayName = 'Content';
export default Content;

View file

@ -0,0 +1,26 @@
import React from 'react';
import { connect } from 'react-redux';
import cn from 'classnames';
import { closeBottomBlock } from 'Duck/components/player';
import { Input, CloseButton } from 'UI';
import stl from './header.module.css';
const Header = ({
children,
className,
closeBottomBlock,
onFilterChange,
showClose = true,
...props
}) => (
<div className={ cn("relative border-r border-l py-1", stl.header) } >
<div className={ cn("w-full h-full flex justify-between items-center", className) } >
<div className="w-full flex items-center justify-between">{ children }</div>
{ showClose && <CloseButton onClick={ closeBottomBlock } size="18" className="ml-2" /> }
</div>
</div>
);
Header.displayName = 'Header';
export default connect(null, { closeBottomBlock })(Header);

View file

@ -0,0 +1,20 @@
import React from 'react';
import cn from 'classnames';
import cls from './infoLine.module.css';
const InfoLine = ({ children }) => (
<div className={ cls.info }>
{ children }
</div>
)
const Point = ({ label = '', value = '', display=true, color, dotColor }) => display
? <div className={ cls.infoPoint } style={{ color }}>
{ dotColor != null && <div className={ cn(cls.dot, `bg-${dotColor}`) } /> }
<span className={cls.label}>{ `${label}` }</span> { value }
</div>
: null;
InfoLine.Point = Point;
export default InfoLine;

View file

@ -0,0 +1,9 @@
.wrapper {
background: $white;
/* padding-right: 10px; */
/* border: solid thin $gray-light; */
height: 300px;
border-top: thin dashed #cccccc;
}

View file

@ -0,0 +1,3 @@
.content {
height: 86%;
}

View file

@ -0,0 +1,6 @@
.header {
padding: 0 10px;
height: 40px;
border-bottom: 1px solid $gray-light;
}

View file

@ -0,0 +1,8 @@
import BottomBlock from './BottomBlock';
import Header from './Header';
import Content from './Content';
BottomBlock.Header = Header;
BottomBlock.Content = Content;
export default BottomBlock;

View file

@ -0,0 +1,31 @@
.info {
padding-left: 10px;
height: 36px;
display: flex;
align-items: center;
& >.infoPoint {
font-size: 14px;
display: flex;
align-items: center;
&:not(:last-child):after {
content: '';
margin: 0 12px;
height: 30px;
border-right: 1px solid $gray-light-shade;
}
& .label {
font-weight: 500;
margin-right: 6px;
}
}
}
.dot {
width: 10px;
height: 10px;
border-radius: 5px;
margin-right: 5px;
}

View file

@ -0,0 +1,9 @@
// import { NONE, CONSOLE, NETWORK, STACKEVENTS, REDUX_STATE, PROFILER, PERFORMANCE, GRAPHQL } from 'Duck/components/player';
//
//
// export default {
// [NONE]: {
// Component: null,
//
// }
// }

View file

@ -0,0 +1,139 @@
import React, { useState } from 'react';
import { connectPlayer, jump } from 'Player';
import Log from 'Types/session/log';
import BottomBlock from '../BottomBlock';
import { LEVEL } from 'Types/session/log';
import { Tabs, Input, Icon, NoContent } from 'UI';
// import Autoscroll from 'App/components/Session_/Autoscroll';
import cn from 'classnames';
import ConsoleRow from '../ConsoleRow';
import { getRE } from 'App/utils';
const ALL = 'ALL';
const INFO = 'INFO';
const WARNINGS = 'WARNINGS';
const ERRORS = 'ERRORS';
const LEVEL_TAB = {
[LEVEL.INFO]: INFO,
[LEVEL.LOG]: INFO,
[LEVEL.WARNING]: WARNINGS,
[LEVEL.ERROR]: ERRORS,
[LEVEL.EXCEPTION]: ERRORS,
};
const TABS = [ALL, ERRORS, WARNINGS, INFO].map((tab) => ({ text: tab, key: tab }));
function renderWithNL(s = '') {
if (typeof s !== 'string') return '';
return s.split('\n').map((line, i) => <div className={cn({ 'ml-20': i !== 0 })}>{line}</div>);
}
const getIconProps = (level: any) => {
switch (level) {
case LEVEL.INFO:
case LEVEL.LOG:
return {
name: 'console/info',
color: 'blue2',
};
case LEVEL.WARN:
case LEVEL.WARNING:
return {
name: 'console/warning',
color: 'red2',
};
case LEVEL.ERROR:
return {
name: 'console/error',
color: 'red',
};
}
return null;
};
interface Props {
logs: any;
exceptions: any;
}
function ConsolePanel(props: Props) {
const { logs } = props;
const additionalHeight = 0;
const [activeTab, setActiveTab] = useState(ALL);
const [filter, setFilter] = useState('');
let filtered = React.useMemo(() => {
const filterRE = getRE(filter, 'i');
let list = logs;
list = list.filter(
({ value, level }: any) =>
(!!filter ? filterRE.test(value) : true) &&
(activeTab === ALL || activeTab === LEVEL_TAB[level])
);
return list;
}, [filter, activeTab]);
const onTabClick = (activeTab: any) => setActiveTab(activeTab);
const onFilterChange = ({ target: { value } }: any) => setFilter(value);
return (
<BottomBlock style={{ height: 300 + additionalHeight + 'px' }}>
<BottomBlock.Header>
<div className="flex items-center">
<span className="font-semibold color-gray-medium mr-4">Console</span>
<Tabs tabs={TABS} active={activeTab} onClick={onTabClick} border={false} />
</div>
<Input
className="input-small h-8"
placeholder="Filter by keyword"
icon="search"
iconPosition="left"
name="filter"
height={28}
onChange={onFilterChange}
/>
</BottomBlock.Header>
<BottomBlock.Content className="overflow-y-auto">
<NoContent
title={
<div className="capitalize flex items-center mt-16">
<Icon name="info-circle" className="mr-2" size="18" />
No Data
</div>
}
size="small"
show={filtered.length === 0}
>
{/* <Autoscroll> */}
{filtered.map((l: any, index: any) => (
<ConsoleRow
key={index}
log={l}
jump={jump}
iconProps={getIconProps(l.level)}
renderWithNL={renderWithNL}
/>
))}
{/* </Autoscroll> */}
</NoContent>
</BottomBlock.Content>
</BottomBlock>
);
}
export default connectPlayer((state: any) => {
const logs = state.logList;
const exceptions = state.exceptionsList; // TODO merge
const logExceptions = exceptions.map(({ time, errorId, name, projectId }: any) =>
Log({
level: LEVEL.ERROR,
value: name,
time,
errorId,
})
);
return {
logs: logs.concat(logExceptions),
};
})(ConsolePanel);

View file

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

View file

@ -0,0 +1,59 @@
import React, { useState } from 'react';
import cn from 'classnames';
// import stl from '../console.module.css';
import { Icon } from 'UI';
import JumpButton from 'Shared/DevTools/JumpButton';
import { useModal } from 'App/components/Modal';
import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorDetailsModal';
interface Props {
log: any;
iconProps: any;
jump?: any;
renderWithNL?: any;
}
function ConsoleRow(props: Props) {
const { log, iconProps, jump, renderWithNL } = props;
const { showModal } = useModal();
const [expanded, setExpanded] = useState(false);
const lines = log.value.split('\n').filter((l: any) => !!l);
const canExpand = lines.length > 1;
const clickable = canExpand || !!log.errorId;
const onErrorClick = () => {
showModal(<ErrorDetailsModal errorId={log.errorId} />, { right: true });
};
return (
<div
className={cn(
'border-b flex items-center py-2 px-4 overflow-hidden group relative select-none',
{
info: !log.isYellow() && !log.isRed(),
warn: log.isYellow(),
error: log.isRed(),
'cursor-pointer': clickable,
}
)}
onClick={
clickable ? () => (!!log.errorId ? onErrorClick() : setExpanded(!expanded)) : () => {}
}
>
<div className="mr-2">
<Icon size="14" {...iconProps} />
</div>
<div key={log.key} data-scroll-item={log.isRed()}>
<div className={cn('flex items-center')}>
{canExpand && (
<Icon name={expanded ? 'caret-down-fill' : 'caret-right-fill'} className="mr-2" />
)}
<span>{renderWithNL(lines.pop())}</span>
</div>
{canExpand && expanded && lines.map((l: any) => <div className="ml-4 mb-1">{l}</div>)}
</div>
<JumpButton onClick={() => jump(log.time)} />
</div>
);
}
export default ConsoleRow;

View file

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

View file

@ -0,0 +1,26 @@
import React from 'react';
import { Icon, Popup } from 'UI';
interface Props {
onClick: any;
tooltip?: string;
}
function JumpButton(props: Props) {
const { tooltip = '' } = props;
return (
<Popup content={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) => {
e.stopPropagation();
props.onClick();
}}
>
<Icon name="caret-right-fill" size="12" color="teal" />
<span>JUMP</span>
</div>
</Popup>
);
}
export default JumpButton;

View file

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

View file

@ -0,0 +1,403 @@
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 { getRE } from 'App/utils';
import Resource, { TYPES } from 'Types/session/resource';
import { formatBytes } from 'App/utils';
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 { useModal } from 'App/components/Modal';
import FetchDetailsModal from 'Shared/FetchDetailsModal';
import { sort } from 'App/duck/sessions';
const ALL = 'ALL';
const XHR = 'xhr';
const JS = 'js';
const CSS = 'css';
const IMG = 'img';
const MEDIA = 'media';
const OTHER = 'other';
const TAB_TO_TYPE_MAP: any = {
[XHR]: TYPES.XHR,
[JS]: TYPES.JS,
[CSS]: TYPES.CSS,
[IMG]: TYPES.IMG,
[MEDIA]: TYPES.MEDIA,
[OTHER]: TYPES.OTHER,
};
const TABS: any = [ALL, XHR, JS, CSS, IMG, MEDIA, OTHER].map((tab) => ({
text: tab === 'xhr' ? 'XHR (Fetch)' : tab,
key: tab,
}));
const DOM_LOADED_TIME_COLOR = 'teal';
const LOAD_TIME_COLOR = 'red';
function compare(a: any, b: any, key: string) {
if (a[key] > b[key]) return 1;
if (a[key] < b[key]) return -1;
return 0;
}
export function renderType(r: any) {
return (
<Popup style={{ width: '100%' }} content={<div>{r.type}</div>}>
<div>{r.type}</div>
</Popup>
);
}
export function renderName(r: any) {
return (
<Popup style={{ width: '100%' }} content={<div>{r.url}</div>}>
<div>{r.name}</div>
</Popup>
);
}
export function renderStart(r: any) {
return (
<div className="flex justify-between items-center grow-0 w-full">
<span>{Duration.fromMillis(r.time).toFormat('mm:ss.SSS')}</span>
</div>
);
}
const renderXHRText = () => (
<span className="flex items-center">
{XHR}
<QuestionMarkHint
onHover={true}
content={
<>
Use our{' '}
<a
className="color-teal underline"
target="_blank"
href="https://docs.openreplay.com/plugins/fetch"
>
Fetch plugin
</a>
{' to capture HTTP requests and responses, including status codes and bodies.'} <br />
We also provide{' '}
<a
className="color-teal underline"
target="_blank"
href="https://docs.openreplay.com/plugins/graphql"
>
support for GraphQL
</a>
{' for easy debugging of your queries.'}
</>
}
className="ml-1"
/>
</span>
);
function renderSize(r: any) {
if (r.responseBodySize) return formatBytes(r.responseBodySize);
let triggerText;
let content;
if (r.decodedBodySize == null) {
triggerText = 'x';
content = 'Not captured';
} else {
const headerSize = r.headerSize || 0;
const encodedSize = r.encodedBodySize || 0;
const transferred = headerSize + encodedSize;
const showTransferred = r.headerSize != null;
triggerText = formatBytes(r.decodedBodySize);
content = (
<ul>
{showTransferred && (
<li>{`${formatBytes(r.encodedBodySize + headerSize)} transfered over network`}</li>
)}
<li>{`Resource size: ${formatBytes(r.decodedBodySize)} `}</li>
</ul>
);
}
return (
<Popup style={{ width: '100%' }} content={content}>
<div>{triggerText}</div>
</Popup>
);
}
export function renderDuration(r: any) {
if (!r.success) return 'x';
const text = `${Math.floor(r.duration)}ms`;
if (!r.isRed() && !r.isYellow()) return text;
let tooltipText;
let className = 'w-full h-full flex items-center ';
if (r.isYellow()) {
tooltipText = 'Slower than average';
className += 'warn color-orange';
} else {
tooltipText = 'Much slower than average';
className += 'error color-red';
}
return (
<Popup style={{ width: '100%' }} content={tooltipText}>
<div> {text} </div>
</Popup>
);
}
interface Props {
location: any;
resources: any;
fetchList: any;
domContentLoadedTime: any;
loadTime: any;
playing: boolean;
domBuildingTime: any;
currentIndex: any;
time: any;
}
function NetworkPanel(props: Props) {
const {
resources,
time,
currentIndex,
domContentLoadedTime,
loadTime,
playing,
domBuildingTime,
fetchList,
} = props;
const { showModal, hideModal } = useModal();
const [activeTab, setActiveTab] = useState(ALL);
const [sortBy, setSortBy] = useState('time');
const [sortAscending, setSortAscending] = useState(true);
const [filter, setFilter] = useState('');
const [showOnlyErrors, setShowOnlyErrors] = useState(false);
const onTabClick = (activeTab: any) => setActiveTab(activeTab);
const onFilterChange = ({ target: { value } }: any) => setFilter(value);
const additionalHeight = 0;
const fetchPresented = fetchList.length > 0;
const resourcesSize = resources.reduce(
(sum: any, { decodedBodySize }: any) => sum + (decodedBodySize || 0),
0
);
const transferredSize = resources.reduce(
(sum: any, { headerSize, encodedBodySize }: any) =>
sum + (headerSize || 0) + (encodedBodySize || 0),
0
);
const filterRE = getRE(filter, 'i');
let filtered = React.useMemo(() => {
let list = resources;
fetchList.forEach(
(fetchCall: any) =>
(list = list.filter((networkCall: any) => networkCall.url !== fetchCall.url))
);
list = list.concat(fetchList);
list = list.sort((a: any, b: any) => {
return compare(a, b, sortBy);
});
if (!sortAscending) {
list = list.reverse();
}
list = list.filter(
({ type, name, status }: any) =>
(!!filter ? filterRE.test(status) || filterRE.test(name) || filterRE.test(type) : true) &&
(activeTab === ALL || type === TAB_TO_TYPE_MAP[activeTab]) &&
(showOnlyErrors ? parseInt(status) >= 400 : true)
);
return list;
}, [filter, sortBy, sortAscending, showOnlyErrors, activeTab]);
// const lastIndex = currentIndex || filtered.filter((item: any) => item.time <= time).length - 1;
const referenceLines = [];
if (domContentLoadedTime != null) {
referenceLines.push({
time: domContentLoadedTime.time,
color: DOM_LOADED_TIME_COLOR,
});
}
if (loadTime != null) {
referenceLines.push({
time: loadTime.time,
color: LOAD_TIME_COLOR,
});
}
const onRowClick = (row: any) => {
showModal(<FetchDetailsModal resource={row} fetchPresented={fetchPresented} />, {
right: true,
});
};
const handleSort = (sortKey: string) => {
if (sortKey === sortBy) {
setSortAscending(!sortAscending);
// setSortBy('time');
}
setSortBy(sortKey);
};
return (
<React.Fragment>
<BottomBlock style={{ height: 300 + additionalHeight + 'px' }} className="border">
<BottomBlock.Header>
<div className="flex items-center">
<span className="font-semibold color-gray-medium mr-4">Network</span>
<Tabs
className="uppercase"
tabs={TABS}
active={activeTab}
onClick={onTabClick}
border={false}
/>
</div>
<Input
className="input-small"
placeholder="Filter by name, type or value"
icon="search"
iconPosition="left"
name="filter"
onChange={onFilterChange}
height={28}
width={230}
/>
</BottomBlock.Header>
<BottomBlock.Content>
<div className="flex items-center justify-between px-4">
<div>
<Toggler
checked={showOnlyErrors}
name="test"
onChange={() => setShowOnlyErrors(!showOnlyErrors)}
label="4xx-5xx Only"
/>
</div>
<InfoLine>
<InfoLine.Point label={filtered.length} value=" requests" />
<InfoLine.Point
label={formatBytes(transferredSize)}
value="transferred"
display={transferredSize > 0}
/>
<InfoLine.Point
label={formatBytes(resourcesSize)}
value="resources"
display={resourcesSize > 0}
/>
<InfoLine.Point
label={formatMs(domBuildingTime)}
value="DOM Building Time"
display={domBuildingTime != null}
/>
<InfoLine.Point
label={domContentLoadedTime && formatMs(domContentLoadedTime.value)}
value="DOMContentLoaded"
display={domContentLoadedTime != null}
dotColor={DOM_LOADED_TIME_COLOR}
/>
<InfoLine.Point
label={loadTime && formatMs(loadTime.value)}
value="Load"
display={loadTime != null}
dotColor={LOAD_TIME_COLOR}
/>
</InfoLine>
</div>
<NoContent
title={
<div className="capitalize flex items-center mt-16">
<Icon name="info-circle" className="mr-2" size="18" />
No Data
</div>
}
size="small"
show={filtered.length === 0}
>
<TimeTable
rows={filtered}
referenceLines={referenceLines}
renderPopup
onRowClick={onRowClick}
additionalHeight={additionalHeight}
onJump={jump}
sortBy={sortBy}
sortAscending={sortAscending}
// activeIndex={lastIndex}
>
{[
// {
// label: 'Start',
// width: 120,
// render: renderStart,
// },
{
label: 'Status',
dataKey: 'status',
width: 70,
onClick: handleSort,
},
{
label: 'Type',
dataKey: 'type',
width: 90,
render: renderType,
onClick: handleSort,
},
{
label: 'Name',
width: 240,
dataKey: 'name',
render: renderName,
onClick: handleSort,
},
{
label: 'Size',
width: 80,
dataKey: 'decodedBodySize',
render: renderSize,
onClick: handleSort,
},
{
label: 'Time',
width: 80,
dataKey: 'duration',
render: renderDuration,
onClick: handleSort,
},
]}
</TimeTable>
</NoContent>
</BottomBlock.Content>
</BottomBlock>
</React.Fragment>
);
}
export default connectPlayer((state: any) => ({
location: state.location,
resources: state.resourceList,
fetchList: state.fetchList.map((i: any) => Resource({ ...i.toJS(), type: TYPES.XHR })),
domContentLoadedTime: state.domContentLoadedTime,
loadTime: state.loadTime,
// time: state.time,
playing: state.playing,
domBuildingTime: state.domBuildingTime,
}))(NetworkPanel);

View file

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

View file

@ -0,0 +1,26 @@
import React from 'react';
interface Props {
profile: any;
}
function ProfilerModal(props: Props) {
const {
profile: { name, args, result },
} = props;
return (
<div className="bg-white overflow-y-auto h-screen p-5" style={{ width: '500px' }}>
<h5 className="mb-2 text-2xl">{name}</h5>
<h5 className="py-3">{'Arguments'}</h5>
<ul className="color-gray-medium">
{args.split(',').map((arg: any) => (
<li> {`${arg}`} </li>
))}
</ul>
<h5 className="py-3">{'Result'}</h5>
<div className="color-gray-medium">{`${result}`}</div>
</div>
);
}
export default ProfilerModal;

View file

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

View file

@ -0,0 +1,75 @@
import React, { useState } from 'react';
import { connectPlayer } from 'Player';
import { TextEllipsis, Input } from 'UI';
import { getRE } from 'App/utils';
// import ProfileInfo from './ProfileInfo';
import TimeTable from '../TimeTable';
import BottomBlock from '../BottomBlock';
import { useModal } from 'App/components/Modal';
import ProfilerModal from '../ProfilerModal';
const renderDuration = (p: any) => `${p.duration}ms`;
const renderName = (p: any) => <TextEllipsis text={p.name} />;
interface Props {
profiles: any;
}
function ProfilerPanel(props: Props) {
const { profiles } = props;
const { showModal } = useModal();
const [filter, setFilter] = useState('');
const filtered: any = React.useMemo(() => {
const filterRE = getRE(filter, 'i');
let list = profiles;
list = list.filter(({ name }: any) => (!!filter ? filterRE.test(name) : true));
return list;
}, [filter]);
const onFilterChange = ({ target: { value } }: any) => setFilter(value);
const onRowClick = (profile: any) => {
showModal(<ProfilerModal profile={profile} />, { right: true });
};
return (
<BottomBlock>
<BottomBlock.Header>
<div className="flex items-center">
<span className="font-semibold color-gray-medium mr-4">Profiler</span>
</div>
<Input
// className="input-small"
placeholder="Filter by name"
icon="search"
name="filter"
onChange={onFilterChange}
height={28}
/>
</BottomBlock.Header>
<BottomBlock.Content>
<TimeTable rows={filtered} onRowClick={onRowClick} hoverable>
{[
{
label: 'Name',
dataKey: 'name',
width: 200,
render: renderName,
},
{
label: 'Time',
key: 'duration',
width: 80,
render: renderDuration,
},
]}
</TimeTable>
</BottomBlock.Content>
</BottomBlock>
);
}
export default connectPlayer((state: any) => {
return {
profiles: state.profilesList,
};
})(ProfilerPanel);

View file

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

View file

@ -0,0 +1,32 @@
import React from 'react';
import { DATADOG, SENTRY, STACKDRIVER, typeList } from 'Types/session/stackEvent';
import JsonViewer from 'Components/Session_/StackEvents/UserEvent/JsonViewer';
import Sentry from 'Components/Session_/StackEvents/UserEvent/Sentry';
interface Props {
event: any;
}
function StackEventModal(props: Props) {
const { event } = props;
const renderPopupContent = () => {
const { source, payload, name } = event;
switch (source) {
case SENTRY:
return <Sentry event={payload} />;
case DATADOG:
return <JsonViewer title={name} data={payload} icon="integrations/datadog" />;
case STACKDRIVER:
return <JsonViewer title={name} data={payload} icon="integrations/stackdriver" />;
default:
return <JsonViewer title={name} data={payload} icon={`integrations/${source}`} />;
}
};
return (
<div className="bg-white overflow-y-auto h-screen p-5" style={{ width: '500px' }}>
<h5 className="mb-2 text-2xl">Stack Event</h5>
{renderPopupContent()}
</div>
);
}
export default StackEventModal;

View file

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

View file

@ -0,0 +1,52 @@
import React from 'react';
import JumpButton from '../JumpButton';
import { Icon } from 'UI';
import cn from 'classnames';
import { OPENREPLAY, SENTRY, DATADOG, STACKDRIVER } from 'Types/session/stackEvent';
import { useModal } from 'App/components/Modal';
import StackEventModal from '../StackEventModal';
interface Props {
event: any;
onJump: any;
}
function StackEventRow(props: Props) {
const { event, onJump } = props;
let message = event.payload[0] || '';
message = typeof message === 'string' ? message : JSON.stringify(message);
const onClickDetails = () => {
showModal(<StackEventModal event={event} />, { right: true });
};
const { showModal } = useModal();
const iconProps: any = React.useMemo(() => {
const { source } = event;
return {
name: `integrations/${source}`,
size: 18,
marginRight: source === OPENREPLAY ? 11 : 10,
};
}, [event]);
return (
<div
data-scroll-item={event.isRed()}
onClick={onClickDetails}
className={cn(
'group flex items-center py-2 px-4 border-b cursor-pointer relative',
'hover:bg-active-blue'
)}
>
<div className={cn('mr-auto flex items-start')}>
<Icon {...iconProps} />
<div>
<div className="capitalize font-medium mb-1">{event.name}</div>
<div className="code-font text-xs">{message}</div>
</div>
</div>
<JumpButton onClick={onJump} />
</div>
);
}
export default StackEventRow;

View file

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

View file

@ -0,0 +1,96 @@
import { Popup } from 'UI';
import { percentOf } from 'App/utils';
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`;
interface Props {
resource: {
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 timeOffset = time - timestart;
ttfb = ttfb || 0;
const trigger = (
<div
className={styles.barWrapper}
style={{
left: `${percentOf(timeOffset, timewidth)}%`,
right: `${100 - percentOf(timeOffset + duration, timewidth)}%`,
minWidth: '5px'
}}
>
<div
className={styles.ttfbBar}
style={{
width: `${percentOf(ttfb, duration)}%`,
}}
/>
<div
className={styles.downloadBar}
style={{
width: `${percentOf(duration - ttfb, duration)}%`,
minWidth: '5px'
}}
/>
</div>
);
if (!popup) return <div key={key} className={tableStyles.row} > {trigger} </div>;
return (
<div key={key} className={tableStyles.row} >
<Popup
basic
content={
<React.Fragment>
{ttfb != null &&
<div className={styles.popupRow}>
<div className={styles.title}>{'Waiting (TTFB)'}</div>
<div className={styles.popupBarWrapper} >
<div
className={styles.ttfbBar}
style={{
left: 0,
width: `${percentOf(ttfb, duration)}%`,
}}
/>
</div>
<div className={styles.time} >{formatTime(ttfb)}</div>
</div>
}
<div className={styles.popupRow}>
<div className={styles.title} >{'Content Download'}</div>
<div className={styles.popupBarWrapper}>
<div
className={styles.downloadBar}
style={{
left: `${percentOf(ttfb, duration)}%`,
width: `${percentOf(duration - ttfb, duration)}%`,
}}
/>
</div>
<div className={styles.time}>{formatTime(duration - ttfb)}</div>
</div>
</React.Fragment>
}
size="mini"
position="top center"
/>
</div>
);
}
BarRow.displayName = "BarRow";
export default BarRow;

View file

@ -0,0 +1,382 @@
import React from 'react';
import { List, AutoSizer } from 'react-virtualized';
import cn from 'classnames';
import { Duration } from 'luxon';
import { NoContent, Icon, Button } from 'UI';
import { percentOf } from 'App/utils';
import BarRow from './BarRow';
import stl from './timeTable.module.css';
import autoscrollStl from '../autoscroll.module.css'; //aaa
import JumpButton from '../JumpButton';
type Timed = {
time: number;
};
type Durationed = {
duration: number;
};
type CanBeRed = {
//+isRed: boolean,
isRed: () => boolean;
};
interface Row extends Timed, Durationed, CanBeRed {
[key: string]: any;
key: string;
}
type Line = {
color: string; // Maybe use typescript?
hint?: string;
onClick?: any;
} & Timed;
type Column = {
label: string;
width: number;
dataKey?: string;
render?: (row: any) => void;
referenceLines?: Array<Line>;
style?: React.CSSProperties;
onClick?: void;
} & RenderOrKey;
// type RenderOrKey = { // Disjoint?
// render: Row => React.Node
// } | {
// dataKey: string,
// }
type RenderOrKey =
| {
render?: (row: Row) => React.ReactNode;
key?: string;
}
| {
dataKey: string;
};
type Props = {
className?: string;
rows: Array<Row>;
children: Array<Column>;
tableHeight?: number;
activeIndex?: number;
renderPopup?: boolean;
navigation?: boolean;
referenceLines?: any[];
additionalHeight?: number;
hoverable?: boolean;
onRowClick?: (row: any, index: number) => void;
onJump?: (time: any) => void;
sortBy?: string;
sortAscending?: boolean;
};
type TimeLineInfo = {
timestart: number;
timewidth: number;
};
type State = TimeLineInfo & typeof initialState;
//const TABLE_HEIGHT = 195;
let _additionalHeight = 0;
const ROW_HEIGHT = 32;
//const VISIBLE_COUNT = Math.ceil(TABLE_HEIGHT/ROW_HEIGHT);
const TIME_SECTIONS_COUNT = 8;
const ZERO_TIMEWIDTH = 1000;
function formatTime(ms: number) {
if (ms < 0) return '';
if (ms < 1000) return Duration.fromMillis(ms).toFormat('0.SSS');
return Duration.fromMillis(ms).toFormat('mm:ss');
}
function computeTimeLine(
rows: Array<Row>,
firstVisibleRowIndex: number,
visibleCount: number
): TimeLineInfo {
const visibleRows = rows.slice(
firstVisibleRowIndex,
firstVisibleRowIndex + visibleCount + _additionalHeight
);
let timestart = visibleRows.length > 0 ? Math.min(...visibleRows.map((r) => r.time)) : 0;
// TODO: GraphQL requests do not have a duration, so their timeline is borked. Assume a duration of 0.2s for every GraphQL request
const timeend =
visibleRows.length > 0 ? Math.max(...visibleRows.map((r) => r.time + (r.duration ?? 200))) : 0;
let timewidth = timeend - timestart;
const offset = timewidth / 70;
if (timestart >= offset) {
timestart -= offset;
}
timewidth *= 1.5; // += offset;
if (timewidth === 0) {
timewidth = ZERO_TIMEWIDTH;
}
return {
timestart,
timewidth,
};
}
const initialState = {
firstVisibleRowIndex: 0,
};
export default class TimeTable extends React.PureComponent<Props, State> {
state = {
...computeTimeLine(this.props.rows, initialState.firstVisibleRowIndex, this.visibleCount),
...initialState,
};
get tableHeight() {
return this.props.tableHeight || 195;
}
get visibleCount() {
return Math.ceil(this.tableHeight / ROW_HEIGHT);
}
scroller = React.createRef<List>();
autoScroll = true;
componentDidMount() {
if (this.scroller.current) {
this.scroller.current.scrollToRow(this.props.activeIndex);
}
}
componentDidUpdate(prevProps: any, prevState: any) {
if (
prevState.firstVisibleRowIndex !== this.state.firstVisibleRowIndex ||
(this.props.rows.length <= this.visibleCount + _additionalHeight &&
prevProps.rows.length !== this.props.rows.length)
) {
this.setState({
...computeTimeLine(this.props.rows, this.state.firstVisibleRowIndex, this.visibleCount),
});
}
if (
this.props.activeIndex &&
this.props.activeIndex >= 0 &&
prevProps.activeIndex !== this.props.activeIndex &&
this.scroller.current
) {
this.scroller.current.scrollToRow(this.props.activeIndex);
}
}
onScroll = ({
scrollTop,
scrollHeight,
clientHeight,
}: {
scrollTop: number;
scrollHeight: number;
clientHeight: number;
}): void => {
const firstVisibleRowIndex = Math.floor(scrollTop / ROW_HEIGHT + 0.33);
if (this.state.firstVisibleRowIndex !== firstVisibleRowIndex) {
this.autoScroll = scrollHeight - clientHeight - scrollTop < ROW_HEIGHT / 2;
this.setState({ firstVisibleRowIndex });
}
};
onJump = (index: any) => {
if (this.props.onJump) {
this.props.onJump(this.props.rows[index].time);
}
};
renderRow = ({ index, key, style: rowStyle }: any) => {
const { activeIndex } = this.props;
const { children: columns, rows, renderPopup, hoverable, onRowClick } = this.props;
const { timestart, timewidth } = this.state;
const row = rows[index];
return (
<div
style={rowStyle}
key={key}
className={cn('border-b border-color-gray-light-shade group items-center', stl.row, {
[stl.hoverable]: hoverable,
'error color-red': !!row.isRed && row.isRed(),
'cursor-pointer': typeof onRowClick === 'function',
[stl.activeRow]: activeIndex === index,
// [stl.inactiveRow]: !activeIndex || index > activeIndex,
})}
onClick={typeof onRowClick === 'function' ? () => onRowClick(row, index) : undefined}
id="table-row"
>
{columns.map(({ dataKey, render, width }) => (
<div className={stl.cell} style={{ width: `${width}px` }}>
{render
? render(row)
: row[dataKey || ''] || <i className="color-gray-light">{'empty'}</i>}
</div>
))}
<div className={cn('relative flex-1 flex', stl.timeBarWrapper)}>
<BarRow resource={row} timestart={timestart} timewidth={timewidth} popup={renderPopup} />
</div>
<JumpButton onClick={() => this.onJump(index)} />
</div>
);
};
onPrevClick = () => {
let prevRedIndex = -1;
for (let i = this.state.firstVisibleRowIndex - 1; i >= 0; i--) {
if (this.props.rows[i].isRed()) {
prevRedIndex = i;
break;
}
}
if (this.scroller.current != null) {
this.scroller.current.scrollToRow(prevRedIndex);
}
};
onNextClick = () => {
let prevRedIndex = -1;
for (let i = this.state.firstVisibleRowIndex + 1; i < this.props.rows.length; i++) {
if (this.props.rows[i].isRed()) {
prevRedIndex = i;
break;
}
}
if (this.scroller.current != null) {
this.scroller.current.scrollToRow(prevRedIndex);
}
};
onColumnClick = (dataKey: string, onClick: any) => {
if (typeof onClick === 'function') {
// this.scroller.current.scrollToRow(0);
onClick(dataKey);
this.scroller.current.forceUpdateGrid();
}
};
render() {
const {
className,
rows,
children: columns,
navigation = false,
referenceLines = [],
additionalHeight = 0,
activeIndex,
sortBy = '',
sortAscending = true,
} = this.props;
const { timewidth, timestart } = this.state;
_additionalHeight = additionalHeight;
const sectionDuration = Math.round(timewidth / TIME_SECTIONS_COUNT);
const timeColumns: number[] = [];
if (timewidth > 0) {
for (let i = 0; i < TIME_SECTIONS_COUNT; i++) {
timeColumns.push(timestart + i * sectionDuration);
}
}
const visibleRefLines = referenceLines.filter(
({ time }) => time > timestart && time < timestart + timewidth
);
const columnsSumWidth = columns.reduce((sum, { width }) => sum + width, 0);
return (
<div className={cn(className, 'relative')}>
{navigation && (
<div className={cn(autoscrollStl.navButtons, 'flex items-center')}>
<Button
variant="text-primary"
icon="chevron-up"
tooltip={{
title: 'Previous Error',
delay: 0,
}}
onClick={this.onPrevClick}
/>
<Button
variant="text-primary"
icon="chevron-down"
tooltip={{
title: 'Next Error',
delay: 0,
}}
onClick={this.onNextClick}
/>
</div>
)}
<div className={stl.headers}>
<div className={stl.infoHeaders}>
{columns.map(({ label, width, dataKey, onClick = null }) => (
<div
className={cn(stl.headerCell, 'flex items-center select-none', {
'cursor-pointer': typeof onClick === 'function',
})}
style={{ width: `${width}px` }}
onClick={() => this.onColumnClick(dataKey, onClick)}
>
<span>{label}</span>
{!!sortBy && sortBy === dataKey && <Icon name={ sortAscending ? "caret-down-fill" : "caret-up-fill" } className="ml-1" />}
</div>
))}
</div>
<div className={stl.waterfallHeaders}>
{timeColumns.map((time, i) => (
<div className={stl.timeCell} key={`tc-${i}`}>
{formatTime(time)}
</div>
))}
</div>
</div>
<NoContent size="small" show={rows.length === 0}>
<div className="relative">
<div className={stl.timePart} style={{ left: `${columnsSumWidth}px` }}>
{timeColumns.map((_, index) => (
<div key={`tc-${index}`} className={stl.timeCell} />
))}
{visibleRefLines.map(({ time, color, onClick }) => (
<div
className={cn(stl.refLine, `bg-${color}`)}
style={{
left: `${percentOf(time - timestart, timewidth)}%`,
cursor: typeof onClick === 'function' ? 'click' : 'auto',
}}
onClick={onClick}
/>
))}
</div>
<AutoSizer disableHeight>
{({ width }: { width: number }) => (
<List
ref={this.scroller}
className={stl.list}
height={this.tableHeight + additionalHeight}
width={width}
overscanRowCount={20}
rowCount={rows.length}
rowHeight={ROW_HEIGHT}
rowRenderer={this.renderRow}
onScroll={this.onScroll}
scrollToAlignment="start"
forceUpdateProp={timestart | timewidth | (activeIndex || 0)}
/>
)}
</AutoSizer>
</div>
</NoContent>
</div>
);
}
}

View file

@ -0,0 +1,45 @@
.barWrapper {
display: flex;
position: absolute;
top: 35%;
bottom: 35%;
border-radius: 3px;
overflow: hidden;
}
.downloadBar, .ttfbBar {
/* box-shadow: inset 0px 0px 0px 1px $teal; */
height: 100%;
box-sizing: border-box;
position: relative;
}
.ttfbBar {
background-color: rgba(175, 226, 221, 0.8);
}
.downloadBar {
background-color: rgba(133, 200, 192, 0.8);
}
.popupRow {
color: $gray-medium;
display: flex;
align-items: center;
padding: 2px 0;
font-size: 12px;
}
.title {
width: 105px;
}
.time {
width: 60px;
padding-left: 10px;
}
.popupBarWrapper {
width: 220px;
height: 15px;
border-radius: 3px;
overflow: hidden;
}

View file

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

View file

@ -0,0 +1,113 @@
$offset: 10px;
.timeCell {
border-left: solid thin rgba(0, 0, 0, 0.05);
}
.headers {
box-shadow: 0 1px 2px 0 $gray-light;
background-color: $gray-lightest;
color: $gray-darkest;
font-size: 12px;
overflow-x: hidden;
white-space: nowrap;
width: 100%;
display: flex;
padding: 0 $offset;
}
.infoHeaders {
text-transform: uppercase;
display: flex;
& .headerCell {
padding: 4px 2px;
}
}
.waterfallHeaders {
display: flex;
flex: 1;
& .timeCell {
flex: 1;
overflow: hidden;
padding: 4px 0;
}
}
.list {
/* TODO hide the scrollbar track */
&::-webkit-scrollbar {
width: 1px;
}
scrollbar-width: thin;
font-size: 12px;
font-family: 'Menlo', 'monaco', 'consolas', monospace;
}
.row {
display: flex;
padding: 0 $offset;
&:hover {
background-color: $active-blue;
}
/*align-items: center;
cursor: pointer;
*/
/* &:nth-child(even) {
background-color: $gray-lightest;
} */
/* & > div:first-child {
padding-left: 5px;
}*/
}
.cell {
height: 100%;
display: flex;
align-items: center;
overflow: hidden;
padding: 0 2px;
white-space: nowrap;
}
.hoverable {
transition: all 0.3s;
cursor: pointer;
&:hover {
background-color: $active-blue;
transition: all 0.2s;
color: $gray-dark;
}
}
.timeBarWrapper{
overflow: hidden;
}
.timePart {
position: absolute;
top: 0;
bottom: 0;
/*left:0;*/
right: 0;
display: flex;
margin: 0 $offset;
& .timeCell {
height: 100%;
flex: 1;
z-index: 1;
pointer-events: none;
}
& .refLine {
position: absolute;
height: 100%;
width: 1px;
z-index: 1;
}
}
.activeRow {
background-color: $teal-light;
}
.inactiveRow {
opacity: 0.5;
}

View file

@ -0,0 +1,12 @@
.navButtons {
position: absolute;
background: rgba(255, 255, 255, 0.5);
padding: 4px;
right: 24px;
top: 8px;
z-index: 1;
}

View file

@ -1,9 +1,11 @@
import React from 'react';
import { JSONTree, NoContent, Button, Tabs } from 'UI';
import { JSONTree, NoContent, Button, Tabs, Icon } from 'UI';
import cn from 'classnames';
import stl from './fetchDetails.module.css';
import Headers from './components/Headers';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import { TYPES } from 'Types/session/resource';
import { formatBytes } from 'App/utils';
const HEADERS = 'HEADERS';
const REQUEST = 'REQUEST';
@ -129,43 +131,107 @@ export default class FetchDetailsModal extends React.PureComponent {
render() {
const {
resource: { method, url, duration },
resource,
fetchPresented,
nextClick,
prevClick,
first = false,
last = false,
} = this.props;
const { method, url, duration } = resource;
const { activeTab, tabs } = this.state;
const _duration = parseInt(duration)
console.log('_duration', _duration);
const _duration = parseInt(duration);
return (
<div className="bg-white p-5 h-screen overflow-y-auto" style={{ width: '500px' }}>
<h5 className="mb-2">{'URL'}</h5>
<div className={cn(stl.url, 'color-gray-darkest')}>{url}</div>
<div className="flex items-start mt-4">
{method && (
<div className="w-4/12">
<div className="font-medium mb-2">Method</div>
<div>{method}</div>
</div>
)}
{!!_duration && (
<div className="w-4/12">
<div className="font-medium mb-2">Duration</div>
<div>{_duration } ms</div>
</div>
)}
<h5 className="mb-2 text-2xl">Network Request</h5>
<div className="flex items-center py-1">
<div className="font-medium">Name</div>
<div className="rounded-lg bg-active-blue px-2 py-1 ml-2 whitespace-nowrap overflow-hidden text-clip">
{resource.name}
</div>
</div>
<div className="mt-6">
<div>
<Tabs tabs={tabs} active={activeTab} onClick={this.onTabClick} border={true} />
<div style={{ height: 'calc(100vh - 314px)', overflowY: 'auto' }}>
{this.renderActiveTab(activeTab)}
<div className="flex items-center py-1">
<div className="font-medium">Type</div>
<div className="rounded bg-active-blue px-2 py-1 ml-2 whitespace-nowrap overflow-hidden text-clip">
{resource.type}
</div>
</div>
{!!resource.decodedBodySize && (
<div className="flex items-center py-1">
<div className="font-medium">Size</div>
<div className="rounded bg-active-blue px-2 py-1 ml-2 whitespace-nowrap overflow-hidden text-clip">
{formatBytes(resource.decodedBodySize)}
</div>
</div>
)}
{method && (
<div className="flex items-center py-1">
<div className="font-medium">Request Method</div>
<div className="rounded bg-active-blue px-2 py-1 ml-2 whitespace-nowrap overflow-hidden text-clip">
{resource.method}
</div>
</div>
)}
{resource.status && (
<div className="flex items-center py-1">
<div className="font-medium">Status</div>
<div className="rounded bg-active-blue px-2 py-1 ml-2 whitespace-nowrap overflow-hidden text-clip flex items-center">
{resource.status === '200' && (
<div className="w-4 h-4 bg-green rounded-full mr-2"></div>
)}
{resource.status}
</div>
</div>
)}
{!!_duration && (
<div className="flex items-center py-1">
<div className="font-medium">Time</div>
<div className="rounded bg-active-blue px-2 py-1 ml-2 whitespace-nowrap overflow-hidden text-clip">
{_duration} ms
</div>
</div>
)}
{resource.type === TYPES.XHR && !fetchPresented && (
<div className="bg-active-blue rounded p-3 mt-4">
<div className="mb-2 flex items-center">
<Icon name="lightbulb" size="18" />
<span className="ml-2 font-medium">Get more out of network requests</span>
</div>
<ul className="list-disc ml-5">
<li>
Integrate{' '}
<a href="" className="link">
Fetch plugin
</a>{' '}
to capture fetch payloads.
</li>
<li>
Find a detailed{' '}
<a href="" className="link">
video tutorial
</a>{' '}
to understand practical example of how to use fetch plugin.
</li>
</ul>
</div>
)}
<div className="mt-6">
{resource.type === TYPES.XHR && fetchPresented && (
<div>
<Tabs tabs={tabs} active={activeTab} onClick={this.onTabClick} border={true} />
<div style={{ height: 'calc(100vh - 314px)', overflowY: 'auto' }}>
{this.renderActiveTab(activeTab)}
</div>
</div>
)}
{/* <div className="flex justify-between absolute bottom-0 left-0 right-0 p-3 border-t bg-white">
<Button variant="outline" onClick={prevClick} disabled={first}>

View file

@ -28,7 +28,7 @@ function FilterSource(props: Props) {
case FilterType.NUMBER:
return (
<div className="relative">
<input name="source" className={cn(stl.inputField, "rounded-l px-1 block")} value={value} onBlur={write} onChange={write} type="number" />
<input name="source" placeholder={filter.sourcePlaceholder} className={cn(stl.inputField, "rounded-l px-1 block")} value={value} onBlur={write} onChange={write} type="number" />
<div className="absolute right-0 top-0 bottom-0 bg-gray-lightest rounded-r px-1 border-l border-color-gray-light flex items-center" style={{ margin: '1px', minWidth: '24px'}}>{filter.sourceUnit}</div>
</div>
);

View file

@ -93,7 +93,8 @@ function FilterValue(props: Props) {
<FilterValueDropdown
// search={true}
value={value}
filter={filter}
placeholder={filter.placeholder}
// filter={filter}
options={filter.options}
onChange={({ value }) => onChange(null, { value }, valueIndex)}
/>
@ -106,6 +107,7 @@ function FilterValue(props: Props) {
// multiple={true}
value={value}
// filter={filter}
placeholder={filter.placeholder}
options={filter.options}
onChange={({ value }) => onChange(null, value, valueIndex)}
onAddValue={onAddValue}
@ -164,7 +166,7 @@ function FilterValue(props: Props) {
endpoint="/events/search"
params={getParms(filter.key)}
headerText={''}
// placeholder={''}
placeholder={filter.placeholder}
onSelect={(e, item) => onChange(e, item, valueIndex)}
icon={filter.icon}
/>

View file

@ -71,6 +71,7 @@ const dropdownStyles = {
interface Props {
// filter: any; // event/filter
// options: any[];
placeholder?: string
value: string;
onChange: (value: any) => void;
className?: string;
@ -84,7 +85,7 @@ interface Props {
isMultilple?: boolean;
}
function FilterValueDropdown(props: Props) {
const { isMultilple = true, search = false, options, onChange, value, className = '', showCloseButton = true, showOrButton = true } = props;
const { placeholder = 'Select', isMultilple = true, search = false, options, onChange, value, className = '', showCloseButton = true, showOrButton = true } = props;
// const options = []
return (
@ -97,7 +98,7 @@ function FilterValueDropdown(props: Props) {
name="issue_type"
defaultValue={ value }
onChange={ (value: any) => onChange(value.value) }
placeholder="Select"
placeholder={placeholder}
styles={dropdownStyles}
/>
<div

View file

@ -26,16 +26,16 @@ function GraphQLDetailsModal(props: Props) {
return (
<div className="p-5 bg-white h-screen overflow-y-auto" style={{ width: '500px' }}>
<h5 className="mb-2">{'Operation Name'}</h5>
<h5 className="mb-2 font-medium">{'Operation Name'}</h5>
<div className={dataClass}>{operationName}</div>
<div className="flex items-center gap-4 mt-4">
<div className="w-6/12">
<div className="mb-2">Operation Kind</div>
<div className="mb-2 font-medium">Operation Kind</div>
<div className={dataClass}>{operationKind}</div>
</div>
<div className="w-6/12">
<div className="mb-2">Duration</div>
<div className="mb-2 font-medium">Duration</div>
<div className={dataClass}>{duration ? parseInt(duration) : '???'} ms</div>
</div>
</div>
@ -43,7 +43,7 @@ function GraphQLDetailsModal(props: Props) {
<div style={{ height: 'calc(100vh - 314px)', overflowY: 'auto' }}>
<div>
<div className="flex justify-between items-start mt-6 mb-2">
<h5 className="mt-1 mr-1">{'Variables'}</h5>
<h5 className="mt-1 mr-1 font-medium">{'Variables'}</h5>
</div>
<div className={dataClass}>
{jsonVars === undefined ? variables : <JSONTree src={jsonVars} />}
@ -53,7 +53,7 @@ function GraphQLDetailsModal(props: Props) {
<div>
<div className="flex justify-between items-start mt-6 mb-2">
<h5 className="mt-1 mr-1">{'Response'}</h5>
<h5 className="mt-1 mr-1 font-medium">{'Response'}</h5>
</div>
<div className={dataClass}>
{jsonResponse === undefined ? response : <JSONTree src={jsonResponse} />}

View file

@ -5,34 +5,43 @@ import stl from './errorItem.module.css';
import { Duration } from 'luxon';
import { useModal } from 'App/components/Modal';
import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorDetailsModal';
import JumpButton from 'Shared/DevTools/JumpButton';
function ErrorItem({ error = {}, onJump, inactive, selected }) {
interface Props {
error: any;
onJump: any;
inactive?: Boolean;
selected?: Boolean;
}
function ErrorItem({ error = {}, onJump, inactive, selected }: Props) {
const { showModal } = useModal();
const onErrorClick = () => {
showModal(<ErrorDetailsModal errorId={error.errorId} />, { right: true });
}
};
return (
<div
className={cn(stl.wrapper, 'py-2 px-4 flex cursor-pointer', {
[stl.inactive]: inactive,
[stl.selected]: selected,
className={cn(stl.wrapper, 'py-2 px-4 flex cursor-pointer hover:bg-active-blue relative group', {
// [stl.inactive]: inactive,
// [stl.selected]: selected,
})}
onClick={onJump}
onClick={onErrorClick}
// onClick={onJump}
>
<div className={'self-start pr-4 color-red'}>
{/* <div className={'self-start pr-4 color-red'}>
{Duration.fromMillis(error.time).toFormat('mm:ss.SSS')}
</div>
<div className="mr-auto overflow-hidden">
</div> */}
<div className="overflow-hidden">
<div className="color-red mb-1 cursor-pointer code-font">
{error.name}
<span className="color-gray-darkest ml-2">{error.stack0InfoString}</span>
</div>
<div className="text-sm color-gray-medium">{error.message}</div>
<div className="text-xs code-font">{error.message}</div>
</div>
<div className="self-center">
{/* <div className="self-center">
<IconButton red onClick={onErrorClick} label="DETAILS" />
</div>
</div> */}
<JumpButton onClick={onJump} />
</div>
);
}

View file

@ -9,10 +9,12 @@ interface Props {
leadingButton?: React.ReactNode;
type?: string;
rows?: number;
height?: number;
width?: number;
[x: string]: any;
}
const Input = React.forwardRef((props: Props, ref: any) => {
const { className = '', leadingButton = '', wrapperClassName = '', icon = '', type = 'text', rows = 4, ...rest } = props;
const { height = 36, width = 0, className = '', leadingButton = '', wrapperClassName = '', icon = '', type = 'text', rows = 4, ...rest } = props;
return (
<div className={cn({ relative: icon || leadingButton }, wrapperClassName)}>
{icon && <Icon name={icon} className="absolute top-0 bottom-0 my-auto ml-4" size="14" />}
@ -29,7 +31,7 @@ const Input = React.forwardRef((props: Props, ref: any) => {
<input
ref={ref}
type={type}
style={{ height: '36px' }}
style={{ height: `${height}px`, width: width? `${width}px` : '' }}
className={cn('p-2 border border-gray-light bg-white w-full rounded', className, { 'pl-10': icon })}
{...rest}
/>

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-arrow-bar-left" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M12.5 15a.5.5 0 0 1-.5-.5v-13a.5.5 0 0 1 1 0v13a.5.5 0 0 1-.5.5zM10 8a.5.5 0 0 1-.5.5H3.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L3.707 7.5H9.5a.5.5 0 0 1 .5.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 352 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-bell-fill" viewBox="0 0 16 16">
<path d="M8 16a2 2 0 0 0 2-2H6a2 2 0 0 0 2 2zm.995-14.901a1 1 0 1 0-1.99 0A5.002 5.002 0 0 0 3 6c0 1.098-.5 6-2 7h14c-1.5-1-2-5.902-2-7 0-2.42-1.72-4.44-4.005-4.901z"/>
</svg>

After

Width:  |  Height:  |  Size: 282 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-bell-slash" viewBox="0 0 16 16">
<path d="M5.164 14H15c-.299-.199-.557-.553-.78-1-.9-1.8-1.22-5.12-1.22-6 0-.264-.02-.523-.06-.776l-.938.938c.02.708.157 2.154.457 3.58.161.767.377 1.566.663 2.258H6.164l-1 1zm5.581-9.91a3.986 3.986 0 0 0-1.948-1.01L8 2.917l-.797.161A4.002 4.002 0 0 0 4 7c0 .628-.134 2.197-.459 3.742-.05.238-.105.479-.166.718l-1.653 1.653c.02-.037.04-.074.059-.113C2.679 11.2 3 7.88 3 7c0-2.42 1.72-4.44 4.005-4.901a1 1 0 1 1 1.99 0c.942.19 1.788.645 2.457 1.284l-.707.707zM10 15a2 2 0 1 1-4 0h4zm-9.375.625a.53.53 0 0 0 .75.75l14.75-14.75a.53.53 0 0 0-.75-.75L.625 15.625z"/>
</svg>

After

Width:  |  Height:  |  Size: 675 B

View file

@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-caret-down-fill" viewBox="0 0 16 16">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-caret-down-fill" viewBox="0 0 16 16">
<path d="M7.247 11.14 2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z"/>
</svg>

Before

Width:  |  Height:  |  Size: 222 B

After

Width:  |  Height:  |  Size: 242 B

View file

@ -1 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M288.662 352H31.338c-17.818 0-26.741-21.543-14.142-34.142l128.662-128.662c7.81-7.81 20.474-7.81 28.284 0l128.662 128.662c12.6 12.599 3.676 34.142-14.142 34.142z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-caret-up-fill" viewBox="0 0 16 16">
<path d="m7.247 4.86-4.796 5.481c-.566.647-.106 1.659.753 1.659h9.592a1 1 0 0 0 .753-1.659l-4.796-5.48a1 1 0 0 0-1.506 0z"/>
</svg>

Before

Width:  |  Height:  |  Size: 240 B

After

Width:  |  Height:  |  Size: 242 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-door-closed" viewBox="0 0 16 16">
<path d="M3 2a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v13h1.5a.5.5 0 0 1 0 1h-13a.5.5 0 0 1 0-1H3V2zm1 13h8V2H4v13z"/>
<path d="M9 9a1 1 0 1 0 2 0 1 1 0 0 0-2 0z"/>
</svg>

After

Width:  |  Height:  |  Size: 271 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-folder-plus" viewBox="0 0 16 16">
<path d="m.5 3 .04.87a1.99 1.99 0 0 0-.342 1.311l.637 7A2 2 0 0 0 2.826 14H9v-1H2.826a1 1 0 0 1-.995-.91l-.637-7A1 1 0 0 1 2.19 4h11.62a1 1 0 0 1 .996 1.09L14.54 8h1.005l.256-2.819A2 2 0 0 0 13.81 3H9.828a2 2 0 0 1-1.414-.586l-.828-.828A2 2 0 0 0 6.172 1H2.5a2 2 0 0 0-2 2zm5.672-1a1 1 0 0 1 .707.293L7.586 3H2.19c-.24 0-.47.042-.683.12L1.5 2.98a1 1 0 0 1 1-.98h3.672z"/>
<path d="M13.5 10a.5.5 0 0 1 .5.5V12h1.5a.5.5 0 1 1 0 1H14v1.5a.5.5 0 1 1-1 0V13h-1.5a.5.5 0 0 1 0-1H13v-1.5a.5.5 0 0 1 .5-.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 619 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-folder2" viewBox="0 0 16 16">
<path d="M1 3.5A1.5 1.5 0 0 1 2.5 2h2.764c.958 0 1.76.56 2.311 1.184C7.985 3.648 8.48 4 9 4h4.5A1.5 1.5 0 0 1 15 5.5v7a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 1 12.5v-9zM2.5 3a.5.5 0 0 0-.5.5V6h12v-.5a.5.5 0 0 0-.5-.5H9c-.964 0-1.71-.629-2.174-1.154C6.374 3.334 5.82 3 5.264 3H2.5zM14 7H2v5.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V7z"/>
</svg>

After

Width:  |  Height:  |  Size: 446 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-gear-fill" viewBox="0 0 16 16">
<path d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"/>
</svg>

After

Width:  |  Height:  |  Size: 796 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-puzzle" viewBox="0 0 16 16">
<path d="M3.112 3.645A1.5 1.5 0 0 1 4.605 2H7a.5.5 0 0 1 .5.5v.382c0 .696-.497 1.182-.872 1.469a.459.459 0 0 0-.115.118.113.113 0 0 0-.012.025L6.5 4.5v.003l.003.01c.004.01.014.028.036.053a.86.86 0 0 0 .27.194C7.09 4.9 7.51 5 8 5c.492 0 .912-.1 1.19-.24a.86.86 0 0 0 .271-.194.213.213 0 0 0 .039-.063v-.009a.112.112 0 0 0-.012-.025.459.459 0 0 0-.115-.118c-.375-.287-.872-.773-.872-1.469V2.5A.5.5 0 0 1 9 2h2.395a1.5 1.5 0 0 1 1.493 1.645L12.645 6.5h.237c.195 0 .42-.147.675-.48.21-.274.528-.52.943-.52.568 0 .947.447 1.154.862C15.877 6.807 16 7.387 16 8s-.123 1.193-.346 1.638c-.207.415-.586.862-1.154.862-.415 0-.733-.246-.943-.52-.255-.333-.48-.48-.675-.48h-.237l.243 2.855A1.5 1.5 0 0 1 11.395 14H9a.5.5 0 0 1-.5-.5v-.382c0-.696.497-1.182.872-1.469a.459.459 0 0 0 .115-.118.113.113 0 0 0 .012-.025L9.5 11.5v-.003a.214.214 0 0 0-.039-.064.859.859 0 0 0-.27-.193C8.91 11.1 8.49 11 8 11c-.491 0-.912.1-1.19.24a.859.859 0 0 0-.271.194.214.214 0 0 0-.039.063v.003l.001.006a.113.113 0 0 0 .012.025c.016.027.05.068.115.118.375.287.872.773.872 1.469v.382a.5.5 0 0 1-.5.5H4.605a1.5 1.5 0 0 1-1.493-1.645L3.356 9.5h-.238c-.195 0-.42.147-.675.48-.21.274-.528.52-.943.52-.568 0-.947-.447-1.154-.862C.123 9.193 0 8.613 0 8s.123-1.193.346-1.638C.553 5.947.932 5.5 1.5 5.5c.415 0 .733.246.943.52.255.333.48.48.675.48h.238l-.244-2.855zM4.605 3a.5.5 0 0 0-.498.55l.001.007.29 3.4A.5.5 0 0 1 3.9 7.5h-.782c-.696 0-1.182-.497-1.469-.872a.459.459 0 0 0-.118-.115.112.112 0 0 0-.025-.012L1.5 6.5h-.003a.213.213 0 0 0-.064.039.86.86 0 0 0-.193.27C1.1 7.09 1 7.51 1 8c0 .491.1.912.24 1.19.07.14.14.225.194.271a.213.213 0 0 0 .063.039H1.5l.006-.001a.112.112 0 0 0 .025-.012.459.459 0 0 0 .118-.115c.287-.375.773-.872 1.469-.872H3.9a.5.5 0 0 1 .498.542l-.29 3.408a.5.5 0 0 0 .497.55h1.878c-.048-.166-.195-.352-.463-.557-.274-.21-.52-.528-.52-.943 0-.568.447-.947.862-1.154C6.807 10.123 7.387 10 8 10s1.193.123 1.638.346c.415.207.862.586.862 1.154 0 .415-.246.733-.52.943-.268.205-.415.39-.463.557h1.878a.5.5 0 0 0 .498-.55l-.001-.007-.29-3.4A.5.5 0 0 1 12.1 8.5h.782c.696 0 1.182.497 1.469.872.05.065.091.099.118.115.013.008.021.01.025.012a.02.02 0 0 0 .006.001h.003a.214.214 0 0 0 .064-.039.86.86 0 0 0 .193-.27c.14-.28.24-.7.24-1.191 0-.492-.1-.912-.24-1.19a.86.86 0 0 0-.194-.271.215.215 0 0 0-.063-.039H14.5l-.006.001a.113.113 0 0 0-.025.012.459.459 0 0 0-.118.115c-.287.375-.773.872-1.469.872H12.1a.5.5 0 0 1-.498-.543l.29-3.407a.5.5 0 0 0-.497-.55H9.517c.048.166.195.352.463.557.274.21.52.528.52.943 0 .568-.447.947-.862 1.154C9.193 5.877 8.613 6 8 6s-1.193-.123-1.638-.346C5.947 5.447 5.5 5.068 5.5 4.5c0-.415.246-.733.52-.943.268-.205.415-.39.463-.557H4.605z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -8,15 +8,15 @@ const containsFilters = [{ key: 'contains', label: 'contains', text: 'contains',
export const filters = [
{ key: FilterKey.CLICK, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Click', operator: 'on', operatorOptions: filterOptions.targetOperators, icon: 'filters/click', isEvent: true },
{ key: FilterKey.INPUT, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Input', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/input', isEvent: true },
{ key: FilterKey.LOCATION, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Path', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/location', isEvent: true },
{ key: FilterKey.CUSTOM, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Custom Events', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/custom', isEvent: true },
{ key: FilterKey.INPUT, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Text Input', placeholder: 'Enter input label name', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/input', isEvent: true },
{ key: FilterKey.LOCATION, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Visited URL', placeholder: 'Enter path', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/location', isEvent: true },
{ key: FilterKey.CUSTOM, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Custom Events', placeholder: 'Enter event key', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/custom', isEvent: true },
// { key: FilterKey.REQUEST, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Fetch', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch', isEvent: true },
{ key: FilterKey.FETCH, type: FilterType.SUB_FILTERS, category: FilterCategory.JAVASCRIPT, operator: 'is', label: 'Network Request', filters: [
{ key: FilterKey.FETCH_URL, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'with URL', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
{ key: FilterKey.FETCH_STATUS_CODE, type: FilterType.NUMBER_MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'with status code', operator: '=', operatorOptions: filterOptions.customOperators, icon: 'filters/fetch' },
{ key: FilterKey.FETCH_METHOD, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.PERFORMANCE, label: 'with method', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch', options: filterOptions.methodOptions },
{ key: FilterKey.FETCH_DURATION, type: FilterType.NUMBER, category: FilterCategory.PERFORMANCE, label: 'with duration (ms)', operator: '=', operatorOptions: filterOptions.customOperators, icon: 'filters/fetch' },
{ key: FilterKey.FETCH_URL, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'with URL', placeholder: 'Enter path or URL', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
{ key: FilterKey.FETCH_STATUS_CODE, type: FilterType.NUMBER_MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'with status code', placeholder: 'Enter status code', operator: '=', operatorOptions: filterOptions.customOperators, icon: 'filters/fetch' },
{ key: FilterKey.FETCH_METHOD, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.PERFORMANCE, label: 'with method', operator: 'is', placeholder: 'Select method type', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch', options: filterOptions.methodOptions },
{ key: FilterKey.FETCH_DURATION, type: FilterType.NUMBER, category: FilterCategory.PERFORMANCE, label: 'with duration (ms)', placeholder: 'E.g. 12', operator: '=', operatorOptions: filterOptions.customOperators, icon: 'filters/fetch' },
{ key: FilterKey.FETCH_REQUEST_BODY, type: FilterType.STRING, category: FilterCategory.PERFORMANCE, label: 'with request body', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
{ key: FilterKey.FETCH_RESPONSE_BODY, type: FilterType.STRING, category: FilterCategory.PERFORMANCE, label: 'with response body', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
], icon: 'filters/fetch', isEvent: true },
@ -26,8 +26,8 @@ export const filters = [
{ key: FilterKey.GRAPHQL_REQUEST_BODY, type: FilterType.STRING, category: FilterCategory.PERFORMANCE, label: 'with request body', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
{ key: FilterKey.GRAPHQL_RESPONSE_BODY, type: FilterType.STRING, category: FilterCategory.PERFORMANCE, label: 'with response body', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
]},
{ key: FilterKey.STATEACTION, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'StateAction', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/state-action', isEvent: true },
{ key: FilterKey.ERROR, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Error', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/error', isEvent: true },
{ key: FilterKey.STATEACTION, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'State Action', placeholder: 'E.g. 12', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/state-action', isEvent: true },
{ key: FilterKey.ERROR, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Error Message', placeholder: 'E.g. Uncaught SyntaxError', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/error', isEvent: true },
// { key: FilterKey.METADATA, type: FilterType.MULTIPLE, category: FilterCategory.METADATA, label: 'Metadata', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/metadata', isEvent: true },
// FILTERS
@ -35,22 +35,22 @@ export const filters = [
{ key: FilterKey.USER_BROWSER, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User Browser', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/browser' },
{ key: FilterKey.USER_DEVICE, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User Device', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/device' },
{ key: FilterKey.PLATFORM, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.GEAR, label: 'Platform', operator: 'is', operatorOptions: filterOptions.baseOperators, icon: 'filters/platform', options: platformOptions },
{ key: FilterKey.REVID, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'Version ID', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'collection' },
{ key: FilterKey.REVID, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'Version ID', placeholder: 'E.g. v1.0.8', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'collection' },
{ key: FilterKey.REFERRER, type: FilterType.MULTIPLE, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'Referrer', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/arrow-return-right' },
{ key: FilterKey.DURATION, type: FilterType.DURATION, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'Duration', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is']), icon: 'filters/duration' },
{ key: FilterKey.USER_COUNTRY, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.USER, label: 'User Country', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/country', options: countryOptions },
// { key: FilterKey.CONSOLE, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Console', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/console' },
{ key: FilterKey.USERID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User Id', operator: 'is', operatorOptions: filterOptions.stringOperators.concat([{ label: 'is undefined', value: 'isUndefined'}]), icon: 'filters/userid' },
{ key: FilterKey.USERID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'Identifier (User ID, Name, Email, etc)', placeholder: 'E.g. Alex, or alex@domain.com, or EMP123', operator: 'is', operatorOptions: filterOptions.stringOperators.concat([{ label: 'is undefined', value: 'isUndefined'}]), icon: 'filters/userid' },
{ key: FilterKey.USERANONYMOUSID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User AnonymousId', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/userid' },
// PERFORMANCE
{ key: FilterKey.DOM_COMPLETE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'DOM Complete', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/dom-complete', isEvent: true, hasSource: true, sourceOperator: '>=', sourceUnit: 'ms', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
{ key: FilterKey.LARGEST_CONTENTFUL_PAINT_TIME, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Largest Contentful Paint', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/lcpt', isEvent: true, hasSource: true, sourceOperator: '>=', sourceUnit: 'ms', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
{ key: FilterKey.TTFB, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Time to First Byte', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/ttfb', isEvent: true, hasSource: true, sourceOperator: '>=', sourceUnit: 'ms', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
{ key: FilterKey.AVG_CPU_LOAD, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg CPU Load', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/cpu-load', isEvent: true, hasSource: true, sourceOperator: '>=', sourceUnit: '%', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
{ key: FilterKey.AVG_MEMORY_USAGE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg Memory Usage', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/memory-load', isEvent: true, hasSource: true, sourceOperator: '>=', sourceUnit: 'mb', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
{ key: FilterKey.FETCH_FAILED, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Failed Request', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, icon: 'filters/fetch-failed', isEvent: true },
{ key: FilterKey.ISSUE, type: FilterType.ISSUE, category: FilterCategory.JAVASCRIPT, label: 'Issue', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/click', options: filterOptions.issueOptions },
{ key: FilterKey.DOM_COMPLETE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'DOM Complete', placeholder: 'Enter path', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/dom-complete', isEvent: true, hasSource: true, sourceOperator: '>=', sourcePlaceholder: 'E.g. 12', sourceUnit: 'ms', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
{ key: FilterKey.LARGEST_CONTENTFUL_PAINT_TIME, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Largest Contentful Paint', placeholder: 'Enter path', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/lcpt', isEvent: true, hasSource: true, sourceOperator: '>=', sourcePlaceholder: 'E.g. 12', sourceUnit: 'ms', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
{ key: FilterKey.TTFB, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Time to First Byte', placeholder: 'Enter path', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/ttfb', isEvent: true, hasSource: true, sourceOperator: '>=', sourceUnit: 'ms', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators, sourcePlaceholder: 'E.g. 12', },
{ key: FilterKey.AVG_CPU_LOAD, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg CPU Load', placeholder: 'Enter path', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/cpu-load', isEvent: true, hasSource: true, sourceOperator: '>=', sourcePlaceholder: 'E.g. 12', sourceUnit: '%', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
{ key: FilterKey.AVG_MEMORY_USAGE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg Memory Usage', placeholder: 'Enter path', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/memory-load', isEvent: true, hasSource: true, sourceOperator: '>=', sourcePlaceholder: 'E.g. 12', sourceUnit: 'mb', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
{ key: FilterKey.FETCH_FAILED, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Failed Request', placeholder: 'Enter path', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, icon: 'filters/fetch-failed', isEvent: true },
{ key: FilterKey.ISSUE, type: FilterType.ISSUE, category: FilterCategory.JAVASCRIPT, label: 'Issue', placeholder: 'Select an issue', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/click', options: filterOptions.issueOptions },
];
const mapFilters = (list) => {
@ -137,6 +137,7 @@ export default Record({
timestamp: 0,
key: '',
label: '',
placeholder: '',
icon: '',
type: '',
value: [""],
@ -155,6 +156,7 @@ export default Record({
source: [""],
sourceType: '',
sourceOperator: '=',
sourcePlaceholder: '',
sourceUnit: '',
sourceOperatorOptions: [],

View file

@ -24,6 +24,7 @@ export default Record({
value: '',
time: undefined,
index: undefined,
errorId: undefined,
}, {
methods: {
isRed() {