change(ui): rewrite issue and resource types
This commit is contained in:
parent
22cdb4f4e7
commit
2a714c0145
17 changed files with 275 additions and 663 deletions
|
|
@ -1,12 +1,15 @@
|
|||
import { createContext } from 'react';
|
||||
import {
|
||||
IWebPlayer,
|
||||
IWebPlayerStore
|
||||
IWebPlayerStore,
|
||||
IWebLivePlayer,
|
||||
IWebLivePlayerStore,
|
||||
} from 'Player'
|
||||
|
||||
export interface IPlayerContext {
|
||||
player: IWebPlayer
|
||||
store: IWebPlayerStore,
|
||||
player: IWebPlayer | IWebLivePlayer
|
||||
store: IWebPlayerStore | IWebLivePlayerStore,
|
||||
}
|
||||
export const defaultContextValue: IPlayerContext = { player: undefined, store: undefined}
|
||||
export const defaultContextValue = { player: undefined, store: undefined}
|
||||
// @ts-ignore
|
||||
export const PlayerContext = createContext<IPlayerContext>(defaultContextValue);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { Icon } from 'UI';
|
|||
import { List, AutoSizer, CellMeasurer } from "react-virtualized";
|
||||
import { TYPES } from 'Types/session/event';
|
||||
import { setEventFilter, filterOutNote } from 'Duck/sessions';
|
||||
import { show as showTargetDefiner } from 'Duck/components/targetDefiner';
|
||||
import EventGroupWrapper from './EventGroupWrapper';
|
||||
import styles from './eventsBlock.module.css';
|
||||
import EventSearch from './EventSearch/EventSearch';
|
||||
|
|
@ -179,9 +178,7 @@ export default connect((state: RootStore) => ({
|
|||
filteredEvents: state.getIn([ 'sessions', 'filteredEvents' ]),
|
||||
query: state.getIn(['sessions', 'eventsQuery']),
|
||||
eventsIndex: state.getIn([ 'sessions', 'eventsIndex' ]),
|
||||
targetDefinerDisplayed: state.getIn([ 'components', 'targetDefiner', 'isDisplayed' ]),
|
||||
}), {
|
||||
showTargetDefiner,
|
||||
setEventFilter,
|
||||
filterOutNote
|
||||
})(observer(EventsBlock))
|
||||
|
|
|
|||
|
|
@ -1,435 +0,0 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
connectPlayer,
|
||||
STORAGE_TYPES,
|
||||
selectStorageType,
|
||||
selectStorageListNow,
|
||||
} from 'Player';
|
||||
import LiveTag from 'Shared/LiveTag';
|
||||
import { jumpToLive } from 'Player';
|
||||
|
||||
import { Icon, Tooltip } from 'UI';
|
||||
import { toggleInspectorMode } from 'Player';
|
||||
import {
|
||||
fullscreenOn,
|
||||
fullscreenOff,
|
||||
toggleBottomBlock,
|
||||
changeSkipInterval,
|
||||
OVERVIEW,
|
||||
CONSOLE,
|
||||
NETWORK,
|
||||
STACKEVENTS,
|
||||
STORAGE,
|
||||
PROFILER,
|
||||
PERFORMANCE,
|
||||
GRAPHQL,
|
||||
INSPECTOR,
|
||||
} from 'Duck/components/player';
|
||||
import { AssistDuration } from './Time';
|
||||
import Timeline from './Timeline';
|
||||
import ControlButton from './ControlButton';
|
||||
import PlayerControls from './components/PlayerControls';
|
||||
|
||||
import styles from './controls.module.css';
|
||||
import XRayButton from 'Shared/XRayButton';
|
||||
|
||||
const SKIP_INTERVALS = {
|
||||
2: 2e3,
|
||||
5: 5e3,
|
||||
10: 1e4,
|
||||
15: 15e3,
|
||||
20: 2e4,
|
||||
30: 3e4,
|
||||
60: 6e4,
|
||||
};
|
||||
|
||||
function getStorageName(type) {
|
||||
switch (type) {
|
||||
case STORAGE_TYPES.REDUX:
|
||||
return 'REDUX';
|
||||
case STORAGE_TYPES.MOBX:
|
||||
return 'MOBX';
|
||||
case STORAGE_TYPES.VUEX:
|
||||
return 'VUEX';
|
||||
case STORAGE_TYPES.NGRX:
|
||||
return 'NGRX';
|
||||
case STORAGE_TYPES.ZUSTAND:
|
||||
return 'ZUSTAND';
|
||||
case STORAGE_TYPES.NONE:
|
||||
return 'STATE';
|
||||
}
|
||||
}
|
||||
|
||||
@connectPlayer((state) => ({
|
||||
time: state.time,
|
||||
endTime: state.endTime,
|
||||
live: state.live,
|
||||
livePlay: state.livePlay,
|
||||
playing: state.playing,
|
||||
completed: state.completed,
|
||||
skip: state.skip,
|
||||
skipToIssue: state.skipToIssue,
|
||||
speed: state.speed,
|
||||
disabled: state.cssLoading || state.messagesLoading || state.inspectorMode || state.markedTargets,
|
||||
inspectorMode: state.inspectorMode,
|
||||
fullscreenDisabled: state.messagesLoading,
|
||||
// logCount: state.logList.length,
|
||||
logRedCount: state.logMarkedCount,
|
||||
showExceptions: state.exceptionsList.length > 0,
|
||||
resourceRedCount: state.resourceMarkedCount,
|
||||
fetchRedCount: state.fetchMarkedCount,
|
||||
showStack: state.stackList.length > 0,
|
||||
stackCount: state.stackList.length,
|
||||
stackRedCount: state.stackMarkedCount,
|
||||
profilesCount: state.profilesList.length,
|
||||
storageCount: selectStorageListNow(state).length,
|
||||
storageType: selectStorageType(state),
|
||||
showStorage: selectStorageType(state) !== STORAGE_TYPES.NONE,
|
||||
showProfiler: state.profilesList.length > 0,
|
||||
showGraphql: state.graphqlList.length > 0,
|
||||
showFetch: state.fetchCount > 0,
|
||||
fetchCount: state.fetchCount,
|
||||
graphqlCount: state.graphqlList.length,
|
||||
liveTimeTravel: state.liveTimeTravel,
|
||||
}))
|
||||
@connect(
|
||||
(state, props) => {
|
||||
const permissions = state.getIn(['user', 'account', 'permissions']) || [];
|
||||
const isEnterprise = state.getIn(['user', 'account', 'edition']) === 'ee';
|
||||
return {
|
||||
disabled: props.disabled || (isEnterprise && !permissions.includes('DEV_TOOLS')),
|
||||
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
|
||||
bottomBlock: state.getIn(['components', 'player', 'bottomBlock']),
|
||||
showStorage:
|
||||
props.showStorage || !state.getIn(['components', 'player', 'hiddenHints', 'storage']),
|
||||
showStack: props.showStack || !state.getIn(['components', 'player', 'hiddenHints', 'stack']),
|
||||
closedLive:
|
||||
!!state.getIn(['sessions', 'errors']) || !state.getIn(['sessions', 'current', 'live']),
|
||||
skipInterval: state.getIn(['components', 'player', 'skipInterval']),
|
||||
};
|
||||
},
|
||||
{
|
||||
fullscreenOn,
|
||||
fullscreenOff,
|
||||
toggleBottomBlock,
|
||||
changeSkipInterval,
|
||||
}
|
||||
)
|
||||
export default class Controls extends React.Component {
|
||||
componentDidMount() {
|
||||
document.addEventListener('keydown', this.onKeyDown);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this.onKeyDown);
|
||||
//this.props.toggleInspectorMode(false);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (
|
||||
nextProps.fullscreen !== this.props.fullscreen ||
|
||||
nextProps.bottomBlock !== this.props.bottomBlock ||
|
||||
nextProps.live !== this.props.live ||
|
||||
nextProps.livePlay !== this.props.livePlay ||
|
||||
nextProps.playing !== this.props.playing ||
|
||||
nextProps.completed !== this.props.completed ||
|
||||
nextProps.skip !== this.props.skip ||
|
||||
nextProps.skipToIssue !== this.props.skipToIssue ||
|
||||
nextProps.speed !== this.props.speed ||
|
||||
nextProps.disabled !== this.props.disabled ||
|
||||
nextProps.fullscreenDisabled !== this.props.fullscreenDisabled ||
|
||||
// nextProps.inspectorMode !== this.props.inspectorMode ||
|
||||
// nextProps.logCount !== this.props.logCount ||
|
||||
nextProps.logRedCount !== this.props.logRedCount ||
|
||||
nextProps.showExceptions !== this.props.showExceptions ||
|
||||
nextProps.resourceRedCount !== this.props.resourceRedCount ||
|
||||
nextProps.fetchRedCount !== this.props.fetchRedCount ||
|
||||
nextProps.showStack !== this.props.showStack ||
|
||||
nextProps.stackCount !== this.props.stackCount ||
|
||||
nextProps.stackRedCount !== this.props.stackRedCount ||
|
||||
nextProps.profilesCount !== this.props.profilesCount ||
|
||||
nextProps.storageCount !== this.props.storageCount ||
|
||||
nextProps.storageType !== this.props.storageType ||
|
||||
nextProps.showStorage !== this.props.showStorage ||
|
||||
nextProps.showProfiler !== this.props.showProfiler ||
|
||||
nextProps.showGraphql !== this.props.showGraphql ||
|
||||
nextProps.showFetch !== this.props.showFetch ||
|
||||
nextProps.fetchCount !== this.props.fetchCount ||
|
||||
nextProps.graphqlCount !== this.props.graphqlCount ||
|
||||
nextProps.liveTimeTravel !== this.props.liveTimeTravel ||
|
||||
nextProps.skipInterval !== this.props.skipInterval
|
||||
)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
onKeyDown = (e) => {
|
||||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
||||
return;
|
||||
}
|
||||
if (this.props.inspectorMode) {
|
||||
if (e.key === 'Esc' || e.key === 'Escape') {
|
||||
toggleInspectorMode(false);
|
||||
}
|
||||
}
|
||||
// if (e.key === ' ') {
|
||||
// document.activeElement.blur();
|
||||
// this.props.togglePlay();
|
||||
// }
|
||||
if (e.key === 'Esc' || e.key === 'Escape') {
|
||||
this.props.fullscreenOff();
|
||||
}
|
||||
if (e.key === 'ArrowRight') {
|
||||
this.forthTenSeconds();
|
||||
}
|
||||
if (e.key === 'ArrowLeft') {
|
||||
this.backTenSeconds();
|
||||
}
|
||||
if (e.key === 'ArrowDown') {
|
||||
this.props.speedDown();
|
||||
}
|
||||
if (e.key === 'ArrowUp') {
|
||||
this.props.speedUp();
|
||||
}
|
||||
};
|
||||
|
||||
forthTenSeconds = () => {
|
||||
const { time, endTime, jump, skipInterval } = this.props;
|
||||
jump(Math.min(endTime, time + SKIP_INTERVALS[skipInterval]));
|
||||
};
|
||||
|
||||
backTenSeconds = () => {
|
||||
//shouldComponentUpdate
|
||||
const { time, jump, skipInterval } = this.props;
|
||||
jump(Math.max(1, time - SKIP_INTERVALS[skipInterval]));
|
||||
};
|
||||
|
||||
goLive = () => this.props.jump(this.props.endTime);
|
||||
|
||||
renderPlayBtn = () => {
|
||||
const { completed, playing } = this.props;
|
||||
let label;
|
||||
let icon;
|
||||
if (completed) {
|
||||
icon = 'arrow-clockwise';
|
||||
label = 'Replay this session';
|
||||
} else if (playing) {
|
||||
icon = 'pause-fill';
|
||||
label = 'Pause';
|
||||
} else {
|
||||
icon = 'play-fill-new';
|
||||
label = 'Pause';
|
||||
label = 'Play';
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={label}
|
||||
className="mr-4"
|
||||
>
|
||||
<div
|
||||
onClick={this.props.togglePlay}
|
||||
className="hover-main color-main cursor-pointer rounded hover:bg-gray-light-shade"
|
||||
>
|
||||
<Icon name={icon} size="36" color="inherit" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
controlIcon = (icon, size, action, isBackwards, additionalClasses) => (
|
||||
<div
|
||||
onClick={action}
|
||||
className={cn('py-2 px-2 hover-main cursor-pointer bg-gray-lightest', additionalClasses)}
|
||||
style={{ transform: isBackwards ? 'rotate(180deg)' : '' }}
|
||||
>
|
||||
<Icon name={icon} size={size} color="inherit" />
|
||||
</div>
|
||||
);
|
||||
|
||||
render() {
|
||||
const {
|
||||
bottomBlock,
|
||||
toggleBottomBlock,
|
||||
live,
|
||||
livePlay,
|
||||
skip,
|
||||
speed,
|
||||
disabled,
|
||||
logRedCount,
|
||||
showExceptions,
|
||||
resourceRedCount,
|
||||
fetchRedCount,
|
||||
showStack,
|
||||
stackRedCount,
|
||||
showStorage,
|
||||
storageType,
|
||||
showProfiler,
|
||||
showGraphql,
|
||||
fullscreen,
|
||||
inspectorMode,
|
||||
closedLive,
|
||||
toggleSpeed,
|
||||
toggleSkip,
|
||||
liveTimeTravel,
|
||||
changeSkipInterval,
|
||||
skipInterval,
|
||||
} = this.props;
|
||||
|
||||
const toggleBottomTools = (blockName) => {
|
||||
if (blockName === INSPECTOR) {
|
||||
toggleInspectorMode();
|
||||
bottomBlock && toggleBottomBlock();
|
||||
} else {
|
||||
toggleInspectorMode(false);
|
||||
toggleBottomBlock(blockName);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.controls}>
|
||||
<Timeline
|
||||
live={live}
|
||||
jump={this.props.jump}
|
||||
liveTimeTravel={liveTimeTravel}
|
||||
pause={this.props.pause}
|
||||
togglePlay={this.props.togglePlay}
|
||||
/>
|
||||
{!fullscreen && (
|
||||
<div className={cn(styles.buttons, { '!px-5 !pt-0': live })} data-is-live={live}>
|
||||
<div className="flex items-center">
|
||||
{!live && (
|
||||
<>
|
||||
<PlayerControls
|
||||
live={live}
|
||||
skip={skip}
|
||||
speed={speed}
|
||||
disabled={disabled}
|
||||
backTenSeconds={this.backTenSeconds}
|
||||
forthTenSeconds={this.forthTenSeconds}
|
||||
toggleSpeed={toggleSpeed}
|
||||
toggleSkip={toggleSkip}
|
||||
playButton={this.renderPlayBtn()}
|
||||
controlIcon={this.controlIcon}
|
||||
ref={this.speedRef}
|
||||
skipIntervals={SKIP_INTERVALS}
|
||||
setSkipInterval={changeSkipInterval}
|
||||
currentInterval={skipInterval}
|
||||
/>
|
||||
<div className={cn('mx-2')} />
|
||||
<XRayButton
|
||||
isActive={bottomBlock === OVERVIEW && !inspectorMode}
|
||||
onClick={() => toggleBottomTools(OVERVIEW)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{live && !closedLive && (
|
||||
<div className={styles.buttonsLeft}>
|
||||
<LiveTag isLive={livePlay} onClick={() => (livePlay ? null : jumpToLive())} />
|
||||
<div className="font-semibold px-2">
|
||||
<AssistDuration isLivePlay={livePlay} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center h-full">
|
||||
<ControlButton
|
||||
disabled={disabled && !inspectorMode}
|
||||
onClick={() => toggleBottomTools(CONSOLE)}
|
||||
active={bottomBlock === CONSOLE && !inspectorMode}
|
||||
label="CONSOLE"
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
hasErrors={logRedCount > 0 || showExceptions}
|
||||
containerClassName="mx-2"
|
||||
/>
|
||||
{!live && (
|
||||
<ControlButton
|
||||
disabled={disabled && !inspectorMode}
|
||||
onClick={() => toggleBottomTools(NETWORK)}
|
||||
active={bottomBlock === NETWORK && !inspectorMode}
|
||||
label="NETWORK"
|
||||
hasErrors={resourceRedCount > 0 || fetchRedCount > 0}
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
containerClassName="mx-2"
|
||||
/>
|
||||
)}
|
||||
{!live && (
|
||||
<ControlButton
|
||||
disabled={disabled && !inspectorMode}
|
||||
onClick={() => toggleBottomTools(PERFORMANCE)}
|
||||
active={bottomBlock === PERFORMANCE && !inspectorMode}
|
||||
label="PERFORMANCE"
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
containerClassName="mx-2"
|
||||
/>
|
||||
)}
|
||||
{!live && showGraphql && (
|
||||
<ControlButton
|
||||
disabled={disabled && !inspectorMode}
|
||||
onClick={() => toggleBottomTools(GRAPHQL)}
|
||||
active={bottomBlock === GRAPHQL && !inspectorMode}
|
||||
label="GRAPHQL"
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
containerClassName="mx-2"
|
||||
/>
|
||||
)}
|
||||
{!live && showStorage && (
|
||||
<ControlButton
|
||||
disabled={disabled && !inspectorMode}
|
||||
onClick={() => toggleBottomTools(STORAGE)}
|
||||
active={bottomBlock === STORAGE && !inspectorMode}
|
||||
label={getStorageName(storageType)}
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
containerClassName="mx-2"
|
||||
/>
|
||||
)}
|
||||
{!live && (
|
||||
<ControlButton
|
||||
disabled={disabled && !inspectorMode}
|
||||
onClick={() => toggleBottomTools(STACKEVENTS)}
|
||||
active={bottomBlock === STACKEVENTS && !inspectorMode}
|
||||
label="EVENTS"
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
containerClassName="mx-2"
|
||||
hasErrors={stackRedCount > 0}
|
||||
/>
|
||||
)}
|
||||
{!live && showProfiler && (
|
||||
<ControlButton
|
||||
disabled={disabled && !inspectorMode}
|
||||
onClick={() => toggleBottomTools(PROFILER)}
|
||||
active={bottomBlock === PROFILER && !inspectorMode}
|
||||
label="PROFILER"
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
containerClassName="mx-2"
|
||||
/>
|
||||
)}
|
||||
{!live && (
|
||||
<Tooltip title="Fullscreen" delay={0} position="top-end" className="mx-4">
|
||||
{this.controlIcon(
|
||||
'arrows-angle-extend',
|
||||
16,
|
||||
this.props.fullscreenOn,
|
||||
false,
|
||||
'rounded hover:bg-gray-light-shade color-gray-medium'
|
||||
)}
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import { PlayerContext } from 'App/components/Session/playerContext';
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import Issue from "Types/session/issue";
|
||||
|
||||
function getTimelinePosition(value: number, scale: number) {
|
||||
const pos = value * scale;
|
||||
|
|
@ -19,7 +20,14 @@ function getTimelinePosition(value: number, scale: number) {
|
|||
return pos > 100 ? 99 : pos;
|
||||
}
|
||||
|
||||
function Timeline(props) {
|
||||
interface IProps {
|
||||
issues: Issue[]
|
||||
setTimelineHoverTime: (t: number) => void
|
||||
startedAt: number
|
||||
tooltipVisible: boolean
|
||||
}
|
||||
|
||||
function Timeline(props: IProps) {
|
||||
const { player, store } = useContext(PlayerContext)
|
||||
const [wasPlaying, setWasPlaying] = useState(false)
|
||||
const { notesStore, settingsStore } = useStore();
|
||||
|
|
@ -35,17 +43,17 @@ function Timeline(props) {
|
|||
live,
|
||||
liveTimeTravel,
|
||||
} = store.get()
|
||||
const { issues } = props;
|
||||
const notes = notesStore.sessionNotes
|
||||
|
||||
const progressRef = useRef<HTMLDivElement>()
|
||||
const timelineRef = useRef<HTMLDivElement>()
|
||||
const progressRef = useRef<HTMLDivElement>(null)
|
||||
const timelineRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
|
||||
const scale = 100 / endTime;
|
||||
|
||||
useEffect(() => {
|
||||
const { issues } = props;
|
||||
const firstIssue = issues.get(0);
|
||||
const firstIssue = issues[0];
|
||||
|
||||
if (firstIssue && skipToIssue) {
|
||||
player.jump(firstIssue.time);
|
||||
|
|
@ -64,7 +72,7 @@ function Timeline(props) {
|
|||
};
|
||||
|
||||
const onDrag: OnDragCallback = (offset) => {
|
||||
if (live && !liveTimeTravel) return;
|
||||
if ((live && !liveTimeTravel) || !progressRef.current) return;
|
||||
|
||||
const p = (offset.x) / progressRef.current.offsetWidth;
|
||||
const time = Math.max(Math.round(p * endTime), 0);
|
||||
|
|
@ -76,7 +84,7 @@ function Timeline(props) {
|
|||
}
|
||||
};
|
||||
|
||||
const getLiveTime = (e) => {
|
||||
const getLiveTime = (e: React.MouseEvent) => {
|
||||
const duration = new Date().getTime() - props.startedAt;
|
||||
const p = e.nativeEvent.offsetX / e.target.offsetWidth;
|
||||
const time = Math.max(Math.round(p * duration), 0);
|
||||
|
|
@ -84,7 +92,7 @@ function Timeline(props) {
|
|||
return [time, duration];
|
||||
};
|
||||
|
||||
const showTimeTooltip = (e) => {
|
||||
const showTimeTooltip = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (e.target !== progressRef.current && e.target !== timelineRef.current) {
|
||||
return props.tooltipVisible && hideTimeTooltip();
|
||||
}
|
||||
|
|
@ -118,13 +126,13 @@ function Timeline(props) {
|
|||
debouncedTooltipChange(timeLineTooltip);
|
||||
};
|
||||
|
||||
const seekProgress = (e) => {
|
||||
const seekProgress = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const time = getTime(e);
|
||||
player.jump(time);
|
||||
hideTimeTooltip();
|
||||
};
|
||||
|
||||
const loadAndSeek = async (e) => {
|
||||
const loadAndSeek = async (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.persist();
|
||||
await player.toggleTimetravel();
|
||||
|
||||
|
|
@ -133,7 +141,7 @@ function Timeline(props) {
|
|||
});
|
||||
};
|
||||
|
||||
const jumpToTime: React.MouseEventHandler = (e) => {
|
||||
const jumpToTime = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (live && !liveTimeTravel) {
|
||||
loadAndSeek(e);
|
||||
} else {
|
||||
|
|
@ -141,7 +149,7 @@ function Timeline(props) {
|
|||
}
|
||||
};
|
||||
|
||||
const getTime = (e: React.MouseEvent, customEndTime?: number) => {
|
||||
const getTime = (e: React.MouseEvent<HTMLDivElement>, customEndTime?: number) => {
|
||||
const p = e.nativeEvent.offsetX / e.target.offsetWidth;
|
||||
const targetTime = customEndTime || endTime;
|
||||
const time = Math.max(Math.round(p * targetTime), 0);
|
||||
|
|
@ -161,7 +169,7 @@ function Timeline(props) {
|
|||
>
|
||||
<div
|
||||
className={stl.progress}
|
||||
onClick={ready ? jumpToTime : null }
|
||||
onClick={ready ? jumpToTime : undefined }
|
||||
ref={progressRef}
|
||||
role="button"
|
||||
onMouseMoveCapture={showTimeTooltip}
|
||||
|
|
@ -197,11 +205,19 @@ function Timeline(props) {
|
|||
|
||||
{events.map((e) => (
|
||||
<div
|
||||
/*@ts-ignore TODO */
|
||||
key={e.key}
|
||||
className={stl.event}
|
||||
style={{ left: `${getTimelinePosition(e.time, scale)}%` }}
|
||||
/>
|
||||
))}
|
||||
{issues.map((i: Issue) => (
|
||||
<div
|
||||
key={i.key}
|
||||
className={stl.redEvent}
|
||||
style={{ left: `${getTimelinePosition(i.time, scale)}%` }}
|
||||
/>
|
||||
))}
|
||||
{notes.map((note) => note.timestamp > 0 ? (
|
||||
<div
|
||||
key={note.noteId}
|
||||
|
|
@ -224,7 +240,7 @@ function Timeline(props) {
|
|||
}
|
||||
|
||||
export default connect(
|
||||
(state) => ({
|
||||
(state: any) => ({
|
||||
issues: state.getIn(['sessions', 'current', 'issues']),
|
||||
startedAt: state.getIn(['sessions', 'current', 'startedAt']),
|
||||
tooltipVisible: state.getIn(['sessions', 'timeLineTooltip', 'isVisible']),
|
||||
|
|
|
|||
|
|
@ -73,6 +73,22 @@
|
|||
.event.location {
|
||||
background: $blue;
|
||||
} */
|
||||
.redEvent {
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: 10px;
|
||||
background: $red;
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
/* top: 0; */
|
||||
/* bottom: 0; */
|
||||
/* &:hover {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-left: -6px;
|
||||
z-index: 1;
|
||||
};*/
|
||||
}
|
||||
|
||||
.markup {
|
||||
position: absolute;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { connect } from 'react-redux';
|
|||
import { findDOMNode } from 'react-dom';
|
||||
import cn from 'classnames';
|
||||
import { EscapeButton } from 'UI';
|
||||
import { hide as hideTargetDefiner } from 'Duck/components/targetDefiner';
|
||||
import {
|
||||
NONE,
|
||||
CONSOLE,
|
||||
|
|
@ -116,7 +115,6 @@ export default connect((state) => {
|
|||
};
|
||||
},
|
||||
{
|
||||
hideTargetDefiner,
|
||||
fullscreenOff,
|
||||
updateLastPlayedSession,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -179,9 +179,9 @@ export default class TimeTable extends React.PureComponent<Props, State> {
|
|||
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>}
|
||||
{columns.map((column, key) => (
|
||||
<div key={column.label.replace(' ', '')} className={stl.cell} style={{ width: `${column.width}px` }}>
|
||||
{column.render ? column.render(row) : row[column.dataKey || ''] || <i className="color-gray-light">{'empty'}</i>}
|
||||
</div>
|
||||
))}
|
||||
<div className={cn('relative flex-1 flex', stl.timeBarWrapper)}>
|
||||
|
|
@ -262,7 +262,7 @@ export default class TimeTable extends React.PureComponent<Props, State> {
|
|||
<div className={stl.headers}>
|
||||
<div className={stl.infoHeaders}>
|
||||
{columns.map(({ label, width }) => (
|
||||
<div className={stl.headerCell} style={{ width: `${width}px` }}>
|
||||
<div key={label.replace(' ', '')} className={stl.headerCell} style={{ width: `${width}px` }}>
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -282,14 +282,15 @@ export default class TimeTable extends React.PureComponent<Props, State> {
|
|||
{timeColumns.map((_, index) => (
|
||||
<div key={`tc-${index}`} className={stl.timeCell} />
|
||||
))}
|
||||
{visibleRefLines.map(({ time, color, onClick }) => (
|
||||
{visibleRefLines.map((line, key) => (
|
||||
<div
|
||||
className={cn(stl.refLine, `bg-${color}`)}
|
||||
key={line.time+key}
|
||||
className={cn(stl.refLine, `bg-${line.color}`)}
|
||||
style={{
|
||||
left: `${percentOf(time - timestart, timewidth)}%`,
|
||||
cursor: typeof onClick === 'function' ? 'click' : 'auto',
|
||||
left: `${percentOf(line.time - timestart, timewidth)}%`,
|
||||
cursor: typeof line.onClick === 'function' ? 'click' : 'auto',
|
||||
}}
|
||||
onClick={onClick}
|
||||
onClick={line.onClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ export default class TimeTable extends React.PureComponent<Props, State> {
|
|||
{columns
|
||||
.filter((i: any) => !i.hidden)
|
||||
.map(({ dataKey, render, width, label }) => (
|
||||
<div key={parseInt(label, 36)} className={stl.cell} style={{ width: `${width}px` }}>
|
||||
<div key={parseInt(label.replace(' ', '')+dataKey, 36)} className={stl.cell} style={{ width: `${width}px` }}>
|
||||
{render
|
||||
? render(row)
|
||||
: row[dataKey || ''] || <i className="color-gray-light">{'empty'}</i>}
|
||||
|
|
@ -327,7 +327,7 @@ export default class TimeTable extends React.PureComponent<Props, State> {
|
|||
<div className={stl.infoHeaders}>
|
||||
{columns.map(({ label, width, dataKey, onClick = null }) => (
|
||||
<div
|
||||
key={parseInt(label, 36)}
|
||||
key={parseInt(label.replace(' ', ''), 36)}
|
||||
className={cn(stl.headerCell, 'flex items-center select-none', {
|
||||
'cursor-pointer': typeof onClick === 'function',
|
||||
})}
|
||||
|
|
@ -355,6 +355,7 @@ export default class TimeTable extends React.PureComponent<Props, State> {
|
|||
))}
|
||||
{visibleRefLines.map(({ time, color, onClick }) => (
|
||||
<div
|
||||
key={time}
|
||||
className={cn(stl.refLine, `bg-${color}`)}
|
||||
style={{
|
||||
left: `${percentOf(time - timestart, timewidth)}%`,
|
||||
|
|
|
|||
|
|
@ -27,11 +27,6 @@ export function createItemInListUpdater(idKey = 'id', shouldAdd = true) {
|
|||
}
|
||||
}
|
||||
|
||||
export function createItemInListFilter(idKey = 'id') {
|
||||
return id =>
|
||||
list => list.filter(item => item[ idKey ] !== id)
|
||||
}
|
||||
|
||||
export const request = type => `${ type }_REQUEST`;
|
||||
export const success = type => `${ type }_SUCCESS`;
|
||||
export const failure = type => `${ type }_FAILURE`;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import jwt from './jwt';
|
|||
import user from './user';
|
||||
import sessions from './sessions';
|
||||
import assignments from './assignments';
|
||||
import targetCustom from './targetCustom';
|
||||
import filters from './filters';
|
||||
import funnelFilters from './funnelFilters';
|
||||
import templates from './templates';
|
||||
|
|
@ -31,7 +30,6 @@ const rootReducer = combineReducers({
|
|||
user,
|
||||
sessions,
|
||||
assignments,
|
||||
targetCustom,
|
||||
filters,
|
||||
funnelFilters,
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
import { Map } from 'immutable';
|
||||
import TargetCustom from 'Types/targetCustom';
|
||||
import crudDuckGenerator from 'Duck/tools/crudDuck';
|
||||
import { reduceDucks } from 'Duck/tools';
|
||||
|
||||
|
||||
const crudDuck = crudDuckGenerator('customTarget', TargetCustom, { endpoints: {
|
||||
fetchList: '/targets_temp',
|
||||
save: '/targets_temp',
|
||||
remove: '/targets_temp',
|
||||
}});
|
||||
export const { fetchList, init, edit, save, remove } = crudDuck.actions;
|
||||
export default crudDuck.reducer;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { runInAction, makeAutoObservable, observable } from 'mobx'
|
||||
import { List, Map } from 'immutable';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import { Map } from 'immutable';
|
||||
import { Duration } from 'luxon';
|
||||
|
||||
const HASH_MOD = 1610612741;
|
||||
const HASH_P = 53;
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
import Record from 'Types/Record';
|
||||
import { List } from 'immutable';
|
||||
import Watchdog from 'Types/watchdog'
|
||||
export const issues_types = List([
|
||||
{ 'type': 'all', 'visible': true, 'order': 0, 'name': 'All', 'icon': '' },
|
||||
{ 'type': 'js_exception', 'visible': true, 'order': 1, 'name': 'Errors', 'icon': 'funnel/exclamation-circle' },
|
||||
{ 'type': 'bad_request', 'visible': true, 'order': 2, 'name': 'Bad Requests', 'icon': 'funnel/file-medical-alt' },
|
||||
{ 'type': 'click_rage', 'visible': true, 'order': 3, 'name': 'Click Rage', 'icon': 'funnel/emoji-angry' },
|
||||
{ 'type': 'crash', 'visible': true, 'order': 4, 'name': 'Crashes', 'icon': 'funnel/file-earmark-break' },
|
||||
// { 'type': 'memory', 'visible': true, 'order': 4, 'name': 'High Memory', 'icon': 'funnel/sd-card' },
|
||||
// { 'type': 'vault', 'visible': true, 'order': 5, 'name': 'Vault', 'icon': 'safe' },
|
||||
// { 'type': 'bookmark', 'visible': true, 'order': 5, 'name': 'Bookmarks', 'icon': 'safe' },
|
||||
// { 'type': 'bad_request', 'visible': true, 'order': 1, 'name': 'Bad Requests', 'icon': 'funnel/file-medical-alt' },
|
||||
// { 'type': 'missing_resource', 'visible': true, 'order': 2, 'name': 'Missing Images', 'icon': 'funnel/image' },
|
||||
// { 'type': 'dead_click', 'visible': true, 'order': 4, 'name': 'Dead Clicks', 'icon': 'funnel/dizzy' },
|
||||
// { 'type': 'cpu', 'visible': true, 'order': 6, 'name': 'High CPU', 'icon': 'funnel/cpu' },
|
||||
// { 'type': 'custom', 'visible': false, 'order': 8, 'name': 'Custom', 'icon': 'funnel/exclamation-circle' }
|
||||
]).map(Watchdog)
|
||||
|
||||
export const issues_types_map = {}
|
||||
issues_types.forEach(i => {
|
||||
issues_types_map[i.type] = { type: i.type, visible: i.visible, order: i.order, name: i.name, }
|
||||
});
|
||||
|
||||
export default Record({
|
||||
issueId: undefined,
|
||||
name: '',
|
||||
visible: true,
|
||||
sessionId: undefined,
|
||||
time: undefined,
|
||||
seqIndex: undefined,
|
||||
payload: {},
|
||||
projectId: undefined,
|
||||
type: '',
|
||||
contextString: '',
|
||||
context: '',
|
||||
icon: 'info'
|
||||
}, {
|
||||
idKey: 'issueId',
|
||||
fromJS: ({ type, ...rest }) => ({
|
||||
...rest,
|
||||
type,
|
||||
icon: issues_types_map[type]?.icon,
|
||||
name: issues_types_map[type]?.name,
|
||||
}),
|
||||
});
|
||||
85
frontend/app/types/session/issue.ts
Normal file
85
frontend/app/types/session/issue.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import Record from 'Types/Record';
|
||||
|
||||
const types = {
|
||||
ALL: 'all',
|
||||
JS_EXCEPTION: 'js_exception',
|
||||
BAD_REQUEST: 'bad_request',
|
||||
CRASH: 'crash',
|
||||
CLICK_RAGE: 'click_rage'
|
||||
} as const
|
||||
|
||||
type TypeKeys = keyof typeof types
|
||||
type TypeValues = typeof types[TypeKeys]
|
||||
|
||||
type IssueType = {
|
||||
[issueTypeKey in TypeValues]: { type: issueTypeKey; visible: boolean; order: number; name: string; icon: string };
|
||||
};
|
||||
|
||||
export const issues_types = [
|
||||
{ 'type': types.ALL, 'visible': true, 'order': 0, 'name': 'All', 'icon': '' },
|
||||
{ 'type': types.JS_EXCEPTION, 'visible': true, 'order': 1, 'name': 'Errors', 'icon': 'funnel/exclamation-circle' },
|
||||
{ 'type': types.BAD_REQUEST, 'visible': true, 'order': 2, 'name': 'Bad Requests', 'icon': 'funnel/file-medical-alt' },
|
||||
{ 'type': types.CLICK_RAGE, 'visible': true, 'order': 3, 'name': 'Click Rage', 'icon': 'funnel/emoji-angry' },
|
||||
{ 'type': types.CRASH, 'visible': true, 'order': 4, 'name': 'Crashes', 'icon': 'funnel/file-earmark-break' },
|
||||
// { 'type': 'memory', 'visible': true, 'order': 4, 'name': 'High Memory', 'icon': 'funnel/sd-card' },
|
||||
// { 'type': 'vault', 'visible': true, 'order': 5, 'name': 'Vault', 'icon': 'safe' },
|
||||
// { 'type': 'bookmark', 'visible': true, 'order': 5, 'name': 'Bookmarks', 'icon': 'safe' },
|
||||
// { 'type': 'bad_request', 'visible': true, 'order': 1, 'name': 'Bad Requests', 'icon': 'funnel/file-medical-alt' },
|
||||
// { 'type': 'missing_resource', 'visible': true, 'order': 2, 'name': 'Missing Images', 'icon': 'funnel/image' },
|
||||
// { 'type': 'dead_click', 'visible': true, 'order': 4, 'name': 'Dead Clicks', 'icon': 'funnel/dizzy' },
|
||||
// { 'type': 'cpu', 'visible': true, 'order': 6, 'name': 'High CPU', 'icon': 'funnel/cpu' },
|
||||
// { 'type': 'custom', 'visible': false, 'order': 8, 'name': 'Custom', 'icon': 'funnel/exclamation-circle' }
|
||||
] as const
|
||||
|
||||
const issues_types_map = <IssueType>{}
|
||||
issues_types.forEach((i) => {
|
||||
Object.assign(issues_types_map, {
|
||||
[i.type]: {
|
||||
type: i.type,
|
||||
visible: i.visible,
|
||||
order: i.order,
|
||||
name: i.name,
|
||||
icon: i.icon,
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
export interface IIssue {
|
||||
issueId: string
|
||||
name: string
|
||||
visible: boolean
|
||||
sessionId: string
|
||||
time: number
|
||||
payload: Record<string, any>
|
||||
projectId: string
|
||||
type: TypeValues
|
||||
contextString: string
|
||||
context: string
|
||||
icon: string
|
||||
timestamp: number
|
||||
startedAt: number
|
||||
}
|
||||
|
||||
export default class Issue {
|
||||
issueId: IIssue["issueId"]
|
||||
name: IIssue["name"]
|
||||
visible: IIssue["visible"]
|
||||
sessionId: IIssue["sessionId"]
|
||||
time: IIssue["time"]
|
||||
payload: IIssue["payload"]
|
||||
projectId: IIssue["projectId"]
|
||||
type: IIssue["type"]
|
||||
contextString: IIssue["contextString"]
|
||||
context: IIssue["context"]
|
||||
icon: IIssue["icon"]
|
||||
key: number
|
||||
|
||||
constructor({ type, ...rest }: IIssue & { key: number }) {
|
||||
Object.assign(this, {
|
||||
...rest,
|
||||
type,
|
||||
icon: issues_types_map[type]?.icon,
|
||||
name: issues_types_map[type]?.name
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
import { List } from 'immutable';
|
||||
import Record from 'Types/Record';
|
||||
import { getResourceName } from 'App/utils';
|
||||
|
||||
const XHR = 'xhr';
|
||||
const FETCH = 'fetch';
|
||||
const JS = 'script';
|
||||
const CSS = 'css';
|
||||
const IMG = 'img';
|
||||
const MEDIA = 'media';
|
||||
const OTHER = 'other';
|
||||
|
||||
|
||||
//
|
||||
// const IMG_EXTENTIONS = [ "png", "gif", "jpg", "jpeg", "svg" ];
|
||||
// const MEDIA_EXTENTIONS = [ 'mp4', 'mkv', 'ogg', 'webm', 'avi', 'mp3' ];
|
||||
//
|
||||
// function getResourceType(type, initiator, url) {
|
||||
// if (type === 'xmlhttprequest') return XHR; // bad?
|
||||
// if (type !== undefined) return type;
|
||||
// if (initiator === 'xmlhttprequest' || initiator === 'fetch') return XHR;
|
||||
// if (initiator === 'img') return IMG;
|
||||
// const pathnameSplit = new URL(url).pathname.split('.');
|
||||
// if (pathnameSplit.length > 1) {
|
||||
// const extention = pathnameSplit.pop();
|
||||
// if (extention === 'css') return CSS;
|
||||
// if (extention === 'js') return JS;
|
||||
// if (IMG_EXTENTIONS.includes(extention)) return IMG
|
||||
// if (MEDIA_EXTENTIONS.includes(extention)) return MEDIA;
|
||||
// }
|
||||
// return OTHER;
|
||||
// }
|
||||
|
||||
const TYPES_MAP = {
|
||||
"stylesheet": CSS,
|
||||
}
|
||||
|
||||
function getResourceStatus(status, success) {
|
||||
if (status != null) return String(status);
|
||||
if (typeof success === 'boolean' || typeof success === 'number') {
|
||||
return !!success
|
||||
? '2xx-3xx'
|
||||
: '4xx-5xx';
|
||||
}
|
||||
return '2xx-3xx';
|
||||
}
|
||||
|
||||
function getResourceSuccess(success, status) {
|
||||
if (success != null) { return !!success }
|
||||
if (status != null) { return status < 400 }
|
||||
return true
|
||||
}
|
||||
|
||||
export const TYPES = {
|
||||
XHR,
|
||||
FETCH,
|
||||
JS,
|
||||
CSS,
|
||||
IMG,
|
||||
MEDIA,
|
||||
OTHER,
|
||||
}
|
||||
|
||||
const YELLOW_BOUND = 10;
|
||||
const RED_BOUND = 80;
|
||||
|
||||
export function isRed(r) {
|
||||
return !r.success || r.score >= RED_BOUND;
|
||||
}
|
||||
export function isYellow(r) {
|
||||
return r.score < RED_BOUND && r.score >= YELLOW_BOUND;
|
||||
}
|
||||
|
||||
export default Record({
|
||||
type: OTHER,
|
||||
url: '',
|
||||
name: '',
|
||||
status: '2xx-3xx',
|
||||
duration: 0,
|
||||
index: undefined,
|
||||
time: undefined,
|
||||
ttfb: 0,
|
||||
timewidth: 0,
|
||||
success: true,
|
||||
score: 0,
|
||||
// initiator: "other",
|
||||
// pagePath: "",
|
||||
method: '',
|
||||
request:'',
|
||||
response: '',
|
||||
headerSize: 0,
|
||||
encodedBodySize: 0,
|
||||
decodedBodySize: 0,
|
||||
responseBodySize: 0,
|
||||
timings: List(),
|
||||
}, {
|
||||
fromJS: ({ type, initiator, status, success, time, datetime, timestamp, timings, ...resource }) => ({
|
||||
...resource,
|
||||
type: TYPES_MAP[type] || type,
|
||||
name: getResourceName(resource.url),
|
||||
status: getResourceStatus(status, success),
|
||||
success: getResourceSuccess(success, status),
|
||||
time: typeof time === 'number' ? time : datetime || timestamp,
|
||||
ttfb: timings && timings.ttfb,
|
||||
timewidth: timings && timings.timewidth,
|
||||
timings,
|
||||
}),
|
||||
name: 'Resource',
|
||||
methods: {
|
||||
isRed() {
|
||||
return isRed(this);
|
||||
},
|
||||
isYellow() {
|
||||
return isYellow(this);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
113
frontend/app/types/session/resource.ts
Normal file
113
frontend/app/types/session/resource.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import Record from 'Types/Record';
|
||||
import { getResourceName } from 'App/utils';
|
||||
|
||||
const XHR = 'xhr' as const;
|
||||
const FETCH = 'fetch' as const;
|
||||
const JS = 'script' as const;
|
||||
const CSS = 'css' as const;
|
||||
const IMG = 'img' as const;
|
||||
const MEDIA = 'media' as const;
|
||||
const OTHER = 'other' as const;
|
||||
|
||||
function getResourceStatus(status: number, success: boolean) {
|
||||
if (status != null) return String(status);
|
||||
if (typeof success === 'boolean' || typeof success === 'number') {
|
||||
return !!success
|
||||
? '2xx-3xx'
|
||||
: '4xx-5xx';
|
||||
}
|
||||
return '2xx-3xx';
|
||||
}
|
||||
|
||||
function getResourceSuccess(success: boolean, status: number) {
|
||||
if (success != null) { return !!success }
|
||||
if (status != null) { return status < 400 }
|
||||
return true
|
||||
}
|
||||
|
||||
export const TYPES = {
|
||||
XHR,
|
||||
FETCH,
|
||||
JS,
|
||||
CSS,
|
||||
IMG,
|
||||
MEDIA,
|
||||
OTHER,
|
||||
"stylesheet": CSS,
|
||||
}
|
||||
|
||||
const YELLOW_BOUND = 10;
|
||||
const RED_BOUND = 80;
|
||||
|
||||
export function isRed(r: IResource) {
|
||||
return !r.success || r.score >= RED_BOUND;
|
||||
}
|
||||
|
||||
interface IResource {
|
||||
type: keyof typeof TYPES,
|
||||
url: string,
|
||||
name: string,
|
||||
status: number,
|
||||
duration: number,
|
||||
index: number,
|
||||
time: number,
|
||||
ttfb: number,
|
||||
timewidth: number,
|
||||
success: boolean,
|
||||
score: number,
|
||||
method: string,
|
||||
request:string,
|
||||
response: string,
|
||||
headerSize: number,
|
||||
encodedBodySize: number,
|
||||
decodedBodySize: number,
|
||||
responseBodySize: number,
|
||||
timings: Record<string, any>
|
||||
datetime: number
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export default class Resource {
|
||||
name = 'Resource'
|
||||
type: IResource["type"]
|
||||
status: string
|
||||
success: IResource["success"]
|
||||
time: IResource["time"]
|
||||
ttfb: IResource["ttfb"]
|
||||
url: IResource["url"]
|
||||
duration: IResource["duration"]
|
||||
index: IResource["index"]
|
||||
timewidth: IResource["timewidth"]
|
||||
score: IResource["score"]
|
||||
method: IResource["method"]
|
||||
request: IResource["request"]
|
||||
response: IResource["response"]
|
||||
headerSize: IResource["headerSize"]
|
||||
encodedBodySize: IResource["encodedBodySize"]
|
||||
decodedBodySize: IResource["decodedBodySize"]
|
||||
responseBodySize: IResource["responseBodySize"]
|
||||
timings: IResource["timings"]
|
||||
|
||||
constructor({ status, success, time, datetime, timestamp, timings, ...resource }: IResource) {
|
||||
|
||||
Object.assign(this, {
|
||||
...resource,
|
||||
name: getResourceName(resource.url),
|
||||
status: getResourceStatus(status, success),
|
||||
success: getResourceSuccess(success, status),
|
||||
time: typeof time === 'number' ? time : datetime || timestamp,
|
||||
ttfb: timings && timings.ttfb,
|
||||
timewidth: timings && timings.timewidth,
|
||||
timings,
|
||||
})
|
||||
}
|
||||
|
||||
isRed() {
|
||||
return !this.success || this.score >= RED_BOUND;
|
||||
}
|
||||
|
||||
isYellow() {
|
||||
return this.score < RED_BOUND && this.score >= YELLOW_BOUND;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -5,7 +5,7 @@ import SessionEvent, { TYPES } from './event';
|
|||
import StackEvent from './stackEvent';
|
||||
import Resource from './resource';
|
||||
import SessionError from './error';
|
||||
import Issue from './issue';
|
||||
import Issue, { IIssue } from './issue';
|
||||
|
||||
const HASH_MOD = 1610612741;
|
||||
const HASH_P = 53;
|
||||
|
|
@ -32,8 +32,8 @@ export default Record(
|
|||
duration: 0,
|
||||
events: List(),
|
||||
stackEvents: List(),
|
||||
resources: List(),
|
||||
missedResources: List(),
|
||||
resources: [],
|
||||
missedResources: [],
|
||||
metadata: Map(),
|
||||
favorite: false,
|
||||
filterId: '',
|
||||
|
|
@ -113,10 +113,11 @@ export default Record(
|
|||
.map((e) => SessionEvent({ ...e, time: e.timestamp - startedAt }))
|
||||
.filter(({ type, time }) => type !== TYPES.CONSOLE && time <= durationSeconds);
|
||||
|
||||
let resources = List(session.resources).map(Resource);
|
||||
resources = resources
|
||||
.map((r) => r.set('time', Math.max(0, r.time - startedAt)))
|
||||
.sort((r1, r2) => r1.time - r2.time);
|
||||
let resources = List(session.resources).map((r) => new Resource(r as any));
|
||||
resources.forEach((r: Resource) => {
|
||||
r.time = Math.max(0, r.time - startedAt)
|
||||
})
|
||||
resources = resources.sort((r1, r2) => r1.time - r2.time);
|
||||
const missedResources = resources.filter(({ success }) => !success);
|
||||
|
||||
const stackEventsList = List(stackEvents)
|
||||
|
|
@ -125,7 +126,7 @@ export default Record(
|
|||
.map((se) => StackEvent({ ...se, time: se.timestamp - startedAt }));
|
||||
const exceptions = List(errors).map(SessionError);
|
||||
|
||||
const issuesList = List(issues).map((e) => Issue({ ...e, time: e.timestamp - startedAt }));
|
||||
const issuesList = (issues as IIssue[]).map((i, k) => new Issue({ ...i, time: i.timestamp - startedAt, key: k }));
|
||||
|
||||
const rawEvents = !session.events
|
||||
? []
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue