change(ui): rewrite issue and resource types

This commit is contained in:
sylenien 2022-12-29 16:01:21 +01:00
parent 22cdb4f4e7
commit 2a714c0145
17 changed files with 275 additions and 663 deletions

View file

@ -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);

View file

@ -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))

View file

@ -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>
);
}
}

View file

@ -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']),

View file

@ -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;

View file

@ -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,
}

View file

@ -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>

View file

@ -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)}%`,

View file

@ -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`;

View file

@ -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,

View file

@ -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;

View file

@ -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;

View file

@ -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,
}),
});

View 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
})
}
}

View file

@ -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);
}
}
});

View 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;
}
}

View file

@ -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
? []