Merge pull request #769 from openreplay/dev-tools-merge
UI - header, preferences and dev tools improvements
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
1
frontend/app/components/Header/DefaultMenuView/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './DefaultMenuView'
|
||||
|
|
@ -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)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
1
frontend/app/components/Header/PreferencesView/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './PreferencesView';
|
||||
66
frontend/app/components/Header/SettingsMenu/SettingsMenu.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1
frontend/app/components/Header/SettingsMenu/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SettingsMenu';
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
57
frontend/app/components/Header/UserMenu/UserMenu.tsx
Normal 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;
|
||||
1
frontend/app/components/Header/UserMenu/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './UserMenu';
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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; */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ConsoleRow';
|
||||
|
|
@ -15,6 +15,9 @@
|
|||
display: flex;
|
||||
align-items: flex-start;
|
||||
border-bottom: solid thin $gray-light-shade;
|
||||
&:hover {
|
||||
background-coor: $active-blue !important;
|
||||
}
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -3,4 +3,5 @@
|
|||
padding: 2px 5px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #ccc;
|
||||
color: $gray-dark !important;
|
||||
}
|
||||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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,22 @@ 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,
|
||||
liveTimeTravel: state.liveTimeTravel,
|
||||
}))
|
||||
@connect(
|
||||
|
|
@ -162,7 +141,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 +157,6 @@ 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.liveTimeTravel !== this.props.liveTimeTravel ||
|
||||
nextProps.skipInterval !== this.props.skipInterval
|
||||
)
|
||||
|
|
@ -284,24 +261,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 +345,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 +371,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 +387,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 +402,6 @@ export default class Controls extends React.Component {
|
|||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
containerClassName="mx-2"
|
||||
count={stackCount}
|
||||
hasErrors={stackRedCount > 0}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -473,7 +410,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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ export default class Profiler extends React.PureComponent {
|
|||
icon="search"
|
||||
name="filter"
|
||||
onChange={ this.onFilterChange }
|
||||
height={28}
|
||||
/>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
.wrapper {
|
||||
background: $white;
|
||||
/* padding-right: 10px; */
|
||||
/* border: solid thin $gray-light; */
|
||||
height: 300px;
|
||||
|
||||
border-top: thin dashed #cccccc;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.content {
|
||||
height: 86%;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
|
||||
.header {
|
||||
padding: 0 10px;
|
||||
height: 40px;
|
||||
border-bottom: 1px solid $gray-light;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
// import { NONE, CONSOLE, NETWORK, STACKEVENTS, REDUX_STATE, PROFILER, PERFORMANCE, GRAPHQL } from 'Duck/components/player';
|
||||
//
|
||||
//
|
||||
// export default {
|
||||
// [NONE]: {
|
||||
// Component: null,
|
||||
//
|
||||
// }
|
||||
// }
|
||||
|
|
@ -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);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ConsolePanel';
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ConsoleRow';
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './JumpButton';
|
||||
|
|
@ -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);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './NetworkPanel'
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ProfilerModal';
|
||||
|
|
@ -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);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ProfilerPanel';
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './StackEventModal';
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './StackEventRow';
|
||||
96
frontend/app/components/shared/DevTools/TimeTable/BarRow.tsx
Normal 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;
|
||||
382
frontend/app/components/shared/DevTools/TimeTable/TimeTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './TimeTable';
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
.navButtons {
|
||||
position: absolute;
|
||||
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
padding: 4px;
|
||||
|
||||
right: 24px;
|
||||
top: 8px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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} />}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
3
frontend/app/svg/icons/arrow-bar-left.svg
Normal 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 |
3
frontend/app/svg/icons/bell-fill.svg
Normal 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 |
3
frontend/app/svg/icons/bell-slash.svg
Normal 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 |
|
|
@ -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 |
|
|
@ -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 |
4
frontend/app/svg/icons/door-closed.svg
Normal 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 |
4
frontend/app/svg/icons/folder-plus.svg
Normal 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 |
3
frontend/app/svg/icons/folder2.svg
Normal 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 |
3
frontend/app/svg/icons/gear-fill.svg
Normal 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 |
3
frontend/app/svg/icons/puzzle.svg
Normal 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 |
|
|
@ -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: [],
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export default Record({
|
|||
value: '',
|
||||
time: undefined,
|
||||
index: undefined,
|
||||
errorId: undefined,
|
||||
}, {
|
||||
methods: {
|
||||
isRed() {
|
||||
|
|
|
|||