From 96453e96e5b99147e2e53627c5fa181ba4c049e5 Mon Sep 17 00:00:00 2001 From: Delirium Date: Thu, 21 Mar 2024 10:40:36 +0100 Subject: [PATCH] feat ui: change in player controls, move ai summary button, refactor old code etc (#1978) * feat(ui): rework for player look * remove unused code * move summary button and block inside xray * move class * fixup mobile controls panel * change notes, change xray feat selection --- .../Player/LivePlayer/LiveControls.tsx | 5 +- .../Player/MobilePlayer/MobileControls.tsx | 78 ++++-- .../Player/MobilePlayer/MobileOverlay.tsx | 7 +- .../Player/ReplayPlayer/AiSubheader.tsx | 46 ---- .../Player/ReplayPlayer/PlayerBlock.tsx | 9 +- .../ReplayPlayer/SummaryBlock/index.tsx | 13 +- .../Session_/EventsBlock/EventGroupWrapper.js | 3 - .../Session_/EventsBlock/NoteEvent.tsx | 43 +-- .../app/components/Session_/Issues/Issues.js | 22 +- .../Session_/OverviewPanel/OverviewPanel.tsx | 256 +++++++++++------- .../FeatureSelection/FeatureSelection.tsx | 93 +++++-- .../Player/Controls/ControlButton.tsx | 43 +-- .../Session_/Player/Controls/Controls.tsx | 156 +++++++---- .../components/ControlsComponents.tsx | 194 +++++++++++++ .../Player/Controls/components/CreateNote.tsx | 131 ++++----- .../Controls/components/KeyboardHelp.tsx | 11 +- .../Controls/components/PlayerControls.tsx | 216 +++------------ .../Controls/components/PlayingTime.tsx | 128 +++++---- .../components/Session_/Player/Overlay.tsx | 6 +- .../Session_/QueueControls/QueueControls.tsx | 35 ++- frontend/app/components/Session_/Subheader.js | 56 ++-- .../Session_/components/NotePopup.tsx | 42 +-- .../shared/AutoplayToggle/AutoplayToggle.tsx | 10 +- .../components/shared/Bookmark/Bookmark.tsx | 30 +- frontend/app/duck/sessions.ts | 14 - frontend/app/mstore/aiSummaryStore.ts | 5 + frontend/app/mstore/sessionStore.ts | 4 - frontend/app/player-ui/FullScreenButton.tsx | 20 +- frontend/app/player-ui/SkipButton.tsx | 11 +- frontend/app/services/AiService.ts | 2 +- frontend/app/services/NotesService.ts | 6 +- 31 files changed, 891 insertions(+), 804 deletions(-) create mode 100644 frontend/app/components/Session_/Player/Controls/components/ControlsComponents.tsx diff --git a/frontend/app/components/Session/Player/LivePlayer/LiveControls.tsx b/frontend/app/components/Session/Player/LivePlayer/LiveControls.tsx index bcf8366ff..647380fd4 100644 --- a/frontend/app/components/Session/Player/LivePlayer/LiveControls.tsx +++ b/frontend/app/components/Session/Player/LivePlayer/LiveControls.tsx @@ -118,11 +118,8 @@ function Controls(props: any) { toggleBottomTools(CONSOLE)} active={bottomBlock === CONSOLE} - label="CONSOLE" - noIcon - labelClassName="!text-base font-semibold" + label="Console" hasErrors={logRedCount > 0 || showExceptions} - containerClassName="mx-2" /> diff --git a/frontend/app/components/Session/Player/MobilePlayer/MobileControls.tsx b/frontend/app/components/Session/Player/MobilePlayer/MobileControls.tsx index f048712dc..c8fbea5c9 100644 --- a/frontend/app/components/Session/Player/MobilePlayer/MobileControls.tsx +++ b/frontend/app/components/Session/Player/MobilePlayer/MobileControls.tsx @@ -1,6 +1,11 @@ -import React from 'react'; -import cn from 'classnames'; -import { connect } from 'react-redux'; +import { + LaunchConsoleShortcut, LaunchEventsShortcut, + LaunchNetworkShortcut, LaunchPerformanceShortcut, + LaunchXRaShortcut +} from "Components/Session_/Player/Controls/components/KeyboardHelp"; +import React from 'react'; +import cn from 'classnames'; +import { connect } from 'react-redux'; import { PlayButton, PlayingState, FullScreenButton } from 'App/player-ui'; import { Tooltip } from 'UI'; @@ -95,7 +100,6 @@ function Controls(props: any) { return (
- {!fullscreen && (
@@ -115,13 +119,9 @@ function Controls(props: any) { startedAt={session.startedAt} />
- toggleBottomTools(OVERVIEW)} - />
-
+
+ +
Get a quick overview on the issues in this session.
+
+ } + label={'X-Ray'} + onClick={() => toggleBottomTools(OVERVIEW)} + active={bottomBlock === OVERVIEW} + /> + + +
Launch Logs
+
+ } disabled={messagesLoading} onClick={() => toggleBottomTools(CONSOLE)} active={bottomBlock === CONSOLE} - label="LOGS" - noIcon - labelClassName="!text-base font-semibold" + label="Logs" hasErrors={logMarkedCountNow > 0 || showExceptions} - containerClassName="mx-2" /> + +
Launch Network
+
+ } disabled={messagesLoading} onClick={() => toggleBottomTools(NETWORK)} active={bottomBlock === NETWORK} - label="NETWORK" + label="Network" hasErrors={resourceMarkedCountNow > 0} - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" /> {showExceptions ? toggleBottomTools(EXCEPTIONS)} active={bottomBlock === EXCEPTIONS} hasErrors={showExceptions} - label="EXCEPTIONS" - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" + label="Exceptions" /> : null} + +
Launch Events
+
+ } disabled={messagesLoading} onClick={() => toggleBottomTools(STACKEVENTS)} active={bottomBlock === STACKEVENTS} - label="EVENTS" - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" + label="Events" /> + +
Launch Performance
+
+ } disabled={messagesLoading} onClick={() => toggleBottomTools(PERFORMANCE)} active={bottomBlock === PERFORMANCE} - label="PERFORMANCE" - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" + label="Performance" /> ); diff --git a/frontend/app/components/Session/Player/MobilePlayer/MobileOverlay.tsx b/frontend/app/components/Session/Player/MobilePlayer/MobileOverlay.tsx index 0c956efc5..00de8365a 100644 --- a/frontend/app/components/Session/Player/MobilePlayer/MobileOverlay.tsx +++ b/frontend/app/components/Session/Player/MobilePlayer/MobileOverlay.tsx @@ -15,7 +15,6 @@ import { observer } from 'mobx-react-lite'; import { Dropdown } from 'antd'; import type { MenuProps } from 'antd'; import { connect } from 'react-redux'; -import { setCreateNoteTooltip } from 'Duck/sessions'; import { Icon } from 'UI'; interface Props { @@ -23,7 +22,6 @@ interface Props { closedLive?: boolean; isClickmap?: boolean; toggleBottomBlock: (block: number) => void; - setCreateNoteTooltip: (args: any) => void; } enum ItemKey { @@ -64,7 +62,7 @@ const menuItems: MenuProps['items'] = [ }, ]; -function Overlay({ nextId, isClickmap, toggleBottomBlock, setCreateNoteTooltip }: Props) { +function Overlay({ nextId, isClickmap, toggleBottomBlock }: Props) { const { player, store } = React.useContext(PlayerContext); const togglePlay = () => player.togglePlay(); @@ -93,7 +91,7 @@ function Overlay({ nextId, isClickmap, toggleBottomBlock, setCreateNoteTooltip } toggleBottomBlock(STORAGE); break; case ItemKey.AddNote: - setCreateNoteTooltip({ time: store.get().time, isVisible: true }); + // TODO setCreateNoteTooltip({ time: store.get().time, isVisible: true }); break; default: return; @@ -114,5 +112,4 @@ function Overlay({ nextId, isClickmap, toggleBottomBlock, setCreateNoteTooltip } export default connect(null, { toggleBottomBlock, - setCreateNoteTooltip, })(observer(Overlay)); diff --git a/frontend/app/components/Session/Player/ReplayPlayer/AiSubheader.tsx b/frontend/app/components/Session/Player/ReplayPlayer/AiSubheader.tsx index 3f7aacae3..35d05408e 100644 --- a/frontend/app/components/Session/Player/ReplayPlayer/AiSubheader.tsx +++ b/frontend/app/components/Session/Player/ReplayPlayer/AiSubheader.tsx @@ -213,52 +213,6 @@ function SubHeader(props: any) { ); } -function SummaryButton({ onClick }: { onClick?: () => void }) { - const [isHovered, setHovered] = React.useState(false); - - return ( -
-
setHovered(true)} - onMouseLeave={() => setHovered(false)}> - -
AI Summary
-
-
- ); -} - -const gradientButton = { - border: 'double 1px transparent', - borderRadius: '60px', - background: - 'linear-gradient(#f6f6f6, #f6f6f6), linear-gradient(to right, #394EFF 0%, #3EAAAF 100%)', - backgroundOrigin: 'border-box', - backgroundClip: 'content-box, border-box', - cursor: 'pointer', -}; -const onHoverFillStyle = { - width: '100%', - height: '100%', - display: 'flex', - borderRadius: '60px', - gap: 2, - alignItems: 'center', - padding: '4px 8px', - background: - 'linear-gradient(156deg, #E3E6FF 0%, #E4F3F4 69.48%)', -}; -const fillStyle = { - width: '100%', - height: '100%', - display: 'flex', - borderRadius: '60px', - gap: 2, - alignItems: 'center', - padding: '4px 8px', -} export default connect((state: Record) => ({ siteId: state.getIn(['site', 'siteId']), diff --git a/frontend/app/components/Session/Player/ReplayPlayer/PlayerBlock.tsx b/frontend/app/components/Session/Player/ReplayPlayer/PlayerBlock.tsx index bdc36d3aa..856a220df 100644 --- a/frontend/app/components/Session/Player/ReplayPlayer/PlayerBlock.tsx +++ b/frontend/app/components/Session/Player/ReplayPlayer/PlayerBlock.tsx @@ -19,16 +19,11 @@ interface IProps { function PlayerBlock(props: IProps) { const { fullscreen, sessionId, disabled, activeTab, jiraConfig, fullView = false, setActiveTab } = props; - const originStr = window.env.ORIGIN || window.location.origin - const isSaas = /app\.openreplay\.com/.test(originStr) - const shouldShowSubHeader = !fullscreen && !fullView; return (
- {shouldShowSubHeader ? - isSaas - ? - : + {shouldShowSubHeader + ? : null}
diff --git a/frontend/app/components/Session/Player/ReplayPlayer/SummaryBlock/index.tsx b/frontend/app/components/Session/Player/ReplayPlayer/SummaryBlock/index.tsx index e68721356..a2ad224bc 100644 --- a/frontend/app/components/Session/Player/ReplayPlayer/SummaryBlock/index.tsx +++ b/frontend/app/components/Session/Player/ReplayPlayer/SummaryBlock/index.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import Icon from 'UI/Icon'; import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; @@ -29,16 +28,12 @@ function SummaryBlock({ sessionId }: { sessionId: string }) { return (
-
- -
AI Summary
+
+ User Behavior Analysis
{aiSummaryStore.text ? (
-
- Here’s the AI breakdown of the session, covering user behavior and technical insights. -
<>{formattedText.map((v) => v)}
) : ( @@ -72,9 +67,9 @@ function TextPlaceholder() { } const summaryBlockStyle: React.CSSProperties = { - background: 'linear-gradient(156deg, #E3E6FF 0%, #E4F3F4 69.48%)', + background: 'linear-gradient(180deg, #E8EBFF -24.14%, rgba(236, 254, 255, 0.00) 100%)', width: '100%', - height: '100vh', + height: '25vh', overflow: 'auto', display: 'flex', flexDirection: 'column', diff --git a/frontend/app/components/Session_/EventsBlock/EventGroupWrapper.js b/frontend/app/components/Session_/EventsBlock/EventGroupWrapper.js index c9ea185eb..e3cccba3c 100644 --- a/frontend/app/components/Session_/EventsBlock/EventGroupWrapper.js +++ b/frontend/app/components/Session_/EventsBlock/EventGroupWrapper.js @@ -8,7 +8,6 @@ import { TYPES } from 'Types/session/event'; import Event from './Event'; import stl from './eventGroupWrapper.module.css'; import NoteEvent from './NoteEvent'; -import { setEditNoteTooltip } from 'Duck/sessions'; // TODO: incapsulate toggler in LocationEvent @withToggle('showLoadInfo', 'toggleLoadInfo') @@ -17,7 +16,6 @@ import { setEditNoteTooltip } from 'Duck/sessions'; members: state.getIn(['members', 'list']), currentUserId: state.getIn(['user', 'account', 'id']) }), - { setEditNoteTooltip } ) class EventGroupWrapper extends React.Component { toggleLoadInfo = (e) => { @@ -80,7 +78,6 @@ class EventGroupWrapper extends React.Component { ) diff --git a/frontend/app/components/Session_/EventsBlock/NoteEvent.tsx b/frontend/app/components/Session_/EventsBlock/NoteEvent.tsx index a7f2ed5c3..2b22d5441 100644 --- a/frontend/app/components/Session_/EventsBlock/NoteEvent.tsx +++ b/frontend/app/components/Session_/EventsBlock/NoteEvent.tsx @@ -1,3 +1,5 @@ +import { useModal } from 'Components/Modal'; +import CreateNote from 'Components/Session_/Player/Controls/components/CreateNote'; import React from 'react'; import { Icon } from 'UI'; import { tagProps, Note } from 'App/services/NotesService'; @@ -15,34 +17,40 @@ interface Props { note: Note; noEdit: boolean; filterOutNote: (id: number) => void; - onEdit: (noteTooltipObj: Record) => void; } function NoteEvent(props: Props) { const { settingsStore, notesStore } = useStore(); const { timezone } = settingsStore.sessionSettings; + const { showModal, hideModal } = useModal(); const onEdit = () => { - props.onEdit({ - isVisible: true, - isEdit: true, - time: props.note.timestamp, - note: { - timestamp: props.note.timestamp, - tag: props.note.tag, - isPublic: props.note.isPublic, - message: props.note.message, - sessionId: props.note.sessionId, - noteId: props.note.noteId, - }, - }); + showModal( + , + { right: true, width: 380 } + ); }; const onCopy = () => { copy( `${window.location.origin}/${window.location.pathname.split('/')[1]}${session( props.note.sessionId - )}${props.note.timestamp > 0 ? `?jumpto=${props.note.timestamp}¬e=${props.note.noteId}` : `?note=${props.note.noteId}`}` + )}${ + props.note.timestamp > 0 + ? `?jumpto=${props.note.timestamp}¬e=${props.note.noteId}` + : `?note=${props.note.noteId}` + }` ); toast.success('Note URL copied to clipboard'); }; @@ -67,10 +75,7 @@ function NoteEvent(props: Props) { { icon: 'trash', text: 'Delete', onClick: onDelete }, ]; return ( -
+
diff --git a/frontend/app/components/Session_/Issues/Issues.js b/frontend/app/components/Session_/Issues/Issues.js index dc610334f..b1ef46f39 100644 --- a/frontend/app/components/Session_/Issues/Issues.js +++ b/frontend/app/components/Session_/Issues/Issues.js @@ -1,8 +1,9 @@ import React from 'react'; import { connect } from 'react-redux'; -import { Popover, Button, Icon } from 'UI'; +import { Popover, Icon } from 'UI'; import IssuesModal from './IssuesModal'; import { fetchProjects, fetchMeta } from 'Duck/assignments'; +import { Popover as AntPopover, Button } from 'antd'; @connect( (state) => ({ @@ -53,7 +54,7 @@ class Issues extends React.Component { }; render() { - const { sessionId, issuesIntegration, isInline } = this.props; + const { sessionId, issuesIntegration } = this.props; const provider = issuesIntegration.first()?.provider || ''; return ( @@ -65,18 +66,13 @@ class Issues extends React.Component {
)} > - {isInline ? ( -
- -
Create Issue
-
- ) : ( -
- -
- )} + +
); } diff --git a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx index 08e974143..394425af1 100644 --- a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx +++ b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx @@ -1,3 +1,5 @@ +import SummaryBlock from 'Components/Session/Player/ReplayPlayer/SummaryBlock'; +import { SummaryButton } from 'Components/Session_/Player/Controls/Controls'; import React, { useEffect } from 'react'; import { toggleBottomBlock } from 'Duck/components/player'; import BottomBlock from '../BottomBlock'; @@ -11,10 +13,18 @@ import cn from 'classnames'; import OverviewPanelContainer from './components/OverviewPanelContainer'; import { NoContent, Icon } from 'UI'; import { observer } from 'mobx-react-lite'; -import {MobilePlayerContext, PlayerContext} from 'App/components/Session/playerContext'; +import { MobilePlayerContext, PlayerContext } from 'App/components/Session/playerContext'; +import { useStore } from 'App/mstore'; -function MobileOverviewPanelCont({ issuesList }: { issuesList: Record[] }) { - const { store, player } = React.useContext(MobilePlayerContext) +function MobileOverviewPanelCont({ + issuesList, + sessionId, +}: { + issuesList: Record[]; + sessionId: string; +}) { + const { aiSummaryStore } = useStore(); + const { store, player } = React.useContext(MobilePlayerContext); const [dataLoaded, setDataLoaded] = React.useState(false); const [selectedFeatures, setSelectedFeatures] = React.useState([ 'PERFORMANCE', @@ -31,16 +41,16 @@ function MobileOverviewPanelCont({ issuesList }: { issuesList: Record 0; const resources = { - NETWORK: fetchList.filter((r: any) => r.status >= 400 || r.isRed || r.isYellow), - ERRORS: exceptionsList, - EVENTS: eventsList, - PERFORMANCE: performanceChartData, - FRUSTRATIONS: frustrationsList, + NETWORK: fetchList.filter((r: any) => r.status >= 400 || r.isRed || r.isYellow), + ERRORS: exceptionsList, + EVENTS: eventsList, + PERFORMANCE: performanceChartData, + FRUSTRATIONS: frustrationsList, }; useEffect(() => { @@ -60,9 +70,11 @@ function MobileOverviewPanelCont({ issuesList }: { issuesList: Record { - player.scale() - }, [selectedFeatures]) + player.scale(); + }, [selectedFeatures]); + const originStr = window.env.ORIGIN || window.location.origin; + const isSaas = /app\.openreplay\.com/.test(originStr); return ( aiSummaryStore.setToggleSummary(!aiSummaryStore.toggleSummary)} + summaryChecked={aiSummaryStore.toggleSummary} /> - ) + ); } -function WebOverviewPanelCont() { +function WebOverviewPanelCont({ sessionId }: { sessionId: string }) { + const { aiSummaryStore } = useStore(); const { store } = React.useContext(PlayerContext); const [selectedFeatures, setSelectedFeatures] = React.useState([ 'PERFORMANCE', @@ -85,26 +102,24 @@ function WebOverviewPanelCont() { 'NETWORK', ]); - const { - endTime, - currentTab, - tabStates, - } = store.get(); + const { endTime, currentTab, tabStates } = store.get(); - const stackEventList = tabStates[currentTab]?.stackList || [] - const frustrationsList = tabStates[currentTab]?.frustrationsList || [] - const exceptionsList = tabStates[currentTab]?.exceptionsList || [] - const resourceListUnmap = tabStates[currentTab]?.resourceList || [] - const fetchList = tabStates[currentTab]?.fetchList || [] - const graphqlList = tabStates[currentTab]?.graphqlList || [] - const performanceChartData = tabStates[currentTab]?.performanceChartData || [] + const stackEventList = tabStates[currentTab]?.stackList || []; + const frustrationsList = tabStates[currentTab]?.frustrationsList || []; + const exceptionsList = tabStates[currentTab]?.exceptionsList || []; + const resourceListUnmap = tabStates[currentTab]?.resourceList || []; + const fetchList = tabStates[currentTab]?.fetchList || []; + const graphqlList = tabStates[currentTab]?.graphqlList || []; + const performanceChartData = tabStates[currentTab]?.performanceChartData || []; const fetchPresented = fetchList.length > 0; const resourceList = resourceListUnmap .filter((r: any) => r.isRed || r.isYellow) + // @ts-ignore .concat(fetchList.filter((i: any) => parseInt(i.status) >= 400)) + // @ts-ignore .concat(graphqlList.filter((i: any) => parseInt(i.status) >= 400)) - .filter((i: any) => i.type === "fetch"); + .filter((i: any) => i.type === 'fetch'); const resources: any = React.useMemo(() => { return { @@ -116,90 +131,122 @@ function WebOverviewPanelCont() { }; }, [tabStates, currentTab]); - return + const originStr = window.env.ORIGIN || window.location.origin; + const isSaas = /app\.openreplay\.com/.test(originStr); + return ( + aiSummaryStore.setToggleSummary(!aiSummaryStore.toggleSummary)} + summaryChecked={aiSummaryStore.toggleSummary} + sessionId={sessionId} + /> + ); } -function PanelComponent({ selectedFeatures, endTime, resources, fetchPresented, setSelectedFeatures, isMobile, performanceList }: any) { +function PanelComponent({ + selectedFeatures, + endTime, + resources, + fetchPresented, + setSelectedFeatures, + isMobile, + performanceList, + showSummary, + toggleSummary, + summaryChecked, + sessionId, +}: any) { return ( - - - - X-RAY -
- -
-
- - - -
+ + +
+ X-Ray + {showSummary ? ( + + ) : null} +
+
+ +
+
+ + {summaryChecked ? : null} + + +
+ + + Select a debug option to visualize on timeline. +
+ } > - - - Select a debug option to visualize on timeline. -
- } - > - - {selectedFeatures.map((feature: any, index: number) => ( -
- ( - - )} - endTime={endTime} - message={HELP_MESSAGE[feature]} - /> - {isMobile && feature === 'PERFORMANCE' ? ( -
- ( -
- -
- )} - endTime={endTime} - /> -
- ) : null} -
- ))} - -
- - - - - ) + + {selectedFeatures.map((feature: any, index: number) => ( +
+ ( + + )} + endTime={endTime} + message={HELP_MESSAGE[feature]} + /> + {isMobile && feature === 'PERFORMANCE' ? ( +
+ ( +
+ +
+ )} + endTime={endTime} + /> +
+ ) : null} +
+ ))} + +
+ + + + + ); } export const OverviewPanel = connect( - (state: any) => ({ + (state: Record) => ({ issuesList: state.getIn(['sessions', 'current']).issues, + sessionId: state.getIn(['sessions', 'current']).sessionId, }), { toggleBottomBlock, @@ -207,10 +254,11 @@ export const OverviewPanel = connect( )(observer(WebOverviewPanelCont)); export const MobileOverviewPanel = connect( - (state: any) => ({ + (state: Record) => ({ issuesList: state.getIn(['sessions', 'current']).issues, + sessionId: state.getIn(['sessions', 'current']).sessionId, }), { toggleBottomBlock, } -)(observer(MobileOverviewPanelCont)); \ No newline at end of file +)(observer(MobileOverviewPanelCont)); diff --git a/frontend/app/components/Session_/OverviewPanel/components/FeatureSelection/FeatureSelection.tsx b/frontend/app/components/Session_/OverviewPanel/components/FeatureSelection/FeatureSelection.tsx index a70c31e82..ef2341112 100644 --- a/frontend/app/components/Session_/OverviewPanel/components/FeatureSelection/FeatureSelection.tsx +++ b/frontend/app/components/Session_/OverviewPanel/components/FeatureSelection/FeatureSelection.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import { Checkbox, Tooltip } from 'UI'; +import { Popover, Checkbox } from 'antd'; +import { Icon } from 'UI' const NETWORK = 'NETWORK'; const ERRORS = 'ERRORS'; @@ -19,35 +20,73 @@ interface Props { list: any[]; updateList: any; } -function FeatureSelection(props: Props) { - const { list } = props; - const features = [NETWORK, ERRORS, EVENTS, PERFORMANCE, FRUSTRATIONS]; - const disabled = list.length >= 5; +const sortPriority = { + [PERFORMANCE]: 1, + [FRUSTRATIONS]: 2, + [ERRORS]: 3, + [NETWORK]: 4, + [EVENTS]: 5, +}; +const featLabels = { + [PERFORMANCE]: 'Performance Overview', + [FRUSTRATIONS]: 'User Frustrations', + [ERRORS]: 'Session Errors', + [NETWORK]: 'Network Events', + [EVENTS]: 'Custom Events', +} + +function FeatureSelection(props: Props) { + const [isOpen, setIsOpen] = React.useState(false); + const features = [NETWORK, ERRORS, EVENTS, PERFORMANCE, FRUSTRATIONS]; + + const toggleFeatureInList = (feat: string) => { + if (props.list.includes(feat)) { + props.updateList(props.list.filter((f) => f !== feat)); + } else { + // @ts-ignore + props.updateList([...props.list, feat].sort((a, b) => sortPriority[a] - sortPriority[b])); + } + }; + const toggleAllFeatures = () => { + if (props.list.length === features.length) { + props.updateList([]); + } else { + props.updateList(features); + } + } return ( - {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]); - } - }} - /> - - ); - })} + +
toggleAllFeatures()} + > + +
All Features
+
+ {features.map((feat) => ( +
toggleFeatureInList(feat)} + > + + {/* @ts-ignore */} +
{featLabels[feat]}
+
+ ))} +
+ } + > +
setIsOpen(!isOpen)} className={'font-semibold flex items-center gap-2 text-main cursor-pointer'}> + +
X-Ray Events
+
+ ); } diff --git a/frontend/app/components/Session_/Player/Controls/ControlButton.tsx b/frontend/app/components/Session_/Player/Controls/ControlButton.tsx index dabe86aa5..3e3b87435 100644 --- a/frontend/app/components/Session_/Player/Controls/ControlButton.tsx +++ b/frontend/app/components/Session_/Player/Controls/ControlButton.tsx @@ -1,9 +1,7 @@ import React from 'react'; import cn from 'classnames'; -import { Icon } from 'UI'; import stl from './controlButton.module.css'; -import {Popover} from 'antd' - +import { Popover, Button } from 'antd'; interface IProps { label: string; @@ -18,48 +16,29 @@ interface IProps { labelClassName?: string; containerClassName?: string; noIcon?: boolean; - popover?: React.ReactNode + popover?: React.ReactNode; } const ControlButton = ({ label, - icon = '', disabled = false, onClick, - // count = 0, hasErrors = false, active = false, - size = 20, - noLabel = false, - labelClassName, - containerClassName, - noIcon, popover = undefined, }: IProps) => ( - + ); diff --git a/frontend/app/components/Session_/Player/Controls/Controls.tsx b/frontend/app/components/Session_/Player/Controls/Controls.tsx index e4f0d0ded..659659671 100644 --- a/frontend/app/components/Session_/Player/Controls/Controls.tsx +++ b/frontend/app/components/Session_/Player/Controls/Controls.tsx @@ -1,21 +1,19 @@ import { useStore } from 'App/mstore'; import { session as sessionRoute, withSiteId } from 'App/routes'; -import KeyboardHelp, { +import { LaunchConsoleShortcut, LaunchEventsShortcut, LaunchNetworkShortcut, LaunchPerformanceShortcut, LaunchStateShortcut, - PlayPauseSessionShortcut, - PlaySessionInFullscreenShortcut, + LaunchXRaShortcut, } from 'Components/Session_/Player/Controls/components/KeyboardHelp'; import React from 'react'; import cn from 'classnames'; import { connect } from 'react-redux'; import { selectStorageType, STORAGE_TYPES, StorageType } from 'Player'; import { PlayButton, PlayingState, FullScreenButton } from 'App/player-ui'; -import { Popover } from 'antd'; - +import { Switch } from 'antd' import { CONSOLE, fullscreenOff, @@ -34,13 +32,13 @@ import { import { PlayerContext } from 'App/components/Session/playerContext'; import { observer } from 'mobx-react-lite'; import { fetchSessions } from 'Duck/liveSearch'; +import { Icon } from 'UI'; import Timeline from './Timeline'; import ControlButton from './ControlButton'; import PlayerControls from './components/PlayerControls'; import styles from './controls.module.css'; -import XRayButton from 'Shared/XRayButton'; import CreateNote from 'Components/Session_/Player/Controls/components/CreateNote'; import useShortcuts from 'Components/Session/Player/ReplayPlayer/useShortcuts'; @@ -57,19 +55,19 @@ export const SKIP_INTERVALS = { function getStorageName(type: any) { switch (type) { case STORAGE_TYPES.REDUX: - return 'REDUX'; + return 'Redux'; case STORAGE_TYPES.MOBX: - return 'MOBX'; + return 'Mobx'; case STORAGE_TYPES.VUEX: - return 'VUEX'; + return 'Vuex'; case STORAGE_TYPES.NGRX: - return 'NGRX'; + return 'NgRx'; case STORAGE_TYPES.ZUSTAND: - return 'ZUSTAND'; + return 'Zustand'; case STORAGE_TYPES.NONE: - return 'STATE'; + return 'State'; default: - return 'STATE'; + return 'State'; } } @@ -140,7 +138,6 @@ function Controls(props: any) { return (
- {!fullscreen && (
@@ -160,14 +157,9 @@ function Controls(props: any) { startedAt={session.startedAt} />
- toggleBottomTools(OVERVIEW)} - /> -
-
+
{uxtestingStore.hideDevtools && uxtestingStore.isUxt() ? null : ( { - const { store } = React.useContext(PlayerContext); + const { aiSummaryStore } = useStore(); + const { store, player } = React.useContext(PlayerContext); + + // @ts-ignore + const originStr = window.env.ORIGIN || window.location.origin; + const isSaas = /app\.openreplay\.com/.test(originStr); const { inspectorMode, currentTab, tabStates } = store.get(); @@ -220,8 +217,30 @@ const DevtoolsButtons = observer( const showProfiler = profilesCount > 0; const showExceptions = exceptionsList.length > 0; const showStorage = storageType !== STORAGE_TYPES.NONE || showStorageRedux; + + const showSummary = () => { + player.pause(); + if (bottomBlock !== OVERVIEW) { + toggleBottomTools(OVERVIEW) + } + aiSummaryStore.setToggleSummary(!aiSummaryStore.toggleSummary); + // showModal(, { right: true, width: 330 }); + }; return ( <> + {isSaas ? : null} + + +
Get a quick overview on the issues in this session.
+
+ } + label={'X-Ray'} + onClick={() => toggleBottomTools(OVERVIEW)} + active={bottomBlock === OVERVIEW && !inspectorMode} + /> + @@ -232,11 +251,8 @@ const DevtoolsButtons = observer( disabled={disableButtons} onClick={() => toggleBottomTools(CONSOLE)} active={bottomBlock === CONSOLE && !inspectorMode} - label="CONSOLE" - noIcon - labelClassName="!text-base font-semibold" + label="Console" hasErrors={logRedCount > 0 || showExceptions} - containerClassName="mx-2" /> toggleBottomTools(NETWORK)} active={bottomBlock === NETWORK && !inspectorMode} - label="NETWORK" + label="Network" hasErrors={resourceRedCount > 0} - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" /> toggleBottomTools(PERFORMANCE)} active={bottomBlock === PERFORMANCE && !inspectorMode} - label="PERFORMANCE" - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" + label="Performance" /> {showGraphql && ( @@ -277,10 +287,7 @@ const DevtoolsButtons = observer( disabled={disableButtons} onClick={() => toggleBottomTools(GRAPHQL)} active={bottomBlock === GRAPHQL && !inspectorMode} - label="GRAPHQL" - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" + label="Graphql" /> )} @@ -296,9 +303,6 @@ const DevtoolsButtons = observer( onClick={() => toggleBottomTools(STORAGE)} active={bottomBlock === STORAGE && !inspectorMode} label={getStorageName(storageType) as string} - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" /> )} toggleBottomTools(STACKEVENTS)} active={bottomBlock === STACKEVENTS && !inspectorMode} - label="EVENTS" - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" + label="Events" hasErrors={stackRedCount > 0} /> {showProfiler && ( @@ -322,10 +323,7 @@ const DevtoolsButtons = observer( disabled={disableButtons} onClick={() => toggleBottomTools(PROFILER)} active={bottomBlock === PROFILER && !inspectorMode} - label="PROFILER" - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" + label="Profiler" /> )} @@ -333,6 +331,68 @@ const DevtoolsButtons = observer( } ); +export function SummaryButton({ + onClick, + withToggle, + onToggle, + toggleValue, +}: { + onClick?: () => void, + withToggle?: boolean, + onToggle?: () => void, + toggleValue?: boolean +}) { + const [isHovered, setHovered] = React.useState(false); + + return ( +
+
setHovered(true)} + onMouseLeave={() => setHovered(false)} + > + {withToggle ? ( + + ) : null} + +
Summary AI
+
+
+ ); +} + +const gradientButton = { + border: 'double 1px transparent', + borderRadius: '60px', + background: + 'linear-gradient(#f6f6f6, #f6f6f6), linear-gradient(to right, #394EFF 0%, #3EAAAF 100%)', + backgroundOrigin: 'border-box', + backgroundClip: 'content-box, border-box', + cursor: 'pointer', +}; +const onHoverFillStyle = { + width: '100%', + height: '100%', + display: 'flex', + borderRadius: '60px', + gap: 2, + alignItems: 'center', + padding: '2px 8px', + background: 'linear-gradient(156deg, #E3E6FF 0%, #E4F3F4 69.48%)', +}; +const fillStyle = { + width: '100%', + height: '100%', + display: 'flex', + borderRadius: '60px', + gap: 2, + alignItems: 'center', + padding: '2px 8px', +}; + const ControlPlayer = observer(Controls); export default connect( diff --git a/frontend/app/components/Session_/Player/Controls/components/ControlsComponents.tsx b/frontend/app/components/Session_/Player/Controls/components/ControlsComponents.tsx new file mode 100644 index 000000000..fc25af36c --- /dev/null +++ b/frontend/app/components/Session_/Player/Controls/components/ControlsComponents.tsx @@ -0,0 +1,194 @@ +import React from 'react' +import { SkipButton } from 'App/player-ui'; +import { + PlaybackSpeedShortcut, + SkipForwardShortcut, + SkipBackwardShortcut, +} from 'Components/Session_/Player/Controls/components/KeyboardHelp'; +import { SPEED_OPTIONS } from 'Player/player/Player'; +import { Popover as AntPopover, Button } from 'antd' +import { Popover } from 'UI' +import cn from 'classnames' + +export function JumpBack({ + currentInterval, + backTenSeconds, +}: { + currentInterval: number; + backTenSeconds: () => void; +}) { + return ( + + +
{`Rewind ${currentInterval}s`}
+
+ } + placement="top" + > + + + ); +} + +export function IntervalSelector({ + skipIntervals, + setSkipInterval, + toggleTooltip, + currentInterval, +}: { + skipIntervals: Record; + setSkipInterval: (interval: number) => void; + toggleTooltip: () => void; + currentInterval: number; +}) { + return ( +
+ ( +
+
+ Jump (Secs) +
+ {Object.keys(skipIntervals).map((interval) => ( +
{ + close(); + setSkipInterval(parseInt(interval, 10)); + }} + className={cn( + 'py-2 px-4 cursor-pointer w-full text-left font-semibold', + 'hover:text-main hover:shadow-border-main border-t', + 'border-borderColor-gray-light-shade' + )} + > + {interval} + s +
+ ))} +
+ )} + > +
+ Set default skip duration
}>{currentInterval}s +
+ +
+ ); +} + +export function JumpForward({ + currentInterval, + forthTenSeconds, +}: { + currentInterval: number; + forthTenSeconds: () => void; +}) { + return ( + + +
{`Forward ${currentInterval}s`}
+
+ } + placement="top" + > + + + ); +} + +export function SpeedOptions({ + toggleSpeed, + disabled, + toggleTooltip, + speed, +}: { + toggleSpeed: (i: number) => void; + disabled: boolean; + toggleTooltip: () => void; + speed: number; +}) { + return ( + ( +
+
Playback speed
+ {Object.keys(SPEED_OPTIONS).map((index: any) => ( +
{ + close(); + toggleSpeed(index); + }} + className={cn( + 'py-2 px-4 cursor-pointer w-full text-left font-semibold', + 'hover:bg-active-blue border-t border-borderColor-gray-light-shade' + )} + > + {SPEED_OPTIONS[index]} + x +
+ ))} +
+ )} + > +
+ + +
Change playback speed
+
+ } + > + + +
+ + ); +} diff --git a/frontend/app/components/Session_/Player/Controls/components/CreateNote.tsx b/frontend/app/components/Session_/Player/Controls/components/CreateNote.tsx index b95bcd843..421d67efa 100644 --- a/frontend/app/components/Session_/Player/Controls/components/CreateNote.tsx +++ b/frontend/app/components/Session_/Player/Controls/components/CreateNote.tsx @@ -3,38 +3,33 @@ import { Icon, Button, Checkbox } from 'UI'; import { Duration } from 'luxon'; import { connect } from 'react-redux'; import { WriteNote, tagProps, TAGS, iTag, Note } from 'App/services/NotesService'; -import { setCreateNoteTooltip, addNote, updateNote } from 'Duck/sessions'; -import stl from './styles.module.css'; +import { addNote, updateNote } from 'Duck/sessions'; import { useStore } from 'App/mstore'; import { toast } from 'react-toastify'; import { fetchList as fetchSlack } from 'Duck/integrations/slack'; import { fetchList as fetchTeams } from 'Duck/integrations/teams'; +import { Tag } from 'antd'; import Select from 'Shared/Select'; -import { TeamBadge } from 'Shared/SessionsTabOverview/components/Notes'; import { List } from 'immutable'; interface Props { - isVisible: boolean; time: number; - setCreateNoteTooltip: (state: any) => void; addNote: (note: Note) => void; updateNote: (note: Note) => void; sessionId: string; - isEdit: string; - editNote: WriteNote; + isEdit?: boolean; + editNote?: WriteNote; slackChannels: List>; teamsChannels: List>; fetchSlack: () => void; fetchTeams: () => void; + hideModal: () => void; } function CreateNote({ - isVisible, time, - setCreateNoteTooltip, sessionId, - addNote, isEdit, editNote, updateNote, @@ -42,6 +37,7 @@ function CreateNote({ fetchSlack, teamsChannels, fetchTeams, + hideModal, }: Props) { const [text, setText] = React.useState(''); const [slackChannel, setSlackChannel] = React.useState(''); @@ -56,7 +52,7 @@ function CreateNote({ const { notesStore } = useStore(); React.useEffect(() => { - if (isEdit) { + if (isEdit && editNote) { setTag(editNote.tag); setText(editNote.message); setPublic(editNote.isPublic); @@ -67,29 +63,29 @@ function CreateNote({ }, [isEdit]); React.useEffect(() => { - if (inputRef.current && isVisible) { + if (inputRef.current) { inputRef.current.focus(); if (teamsChannels.size === 0 || slackChannels.size === 0) { fetchSlack(); fetchTeams(); } } - }, [isVisible]); + }, []); const duration = Duration.fromMillis(time || 0).toFormat('mm:ss'); const cleanUp = () => { - setCreateNoteTooltip({ isVisible: false, time: 0 }); setText(''); setTag(TAGS[0]); - } + hideModal(); + }; const onSubmit = () => { if (text === '') return; const note: WriteNote = { message: text, tag, - timestamp: useTimestamp ? Math.floor((isEdit ? editNote.timestamp : time)) : -1, + timestamp: useTimestamp ? Math.floor(isEdit && editNote ? editNote.timestamp : time) : -1, isPublic, }; const onSuccess = (noteId: string) => { @@ -100,7 +96,7 @@ function CreateNote({ notesStore.sendMsTeamsNotification(noteId, teamsChannel); } }; - if (isEdit) { + if (isEdit && editNote) { return notesStore .updateNote(editNote.noteId!, note) .then((r) => { @@ -115,7 +111,7 @@ function CreateNote({ console.error(e); }) .finally(() => { - cleanUp() + cleanUp(); }); } @@ -124,20 +120,19 @@ function CreateNote({ .then((r) => { onSuccess(r!.noteId as unknown as string); toast.success('Note added'); - void notesStore.fetchSessionNotes(sessionId) + void notesStore.fetchSessionNotes(sessionId); }) .catch((e) => { toast.error('Error adding note'); console.error(e); }) .finally(() => { - cleanUp() + cleanUp(); }); }; const closeTooltip = () => { - cleanUp() - setCreateNoteTooltip({ isVisible: false, time: 100 }); + hideModal(); }; const tagActive = (noteTag: iTag) => tag === noteTag; @@ -174,69 +169,66 @@ function CreateNote({ return (
e.stopPropagation()} > -
+

{isEdit ? 'Edit Note' : 'Add Note'}

-
setUseTs(!useTimestamp)}> - - {`at ${duration}`} -
-
+
setUseTs(!useTimestamp)} + > + + Add note at current time frame +
{duration}
+
+
Note