change(ui) - merge dev-tools and resovle conflicts
|
|
@ -2,27 +2,27 @@ import logger from 'App/logger';
|
||||||
import APIClient from './api_client';
|
import APIClient from './api_client';
|
||||||
import { UPDATE, DELETE } from './duck/jwt';
|
import { UPDATE, DELETE } from './duck/jwt';
|
||||||
|
|
||||||
export default store => next => (action) => {
|
export default (store) => (next) => (action) => {
|
||||||
const { types, call, ...rest } = action;
|
const { types, call, ...rest } = action;
|
||||||
if (!call) {
|
if (!call) {
|
||||||
return next(action);
|
return next(action);
|
||||||
}
|
}
|
||||||
const [ REQUEST, SUCCESS, FAILURE ] = types;
|
const [REQUEST, SUCCESS, FAILURE] = types;
|
||||||
next({ ...rest, type: REQUEST });
|
next({ ...rest, type: REQUEST });
|
||||||
const client = new APIClient();
|
const client = new APIClient();
|
||||||
|
|
||||||
return call(client)
|
return call(client)
|
||||||
.then(async response => {
|
.then(async (response) => {
|
||||||
if (response.status === 403) {
|
if (response.status === 403) {
|
||||||
next({ type: DELETE });
|
next({ type: DELETE });
|
||||||
}
|
}
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const text = await response.text()
|
const text = await response.text();
|
||||||
return Promise.reject(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 }) => {
|
.then(({ jwt, errors, data }) => {
|
||||||
if (errors) {
|
if (errors) {
|
||||||
next({ type: FAILURE, errors, data });
|
next({ type: FAILURE, errors, data });
|
||||||
|
|
@ -34,14 +34,22 @@ export default store => next => (action) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
logger.error("Error during API request. ", e)
|
logger.error('Error during API request. ', e);
|
||||||
return next({ type: FAILURE, errors: JSON.parse(e).errors || [] });
|
return next({ type: FAILURE, errors: parseError(e) });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function parseError(e) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(e).errors || [];
|
||||||
|
} catch {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function jwtExpired(token) {
|
function jwtExpired(token) {
|
||||||
try {
|
try {
|
||||||
const base64Url = token.split('.')[ 1 ];
|
const base64Url = token.split('.')[1];
|
||||||
const base64 = base64Url.replace('-', '+').replace('_', '/');
|
const base64 = base64Url.replace('-', '+').replace('_', '/');
|
||||||
const tokenObj = JSON.parse(window.atob(base64));
|
const tokenObj = JSON.parse(window.atob(base64));
|
||||||
return tokenObj.exp * 1000 < Date.now(); // exp in Unix time (sec)
|
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 }>
|
<div className={ stl.counter } data-hidden={ count === 0 }>
|
||||||
{ count }
|
{ count }
|
||||||
</div>
|
</div>
|
||||||
<Icon name="bell" size="18" />
|
<Icon name="bell-fill" size="18" />
|
||||||
</div>
|
</div>
|
||||||
</Popup>
|
</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 { Icon, Popup } from 'UI';
|
||||||
import SiteDropdown from './SiteDropdown';
|
import SiteDropdown from './SiteDropdown';
|
||||||
import styles from './header.module.css';
|
import styles from './header.module.css';
|
||||||
import OnboardingExplore from './OnboardingExplore/OnboardingExplore'
|
import OnboardingExplore from './OnboardingExplore/OnboardingExplore';
|
||||||
import Announcements from '../Announcements';
|
import Announcements from '../Announcements';
|
||||||
import Notifications from '../Alerts/Notifications';
|
import Notifications from '../Alerts/Notifications';
|
||||||
import { init as initSite } from 'Duck/site';
|
import { init as initSite } from 'Duck/site';
|
||||||
|
import { getInitials } from 'App/utils';
|
||||||
|
|
||||||
import ErrorGenPanel from 'App/dev/components';
|
import ErrorGenPanel from 'App/dev/components';
|
||||||
import Alerts from '../Alerts/Alerts';
|
import Alerts from '../Alerts/Alerts';
|
||||||
|
|
@ -27,6 +28,10 @@ import AnimatedSVG, { ICONS } from '../shared/AnimatedSVG/AnimatedSVG';
|
||||||
import { fetchListActive as fetchMetadata } from 'Duck/customField';
|
import { fetchListActive as fetchMetadata } from 'Duck/customField';
|
||||||
import { useStore } from 'App/mstore';
|
import { useStore } from 'App/mstore';
|
||||||
import { useObserver } from 'mobx-react-lite';
|
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 DASHBOARD_PATH = dashboard();
|
||||||
const ALERTS_PATH = alerts();
|
const ALERTS_PATH = alerts();
|
||||||
|
|
@ -37,20 +42,25 @@ const CLIENT_PATH = client(CLIENT_DEFAULT_TAB);
|
||||||
|
|
||||||
const Header = (props) => {
|
const Header = (props) => {
|
||||||
const {
|
const {
|
||||||
sites, location, account,
|
sites,
|
||||||
onLogoutClick, siteId,
|
location,
|
||||||
boardingCompletion = 100, showAlerts = false,
|
account,
|
||||||
|
onLogoutClick,
|
||||||
|
siteId,
|
||||||
|
boardingCompletion = 100,
|
||||||
|
showAlerts = false,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const name = account.get('name').split(" ")[0];
|
const name = account.get('name');
|
||||||
const [hideDiscover, setHideDiscover] = useState(false)
|
const [hideDiscover, setHideDiscover] = useState(false);
|
||||||
const { userStore, notificationStore } = useStore();
|
const { userStore, notificationStore } = useStore();
|
||||||
const initialDataFetched = useObserver(() => userStore.initialDataFetched);
|
const initialDataFetched = useObserver(() => userStore.initialDataFetched);
|
||||||
let activeSite = null;
|
let activeSite = null;
|
||||||
|
const isPreferences = window.location.pathname.includes('/client/');
|
||||||
|
|
||||||
const onAccountClick = () => {
|
const onAccountClick = () => {
|
||||||
props.history.push(CLIENT_PATH);
|
props.history.push(CLIENT_PATH);
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!account.id || initialDataFetched) return;
|
if (!account.id || initialDataFetched) return;
|
||||||
|
|
@ -67,92 +77,62 @@ const Header = (props) => {
|
||||||
}, [account]);
|
}, [account]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
activeSite = sites.find(s => s.id == siteId);
|
activeSite = sites.find((s) => s.id == siteId);
|
||||||
props.initSite(activeSite);
|
props.initSite(activeSite);
|
||||||
}, [siteId])
|
}, [siteId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ cn(styles.header) } style={{ height: '50px'}}>
|
<div
|
||||||
<NavLink to={ withSiteId(SESSIONS_PATH, siteId) }>
|
className={cn(styles.header, 'fixed w-full bg-white flex justify-between')}
|
||||||
<div className="relative select-none">
|
style={{ height: '50px' }}
|
||||||
<div className="px-4 py-2">
|
>
|
||||||
<AnimatedSVG name={ICONS.LOGO_SMALL} size="30" />
|
{!isPreferences && <DefaultMenuView siteId={siteId} />}
|
||||||
</div>
|
{isPreferences && <PreferencesView />}
|
||||||
<div className="absolute bottom-0" style={{ fontSize: '7px', right: '5px' }}>v{window.env.VERSION}</div>
|
<div className={styles.right}>
|
||||||
</div>
|
{boardingCompletion < 100 && !hideDiscover && (
|
||||||
</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) && (
|
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<OnboardingExplore onComplete={() => setHideDiscover(true)} />
|
<OnboardingExplore onComplete={() => setHideDiscover(true)} />
|
||||||
<div className={ styles.divider } />
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Notifications />
|
<Notifications />
|
||||||
<div className={ styles.divider } />
|
<Popup content={`Preferences`} disabled>
|
||||||
<Popup content={ `Preferences` } >
|
<div className="group relative">
|
||||||
<NavLink to={ CLIENT_PATH } className={ styles.headerIcon }><Icon name="cog" size="20" /></NavLink>
|
<NavLink to={CLIENT_PATH} className={styles.headerIcon}>
|
||||||
|
<Icon name="gear-fill" size="20" />
|
||||||
|
</NavLink>
|
||||||
|
|
||||||
|
<SettingsMenu className="invisible group-hover:visible" account={account} />
|
||||||
|
</div>
|
||||||
</Popup>
|
</Popup>
|
||||||
|
|
||||||
<div className={ styles.divider } />
|
<div className={cn(styles.userDetails, 'group cursor-pointer')}>
|
||||||
<div className={ styles.userDetails }>
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="mr-5">{ name }</div>
|
<div className="w-10 h-10 bg-tealx rounded-full flex items-center justify-center color-white">
|
||||||
<Icon color="gray-medium" name="ellipsis-v" size="24" />
|
{getInitials(name)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul>
|
<UserMenu className="invisible group-hover:visible" />
|
||||||
<li><button onClick={ onAccountClick }>{ 'Account' }</button></li>
|
|
||||||
<li><button onClick={ onLogoutClick }>{ 'Logout' }</button></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{<ErrorGenPanel />}
|
||||||
</div>
|
</div>
|
||||||
{ <ErrorGenPanel/> }
|
|
||||||
{showAlerts && <Alerts />}
|
{showAlerts && <Alerts />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withRouter(connect(
|
export default withRouter(
|
||||||
state => ({
|
connect(
|
||||||
account: state.getIn([ 'user', 'account' ]),
|
(state) => ({
|
||||||
siteId: state.getIn([ 'site', 'siteId' ]),
|
account: state.getIn(['user', 'account']),
|
||||||
sites: state.getIn([ 'site', 'list' ]),
|
siteId: state.getIn(['site', 'siteId']),
|
||||||
showAlerts: state.getIn([ 'dashboard', 'showAlerts' ]),
|
sites: state.getIn(['site', 'list']),
|
||||||
boardingCompletion: state.getIn([ 'dashboard', 'boardingCompletion' ])
|
showAlerts: state.getIn(['dashboard', 'showAlerts']),
|
||||||
}),
|
boardingCompletion: state.getIn(['dashboard', 'boardingCompletion']),
|
||||||
{ onLogoutClick: logout, initSite, fetchMetadata },
|
}),
|
||||||
)(Header));
|
{ onLogoutClick: logout, initSite, fetchMetadata }
|
||||||
|
)(Header)
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -24,13 +24,17 @@ function NewProjectButton(props: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<li onClick={onClick}>
|
||||||
className={cn('flex items-center justify-center py-3 cursor-pointer hover:bg-active-blue ', { disabled: !canAddProject })}
|
<Icon name="folder-plus" size="16" color="teal" />
|
||||||
onClick={onClick}
|
<span className="ml-3 color-teal">Add Project</span>
|
||||||
>
|
</li>
|
||||||
<Icon name="plus" size={12} className="mr-2" color="teal" />
|
// <div
|
||||||
<span className="color-teal">Add New Project</span>
|
// className={cn('flex items-center justify-center py-3 cursor-pointer hover:bg-active-blue ', { disabled: !canAddProject })}
|
||||||
</div>
|
// 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 { setSiteId } from 'Duck/site';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { hasSiteId, siteChangeAvaliable } from 'App/routes';
|
import { hasSiteId, siteChangeAvaliable } from 'App/routes';
|
||||||
import { STATUS_COLOR_MAP, GREEN } from 'Types/site';
|
|
||||||
import { Icon } from 'UI';
|
import { Icon } from 'UI';
|
||||||
import { pushNewSite } from 'Duck/user';
|
import { pushNewSite } from 'Duck/user';
|
||||||
import { init } from 'Duck/site';
|
import { init } from 'Duck/site';
|
||||||
|
|
@ -13,7 +12,6 @@ import { clearSearch } from 'Duck/search';
|
||||||
import { clearSearch as clearSearchLive } from 'Duck/liveSearch';
|
import { clearSearch as clearSearchLive } from 'Duck/liveSearch';
|
||||||
import { fetchListActive as fetchIntegrationVariables } from 'Duck/customField';
|
import { fetchListActive as fetchIntegrationVariables } from 'Duck/customField';
|
||||||
import { withStore } from 'App/mstore';
|
import { withStore } from 'App/mstore';
|
||||||
import AnimatedSVG, { ICONS } from '../shared/AnimatedSVG/AnimatedSVG';
|
|
||||||
import NewProjectButton from './NewProjectButton';
|
import NewProjectButton from './NewProjectButton';
|
||||||
|
|
||||||
@withStore
|
@withStore
|
||||||
|
|
@ -63,37 +61,27 @@ export default class SiteDropdown extends React.PureComponent {
|
||||||
account,
|
account,
|
||||||
location: { pathname },
|
location: { pathname },
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { showProductModal } = this.state;
|
|
||||||
const isAdmin = account.admin || account.superAdmin;
|
const isAdmin = account.admin || account.superAdmin;
|
||||||
const activeSite = sites.find((s) => s.id == siteId);
|
const activeSite = sites.find((s) => s.id == siteId);
|
||||||
const disabled = !siteChangeAvaliable(pathname);
|
const disabled = !siteChangeAvaliable(pathname);
|
||||||
const showCurrent = hasSiteId(pathname) || siteChangeAvaliable(pathname);
|
const showCurrent = hasSiteId(pathname) || siteChangeAvaliable(pathname);
|
||||||
// const canAddSites = isAdmin && account.limits.projects && account.limits.projects.remaining !== 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<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>
|
<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" />
|
<Icon className={styles.drodownIcon} color="gray-light" name="chevron-down" size="16" />
|
||||||
<div className={styles.menu}>
|
<div className={styles.menu}>
|
||||||
<ul data-can-disable={disabled}>
|
<ul data-can-disable={disabled}>
|
||||||
{!showCurrent && <li>{'Project selection is not applicable.'}</li>}
|
{isAdmin && (
|
||||||
|
<NewProjectButton onClick={this.newSite} isAdmin={isAdmin} />
|
||||||
|
)}
|
||||||
{sites.map((site) => (
|
{sites.map((site) => (
|
||||||
<li key={site.id} onClick={() => this.switchSite(site.id)}>
|
<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] }} />
|
<Icon name="folder2" size="16" />
|
||||||
{site.host}
|
<span className="ml-3">{site.host}</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<NewProjectButton onClick={this.newSite} isAdmin={isAdmin} />
|
|
||||||
</div>
|
</div>
|
||||||
</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;
|
$height: 50px;
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
position: fixed;
|
/* position: fixed; */
|
||||||
width: 100%;
|
/* width: 100%; */
|
||||||
display: flex;
|
/* display: flex; */
|
||||||
justify-content: space-between;
|
/* justify-content: space-between; */
|
||||||
border-bottom: solid thin $gray-light;
|
border-bottom: solid thin $gray-light;
|
||||||
/* padding: 0 15px; */
|
/* padding: 0 15px; */
|
||||||
background: $white;
|
/* background: $white; */
|
||||||
z-index: $header;
|
z-index: $header;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,7 +45,7 @@ $height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right {
|
.right {
|
||||||
margin-left: auto;
|
/* margin-left: auto; */
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -72,11 +72,10 @@ $height: 50px;
|
||||||
.userDetails {
|
.userDetails {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0 5px 0 15px;
|
padding: 0 10px;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
min-width: 100px;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $gray-lightest;
|
background-color: $gray-lightest;
|
||||||
|
|
@ -102,7 +101,7 @@ $height: 50px;
|
||||||
border-top: 1px solid $gray-light;
|
border-top: 1px solid $gray-light;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
& a, & button {
|
/* & a, & button {
|
||||||
color: $gray-darkest;
|
color: $gray-darkest;
|
||||||
display: block;
|
display: block;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
@ -113,7 +112,7 @@ $height: 50px;
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $gray-lightest;
|
background-color: $gray-lightest;
|
||||||
}
|
}
|
||||||
}
|
} */
|
||||||
}
|
}
|
||||||
|
|
||||||
.userIcon {
|
.userIcon {
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,20 @@
|
||||||
.wrapper {
|
.wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-left: solid thin $gray-light !important;
|
/* border-left: solid thin $gray-light !important; */
|
||||||
padding: 10px 10px;
|
padding: 10px 10px;
|
||||||
min-width: 180px;
|
min-width: 180px;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
position: relative;
|
position: relative;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
border: solid thin transparent;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin: 10px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $gray-lightest;
|
background-color: $active-blue;
|
||||||
|
/* border: solid thin $active-blue-border; */
|
||||||
& .drodownIcon {
|
& .drodownIcon {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
|
@ -39,11 +44,12 @@
|
||||||
& .menu {
|
& .menu {
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50px;
|
top: 28px;
|
||||||
left: -1px;
|
left: -1px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
border-radius: 3px;
|
||||||
border: 1px solid $gray-light;
|
border: 1px solid $gray-light;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,9 +74,13 @@
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $gray-lightest;
|
background-color: $gray-lightest;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
color: $teal;
|
||||||
|
svg {
|
||||||
|
fill: $teal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&:first-child {
|
&: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}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={stl.navButtons}>
|
{/* <div className={stl.navButtons}>
|
||||||
{/* <label><input type={'checkbox'} checked={this.state.autoScroll} onChange={(e) => this.setState({ autoScroll: !this.state.autoScroll })} /> Autoscroll</label> */}
|
<label><input type={'checkbox'} checked={this.state.autoScroll} onChange={(e) => this.setState({ autoScroll: !this.state.autoScroll })} /> Autoscroll</label>
|
||||||
{navigation && (
|
{navigation && (
|
||||||
<>
|
<>
|
||||||
<IconButton size="small" icon="chevron-up" onClick={this.onPrevClick} />
|
<IconButton size="small" icon="chevron-up" onClick={this.onPrevClick} />
|
||||||
<IconButton size="small" icon="chevron-down" onClick={this.onNextClick} className="mt-5" />
|
<IconButton size="small" icon="chevron-down" onClick={this.onNextClick} className="mt-5" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
& >.infoPoint {
|
& >.infoPoint {
|
||||||
font-size: 12px;
|
font-size: 14px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
&:not(:last-child):after {
|
&:not(:last-child):after {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ import { LEVEL } from 'Types/session/log';
|
||||||
import Autoscroll from '../Autoscroll';
|
import Autoscroll from '../Autoscroll';
|
||||||
import BottomBlock from '../BottomBlock';
|
import BottomBlock from '../BottomBlock';
|
||||||
import stl from './console.module.css';
|
import stl from './console.module.css';
|
||||||
import { Duration } from 'luxon';
|
import ConsoleRow from './ConsoleRow';
|
||||||
|
// import { Duration } from 'luxon';
|
||||||
|
|
||||||
const ALL = 'ALL';
|
const ALL = 'ALL';
|
||||||
const INFO = 'INFO';
|
const INFO = 'INFO';
|
||||||
|
|
@ -83,44 +84,34 @@ export default class ConsoleContent extends React.PureComponent {
|
||||||
<Tabs tabs={TABS} active={activeTab} onClick={this.onTabClick} border={false} />
|
<Tabs tabs={TABS} active={activeTab} onClick={this.onTabClick} border={false} />
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
className="input-small"
|
className="input-small h-8"
|
||||||
placeholder="Filter by keyword"
|
placeholder="Filter by keyword"
|
||||||
icon="search"
|
icon="search"
|
||||||
iconPosition="left"
|
iconPosition="left"
|
||||||
name="filter"
|
name="filter"
|
||||||
|
height={28}
|
||||||
onChange={this.onFilterChange}
|
onChange={this.onFilterChange}
|
||||||
/>
|
/>
|
||||||
</BottomBlock.Header>
|
</BottomBlock.Header>
|
||||||
<BottomBlock.Content>
|
<BottomBlock.Content>
|
||||||
<NoContent title={
|
<NoContent
|
||||||
<div className="capitalize flex items-center mt-16">
|
title={
|
||||||
|
<div className="capitalize flex items-center mt-16">
|
||||||
<Icon name="info-circle" className="mr-2" size="18" />
|
<Icon name="info-circle" className="mr-2" size="18" />
|
||||||
No Data
|
No Data
|
||||||
</div>
|
</div>
|
||||||
} size="small" show={filtered.length === 0}>
|
}
|
||||||
|
size="small"
|
||||||
|
show={filtered.length === 0}
|
||||||
|
>
|
||||||
<Autoscroll autoScrollTo={Math.max(lastIndex, 0)}>
|
<Autoscroll autoScrollTo={Math.max(lastIndex, 0)}>
|
||||||
{filtered.map((l, index) => (
|
{filtered.map((l) => (
|
||||||
<div
|
<ConsoleRow
|
||||||
className={cn('flex py-2 px-4', {
|
log={l}
|
||||||
info: !l.isYellow() && !l.isRed(),
|
jump={jump}
|
||||||
warn: l.isYellow(),
|
iconProps={getIconProps(l.level)}
|
||||||
error: l.isRed(),
|
renderWithNL={renderWithNL}
|
||||||
[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>
|
|
||||||
))}
|
))}
|
||||||
</Autoscroll>
|
</Autoscroll>
|
||||||
</NoContent>
|
</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;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
border-bottom: solid thin $gray-light-shade;
|
border-bottom: solid thin $gray-light-shade;
|
||||||
|
&:hover {
|
||||||
|
background-coor: $active-blue !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.timestamp {
|
.timestamp {
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import BottomBlock from '../BottomBlock';
|
||||||
@connectPlayer((state) => ({
|
@connectPlayer((state) => ({
|
||||||
logs: state.logListNow,
|
logs: state.logListNow,
|
||||||
exceptions: state.exceptionsList,
|
exceptions: state.exceptionsList,
|
||||||
exceptionsNow: state.exceptionsListNow,
|
// exceptionsNow: state.exceptionsListNow,
|
||||||
}))
|
}))
|
||||||
@connect(
|
@connect(
|
||||||
(state) => ({
|
(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));
|
const filtered = exceptions.filter((e) => filterRE.test(e.name) || filterRE.test(e.message));
|
||||||
|
|
||||||
let lastIndex = -1;
|
// let lastIndex = -1;
|
||||||
filtered.forEach((item, index) => {
|
// filtered.forEach((item, index) => {
|
||||||
if (
|
// if (
|
||||||
this.props.exceptionsNow.length > 0 &&
|
// this.props.exceptionsNow.length > 0 &&
|
||||||
item.time <= this.props.exceptionsNow[this.props.exceptionsNow.length - 1].time
|
// item.time <= this.props.exceptionsNow[this.props.exceptionsNow.length - 1].time
|
||||||
) {
|
// ) {
|
||||||
lastIndex = index;
|
// lastIndex = index;
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -113,6 +113,7 @@ export default class Exceptions extends React.PureComponent {
|
||||||
iconPosition="left"
|
iconPosition="left"
|
||||||
name="filter"
|
name="filter"
|
||||||
onChange={this.onFilterChange}
|
onChange={this.onFilterChange}
|
||||||
|
height={28}
|
||||||
/>
|
/>
|
||||||
<QuestionMarkHint
|
<QuestionMarkHint
|
||||||
className={'mx-4'}
|
className={'mx-4'}
|
||||||
|
|
@ -133,13 +134,13 @@ export default class Exceptions extends React.PureComponent {
|
||||||
</BottomBlock.Header>
|
</BottomBlock.Header>
|
||||||
<BottomBlock.Content>
|
<BottomBlock.Content>
|
||||||
<NoContent size="small" show={filtered.length === 0} title="No recordings found">
|
<NoContent size="small" show={filtered.length === 0} title="No recordings found">
|
||||||
<Autoscroll autoScrollTo={Math.max(lastIndex, 0)}>
|
<Autoscroll>
|
||||||
{filtered.map((e, index) => (
|
{filtered.map((e, index) => (
|
||||||
<ErrorItem
|
<ErrorItem
|
||||||
onJump={() => jump(e.time)}
|
onJump={() => jump(e.time)}
|
||||||
error={e}
|
error={e}
|
||||||
key={e.key}
|
key={e.key}
|
||||||
selected={lastIndex === index}
|
// selected={lastIndex === index}
|
||||||
// inactive={index > lastIndex}
|
// inactive={index > lastIndex}
|
||||||
onErrorClick={(jsEvent) => {
|
onErrorClick={(jsEvent) => {
|
||||||
jsEvent.stopPropagation();
|
jsEvent.stopPropagation();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
// import { connectPlayer } from 'Player';
|
// 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 { getRE } from 'App/utils';
|
||||||
import { TYPES } from 'Types/session/resource';
|
import { TYPES } from 'Types/session/resource';
|
||||||
import { formatBytes } from 'App/utils';
|
import { formatBytes } from 'App/utils';
|
||||||
|
|
@ -48,22 +48,17 @@ export function renderType(r) {
|
||||||
|
|
||||||
export function renderName(r) {
|
export function renderName(r) {
|
||||||
return (
|
return (
|
||||||
<Popup
|
<Popup style={{ width: '100%' }} content={<div className={stl.popupNameContent}>{r.url}</div>}>
|
||||||
style={{ width: '100%' }}
|
<div className={stl.popupNameTrigger}>{r.name}</div>
|
||||||
content={<div className={stl.popupNameContent}>{r.url}</div>}
|
</Popup>
|
||||||
>
|
|
||||||
<div className={stl.popupNameTrigger}>{r.name}</div>
|
|
||||||
</Popup>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderStart(r) {
|
export function renderStart(r) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between items-center grow-0 w-full">
|
<div className="flex justify-between items-center grow-0 w-full">
|
||||||
<span>
|
<span>{Duration.fromMillis(r.time).toFormat('mm:ss.SSS')}</span>
|
||||||
{Duration.fromMillis(r.time).toFormat('mm:ss.SSS')}
|
{/* <Button
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
variant="text"
|
variant="text"
|
||||||
className="right-0 text-xs uppercase p-2 color-gray-500 hover:color-teal"
|
className="right-0 text-xs uppercase p-2 color-gray-500 hover:color-teal"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|
@ -72,9 +67,9 @@ export function renderStart(r) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Jump
|
Jump
|
||||||
</Button>
|
</Button> */}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderXHRText = () => (
|
const renderXHRText = () => (
|
||||||
|
|
@ -243,39 +238,45 @@ export default class NetworkContent extends React.PureComponent {
|
||||||
iconPosition="left"
|
iconPosition="left"
|
||||||
name="filter"
|
name="filter"
|
||||||
onChange={this.onFilterChange}
|
onChange={this.onFilterChange}
|
||||||
|
height={28}
|
||||||
/>
|
/>
|
||||||
</BottomBlock.Header>
|
</BottomBlock.Header>
|
||||||
<BottomBlock.Content>
|
<BottomBlock.Content>
|
||||||
<InfoLine>
|
<div className="flex items-center justify-between px-4">
|
||||||
<InfoLine.Point label={filtered.length} value=" requests" />
|
<div>
|
||||||
<InfoLine.Point
|
<Toggler checked={true} name="test" onChange={() => {}} label="4xx-5xx Only" />
|
||||||
label={formatBytes(transferredSize)}
|
</div>
|
||||||
value="transferred"
|
<InfoLine>
|
||||||
display={transferredSize > 0}
|
<InfoLine.Point label={filtered.length} value=" requests" />
|
||||||
/>
|
<InfoLine.Point
|
||||||
<InfoLine.Point
|
label={formatBytes(transferredSize)}
|
||||||
label={formatBytes(resourcesSize)}
|
value="transferred"
|
||||||
value="resources"
|
display={transferredSize > 0}
|
||||||
display={resourcesSize > 0}
|
/>
|
||||||
/>
|
<InfoLine.Point
|
||||||
<InfoLine.Point
|
label={formatBytes(resourcesSize)}
|
||||||
label={formatMs(domBuildingTime)}
|
value="resources"
|
||||||
value="DOM Building Time"
|
display={resourcesSize > 0}
|
||||||
display={domBuildingTime != null}
|
/>
|
||||||
/>
|
<InfoLine.Point
|
||||||
<InfoLine.Point
|
label={formatMs(domBuildingTime)}
|
||||||
label={domContentLoadedTime && formatMs(domContentLoadedTime.value)}
|
value="DOM Building Time"
|
||||||
value="DOMContentLoaded"
|
display={domBuildingTime != null}
|
||||||
display={domContentLoadedTime != null}
|
/>
|
||||||
dotColor={DOM_LOADED_TIME_COLOR}
|
<InfoLine.Point
|
||||||
/>
|
label={domContentLoadedTime && formatMs(domContentLoadedTime.value)}
|
||||||
<InfoLine.Point
|
value="DOMContentLoaded"
|
||||||
label={loadTime && formatMs(loadTime.value)}
|
display={domContentLoadedTime != null}
|
||||||
value="Load"
|
dotColor={DOM_LOADED_TIME_COLOR}
|
||||||
display={loadTime != null}
|
/>
|
||||||
dotColor={LOAD_TIME_COLOR}
|
<InfoLine.Point
|
||||||
/>
|
label={loadTime && formatMs(loadTime.value)}
|
||||||
</InfoLine>
|
value="Load"
|
||||||
|
display={loadTime != null}
|
||||||
|
dotColor={LOAD_TIME_COLOR}
|
||||||
|
/>
|
||||||
|
</InfoLine>
|
||||||
|
</div>
|
||||||
<NoContent
|
<NoContent
|
||||||
title={
|
title={
|
||||||
<div className="capitalize flex items-center mt-16">
|
<div className="capitalize flex items-center mt-16">
|
||||||
|
|
@ -296,11 +297,11 @@ export default class NetworkContent extends React.PureComponent {
|
||||||
activeIndex={lastIndex}
|
activeIndex={lastIndex}
|
||||||
>
|
>
|
||||||
{[
|
{[
|
||||||
{
|
// {
|
||||||
label: 'Start',
|
// label: 'Start',
|
||||||
width: 120,
|
// width: 120,
|
||||||
render: renderStart,
|
// render: renderStart,
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
label: 'Status',
|
label: 'Status',
|
||||||
dataKey: 'status',
|
dataKey: 'status',
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@ import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Controls as PlayerControls, connectPlayer } from 'Player';
|
import { Controls as PlayerControls, connectPlayer } from 'Player';
|
||||||
import {
|
import {
|
||||||
AreaChart,
|
AreaChart,
|
||||||
Area,
|
Area,
|
||||||
ComposedChart,
|
ComposedChart,
|
||||||
Line,
|
Line,
|
||||||
XAxis,
|
XAxis,
|
||||||
YAxis,
|
YAxis,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
ResponsiveContainer,
|
ResponsiveContainer,
|
||||||
ReferenceLine,
|
ReferenceLine,
|
||||||
CartesianGrid,
|
CartesianGrid,
|
||||||
|
|
@ -23,40 +23,35 @@ import stl from './performance.module.css';
|
||||||
import BottomBlock from '../BottomBlock';
|
import BottomBlock from '../BottomBlock';
|
||||||
import InfoLine from '../BottomBlock/InfoLine';
|
import InfoLine from '../BottomBlock/InfoLine';
|
||||||
|
|
||||||
|
|
||||||
const CPU_VISUAL_OFFSET = 10;
|
const CPU_VISUAL_OFFSET = 10;
|
||||||
|
|
||||||
|
|
||||||
const FPS_COLOR = '#C5E5E7';
|
const FPS_COLOR = '#C5E5E7';
|
||||||
const FPS_STROKE_COLOR = "#92C7CA";
|
const FPS_STROKE_COLOR = '#92C7CA';
|
||||||
const FPS_LOW_COLOR = "pink";
|
const FPS_LOW_COLOR = 'pink';
|
||||||
const FPS_VERY_LOW_COLOR = "red";
|
const FPS_VERY_LOW_COLOR = 'red';
|
||||||
const CPU_COLOR = "#A8D1DE";
|
const CPU_COLOR = '#A8D1DE';
|
||||||
const CPU_STROKE_COLOR = "#69A5B8";
|
const CPU_STROKE_COLOR = '#69A5B8';
|
||||||
const USED_HEAP_COLOR = '#A9ABDC';
|
const USED_HEAP_COLOR = '#A9ABDC';
|
||||||
const USED_HEAP_STROKE_COLOR = "#8588CF";
|
const USED_HEAP_STROKE_COLOR = '#8588CF';
|
||||||
const TOTAL_HEAP_STROKE_COLOR = '#4A4EB7';
|
const TOTAL_HEAP_STROKE_COLOR = '#4A4EB7';
|
||||||
const NODES_COUNT_COLOR = "#C6A9DC";
|
const NODES_COUNT_COLOR = '#C6A9DC';
|
||||||
const NODES_COUNT_STROKE_COLOR = "#7360AC";
|
const NODES_COUNT_STROKE_COLOR = '#7360AC';
|
||||||
const HIDDEN_SCREEN_COLOR = "#CCC";
|
const HIDDEN_SCREEN_COLOR = '#CCC';
|
||||||
|
|
||||||
|
const CURSOR_COLOR = '#394EFF';
|
||||||
const CURSOR_COLOR = "#394EFF";
|
|
||||||
|
|
||||||
const Gradient = ({ color, id }) => (
|
const Gradient = ({ color, id }) => (
|
||||||
<linearGradient id={ id } x1="-1" y1="0" x2="0" y2="1">
|
<linearGradient id={id} x1="-1" y1="0" x2="0" y2="1">
|
||||||
<stop offset="5%" stopColor={ color } stopOpacity={ 0.7 } />
|
<stop offset="5%" stopColor={color} stopOpacity={0.7} />
|
||||||
<stop offset="95%" stopColor={ color } stopOpacity={ 0.2 } />
|
<stop offset="95%" stopColor={color} stopOpacity={0.2} />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const TOTAL_HEAP = 'Allocated Heap';
|
||||||
const TOTAL_HEAP = "Allocated Heap";
|
const USED_HEAP = 'JS Heap';
|
||||||
const USED_HEAP = "JS Heap";
|
const FPS = 'Framerate';
|
||||||
const FPS = "Framerate";
|
const CPU = 'CPU Load';
|
||||||
const CPU = "CPU Load";
|
const NODES_COUNT = 'Nodes Сount';
|
||||||
const NODES_COUNT = "Nodes Сount";
|
|
||||||
|
|
||||||
|
|
||||||
const FPSTooltip = ({ active, payload }) => {
|
const FPSTooltip = ({ active, payload }) => {
|
||||||
if (!active || !payload || payload.length < 3) {
|
if (!active || !payload || payload.length < 3) {
|
||||||
|
|
@ -64,8 +59,8 @@ const FPSTooltip = ({ active, payload }) => {
|
||||||
}
|
}
|
||||||
if (payload[0].value === null) {
|
if (payload[0].value === null) {
|
||||||
return (
|
return (
|
||||||
<div className={ stl.tooltipWrapper } style={{ color: HIDDEN_SCREEN_COLOR }}>
|
<div className={stl.tooltipWrapper} style={{ color: HIDDEN_SCREEN_COLOR }}>
|
||||||
{"Page is not active. User switched the tab or hid the window."}
|
{'Page is not active. User switched the tab or hid the window.'}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -79,9 +74,9 @@ const FPSTooltip = ({ active, payload }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ stl.tooltipWrapper } style={ style }>
|
<div className={stl.tooltipWrapper} style={style}>
|
||||||
<span className="font-medium">{`${ FPS }: `}</span>
|
<span className="font-medium">{`${FPS}: `}</span>
|
||||||
{ Math.trunc(payload[0].value) }
|
{Math.trunc(payload[0].value)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -91,47 +86,47 @@ const CPUTooltip = ({ active, payload }) => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className={ stl.tooltipWrapper } >
|
<div className={stl.tooltipWrapper}>
|
||||||
<span className="font-medium">{`${ CPU }: `}</span>
|
<span className="font-medium">{`${CPU}: `}</span>
|
||||||
{ payload[0].value - CPU_VISUAL_OFFSET }
|
{payload[0].value - CPU_VISUAL_OFFSET}
|
||||||
{"%"}
|
{'%'}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const HeapTooltip = ({ active, payload}) => {
|
const HeapTooltip = ({ active, payload }) => {
|
||||||
if (!active || payload.length < 2) return null;
|
if (!active || payload.length < 2) return null;
|
||||||
return (
|
return (
|
||||||
<div className={ stl.tooltipWrapper } >
|
<div className={stl.tooltipWrapper}>
|
||||||
<p>
|
<p>
|
||||||
<span className="font-medium">{`${ TOTAL_HEAP }: `}</span>
|
<span className="font-medium">{`${TOTAL_HEAP}: `}</span>
|
||||||
{ formatBytes(payload[0].value)}
|
{formatBytes(payload[0].value)}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="font-medium">{`${ USED_HEAP }: `}</span>
|
<span className="font-medium">{`${USED_HEAP}: `}</span>
|
||||||
{ formatBytes(payload[1].value)}
|
{formatBytes(payload[1].value)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const NodesCountTooltip = ({ active, payload} ) => {
|
const NodesCountTooltip = ({ active, payload }) => {
|
||||||
if (!active || !payload || payload.length === 0) return null;
|
if (!active || !payload || payload.length === 0) return null;
|
||||||
return (
|
return (
|
||||||
<div className={ stl.tooltipWrapper } >
|
<div className={stl.tooltipWrapper}>
|
||||||
<p>
|
<p>
|
||||||
<span className="font-medium">{`${ NODES_COUNT }: `}</span>
|
<span className="font-medium">{`${NODES_COUNT}: `}</span>
|
||||||
{ payload[0].value }
|
{payload[0].value}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const TICKS_COUNT = 10;
|
const TICKS_COUNT = 10;
|
||||||
function generateTicks(data: Array<Timed>): Array<number> {
|
function generateTicks(data: Array<Timed>): Array<number> {
|
||||||
if (data.length === 0) return [];
|
if (data.length === 0) return [];
|
||||||
const minTime = data[0].time;
|
const minTime = data[0].time;
|
||||||
const maxTime = data[data.length-1].time;
|
const maxTime = data[data.length - 1].time;
|
||||||
|
|
||||||
const ticks = [];
|
const ticks = [];
|
||||||
const tickGap = (maxTime - minTime) / (TICKS_COUNT + 1);
|
const tickGap = (maxTime - minTime) / (TICKS_COUNT + 1);
|
||||||
|
|
@ -159,8 +154,9 @@ function addFpsMetadata(data) {
|
||||||
} else if (point.fps < LOW_FPS) {
|
} else if (point.fps < LOW_FPS) {
|
||||||
fpsLowMarker = LOW_FPS_MARKER_VALUE;
|
fpsLowMarker = LOW_FPS_MARKER_VALUE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (point.fps == null ||
|
if (
|
||||||
|
point.fps == null ||
|
||||||
(i > 0 && data[i - 1].fps == null) //||
|
(i > 0 && data[i - 1].fps == null) //||
|
||||||
//(i < data.length-1 && data[i + 1].fps == null)
|
//(i < data.length-1 && data[i + 1].fps == null)
|
||||||
) {
|
) {
|
||||||
|
|
@ -174,17 +170,17 @@ function addFpsMetadata(data) {
|
||||||
fpsLowMarker,
|
fpsLowMarker,
|
||||||
fpsVeryLowMarker,
|
fpsVeryLowMarker,
|
||||||
hiddenScreenMarker,
|
hiddenScreenMarker,
|
||||||
}
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@connect(state => ({
|
@connect((state) => ({
|
||||||
userDeviceHeapSize: state.getIn([ "sessions", "current", "userDeviceHeapSize" ]),
|
userDeviceHeapSize: state.getIn(['sessions', 'current', 'userDeviceHeapSize']),
|
||||||
userDeviceMemorySize: state.getIn([ "sessions", "current", "userDeviceMemorySize" ]),
|
userDeviceMemorySize: state.getIn(['sessions', 'current', 'userDeviceMemorySize']),
|
||||||
}))
|
}))
|
||||||
export default class Performance extends React.PureComponent {
|
export default class Performance extends React.PureComponent {
|
||||||
_timeTicks = generateTicks(this.props.performanceChartData)
|
_timeTicks = generateTicks(this.props.performanceChartData);
|
||||||
_data = addFpsMetadata(this.props.performanceChartData)
|
_data = addFpsMetadata(this.props.performanceChartData);
|
||||||
// state = {
|
// state = {
|
||||||
// totalHeap: false,
|
// totalHeap: false,
|
||||||
// usedHeap: true,
|
// usedHeap: true,
|
||||||
|
|
@ -197,7 +193,7 @@ export default class Performance extends React.PureComponent {
|
||||||
if (!!point) {
|
if (!!point) {
|
||||||
PlayerControls.jump(point.time);
|
PlayerControls.jump(point.time);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
onChartClick = (e) => {
|
onChartClick = (e) => {
|
||||||
if (e === null) return;
|
if (e === null) return;
|
||||||
|
|
@ -206,10 +202,10 @@ export default class Performance extends React.PureComponent {
|
||||||
if (!!point) {
|
if (!!point) {
|
||||||
PlayerControls.jump(point.time);
|
PlayerControls.jump(point.time);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
userDeviceHeapSize,
|
userDeviceHeapSize,
|
||||||
userDeviceMemorySize,
|
userDeviceMemorySize,
|
||||||
connType,
|
connType,
|
||||||
|
|
@ -218,19 +214,19 @@ export default class Performance extends React.PureComponent {
|
||||||
avaliability = {},
|
avaliability = {},
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { fps, cpu, heap, nodes } = avaliability;
|
const { fps, cpu, heap, nodes } = avaliability;
|
||||||
const avaliableCount = [ fps, cpu, heap, nodes ].reduce((c, av) => av ? c + 1 : c, 0);
|
const avaliableCount = [fps, cpu, heap, nodes].reduce((c, av) => (av ? c + 1 : c), 0);
|
||||||
const height = avaliableCount === 0 ? "0" : `${100 / avaliableCount}%`;
|
const height = avaliableCount === 0 ? '0' : `${100 / avaliableCount}%`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BottomBlock>
|
<BottomBlock>
|
||||||
<BottomBlock.Header>
|
<BottomBlock.Header>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center w-full">
|
||||||
<span className="font-semibold color-gray-medium mr-4">Performance</span>
|
<div className="font-semibold color-gray-medium mr-auto">Performance</div>
|
||||||
<InfoLine>
|
<InfoLine>
|
||||||
<InfoLine.Point
|
<InfoLine.Point
|
||||||
label="Device Heap Size"
|
label="Device Heap Size"
|
||||||
value={ formatBytes(userDeviceHeapSize) }
|
value={formatBytes(userDeviceHeapSize)}
|
||||||
display={ userDeviceHeapSize != null }
|
display={true}
|
||||||
/>
|
/>
|
||||||
{/* <InfoLine.Point */}
|
{/* <InfoLine.Point */}
|
||||||
{/* label="Device Memory Size" */}
|
{/* label="Device Memory Size" */}
|
||||||
|
|
@ -238,55 +234,52 @@ export default class Performance extends React.PureComponent {
|
||||||
{/* /> */}
|
{/* /> */}
|
||||||
<InfoLine.Point
|
<InfoLine.Point
|
||||||
label="Connection Type"
|
label="Connection Type"
|
||||||
value={ connType }
|
value={connType}
|
||||||
display={ connType != null && connType !== "unknown" }
|
display={connType != null && connType !== 'unknown'}
|
||||||
/>
|
/>
|
||||||
<InfoLine.Point
|
<InfoLine.Point
|
||||||
label="Connection Speed"
|
label="Connection Speed"
|
||||||
value={ connBandwidth >= 1000
|
value={
|
||||||
? `${ connBandwidth / 1000 } Mbps`
|
connBandwidth >= 1000 ? `${connBandwidth / 1000} Mbps` : `${connBandwidth} Kbps`
|
||||||
: `${ connBandwidth } Kbps`
|
|
||||||
}
|
}
|
||||||
display={ connBandwidth != null }
|
display={connBandwidth != null}
|
||||||
/>
|
/>
|
||||||
</InfoLine>
|
</InfoLine>
|
||||||
</div>
|
</div>
|
||||||
</BottomBlock.Header>
|
</BottomBlock.Header>
|
||||||
<BottomBlock.Content>
|
<BottomBlock.Content>
|
||||||
{ fps &&
|
{fps && (
|
||||||
<ResponsiveContainer height={ height }>
|
<ResponsiveContainer height={height}>
|
||||||
<AreaChart
|
<AreaChart
|
||||||
onClick={ this.onChartClick }
|
onClick={this.onChartClick}
|
||||||
data={this._data}
|
data={this._data}
|
||||||
syncId="s"
|
syncId="s"
|
||||||
margin={{
|
margin={{
|
||||||
top: 0, right: 0, left: 0, bottom: 0,
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
left: 0,
|
||||||
|
bottom: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<Gradient id="fpsGradient" color={ FPS_COLOR } />
|
<Gradient id="fpsGradient" color={FPS_COLOR} />
|
||||||
</defs>
|
</defs>
|
||||||
{/* <CartesianGrid strokeDasharray="1 1" stroke="#ddd" horizontal={ false } /> */}
|
{/* <CartesianGrid strokeDasharray="1 1" stroke="#ddd" horizontal={ false } /> */}
|
||||||
{/* <CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" /> */}
|
{/* <CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" /> */}
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="time"
|
dataKey="time"
|
||||||
type="number"
|
type="number"
|
||||||
mirror
|
mirror
|
||||||
orientation="top"
|
orientation="top"
|
||||||
tickLine={ false }
|
tickLine={false}
|
||||||
tickFormatter={ durationFromMsFormatted }
|
tickFormatter={durationFromMsFormatted}
|
||||||
tick={{ fontSize: "12px" }}
|
tick={{ fontSize: '12px', fill: '#333' }}
|
||||||
domain={[0, 'dataMax']}
|
domain={[0, 'dataMax']}
|
||||||
ticks={this._timeTicks}
|
ticks={this._timeTicks}
|
||||||
>
|
>
|
||||||
<Label value="FPS" position="insideTopRight" className="fill-gray-medium" />
|
<Label value="FPS" position="insideTopRight" className="fill-gray-darkest" />
|
||||||
</XAxis>
|
</XAxis>
|
||||||
<YAxis
|
<YAxis axisLine={false} tick={false} mirror domain={[0, 85]} />
|
||||||
axisLine={ false }
|
|
||||||
tick={ false }
|
|
||||||
mirror
|
|
||||||
domain={[0, 85]}
|
|
||||||
/>
|
|
||||||
{/* <YAxis */}
|
{/* <YAxis */}
|
||||||
{/* yAxisId="r" */}
|
{/* yAxisId="r" */}
|
||||||
{/* axisLine={ false } */}
|
{/* axisLine={ false } */}
|
||||||
|
|
@ -295,41 +288,41 @@ export default class Performance extends React.PureComponent {
|
||||||
{/* domain={[0, 120]} */}
|
{/* domain={[0, 120]} */}
|
||||||
{/* orientation="right" */}
|
{/* orientation="right" */}
|
||||||
{/* /> */}
|
{/* /> */}
|
||||||
<Area
|
<Area
|
||||||
dataKey="fps"
|
dataKey="fps"
|
||||||
type="stepBefore"
|
type="stepBefore"
|
||||||
stroke={FPS_STROKE_COLOR}
|
stroke={FPS_STROKE_COLOR}
|
||||||
fill="url(#fpsGradient)"
|
fill="url(#fpsGradient)"
|
||||||
dot={false}
|
dot={false}
|
||||||
activeDot={{
|
activeDot={{
|
||||||
onClick: this.onDotClick,
|
onClick: this.onDotClick,
|
||||||
style: { cursor: "pointer" },
|
style: { cursor: 'pointer' },
|
||||||
}}
|
}}
|
||||||
isAnimationActive={ false }
|
isAnimationActive={false}
|
||||||
/>
|
/>
|
||||||
<Area
|
<Area
|
||||||
dataKey="fpsLowMarker"
|
dataKey="fpsLowMarker"
|
||||||
type="stepBefore"
|
type="stepBefore"
|
||||||
stroke="none"
|
stroke="none"
|
||||||
fill={ FPS_LOW_COLOR }
|
fill={FPS_LOW_COLOR}
|
||||||
activeDot={false}
|
activeDot={false}
|
||||||
isAnimationActive={ false }
|
isAnimationActive={false}
|
||||||
/>
|
/>
|
||||||
<Area
|
<Area
|
||||||
dataKey="fpsVeryLowMarker"
|
dataKey="fpsVeryLowMarker"
|
||||||
type="stepBefore"
|
type="stepBefore"
|
||||||
stroke="none"
|
stroke="none"
|
||||||
fill={ FPS_VERY_LOW_COLOR }
|
fill={FPS_VERY_LOW_COLOR}
|
||||||
activeDot={false}
|
activeDot={false}
|
||||||
isAnimationActive={ false }
|
isAnimationActive={false}
|
||||||
/>
|
/>
|
||||||
<Area
|
<Area
|
||||||
dataKey="hiddenScreenMarker"
|
dataKey="hiddenScreenMarker"
|
||||||
type="stepBefore"
|
type="stepBefore"
|
||||||
stroke="none"
|
stroke="none"
|
||||||
fill={ HIDDEN_SCREEN_COLOR }
|
fill={HIDDEN_SCREEN_COLOR}
|
||||||
activeDot={false}
|
activeDot={false}
|
||||||
isAnimationActive={ false }
|
isAnimationActive={false}
|
||||||
/>
|
/>
|
||||||
{/* <Area */}
|
{/* <Area */}
|
||||||
{/* yAxisId="r" */}
|
{/* yAxisId="r" */}
|
||||||
|
|
@ -346,101 +339,95 @@ export default class Performance extends React.PureComponent {
|
||||||
{/* isAnimationActive={ false } */}
|
{/* isAnimationActive={ false } */}
|
||||||
{/* /> */}
|
{/* /> */}
|
||||||
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
|
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
|
||||||
<Tooltip
|
<Tooltip content={FPSTooltip} filterNull={false} />
|
||||||
content={FPSTooltip}
|
|
||||||
filterNull={ false }
|
|
||||||
/>
|
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
}
|
)}
|
||||||
{ cpu &&
|
{cpu && (
|
||||||
<ResponsiveContainer height={ height }>
|
<ResponsiveContainer height={height}>
|
||||||
<AreaChart
|
<AreaChart
|
||||||
onClick={ this.onChartClick }
|
onClick={this.onChartClick}
|
||||||
data={this._data}
|
data={this._data}
|
||||||
syncId="s"
|
syncId="s"
|
||||||
margin={{
|
margin={{
|
||||||
top: 0, right: 0, left: 0, bottom: 0,
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
left: 0,
|
||||||
|
bottom: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<Gradient id="cpuGradient" color={ CPU_COLOR } />
|
<Gradient id="cpuGradient" color={CPU_COLOR} />
|
||||||
</defs>
|
</defs>
|
||||||
{/* <CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" /> */}
|
{/* <CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" /> */}
|
||||||
<XAxis
|
<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
|
|
||||||
dataKey="time"
|
dataKey="time"
|
||||||
type="number"
|
type="number"
|
||||||
mirror
|
mirror
|
||||||
orientation="top"
|
orientation="top"
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
tickFormatter={()=> ""} // tick={false} + this._timeTicks to cartesian array
|
tickFormatter={() => ''}
|
||||||
domain={[0, 'dataMax']}
|
domain={[0, 'dataMax']}
|
||||||
ticks={this._timeTicks}
|
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>
|
</XAxis>
|
||||||
<YAxis
|
<YAxis
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
|
|
@ -448,19 +435,19 @@ export default class Performance extends React.PureComponent {
|
||||||
mirror
|
mirror
|
||||||
// Hack to keep only end tick
|
// Hack to keep only end tick
|
||||||
minTickGap={Number.MAX_SAFE_INTEGER}
|
minTickGap={Number.MAX_SAFE_INTEGER}
|
||||||
domain={[0, max => max*1.2]}
|
domain={[0, (max) => max * 1.2]}
|
||||||
/>
|
/>
|
||||||
<Line
|
<Line
|
||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey="totalHeap"
|
dataKey="totalHeap"
|
||||||
// fill="url(#totalHeapGradient)"
|
// fill="url(#totalHeapGradient)"
|
||||||
stroke={TOTAL_HEAP_STROKE_COLOR}
|
stroke={TOTAL_HEAP_STROKE_COLOR}
|
||||||
dot={false}
|
dot={false}
|
||||||
activeDot={{
|
activeDot={{
|
||||||
onClick: this.onDotClick,
|
onClick: this.onDotClick,
|
||||||
style: { cursor: "pointer" },
|
style: { cursor: 'pointer' },
|
||||||
}}
|
}}
|
||||||
isAnimationActive={ false }
|
isAnimationActive={false}
|
||||||
/>
|
/>
|
||||||
<Area
|
<Area
|
||||||
dataKey="usedHeap"
|
dataKey="usedHeap"
|
||||||
|
|
@ -468,81 +455,78 @@ export default class Performance extends React.PureComponent {
|
||||||
fill="url(#usedHeapGradient)"
|
fill="url(#usedHeapGradient)"
|
||||||
stroke={USED_HEAP_STROKE_COLOR}
|
stroke={USED_HEAP_STROKE_COLOR}
|
||||||
dot={false}
|
dot={false}
|
||||||
activeDot={{
|
activeDot={{
|
||||||
onClick: this.onDotClick,
|
onClick: this.onDotClick,
|
||||||
style: { cursor: "pointer" },
|
style: { cursor: 'pointer' },
|
||||||
}}
|
}}
|
||||||
isAnimationActive={ false }
|
isAnimationActive={false}
|
||||||
/>
|
/>
|
||||||
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
|
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
|
||||||
<Tooltip
|
<Tooltip content={HeapTooltip} filterNull={false} />
|
||||||
content={HeapTooltip}
|
|
||||||
filterNull={ false }
|
|
||||||
/>
|
|
||||||
</ComposedChart>
|
</ComposedChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
}
|
)}
|
||||||
{ nodes &&
|
{nodes && (
|
||||||
<ResponsiveContainer height={ height }>
|
<ResponsiveContainer height={height}>
|
||||||
<AreaChart
|
<AreaChart
|
||||||
onClick={ this.onChartClick }
|
onClick={this.onChartClick}
|
||||||
data={this._data}
|
data={this._data}
|
||||||
syncId="s"
|
syncId="s"
|
||||||
margin={{
|
margin={{
|
||||||
top: 0, right: 0, left: 0, bottom: 0,
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
left: 0,
|
||||||
|
bottom: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<Gradient id="nodesGradient" color={ NODES_COUNT_COLOR } />
|
<Gradient id="nodesGradient" color={NODES_COUNT_COLOR} />
|
||||||
</defs>
|
</defs>
|
||||||
{/* <CartesianGrid strokeDasharray="1 1" stroke="#ddd" horizontal={ false } /> */}
|
{/* <CartesianGrid strokeDasharray="1 1" stroke="#ddd" horizontal={ false } /> */}
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="time"
|
dataKey="time"
|
||||||
type="number"
|
type="number"
|
||||||
mirror
|
mirror
|
||||||
orientation="top"
|
orientation="top"
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
tickFormatter={()=> ""}
|
tickFormatter={() => ''}
|
||||||
domain={[0, 'dataMax']}
|
domain={[0, 'dataMax']}
|
||||||
ticks={this._timeTicks}
|
ticks={this._timeTicks}
|
||||||
>
|
>
|
||||||
<Label value="NODES" position="insideTopRight" className="fill-gray-medium" />
|
<Label value="NODES" position="insideTopRight" className="fill-gray-darkest" />
|
||||||
</XAxis>
|
</XAxis>
|
||||||
<YAxis
|
<YAxis
|
||||||
axisLine={ false }
|
axisLine={false}
|
||||||
tick={ false }
|
tick={false}
|
||||||
mirror
|
mirror
|
||||||
orientation="right"
|
orientation="right"
|
||||||
domain={[0, max => max*1.2]}
|
domain={[0, (max) => max * 1.2]}
|
||||||
/>
|
/>
|
||||||
<Area
|
<Area
|
||||||
dataKey="nodesCount"
|
dataKey="nodesCount"
|
||||||
type="monotone"
|
type="monotone"
|
||||||
stroke={NODES_COUNT_STROKE_COLOR}
|
stroke={NODES_COUNT_STROKE_COLOR}
|
||||||
// fill="none"
|
// fill="none"
|
||||||
fill="url(#nodesGradient)"
|
fill="url(#nodesGradient)"
|
||||||
dot={false}
|
dot={false}
|
||||||
activeDot={{
|
activeDot={{
|
||||||
onClick: this.onDotClick,
|
onClick: this.onDotClick,
|
||||||
style: { cursor: "pointer" },
|
style: { cursor: 'pointer' },
|
||||||
}}
|
}}
|
||||||
isAnimationActive={ false }
|
isAnimationActive={false}
|
||||||
/>
|
|
||||||
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
|
|
||||||
<Tooltip
|
|
||||||
content={NodesCountTooltip}
|
|
||||||
filterNull={ false }
|
|
||||||
/>
|
/>
|
||||||
|
<ReferenceLine x={performanceChartTime} stroke={CURSOR_COLOR} />
|
||||||
|
<Tooltip content={NodesCountTooltip} filterNull={false} />
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
}
|
)}
|
||||||
</BottomBlock.Content>
|
</BottomBlock.Content>
|
||||||
</BottomBlock>
|
</BottomBlock>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConnectedPerformance = connectPlayer(state => ({
|
export const ConnectedPerformance = connectPlayer((state) => ({
|
||||||
performanceChartTime: state.performanceChartTime,
|
performanceChartTime: state.performanceChartTime,
|
||||||
performanceChartData: state.performanceChartData,
|
performanceChartData: state.performanceChartData,
|
||||||
connType: state.connType,
|
connType: state.connType,
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,5 @@
|
||||||
padding: 2px 5px;
|
padding: 2px 5px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
|
color: $gray-dark !important;
|
||||||
}
|
}
|
||||||
|
|
@ -8,7 +8,7 @@ const ControlButton = ({
|
||||||
icon = '',
|
icon = '',
|
||||||
disabled = false,
|
disabled = false,
|
||||||
onClick,
|
onClick,
|
||||||
count = 0,
|
// count = 0,
|
||||||
hasErrors = false,
|
hasErrors = false,
|
||||||
active = false,
|
active = false,
|
||||||
size = 20,
|
size = 20,
|
||||||
|
|
@ -31,7 +31,7 @@ const ControlButton = ({
|
||||||
>
|
>
|
||||||
<div className={stl.labels}>
|
<div className={stl.labels}>
|
||||||
{hasErrors && <div className={stl.errorSymbol} />}
|
{hasErrors && <div className={stl.errorSymbol} />}
|
||||||
{count > 0 && <div className={stl.countLabel}>{count}</div>}
|
{/* {count > 0 && <div className={stl.countLabel}>{count}</div>} */}
|
||||||
</div>
|
</div>
|
||||||
{!noIcon && <Icon name={icon} size={size} color="gray-dark" />}
|
{!noIcon && <Icon name={icon} size={size} color="gray-dark" />}
|
||||||
{!noLabel && (
|
{!noLabel && (
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
selectStorageListNow,
|
selectStorageListNow,
|
||||||
} from 'Player/store';
|
} from 'Player/store';
|
||||||
import LiveTag from 'Shared/LiveTag';
|
import LiveTag from 'Shared/LiveTag';
|
||||||
import { toggleTimetravel, jumpToLive } from 'Player';
|
import { jumpToLive } from 'Player';
|
||||||
|
|
||||||
import { Icon } from 'UI';
|
import { Icon } from 'UI';
|
||||||
import { toggleInspectorMode } from 'Player';
|
import { toggleInspectorMode } from 'Player';
|
||||||
|
|
@ -25,8 +25,6 @@ import {
|
||||||
PROFILER,
|
PROFILER,
|
||||||
PERFORMANCE,
|
PERFORMANCE,
|
||||||
GRAPHQL,
|
GRAPHQL,
|
||||||
FETCH,
|
|
||||||
EXCEPTIONS,
|
|
||||||
INSPECTOR,
|
INSPECTOR,
|
||||||
} from 'Duck/components/player';
|
} from 'Duck/components/player';
|
||||||
import { AssistDuration } from './Time';
|
import { AssistDuration } from './Time';
|
||||||
|
|
@ -38,23 +36,6 @@ import styles from './controls.module.css';
|
||||||
import { Tooltip } from 'react-tippy';
|
import { Tooltip } from 'react-tippy';
|
||||||
import XRayButton from 'Shared/XRayButton';
|
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 = {
|
const SKIP_INTERVALS = {
|
||||||
2: 2e3,
|
2: 2e3,
|
||||||
5: 5e3,
|
5: 5e3,
|
||||||
|
|
@ -95,24 +76,23 @@ function getStorageName(type) {
|
||||||
disabled: state.cssLoading || state.messagesLoading || state.inspectorMode || state.markedTargets,
|
disabled: state.cssLoading || state.messagesLoading || state.inspectorMode || state.markedTargets,
|
||||||
inspectorMode: state.inspectorMode,
|
inspectorMode: state.inspectorMode,
|
||||||
fullscreenDisabled: state.messagesLoading,
|
fullscreenDisabled: state.messagesLoading,
|
||||||
logCount: state.logListNow.length,
|
// logCount: state.logList.length,
|
||||||
logRedCount: state.logRedCountNow,
|
logRedCount: state.logRedCount,
|
||||||
resourceRedCount: state.resourceRedCountNow,
|
resourceRedCount: state.resourceRedCount,
|
||||||
fetchRedCount: state.fetchRedCountNow,
|
fetchRedCount: state.fetchRedCount,
|
||||||
showStack: state.stackList.length > 0,
|
showStack: state.stackList.length > 0,
|
||||||
stackCount: state.stackListNow.length,
|
stackCount: state.stackList.length,
|
||||||
stackRedCount: state.stackRedCountNow,
|
stackRedCount: state.stackRedCount,
|
||||||
profilesCount: state.profilesListNow.length,
|
profilesCount: state.profilesList.length,
|
||||||
storageCount: selectStorageListNow(state).length,
|
storageCount: selectStorageListNow(state).length,
|
||||||
storageType: selectStorageType(state),
|
storageType: selectStorageType(state),
|
||||||
showStorage: selectStorageType(state) !== STORAGE_TYPES.NONE,
|
showStorage: selectStorageType(state) !== STORAGE_TYPES.NONE,
|
||||||
showProfiler: state.profilesList.length > 0,
|
showProfiler: state.profilesList.length > 0,
|
||||||
showGraphql: state.graphqlList.length > 0,
|
showGraphql: state.graphqlList.length > 0,
|
||||||
showFetch: state.fetchCount > 0,
|
showFetch: state.fetchCount > 0,
|
||||||
fetchCount: state.fetchCountNow,
|
fetchCount: state.fetchCount,
|
||||||
graphqlCount: state.graphqlListNow.length,
|
graphqlCount: state.graphqlList.length,
|
||||||
exceptionsCount: state.exceptionsListNow.length,
|
showLongtasks: state.longtasksList.length > 0,
|
||||||
showExceptions: state.exceptionsList.length > 0,
|
|
||||||
liveTimeTravel: state.liveTimeTravel,
|
liveTimeTravel: state.liveTimeTravel,
|
||||||
}))
|
}))
|
||||||
@connect(
|
@connect(
|
||||||
|
|
@ -162,7 +142,7 @@ export default class Controls extends React.Component {
|
||||||
nextProps.disabled !== this.props.disabled ||
|
nextProps.disabled !== this.props.disabled ||
|
||||||
nextProps.fullscreenDisabled !== this.props.fullscreenDisabled ||
|
nextProps.fullscreenDisabled !== this.props.fullscreenDisabled ||
|
||||||
// nextProps.inspectorMode !== this.props.inspectorMode ||
|
// nextProps.inspectorMode !== this.props.inspectorMode ||
|
||||||
nextProps.logCount !== this.props.logCount ||
|
// nextProps.logCount !== this.props.logCount ||
|
||||||
nextProps.logRedCount !== this.props.logRedCount ||
|
nextProps.logRedCount !== this.props.logRedCount ||
|
||||||
nextProps.resourceRedCount !== this.props.resourceRedCount ||
|
nextProps.resourceRedCount !== this.props.resourceRedCount ||
|
||||||
nextProps.fetchRedCount !== this.props.fetchRedCount ||
|
nextProps.fetchRedCount !== this.props.fetchRedCount ||
|
||||||
|
|
@ -178,8 +158,7 @@ export default class Controls extends React.Component {
|
||||||
nextProps.showFetch !== this.props.showFetch ||
|
nextProps.showFetch !== this.props.showFetch ||
|
||||||
nextProps.fetchCount !== this.props.fetchCount ||
|
nextProps.fetchCount !== this.props.fetchCount ||
|
||||||
nextProps.graphqlCount !== this.props.graphqlCount ||
|
nextProps.graphqlCount !== this.props.graphqlCount ||
|
||||||
nextProps.showExceptions !== this.props.showExceptions ||
|
nextProps.showLongtasks !== this.props.showLongtasks ||
|
||||||
nextProps.exceptionsCount !== this.props.exceptionsCount ||
|
|
||||||
nextProps.liveTimeTravel !== this.props.liveTimeTravel ||
|
nextProps.liveTimeTravel !== this.props.liveTimeTravel ||
|
||||||
nextProps.skipInterval !== this.props.skipInterval
|
nextProps.skipInterval !== this.props.skipInterval
|
||||||
)
|
)
|
||||||
|
|
@ -284,24 +263,14 @@ export default class Controls extends React.Component {
|
||||||
skip,
|
skip,
|
||||||
speed,
|
speed,
|
||||||
disabled,
|
disabled,
|
||||||
logCount,
|
|
||||||
logRedCount,
|
logRedCount,
|
||||||
resourceRedCount,
|
resourceRedCount,
|
||||||
fetchRedCount,
|
|
||||||
showStack,
|
showStack,
|
||||||
stackCount,
|
|
||||||
stackRedCount,
|
stackRedCount,
|
||||||
profilesCount,
|
|
||||||
storageCount,
|
|
||||||
showStorage,
|
showStorage,
|
||||||
storageType,
|
storageType,
|
||||||
showProfiler,
|
showProfiler,
|
||||||
showGraphql,
|
showGraphql,
|
||||||
showFetch,
|
|
||||||
fetchCount,
|
|
||||||
graphqlCount,
|
|
||||||
exceptionsCount,
|
|
||||||
showExceptions,
|
|
||||||
fullscreen,
|
fullscreen,
|
||||||
inspectorMode,
|
inspectorMode,
|
||||||
closedLive,
|
closedLive,
|
||||||
|
|
@ -378,7 +347,6 @@ export default class Controls extends React.Component {
|
||||||
label="CONSOLE"
|
label="CONSOLE"
|
||||||
noIcon
|
noIcon
|
||||||
labelClassName="!text-base font-semibold"
|
labelClassName="!text-base font-semibold"
|
||||||
count={logCount}
|
|
||||||
hasErrors={logRedCount > 0}
|
hasErrors={logRedCount > 0}
|
||||||
containerClassName="mx-2"
|
containerClassName="mx-2"
|
||||||
/>
|
/>
|
||||||
|
|
@ -405,25 +373,11 @@ export default class Controls extends React.Component {
|
||||||
containerClassName="mx-2"
|
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 && (
|
{!live && showGraphql && (
|
||||||
<ControlButton
|
<ControlButton
|
||||||
disabled={disabled && !inspectorMode}
|
disabled={disabled && !inspectorMode}
|
||||||
onClick={() => toggleBottomTools(GRAPHQL)}
|
onClick={() => toggleBottomTools(GRAPHQL)}
|
||||||
active={bottomBlock === GRAPHQL && !inspectorMode}
|
active={bottomBlock === GRAPHQL && !inspectorMode}
|
||||||
count={graphqlCount}
|
|
||||||
label="GRAPHQL"
|
label="GRAPHQL"
|
||||||
noIcon
|
noIcon
|
||||||
labelClassName="!text-base font-semibold"
|
labelClassName="!text-base font-semibold"
|
||||||
|
|
@ -435,26 +389,12 @@ export default class Controls extends React.Component {
|
||||||
disabled={disabled && !inspectorMode}
|
disabled={disabled && !inspectorMode}
|
||||||
onClick={() => toggleBottomTools(STORAGE)}
|
onClick={() => toggleBottomTools(STORAGE)}
|
||||||
active={bottomBlock === STORAGE && !inspectorMode}
|
active={bottomBlock === STORAGE && !inspectorMode}
|
||||||
count={storageCount}
|
|
||||||
label={getStorageName(storageType)}
|
label={getStorageName(storageType)}
|
||||||
noIcon
|
noIcon
|
||||||
labelClassName="!text-base font-semibold"
|
labelClassName="!text-base font-semibold"
|
||||||
containerClassName="mx-2"
|
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 && (
|
{!live && showStack && (
|
||||||
<ControlButton
|
<ControlButton
|
||||||
disabled={disabled && !inspectorMode}
|
disabled={disabled && !inspectorMode}
|
||||||
|
|
@ -464,7 +404,6 @@ export default class Controls extends React.Component {
|
||||||
noIcon
|
noIcon
|
||||||
labelClassName="!text-base font-semibold"
|
labelClassName="!text-base font-semibold"
|
||||||
containerClassName="mx-2"
|
containerClassName="mx-2"
|
||||||
count={stackCount}
|
|
||||||
hasErrors={stackRedCount > 0}
|
hasErrors={stackRedCount > 0}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
@ -473,7 +412,6 @@ export default class Controls extends React.Component {
|
||||||
disabled={disabled && !inspectorMode}
|
disabled={disabled && !inspectorMode}
|
||||||
onClick={() => toggleBottomTools(PROFILER)}
|
onClick={() => toggleBottomTools(PROFILER)}
|
||||||
active={bottomBlock === PROFILER && !inspectorMode}
|
active={bottomBlock === PROFILER && !inspectorMode}
|
||||||
count={profilesCount}
|
|
||||||
label="PROFILER"
|
label="PROFILER"
|
||||||
noIcon
|
noIcon
|
||||||
labelClassName="!text-base font-semibold"
|
labelClassName="!text-base font-semibold"
|
||||||
|
|
|
||||||
|
|
@ -14,20 +14,18 @@ import {
|
||||||
PROFILER,
|
PROFILER,
|
||||||
PERFORMANCE,
|
PERFORMANCE,
|
||||||
GRAPHQL,
|
GRAPHQL,
|
||||||
FETCH,
|
|
||||||
EXCEPTIONS,
|
EXCEPTIONS,
|
||||||
LONGTASKS,
|
LONGTASKS,
|
||||||
INSPECTOR,
|
INSPECTOR,
|
||||||
OVERVIEW,
|
OVERVIEW,
|
||||||
} from 'Duck/components/player';
|
} from 'Duck/components/player';
|
||||||
import Network from '../Network';
|
import NetworkPanel from 'Shared/DevTools/NetworkPanel';
|
||||||
import Console from '../Console/Console';
|
import Console from '../Console/Console';
|
||||||
import StackEvents from '../StackEvents/StackEvents';
|
import StackEvents from '../StackEvents/StackEvents';
|
||||||
import Storage from '../Storage';
|
import Storage from '../Storage';
|
||||||
import Profiler from '../Profiler';
|
import Profiler from '../Profiler';
|
||||||
import { ConnectedPerformance } from '../Performance';
|
import { ConnectedPerformance } from '../Performance';
|
||||||
import GraphQL from '../GraphQL';
|
import GraphQL from '../GraphQL';
|
||||||
import Fetch from '../Fetch';
|
|
||||||
import Exceptions from '../Exceptions/Exceptions';
|
import Exceptions from '../Exceptions/Exceptions';
|
||||||
import LongTasks from '../LongTasks';
|
import LongTasks from '../LongTasks';
|
||||||
import Inspector from '../Inspector';
|
import Inspector from '../Inspector';
|
||||||
|
|
@ -42,29 +40,38 @@ import Overlay from './Overlay';
|
||||||
import stl from './player.module.css';
|
import stl from './player.module.css';
|
||||||
import { updateLastPlayedSession } from 'Duck/sessions';
|
import { updateLastPlayedSession } from 'Duck/sessions';
|
||||||
import OverviewPanel from '../OverviewPanel';
|
import OverviewPanel from '../OverviewPanel';
|
||||||
|
import ConsolePanel from 'Shared/DevTools/ConsolePanel';
|
||||||
|
import ProfilerPanel from 'Shared/DevTools/ProfilerPanel';
|
||||||
|
|
||||||
@connectPlayer(state => ({
|
@connectPlayer((state) => ({
|
||||||
live: state.live,
|
live: state.live,
|
||||||
}))
|
}))
|
||||||
@connect(state => {
|
@connect(
|
||||||
const isAssist = window.location.pathname.includes('/assist/');
|
(state) => {
|
||||||
return {
|
const isAssist = window.location.pathname.includes('/assist/');
|
||||||
fullscreen: state.getIn([ 'components', 'player', 'fullscreen' ]),
|
return {
|
||||||
nextId: state.getIn([ 'sessions', 'nextId' ]),
|
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
|
||||||
sessionId: state.getIn([ 'sessions', 'current', 'sessionId' ]),
|
nextId: state.getIn(['sessions', 'nextId']),
|
||||||
closedLive: !!state.getIn([ 'sessions', 'errors' ]) || (isAssist && !state.getIn([ 'sessions', 'current', 'live' ])),
|
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 {
|
export default class Player extends React.PureComponent {
|
||||||
screenWrapper = React.createRef();
|
screenWrapper = React.createRef();
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if ([ prevProps.bottomBlock, this.props.bottomBlock ].includes(NONE) ||
|
if (
|
||||||
prevProps.fullscreen !== this.props.fullscreen) {
|
[prevProps.bottomBlock, this.props.bottomBlock].includes(NONE) ||
|
||||||
|
prevProps.fullscreen !== this.props.fullscreen
|
||||||
|
) {
|
||||||
scalePlayerScreen();
|
scalePlayerScreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -73,7 +80,7 @@ export default class Player extends React.PureComponent {
|
||||||
this.props.updateLastPlayedSession(this.props.sessionId);
|
this.props.updateLastPlayedSession(this.props.sessionId);
|
||||||
if (this.props.closedLive) return;
|
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);
|
attachPlayer(parentElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,66 +93,39 @@ export default class Player extends React.PureComponent {
|
||||||
nextId,
|
nextId,
|
||||||
closedLive,
|
closedLive,
|
||||||
bottomBlock,
|
bottomBlock,
|
||||||
activeTab
|
activeTab,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const maxWidth = activeTab ? 'calc(100vw - 270px)' : '100vw'
|
const maxWidth = activeTab ? 'calc(100vw - 270px)' : '100vw';
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={ cn(className, stl.playerBody, "flex flex-col relative", fullscreen && 'pb-2') }
|
className={cn(className, stl.playerBody, 'flex flex-col relative', fullscreen && 'pb-2')}
|
||||||
data-bottom-block={ bottomBlockIsActive }
|
data-bottom-block={bottomBlockIsActive}
|
||||||
>
|
>
|
||||||
{fullscreen && <EscapeButton onClose={ fullscreenOff } />}
|
{fullscreen && <EscapeButton onClose={fullscreenOff} />}
|
||||||
<div className="relative flex-1 overflow-hidden">
|
<div className="relative flex-1 overflow-hidden">
|
||||||
<Overlay nextId={nextId} togglePlay={PlayerControls.togglePlay} closedLive={closedLive} />
|
<Overlay nextId={nextId} togglePlay={PlayerControls.togglePlay} closedLive={closedLive} />
|
||||||
<div
|
<div className={stl.screenWrapper} ref={this.screenWrapper} />
|
||||||
className={ stl.screenWrapper }
|
|
||||||
ref={ this.screenWrapper }
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{ !fullscreen && !!bottomBlock &&
|
{!fullscreen && !!bottomBlock && (
|
||||||
<div style={{ maxWidth, width: '100%' }}>
|
<div style={{ maxWidth, width: '100%' }}>
|
||||||
{ bottomBlock === OVERVIEW &&
|
{bottomBlock === OVERVIEW && <OverviewPanel />}
|
||||||
<OverviewPanel />
|
{bottomBlock === CONSOLE && <ConsolePanel />}
|
||||||
}
|
{bottomBlock === NETWORK && (
|
||||||
{ bottomBlock === CONSOLE &&
|
// <Network />
|
||||||
<Console />
|
<NetworkPanel />
|
||||||
}
|
)}
|
||||||
{ bottomBlock === NETWORK &&
|
{bottomBlock === STACKEVENTS && <StackEvents />}
|
||||||
<Network />
|
{bottomBlock === STORAGE && <Storage />}
|
||||||
}
|
{bottomBlock === PROFILER && <ProfilerPanel />}
|
||||||
{ bottomBlock === STACKEVENTS &&
|
{bottomBlock === PERFORMANCE && <ConnectedPerformance />}
|
||||||
<StackEvents />
|
{bottomBlock === GRAPHQL && <GraphQL />}
|
||||||
}
|
{bottomBlock === EXCEPTIONS && <Exceptions />}
|
||||||
{ bottomBlock === STORAGE &&
|
{bottomBlock === LONGTASKS && <LongTasks />}
|
||||||
<Storage />
|
{bottomBlock === INSPECTOR && <Inspector />}
|
||||||
}
|
|
||||||
{ bottomBlock === PROFILER &&
|
|
||||||
<Profiler />
|
|
||||||
}
|
|
||||||
{ bottomBlock === PERFORMANCE &&
|
|
||||||
<ConnectedPerformance />
|
|
||||||
}
|
|
||||||
{ bottomBlock === GRAPHQL &&
|
|
||||||
<GraphQL />
|
|
||||||
}
|
|
||||||
{ bottomBlock === FETCH &&
|
|
||||||
<Fetch />
|
|
||||||
}
|
|
||||||
{ bottomBlock === EXCEPTIONS &&
|
|
||||||
<Exceptions />
|
|
||||||
}
|
|
||||||
{ bottomBlock === LONGTASKS &&
|
|
||||||
<LongTasks />
|
|
||||||
}
|
|
||||||
{ bottomBlock === INSPECTOR &&
|
|
||||||
<Inspector />
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
<Controls
|
<Controls {...PlayerControls} />
|
||||||
{ ...PlayerControls }
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ export default class Profiler extends React.PureComponent {
|
||||||
icon="search"
|
icon="search"
|
||||||
name="filter"
|
name="filter"
|
||||||
onChange={ this.onFilterChange }
|
onChange={ this.onFilterChange }
|
||||||
|
height={28}
|
||||||
/>
|
/>
|
||||||
</BottomBlock.Header>
|
</BottomBlock.Header>
|
||||||
<BottomBlock.Content>
|
<BottomBlock.Content>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import withEnumToggle from 'HOCs/withEnumToggle';
|
||||||
import { connectPlayer, jump } from 'Player';
|
import { connectPlayer, jump } from 'Player';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import StackEventRow from 'Shared/DevTools/StackEventRow';
|
||||||
import { DATADOG, SENTRY, STACKDRIVER, typeList } from 'Types/session/stackEvent';
|
import { DATADOG, SENTRY, STACKDRIVER, typeList } from 'Types/session/stackEvent';
|
||||||
import { NoContent, SlideModal, Tabs, Link } from 'UI';
|
import { NoContent, SlideModal, Tabs, Link } from 'UI';
|
||||||
import Autoscroll from '../Autoscroll';
|
import Autoscroll from '../Autoscroll';
|
||||||
|
|
@ -19,7 +20,7 @@ const TABS = [ALL, ...typeList].map((tab) => ({ text: tab, key: tab }));
|
||||||
@withEnumToggle('activeTab', 'setActiveTab', ALL)
|
@withEnumToggle('activeTab', 'setActiveTab', ALL)
|
||||||
@connectPlayer((state) => ({
|
@connectPlayer((state) => ({
|
||||||
stackEvents: state.stackList,
|
stackEvents: state.stackList,
|
||||||
stackEventsNow: state.stackListNow,
|
// stackEventsNow: state.stackListNow,
|
||||||
}))
|
}))
|
||||||
@connect(
|
@connect(
|
||||||
(state) => ({
|
(state) => ({
|
||||||
|
|
@ -69,20 +70,20 @@ export default class StackEvents extends React.PureComponent {
|
||||||
({ key }) => key === ALL || stackEvents.some(({ source }) => key === source)
|
({ key }) => key === ALL || stackEvents.some(({ source }) => key === source)
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredStackEvents = stackEvents
|
const filteredStackEvents = stackEvents.filter(
|
||||||
// .filter(({ data }) => data.includes(filter))
|
({ source }) => activeTab === ALL || activeTab === source
|
||||||
.filter(({ source }) => activeTab === ALL || activeTab === source);
|
);
|
||||||
|
|
||||||
let lastIndex = -1;
|
// let lastIndex = -1;
|
||||||
// TODO: Need to do filtering in store, or preferably in a selector
|
// // TODO: Need to do filtering in store, or preferably in a selector
|
||||||
filteredStackEvents.forEach((item, index) => {
|
// filteredStackEvents.forEach((item, index) => {
|
||||||
if (
|
// if (
|
||||||
this.props.stackEventsNow.length > 0 &&
|
// this.props.stackEventsNow.length > 0 &&
|
||||||
item.time <= this.props.stackEventsNow[this.props.stackEventsNow.length - 1].time
|
// item.time <= this.props.stackEventsNow[this.props.stackEventsNow.length - 1].time
|
||||||
) {
|
// ) {
|
||||||
lastIndex = index;
|
// lastIndex = index;
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -154,16 +155,21 @@ export default class StackEvents extends React.PureComponent {
|
||||||
size="small"
|
size="small"
|
||||||
show={filteredStackEvents.length === 0}
|
show={filteredStackEvents.length === 0}
|
||||||
>
|
>
|
||||||
<Autoscroll autoScrollTo={Math.max(lastIndex, 0)}>
|
<Autoscroll>
|
||||||
{filteredStackEvents.map((userEvent, index) => (
|
{filteredStackEvents.map((userEvent, index) => (
|
||||||
<UserEvent
|
<StackEventRow
|
||||||
key={userEvent.key}
|
key={userEvent.key}
|
||||||
onDetailsClick={this.onDetailsClick.bind(this)}
|
event={userEvent}
|
||||||
inactive={index > lastIndex}
|
|
||||||
selected={lastIndex === index}
|
|
||||||
userEvent={userEvent}
|
|
||||||
onJump={() => jump(userEvent.time)}
|
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>
|
</Autoscroll>
|
||||||
</NoContent>
|
</NoContent>
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,9 @@ export default class JsonViewer extends React.PureComponent {
|
||||||
{isObjectData && <JSONTree src={data} collapsed={false} />}
|
{isObjectData && <JSONTree src={data} collapsed={false} />}
|
||||||
{!isObjectData && Array.isArray(data) && (
|
{!isObjectData && Array.isArray(data) && (
|
||||||
<div>
|
<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} />
|
<JSONTree src={data[1]} collapsed={false} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
import { OPENREPLAY, SENTRY, DATADOG, STACKDRIVER } from 'Types/session/stackEvent';
|
import { OPENREPLAY, SENTRY, DATADOG, STACKDRIVER } from 'Types/session/stackEvent';
|
||||||
import { Icon, IconButton } from 'UI';
|
import { Icon } from 'UI';
|
||||||
import withToggle from 'HOCs/withToggle';
|
import withToggle from 'HOCs/withToggle';
|
||||||
import Sentry from './Sentry';
|
import Sentry from './Sentry';
|
||||||
import JsonViewer from './JsonViewer';
|
import JsonViewer from './JsonViewer';
|
||||||
import stl from './userEvent.module.css';
|
import stl from './userEvent.module.css';
|
||||||
import { Duration } from 'luxon';
|
import { Duration } from 'luxon';
|
||||||
|
import JumpButton from 'Shared/DevTools/JumpButton';
|
||||||
|
|
||||||
// const modalSources = [ SENTRY, DATADOG ];
|
// const modalSources = [ SENTRY, DATADOG ];
|
||||||
|
|
||||||
|
|
@ -34,34 +35,33 @@ export default class UserEvent extends React.PureComponent {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { userEvent, inactive, selected } = this.props;
|
const { userEvent, inactive, selected } = this.props;
|
||||||
//const message = this.getEventMessage();
|
let message = userEvent.payload[0] || '';
|
||||||
|
message = typeof message === 'string' ? message : JSON.stringify(message);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-scroll-item={userEvent.isRed()}
|
data-scroll-item={userEvent.isRed()}
|
||||||
// onClick={ this.props.switchOpen } //
|
onClick={this.onClickDetails}
|
||||||
onClick={this.props.onJump} //
|
className={cn(
|
||||||
className={cn('group flex py-2 px-4 ', stl.userEvent, this.getLevelClassname(), {
|
'group flex items-center py-2 px-4 border-b cursor-pointer relative',
|
||||||
// [stl.inactive]: inactive,
|
// stl.userEvent,
|
||||||
[stl.selected]: selected,
|
// 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')}
|
{Duration.fromMillis(userEvent.time).toFormat('mm:ss.SSS')}
|
||||||
</div>
|
</div> */}
|
||||||
<div className={cn('mr-auto', stl.infoWrapper)}>
|
<div className={cn('mr-auto flex items-start')}>
|
||||||
<div className={stl.title}>
|
<Icon {...this.getIconProps()} />
|
||||||
<Icon {...this.getIconProps()} />
|
<div>
|
||||||
{userEvent.name}
|
<div className="capitalize font-medium mb-1">{userEvent.name}</div>
|
||||||
|
<div className="code-font text-xs">{message}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="self-center">
|
<JumpButton onClick={this.props.onJump} />
|
||||||
<IconButton
|
|
||||||
outline={!userEvent.isRed()}
|
|
||||||
red={userEvent.isRed()}
|
|
||||||
onClick={this.onClickDetails}
|
|
||||||
label="DETAILS"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ $offset: 10px;
|
||||||
.headers {
|
.headers {
|
||||||
box-shadow: 0 1px 2px 0 $gray-light;
|
box-shadow: 0 1px 2px 0 $gray-light;
|
||||||
background-color: $gray-lightest;
|
background-color: $gray-lightest;
|
||||||
color: $gray-medium;
|
color: $gray-darkest;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
@ -47,6 +47,10 @@ $offset: 10px;
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0 $offset;
|
padding: 0 $offset;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $active-blue;
|
||||||
|
}
|
||||||
/*align-items: center;
|
/*align-items: center;
|
||||||
cursor: pointer;
|
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 React from 'react';
|
||||||
import { JSONTree, NoContent, Button, Tabs } from 'UI';
|
import { JSONTree, NoContent, Button, Tabs, Icon } from 'UI';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
import stl from './fetchDetails.module.css';
|
import stl from './fetchDetails.module.css';
|
||||||
import Headers from './components/Headers';
|
import Headers from './components/Headers';
|
||||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||||
|
import { TYPES } from 'Types/session/resource';
|
||||||
|
import { formatBytes } from 'App/utils';
|
||||||
|
|
||||||
const HEADERS = 'HEADERS';
|
const HEADERS = 'HEADERS';
|
||||||
const REQUEST = 'REQUEST';
|
const REQUEST = 'REQUEST';
|
||||||
|
|
@ -129,43 +131,107 @@ export default class FetchDetailsModal extends React.PureComponent {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
resource: { method, url, duration },
|
resource,
|
||||||
|
fetchPresented,
|
||||||
nextClick,
|
nextClick,
|
||||||
prevClick,
|
prevClick,
|
||||||
first = false,
|
first = false,
|
||||||
last = false,
|
last = false,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
const { method, url, duration } = resource;
|
||||||
const { activeTab, tabs } = this.state;
|
const { activeTab, tabs } = this.state;
|
||||||
|
const _duration = parseInt(duration);
|
||||||
const _duration = parseInt(duration)
|
|
||||||
console.log('_duration', _duration);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white p-5 h-screen overflow-y-auto" style={{ width: '500px' }}>
|
<div className="bg-white p-5 h-screen overflow-y-auto" style={{ width: '500px' }}>
|
||||||
<h5 className="mb-2">{'URL'}</h5>
|
<h5 className="mb-2 text-2xl">Network Request</h5>
|
||||||
<div className={cn(stl.url, 'color-gray-darkest')}>{url}</div>
|
<div className="flex items-center py-1">
|
||||||
<div className="flex items-start mt-4">
|
<div className="font-medium">Name</div>
|
||||||
{method && (
|
<div className="rounded-lg bg-active-blue px-2 py-1 ml-2 whitespace-nowrap overflow-hidden text-clip">
|
||||||
<div className="w-4/12">
|
{resource.name}
|
||||||
<div className="font-medium mb-2">Method</div>
|
</div>
|
||||||
<div>{method}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!!_duration && (
|
|
||||||
<div className="w-4/12">
|
|
||||||
<div className="font-medium mb-2">Duration</div>
|
|
||||||
<div>{_duration } ms</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6">
|
<div className="flex items-center py-1">
|
||||||
<div>
|
<div className="font-medium">Type</div>
|
||||||
<Tabs tabs={tabs} active={activeTab} onClick={this.onTabClick} border={true} />
|
<div className="rounded bg-active-blue px-2 py-1 ml-2 whitespace-nowrap overflow-hidden text-clip">
|
||||||
<div style={{ height: 'calc(100vh - 314px)', overflowY: 'auto' }}>
|
{resource.type}
|
||||||
{this.renderActiveTab(activeTab)}
|
</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>
|
||||||
</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">
|
{/* <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}>
|
<Button variant="outline" onClick={prevClick} disabled={first}>
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ function FilterSource(props: Props) {
|
||||||
case FilterType.NUMBER:
|
case FilterType.NUMBER:
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<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 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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,8 @@ function FilterValue(props: Props) {
|
||||||
<FilterValueDropdown
|
<FilterValueDropdown
|
||||||
// search={true}
|
// search={true}
|
||||||
value={value}
|
value={value}
|
||||||
filter={filter}
|
placeholder={filter.placeholder}
|
||||||
|
// filter={filter}
|
||||||
options={filter.options}
|
options={filter.options}
|
||||||
onChange={({ value }) => onChange(null, { value }, valueIndex)}
|
onChange={({ value }) => onChange(null, { value }, valueIndex)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -106,6 +107,7 @@ function FilterValue(props: Props) {
|
||||||
// multiple={true}
|
// multiple={true}
|
||||||
value={value}
|
value={value}
|
||||||
// filter={filter}
|
// filter={filter}
|
||||||
|
placeholder={filter.placeholder}
|
||||||
options={filter.options}
|
options={filter.options}
|
||||||
onChange={({ value }) => onChange(null, value, valueIndex)}
|
onChange={({ value }) => onChange(null, value, valueIndex)}
|
||||||
onAddValue={onAddValue}
|
onAddValue={onAddValue}
|
||||||
|
|
@ -164,7 +166,7 @@ function FilterValue(props: Props) {
|
||||||
endpoint="/events/search"
|
endpoint="/events/search"
|
||||||
params={getParms(filter.key)}
|
params={getParms(filter.key)}
|
||||||
headerText={''}
|
headerText={''}
|
||||||
// placeholder={''}
|
placeholder={filter.placeholder}
|
||||||
onSelect={(e, item) => onChange(e, item, valueIndex)}
|
onSelect={(e, item) => onChange(e, item, valueIndex)}
|
||||||
icon={filter.icon}
|
icon={filter.icon}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ const dropdownStyles = {
|
||||||
interface Props {
|
interface Props {
|
||||||
// filter: any; // event/filter
|
// filter: any; // event/filter
|
||||||
// options: any[];
|
// options: any[];
|
||||||
|
placeholder?: string
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: any) => void;
|
onChange: (value: any) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
@ -84,7 +85,7 @@ interface Props {
|
||||||
isMultilple?: boolean;
|
isMultilple?: boolean;
|
||||||
}
|
}
|
||||||
function FilterValueDropdown(props: Props) {
|
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 = []
|
// const options = []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -97,7 +98,7 @@ function FilterValueDropdown(props: Props) {
|
||||||
name="issue_type"
|
name="issue_type"
|
||||||
defaultValue={ value }
|
defaultValue={ value }
|
||||||
onChange={ (value: any) => onChange(value.value) }
|
onChange={ (value: any) => onChange(value.value) }
|
||||||
placeholder="Select"
|
placeholder={placeholder}
|
||||||
styles={dropdownStyles}
|
styles={dropdownStyles}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -26,16 +26,16 @@ function GraphQLDetailsModal(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-5 bg-white h-screen overflow-y-auto" style={{ width: '500px' }}>
|
<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={dataClass}>{operationName}</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4 mt-4">
|
<div className="flex items-center gap-4 mt-4">
|
||||||
<div className="w-6/12">
|
<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 className={dataClass}>{operationKind}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-6/12">
|
<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 className={dataClass}>{duration ? parseInt(duration) : '???'} ms</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -43,7 +43,7 @@ function GraphQLDetailsModal(props: Props) {
|
||||||
<div style={{ height: 'calc(100vh - 314px)', overflowY: 'auto' }}>
|
<div style={{ height: 'calc(100vh - 314px)', overflowY: 'auto' }}>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between items-start mt-6 mb-2">
|
<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>
|
||||||
<div className={dataClass}>
|
<div className={dataClass}>
|
||||||
{jsonVars === undefined ? variables : <JSONTree src={jsonVars} />}
|
{jsonVars === undefined ? variables : <JSONTree src={jsonVars} />}
|
||||||
|
|
@ -53,7 +53,7 @@ function GraphQLDetailsModal(props: Props) {
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between items-start mt-6 mb-2">
|
<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>
|
||||||
<div className={dataClass}>
|
<div className={dataClass}>
|
||||||
{jsonResponse === undefined ? response : <JSONTree src={jsonResponse} />}
|
{jsonResponse === undefined ? response : <JSONTree src={jsonResponse} />}
|
||||||
|
|
|
||||||
|
|
@ -5,34 +5,43 @@ import stl from './errorItem.module.css';
|
||||||
import { Duration } from 'luxon';
|
import { Duration } from 'luxon';
|
||||||
import { useModal } from 'App/components/Modal';
|
import { useModal } from 'App/components/Modal';
|
||||||
import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorDetailsModal';
|
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 { showModal } = useModal();
|
||||||
|
|
||||||
const onErrorClick = () => {
|
const onErrorClick = () => {
|
||||||
showModal(<ErrorDetailsModal errorId={error.errorId} />, { right: true });
|
showModal(<ErrorDetailsModal errorId={error.errorId} />, { right: true });
|
||||||
}
|
};
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(stl.wrapper, 'py-2 px-4 flex cursor-pointer', {
|
className={cn(stl.wrapper, 'py-2 px-4 flex cursor-pointer hover:bg-active-blue relative group', {
|
||||||
[stl.inactive]: inactive,
|
// [stl.inactive]: inactive,
|
||||||
[stl.selected]: selected,
|
// [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')}
|
{Duration.fromMillis(error.time).toFormat('mm:ss.SSS')}
|
||||||
</div>
|
</div> */}
|
||||||
<div className="mr-auto overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
<div className="color-red mb-1 cursor-pointer code-font">
|
<div className="color-red mb-1 cursor-pointer code-font">
|
||||||
{error.name}
|
{error.name}
|
||||||
<span className="color-gray-darkest ml-2">{error.stack0InfoString}</span>
|
<span className="color-gray-darkest ml-2">{error.stack0InfoString}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm color-gray-medium">{error.message}</div>
|
<div className="text-xs code-font">{error.message}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="self-center">
|
{/* <div className="self-center">
|
||||||
<IconButton red onClick={onErrorClick} label="DETAILS" />
|
<IconButton red onClick={onErrorClick} label="DETAILS" />
|
||||||
</div>
|
</div> */}
|
||||||
|
<JumpButton onClick={onJump} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -9,10 +9,12 @@ interface Props {
|
||||||
leadingButton?: React.ReactNode;
|
leadingButton?: React.ReactNode;
|
||||||
type?: string;
|
type?: string;
|
||||||
rows?: number;
|
rows?: number;
|
||||||
|
height?: number;
|
||||||
|
width?: number;
|
||||||
[x: string]: any;
|
[x: string]: any;
|
||||||
}
|
}
|
||||||
const Input = React.forwardRef((props: Props, ref: 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 (
|
return (
|
||||||
<div className={cn({ relative: icon || leadingButton }, wrapperClassName)}>
|
<div className={cn({ relative: icon || leadingButton }, wrapperClassName)}>
|
||||||
{icon && <Icon name={icon} className="absolute top-0 bottom-0 my-auto ml-4" size="14" />}
|
{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
|
<input
|
||||||
ref={ref}
|
ref={ref}
|
||||||
type={type}
|
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 })}
|
className={cn('p-2 border border-gray-light bg-white w-full rounded', className, { 'pl-10': icon })}
|
||||||
{...rest}
|
{...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"/>
|
<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>
|
</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 = [
|
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.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.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: 'Path', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/location', 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', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/custom', 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.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, 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_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', operator: '=', operatorOptions: filterOptions.customOperators, 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', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch', options: filterOptions.methodOptions },
|
{ 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)', operator: '=', operatorOptions: filterOptions.customOperators, icon: 'filters/fetch' },
|
{ 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_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' },
|
{ 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 },
|
], 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_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.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.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', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/error', 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 },
|
// { key: FilterKey.METADATA, type: FilterType.MULTIPLE, category: FilterCategory.METADATA, label: 'Metadata', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/metadata', isEvent: true },
|
||||||
|
|
||||||
// FILTERS
|
// 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_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.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.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.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.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.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.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' },
|
{ key: FilterKey.USERANONYMOUSID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User AnonymousId', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/userid' },
|
||||||
|
|
||||||
// PERFORMANCE
|
// 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.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', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/lcpt', 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', 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', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/ttfb', 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', 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', 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_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', 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.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', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, icon: 'filters/fetch-failed', isEvent: true },
|
{ 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', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/click', options: filterOptions.issueOptions },
|
{ 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) => {
|
const mapFilters = (list) => {
|
||||||
|
|
@ -137,6 +137,7 @@ export default Record({
|
||||||
timestamp: 0,
|
timestamp: 0,
|
||||||
key: '',
|
key: '',
|
||||||
label: '',
|
label: '',
|
||||||
|
placeholder: '',
|
||||||
icon: '',
|
icon: '',
|
||||||
type: '',
|
type: '',
|
||||||
value: [""],
|
value: [""],
|
||||||
|
|
@ -155,6 +156,7 @@ export default Record({
|
||||||
source: [""],
|
source: [""],
|
||||||
sourceType: '',
|
sourceType: '',
|
||||||
sourceOperator: '=',
|
sourceOperator: '=',
|
||||||
|
sourcePlaceholder: '',
|
||||||
sourceUnit: '',
|
sourceUnit: '',
|
||||||
sourceOperatorOptions: [],
|
sourceOperatorOptions: [],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ export default Record({
|
||||||
value: '',
|
value: '',
|
||||||
time: undefined,
|
time: undefined,
|
||||||
index: undefined,
|
index: undefined,
|
||||||
|
errorId: undefined,
|
||||||
}, {
|
}, {
|
||||||
methods: {
|
methods: {
|
||||||
isRed() {
|
isRed() {
|
||||||
|
|
|
||||||