From fbbd69732e4f8d9b047e2254f4c5a903eabf4d66 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Wed, 20 Jul 2022 18:16:02 +0200 Subject: [PATCH 01/16] feat(ui) - timeline overview - wip --- .../Session_/BottomBlock/BottomBlock.js | 6 +- .../Session_/OverviewPanel/OverviewPanel.tsx | 116 ++++++++++++++++++ .../components/EventRow/EventRow.tsx | 39 ++++++ .../components/EventRow/index.ts | 1 + .../Session_/OverviewPanel/index.ts | 1 + .../OverviewPanel/overviewPanel.module.css | 13 ++ .../Session_/Player/Controls/Controls.js | 12 ++ .../app/components/Session_/Player/Player.js | 5 + .../app/components/Session_/PlayerBlock.js | 7 +- frontend/app/duck/components/player.js | 1 + frontend/app/utils.ts | 13 +- 11 files changed, 206 insertions(+), 8 deletions(-) create mode 100644 frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx create mode 100644 frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx create mode 100644 frontend/app/components/Session_/OverviewPanel/components/EventRow/index.ts create mode 100644 frontend/app/components/Session_/OverviewPanel/index.ts create mode 100644 frontend/app/components/Session_/OverviewPanel/overviewPanel.module.css diff --git a/frontend/app/components/Session_/BottomBlock/BottomBlock.js b/frontend/app/components/Session_/BottomBlock/BottomBlock.js index 39983c0c1..069757e60 100644 --- a/frontend/app/components/Session_/BottomBlock/BottomBlock.js +++ b/frontend/app/components/Session_/BottomBlock/BottomBlock.js @@ -3,9 +3,9 @@ import cn from 'classnames'; import stl from './bottomBlock.module.css'; const BottomBlock = ({ - children, - className, - additionalHeight, + children = null, + className = '', + additionalHeight = 0, ...props }) => (
diff --git a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx new file mode 100644 index 000000000..d634643b0 --- /dev/null +++ b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx @@ -0,0 +1,116 @@ +import { connectPlayer } from 'App/player'; +import React from 'react'; +import BottomBlock from '../BottomBlock'; +import EventRow from './components/EventRow'; +import { TYPES } from 'Types/session/event'; +import { Icon } from 'UI'; +import { Tooltip } from 'react-tippy'; +import stl from './overviewPanel.module.css'; + +interface Props { + resourceList: any[]; + exceptionsList: any[]; + eventsList: any[]; + endTime: number; +} +function OverviewPanel(props: Props) { + const { resourceList, exceptionsList, eventsList, endTime } = props; + const clickRageList = React.useMemo(() => { + return eventsList.filter((item: any) => item.type === TYPES.CLICKRAGE); + }, [eventsList]); + + const containerRef = React.useRef(null); + const innerRef = React.createRef(); + const scale = 100 / endTime; + + let width = 100; + const SPEED = 5; + + const onWheel = (e: React.UIEvent) => { + e.preventDefault(); + e.stopPropagation(); + const delta = e.deltaY; + if (delta > 0) { + width += SPEED; + } else { + width -= SPEED; + } + if (width < 100) { + width = 100; + } + if (innerRef.current) { + innerRef.current.style.width = width + '%'; + if (containerRef.current) { + containerRef.current.style.left = (100 - width) / 2 + '%'; + } + } + }; + + const renderNetworkElement = (item: any) => { + return
; + }; + + const renderClickRageElement = (item: any) => { + return ( +
+ +
+ ); + }; + + const renderExceptionElement = (item: any) => { + // console.log('item', item); + return ( + + {'Exception'} +
+ {item.message} +
+ } + delay={0} + position="top" + > + + + ); + }; + + return ( + + +
+ Overview +
+
+ +
+
+ + +
+ +
+ +
+
+
+
+ ); +} + +export default connectPlayer((state: any) => ({ + resourceList: state.resourceList, + exceptionsList: state.exceptionsList, + eventsList: state.eventList, + endTime: state.endTime, +}))(OverviewPanel); + +const VerticalPointerLine = connectPlayer((state: any) => ({ + time: state.time, + scale: 100 / state.endTime, +}))(({ time, scale }: any) => { + const left = time * scale; + return
; +}); diff --git a/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx b/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx new file mode 100644 index 000000000..b1fec7cec --- /dev/null +++ b/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import cn from 'classnames' +import { getTimelinePosition } from 'App/utils'; + +interface Props { + list?: any[]; + scale?: number; + title: string; + className?: string; + renderElement?: (item: any) => React.ReactNode; +} +function EventRow(props: Props) { + const { title, className, list = [], scale = 0 } = props; + const _list = React.useMemo(() => { + return list.map((item: any, _index: number) => { + return { + ...item.toJS(), + left: getTimelinePosition(item.time, scale), + } + }) + }, [list]); + return ( +
+
{title}
+
+ {_list.map((item: any, index: number) => { + return ( +
+ {props.renderElement ? props.renderElement(item) : null} +
+ ) + } + )} +
+
+ ); +} + +export default EventRow; \ No newline at end of file diff --git a/frontend/app/components/Session_/OverviewPanel/components/EventRow/index.ts b/frontend/app/components/Session_/OverviewPanel/components/EventRow/index.ts new file mode 100644 index 000000000..ec0281d5a --- /dev/null +++ b/frontend/app/components/Session_/OverviewPanel/components/EventRow/index.ts @@ -0,0 +1 @@ +export { default } from './EventRow'; \ No newline at end of file diff --git a/frontend/app/components/Session_/OverviewPanel/index.ts b/frontend/app/components/Session_/OverviewPanel/index.ts new file mode 100644 index 000000000..328795cd7 --- /dev/null +++ b/frontend/app/components/Session_/OverviewPanel/index.ts @@ -0,0 +1 @@ +export { default } from './OverviewPanel'; \ No newline at end of file diff --git a/frontend/app/components/Session_/OverviewPanel/overviewPanel.module.css b/frontend/app/components/Session_/OverviewPanel/overviewPanel.module.css new file mode 100644 index 000000000..979eebb13 --- /dev/null +++ b/frontend/app/components/Session_/OverviewPanel/overviewPanel.module.css @@ -0,0 +1,13 @@ +.popup { + max-width: 300px !important; + /* max-height: 300px !important; */ + overflow: hidden; + text-overflow: ellipsis; + & span { + display: block; + max-height: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} diff --git a/frontend/app/components/Session_/Player/Controls/Controls.js b/frontend/app/components/Session_/Player/Controls/Controls.js index e92099393..0aa33283d 100644 --- a/frontend/app/components/Session_/Player/Controls/Controls.js +++ b/frontend/app/components/Session_/Player/Controls/Controls.js @@ -15,6 +15,7 @@ import { fullscreenOn, fullscreenOff, toggleBottomBlock, + OVERVIEW, CONSOLE, NETWORK, STACKEVENTS, @@ -377,6 +378,17 @@ export default class Controls extends React.Component { containerClassName="mx-2" /> )} */} + toggleBottomTools(OVERVIEW) } + active={ bottomBlock === OVERVIEW && !inspectorMode} + label="OVERVIEW" + noIcon + labelClassName="!text-base font-semibold" + // count={ logCount } + // hasErrors={ logRedCount > 0 } + containerClassName="mx-2" + /> toggleBottomTools(CONSOLE) } diff --git a/frontend/app/components/Session_/Player/Player.js b/frontend/app/components/Session_/Player/Player.js index 4b5006338..6d7089d77 100644 --- a/frontend/app/components/Session_/Player/Player.js +++ b/frontend/app/components/Session_/Player/Player.js @@ -18,6 +18,7 @@ import { EXCEPTIONS, LONGTASKS, INSPECTOR, + OVERVIEW, } from 'Duck/components/player'; import Network from '../Network'; import Console from '../Console/Console'; @@ -40,6 +41,7 @@ import Controls from './Controls'; import Overlay from './Overlay'; import stl from './player.module.css'; import { updateLastPlayedSession } from 'Duck/sessions'; +import OverviewPanel from '../OverviewPanel'; @connectPlayer(state => ({ live: state.live, @@ -104,6 +106,9 @@ export default class Player extends React.PureComponent {
{ !fullscreen && !!bottomBlock &&
+ { //bottomBlock === OVERVIEW && + + } { bottomBlock === CONSOLE && } diff --git a/frontend/app/components/Session_/PlayerBlock.js b/frontend/app/components/Session_/PlayerBlock.js index e6bcbaa33..487809649 100644 --- a/frontend/app/components/Session_/PlayerBlock.js +++ b/frontend/app/components/Session_/PlayerBlock.js @@ -3,7 +3,7 @@ import cn from "classnames"; import { connect } from 'react-redux'; import { } from 'Player'; import { - NONE, + NONE, OVERVIEW, } from 'Duck/components/player'; import Player from './Player'; import SubHeader from './Subheader'; @@ -37,8 +37,9 @@ export default class PlayerBlock extends React.PureComponent { />} diff --git a/frontend/app/duck/components/player.js b/frontend/app/duck/components/player.js index 1a34bcd95..cf6263bf4 100644 --- a/frontend/app/duck/components/player.js +++ b/frontend/app/duck/components/player.js @@ -12,6 +12,7 @@ export const FETCH = 8; export const EXCEPTIONS = 9; export const LONGTASKS = 10; export const INSPECTOR = 11; +export const OVERVIEW = 12; const TOGGLE_FULLSCREEN = 'player/TOGGLE_FS'; const TOGGLE_BOTTOM_BLOCK = 'player/SET_BOTTOM_BLOCK'; diff --git a/frontend/app/utils.ts b/frontend/app/utils.ts index 9765d69c3..53d7375ad 100644 --- a/frontend/app/utils.ts +++ b/frontend/app/utils.ts @@ -324,8 +324,12 @@ export const fetchErrorCheck = async (response: any) => { export const cleanSessionFilters = (data: any) => { const { filters, ...rest } = data; const _fitlers = filters.filter((f: any) => { - if (f.operator === 'isAny' || f.operator === 'onAny') { return true } // ignore filter with isAny/onAny operator - if (Array.isArray(f.filters) && f.filters.length > 0) { return true } // ignore subfilters + if (f.operator === 'isAny' || f.operator === 'onAny') { + return true; + } // ignore filter with isAny/onAny operator + if (Array.isArray(f.filters) && f.filters.length > 0) { + return true; + } // ignore subfilters return f.value !== '' && Array.isArray(f.value) && f.value.length > 0; }); @@ -343,3 +347,8 @@ export const setSessionFilter = (filter: any) => { export const compareJsonObjects = (obj1: any, obj2: any) => { return JSON.stringify(obj1) === JSON.stringify(obj2); }; + +export function getTimelinePosition(value: any, scale: any) { + const pos = value * scale; + return pos > 100 ? 100 : pos; +} From b368dc3adf30b08714ae3d216ac600fe7cdb6385 Mon Sep 17 00:00:00 2001 From: rjshrjndrn Date: Thu, 21 Jul 2022 12:06:40 +0200 Subject: [PATCH 02/16] TODO: revert in 1.8.0 force run http as root, as v1.7.0 http listens in 80 Signed-off-by: rjshrjndrn --- scripts/helmcharts/openreplay/values.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts/helmcharts/openreplay/values.yaml b/scripts/helmcharts/openreplay/values.yaml index 65b67c6bc..1652dedab 100644 --- a/scripts/helmcharts/openreplay/values.yaml +++ b/scripts/helmcharts/openreplay/values.yaml @@ -92,6 +92,13 @@ nginx-ingress: # Overrides the image tag whose default is the chart appVersion. tag: "buster" +# By default http listens to 80 port, and for v1.7.0 http listens in 80 +http: + podSecurityContext: + runAsUser: 0 + runAsGroup: 0 + fsGroup: 0 + fsGroupChangePolicy: "OnRootMismatch" # Running sink and storage as non root users, because of existing volume permission change will take time sink: podSecurityContext: From a4f0b323c50394cbca741ac6fb944a81098b95a5 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Thu, 21 Jul 2022 15:35:01 +0200 Subject: [PATCH 03/16] fix(ui) - click maps count and percentage, dropdown --- .../PageInsightsPanel/PageInsightsPanel.tsx | 178 +++++++++--------- .../components/SelectorCard/SelectorCard.tsx | 44 +++-- .../SelectorsList/SelectorsList.tsx | 41 ++-- .../StatedScreen/StatedScreen.ts | 4 +- 4 files changed, 133 insertions(+), 134 deletions(-) diff --git a/frontend/app/components/Session_/PageInsightsPanel/PageInsightsPanel.tsx b/frontend/app/components/Session_/PageInsightsPanel/PageInsightsPanel.tsx index cec3e1af2..03d74a247 100644 --- a/frontend/app/components/Session_/PageInsightsPanel/PageInsightsPanel.tsx +++ b/frontend/app/components/Session_/PageInsightsPanel/PageInsightsPanel.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { Dropdown, Loader, Icon } from 'UI'; -import DateRange from 'Shared/DateRange'; +import { Loader, Icon } from 'UI'; import { connect } from 'react-redux'; import { fetchInsights } from 'Duck/sessions'; import SelectorsList from './components/SelectorsList/SelectorsList'; @@ -11,100 +10,103 @@ import Period from 'Types/app/period'; const JUMP_OFFSET = 1000; interface Props { - filters: any - fetchInsights: (filters: Record) => void - urls: [] - insights: any - events: Array - urlOptions: Array - loading: boolean - host: string - setActiveTab: (tab: string) => void + filters: any; + fetchInsights: (filters: Record) => void; + urls: []; + insights: any; + events: Array; + urlOptions: Array; + loading: boolean; + host: string; + setActiveTab: (tab: string) => void; } -function PageInsightsPanel({ - filters, fetchInsights, events = [], insights, urlOptions, host, loading = true, setActiveTab -}: Props) { - const [insightsFilters, setInsightsFilters] = useState(filters) - const defaultValue = (urlOptions && urlOptions[0]) ? urlOptions[0].value : '' +function PageInsightsPanel({ filters, fetchInsights, events = [], insights, urlOptions, host, loading = true, setActiveTab }: Props) { + const [insightsFilters, setInsightsFilters] = useState(filters); + const defaultValue = urlOptions && urlOptions[0] ? urlOptions[0].value : ''; - const period = new Period({ - start: insightsFilters.startDate, - end: insightsFilters.endDate, - rangeName: insightsFilters.rangeValue - }); + const period = Period({ + start: insightsFilters.startDate, + end: insightsFilters.endDate, + rangeName: insightsFilters.rangeValue, + }); - const onDateChange = (e) => { - const { startDate, endDate, rangeValue } = e.toJSON(); - setInsightsFilters({ ...insightsFilters, startDate, endDate, rangeValue }) - } + const onDateChange = (e: any) => { + const { startDate, endDate, rangeValue } = e.toJSON(); + setInsightsFilters({ ...insightsFilters, startDate, endDate, rangeValue }); + }; - useEffect(() => { - markTargets(insights.toJS()); - return () => { - markTargets(null) - } - }, [insights]) + useEffect(() => { + markTargets(insights.toJS()); + return () => { + markTargets(null); + }; + }, [insights]); - useEffect(() => { - if (urlOptions && urlOptions[0]) { - const url = insightsFilters.url ? insightsFilters.url : host + urlOptions[0].value; - Player.pause(); - fetchInsights({ ...insightsFilters, url }) - } - }, [insightsFilters]) + useEffect(() => { + if (urlOptions && urlOptions[0]) { + const url = insightsFilters.url ? insightsFilters.url : host + urlOptions[0].value; + Player.pause(); + fetchInsights({ ...insightsFilters, url }); + } + }, [insightsFilters]); - const onPageSelect = ({ value }: { value: Array }) => { - const event = events.find(item => item.url === value) - Player.jump(event.time + JUMP_OFFSET) - setInsightsFilters({ ...insightsFilters, url: host + value }) - markTargets([]) - }; + const onPageSelect = ({ value }: any) => { + const event = events.find((item) => item.url === value.value); + Player.jump(event.time + JUMP_OFFSET); + setInsightsFilters({ ...insightsFilters, url: host + value.value }); + markTargets([]); + }; - return ( -
-
-
- Clicks - + return ( +
+
+
+ Clicks + +
+
{ + setActiveTab(''); + }} + className="ml-auto flex items-center justify-center bg-white cursor-pointer" + > + +
+
+
+
In Page
+ -
- - - -
- ) + ); } -export default connect(state => { - const events = state.getIn([ 'sessions', 'visitedEvents' ]) - return { - filters: state.getIn(['sessions', 'insightFilters']), - host: state.getIn([ 'sessions', 'host' ]), - insights: state.getIn([ 'sessions', 'insights' ]), - events: events, - urlOptions: events.map(({ url, host }: any) => ({ label: url, value: url, host })), - loading: state.getIn([ 'sessions', 'fetchInsightsRequest', 'loading' ]), - } -}, { fetchInsights })(PageInsightsPanel); +export default connect( + (state) => { + const events = state.getIn(['sessions', 'visitedEvents']); + return { + filters: state.getIn(['sessions', 'insightFilters']), + host: state.getIn(['sessions', 'host']), + insights: state.getIn(['sessions', 'insights']), + events: events, + urlOptions: events.map(({ url, host }: any) => ({ label: url, value: url, host })), + loading: state.getIn(['sessions', 'fetchInsightsRequest', 'loading']), + }; + }, + { fetchInsights } +)(PageInsightsPanel); diff --git a/frontend/app/components/Session_/PageInsightsPanel/components/SelectorCard/SelectorCard.tsx b/frontend/app/components/Session_/PageInsightsPanel/components/SelectorCard/SelectorCard.tsx index 9007b6684..9a3ecc210 100644 --- a/frontend/app/components/Session_/PageInsightsPanel/components/SelectorCard/SelectorCard.tsx +++ b/frontend/app/components/Session_/PageInsightsPanel/components/SelectorCard/SelectorCard.tsx @@ -1,30 +1,34 @@ -import React, { useState } from 'react' -import stl from './SelectorCard.module.css' +import React, { useState } from 'react'; +import stl from './SelectorCard.module.css'; import cn from 'classnames'; import type { MarkedTarget } from 'Player/MessageDistributor/StatedScreen/StatedScreen'; import { activeTarget } from 'Player'; import { Tooltip } from 'react-tippy'; interface Props { - index?: number, - target: MarkedTarget, - showContent: boolean + index?: number; + target: MarkedTarget; + showContent: boolean; } -export default function SelectorCard({ index = 1, target, showContent } : Props) { - return ( -
activeTarget(index)}> -
- {/* @ts-ignore */} -
{index + 1}
-
{target.selector}
-
- { showContent && ( -
-
{target.count} Clicks - {target.percent}%
-
TOTAL CLICKS
+export default function SelectorCard({ index = 1, target, showContent }: Props) { + return ( +
activeTarget(index)}> +
+ {/* @ts-ignore */} + +
{index + 1}
+
+
{target.selector}
+
+ {showContent && ( +
+
+ {target.count} Clicks - {target.percent}% +
+
TOTAL CLICKS
+
+ )}
- ) } -
- ) + ); } diff --git a/frontend/app/components/Session_/PageInsightsPanel/components/SelectorsList/SelectorsList.tsx b/frontend/app/components/Session_/PageInsightsPanel/components/SelectorsList/SelectorsList.tsx index aceefb3b7..86274baba 100644 --- a/frontend/app/components/Session_/PageInsightsPanel/components/SelectorsList/SelectorsList.tsx +++ b/frontend/app/components/Session_/PageInsightsPanel/components/SelectorsList/SelectorsList.tsx @@ -1,33 +1,26 @@ -import React, { useState } from 'react' -import { NoContent } from 'UI' +import React from 'react'; +import { NoContent } from 'UI'; import { connectPlayer } from 'Player/store'; import SelectorCard from '../SelectorCard/SelectorCard'; import type { MarkedTarget } from 'Player/MessageDistributor/StatedScreen/StatedScreen'; -import stl from './selectorList.module.css' +import stl from './selectorList.module.css'; interface Props { - targets: Array, - activeTargetIndex: number + targets: Array; + activeTargetIndex: number; } -function SelectorsList({ targets, activeTargetIndex }: Props) { - return ( - -
- { targets && targets.map((target, index) => ( - - ))} -
-
- ) +function SelectorsList({ targets, activeTargetIndex }: Props) { + return ( + +
+ {targets && targets.map((target, index) => )} +
+
+ ); } - -export default connectPlayer(state => ({ - targets: state.markedTargets, - activeTargetIndex: state.activeTargetIndex, -}))(SelectorsList) +export default connectPlayer((state: any) => ({ + targets: state.markedTargets, + activeTargetIndex: state.activeTargetIndex, +}))(SelectorsList); diff --git a/frontend/app/player/MessageDistributor/StatedScreen/StatedScreen.ts b/frontend/app/player/MessageDistributor/StatedScreen/StatedScreen.ts index 5b74d9a3f..177419f35 100644 --- a/frontend/app/player/MessageDistributor/StatedScreen/StatedScreen.ts +++ b/frontend/app/player/MessageDistributor/StatedScreen/StatedScreen.ts @@ -145,9 +145,9 @@ export default class StatedScreen extends Screen { ...s, el, index: index++, - percent: 0, + percent: Math.round((s.count * 100) / totalCount), boundingRect: this.calculateRelativeBoundingRect(el), - count: Math.round((s.count * 100) / totalCount) + count: s.count, }) }); From 26100b81e67e7e2babfdeadad8e592ef03d8d690 Mon Sep 17 00:00:00 2001 From: sylenien Date: Thu, 21 Jul 2022 16:12:05 +0200 Subject: [PATCH 04/16] fix(ui): fix clickmap card items selectors --- .../MessageDistributor/StatedScreen/Screen/BaseScreen.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/BaseScreen.ts b/frontend/app/player/MessageDistributor/StatedScreen/Screen/BaseScreen.ts index fc52660f5..81ced7774 100644 --- a/frontend/app/player/MessageDistributor/StatedScreen/Screen/BaseScreen.ts +++ b/frontend/app/player/MessageDistributor/StatedScreen/Screen/BaseScreen.ts @@ -138,7 +138,8 @@ export default abstract class BaseScreen { getElementBySelector(selector: string): Element | null { if (!selector) return null; try { - return this.document?.querySelector(selector) || null; + const safeSelector = selector.replace(/:/g, '\\\\3A ').replace(/\//g, '\\/'); + return this.document?.querySelector(safeSelector) || null; } catch (e) { console.error("Can not select element. ", e) return null @@ -186,4 +187,4 @@ export default abstract class BaseScreen { clean() { window.removeEventListener('resize', this.scale); } -} \ No newline at end of file +} From 00a3c52e8db30c5d3795609c4f8a1a2b3744f58b Mon Sep 17 00:00:00 2001 From: Delirium Date: Thu, 21 Jul 2022 17:28:28 +0300 Subject: [PATCH 05/16] fix(ui): fix timezone settings (#629) --- .../SessionList/SessionListHeader.js | 26 ++++++++++++++++--- .../SelectDateRange/SelectDateRange.tsx | 17 +++++++----- .../components/shared/SessionItem/Counter.tsx | 4 +-- frontend/app/types/app/period.js | 11 +++++++- 4 files changed, 46 insertions(+), 12 deletions(-) diff --git a/frontend/app/components/BugFinder/SessionList/SessionListHeader.js b/frontend/app/components/BugFinder/SessionList/SessionListHeader.js index bf4bf8b55..5e2702639 100644 --- a/frontend/app/components/BugFinder/SessionList/SessionListHeader.js +++ b/frontend/app/components/BugFinder/SessionList/SessionListHeader.js @@ -4,7 +4,10 @@ import SortDropdown from '../Filters/SortDropdown'; import { numberWithCommas } from 'App/utils'; import SelectDateRange from 'Shared/SelectDateRange'; import { applyFilter } from 'Duck/search'; -import Period from 'Types/app/period'; +import Record from 'Types/app/period'; +import { useStore } from 'App/mstore'; +import { useObserver } from 'mobx-react-lite'; +import { moment } from 'App/dateRange'; const sortOptionsMap = { 'startTs-desc': 'Newest', @@ -15,13 +18,30 @@ const sortOptionsMap = { const sortOptions = Object.entries(sortOptionsMap).map(([value, label]) => ({ value, label })); function SessionListHeader({ activeTab, count, applyFilter, filter }) { + const { settingsStore } = useStore(); + + const label = useObserver(() => settingsStore.sessionSettings.timezone.label); + const getTimeZoneOffset = React.useCallback(() => { + return label.slice(-6); + }, [label]); + const { startDate, endDate, rangeValue } = filter; - const period = new Period({ start: startDate, end: endDate, rangeName: rangeValue }); + const period = new Record({ start: startDate, end: endDate, rangeName: rangeValue }); const onDateChange = (e) => { const dateValues = e.toJSON(); + dateValues.startDate = moment(dateValues.startDate).utcOffset(getTimeZoneOffset(), true).valueOf(); + dateValues.endDate = moment(dateValues.endDate).utcOffset(getTimeZoneOffset(), true).valueOf(); applyFilter(dateValues); }; + + React.useEffect(() => { + const dateValues = period.toJSON(); + dateValues.startDate = moment(dateValues.startDate).startOf('day').utcOffset(getTimeZoneOffset(), true).valueOf(); + dateValues.endDate = moment(dateValues.endDate).endOf('day').utcOffset(getTimeZoneOffset(), true).valueOf(); + applyFilter(dateValues); + }, [label]); + return (
@@ -32,7 +52,7 @@ function SessionListHeader({ activeTab, count, applyFilter, filter }) { {
Sessions Captured in - +
}
diff --git a/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx b/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx index 12b6ba016..da8a940a5 100644 --- a/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx +++ b/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx @@ -6,17 +6,19 @@ import { components } from 'react-select'; import DateRangePopup from 'Shared/DateRangeDropdown/DateRangePopup'; import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv'; import cn from 'classnames'; +import { observer } from 'mobx-react-lite'; interface Props { period: any; onChange: (data: any) => void; disableCustom?: boolean; right?: boolean; + timezone?: string; [x: string]: any; } function SelectDateRange(props: Props) { const [isCustom, setIsCustom] = React.useState(false); - const { right = false, period, disableCustom = false, ...rest } = props; + const { right = false, period, disableCustom = false, timezone, ...rest } = props; let selectedValue = DATE_RANGE_OPTIONS.find((obj: any) => obj.value === period.rangeName); const options = DATE_RANGE_OPTIONS.filter((obj: any) => (disableCustom ? obj.value !== CUSTOM_RANGE : true)); @@ -24,15 +26,20 @@ function SelectDateRange(props: Props) { if (value === CUSTOM_RANGE) { setIsCustom(true); } else { + // @ts-ignore props.onChange(new Period({ rangeName: value })); } }; const onApplyDateRange = (value: any) => { - props.onChange(new Period({ rangeName: CUSTOM_RANGE, start: value.start, end: value.end })); + // @ts-ignore + const range = new Period({ rangeName: CUSTOM_RANGE, start: value.start, end: value.end }) + props.onChange(range); setIsCustom(false); }; + const isCustomRange = period.rangeName === CUSTOM_RANGE; + const customRange = isCustomRange ? period.rangeFormatted(undefined, timezone) : ''; return (
-
- - Upload Source Maps - and see source code context obtained from stack traces in their original form. - - } - /> +
+
+ +
+ + Upload Source Maps + and see source code context obtained from stack traces in their original form. + + } + /> +
diff --git a/frontend/app/components/Session_/Fetch/Fetch.js b/frontend/app/components/Session_/Fetch/Fetch.js index 2dff482c9..1b62d88c8 100644 --- a/frontend/app/components/Session_/Fetch/Fetch.js +++ b/frontend/app/components/Session_/Fetch/Fetch.js @@ -116,7 +116,7 @@ export default class Fetch extends React.PureComponent { /> -

Fetch

+ Fetch
-

GraphQL

+ GraphQL
-

Long Tasks

+ Long Tasks
(); const scale = 100 / endTime; + const createEventClickHandler = (pointer: any) => (e: any) => { + console.log('here...'); + e.stopPropagation(); + Controls.jump(pointer.time); + // props.setTimelinePointer(pointer); + }; + let width = 100; const SPEED = 5; - - const onWheel = (e: React.UIEvent) => { + const onWheel = (e: any) => { e.preventDefault(); e.stopPropagation(); - const delta = e.deltaY; - if (delta > 0) { - width += SPEED; - } else { - width -= SPEED; + // console.log('e', e) + + // horizontal + if (e.deltaX != '-0') { + // e.preventDefault(); + console.log('e.deltaX', e.deltaX); } - if (width < 100) { - width = 100; - } - if (innerRef.current) { - innerRef.current.style.width = width + '%'; - if (containerRef.current) { - containerRef.current.style.left = (100 - width) / 2 + '%'; + // Vertical + if (e.deltaY != '-0') { + console.log('e.deltaY', e.deltaY); + // e.preventDefault(); + const delta = e.deltaY; + if (delta > 0) { + width += SPEED; + } else { + width -= SPEED; + } + if (width < 100) { + width = 100; + } + + if (innerRef.current) { + innerRef.current.style.width = width + '%'; + if (containerRef.current) { + containerRef.current.style.left = (100 - width) / 2 + '%'; + } } } }; + useEffect(() => { + if (containerRef.current) { + containerRef.current.addEventListener('wheel', onWheel, { passive: false }); + } + + return () => { + if (containerRef.current) { + containerRef.current.removeEventListener('wheel', onWheel); + } + }; + }, []); + const renderNetworkElement = (item: any) => { - return
; + return ( + + {item.success ? 'Slow resource: ' : 'Missing resource:'} +
+ {item.name} +
+ } + delay={0} + position="top" + > +
+
+
+ + ); }; const renderClickRageElement = (item: any) => { return ( -
- -
+ + {'Click Rage'} +
+ } + delay={0} + position="top" + > +
+ +
+ ); }; const renderExceptionElement = (item: any) => { - // console.log('item', item); return ( - +
+ +
); }; return ( - + -
- Overview -
+ Overview
@@ -101,7 +157,7 @@ function OverviewPanel(props: Props) { } export default connectPlayer((state: any) => ({ - resourceList: state.resourceList, + resourceList: state.resourceList.filter((r: any) => r.isRed() || r.isYellow()), exceptionsList: state.exceptionsList, eventsList: state.eventList, endTime: state.endTime, @@ -112,5 +168,5 @@ const VerticalPointerLine = connectPlayer((state: any) => ({ scale: 100 / state.endTime, }))(({ time, scale }: any) => { const left = time * scale; - return
; + return
; }); diff --git a/frontend/app/components/Session_/Player/Player.js b/frontend/app/components/Session_/Player/Player.js index 6d7089d77..9a95121ac 100644 --- a/frontend/app/components/Session_/Player/Player.js +++ b/frontend/app/components/Session_/Player/Player.js @@ -106,7 +106,7 @@ export default class Player extends React.PureComponent {
{ !fullscreen && !!bottomBlock &&
- { //bottomBlock === OVERVIEW && + { bottomBlock === OVERVIEW && } { bottomBlock === CONSOLE && diff --git a/frontend/app/components/Session_/PlayerBlock.js b/frontend/app/components/Session_/PlayerBlock.js index 487809649..1000a63a3 100644 --- a/frontend/app/components/Session_/PlayerBlock.js +++ b/frontend/app/components/Session_/PlayerBlock.js @@ -37,9 +37,9 @@ export default class PlayerBlock extends React.PureComponent { />} diff --git a/frontend/app/components/Session_/Profiler/Profiler.js b/frontend/app/components/Session_/Profiler/Profiler.js index 83b13c89c..9f8fdc284 100644 --- a/frontend/app/components/Session_/Profiler/Profiler.js +++ b/frontend/app/components/Session_/Profiler/Profiler.js @@ -42,7 +42,9 @@ export default class Profiler extends React.PureComponent { /> -

Profiler

+
+ Profiler +
Date: Mon, 25 Jul 2022 13:50:18 +0200 Subject: [PATCH 07/16] feat(ui) - timeline overview - show panels on click --- .../Session_/OverviewPanel/OverviewPanel.tsx | 94 ++++++------------- 1 file changed, 27 insertions(+), 67 deletions(-) diff --git a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx index 844d31488..87dc542d6 100644 --- a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx +++ b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx @@ -1,82 +1,38 @@ import { connectPlayer, Controls } from 'App/player'; -import React, { useEffect } from 'react'; +import { toggleBottomBlock, NETWORK, EXCEPTIONS } from 'Duck/components/player'; +import React from 'react'; import BottomBlock from '../BottomBlock'; import EventRow from './components/EventRow'; import { TYPES } from 'Types/session/event'; import { Icon } from 'UI'; import { Tooltip } from 'react-tippy'; import stl from './overviewPanel.module.css'; +import { connect } from 'react-redux'; interface Props { resourceList: any[]; exceptionsList: any[]; eventsList: any[]; endTime: number; + toggleBottomBlock: any; } function OverviewPanel(props: Props) { const { resourceList, exceptionsList, eventsList, endTime } = props; const clickRageList = React.useMemo(() => { return eventsList.filter((item: any) => item.type === TYPES.CLICKRAGE); }, [eventsList]); - - const containerRef = React.useRef(null); - const innerRef = React.createRef(); const scale = 100 / endTime; - const createEventClickHandler = (pointer: any) => (e: any) => { - console.log('here...'); + const createEventClickHandler = (pointer: any, type: any) => (e: any) => { e.stopPropagation(); Controls.jump(pointer.time); - // props.setTimelinePointer(pointer); + if (!type) { + return; + } + + props.toggleBottomBlock(type); }; - let width = 100; - const SPEED = 5; - const onWheel = (e: any) => { - e.preventDefault(); - e.stopPropagation(); - // console.log('e', e) - - // horizontal - if (e.deltaX != '-0') { - // e.preventDefault(); - console.log('e.deltaX', e.deltaX); - } - // Vertical - if (e.deltaY != '-0') { - console.log('e.deltaY', e.deltaY); - // e.preventDefault(); - const delta = e.deltaY; - if (delta > 0) { - width += SPEED; - } else { - width -= SPEED; - } - if (width < 100) { - width = 100; - } - - if (innerRef.current) { - innerRef.current.style.width = width + '%'; - if (containerRef.current) { - containerRef.current.style.left = (100 - width) / 2 + '%'; - } - } - } - }; - - useEffect(() => { - if (containerRef.current) { - containerRef.current.addEventListener('wheel', onWheel, { passive: false }); - } - - return () => { - if (containerRef.current) { - containerRef.current.removeEventListener('wheel', onWheel); - } - }; - }, []); - const renderNetworkElement = (item: any) => { return ( -
-
+
+
); @@ -108,8 +64,8 @@ function OverviewPanel(props: Props) { delay={0} position="top" > -
- +
+
); @@ -128,7 +84,7 @@ function OverviewPanel(props: Props) { delay={0} position="top" > -
+
@@ -141,8 +97,8 @@ function OverviewPanel(props: Props) { Overview -
-
+
+
@@ -156,12 +112,16 @@ function OverviewPanel(props: Props) { ); } -export default connectPlayer((state: any) => ({ - resourceList: state.resourceList.filter((r: any) => r.isRed() || r.isYellow()), - exceptionsList: state.exceptionsList, - eventsList: state.eventList, - endTime: state.endTime, -}))(OverviewPanel); +export default connect(null, { + toggleBottomBlock, +})( + connectPlayer((state: any) => ({ + resourceList: state.resourceList.filter((r: any) => r.isRed() || r.isYellow()), + exceptionsList: state.exceptionsList, + eventsList: state.eventList, + endTime: state.endTime, + }))(OverviewPanel) +); const VerticalPointerLine = connectPlayer((state: any) => ({ time: state.time, From 6b7f66e94932e6445d0993e8f081504269ff6418 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 8 Aug 2022 17:23:53 +0200 Subject: [PATCH 08/16] feat(ui) - overview - errordetails modal --- .../Session_/OverviewPanel/OverviewPanel.tsx | 20 ++++- .../components/EventRow/EventRow.tsx | 2 +- .../FeatureSelection/FeatureSelection.tsx | 73 +++++++++++++++++ .../TimelineScale/TimelineScale.tsx | 14 ++++ .../components/TimelineScale/index.ts | 1 + .../app/components/ui/Checkbox/Checkbox.tsx | 25 +++--- .../ui/ErrorDetails/ErrorDetails.js | 65 --------------- .../ui/ErrorDetails/ErrorDetails.tsx | 79 +++++++++++++++++++ 8 files changed, 195 insertions(+), 84 deletions(-) create mode 100644 frontend/app/components/Session_/OverviewPanel/components/FeatureSelection/FeatureSelection.tsx create mode 100644 frontend/app/components/Session_/OverviewPanel/components/TimelineScale/TimelineScale.tsx create mode 100644 frontend/app/components/Session_/OverviewPanel/components/TimelineScale/index.ts delete mode 100644 frontend/app/components/ui/ErrorDetails/ErrorDetails.js create mode 100644 frontend/app/components/ui/ErrorDetails/ErrorDetails.tsx diff --git a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx index 87dc542d6..a982e6fd8 100644 --- a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx +++ b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx @@ -4,10 +4,13 @@ import React from 'react'; import BottomBlock from '../BottomBlock'; import EventRow from './components/EventRow'; import { TYPES } from 'Types/session/event'; -import { Icon } from 'UI'; +import { Icon, Checkbox, ErrorDetails } from 'UI'; import { Tooltip } from 'react-tippy'; import stl from './overviewPanel.module.css'; import { connect } from 'react-redux'; +import TimelineScale from './components/TimelineScale'; +import FeatureSelection from './components/FeatureSelection/FeatureSelection'; +import { useModal } from 'App/components/Modal'; interface Props { resourceList: any[]; @@ -22,6 +25,8 @@ function OverviewPanel(props: Props) { return eventsList.filter((item: any) => item.type === TYPES.CLICKRAGE); }, [eventsList]); const scale = 100 / endTime; + const selectedFeatures = React.useMemo(() => ['NETWORK', 'ERRORS', 'EVENTS'], []); + const { showModal } = useModal(); const createEventClickHandler = (pointer: any, type: any) => (e: any) => { e.stopPropagation(); @@ -30,7 +35,10 @@ function OverviewPanel(props: Props) { return; } - props.toggleBottomBlock(type); + if (type === EXCEPTIONS) { + showModal(, { right: true }); + } + // props.toggleBottomBlock(type); }; const renderNetworkElement = (item: any) => { @@ -92,12 +100,16 @@ function OverviewPanel(props: Props) { }; return ( - + Overview +
+ {}} /> +
-
+
+
diff --git a/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx b/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx index b1fec7cec..f06003463 100644 --- a/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx +++ b/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx @@ -20,7 +20,7 @@ function EventRow(props: Props) { }) }, [list]); return ( -
+
{title}
{_list.map((item: any, index: number) => { diff --git a/frontend/app/components/Session_/OverviewPanel/components/FeatureSelection/FeatureSelection.tsx b/frontend/app/components/Session_/OverviewPanel/components/FeatureSelection/FeatureSelection.tsx new file mode 100644 index 000000000..c4bb3905e --- /dev/null +++ b/frontend/app/components/Session_/OverviewPanel/components/FeatureSelection/FeatureSelection.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { Checkbox } from 'UI'; + +const NETWORK = 'NETWORK'; +const ERRORS = 'ERRORS'; +const EVENTS = 'EVENTS'; +const CLICKRAGE = 'CLICK RAGE'; +const PERFORMANCE = 'PERFORMANCE'; + +interface Props { + list: any[]; + updateList: any; +} +function FeatureSelection(props: Props) { + const { list } = props; + + return ( + + { + console.log(e); + }} + label={NETWORK} + /> + { + console.log(e); + }} + label={ERRORS} + /> + { + console.log(e); + }} + label={EVENTS} + /> + { + console.log(e); + }} + label={CLICKRAGE} + /> + { + console.log(e); + }} + label={PERFORMANCE} + /> + + ); +} + +export default FeatureSelection; diff --git a/frontend/app/components/Session_/OverviewPanel/components/TimelineScale/TimelineScale.tsx b/frontend/app/components/Session_/OverviewPanel/components/TimelineScale/TimelineScale.tsx new file mode 100644 index 000000000..e0b4be0b7 --- /dev/null +++ b/frontend/app/components/Session_/OverviewPanel/components/TimelineScale/TimelineScale.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +interface Props { + +} +function TimelineScale(props: Props) { + return ( +
+ +
+ ); +} + +export default TimelineScale; \ No newline at end of file diff --git a/frontend/app/components/Session_/OverviewPanel/components/TimelineScale/index.ts b/frontend/app/components/Session_/OverviewPanel/components/TimelineScale/index.ts new file mode 100644 index 000000000..9a2302a32 --- /dev/null +++ b/frontend/app/components/Session_/OverviewPanel/components/TimelineScale/index.ts @@ -0,0 +1 @@ +export { default } from './TimelineScale'; \ No newline at end of file diff --git a/frontend/app/components/ui/Checkbox/Checkbox.tsx b/frontend/app/components/ui/Checkbox/Checkbox.tsx index 0781183b1..2b68ccc97 100644 --- a/frontend/app/components/ui/Checkbox/Checkbox.tsx +++ b/frontend/app/components/ui/Checkbox/Checkbox.tsx @@ -2,19 +2,16 @@ import React from 'react'; import cn from 'classnames'; interface Props { - classNam?: string; - label?: string; - [x: string]: any; + classNam?: string; + label?: string; + [x: string]: any; } export default (props: Props) => { - const { className = '', label = '', ...rest } = props; - return ( - - ) -}; \ No newline at end of file + const { className = '', label = '', ...rest } = props; + return ( + + ); +}; diff --git a/frontend/app/components/ui/ErrorDetails/ErrorDetails.js b/frontend/app/components/ui/ErrorDetails/ErrorDetails.js deleted file mode 100644 index 2a6afdd1e..000000000 --- a/frontend/app/components/ui/ErrorDetails/ErrorDetails.js +++ /dev/null @@ -1,65 +0,0 @@ -import React, { useState } from 'react' -import ErrorFrame from '../ErrorFrame/ErrorFrame' -import cn from 'classnames'; -import { IconButton, Icon } from 'UI'; -import { connect } from 'react-redux'; - -const docLink = 'https://docs.openreplay.com/installation/upload-sourcemaps'; - -function ErrorDetails({ className, name = "Error", message, errorStack, sourcemapUploaded }) { - const [showRaw, setShowRaw] = useState(false) - const firstFunc = errorStack.first() && errorStack.first().function - - const openDocs = () => { - window.open(docLink, '_blank'); - } - - return ( -
- { !sourcemapUploaded && ( -
- -
Source maps must be uploaded to OpenReplay to be able to see stack traces. Learn more.
-
- ) } -
-

- Stacktrace -

-
- setShowRaw(false) } - label="FULL" - plain={!showRaw} - primaryText={!showRaw} - /> - setShowRaw(true) } - plain={showRaw} - label="RAW" - /> -
-
-
-
{ name }
-
{message}
-
- { showRaw && -
{name} : {firstFunc ? firstFunc : '?' }
- } - { errorStack.map((frame, i) => ( -
- -
- )) - } -
- ) -} - -ErrorDetails.displayName = "ErrorDetails"; -export default ErrorDetails; diff --git a/frontend/app/components/ui/ErrorDetails/ErrorDetails.tsx b/frontend/app/components/ui/ErrorDetails/ErrorDetails.tsx new file mode 100644 index 000000000..331535461 --- /dev/null +++ b/frontend/app/components/ui/ErrorDetails/ErrorDetails.tsx @@ -0,0 +1,79 @@ +import React, { useEffect, useState } from 'react'; +import ErrorFrame from '../ErrorFrame/ErrorFrame'; +import { fetchErrorStackList } from 'Duck/sessions'; +import { IconButton, Icon } from 'UI'; +import { connect } from 'react-redux'; + +const docLink = 'https://docs.openreplay.com/installation/upload-sourcemaps'; + +interface Props { + fetchErrorStackList: any; + sourcemapUploaded?: boolean; + errorStack?: any; + message?: string; + sessionId: string; + error: any; +} +function ErrorDetails(props: Props) { + const { error, sessionId, message = '', errorStack = [], sourcemapUploaded = false } = props; + const [showRaw, setShowRaw] = useState(false); + const firstFunc = errorStack.first() && errorStack.first().function; + + const openDocs = () => { + window.open(docLink, '_blank'); + }; + + useEffect(() => { + props.fetchErrorStackList(sessionId, error.errorId); + }, []); + + return ( +
+ {!sourcemapUploaded && ( +
+ +
+ Source maps must be uploaded to OpenReplay to be able to see stack traces.{' '} + + Learn more. + +
+
+ )} +
+

Stacktrace

+
+ setShowRaw(false)} label="FULL" plain={!showRaw} primaryText={!showRaw} /> + setShowRaw(true)} plain={showRaw} label="RAW" /> +
+
+
+
{error.name}
+
{message}
+
+ {showRaw && ( +
+ {error.name} : {firstFunc ? firstFunc : '?'} +
+ )} + ; + {errorStack.map((frame: any, i: any) => ( +
+ +
+ ))} +
+ ); +} + +ErrorDetails.displayName = 'ErrorDetails'; +export default connect( + (state: any) => ({ + errorStack: state.getIn(['sessions', 'errorStack']), + sessionId: state.getIn(['sessions', 'current', 'sessionId']), + }), + { fetchErrorStackList } +)(ErrorDetails); From 5387f2a57e6c33512d4f8b520e9ca36370f7cd93 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Tue, 9 Aug 2022 12:07:02 +0200 Subject: [PATCH 09/16] feat(ui) - overview - components --- .../Session_/OverviewPanel/OverviewPanel.tsx | 112 ++++-------------- .../TimelinePointer/TimelinePointer.tsx | 101 ++++++++++++++++ .../components/TimelinePointer/index.ts | 1 + .../VerticalPointerLine.tsx | 17 +++ .../components/VerticalPointerLine/index.ts | 1 + .../app/components/Session_/Player/Player.js | 4 +- 6 files changed, 146 insertions(+), 90 deletions(-) create mode 100644 frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx create mode 100644 frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/index.ts create mode 100644 frontend/app/components/Session_/OverviewPanel/components/VerticalPointerLine/VerticalPointerLine.tsx create mode 100644 frontend/app/components/Session_/OverviewPanel/components/VerticalPointerLine/index.ts diff --git a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx index a982e6fd8..02421b6f9 100644 --- a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx +++ b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx @@ -1,16 +1,14 @@ -import { connectPlayer, Controls } from 'App/player'; +import { connectPlayer } from 'App/player'; import { toggleBottomBlock, NETWORK, EXCEPTIONS } from 'Duck/components/player'; import React from 'react'; import BottomBlock from '../BottomBlock'; import EventRow from './components/EventRow'; import { TYPES } from 'Types/session/event'; -import { Icon, Checkbox, ErrorDetails } from 'UI'; -import { Tooltip } from 'react-tippy'; -import stl from './overviewPanel.module.css'; import { connect } from 'react-redux'; import TimelineScale from './components/TimelineScale'; import FeatureSelection from './components/FeatureSelection/FeatureSelection'; -import { useModal } from 'App/components/Modal'; +import TimelinePointer from './components/TimelinePointer'; +import VerticalPointerLine from './components/VerticalPointerLine'; interface Props { resourceList: any[]; @@ -26,78 +24,6 @@ function OverviewPanel(props: Props) { }, [eventsList]); const scale = 100 / endTime; const selectedFeatures = React.useMemo(() => ['NETWORK', 'ERRORS', 'EVENTS'], []); - const { showModal } = useModal(); - - const createEventClickHandler = (pointer: any, type: any) => (e: any) => { - e.stopPropagation(); - Controls.jump(pointer.time); - if (!type) { - return; - } - - if (type === EXCEPTIONS) { - showModal(, { right: true }); - } - // props.toggleBottomBlock(type); - }; - - const renderNetworkElement = (item: any) => { - return ( - - {item.success ? 'Slow resource: ' : 'Missing resource:'} -
- {item.name} -
- } - delay={0} - position="top" - > -
-
-
- - ); - }; - - const renderClickRageElement = (item: any) => { - return ( - - {'Click Rage'} -
- } - delay={0} - position="top" - > -
- -
- - ); - }; - - const renderExceptionElement = (item: any) => { - return ( - - {'Exception'} -
- {item.message} -
- } - delay={0} - position="top" - > -
- -
- - ); - }; return ( @@ -112,11 +38,29 @@ function OverviewPanel(props: Props) {
- + } + />
- + } + />
- + } + />
@@ -134,11 +78,3 @@ export default connect(null, { endTime: state.endTime, }))(OverviewPanel) ); - -const VerticalPointerLine = connectPlayer((state: any) => ({ - time: state.time, - scale: 100 / state.endTime, -}))(({ time, scale }: any) => { - const left = time * scale; - return
; -}); diff --git a/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx b/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx new file mode 100644 index 000000000..b74cd068d --- /dev/null +++ b/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import { connectPlayer, Controls } from 'App/player'; +import { toggleBottomBlock, NETWORK, EXCEPTIONS } from 'Duck/components/player'; +import { useModal } from 'App/components/Modal'; +import { Icon, ErrorDetails } from 'UI'; +import { Tooltip } from 'react-tippy'; +import { TYPES as EVENT_TYPES } from 'Types/session/event'; + +interface Props { + pointer: any; + type: any; +} +function TimelinePointer(props: Props) { + const { showModal } = useModal(); + const createEventClickHandler = (pointer: any, type: any) => (e: any) => { + e.stopPropagation(); + Controls.jump(pointer.time); + if (!type) { + return; + } + + if (type === EXCEPTIONS) { + showModal(, { right: true }); + } + // props.toggleBottomBlock(type); + }; + + const renderNetworkElement = (item: any) => { + return ( + + {item.success ? 'Slow resource: ' : 'Missing resource:'} +
+ {item.name} +
+ } + delay={0} + position="top" + > +
+
+
+ + ); + }; + + const renderClickRageElement = (item: any) => { + return ( + + {'Click Rage'} +
+ } + delay={0} + position="top" + > +
+ +
+ + ); + }; + + const renderExceptionElement = (item: any) => { + return ( + + {'Exception'} +
+ {item.message} +
+ } + delay={0} + position="top" + > +
+ +
+ + ); + }; + + const render = () => { + const { pointer, type } = props; + if (type === NETWORK) { + return renderNetworkElement(pointer); + } + if (type === EVENT_TYPES.CLICKRAGE) { + return renderClickRageElement(pointer); + } + if (type === EXCEPTIONS) { + return renderExceptionElement(pointer); + } + }; + return
{render()}
; +} + +export default TimelinePointer; diff --git a/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/index.ts b/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/index.ts new file mode 100644 index 000000000..e0f9399ff --- /dev/null +++ b/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/index.ts @@ -0,0 +1 @@ +export { default } from './TimelinePointer' \ No newline at end of file diff --git a/frontend/app/components/Session_/OverviewPanel/components/VerticalPointerLine/VerticalPointerLine.tsx b/frontend/app/components/Session_/OverviewPanel/components/VerticalPointerLine/VerticalPointerLine.tsx new file mode 100644 index 000000000..b8997e111 --- /dev/null +++ b/frontend/app/components/Session_/OverviewPanel/components/VerticalPointerLine/VerticalPointerLine.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { connectPlayer } from 'App/player'; + +interface Props { + time: number; + scale: number; +} +function VerticalPointerLine(props: Props) { + const { time, scale } = props; + const left = time * scale; + return
; +} + +export default connectPlayer((state: any) => ({ + time: state.time, + scale: 100 / state.endTime, +}))(VerticalPointerLine); diff --git a/frontend/app/components/Session_/OverviewPanel/components/VerticalPointerLine/index.ts b/frontend/app/components/Session_/OverviewPanel/components/VerticalPointerLine/index.ts new file mode 100644 index 000000000..4a75fc048 --- /dev/null +++ b/frontend/app/components/Session_/OverviewPanel/components/VerticalPointerLine/index.ts @@ -0,0 +1 @@ +export { default } from './VerticalPointerLine' \ No newline at end of file diff --git a/frontend/app/components/Session_/Player/Player.js b/frontend/app/components/Session_/Player/Player.js index 9a95121ac..ef69d5c2d 100644 --- a/frontend/app/components/Session_/Player/Player.js +++ b/frontend/app/components/Session_/Player/Player.js @@ -104,9 +104,9 @@ export default class Player extends React.PureComponent { ref={ this.screenWrapper } />
- { !fullscreen && !!bottomBlock && + { // !fullscreen && !!bottomBlock &&
- { bottomBlock === OVERVIEW && + { // bottomBlock === OVERVIEW && } { bottomBlock === CONSOLE && From f3efb317769e9795da13d22eb84b3f112a67a2ca Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Wed, 10 Aug 2022 15:12:37 +0200 Subject: [PATCH 10/16] feat(ui) - overview - feature selection --- .../Session_/OverviewPanel/OverviewPanel.tsx | 39 ++++------ .../FeatureSelection/FeatureSelection.tsx | 74 ++++++------------- 2 files changed, 37 insertions(+), 76 deletions(-) diff --git a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx index 02421b6f9..7d1ee0165 100644 --- a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx +++ b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx @@ -9,6 +9,7 @@ import TimelineScale from './components/TimelineScale'; import FeatureSelection from './components/FeatureSelection/FeatureSelection'; import TimelinePointer from './components/TimelinePointer'; import VerticalPointerLine from './components/VerticalPointerLine'; +import cn from 'classnames'; interface Props { resourceList: any[]; @@ -23,14 +24,14 @@ function OverviewPanel(props: Props) { return eventsList.filter((item: any) => item.type === TYPES.CLICKRAGE); }, [eventsList]); const scale = 100 / endTime; - const selectedFeatures = React.useMemo(() => ['NETWORK', 'ERRORS', 'EVENTS'], []); + const [selectedFeatures, setSelectedFeatures] = React.useState(['NETWORK', 'ERRORS', 'EVENTS']); return ( Overview
- {}} /> +
@@ -38,29 +39,17 @@ function OverviewPanel(props: Props) {
- } - /> -
- } - /> -
- } - /> + {selectedFeatures.map((feature: any, index: number) => ( +
+ } + /> +
+ ))}
diff --git a/frontend/app/components/Session_/OverviewPanel/components/FeatureSelection/FeatureSelection.tsx b/frontend/app/components/Session_/OverviewPanel/components/FeatureSelection/FeatureSelection.tsx index c4bb3905e..77a498785 100644 --- a/frontend/app/components/Session_/OverviewPanel/components/FeatureSelection/FeatureSelection.tsx +++ b/frontend/app/components/Session_/OverviewPanel/components/FeatureSelection/FeatureSelection.tsx @@ -13,59 +13,31 @@ interface Props { } function FeatureSelection(props: Props) { const { list } = props; - + const features = [NETWORK, ERRORS, EVENTS, CLICKRAGE, PERFORMANCE]; + const disabled = list.length >= 3; + return ( - { - console.log(e); - }} - label={NETWORK} - /> - { - console.log(e); - }} - label={ERRORS} - /> - { - console.log(e); - }} - label={EVENTS} - /> - { - console.log(e); - }} - label={CLICKRAGE} - /> - { - console.log(e); - }} - label={PERFORMANCE} - /> + {features.map((feature, index) => { + const checked = list.includes(feature); + const _disabled = disabled && !checked; + return ( + { + if (checked) { + props.updateList(list.filter((item: any) => item !== feature)); + } else { + props.updateList([...list, feature]); + } + }} + /> + ); + })} ); } From f3bf7be8cc6ecc01d45707c6976337af247e6cd0 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Wed, 10 Aug 2022 18:18:06 +0200 Subject: [PATCH 11/16] feat(ui) - overview - events and errors wip --- .../Session_/OverviewPanel/OverviewPanel.tsx | 30 +- .../components/EventRow/EventRow.tsx | 10 +- .../FeatureSelection/FeatureSelection.tsx | 2 +- .../StackEventModal/StackEventModal.tsx | 14 + .../components/StackEventModal/index.ts | 1 + .../TimelinePointer/TimelinePointer.tsx | 61 ++- .../Session_/Player/Controls/Timeline.js | 420 +++++++++--------- 7 files changed, 304 insertions(+), 234 deletions(-) create mode 100644 frontend/app/components/Session_/OverviewPanel/components/StackEventModal/StackEventModal.tsx create mode 100644 frontend/app/components/Session_/OverviewPanel/components/StackEventModal/index.ts diff --git a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx index 7d1ee0165..53ba3ddf5 100644 --- a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx +++ b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx @@ -1,5 +1,5 @@ import { connectPlayer } from 'App/player'; -import { toggleBottomBlock, NETWORK, EXCEPTIONS } from 'Duck/components/player'; +import { toggleBottomBlock } from 'Duck/components/player'; import React from 'react'; import BottomBlock from '../BottomBlock'; import EventRow from './components/EventRow'; @@ -15,16 +15,25 @@ interface Props { resourceList: any[]; exceptionsList: any[]; eventsList: any[]; - endTime: number; toggleBottomBlock: any; + stackEventList: any[]; + issuesList: any[]; } function OverviewPanel(props: Props) { - const { resourceList, exceptionsList, eventsList, endTime } = props; + const { resourceList, exceptionsList, eventsList, stackEventList, issuesList } = props; const clickRageList = React.useMemo(() => { return eventsList.filter((item: any) => item.type === TYPES.CLICKRAGE); }, [eventsList]); - const scale = 100 / endTime; - const [selectedFeatures, setSelectedFeatures] = React.useState(['NETWORK', 'ERRORS', 'EVENTS']); + // const scale = 100 / endTime; + const [selectedFeatures, setSelectedFeatures] = React.useState(['PERFORMANCE', 'ERRORS', 'EVENTS']); + + const resources: any = { + NETWORK: resourceList, + ERRORS: exceptionsList, + EVENTS: stackEventList, + CLICKRAGE: clickRageList, + PERFORMANCE: issuesList, + }; return ( @@ -40,12 +49,11 @@ function OverviewPanel(props: Props) {
{selectedFeatures.map((feature: any, index: number) => ( -
+
} />
@@ -57,13 +65,15 @@ function OverviewPanel(props: Props) { ); } -export default connect(null, { +export default connect((state: any) => ({ + issuesList: state.getIn(['sessions', 'current', 'issues']), +}), { toggleBottomBlock, })( connectPlayer((state: any) => ({ resourceList: state.resourceList.filter((r: any) => r.isRed() || r.isYellow()), exceptionsList: state.exceptionsList, eventsList: state.eventList, - endTime: state.endTime, + stackEventList: state.stackList, }))(OverviewPanel) ); diff --git a/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx b/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx index f06003463..0447fcaa9 100644 --- a/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx +++ b/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx @@ -1,16 +1,18 @@ import React from 'react'; import cn from 'classnames' import { getTimelinePosition } from 'App/utils'; +import { connectPlayer } from 'App/player'; interface Props { list?: any[]; - scale?: number; title: string; className?: string; + endTime?: number; renderElement?: (item: any) => React.ReactNode; } function EventRow(props: Props) { - const { title, className, list = [], scale = 0 } = props; + const { title, className, list = [], endTime = 0 } = props; + const scale = 100 / endTime; const _list = React.useMemo(() => { return list.map((item: any, _index: number) => { return { @@ -36,4 +38,6 @@ function EventRow(props: Props) { ); } -export default EventRow; \ No newline at end of file +export default connectPlayer((state: any) => ({ + endTime: state.endTime, +}))(EventRow); \ No newline at end of file diff --git a/frontend/app/components/Session_/OverviewPanel/components/FeatureSelection/FeatureSelection.tsx b/frontend/app/components/Session_/OverviewPanel/components/FeatureSelection/FeatureSelection.tsx index 77a498785..1f74d20ab 100644 --- a/frontend/app/components/Session_/OverviewPanel/components/FeatureSelection/FeatureSelection.tsx +++ b/frontend/app/components/Session_/OverviewPanel/components/FeatureSelection/FeatureSelection.tsx @@ -4,7 +4,7 @@ import { Checkbox } from 'UI'; const NETWORK = 'NETWORK'; const ERRORS = 'ERRORS'; const EVENTS = 'EVENTS'; -const CLICKRAGE = 'CLICK RAGE'; +const CLICKRAGE = 'CLICKRAGE'; const PERFORMANCE = 'PERFORMANCE'; interface Props { diff --git a/frontend/app/components/Session_/OverviewPanel/components/StackEventModal/StackEventModal.tsx b/frontend/app/components/Session_/OverviewPanel/components/StackEventModal/StackEventModal.tsx new file mode 100644 index 000000000..07d163dbe --- /dev/null +++ b/frontend/app/components/Session_/OverviewPanel/components/StackEventModal/StackEventModal.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +interface Props { + event: any; +} +function StackEventModal(props: Props) { + return ( +
+ Content +
+ ); +} + +export default StackEventModal; diff --git a/frontend/app/components/Session_/OverviewPanel/components/StackEventModal/index.ts b/frontend/app/components/Session_/OverviewPanel/components/StackEventModal/index.ts new file mode 100644 index 000000000..93a084d28 --- /dev/null +++ b/frontend/app/components/Session_/OverviewPanel/components/StackEventModal/index.ts @@ -0,0 +1 @@ +export { default } from './StackEventModal'; diff --git a/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx b/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx index b74cd068d..a9d58f4a2 100644 --- a/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx +++ b/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx @@ -1,17 +1,18 @@ import React from 'react'; import { connectPlayer, Controls } from 'App/player'; -import { toggleBottomBlock, NETWORK, EXCEPTIONS } from 'Duck/components/player'; +import { toggleBottomBlock, NETWORK, EXCEPTIONS, PERFORMANCE } from 'Duck/components/player'; import { useModal } from 'App/components/Modal'; import { Icon, ErrorDetails } from 'UI'; import { Tooltip } from 'react-tippy'; import { TYPES as EVENT_TYPES } from 'Types/session/event'; +import StackEventModal from '../StackEventModal'; interface Props { pointer: any; type: any; } function TimelinePointer(props: Props) { - const { showModal } = useModal(); + const { showModal, hideModal } = useModal(); const createEventClickHandler = (pointer: any, type: any) => (e: any) => { e.stopPropagation(); Controls.jump(pointer.time); @@ -19,9 +20,13 @@ function TimelinePointer(props: Props) { return; } - if (type === EXCEPTIONS) { + if (type === 'EXCEPTIONS') { showModal(, { right: true }); } + + if (type === 'EVENT') { + showModal(, { right: true }); + } // props.toggleBottomBlock(type); }; @@ -63,6 +68,43 @@ function TimelinePointer(props: Props) { ); }; + const renderStackEventElement = (item: any) => { + return ( + + {'Stack Event'} +
+ } + delay={0} + position="top" + > +
+ {/* */} +
+ + ); + }; + + const renderPerformanceElement = (item: any) => { + console.log('item', item) + return ( + + {item.name} +
+ } + delay={0} + position="top" + > +
+ {/* */} +
+ + ); + }; + const renderExceptionElement = (item: any) => { return ( { const { pointer, type } = props; - if (type === NETWORK) { + if (type === 'NETWORK') { return renderNetworkElement(pointer); } - if (type === EVENT_TYPES.CLICKRAGE) { + if (type === 'CLICKRAGE') { return renderClickRageElement(pointer); } - if (type === EXCEPTIONS) { + if (type === 'ERRORS') { return renderExceptionElement(pointer); } + if (type === 'EVENTS') { + return renderStackEventElement(pointer); + } + + if (type === 'PERFORMANCE') { + return renderPerformanceElement(pointer); + } }; return
{render()}
; } diff --git a/frontend/app/components/Session_/Player/Controls/Timeline.js b/frontend/app/components/Session_/Player/Controls/Timeline.js index 3acdb4c11..91603fbf5 100644 --- a/frontend/app/components/Session_/Player/Controls/Timeline.js +++ b/frontend/app/components/Session_/Player/Controls/Timeline.js @@ -12,206 +12,199 @@ import CustomDragLayer from './CustomDragLayer'; import { debounce } from 'App/utils'; import { Tooltip } from 'react-tippy'; -const BOUNDRY = 15 +const BOUNDRY = 15; function getTimelinePosition(value, scale) { - const pos = value * scale; + const pos = value * scale; - return pos > 100 ? 100 : pos; + return pos > 100 ? 100 : pos; } const getPointerIcon = (type) => { - // exception, - switch(type) { - case 'fetch': - return 'funnel/file-earmark-minus-fill'; - case 'exception': - return 'funnel/exclamation-circle-fill'; - case 'log': - return 'funnel/exclamation-circle-fill'; - case 'stack': - return 'funnel/patch-exclamation-fill'; - case 'resource': - return 'funnel/file-earmark-minus-fill'; + // exception, + switch (type) { + case 'fetch': + return 'funnel/file-earmark-minus-fill'; + case 'exception': + return 'funnel/exclamation-circle-fill'; + case 'log': + return 'funnel/exclamation-circle-fill'; + case 'stack': + return 'funnel/patch-exclamation-fill'; + case 'resource': + return 'funnel/file-earmark-minus-fill'; - case 'dead_click': - return 'funnel/dizzy'; - case 'click_rage': - return 'funnel/dizzy'; - case 'excessive_scrolling': - return 'funnel/mouse'; - case 'bad_request': - return 'funnel/file-medical-alt'; - case 'missing_resource': - return 'funnel/file-earmark-minus-fill'; - case 'memory': - return 'funnel/sd-card'; - case 'cpu': - return 'funnel/microchip'; - case 'slow_resource': - return 'funnel/hourglass-top'; - case 'slow_page_load': - return 'funnel/hourglass-top'; - case 'crash': - return 'funnel/file-exclamation'; - case 'js_exception': - return 'funnel/exclamation-circle-fill'; - } - - return 'info'; -} + case 'dead_click': + return 'funnel/dizzy'; + case 'click_rage': + return 'funnel/dizzy'; + case 'excessive_scrolling': + return 'funnel/mouse'; + case 'bad_request': + return 'funnel/file-medical-alt'; + case 'missing_resource': + return 'funnel/file-earmark-minus-fill'; + case 'memory': + return 'funnel/sd-card'; + case 'cpu': + return 'funnel/microchip'; + case 'slow_resource': + return 'funnel/hourglass-top'; + case 'slow_page_load': + return 'funnel/hourglass-top'; + case 'crash': + return 'funnel/file-exclamation'; + case 'js_exception': + return 'funnel/exclamation-circle-fill'; + } + return 'info'; +}; let deboucneJump = () => null; -@connectPlayer(state => ({ - playing: state.playing, - time: state.time, - skipIntervals: state.skipIntervals, - events: state.eventList, - skip: state.skip, - // not updating properly rn - // skipToIssue: state.skipToIssue, - disabled: state.cssLoading || state.messagesLoading || state.markedTargets, - endTime: state.endTime, - live: state.live, - logList: state.logList, - exceptionsList: state.exceptionsList, - resourceList: state.resourceList, - stackList: state.stackList, - fetchList: state.fetchList, +@connectPlayer((state) => ({ + playing: state.playing, + time: state.time, + skipIntervals: state.skipIntervals, + events: state.eventList, + skip: state.skip, + // not updating properly rn + // skipToIssue: state.skipToIssue, + disabled: state.cssLoading || state.messagesLoading || state.markedTargets, + endTime: state.endTime, + live: state.live, + logList: state.logList, + exceptionsList: state.exceptionsList, + resourceList: state.resourceList, + stackList: state.stackList, + fetchList: state.fetchList, })) -@connect(state => ({ - issues: state.getIn([ 'sessions', 'current', 'issues' ]), - clickRageTime: state.getIn([ 'sessions', 'current', 'clickRage' ]) && - state.getIn([ 'sessions', 'current', 'clickRageTime' ]), - returningLocationTime: state.getIn([ 'sessions', 'current', 'returningLocation' ]) && - state.getIn([ 'sessions', 'current', 'returningLocationTime' ]), -}), { setTimelinePointer }) +@connect( + (state) => ({ + issues: state.getIn(['sessions', 'current', 'issues']), + clickRageTime: state.getIn(['sessions', 'current', 'clickRage']) && state.getIn(['sessions', 'current', 'clickRageTime']), + returningLocationTime: + state.getIn(['sessions', 'current', 'returningLocation']) && state.getIn(['sessions', 'current', 'returningLocationTime']), + }), + { setTimelinePointer } +) export default class Timeline extends React.PureComponent { - progressRef = React.createRef() - wasPlaying = false + progressRef = React.createRef(); + wasPlaying = false; - seekProgress = (e) => { - const { endTime } = this.props; - const p = e.nativeEvent.offsetX / e.target.offsetWidth; - const time = Math.max(Math.round(p * endTime), 0); - this.props.jump(time); - } + seekProgress = (e) => { + const { endTime } = this.props; + const p = e.nativeEvent.offsetX / e.target.offsetWidth; + const time = Math.max(Math.round(p * endTime), 0); + this.props.jump(time); + }; - createEventClickHandler = pointer => (e) => { - e.stopPropagation(); - this.props.jump(pointer.time); - this.props.setTimelinePointer(pointer); - } + createEventClickHandler = (pointer) => (e) => { + e.stopPropagation(); + this.props.jump(pointer.time); + this.props.setTimelinePointer(pointer); + }; - componentDidMount() { - const { issues } = this.props; - const skipToIssue = Controls.updateSkipToIssue(); - const firstIssue = issues.get(0); - deboucneJump = debounce(this.props.jump, 500); + componentDidMount() { + const { issues } = this.props; + const skipToIssue = Controls.updateSkipToIssue(); + const firstIssue = issues.get(0); + deboucneJump = debounce(this.props.jump, 500); - if (firstIssue && skipToIssue) { - this.props.jump(firstIssue.time); + if (firstIssue && skipToIssue) { + this.props.jump(firstIssue.time); + } } - } - onDragEnd = () => { - if (this.wasPlaying) { - this.props.togglePlay(); - } - } + onDragEnd = () => { + if (this.wasPlaying) { + this.props.togglePlay(); + } + }; - onDrag = (offset) => { - const { endTime } = this.props; + onDrag = (offset) => { + const { endTime } = this.props; - const p = (offset.x - BOUNDRY) / this.progressRef.current.offsetWidth; - const time = Math.max(Math.round(p * endTime), 0); - deboucneJump(time); - if (this.props.playing) { - this.wasPlaying = true; - this.props.pause(); - } - } + const p = (offset.x - BOUNDRY) / this.progressRef.current.offsetWidth; + const time = Math.max(Math.round(p * endTime), 0); + deboucneJump(time); + if (this.props.playing) { + this.wasPlaying = true; + this.props.pause(); + } + }; - render() { - const { - events, - skip, - skipIntervals, - disabled, - endTime, - live, - logList, - exceptionsList, - resourceList, - clickRageTime, - stackList, - fetchList, - issues, - } = this.props; + render() { + const { + events, + skip, + skipIntervals, + disabled, + endTime, + live, + logList, + exceptionsList, + resourceList, + clickRageTime, + stackList, + fetchList, + issues, + } = this.props; - const scale = 100 / endTime; + const scale = 100 / endTime; - return ( -
-
- - - - { skip && skipIntervals.map(interval => - (
)) - } -
- { events.map(e => ( -
- )) - } - { - issues.map(iss => ( -
- - { iss.name } -
- } - > - - -
- )) - } - { events.filter(e => e.type === TYPES.CLICKRAGE).map(e => ( + return ( +
+
+ + + + {skip && + skipIntervals.map((interval) => ( +
+ ))} +
+ {/* {events.map((e) => ( +
+ ))} */} + {/* {issues.map((iss) => ( +
+ + {iss.name} +
+ } + > + + +
+ ))} */} + {/* { events.filter(e => e.type === TYPES.CLICKRAGE).map(e => (
- ))} - {typeof clickRageTime === 'number' && + ))} */} + {/* {typeof clickRageTime === 'number' &&
- } - { exceptionsList + } */} + {/* { exceptionsList .map(e => (
)) - } - { resourceList + } */} + {/* { resourceList .filter(r => r.isRed() || r.isYellow()) .map(r => (
)) - } - { fetchList + } */} + {/* { fetchList .filter(e => e.isRed()) .map(e => (
)) - } - { stackList - .filter(e => e.isRed()) - .map(e => ( -
- - Stack Event -
- { e.name } -
- } - > - - + } */} + {/* {stackList + .filter((e) => e.isRed()) + .map((e) => ( +
+ + Stack Event +
+ {e.name} +
+ } + > + + +
+ ))} */}
- )) - } -
-
- ); - } +
+ ); + } } From 8805c84735969c97df7f2a629566c0968304035f Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Thu, 11 Aug 2022 15:58:33 +0200 Subject: [PATCH 12/16] feat(ui) - overview - error details modal --- .../app/components/Errors/Error/ErrorInfo.js | 149 ++++++------- .../components/Errors/Error/MainSection.js | 206 ++++++++---------- .../Session_/Exceptions/Exceptions.js | 2 +- .../Session_/OverviewPanel/OverviewPanel.tsx | 4 +- .../components/EventRow/EventRow.tsx | 8 +- .../TimelinePointer/TimelinePointer.tsx | 44 ++-- .../Session_/Player/Controls/Timeline.js | 2 +- .../ui/ErrorDetails/ErrorDetails.tsx | 11 +- 8 files changed, 201 insertions(+), 225 deletions(-) diff --git a/frontend/app/components/Errors/Error/ErrorInfo.js b/frontend/app/components/Errors/Error/ErrorInfo.js index b1d18ab45..8407826de 100644 --- a/frontend/app/components/Errors/Error/ErrorInfo.js +++ b/frontend/app/components/Errors/Error/ErrorInfo.js @@ -2,82 +2,77 @@ import React from 'react'; import { connect } from 'react-redux'; import withSiteIdRouter from 'HOCs/withSiteIdRouter'; import { errors as errorsRoute, error as errorRoute } from 'App/routes'; -import { NoContent , Loader, IconButton, Icon, Popup, BackLink, } from 'UI'; +import { NoContent, Loader, IconButton, Icon, Popup, BackLink } from 'UI'; import { fetch, fetchTrace } from 'Duck/errors'; import MainSection from './MainSection'; import SideSection from './SideSection'; import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; -@connect(state =>({ - errorIdInStore: state.getIn(["errors", "instance"]).errorId, - loading: state.getIn([ "errors", "fetch", "loading" ]) || state.getIn([ "errors", "fetchTrace", "loading" ]), - errorOnFetch: state.getIn(["errors", "fetch", "errors"]) || state.getIn([ "errors", "fetchTrace", "errors" ]), -}), { - fetch, - fetchTrace, -}) +@connect( + (state) => ({ + errorIdInStore: state.getIn(['errors', 'instance']).errorId, + loading: state.getIn(['errors', 'fetch', 'loading']) || state.getIn(['errors', 'fetchTrace', 'loading']), + errorOnFetch: state.getIn(['errors', 'fetch', 'errors']) || state.getIn(['errors', 'fetchTrace', 'errors']), + }), + { + fetch, + fetchTrace, + } +) @withSiteIdRouter export default class ErrorInfo extends React.PureComponent { - ensureInstance() { - const { errorId, loading, errorOnFetch } = this.props; - if (!loading && - this.props.errorIdInStore !== errorId && - errorId != null) { - this.props.fetch(errorId); - this.props.fetchTrace(errorId) - } - } - componentDidMount() { - this.ensureInstance(); - } - componentDidUpdate() { - this.ensureInstance(); - } - next = () => { - const { list, errorId } = this.props; - const curIndex = list.findIndex(e => e.errorId === errorId); - const next = list.get(curIndex + 1); - if (next != null) { - this.props.history.push(errorRoute(next.errorId)) - } - } - prev = () => { - const { list, errorId } = this.props; - const curIndex = list.findIndex(e => e.errorId === errorId); - const prev = list.get(curIndex - 1); - if (prev != null) { - this.props.history.push(errorRoute(prev.errorId)) - } - - } - render() { - const { - loading, - errorIdInStore, - list, - errorId, - } = this.props; + ensureInstance() { + const { errorId, loading, errorOnFetch } = this.props; + if (!loading && this.props.errorIdInStore !== errorId && errorId != null) { + this.props.fetch(errorId); + this.props.fetchTrace(errorId); + } + } + componentDidMount() { + this.ensureInstance(); + } + componentDidUpdate() { + this.ensureInstance(); + } + next = () => { + const { list, errorId } = this.props; + const curIndex = list.findIndex((e) => e.errorId === errorId); + const next = list.get(curIndex + 1); + if (next != null) { + this.props.history.push(errorRoute(next.errorId)); + } + }; + prev = () => { + const { list, errorId } = this.props; + const curIndex = list.findIndex((e) => e.errorId === errorId); + const prev = list.get(curIndex - 1); + if (prev != null) { + this.props.history.push(errorRoute(prev.errorId)); + } + }; + render() { + const { loading, errorIdInStore, list, errorId } = this.props; - let nextDisabled = true, - prevDisabled = true; - if (list.size > 0) { - nextDisabled = loading || list.last().errorId === errorId; - prevDisabled = loading || list.first().errorId === errorId; - } + let nextDisabled = true, + prevDisabled = true; + if (list.size > 0) { + nextDisabled = loading || list.last().errorId === errorId; + prevDisabled = loading || list.first().errorId === errorId; + } - return ( - - -
No Error Found!
-
- } - subtext="Please try to find existing one." - // animatedIcon="no-results" - show={ !loading && errorIdInStore == null } - > - {/*
+ return ( + + +
No Error Found!
+
+ } + subtext="Please try to find existing one." + // animatedIcon="no-results" + show={!loading && errorIdInStore == null} + > + {/*
@@ -111,13 +106,13 @@ export default class ErrorInfo extends React.PureComponent {
*/} -
- - - - -
- - ); - } -} \ No newline at end of file +
+ + + + +
+ + ); + } +} diff --git a/frontend/app/components/Errors/Error/MainSection.js b/frontend/app/components/Errors/Error/MainSection.js index 4a81ca062..534f417f8 100644 --- a/frontend/app/components/Errors/Error/MainSection.js +++ b/frontend/app/components/Errors/Error/MainSection.js @@ -5,105 +5,89 @@ import withSiteIdRouter from 'HOCs/withSiteIdRouter'; import { ErrorDetails, IconButton, Icon, Loader, Button } from 'UI'; import { sessions as sessionsRoute } from 'App/routes'; import { TYPES as EV_FILER_TYPES } from 'Types/filter/event'; -import { UNRESOLVED, RESOLVED, IGNORED } from "Types/errorInfo"; +import { UNRESOLVED, RESOLVED, IGNORED } from 'Types/errorInfo'; import { addFilterByKeyAndValue } from 'Duck/search'; -import { resolve,unresolve,ignore, toggleFavorite } from "Duck/errors"; +import { resolve, unresolve, ignore, toggleFavorite } from 'Duck/errors'; import { resentOrDate } from 'App/date'; import Divider from 'Components/Errors/ui/Divider'; import ErrorName from 'Components/Errors/ui/ErrorName'; import Label from 'Components/Errors/ui/Label'; -import SharePopup from 'Shared/SharePopup' +import SharePopup from 'Shared/SharePopup'; import { FilterKey } from 'Types/filter/filterType'; import SessionBar from './SessionBar'; @withSiteIdRouter -@connect(state => ({ - error: state.getIn([ "errors", "instance" ]), - trace: state.getIn([ "errors", "instanceTrace" ]), - sourcemapUploaded: state.getIn([ "errors", "sourcemapUploaded" ]), - resolveToggleLoading: state.getIn(["errors", "resolve", "loading"]) || - state.getIn(["errors", "unresolve", "loading"]), - ignoreLoading: state.getIn([ "errors", "ignore", "loading" ]), - toggleFavoriteLoading: state.getIn([ "errors", "toggleFavorite", "loading" ]), - traceLoading: state.getIn([ "errors", "fetchTrace", "loading"]), -}),{ - resolve, - unresolve, - ignore, - toggleFavorite, - addFilterByKeyAndValue, -}) +@connect( + (state) => ({ + error: state.getIn(['errors', 'instance']), + trace: state.getIn(['errors', 'instanceTrace']), + sourcemapUploaded: state.getIn(['errors', 'sourcemapUploaded']), + resolveToggleLoading: state.getIn(['errors', 'resolve', 'loading']) || state.getIn(['errors', 'unresolve', 'loading']), + ignoreLoading: state.getIn(['errors', 'ignore', 'loading']), + toggleFavoriteLoading: state.getIn(['errors', 'toggleFavorite', 'loading']), + traceLoading: state.getIn(['errors', 'fetchTrace', 'loading']), + }), + { + resolve, + unresolve, + ignore, + toggleFavorite, + addFilterByKeyAndValue, + } +) export default class MainSection extends React.PureComponent { - resolve = () => { - const { error } = this.props; - this.props.resolve(error.errorId) - } + resolve = () => { + const { error } = this.props; + this.props.resolve(error.errorId); + }; - unresolve = () => { - const { error } = this.props; - this.props.unresolve(error.errorId) - } + unresolve = () => { + const { error } = this.props; + this.props.unresolve(error.errorId); + }; - ignore = () => { - const { error } = this.props; - this.props.ignore(error.errorId) - } - bookmark = () => { - const { error } = this.props; - this.props.toggleFavorite(error.errorId); - } + ignore = () => { + const { error } = this.props; + this.props.ignore(error.errorId); + }; + bookmark = () => { + const { error } = this.props; + this.props.toggleFavorite(error.errorId); + }; - findSessions = () => { - this.props.addFilterByKeyAndValue(FilterKey.ERROR, this.props.error.message); - this.props.history.push(sessionsRoute()); - } + findSessions = () => { + this.props.addFilterByKeyAndValue(FilterKey.ERROR, this.props.error.message); + this.props.history.push(sessionsRoute()); + }; - render() { - const { - error, - trace, - sourcemapUploaded, - ignoreLoading, - resolveToggleLoading, - toggleFavoriteLoading, - className, - traceLoading, - } = this.props; + render() { + const { error, trace, sourcemapUploaded, ignoreLoading, resolveToggleLoading, toggleFavoriteLoading, className, traceLoading } = this.props; - return ( -
-
- -
-
- { error.message } -
-
-
-
-
Over the past 30 days
-
-
- -
+ return ( +
+
+ +
+
+ {error.message} +
+
+
+
+
Over the past 30 days
+
+
+
- {/* + {/*
{ error.status === UNRESOLVED ?
*/} - -
-

Last session with this error

- { resentOrDate(error.lastOccurrence) } - - -
- -
- - - -
- -
- ); - } -} \ No newline at end of file + +
+

Last session with this error

+ {resentOrDate(error.lastOccurrence)} + + +
+ +
+ + + +
+
+ ); + } +} diff --git a/frontend/app/components/Session_/Exceptions/Exceptions.js b/frontend/app/components/Session_/Exceptions/Exceptions.js index 0cdd9d01e..2f3b83483 100644 --- a/frontend/app/components/Session_/Exceptions/Exceptions.js +++ b/frontend/app/components/Session_/Exceptions/Exceptions.js @@ -64,7 +64,7 @@ export default class Exceptions extends React.PureComponent { show={ !loading && errorStack.size === 0 } title="Nothing found!" > - +
diff --git a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx index 53ba3ddf5..b5b9b91cd 100644 --- a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx +++ b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx @@ -36,10 +36,10 @@ function OverviewPanel(props: Props) { }; return ( - + Overview -
+
diff --git a/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx b/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx index 0447fcaa9..cff0c6182 100644 --- a/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx +++ b/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx @@ -10,7 +10,7 @@ interface Props { endTime?: number; renderElement?: (item: any) => React.ReactNode; } -function EventRow(props: Props) { +const EventRow = React.memo((props: Props) => { const { title, className, list = [], endTime = 0 } = props; const scale = 100 / endTime; const _list = React.useMemo(() => { @@ -22,8 +22,8 @@ function EventRow(props: Props) { }) }, [list]); return ( -
-
{title}
+
+
{title}
{_list.map((item: any, index: number) => { return ( @@ -36,7 +36,7 @@ function EventRow(props: Props) {
); -} +}); export default connectPlayer((state: any) => ({ endTime: state.endTime, diff --git a/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx b/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx index a9d58f4a2..c3cf175dd 100644 --- a/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx +++ b/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx @@ -2,16 +2,17 @@ import React from 'react'; import { connectPlayer, Controls } from 'App/player'; import { toggleBottomBlock, NETWORK, EXCEPTIONS, PERFORMANCE } from 'Duck/components/player'; import { useModal } from 'App/components/Modal'; -import { Icon, ErrorDetails } from 'UI'; +import { Icon, ErrorDetails, Popup } from 'UI'; import { Tooltip } from 'react-tippy'; import { TYPES as EVENT_TYPES } from 'Types/session/event'; import StackEventModal from '../StackEventModal'; +import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorDetailsModal'; interface Props { pointer: any; type: any; } -function TimelinePointer(props: Props) { +const TimelinePointer = React.memo((props: Props) => { const { showModal, hideModal } = useModal(); const createEventClickHandler = (pointer: any, type: any) => (e: any) => { e.stopPropagation(); @@ -20,8 +21,8 @@ function TimelinePointer(props: Props) { return; } - if (type === 'EXCEPTIONS') { - showModal(, { right: true }); + if (type === 'ERRORS') { + showModal(, { right: true }); } if (type === 'EVENT') { @@ -32,8 +33,8 @@ function TimelinePointer(props: Props) { const renderNetworkElement = (item: any) => { return ( - {item.success ? 'Slow resource: ' : 'Missing resource:'}
@@ -46,14 +47,14 @@ function TimelinePointer(props: Props) {
- + ); }; const renderClickRageElement = (item: any) => { return ( - {'Click Rage'}
@@ -64,14 +65,14 @@ function TimelinePointer(props: Props) {
-
+ ); }; const renderStackEventElement = (item: any) => { return ( - {'Stack Event'}
@@ -82,15 +83,14 @@ function TimelinePointer(props: Props) {
{/* */}
- + ); }; const renderPerformanceElement = (item: any) => { - console.log('item', item) return ( - {item.name}
@@ -101,14 +101,14 @@ function TimelinePointer(props: Props) {
{/* */}
- + ); }; const renderExceptionElement = (item: any) => { return ( - {'Exception'}
@@ -118,10 +118,10 @@ function TimelinePointer(props: Props) { delay={0} position="top" > -
+
- + ); }; @@ -145,6 +145,6 @@ function TimelinePointer(props: Props) { } }; return
{render()}
; -} +}); export default TimelinePointer; diff --git a/frontend/app/components/Session_/Player/Controls/Timeline.js b/frontend/app/components/Session_/Player/Controls/Timeline.js index 91603fbf5..0899f1d2a 100644 --- a/frontend/app/components/Session_/Player/Controls/Timeline.js +++ b/frontend/app/components/Session_/Player/Controls/Timeline.js @@ -12,7 +12,7 @@ import CustomDragLayer from './CustomDragLayer'; import { debounce } from 'App/utils'; import { Tooltip } from 'react-tippy'; -const BOUNDRY = 15; +const BOUNDRY = 0; function getTimelinePosition(value, scale) { const pos = value * scale; diff --git a/frontend/app/components/ui/ErrorDetails/ErrorDetails.tsx b/frontend/app/components/ui/ErrorDetails/ErrorDetails.tsx index 331535461..047c29f0a 100644 --- a/frontend/app/components/ui/ErrorDetails/ErrorDetails.tsx +++ b/frontend/app/components/ui/ErrorDetails/ErrorDetails.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import ErrorFrame from '../ErrorFrame/ErrorFrame'; import { fetchErrorStackList } from 'Duck/sessions'; -import { IconButton, Icon } from 'UI'; +import { Button, Icon } from 'UI'; import { connect } from 'react-redux'; const docLink = 'https://docs.openreplay.com/installation/upload-sourcemaps'; @@ -46,8 +46,12 @@ function ErrorDetails(props: Props) {

Stacktrace

- setShowRaw(false)} label="FULL" plain={!showRaw} primaryText={!showRaw} /> - setShowRaw(true)} plain={showRaw} label="RAW" /> + +
@@ -59,7 +63,6 @@ function ErrorDetails(props: Props) { {error.name} : {firstFunc ? firstFunc : '?'}
)} - ; {errorStack.map((frame: any, i: any) => (
From 930e502f2521fee9616d0dc14ebc956df9fadf77 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Thu, 11 Aug 2022 17:09:28 +0200 Subject: [PATCH 13/16] feat(ui) - overview - vertical line follow and jump --- .../Session_/OverviewPanel/OverviewPanel.tsx | 41 ++++--- .../OverviewPanelContainer.tsx | 44 +++++++ .../OverviewPanelContainer/index.ts | 1 + .../components/VerticalLine/VerticalLine.tsx | 15 +++ .../components/VerticalLine/index.ts | 1 + .../VerticalPointerLine.tsx | 3 +- .../Session_/Player/Controls/Timeline.js | 112 +++++++++--------- .../ui/ErrorDetails/ErrorDetails.tsx | 2 +- 8 files changed, 144 insertions(+), 75 deletions(-) create mode 100644 frontend/app/components/Session_/OverviewPanel/components/OverviewPanelContainer/OverviewPanelContainer.tsx create mode 100644 frontend/app/components/Session_/OverviewPanel/components/OverviewPanelContainer/index.ts create mode 100644 frontend/app/components/Session_/OverviewPanel/components/VerticalLine/VerticalLine.tsx create mode 100644 frontend/app/components/Session_/OverviewPanel/components/VerticalLine/index.ts diff --git a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx index b5b9b91cd..26fda0a30 100644 --- a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx +++ b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx @@ -10,6 +10,8 @@ import FeatureSelection from './components/FeatureSelection/FeatureSelection'; import TimelinePointer from './components/TimelinePointer'; import VerticalPointerLine from './components/VerticalPointerLine'; import cn from 'classnames'; +import VerticalLine from './components/VerticalLine'; +import OverviewPanelContainer from './components/OverviewPanelContainer'; interface Props { resourceList: any[]; @@ -21,22 +23,24 @@ interface Props { } function OverviewPanel(props: Props) { const { resourceList, exceptionsList, eventsList, stackEventList, issuesList } = props; + const clickRageList = React.useMemo(() => { return eventsList.filter((item: any) => item.type === TYPES.CLICKRAGE); }, [eventsList]); - // const scale = 100 / endTime; const [selectedFeatures, setSelectedFeatures] = React.useState(['PERFORMANCE', 'ERRORS', 'EVENTS']); - const resources: any = { - NETWORK: resourceList, - ERRORS: exceptionsList, - EVENTS: stackEventList, - CLICKRAGE: clickRageList, - PERFORMANCE: issuesList, - }; + const resources: any = React.useMemo(() => { + return { + NETWORK: resourceList, + ERRORS: exceptionsList, + EVENTS: stackEventList, + CLICKRAGE: clickRageList, + PERFORMANCE: issuesList, + }; + }, [resourceList, exceptionsList, stackEventList, clickRageList, issuesList]); return ( - + Overview
@@ -44,9 +48,9 @@ function OverviewPanel(props: Props) {
-
+ -
+
{selectedFeatures.map((feature: any, index: number) => (
@@ -59,17 +63,20 @@ function OverviewPanel(props: Props) {
))}
-
+
); } -export default connect((state: any) => ({ - issuesList: state.getIn(['sessions', 'current', 'issues']), -}), { - toggleBottomBlock, -})( +export default connect( + (state: any) => ({ + issuesList: state.getIn(['sessions', 'current', 'issues']), + }), + { + toggleBottomBlock, + } +)( connectPlayer((state: any) => ({ resourceList: state.resourceList.filter((r: any) => r.isRed() || r.isYellow()), exceptionsList: state.exceptionsList, diff --git a/frontend/app/components/Session_/OverviewPanel/components/OverviewPanelContainer/OverviewPanelContainer.tsx b/frontend/app/components/Session_/OverviewPanel/components/OverviewPanelContainer/OverviewPanelContainer.tsx new file mode 100644 index 000000000..979e8bb53 --- /dev/null +++ b/frontend/app/components/Session_/OverviewPanel/components/OverviewPanelContainer/OverviewPanelContainer.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import VerticalLine from '../VerticalLine'; +import { connectPlayer, Controls } from 'App/player'; + +interface Props { + children: React.ReactNode; + endTime: number; +} + +function OverviewPanelContainer(props: Props) { + const { endTime } = props; + const [mouseX, setMouseX] = React.useState(0); + const [mouseIn, setMouseIn] = React.useState(false); + const onClickTrack = (e: any) => { + const p = e.nativeEvent.offsetX / e.target.offsetWidth; + const time = Math.max(Math.round(p * endTime), 0); + Controls.jump(time); + }; + + const onMouseMoveCapture = (e: any) => { + if (!mouseIn) { + return; + } + const p = e.nativeEvent.offsetX / e.target.offsetWidth; + setMouseX(p * 100); + }; + + return ( +
setMouseIn(true)} + // onMouseOut={() => setMouseIn(false)} + > + {mouseIn && } +
{props.children}
+
+ ); +} + +export default connectPlayer((state: any) => ({ + endTime: state.endTime, +}))(OverviewPanelContainer); diff --git a/frontend/app/components/Session_/OverviewPanel/components/OverviewPanelContainer/index.ts b/frontend/app/components/Session_/OverviewPanel/components/OverviewPanelContainer/index.ts new file mode 100644 index 000000000..788665588 --- /dev/null +++ b/frontend/app/components/Session_/OverviewPanel/components/OverviewPanelContainer/index.ts @@ -0,0 +1 @@ +export { default } from './OverviewPanelContainer'; \ No newline at end of file diff --git a/frontend/app/components/Session_/OverviewPanel/components/VerticalLine/VerticalLine.tsx b/frontend/app/components/Session_/OverviewPanel/components/VerticalLine/VerticalLine.tsx new file mode 100644 index 000000000..43a536f13 --- /dev/null +++ b/frontend/app/components/Session_/OverviewPanel/components/VerticalLine/VerticalLine.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import cn from 'classnames'; + +interface Props { + left: number; + className?: string; + height?: string; + width?: string; +} +function VerticalLine(props: Props) { + const { left, className = 'border-gray-dark', height = '221px', width = '1px' } = props; + return
; +} + +export default VerticalLine; diff --git a/frontend/app/components/Session_/OverviewPanel/components/VerticalLine/index.ts b/frontend/app/components/Session_/OverviewPanel/components/VerticalLine/index.ts new file mode 100644 index 000000000..423077b49 --- /dev/null +++ b/frontend/app/components/Session_/OverviewPanel/components/VerticalLine/index.ts @@ -0,0 +1 @@ +export { default } from './VerticalLine' \ No newline at end of file diff --git a/frontend/app/components/Session_/OverviewPanel/components/VerticalPointerLine/VerticalPointerLine.tsx b/frontend/app/components/Session_/OverviewPanel/components/VerticalPointerLine/VerticalPointerLine.tsx index b8997e111..8db015447 100644 --- a/frontend/app/components/Session_/OverviewPanel/components/VerticalPointerLine/VerticalPointerLine.tsx +++ b/frontend/app/components/Session_/OverviewPanel/components/VerticalPointerLine/VerticalPointerLine.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { connectPlayer } from 'App/player'; +import VerticalLine from '../VerticalLine'; interface Props { time: number; @@ -8,7 +9,7 @@ interface Props { function VerticalPointerLine(props: Props) { const { time, scale } = props; const left = time * scale; - return
; + return ; } export default connectPlayer((state: any) => ({ diff --git a/frontend/app/components/Session_/Player/Controls/Timeline.js b/frontend/app/components/Session_/Player/Controls/Timeline.js index 0899f1d2a..e6879671d 100644 --- a/frontend/app/components/Session_/Player/Controls/Timeline.js +++ b/frontend/app/components/Session_/Player/Controls/Timeline.js @@ -1,8 +1,8 @@ import React from 'react'; import { connect } from 'react-redux'; -import cn from 'classnames'; +// import cn from 'classnames'; import { connectPlayer, Controls } from 'Player'; -import { TimelinePointer, Icon } from 'UI'; +// import { TimelinePointer, Icon } from 'UI'; import TimeTracker from './TimeTracker'; import stl from './timeline.module.css'; import { TYPES } from 'Types/session/event'; @@ -10,7 +10,7 @@ import { setTimelinePointer } from 'Duck/sessions'; import DraggableCircle from './DraggableCircle'; import CustomDragLayer from './CustomDragLayer'; import { debounce } from 'App/utils'; -import { Tooltip } from 'react-tippy'; +// import { Tooltip } from 'react-tippy'; const BOUNDRY = 0; @@ -20,46 +20,46 @@ function getTimelinePosition(value, scale) { return pos > 100 ? 100 : pos; } -const getPointerIcon = (type) => { - // exception, - switch (type) { - case 'fetch': - return 'funnel/file-earmark-minus-fill'; - case 'exception': - return 'funnel/exclamation-circle-fill'; - case 'log': - return 'funnel/exclamation-circle-fill'; - case 'stack': - return 'funnel/patch-exclamation-fill'; - case 'resource': - return 'funnel/file-earmark-minus-fill'; +// const getPointerIcon = (type) => { +// // exception, +// switch (type) { +// case 'fetch': +// return 'funnel/file-earmark-minus-fill'; +// case 'exception': +// return 'funnel/exclamation-circle-fill'; +// case 'log': +// return 'funnel/exclamation-circle-fill'; +// case 'stack': +// return 'funnel/patch-exclamation-fill'; +// case 'resource': +// return 'funnel/file-earmark-minus-fill'; - case 'dead_click': - return 'funnel/dizzy'; - case 'click_rage': - return 'funnel/dizzy'; - case 'excessive_scrolling': - return 'funnel/mouse'; - case 'bad_request': - return 'funnel/file-medical-alt'; - case 'missing_resource': - return 'funnel/file-earmark-minus-fill'; - case 'memory': - return 'funnel/sd-card'; - case 'cpu': - return 'funnel/microchip'; - case 'slow_resource': - return 'funnel/hourglass-top'; - case 'slow_page_load': - return 'funnel/hourglass-top'; - case 'crash': - return 'funnel/file-exclamation'; - case 'js_exception': - return 'funnel/exclamation-circle-fill'; - } +// case 'dead_click': +// return 'funnel/dizzy'; +// case 'click_rage': +// return 'funnel/dizzy'; +// case 'excessive_scrolling': +// return 'funnel/mouse'; +// case 'bad_request': +// return 'funnel/file-medical-alt'; +// case 'missing_resource': +// return 'funnel/file-earmark-minus-fill'; +// case 'memory': +// return 'funnel/sd-card'; +// case 'cpu': +// return 'funnel/microchip'; +// case 'slow_resource': +// return 'funnel/hourglass-top'; +// case 'slow_page_load': +// return 'funnel/hourglass-top'; +// case 'crash': +// return 'funnel/file-exclamation'; +// case 'js_exception': +// return 'funnel/exclamation-circle-fill'; +// } - return 'info'; -}; +// return 'info'; +// }; let deboucneJump = () => null; @connectPlayer((state) => ({ @@ -73,11 +73,11 @@ let deboucneJump = () => null; disabled: state.cssLoading || state.messagesLoading || state.markedTargets, endTime: state.endTime, live: state.live, - logList: state.logList, - exceptionsList: state.exceptionsList, - resourceList: state.resourceList, - stackList: state.stackList, - fetchList: state.fetchList, + // logList: state.logList, + // exceptionsList: state.exceptionsList, + // resourceList: state.resourceList, + // stackList: state.stackList, + // fetchList: state.fetchList, })) @connect( (state) => ({ @@ -141,14 +141,14 @@ export default class Timeline extends React.PureComponent { skipIntervals, disabled, endTime, - live, - logList, - exceptionsList, - resourceList, - clickRageTime, - stackList, - fetchList, - issues, + // live, + // logList, + // exceptionsList, + // resourceList, + // clickRageTime, + // stackList, + // fetchList, + // issues, } = this.props; const scale = 100 / endTime; @@ -175,9 +175,9 @@ export default class Timeline extends React.PureComponent { /> ))}
- {/* {events.map((e) => ( + {events.map((e) => (
- ))} */} + ))} {/* {issues.map((iss) => (
+
{!sourcemapUploaded && (
Date: Thu, 11 Aug 2022 17:51:10 +0200 Subject: [PATCH 14/16] feat(ui) - overview - stack event modal --- .../StackEventModal/StackEventModal.tsx | 22 +++++- .../components/JsonViewer/JsonViewer.js | 15 +++++ .../components/JsonViewer/index.ts | 1 + .../components/Sentry/Sentry.js | 67 +++++++++++++++++++ .../components/Sentry/index.ts | 1 + .../components/Sentry/sentry.module.css | 47 +++++++++++++ 6 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 frontend/app/components/Session_/OverviewPanel/components/StackEventModal/components/JsonViewer/JsonViewer.js create mode 100644 frontend/app/components/Session_/OverviewPanel/components/StackEventModal/components/JsonViewer/index.ts create mode 100644 frontend/app/components/Session_/OverviewPanel/components/StackEventModal/components/Sentry/Sentry.js create mode 100644 frontend/app/components/Session_/OverviewPanel/components/StackEventModal/components/Sentry/index.ts create mode 100644 frontend/app/components/Session_/OverviewPanel/components/StackEventModal/components/Sentry/sentry.module.css diff --git a/frontend/app/components/Session_/OverviewPanel/components/StackEventModal/StackEventModal.tsx b/frontend/app/components/Session_/OverviewPanel/components/StackEventModal/StackEventModal.tsx index 07d163dbe..76490900a 100644 --- a/frontend/app/components/Session_/OverviewPanel/components/StackEventModal/StackEventModal.tsx +++ b/frontend/app/components/Session_/OverviewPanel/components/StackEventModal/StackEventModal.tsx @@ -1,12 +1,30 @@ import React from 'react'; +import JsonViewer from './components/JsonViewer'; +import Sentry from './components/Sentry'; +import { OPENREPLAY, SENTRY, DATADOG, STACKDRIVER } from 'Types/session/stackEvent'; interface Props { event: any; } function StackEventModal(props: Props) { + const renderPopupContent = () => { + const { + event: { source, payload, name }, + } = props; + switch (source) { + case SENTRY: + return ; + case DATADOG: + return ; + case STACKDRIVER: + return ; + default: + return ; + } + }; return ( -
- Content +
+ {renderPopupContent()}
); } diff --git a/frontend/app/components/Session_/OverviewPanel/components/StackEventModal/components/JsonViewer/JsonViewer.js b/frontend/app/components/Session_/OverviewPanel/components/StackEventModal/components/JsonViewer/JsonViewer.js new file mode 100644 index 000000000..985191896 --- /dev/null +++ b/frontend/app/components/Session_/OverviewPanel/components/StackEventModal/components/JsonViewer/JsonViewer.js @@ -0,0 +1,15 @@ +import React from 'react'; +import { Icon, JSONTree } from 'UI'; + +export default class JsonViewer extends React.PureComponent { + render() { + const { data, title, icon } = this.props; + return ( +
+ +

{title}

+ +
+ ); + } +} diff --git a/frontend/app/components/Session_/OverviewPanel/components/StackEventModal/components/JsonViewer/index.ts b/frontend/app/components/Session_/OverviewPanel/components/StackEventModal/components/JsonViewer/index.ts new file mode 100644 index 000000000..155729246 --- /dev/null +++ b/frontend/app/components/Session_/OverviewPanel/components/StackEventModal/components/JsonViewer/index.ts @@ -0,0 +1 @@ +export { default } from './JsonViewer'; \ No newline at end of file diff --git a/frontend/app/components/Session_/OverviewPanel/components/StackEventModal/components/Sentry/Sentry.js b/frontend/app/components/Session_/OverviewPanel/components/StackEventModal/components/Sentry/Sentry.js new file mode 100644 index 000000000..0e1ea0747 --- /dev/null +++ b/frontend/app/components/Session_/OverviewPanel/components/StackEventModal/components/Sentry/Sentry.js @@ -0,0 +1,67 @@ +import React from 'react'; +import { getIn, get } from 'immutable'; +import cn from 'classnames'; +import { withRequest } from 'HOCs'; +import { Loader, Icon, JSONTree } from 'UI'; +import { Accordion } from 'semantic-ui-react'; +import stl from './sentry.module.css'; + +@withRequest({ + endpoint: (props) => `/integrations/sentry/events/${props.event.id}`, + dataName: 'detailedEvent', + loadOnInitialize: true, +}) +export default class SentryEventInfo extends React.PureComponent { + makePanelsFromStackTrace(stacktrace) { + return get(stacktrace, 'frames', []).map(({ filename, function: method, lineNo, context = [] }) => ({ + key: `${filename}_${method}_${lineNo}`, + title: { + content: ( + + {filename} + {' in '} + {method} + {' at line '} + {lineNo} + + ), + }, + content: { + content: ( +
    + {context.map(([ctxLineNo, codeText]) => ( +
  1. {codeText}
  2. + ))} +
+ ), + }, + })); + } + + renderBody() { + const { detailedEvent, requestError, event } = this.props; + + const exceptionEntry = get(detailedEvent, ['entries'], []).find(({ type }) => type === 'exception'); + const stacktraces = getIn(exceptionEntry, ['data', 'values']); + if (!stacktraces) { + return ; + } + return stacktraces.map(({ type, value, stacktrace }) => ( +
+
{type}
+

{value}

+ +
+ )); + } + + render() { + const { open, toggleOpen, loading } = this.props; + return ( +
+ + {this.renderBody()} +
+ ); + } +} diff --git a/frontend/app/components/Session_/OverviewPanel/components/StackEventModal/components/Sentry/index.ts b/frontend/app/components/Session_/OverviewPanel/components/StackEventModal/components/Sentry/index.ts new file mode 100644 index 000000000..534162c8b --- /dev/null +++ b/frontend/app/components/Session_/OverviewPanel/components/StackEventModal/components/Sentry/index.ts @@ -0,0 +1 @@ +export { default } from './Sentry'; \ No newline at end of file diff --git a/frontend/app/components/Session_/OverviewPanel/components/StackEventModal/components/Sentry/sentry.module.css b/frontend/app/components/Session_/OverviewPanel/components/StackEventModal/components/Sentry/sentry.module.css new file mode 100644 index 000000000..75956a074 --- /dev/null +++ b/frontend/app/components/Session_/OverviewPanel/components/StackEventModal/components/Sentry/sentry.module.css @@ -0,0 +1,47 @@ + +.wrapper { + padding: 20px 40px 30px; +} +.icon { + margin-left: -5px; +} +.stacktrace { + & h6 { + display: flex; + align-items: center; + font-size: 17px; + padding-top: 7px; + margin-bottom: 10px; + } + & p { + font-family: 'Menlo', 'monaco', 'consolas', monospace; + } +} + + +.accordionTitle { + font-weight: 100; + & > b { + font-weight: 700; + } +} + +.lineList { + list-style-position: inside; + list-style-type: decimal-leading-zero; + background: $gray-lightest; +} + +.codeLine { + font-family: 'Menlo', 'monaco', 'consolas', monospace; + line-height: 24px; + font-size: 12px; + white-space: pre-wrap; + word-wrap: break-word; + min-height: 24px; + padding: 0 25px; + &.highlighted { + background: $red; + color: $white; + } +} \ No newline at end of file From f7c0cf11fb4d978e9c605b8b0b6e193180b1cf56 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Tue, 16 Aug 2022 14:49:20 +0200 Subject: [PATCH 15/16] feat(ui) - overview - toggle button and other optimizations --- frontend/app/components/Header/Header.js | 1 + frontend/app/components/Session/LivePlayer.js | 2 +- frontend/app/components/Session/WebPlayer.js | 2 +- .../Session_/EventsBlock/EventsBlock.js | 2 +- .../Session_/OverviewPanel/OverviewPanel.tsx | 92 +- .../components/EventRow/EventRow.tsx | 55 +- .../OverviewPanelContainer.tsx | 32 +- .../PerformanceGraph/PerformanceGraph.tsx | 83 ++ .../components/PerformanceGraph/index.ts | 1 + .../TimelinePointer/TimelinePointer.tsx | 4 +- .../TimelineScale/TimelineScale.tsx | 60 +- .../Session_/Player/Controls/Controls.js | 872 +++++++++--------- .../Session_/Player/Controls/Timeline.js | 733 ++++++++------- .../Controls/components/PlayerControls.tsx | 155 ++-- .../app/components/Session_/Player/Player.js | 4 +- .../shared/XRayButton/XRayButton.tsx | 18 + .../app/components/shared/XRayButton/index.ts | 1 + .../shared/XRayButton/xrayButton.module.css | 17 + frontend/app/utils.ts | 6 + 19 files changed, 1151 insertions(+), 989 deletions(-) create mode 100644 frontend/app/components/Session_/OverviewPanel/components/PerformanceGraph/PerformanceGraph.tsx create mode 100644 frontend/app/components/Session_/OverviewPanel/components/PerformanceGraph/index.ts create mode 100644 frontend/app/components/shared/XRayButton/XRayButton.tsx create mode 100644 frontend/app/components/shared/XRayButton/index.ts create mode 100644 frontend/app/components/shared/XRayButton/xrayButton.module.css diff --git a/frontend/app/components/Header/Header.js b/frontend/app/components/Header/Header.js index 1b5b97c22..7212c35c3 100644 --- a/frontend/app/components/Header/Header.js +++ b/frontend/app/components/Header/Header.js @@ -122,6 +122,7 @@ const Header = (props) => {
    +
diff --git a/frontend/app/components/Session/LivePlayer.js b/frontend/app/components/Session/LivePlayer.js index c142167f0..0c07b134b 100644 --- a/frontend/app/components/Session/LivePlayer.js +++ b/frontend/app/components/Session/LivePlayer.js @@ -49,7 +49,7 @@ function LivePlayer ({ }, []) const TABS = { - EVENTS: 'Events', + EVENTS: 'User Actions', HEATMAPS: 'Click Map', } const [activeTab, setActiveTab] = useState(''); diff --git a/frontend/app/components/Session/WebPlayer.js b/frontend/app/components/Session/WebPlayer.js index 1285ac9b1..cef4d4bae 100644 --- a/frontend/app/components/Session/WebPlayer.js +++ b/frontend/app/components/Session/WebPlayer.js @@ -14,7 +14,7 @@ import styles from '../Session_/session.module.css'; import { countDaysFrom } from 'App/date'; const TABS = { - EVENTS: 'Events', + EVENTS: 'User Actions', HEATMAPS: 'Click Map', }; diff --git a/frontend/app/components/Session_/EventsBlock/EventsBlock.js b/frontend/app/components/Session_/EventsBlock/EventsBlock.js index 1bc4419d4..e690ce3cc 100644 --- a/frontend/app/components/Session_/EventsBlock/EventsBlock.js +++ b/frontend/app/components/Session_/EventsBlock/EventsBlock.js @@ -197,7 +197,7 @@ export default class EventsBlock extends React.PureComponent { setActiveTab={setActiveTab} value={query} header={ -
User Events { events.size }
+
User Actions { events.size }
} />
diff --git a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx index 26fda0a30..4cadd1fad 100644 --- a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx +++ b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx @@ -1,6 +1,6 @@ import { connectPlayer } from 'App/player'; import { toggleBottomBlock } from 'Duck/components/player'; -import React from 'react'; +import React, { useEffect } from 'react'; import BottomBlock from '../BottomBlock'; import EventRow from './components/EventRow'; import { TYPES } from 'Types/session/event'; @@ -10,7 +10,7 @@ import FeatureSelection from './components/FeatureSelection/FeatureSelection'; import TimelinePointer from './components/TimelinePointer'; import VerticalPointerLine from './components/VerticalPointerLine'; import cn from 'classnames'; -import VerticalLine from './components/VerticalLine'; +// import VerticalLine from './components/VerticalLine'; import OverviewPanelContainer from './components/OverviewPanelContainer'; interface Props { @@ -20,52 +20,67 @@ interface Props { toggleBottomBlock: any; stackEventList: any[]; issuesList: any[]; + performanceChartData: any; + endTime: number; } function OverviewPanel(props: Props) { - const { resourceList, exceptionsList, eventsList, stackEventList, issuesList } = props; - - const clickRageList = React.useMemo(() => { - return eventsList.filter((item: any) => item.type === TYPES.CLICKRAGE); - }, [eventsList]); + const [dataLoaded, setDataLoaded] = React.useState(false); const [selectedFeatures, setSelectedFeatures] = React.useState(['PERFORMANCE', 'ERRORS', 'EVENTS']); const resources: any = React.useMemo(() => { + const { resourceList, exceptionsList, eventsList, stackEventList, issuesList, performanceChartData } = props; return { NETWORK: resourceList, ERRORS: exceptionsList, EVENTS: stackEventList, - CLICKRAGE: clickRageList, - PERFORMANCE: issuesList, + CLICKRAGE: eventsList.filter((item: any) => item.type === TYPES.CLICKRAGE), + PERFORMANCE: performanceChartData, }; - }, [resourceList, exceptionsList, stackEventList, clickRageList, issuesList]); + }, [dataLoaded]); + + useEffect(() => { + if (dataLoaded) { + return; + } + + if (props.resourceList.length > 0) { + setDataLoaded(true); + } + }, [props.resourceList]); return ( - - - Overview -
- -
-
- - - -
- - {selectedFeatures.map((feature: any, index: number) => ( -
- } - /> + dataLoaded && ( + + + + X-RAY +
+ +
+
+ + + +
+ + {selectedFeatures.map((feature: any, index: number) => ( +
+ } + endTime={props.endTime} + /> +
+ ))}
- ))} -
- - - + + + + + ) ); } @@ -82,5 +97,12 @@ export default connect( exceptionsList: state.exceptionsList, eventsList: state.eventList, stackEventList: state.stackList, + performanceChartData: state.performanceChartData, + endTime: state.endTime, + // endTime: 30000000, }))(OverviewPanel) ); + +const Wrapper = React.memo((props: any) => { + return
{props.children}
; +}); diff --git a/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx b/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx index cff0c6182..ee7bcb857 100644 --- a/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx +++ b/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx @@ -1,43 +1,48 @@ import React from 'react'; -import cn from 'classnames' +import cn from 'classnames'; import { getTimelinePosition } from 'App/utils'; import { connectPlayer } from 'App/player'; - +import PerformanceGraph from '../PerformanceGraph'; interface Props { list?: any[]; title: string; className?: string; endTime?: number; renderElement?: (item: any) => React.ReactNode; + isGraph?: boolean; } const EventRow = React.memo((props: Props) => { - const { title, className, list = [], endTime = 0 } = props; + const { title, className, list = [], endTime = 0, isGraph = false } = props; const scale = 100 / endTime; - const _list = React.useMemo(() => { - return list.map((item: any, _index: number) => { - return { - ...item.toJS(), - left: getTimelinePosition(item.time, scale), - } - }) - }, [list]); + const _list = + !isGraph && + React.useMemo(() => { + return list.map((item: any, _index: number) => { + return { + ...item.toJS(), + left: getTimelinePosition(item.time, scale), + }; + }); + }, [list]); + return ( -
-
{title}
-
- {_list.map((item: any, index: number) => { - return ( -
- {props.renderElement ? props.renderElement(item) : null} -
- ) - } - )} +
+
{title}
+
+ {isGraph ? ( + + ) : ( + _list.map((item: any, index: number) => { + return ( +
+ {props.renderElement ? props.renderElement(item) : null} +
+ ); + }) + )}
); }); -export default connectPlayer((state: any) => ({ - endTime: state.endTime, -}))(EventRow); \ No newline at end of file +export default EventRow; diff --git a/frontend/app/components/Session_/OverviewPanel/components/OverviewPanelContainer/OverviewPanelContainer.tsx b/frontend/app/components/Session_/OverviewPanel/components/OverviewPanelContainer/OverviewPanelContainer.tsx index 979e8bb53..5a898c67e 100644 --- a/frontend/app/components/Session_/OverviewPanel/components/OverviewPanelContainer/OverviewPanelContainer.tsx +++ b/frontend/app/components/Session_/OverviewPanel/components/OverviewPanelContainer/OverviewPanelContainer.tsx @@ -7,29 +7,31 @@ interface Props { endTime: number; } -function OverviewPanelContainer(props: Props) { +const OverviewPanelContainer = React.memo((props: Props) => { const { endTime } = props; const [mouseX, setMouseX] = React.useState(0); const [mouseIn, setMouseIn] = React.useState(false); const onClickTrack = (e: any) => { const p = e.nativeEvent.offsetX / e.target.offsetWidth; const time = Math.max(Math.round(p * endTime), 0); - Controls.jump(time); + if (time) { + Controls.jump(time); + } }; - const onMouseMoveCapture = (e: any) => { - if (!mouseIn) { - return; - } - const p = e.nativeEvent.offsetX / e.target.offsetWidth; - setMouseX(p * 100); - }; + // const onMouseMoveCapture = (e: any) => { + // if (!mouseIn) { + // return; + // } + // const p = e.nativeEvent.offsetX / e.target.offsetWidth; + // setMouseX(p * 100); + // }; return (
setMouseIn(true)} // onMouseOut={() => setMouseIn(false)} > @@ -37,8 +39,10 @@ function OverviewPanelContainer(props: Props) {
{props.children}
); -} +}); -export default connectPlayer((state: any) => ({ - endTime: state.endTime, -}))(OverviewPanelContainer); +export default OverviewPanelContainer; + +// export default connectPlayer((state: any) => ({ +// endTime: state.endTime, +// }))(OverviewPanelContainer); diff --git a/frontend/app/components/Session_/OverviewPanel/components/PerformanceGraph/PerformanceGraph.tsx b/frontend/app/components/Session_/OverviewPanel/components/PerformanceGraph/PerformanceGraph.tsx new file mode 100644 index 000000000..28193cd10 --- /dev/null +++ b/frontend/app/components/Session_/OverviewPanel/components/PerformanceGraph/PerformanceGraph.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { connectPlayer } from 'App/player'; +import { AreaChart, Area, Tooltip, ResponsiveContainer } from 'recharts'; + +interface Props { + list: any; +} +const PerformanceGraph = React.memo((props: Props) => { + const { list } = props; + + const finalValues = React.useMemo(() => { + const cpuMax = list.reduce((acc: number, item: any) => { + return Math.max(acc, item.cpu); + }, 0); + const cpuMin = list.reduce((acc: number, item: any) => { + return Math.min(acc, item.cpu); + }, Infinity); + + const memoryMin = list.reduce((acc: number, item: any) => { + return Math.min(acc, item.usedHeap); + }, Infinity); + const memoryMax = list.reduce((acc: number, item: any) => { + return Math.max(acc, item.usedHeap); + }, 0); + + const convertToPercentage = (val: number, max: number, min: number) => { + return ((val - min) / (max - min)) * 100; + }; + const cpuValues = list.map((item: any) => convertToPercentage(item.cpu, cpuMax, cpuMin)); + const memoryValues = list.map((item: any) => convertToPercentage(item.usedHeap, memoryMax, memoryMin)); + const mergeArraysWithMaxNumber = (arr1: any[], arr2: any[]) => { + const maxLength = Math.max(arr1.length, arr2.length); + const result = []; + for (let i = 0; i < maxLength; i++) { + const num = Math.round(Math.max(arr1[i] || 0, arr2[i] || 0)); + result.push(num > 60 ? num : 1); + } + return result; + }; + const finalValues = mergeArraysWithMaxNumber(cpuValues, memoryValues); + return finalValues; + }, []); + + const data = list.map((item: any, index: number) => { + return { + time: item.time, + cpu: finalValues[index], + }; + }); + + return ( + + + + + + + + + {/* */} + + + + ); +}); + +export default PerformanceGraph; diff --git a/frontend/app/components/Session_/OverviewPanel/components/PerformanceGraph/index.ts b/frontend/app/components/Session_/OverviewPanel/components/PerformanceGraph/index.ts new file mode 100644 index 000000000..2c5c88675 --- /dev/null +++ b/frontend/app/components/Session_/OverviewPanel/components/PerformanceGraph/index.ts @@ -0,0 +1 @@ +export { default } from './PerformanceGraph'; \ No newline at end of file diff --git a/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx b/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx index c3cf175dd..6e45b5e99 100644 --- a/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx +++ b/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx @@ -45,7 +45,7 @@ const TimelinePointer = React.memo((props: Props) => { position="top" >
-
+
); @@ -92,7 +92,7 @@ const TimelinePointer = React.memo((props: Props) => { - {item.name} + {item.type}
} delay={0} diff --git a/frontend/app/components/Session_/OverviewPanel/components/TimelineScale/TimelineScale.tsx b/frontend/app/components/Session_/OverviewPanel/components/TimelineScale/TimelineScale.tsx index e0b4be0b7..3b7fc453e 100644 --- a/frontend/app/components/Session_/OverviewPanel/components/TimelineScale/TimelineScale.tsx +++ b/frontend/app/components/Session_/OverviewPanel/components/TimelineScale/TimelineScale.tsx @@ -1,14 +1,60 @@ import React from 'react'; +import { connectPlayer } from 'App/player'; +import { millisToMinutesAndSeconds } from 'App/utils'; interface Props { - + endTime: number; } function TimelineScale(props: Props) { - return ( -
- -
- ); + const { endTime } = props; + const scaleRef = React.useRef(null); + const gap = 60; + + const drawScale = (container: any) => { + const width = container.offsetWidth; + const part = Math.round(width / gap); + container.replaceChildren(); + for (var i = 0; i < part; i++) { + const txt = millisToMinutesAndSeconds(i * (endTime / part)); + const el = document.createElement('div'); + // el.style.height = '10px'; + // el.style.width = '1px'; + // el.style.backgroundColor = '#ccc'; + el.style.position = 'absolute'; + el.style.left = `${i * gap}px`; + el.style.paddingTop = '1px'; + el.style.opacity = '0.8'; + el.innerHTML = txt + ''; + el.style.fontSize = '12px'; + el.style.color = 'white'; + + container.appendChild(el); + } + }; + + React.useEffect(() => { + if (!scaleRef.current) { + return; + } + + drawScale(scaleRef.current); + + // const resize = () => drawScale(scaleRef.current); + + // window.addEventListener('resize', resize); + // return () => { + // window.removeEventListener('resize', resize); + // }; + }, [scaleRef]); + return ( +
+ {/*
*/} +
+ ); } -export default TimelineScale; \ No newline at end of file +export default TimelineScale; + +// export default connectPlayer((state: any) => ({ +// endTime: state.endTime, +// }))(TimelineScale); diff --git a/frontend/app/components/Session_/Player/Controls/Controls.js b/frontend/app/components/Session_/Player/Controls/Controls.js index e2197e6b3..ec0fa4ab3 100644 --- a/frontend/app/components/Session_/Player/Controls/Controls.js +++ b/frontend/app/components/Session_/Player/Controls/Controls.js @@ -1,344 +1,342 @@ import React from 'react'; import cn from 'classnames'; import { connect } from 'react-redux'; -import { - connectPlayer, - STORAGE_TYPES, - selectStorageType, - selectStorageListNow, -} from 'Player/store'; +import { connectPlayer, STORAGE_TYPES, selectStorageType, selectStorageListNow } from 'Player/store'; import LiveTag from 'Shared/LiveTag'; -import { session as sessionRoute, withSiteId } from 'App/routes'; -import { - toggleTimetravel, - jumpToLive, -} from 'Player'; +import { toggleTimetravel, jumpToLive } from 'Player'; -import { Icon } from 'UI'; +import { Icon, Button } from 'UI'; import { toggleInspectorMode } from 'Player'; import { - fullscreenOn, - fullscreenOff, - toggleBottomBlock, - OVERVIEW, - CONSOLE, - NETWORK, - STACKEVENTS, - STORAGE, - PROFILER, - PERFORMANCE, - GRAPHQL, - FETCH, - EXCEPTIONS, - INSPECTOR, + fullscreenOn, + fullscreenOff, + toggleBottomBlock, + OVERVIEW, + CONSOLE, + NETWORK, + STACKEVENTS, + STORAGE, + PROFILER, + PERFORMANCE, + GRAPHQL, + FETCH, + EXCEPTIONS, + INSPECTOR, } from 'Duck/components/player'; import { ReduxTime, AssistDuration } from './Time'; import Timeline from './Timeline'; import ControlButton from './ControlButton'; -import PlayerControls from './components/PlayerControls' +import PlayerControls from './components/PlayerControls'; import styles from './controls.module.css'; import { Tooltip } from 'react-tippy'; - +import XRayButton from 'Shared/XRayButton'; function getStorageIconName(type) { - switch(type) { - case STORAGE_TYPES.REDUX: - return "vendors/redux"; - case STORAGE_TYPES.MOBX: - return "vendors/mobx" - case STORAGE_TYPES.VUEX: - return "vendors/vuex"; - case STORAGE_TYPES.NGRX: - return "vendors/ngrx"; - case STORAGE_TYPES.NONE: - return "store" - } + 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.NONE: + return 'store'; + } } 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.NONE: - return "STATE"; - } + 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.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.logListNow.length, - logRedCount: state.logRedCountNow, - resourceRedCount: state.resourceRedCountNow, - fetchRedCount: state.fetchRedCountNow, - showStack: state.stackList.length > 0, - stackCount: state.stackListNow.length, - stackRedCount: state.stackRedCountNow, - profilesCount: state.profilesListNow.length, - storageCount: selectStorageListNow(state).length, - storageType: selectStorageType(state), - showStorage: selectStorageType(state) !== STORAGE_TYPES.NONE, - showProfiler: state.profilesList.length > 0, - showGraphql: state.graphqlList.length > 0, - showFetch: state.fetchCount > 0, - fetchCount: state.fetchCountNow, - graphqlCount: state.graphqlListNow.length, - exceptionsCount: state.exceptionsListNow.length, - showExceptions: state.exceptionsList.length > 0, - showLongtasks: state.longtasksList.length > 0, - liveTimeTravel: state.liveTimeTravel, +@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.logListNow.length, + logRedCount: state.logRedCountNow, + resourceRedCount: state.resourceRedCountNow, + fetchRedCount: state.fetchRedCountNow, + showStack: state.stackList.length > 0, + stackCount: state.stackListNow.length, + stackRedCount: state.stackRedCountNow, + profilesCount: state.profilesListNow.length, + storageCount: selectStorageListNow(state).length, + storageType: selectStorageType(state), + showStorage: selectStorageType(state) !== STORAGE_TYPES.NONE, + showProfiler: state.profilesList.length > 0, + showGraphql: state.graphqlList.length > 0, + showFetch: state.fetchCount > 0, + fetchCount: state.fetchCountNow, + graphqlCount: state.graphqlListNow.length, + exceptionsCount: state.exceptionsListNow.length, + showExceptions: state.exceptionsList.length > 0, + showLongtasks: state.longtasksList.length > 0, + 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' ]), - } -}, { - fullscreenOn, - fullscreenOff, - toggleBottomBlock, -}) +@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']), + }; + }, + { + fullscreenOn, + fullscreenOff, + toggleBottomBlock, + } +) 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.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.showExceptions !== this.props.showExceptions || - nextProps.exceptionsCount !== this.props.exceptionsCount || - nextProps.showLongtasks !== this.props.showLongtasks || - nextProps.liveTimeTravel !== this.props.liveTimeTravel - ) return true; - return false; - } - - onKeyDown = (e) => { - if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { - return; + componentDidMount() { + document.addEventListener('keydown', this.onKeyDown); } - if (this.props.inspectorMode) { - if (e.key === 'Esc' || e.key === 'Escape') { - toggleInspectorMode(false); - } + + 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.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.showExceptions !== this.props.showExceptions || + nextProps.exceptionsCount !== this.props.exceptionsCount || + nextProps.showLongtasks !== this.props.showLongtasks || + nextProps.liveTimeTravel !== this.props.liveTimeTravel + ) + 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(); + } }; - // 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 } = this.props; - jump(Math.min(endTime, time + 1e4)) - } + forthTenSeconds = () => { + const { time, endTime, jump } = this.props; + jump(Math.min(endTime, time + 1e4)); + }; - backTenSeconds = () => { //shouldComponentUpdate - const { time, jump } = this.props; - jump(Math.max(0, time - 1e4)); - } + backTenSeconds = () => { + //shouldComponentUpdate + const { time, jump } = this.props; + jump(Math.max(0, time - 1e4)); + }; - goLive =() => this.props.jump(this.props.endTime) + 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' - } + 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 ( - -
- -
-
- ) - } - - controlIcon = (icon, size, action, isBackwards, additionalClasses) => -
- -
- - render() { - const { - bottomBlock, - toggleBottomBlock, - live, - livePlay, - skip, - speed, - disabled, - logCount, - logRedCount, - resourceRedCount, - fetchRedCount, - showStack, - stackCount, - stackRedCount, - profilesCount, - storageCount, - showStorage, - storageType, - showProfiler, - showGraphql, - showFetch, - fetchCount, - graphqlCount, - exceptionsCount, - showExceptions, - fullscreen, - inspectorMode, - closedLive, - toggleSpeed, - toggleSkip, - liveTimeTravel, - } = this.props; - - const toggleBottomTools = (blockName) => { - if (blockName === INSPECTOR) { - toggleInspectorMode(); - bottomBlock && toggleBottomBlock(); - } else { - toggleInspectorMode(false); - toggleBottomBlock(blockName); - } - } - - return ( -
- { !live || liveTimeTravel ? : null} - { !fullscreen && -
-
- {!live && ( - - )} - - { live && !closedLive && ( -
- livePlay ? null : jumpToLive()} /> -
- - {!liveTimeTravel && ( -
- See Past Activity -
- )} + return ( + +
+
- )} -
+ + ); + }; -
- { !live &&
} - {/* ! TEMP DISABLED ! + controlIcon = (icon, size, action, isBackwards, additionalClasses) => ( +
+ +
+ ); + + render() { + const { + bottomBlock, + toggleBottomBlock, + live, + livePlay, + skip, + speed, + disabled, + logCount, + logRedCount, + resourceRedCount, + fetchRedCount, + showStack, + stackCount, + stackRedCount, + profilesCount, + storageCount, + showStorage, + storageType, + showProfiler, + showGraphql, + showFetch, + fetchCount, + graphqlCount, + exceptionsCount, + showExceptions, + fullscreen, + inspectorMode, + closedLive, + toggleSpeed, + toggleSkip, + liveTimeTravel, + } = this.props; + + const toggleBottomTools = (blockName) => { + if (blockName === INSPECTOR) { + toggleInspectorMode(); + bottomBlock && toggleBottomBlock(); + } else { + toggleInspectorMode(false); + toggleBottomBlock(blockName); + } + }; + + return ( +
+ {!live || liveTimeTravel ? ( + + ) : null} + {!fullscreen && ( +
+
+ {!live && ( + <> + + {/* */} +
+ toggleBottomTools(OVERVIEW)} /> + + )} + + {live && !closedLive && ( +
+ (livePlay ? null : jumpToLive())} /> +
+ +
+ + {!liveTimeTravel && ( +
+ See Past Activity +
+ )} +
+ )} +
+ +
+ {/* { !live &&
} */} + {/* ! TEMP DISABLED ! {!live && ( )} */} - toggleBottomTools(OVERVIEW) } active={ bottomBlock === OVERVIEW && !inspectorMode} @@ -360,132 +358,132 @@ export default class Controls extends React.Component { // count={ logCount } // hasErrors={ logRedCount > 0 } containerClassName="mx-2" - /> - toggleBottomTools(CONSOLE) } - active={ bottomBlock === CONSOLE && !inspectorMode} - label="CONSOLE" - noIcon - labelClassName="!text-base font-semibold" - count={ logCount } - hasErrors={ logRedCount > 0 } - containerClassName="mx-2" - /> - { !live && - toggleBottomTools(NETWORK) } - active={ bottomBlock === NETWORK && !inspectorMode } - label="NETWORK" - hasErrors={ resourceRedCount > 0 } - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" - /> - } - {!live && - toggleBottomTools(PERFORMANCE) } - active={ bottomBlock === PERFORMANCE && !inspectorMode } - label="PERFORMANCE" - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" - /> - } - {showFetch && - toggleBottomTools(FETCH) } - active={ bottomBlock === FETCH && !inspectorMode } - hasErrors={ fetchRedCount > 0 } - count={ fetchCount } - label="FETCH" - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" - /> - } - { !live && showGraphql && - toggleBottomTools(GRAPHQL) } - active={ bottomBlock === GRAPHQL && !inspectorMode } - count={ graphqlCount } - label="GRAPHQL" - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" - /> - } - { !live && showStorage && - toggleBottomTools(STORAGE) } - active={ bottomBlock === STORAGE && !inspectorMode } - count={ storageCount } - label={ getStorageName(storageType) } - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" - /> - } - { showExceptions && - toggleBottomTools(EXCEPTIONS) } - active={ bottomBlock === EXCEPTIONS && !inspectorMode } - label="EXCEPTIONS" - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" - count={ exceptionsCount } - hasErrors={ exceptionsCount > 0 } - /> - } - { !live && showStack && - toggleBottomTools(STACKEVENTS) } - active={ bottomBlock === STACKEVENTS && !inspectorMode } - label="EVENTS" - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" - count={ stackCount } - hasErrors={ stackRedCount > 0 } - /> - } - { !live && showProfiler && - toggleBottomTools(PROFILER) } - active={ bottomBlock === PROFILER && !inspectorMode } - count={ profilesCount } - label="PROFILER" - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" - /> - } - { !live &&
} - { !live && ( - - {this.controlIcon("arrows-angle-extend", 18, this.props.fullscreenOn, false, "rounded hover:bg-gray-light-shade color-gray-medium")} - - ) - } + /> */} + toggleBottomTools(CONSOLE)} + active={bottomBlock === CONSOLE && !inspectorMode} + label="CONSOLE" + noIcon + labelClassName="!text-base font-semibold" + count={logCount} + hasErrors={logRedCount > 0} + containerClassName="mx-2" + /> + {!live && ( + toggleBottomTools(NETWORK)} + active={bottomBlock === NETWORK && !inspectorMode} + label="NETWORK" + hasErrors={resourceRedCount > 0} + noIcon + labelClassName="!text-base font-semibold" + containerClassName="mx-2" + /> + )} + {!live && ( + toggleBottomTools(PERFORMANCE)} + active={bottomBlock === PERFORMANCE && !inspectorMode} + label="PERFORMANCE" + noIcon + labelClassName="!text-base font-semibold" + containerClassName="mx-2" + /> + )} + {showFetch && ( + toggleBottomTools(FETCH)} + active={bottomBlock === FETCH && !inspectorMode} + hasErrors={fetchRedCount > 0} + count={fetchCount} + label="FETCH" + noIcon + labelClassName="!text-base font-semibold" + containerClassName="mx-2" + /> + )} + {!live && showGraphql && ( + toggleBottomTools(GRAPHQL)} + active={bottomBlock === GRAPHQL && !inspectorMode} + count={graphqlCount} + label="GRAPHQL" + noIcon + labelClassName="!text-base font-semibold" + containerClassName="mx-2" + /> + )} + {!live && showStorage && ( + toggleBottomTools(STORAGE)} + active={bottomBlock === STORAGE && !inspectorMode} + count={storageCount} + label={getStorageName(storageType)} + noIcon + labelClassName="!text-base font-semibold" + containerClassName="mx-2" + /> + )} + {showExceptions && ( + toggleBottomTools(EXCEPTIONS)} + active={bottomBlock === EXCEPTIONS && !inspectorMode} + label="EXCEPTIONS" + noIcon + labelClassName="!text-base font-semibold" + containerClassName="mx-2" + count={exceptionsCount} + hasErrors={exceptionsCount > 0} + /> + )} + {!live && showStack && ( + toggleBottomTools(STACKEVENTS)} + active={bottomBlock === STACKEVENTS && !inspectorMode} + label="EVENTS" + noIcon + labelClassName="!text-base font-semibold" + containerClassName="mx-2" + count={stackCount} + hasErrors={stackRedCount > 0} + /> + )} + {!live && showProfiler && ( + toggleBottomTools(PROFILER)} + active={bottomBlock === PROFILER && !inspectorMode} + count={profilesCount} + label="PROFILER" + noIcon + labelClassName="!text-base font-semibold" + containerClassName="mx-2" + /> + )} + {!live &&
} + {!live && ( + + {this.controlIcon( + 'arrows-angle-extend', + 18, + this.props.fullscreenOn, + false, + 'rounded hover:bg-gray-light-shade color-gray-medium' + )} + + )} +
+
+ )}
-
- } -
- ); - } + ); + } } diff --git a/frontend/app/components/Session_/Player/Controls/Timeline.js b/frontend/app/components/Session_/Player/Controls/Timeline.js index c177c14e0..5ae8d67d2 100644 --- a/frontend/app/components/Session_/Player/Controls/Timeline.js +++ b/frontend/app/components/Session_/Player/Controls/Timeline.js @@ -13,393 +13,392 @@ import { debounce } from 'App/utils'; import { Tooltip } from 'react-tippy'; import TooltipContainer from './components/TooltipContainer'; -const BOUNDRY = 15 +const BOUNDRY = 0; function getTimelinePosition(value, scale) { - const pos = value * scale; + const pos = value * scale; - return pos > 100 ? 100 : pos; + return pos > 100 ? 100 : pos; } const getPointerIcon = (type) => { - // exception, - switch(type) { - case 'fetch': - return 'funnel/file-earmark-minus-fill'; - case 'exception': - return 'funnel/exclamation-circle-fill'; - case 'log': - return 'funnel/exclamation-circle-fill'; - case 'stack': - return 'funnel/patch-exclamation-fill'; - case 'resource': - return 'funnel/file-earmark-minus-fill'; + // exception, + switch (type) { + case 'fetch': + return 'funnel/file-earmark-minus-fill'; + case 'exception': + return 'funnel/exclamation-circle-fill'; + case 'log': + return 'funnel/exclamation-circle-fill'; + case 'stack': + return 'funnel/patch-exclamation-fill'; + case 'resource': + return 'funnel/file-earmark-minus-fill'; - case 'dead_click': - return 'funnel/dizzy'; - case 'click_rage': - return 'funnel/dizzy'; - case 'excessive_scrolling': - return 'funnel/mouse'; - case 'bad_request': - return 'funnel/file-medical-alt'; - case 'missing_resource': - return 'funnel/file-earmark-minus-fill'; - case 'memory': - return 'funnel/sd-card'; - case 'cpu': - return 'funnel/microchip'; - case 'slow_resource': - return 'funnel/hourglass-top'; - case 'slow_page_load': - return 'funnel/hourglass-top'; - case 'crash': - return 'funnel/file-exclamation'; - case 'js_exception': - return 'funnel/exclamation-circle-fill'; - } - - return 'info'; -} + case 'dead_click': + return 'funnel/dizzy'; + case 'click_rage': + return 'funnel/dizzy'; + case 'excessive_scrolling': + return 'funnel/mouse'; + case 'bad_request': + return 'funnel/file-medical-alt'; + case 'missing_resource': + return 'funnel/file-earmark-minus-fill'; + case 'memory': + return 'funnel/sd-card'; + case 'cpu': + return 'funnel/microchip'; + case 'slow_resource': + return 'funnel/hourglass-top'; + case 'slow_page_load': + return 'funnel/hourglass-top'; + case 'crash': + return 'funnel/file-exclamation'; + case 'js_exception': + return 'funnel/exclamation-circle-fill'; + } + return 'info'; +}; let deboucneJump = () => null; let debounceTooltipChange = () => null; -@connectPlayer(state => ({ - playing: state.playing, - time: state.time, - skipIntervals: state.skipIntervals, - events: state.eventList, - skip: state.skip, - // not updating properly rn - // skipToIssue: state.skipToIssue, - disabled: state.cssLoading || state.messagesLoading || state.markedTargets, - endTime: state.endTime, - live: state.live, - logList: state.logList, - exceptionsList: state.exceptionsList, - resourceList: state.resourceList, - stackList: state.stackList, - fetchList: state.fetchList, +@connectPlayer((state) => ({ + playing: state.playing, + time: state.time, + skipIntervals: state.skipIntervals, + events: state.eventList, + skip: state.skip, + // not updating properly rn + // skipToIssue: state.skipToIssue, + disabled: state.cssLoading || state.messagesLoading || state.markedTargets, + endTime: state.endTime, + live: state.live, + logList: state.logList, + exceptionsList: state.exceptionsList, + resourceList: state.resourceList, + stackList: state.stackList, + fetchList: state.fetchList, })) -@connect(state => ({ - issues: state.getIn([ 'sessions', 'current', 'issues' ]), - clickRageTime: state.getIn([ 'sessions', 'current', 'clickRage' ]) && - state.getIn([ 'sessions', 'current', 'clickRageTime' ]), - returningLocationTime: state.getIn([ 'sessions', 'current', 'returningLocation' ]) && - state.getIn([ 'sessions', 'current', 'returningLocationTime' ]), - tooltipVisible: state.getIn(['sessions', 'timeLineTooltip', 'isVisible']) -}), { setTimelinePointer, setTimelineHoverTime }) +@connect( + (state) => ({ + issues: state.getIn(['sessions', 'current', 'issues']), + clickRageTime: state.getIn(['sessions', 'current', 'clickRage']) && state.getIn(['sessions', 'current', 'clickRageTime']), + returningLocationTime: + state.getIn(['sessions', 'current', 'returningLocation']) && state.getIn(['sessions', 'current', 'returningLocationTime']), + tooltipVisible: state.getIn(['sessions', 'timeLineTooltip', 'isVisible']), + }), + { setTimelinePointer, setTimelineHoverTime } +) export default class Timeline extends React.PureComponent { - progressRef = React.createRef() - timelineRef = React.createRef() - wasPlaying = false + progressRef = React.createRef(); + timelineRef = React.createRef(); + wasPlaying = false; - seekProgress = (e) => { - const time = this.getTime(e) - this.props.jump(time); - this.hideTimeTooltip() - } + seekProgress = (e) => { + const time = this.getTime(e); + this.props.jump(time); + this.hideTimeTooltip(); + }; - getTime = (e) => { - const { endTime } = this.props; - const p = e.nativeEvent.offsetX / e.target.offsetWidth; - const time = Math.max(Math.round(p * endTime), 0); + getTime = (e) => { + const { endTime } = this.props; + const p = e.nativeEvent.offsetX / e.target.offsetWidth; + const time = Math.max(Math.round(p * endTime), 0); - return time - } + return time; + }; - createEventClickHandler = pointer => (e) => { - e.stopPropagation(); - this.props.jump(pointer.time); - this.props.setTimelinePointer(pointer); - } + createEventClickHandler = (pointer) => (e) => { + e.stopPropagation(); + this.props.jump(pointer.time); + this.props.setTimelinePointer(pointer); + }; - componentDidMount() { - const { issues } = this.props; - const skipToIssue = Controls.updateSkipToIssue(); - const firstIssue = issues.get(0); - deboucneJump = debounce(this.props.jump, 500); - debounceTooltipChange = debounce(this.props.setTimelineHoverTime, 50); + componentDidMount() { + const { issues } = this.props; + const skipToIssue = Controls.updateSkipToIssue(); + const firstIssue = issues.get(0); + deboucneJump = debounce(this.props.jump, 500); + debounceTooltipChange = debounce(this.props.setTimelineHoverTime, 50); - if (firstIssue && skipToIssue) { - this.props.jump(firstIssue.time); + if (firstIssue && skipToIssue) { + this.props.jump(firstIssue.time); + } } - } - onDragEnd = () => { - if (this.wasPlaying) { - this.props.togglePlay(); + onDragEnd = () => { + if (this.wasPlaying) { + this.props.togglePlay(); + } + }; + + onDrag = (offset) => { + const { endTime } = this.props; + + const p = (offset.x - BOUNDRY) / this.progressRef.current.offsetWidth; + const time = Math.max(Math.round(p * endTime), 0); + deboucneJump(time); + this.hideTimeTooltip(); + if (this.props.playing) { + this.wasPlaying = true; + this.props.pause(); + } + }; + + showTimeTooltip = (e) => { + if (e.target !== this.progressRef.current && e.target !== this.timelineRef.current) { + return this.props.tooltipVisible && this.hideTimeTooltip(); + } + const time = this.getTime(e); + const { endTime, liveTimeTravel } = this.props; + + const timeLineTooltip = { + time: liveTimeTravel ? endTime - time : time, + offset: e.nativeEvent.offsetX, + isVisible: true, + }; + debounceTooltipChange(timeLineTooltip); + }; + + hideTimeTooltip = () => { + const timeLineTooltip = { isVisible: false }; + debounceTooltipChange(timeLineTooltip); + }; + + render() { + const { + events, + skip, + skipIntervals, + disabled, + endTime, + exceptionsList, + resourceList, + clickRageTime, + stackList, + fetchList, + issues, + liveTimeTravel, + } = this.props; + + const scale = 100 / endTime; + + return ( +
+
+ + {/* custo color is live */} + + + + + {skip && + skipIntervals.map((interval) => ( +
+ ))} +
+ + {events.map((e) => ( +
+ ))} + {/* {issues.map((iss) => ( +
+ + {iss.name} +
+ } + > + + +
+ ))} + {events + .filter((e) => e.type === TYPES.CLICKRAGE) + .map((e) => ( +
+ + {'Click Rage'} +
+ } + > + + +
+ ))} + {typeof clickRageTime === 'number' && ( +
+ + {'Click Rage'} +
+ } + > + + +
+ )} + {exceptionsList.map((e) => ( +
+ + {'Exception'} +
+ {e.message} +
+ } + > + + +
+ ))} + {resourceList + .filter((r) => r.isRed() || r.isYellow()) + .map((r) => ( +
+ + {r.success ? 'Slow resource: ' : 'Missing resource:'} +
+ {r.name} +
+ } + > + + +
+ ))} + {fetchList + .filter((e) => e.isRed()) + .map((e) => ( +
+ + Failed Fetch +
+ {e.name} +
+ } + > + + +
+ ))} + {stackList + .filter((e) => e.isRed()) + .map((e) => ( +
+ + Stack Event +
+ {e.name} +
+ } + > + + +
+ ))} */} +
+
+ ); } - } - - onDrag = (offset) => { - const { endTime } = this.props; - - const p = (offset.x - BOUNDRY) / this.progressRef.current.offsetWidth; - const time = Math.max(Math.round(p * endTime), 0); - deboucneJump(time); - this.hideTimeTooltip(); - if (this.props.playing) { - this.wasPlaying = true; - this.props.pause(); - } - } - - showTimeTooltip = (e) => { - if (e.target !== this.progressRef.current && e.target !== this.timelineRef.current) { - return this.props.tooltipVisible && this.hideTimeTooltip() - } - const time = this.getTime(e); - const { endTime, liveTimeTravel } = this.props; - - const timeLineTooltip = { - time: liveTimeTravel ? endTime - time : time, - offset: e.nativeEvent.offsetX, - isVisible: true - } - debounceTooltipChange(timeLineTooltip) - } - - hideTimeTooltip = () => { - const timeLineTooltip = { isVisible: false } - debounceTooltipChange(timeLineTooltip) - } - - render() { - const { - events, - skip, - skipIntervals, - disabled, - endTime, - exceptionsList, - resourceList, - clickRageTime, - stackList, - fetchList, - issues, - liveTimeTravel, - } = this.props; - - const scale = 100 / endTime; - - return ( -
-
- - {/* custo color is live */} - - - - - { skip && skipIntervals.map(interval => - (
)) - } -
- - { events.map(e => ( -
- )) - } - { - issues.map(iss => ( -
- - { iss.name } -
- } - > - - -
- )) - } - { events.filter(e => e.type === TYPES.CLICKRAGE).map(e => ( -
- - { "Click Rage" } -
- } - > - - -
- ))} - {typeof clickRageTime === 'number' && -
- - { "Click Rage" } -
- } - > - - -
- } - { exceptionsList - .map(e => ( -
- - { "Exception" } -
- { e.message } -
- } - > - - -
- )) - } - { resourceList - .filter(r => r.isRed() || r.isYellow()) - .map(r => ( -
- - { r.success ? "Slow resource: " : "Missing resource:" } -
- { r.name } -
- } - > - - -
- )) - } - { fetchList - .filter(e => e.isRed()) - .map(e => ( -
- - Failed Fetch -
- { e.name } -
- } - > - - -
- )) - } - { stackList - .filter(e => e.isRed()) - .map(e => ( -
- - Stack Event -
- { e.name } -
- } - > - - -
- )) - } -
-
- ); - } } diff --git a/frontend/app/components/Session_/Player/Controls/components/PlayerControls.tsx b/frontend/app/components/Session_/Player/Controls/components/PlayerControls.tsx index be3ac24b3..5b07012df 100644 --- a/frontend/app/components/Session_/Player/Controls/components/PlayerControls.tsx +++ b/frontend/app/components/Session_/Player/Controls/components/PlayerControls.tsx @@ -1,111 +1,72 @@ -import React from 'react' +import React from 'react'; import { Tooltip } from 'react-tippy'; import { ReduxTime } from '../Time'; import { Icon } from 'UI'; import cn from 'classnames'; // @ts-ignore -import styles from '../controls.module.css' +import styles from '../controls.module.css'; interface Props { - live: boolean; - skip: boolean; - speed: number; - disabled: boolean; - playButton: JSX.Element; - backTenSeconds: () => void; - forthTenSeconds: () => void; - toggleSpeed: () => void; - toggleSkip: () => void; - controlIcon: (icon: string, size: number, action: () => void, isBackwards: boolean, additionalClasses: string) => JSX.Element; + live: boolean; + skip: boolean; + speed: number; + disabled: boolean; + playButton: JSX.Element; + backTenSeconds: () => void; + forthTenSeconds: () => void; + toggleSpeed: () => void; + toggleSkip: () => void; + controlIcon: (icon: string, size: number, action: () => void, isBackwards: boolean, additionalClasses: string) => JSX.Element; } function PlayerControls(props: Props) { - const { - live, - skip, - speed, - disabled, - playButton, - backTenSeconds, - forthTenSeconds, - toggleSpeed, - toggleSkip, - controlIcon - } = props; - return ( -
- {playButton} - {!live && ( -
- {/* @ts-ignore */} - - / - {/* @ts-ignore */} - + const { live, skip, speed, disabled, playButton, backTenSeconds, forthTenSeconds, toggleSpeed, toggleSkip, controlIcon } = props; + return ( +
+ {playButton} + {!live && ( +
+ {/* @ts-ignore */} + + / + {/* @ts-ignore */} + +
+ )} + +
+ {/* @ts-ignore */} + + {controlIcon('skip-forward-fill', 18, backTenSeconds, true, 'hover:bg-active-blue-border color-main h-full flex items-center')} + +
10s
+ {/* @ts-ignore */} + + {controlIcon('skip-forward-fill', 18, forthTenSeconds, false, 'hover:bg-active-blue-border color-main h-full flex items-center')} + +
+ + {!live && ( +
+ {/* @ts-ignore */} + + + + + +
+ )}
- )} - -
- {/* @ts-ignore */} - - {controlIcon( - "skip-forward-fill", - 18, - backTenSeconds, - true, - 'hover:bg-active-blue-border color-main h-full flex items-center' - )} - -
10s
- {/* @ts-ignore */} - - {controlIcon( - "skip-forward-fill", - 18, - forthTenSeconds, - false, - 'hover:bg-active-blue-border color-main h-full flex items-center' - )} - -
- - {!live && -
- {/* @ts-ignore */} - - - - - -
- } -
- ) + ); } export default PlayerControls; diff --git a/frontend/app/components/Session_/Player/Player.js b/frontend/app/components/Session_/Player/Player.js index ef69d5c2d..9a95121ac 100644 --- a/frontend/app/components/Session_/Player/Player.js +++ b/frontend/app/components/Session_/Player/Player.js @@ -104,9 +104,9 @@ export default class Player extends React.PureComponent { ref={ this.screenWrapper } />
- { // !fullscreen && !!bottomBlock && + { !fullscreen && !!bottomBlock &&
- { // bottomBlock === OVERVIEW && + { bottomBlock === OVERVIEW && } { bottomBlock === CONSOLE && diff --git a/frontend/app/components/shared/XRayButton/XRayButton.tsx b/frontend/app/components/shared/XRayButton/XRayButton.tsx new file mode 100644 index 000000000..2d66ed283 --- /dev/null +++ b/frontend/app/components/shared/XRayButton/XRayButton.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import stl from './xrayButton.module.css'; +import cn from 'classnames'; + +interface Props { + onClick?: () => void; + isActive?: boolean; +} +function XRayButton(props: Props) { + const { isActive } = props; + return ( + + ); +} + +export default XRayButton; diff --git a/frontend/app/components/shared/XRayButton/index.ts b/frontend/app/components/shared/XRayButton/index.ts new file mode 100644 index 000000000..45f067067 --- /dev/null +++ b/frontend/app/components/shared/XRayButton/index.ts @@ -0,0 +1 @@ +export { default } from './XRayButton'; \ No newline at end of file diff --git a/frontend/app/components/shared/XRayButton/xrayButton.module.css b/frontend/app/components/shared/XRayButton/xrayButton.module.css new file mode 100644 index 000000000..3b3f76ca4 --- /dev/null +++ b/frontend/app/components/shared/XRayButton/xrayButton.module.css @@ -0,0 +1,17 @@ +.wrapper { + text-align: center; + padding: 4px 14px; + border: none; + border-radius: 6px; + font-weight: 500; + + &.default { + color: white; + background: linear-gradient(90deg, rgba(57, 78, 255, 0.87) 0%, rgba(62, 170, 175, 0.87) 100%); + } + + &.active { + background: rgba(63, 81, 181, 0.08); + color: $gray-darkest; + } +} diff --git a/frontend/app/utils.ts b/frontend/app/utils.ts index 517ab5e72..1e738024c 100644 --- a/frontend/app/utils.ts +++ b/frontend/app/utils.ts @@ -356,3 +356,9 @@ export function getTimelinePosition(value: any, scale: any) { const pos = value * scale; return pos > 100 ? 100 : pos; } + +export function millisToMinutesAndSeconds(millis: any) { + const minutes = Math.floor(millis / 60000); + const seconds: any = ((millis % 60000) / 1000).toFixed(0); + return minutes + 'm' + (seconds < 10 ? '0' : '') + seconds + 's'; +} \ No newline at end of file From 32450c49de649a36abb45a8d6598d128689be4ce Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Tue, 16 Aug 2022 15:06:51 +0200 Subject: [PATCH 16/16] change(ui) - player controls margin --- .../Player/Controls/components/PlayerControls.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/app/components/Session_/Player/Controls/components/PlayerControls.tsx b/frontend/app/components/Session_/Player/Controls/components/PlayerControls.tsx index 9f813ce93..4a8276482 100644 --- a/frontend/app/components/Session_/Player/Controls/components/PlayerControls.tsx +++ b/frontend/app/components/Session_/Player/Controls/components/PlayerControls.tsx @@ -83,7 +83,10 @@ function PlayerControls(props: Props) {
{/* @ts-ignore */} -
{/* @ts-ignore */} -
{!live && ( -
+
{/* @ts-ignore */}