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
This commit is contained in:
parent
1f1e4703c2
commit
96453e96e5
31 changed files with 891 additions and 804 deletions
|
|
@ -118,11 +118,8 @@ function Controls(props: any) {
|
|||
<ControlButton
|
||||
onClick={() => toggleBottomTools(CONSOLE)}
|
||||
active={bottomBlock === CONSOLE}
|
||||
label="CONSOLE"
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
label="Console"
|
||||
hasErrors={logRedCount > 0 || showExceptions}
|
||||
containerClassName="mx-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className={styles.controls}>
|
||||
<Timeline isMobile />
|
||||
<CreateNote />
|
||||
{!fullscreen && (
|
||||
<div className={cn(styles.buttons, '!px-2')}>
|
||||
<div className="flex items-center">
|
||||
|
|
@ -115,13 +119,9 @@ function Controls(props: any) {
|
|||
startedAt={session.startedAt}
|
||||
/>
|
||||
<div className={cn('mx-2')} />
|
||||
<XRayButton
|
||||
isActive={bottomBlock === OVERVIEW}
|
||||
onClick={() => toggleBottomTools(OVERVIEW)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center h-full">
|
||||
<div className="flex items-center h-full gap-2">
|
||||
<DevtoolsButtons toggleBottomTools={toggleBottomTools} bottomBlock={bottomBlock} />
|
||||
<Tooltip title="Fullscreen" delay={0} placement="top-start" className="mx-4">
|
||||
<FullScreenButton
|
||||
|
|
@ -151,24 +151,41 @@ function DevtoolsButtons({ toggleBottomTools, bottomBlock }: DevtoolsButtonsProp
|
|||
return (
|
||||
<>
|
||||
<ControlButton
|
||||
popover={
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<LaunchXRaShortcut />
|
||||
<div>Get a quick overview on the issues in this session.</div>
|
||||
</div>
|
||||
}
|
||||
label={'X-Ray'}
|
||||
onClick={() => toggleBottomTools(OVERVIEW)}
|
||||
active={bottomBlock === OVERVIEW}
|
||||
/>
|
||||
<ControlButton
|
||||
popover={
|
||||
<div className={'flex gap-2 items-center'}>
|
||||
<LaunchConsoleShortcut />
|
||||
<div>Launch Logs</div>
|
||||
</div>
|
||||
}
|
||||
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"
|
||||
/>
|
||||
<ControlButton
|
||||
popover={
|
||||
<div className={'flex gap-2 items-center'}>
|
||||
<LaunchNetworkShortcut />
|
||||
<div>Launch Network</div>
|
||||
</div>
|
||||
}
|
||||
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 ?
|
||||
<ControlButton
|
||||
|
|
@ -176,29 +193,32 @@ function DevtoolsButtons({ toggleBottomTools, bottomBlock }: DevtoolsButtonsProp
|
|||
onClick={() => toggleBottomTools(EXCEPTIONS)}
|
||||
active={bottomBlock === EXCEPTIONS}
|
||||
hasErrors={showExceptions}
|
||||
label="EXCEPTIONS"
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
containerClassName="mx-2"
|
||||
label="Exceptions"
|
||||
/>
|
||||
: null}
|
||||
<ControlButton
|
||||
popover={
|
||||
<div className={'flex gap-2 items-center'}>
|
||||
<LaunchEventsShortcut />
|
||||
<div>Launch Events</div>
|
||||
</div>
|
||||
}
|
||||
disabled={messagesLoading}
|
||||
onClick={() => toggleBottomTools(STACKEVENTS)}
|
||||
active={bottomBlock === STACKEVENTS}
|
||||
label="EVENTS"
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
containerClassName="mx-2"
|
||||
label="Events"
|
||||
/>
|
||||
<ControlButton
|
||||
popover={
|
||||
<div className={'flex gap-2 items-center'}>
|
||||
<LaunchPerformanceShortcut />
|
||||
<div>Launch Performance</div>
|
||||
</div>
|
||||
}
|
||||
disabled={messagesLoading}
|
||||
onClick={() => toggleBottomTools(PERFORMANCE)}
|
||||
active={bottomBlock === PERFORMANCE}
|
||||
label="PERFORMANCE"
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
containerClassName="mx-2"
|
||||
label="Performance"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -213,52 +213,6 @@ function SubHeader(props: any) {
|
|||
);
|
||||
}
|
||||
|
||||
function SummaryButton({ onClick }: { onClick?: () => void }) {
|
||||
const [isHovered, setHovered] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={gradientButton}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div style={isHovered ? onHoverFillStyle : fillStyle} onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}>
|
||||
<Icon name={'sparkles'} size={16} />
|
||||
<div className={'font-semibold text-main'}>AI Summary</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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<string, any>) => ({
|
||||
siteId: state.getIn(['site', 'siteId']),
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className={cn(styles.playerBlock, 'flex flex-col', 'overflow-x-hidden')}>
|
||||
{shouldShowSubHeader ?
|
||||
isSaas
|
||||
? <AiSubheader sessionId={sessionId} disabled={disabled} jiraConfig={jiraConfig} activeTab={activeTab} setActiveTab={setActiveTab} />
|
||||
: <SubHeader sessionId={sessionId} disabled={disabled} jiraConfig={jiraConfig} />
|
||||
{shouldShowSubHeader
|
||||
? <SubHeader sessionId={sessionId} disabled={disabled} jiraConfig={jiraConfig} />
|
||||
: null}
|
||||
<Player setActiveTab={setActiveTab} activeTab={activeTab} fullView={fullView} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div style={summaryBlockStyle}>
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<Icon name={'sparkles'} size={18} />
|
||||
<div className={'font-semibold text-xl'}>AI Summary</div>
|
||||
<div className={'flex items-center gap-2 px-2 py-1 rounded border border-gray-light bg-white w-fit'}>
|
||||
User Behavior Analysis
|
||||
</div>
|
||||
|
||||
{aiSummaryStore.text ? (
|
||||
<div className={'rounded p-4 bg-white whitespace-pre-wrap flex flex-col'}>
|
||||
<div>
|
||||
Here’s the AI breakdown of the session, covering user behavior and technical insights.
|
||||
</div>
|
||||
<>{formattedText.map((v) => v)}</>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
<NoteEvent
|
||||
note={event}
|
||||
filterOutNote={filterOutNote}
|
||||
onEdit={this.props.setEditNoteTooltip}
|
||||
noEdit={this.props.currentUserId !== event.userId}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<string, any>) => 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(
|
||||
<CreateNote
|
||||
hideModal={hideModal}
|
||||
isEdit
|
||||
time={props.note.timestamp}
|
||||
editNote={{
|
||||
timestamp: props.note.timestamp,
|
||||
tag: props.note.tag,
|
||||
isPublic: props.note.isPublic,
|
||||
message: props.note.message,
|
||||
noteId: props.note.noteId.toString(),
|
||||
}}
|
||||
/>,
|
||||
{ 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 (
|
||||
<div
|
||||
className="flex items-start flex-col p-2 border rounded"
|
||||
style={{ background: '#FFFEF5' }}
|
||||
>
|
||||
<div className="flex items-start flex-col p-2 border rounded" style={{ background: '#FFFEF5' }}>
|
||||
<div className="flex items-center w-full relative">
|
||||
<div className="p-3 bg-gray-light rounded-full">
|
||||
<Icon name="quotes" color="main" />
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
</div>
|
||||
)}
|
||||
>
|
||||
{isInline ? (
|
||||
<div className={'flex items-center gap-2 p-3 text-black'}>
|
||||
<Icon name={`integrations/${provider === 'jira' ? 'jira' : 'github'}`} size={16} />
|
||||
<div>Create Issue</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<Button icon={`integrations/${provider === 'jira' ? 'jira' : 'github'}`} variant="text">
|
||||
Create Issue
|
||||
<div>
|
||||
<AntPopover content={'Create Issue'}>
|
||||
<Button size={'small'} className={'flex items-center justify-center'}>
|
||||
<Icon name={`integrations/${provider === 'jira' ? 'jira' : 'github'}`} />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</AntPopover>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, any>[] }) {
|
||||
const { store, player } = React.useContext(MobilePlayerContext)
|
||||
function MobileOverviewPanelCont({
|
||||
issuesList,
|
||||
sessionId,
|
||||
}: {
|
||||
issuesList: Record<string, any>[];
|
||||
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<string, a
|
|||
fetchList,
|
||||
performanceChartData,
|
||||
performanceList,
|
||||
} = store.get()
|
||||
} = store.get();
|
||||
|
||||
const fetchPresented = fetchList.length > 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<string, a
|
|||
}, [issuesList, exceptionsList, eventsList, performanceChartData, frustrationsList]);
|
||||
|
||||
React.useEffect(() => {
|
||||
player.scale()
|
||||
}, [selectedFeatures])
|
||||
player.scale();
|
||||
}, [selectedFeatures]);
|
||||
|
||||
const originStr = window.env.ORIGIN || window.location.origin;
|
||||
const isSaas = /app\.openreplay\.com/.test(originStr);
|
||||
return (
|
||||
<PanelComponent
|
||||
resources={resources}
|
||||
|
|
@ -72,11 +84,16 @@ function MobileOverviewPanelCont({ issuesList }: { issuesList: Record<string, a
|
|||
setSelectedFeatures={setSelectedFeatures}
|
||||
isMobile
|
||||
performanceList={performanceList}
|
||||
sessionId={sessionId}
|
||||
showSummary={isSaas}
|
||||
toggleSummary={() => 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 <PanelComponent resources={resources} endTime={endTime} selectedFeatures={selectedFeatures} fetchPresented={fetchPresented} setSelectedFeatures={setSelectedFeatures} />
|
||||
const originStr = window.env.ORIGIN || window.location.origin;
|
||||
const isSaas = /app\.openreplay\.com/.test(originStr);
|
||||
return (
|
||||
<PanelComponent
|
||||
resources={resources}
|
||||
endTime={endTime}
|
||||
selectedFeatures={selectedFeatures}
|
||||
fetchPresented={fetchPresented}
|
||||
setSelectedFeatures={setSelectedFeatures}
|
||||
showSummary={isSaas}
|
||||
toggleSummary={() => 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 (
|
||||
<React.Fragment>
|
||||
<BottomBlock style={{ height: '100%' }}>
|
||||
<BottomBlock.Header>
|
||||
<span className="font-semibold color-gray-medium mr-4">X-RAY</span>
|
||||
<div className="flex items-center h-20">
|
||||
<FeatureSelection list={selectedFeatures} updateList={setSelectedFeatures} />
|
||||
</div>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content>
|
||||
<OverviewPanelContainer endTime={endTime}>
|
||||
<TimelineScale endTime={endTime} />
|
||||
<div
|
||||
// style={{ width: '100%', height: '187px', overflow: 'hidden' }}
|
||||
style={{ width: 'calc(100% - 1rem)', margin: '0 auto' }}
|
||||
className="transition relative"
|
||||
<React.Fragment>
|
||||
<BottomBlock style={{ height: '100%' }}>
|
||||
<BottomBlock.Header>
|
||||
<div className="mr-4 flex items-center gap-2">
|
||||
<span className={'font-semibold text-black'}>X-Ray</span>
|
||||
{showSummary ? (
|
||||
<SummaryButton withToggle onClick={toggleSummary} toggleValue={summaryChecked} />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center h-20 mr-4">
|
||||
<FeatureSelection list={selectedFeatures} updateList={setSelectedFeatures} />
|
||||
</div>
|
||||
</BottomBlock.Header>
|
||||
<BottomBlock.Content className={'overflow-y-auto'}>
|
||||
{summaryChecked ? <SummaryBlock sessionId={sessionId} /> : null}
|
||||
<OverviewPanelContainer endTime={endTime}>
|
||||
<TimelineScale endTime={endTime} />
|
||||
<div
|
||||
style={{ width: 'calc(100% - 1rem)', margin: '0 auto' }}
|
||||
className="transition relative"
|
||||
>
|
||||
<NoContent
|
||||
show={selectedFeatures.length === 0}
|
||||
style={{ height: '60px', minHeight: 'unset', padding: 0 }}
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<Icon name="info-circle" className="mr-2" size="18" />
|
||||
Select a debug option to visualize on timeline.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<NoContent
|
||||
show={selectedFeatures.length === 0}
|
||||
style={{ height: '60px', minHeight: 'unset', padding: 0 }}
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<Icon name="info-circle" className="mr-2" size="18" />
|
||||
Select a debug option to visualize on timeline.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<VerticalPointerLine />
|
||||
{selectedFeatures.map((feature: any, index: number) => (
|
||||
<div
|
||||
key={feature}
|
||||
className={cn('border-b last:border-none relative', { 'bg-white': index % 2 })}
|
||||
>
|
||||
<EventRow
|
||||
isGraph={feature === 'PERFORMANCE'}
|
||||
title={feature}
|
||||
list={resources[feature]}
|
||||
renderElement={(pointer: any) => (
|
||||
<TimelinePointer
|
||||
pointer={pointer}
|
||||
type={feature}
|
||||
fetchPresented={fetchPresented}
|
||||
/>
|
||||
)}
|
||||
endTime={endTime}
|
||||
message={HELP_MESSAGE[feature]}
|
||||
/>
|
||||
{isMobile && feature === 'PERFORMANCE' ? (
|
||||
<div className={"absolute top-0 left-0 py-2 flex items-center py-4 w-full"}>
|
||||
<EventRow
|
||||
isGraph={false}
|
||||
title={''}
|
||||
list={performanceList}
|
||||
renderElement={(pointer: any) => (
|
||||
<div className="rounded bg-white p-1 border">
|
||||
<TimelinePointer
|
||||
pointer={pointer}
|
||||
type={"FRUSTRATIONS"}
|
||||
fetchPresented={fetchPresented}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
endTime={endTime}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</NoContent>
|
||||
</div>
|
||||
</OverviewPanelContainer>
|
||||
</BottomBlock.Content>
|
||||
</BottomBlock>
|
||||
</React.Fragment>
|
||||
)
|
||||
<VerticalPointerLine />
|
||||
{selectedFeatures.map((feature: any, index: number) => (
|
||||
<div
|
||||
key={feature}
|
||||
className={cn('border-b last:border-none relative', { 'bg-white': index % 2 })}
|
||||
>
|
||||
<EventRow
|
||||
isGraph={feature === 'PERFORMANCE'}
|
||||
title={feature}
|
||||
list={resources[feature]}
|
||||
renderElement={(pointer: any) => (
|
||||
<TimelinePointer
|
||||
pointer={pointer}
|
||||
type={feature}
|
||||
fetchPresented={fetchPresented}
|
||||
/>
|
||||
)}
|
||||
endTime={endTime}
|
||||
message={HELP_MESSAGE[feature]}
|
||||
/>
|
||||
{isMobile && feature === 'PERFORMANCE' ? (
|
||||
<div className={'absolute top-0 left-0 flex items-center py-4 w-full'}>
|
||||
<EventRow
|
||||
isGraph={false}
|
||||
title={''}
|
||||
list={performanceList}
|
||||
renderElement={(pointer: any) => (
|
||||
<div className="rounded bg-white p-1 border">
|
||||
<TimelinePointer
|
||||
pointer={pointer}
|
||||
type={'FRUSTRATIONS'}
|
||||
fetchPresented={fetchPresented}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
endTime={endTime}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</NoContent>
|
||||
</div>
|
||||
</OverviewPanelContainer>
|
||||
</BottomBlock.Content>
|
||||
</BottomBlock>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export const OverviewPanel = connect(
|
||||
(state: any) => ({
|
||||
(state: Record<string, any>) => ({
|
||||
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<string, any>) => ({
|
||||
issuesList: state.getIn(['sessions', 'current']).issues,
|
||||
sessionId: state.getIn(['sessions', 'current']).sessionId,
|
||||
}),
|
||||
{
|
||||
toggleBottomBlock,
|
||||
}
|
||||
)(observer(MobileOverviewPanelCont));
|
||||
)(observer(MobileOverviewPanelCont));
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<React.Fragment>
|
||||
{features.map((feature, index) => {
|
||||
const checked = list.includes(feature);
|
||||
const _disabled = disabled && !checked;
|
||||
return (
|
||||
<Tooltip key={index} title="X-RAY supports up to 5 views" disabled={!_disabled} delay={0}>
|
||||
<Checkbox
|
||||
key={index}
|
||||
label={feature}
|
||||
checked={checked}
|
||||
className="mx-4"
|
||||
disabled={_disabled}
|
||||
onClick={() => {
|
||||
if (checked) {
|
||||
props.updateList(list.filter((item: any) => item !== feature));
|
||||
} else {
|
||||
props.updateList([...list, feature]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
<Popover
|
||||
open={isOpen}
|
||||
content={
|
||||
<div>
|
||||
<div
|
||||
className={'flex items-center gap-2 cursor-pointer'}
|
||||
onClick={() => toggleAllFeatures()}
|
||||
>
|
||||
<Checkbox checked={props.list.length === features.length} />
|
||||
<div>All Features</div>
|
||||
</div>
|
||||
{features.map((feat) => (
|
||||
<div
|
||||
key={feat}
|
||||
className={'flex items-center gap-2 cursor-pointer'}
|
||||
onClick={() => toggleFeatureInList(feat)}
|
||||
>
|
||||
<Checkbox checked={props.list.includes(feat)} />
|
||||
{/* @ts-ignore */}
|
||||
<div>{featLabels[feat]}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div onClick={() => setIsOpen(!isOpen)} className={'font-semibold flex items-center gap-2 text-main cursor-pointer'}>
|
||||
<Icon size={16} name={'funnel'} color={'main'} />
|
||||
<div>X-Ray Events</div>
|
||||
</div>
|
||||
</Popover>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
<Popover content={popover} open={popover ? undefined : false}>
|
||||
<button
|
||||
className={cn(
|
||||
stl.controlButton,
|
||||
{ [stl.disabled]: disabled },
|
||||
'relative',
|
||||
active ? 'border-b-2 border-main' : 'rounded',
|
||||
containerClassName
|
||||
)}
|
||||
onClick={onClick}
|
||||
id={'control-button-' + label.toLowerCase()}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className={stl.labels}>
|
||||
{hasErrors && <div className={stl.errorSymbol} />}
|
||||
{/* {count > 0 && <div className={stl.countLabel}>{count}</div>} */}
|
||||
</div>
|
||||
{!noIcon && <Icon name={icon} size={size} color="gray-dark" />}
|
||||
{!noLabel && (
|
||||
<span className={cn(stl.label, labelClassName, active ? 'color-main' : 'color-gray-darkest')}>
|
||||
<Button
|
||||
size={'small'}
|
||||
onClick={onClick}
|
||||
id={'control-button-' + label.toLowerCase()}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className={stl.labels}>{hasErrors && <div className={stl.errorSymbol} />}</div>
|
||||
<span className={cn('font-semibold hover:text-main', active ? 'color-main' : 'color-gray-darkest')}>
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className={styles.controls}>
|
||||
<Timeline />
|
||||
<CreateNote />
|
||||
{!fullscreen && (
|
||||
<div className={cn(styles.buttons, '!px-2')}>
|
||||
<div className="flex items-center">
|
||||
|
|
@ -160,14 +157,9 @@ function Controls(props: any) {
|
|||
startedAt={session.startedAt}
|
||||
/>
|
||||
<div className={cn('mx-2')} />
|
||||
<XRayButton
|
||||
isActive={bottomBlock === OVERVIEW && !inspectorMode}
|
||||
onClick={() => toggleBottomTools(OVERVIEW)}
|
||||
/>
|
||||
<KeyboardHelp />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center h-full">
|
||||
<div className="flex gap-2 items-center h-full">
|
||||
{uxtestingStore.hideDevtools && uxtestingStore.isUxt() ? null : (
|
||||
<DevtoolsButtons
|
||||
showStorageRedux={showStorageRedux}
|
||||
|
|
@ -198,7 +190,12 @@ interface IDevtoolsButtons {
|
|||
|
||||
const DevtoolsButtons = observer(
|
||||
({ showStorageRedux, toggleBottomTools, bottomBlock, disabled }: IDevtoolsButtons) => {
|
||||
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(<SummaryBlock sessionId={sessionId} />, { right: true, width: 330 });
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{isSaas ? <SummaryButton onClick={showSummary} /> : null}
|
||||
<ControlButton
|
||||
popover={
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<LaunchXRaShortcut />
|
||||
<div>Get a quick overview on the issues in this session.</div>
|
||||
</div>
|
||||
}
|
||||
label={'X-Ray'}
|
||||
onClick={() => toggleBottomTools(OVERVIEW)}
|
||||
active={bottomBlock === OVERVIEW && !inspectorMode}
|
||||
/>
|
||||
|
||||
<ControlButton
|
||||
popover={
|
||||
<div className={'flex gap-2 items-center'}>
|
||||
|
|
@ -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"
|
||||
/>
|
||||
|
||||
<ControlButton
|
||||
|
|
@ -249,11 +265,8 @@ const DevtoolsButtons = observer(
|
|||
disabled={disableButtons}
|
||||
onClick={() => toggleBottomTools(NETWORK)}
|
||||
active={bottomBlock === NETWORK && !inspectorMode}
|
||||
label="NETWORK"
|
||||
label="Network"
|
||||
hasErrors={resourceRedCount > 0}
|
||||
noIcon
|
||||
labelClassName="!text-base font-semibold"
|
||||
containerClassName="mx-2"
|
||||
/>
|
||||
|
||||
<ControlButton
|
||||
|
|
@ -266,10 +279,7 @@ const DevtoolsButtons = observer(
|
|||
disabled={disableButtons}
|
||||
onClick={() => 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"
|
||||
/>
|
||||
)}
|
||||
<ControlButton
|
||||
|
|
@ -311,10 +315,7 @@ const DevtoolsButtons = observer(
|
|||
disabled={disableButtons}
|
||||
onClick={() => 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 (
|
||||
<div style={gradientButton} onClick={onClick}>
|
||||
<div
|
||||
style={isHovered ? onHoverFillStyle : fillStyle}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
{withToggle ? (
|
||||
<Switch
|
||||
checked={toggleValue}
|
||||
onChange={onToggle}
|
||||
/>
|
||||
) : null}
|
||||
<Icon name={'sparkles'} size={16} />
|
||||
<div className={'font-semibold text-main'}>Summary AI</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<AntPopover
|
||||
content={
|
||||
<div className={'flex gap-2 items-center'}>
|
||||
<SkipBackwardShortcut />
|
||||
<div>{`Rewind ${currentInterval}s`}</div>
|
||||
</div>
|
||||
}
|
||||
placement="top"
|
||||
>
|
||||
<button
|
||||
style={{ height: 24, background: 'transparent', border: 0 }}
|
||||
className={'hover:shadow-border-main hover:text-main rounded-l '}
|
||||
>
|
||||
<SkipButton
|
||||
size={16}
|
||||
onClick={backTenSeconds}
|
||||
isBackwards={true}
|
||||
customClasses={'h-full flex items-center'}
|
||||
/>
|
||||
</button>
|
||||
</AntPopover>
|
||||
);
|
||||
}
|
||||
|
||||
export function IntervalSelector({
|
||||
skipIntervals,
|
||||
setSkipInterval,
|
||||
toggleTooltip,
|
||||
currentInterval,
|
||||
}: {
|
||||
skipIntervals: Record<number, number>;
|
||||
setSkipInterval: (interval: number) => void;
|
||||
toggleTooltip: () => void;
|
||||
currentInterval: number;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
style={{ height: 24 }}
|
||||
className="border-l border-r flex items-center justify-center px-1 hover:shadow-border-main hover:text-main"
|
||||
>
|
||||
<Popover
|
||||
// @ts-ignore
|
||||
theme="nopadding"
|
||||
animation="none"
|
||||
duration={0}
|
||||
className="cursor-pointer select-none"
|
||||
distance={20}
|
||||
render={({ close }: any) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col bg-white border',
|
||||
'border-borderColor-gray-light-shade text-figmaColors-text-primary rounded'
|
||||
)}
|
||||
>
|
||||
<div className="font-semibold py-2 px-4 w-full text-left">
|
||||
Jump <span className="text-disabled-text">(Secs)</span>
|
||||
</div>
|
||||
{Object.keys(skipIntervals).map((interval) => (
|
||||
<div
|
||||
key={interval}
|
||||
onClick={() => {
|
||||
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}
|
||||
<span className="text-disabled-text">s</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div onClick={toggleTooltip} className="cursor-pointer select-none font-semibold">
|
||||
<AntPopover content={<div>Set default skip duration</div>}>{currentInterval}s</AntPopover>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function JumpForward({
|
||||
currentInterval,
|
||||
forthTenSeconds,
|
||||
}: {
|
||||
currentInterval: number;
|
||||
forthTenSeconds: () => void;
|
||||
}) {
|
||||
return (
|
||||
<AntPopover
|
||||
content={
|
||||
<div className={'flex gap-2 items-center'}>
|
||||
<SkipForwardShortcut />
|
||||
<div>{`Forward ${currentInterval}s`}</div>
|
||||
</div>
|
||||
}
|
||||
placement="top"
|
||||
>
|
||||
<button
|
||||
style={{ height: 24, background: 'transparent', border: 0 }}
|
||||
className={'hover:text-main hover:shadow-border-main rounded-r '}
|
||||
>
|
||||
<SkipButton
|
||||
size={16}
|
||||
onClick={forthTenSeconds}
|
||||
customClasses={'h-full flex items-center'}
|
||||
/>
|
||||
</button>
|
||||
</AntPopover>
|
||||
);
|
||||
}
|
||||
|
||||
export function SpeedOptions({
|
||||
toggleSpeed,
|
||||
disabled,
|
||||
toggleTooltip,
|
||||
speed,
|
||||
}: {
|
||||
toggleSpeed: (i: number) => void;
|
||||
disabled: boolean;
|
||||
toggleTooltip: () => void;
|
||||
speed: number;
|
||||
}) {
|
||||
return (
|
||||
<Popover
|
||||
// @ts-ignore
|
||||
theme="nopadding"
|
||||
animation="none"
|
||||
duration={0}
|
||||
className="cursor-pointer select-none"
|
||||
distance={20}
|
||||
render={({ close }: any) => (
|
||||
<div className="flex flex-col bg-white border border-borderColor-gray-light-shade text-figmaColors-text-primary rounded">
|
||||
<div className="font-semibold py-2 px-4 w-full text-left">Playback speed</div>
|
||||
{Object.keys(SPEED_OPTIONS).map((index: any) => (
|
||||
<div
|
||||
key={SPEED_OPTIONS[index]}
|
||||
onClick={() => {
|
||||
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]}
|
||||
<span className="text-disabled-text">x</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div onClick={toggleTooltip} className="cursor-pointer select-none">
|
||||
<AntPopover
|
||||
content={
|
||||
<div className={'flex gap-2 items-center'}>
|
||||
<PlaybackSpeedShortcut />
|
||||
<div>Change playback speed</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button disabled={disabled} size={'small'} className={'font-semibold'}>
|
||||
{speed + 'x'}
|
||||
</Button>
|
||||
</AntPopover>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<Record<string, any>>;
|
||||
teamsChannels: List<Record<string, any>>;
|
||||
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 (
|
||||
<div
|
||||
className={stl.noteTooltip}
|
||||
style={{
|
||||
width: 350,
|
||||
left: 'calc(50% - 175px)',
|
||||
display: isVisible ? 'flex' : 'none',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem',
|
||||
bottom: '15vh',
|
||||
zIndex: 110,
|
||||
}}
|
||||
className={'bg-white h-screen w-full p-4 flex flex-col gap-4'}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center bg-gray-lightest">
|
||||
<div className="flex items-center">
|
||||
<Icon name="quotes" size={20} />
|
||||
<h3 className="text-xl ml-2 mr-4 font-semibold">{isEdit ? 'Edit Note' : 'Add Note'}</h3>
|
||||
<div className="flex items-center cursor-pointer" onClick={() => setUseTs(!useTimestamp)}>
|
||||
<Checkbox checked={useTimestamp} />
|
||||
<span className="ml-1"> {`at ${duration}`} </span>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto cursor-pointer" onClick={closeTooltip}>
|
||||
<Icon name="close" size={20} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center cursor-pointer gap-2"
|
||||
onClick={() => setUseTs(!useTimestamp)}
|
||||
>
|
||||
<Checkbox checked={useTimestamp} />
|
||||
<span>Add note at current time frame</span>
|
||||
<div className={'border px-1 bg-gray-lightest rounded'}>{duration}</div>
|
||||
</div>
|
||||
|
||||
<div className="">
|
||||
<div className={'font-semibold'}>Note</div>
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
name="message"
|
||||
id="message"
|
||||
placeholder="Note..."
|
||||
placeholder="Add Messave"
|
||||
rows={3}
|
||||
value={text}
|
||||
autoFocus
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
className="text-area"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2" style={{ lineHeight: '15px' }}>
|
||||
{TAGS.map((tag) => (
|
||||
<div
|
||||
key={tag}
|
||||
style={{
|
||||
background: tagActive(tag) ? tagProps[tag] : 'rgba(0,0,0, 0.38)',
|
||||
userSelect: 'none',
|
||||
minWidth: 50,
|
||||
fontSize: 11,
|
||||
}}
|
||||
className="cursor-pointer rounded-full justify-center px-2 py-1 text-white flex items-center gap-2"
|
||||
onClick={() => addTag(tag)}
|
||||
>
|
||||
{tagActive(tag) ? <Icon name="check-circle-fill" color="white" size={13} /> : null}
|
||||
<div>{tag}</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-center gap-1" style={{ lineHeight: '15px' }}>
|
||||
{TAGS.map((tag) => (
|
||||
<Tag
|
||||
onClick={() => addTag(tag)}
|
||||
key={tag}
|
||||
className={'cursor-pointer'}
|
||||
color={tagActive(tag) ? tagProps[tag] : undefined}
|
||||
>
|
||||
<div className={'flex items-center gap-2'}>
|
||||
{tagActive(tag) ? (
|
||||
<Icon name="check-circle-fill" color="inherit" size={13} />
|
||||
) : null}
|
||||
{tag}
|
||||
</div>
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={'flex items-center gap-2 cursor-pointer'} onClick={() => setPublic(!isPublic)}>
|
||||
<Checkbox checked={isPublic} />
|
||||
<div>Visible to team members</div>
|
||||
</div>
|
||||
|
||||
{slackChannelsOptions.length > 0 ? (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center cursor-pointer" onClick={() => setSlack(!useSlack)}>
|
||||
<Checkbox checked={useSlack} />
|
||||
<span className="ml-1 mr-3"> Send to Slack? </span>
|
||||
<span className="ml-1 mr-3"> Share via Slack </span>
|
||||
</div>
|
||||
|
||||
{useSlack && (
|
||||
|
|
@ -257,7 +249,7 @@ function CreateNote({
|
|||
<div className="flex flex-col">
|
||||
<div className="flex items-center cursor-pointer" onClick={() => setTeams(!useTeams)}>
|
||||
<Checkbox checked={useTeams} />
|
||||
<span className="ml-1 mr-3"> Send to MSTeams? </span>
|
||||
<span className="ml-1 mr-3"> Share via MS Teams </span>
|
||||
</div>
|
||||
|
||||
{useTeams && (
|
||||
|
|
@ -276,12 +268,11 @@ function CreateNote({
|
|||
|
||||
<div className="flex">
|
||||
<Button variant="primary" className="mr-4" disabled={text === ''} onClick={onSubmit}>
|
||||
{isEdit ? 'Save Note' : 'Add Note'}
|
||||
{isEdit ? 'Save Note' : `Add Note ${useTeams || useSlack ? '& Share' : ''}`}
|
||||
</Button>
|
||||
<Button variant={'text'} onClick={closeTooltip}>
|
||||
Cancel
|
||||
</Button>
|
||||
<div className="flex items-center cursor-pointer" onClick={() => setPublic(!isPublic)}>
|
||||
<Checkbox checked={isPublic} />
|
||||
<TeamBadge />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -289,16 +280,10 @@ function CreateNote({
|
|||
|
||||
export default connect(
|
||||
(state: any) => {
|
||||
const {
|
||||
isVisible,
|
||||
time = 0,
|
||||
isEdit,
|
||||
note: editNote,
|
||||
} = state.getIn(['sessions', 'createNoteTooltip']);
|
||||
const slackChannels = state.getIn(['slack', 'list']);
|
||||
const teamsChannels = state.getIn(['teams', 'list']);
|
||||
const sessionId = state.getIn(['sessions', 'current']).sessionId;
|
||||
return { isVisible, time, sessionId, isEdit, editNote, slackChannels, teamsChannels };
|
||||
return { sessionId, slackChannels, teamsChannels };
|
||||
},
|
||||
{ setCreateNoteTooltip, addNote, updateNote, fetchSlack, fetchTeams }
|
||||
{ addNote, updateNote, fetchSlack, fetchTeams }
|
||||
)(CreateNote);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import { Popover } from 'antd';
|
||||
import { Popover, Button } from 'antd';
|
||||
|
||||
const Key = ({ label }: { label: string }) => <div style={{ minWidth: 52 }} className="whitespace-nowrap font-bold bg-gray-lightest rounded shadow px-2 py-1 text-figmaColors-text-primary text-center">{label}</div>;
|
||||
function Cell({ shortcut, text }: any) {
|
||||
|
|
@ -60,13 +60,12 @@ function KeyboardHelp() {
|
|||
title={<div className={'w-full text-center font-semibold'}>Keyboard Shortcuts</div>}
|
||||
content={<ShortcutGrid />}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
'py-1 px-2 rounded cursor-help bg-gray-lightest hover:bg-active-blue-border mx-2'
|
||||
}
|
||||
<Button
|
||||
size={'small'}
|
||||
className={'flex items-center justify-center'}
|
||||
>
|
||||
<Icon name={'keyboard'} size={21} color={'black'} />
|
||||
</div>
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,8 @@
|
|||
import {
|
||||
PlaybackSpeedShortcut,
|
||||
SkipBackwardShortcut,
|
||||
SkipForwardShortcut,
|
||||
SkipIntervalChangeShortcut
|
||||
} from "Components/Session_/Player/Controls/components/KeyboardHelp";
|
||||
import * as constants from 'constants';
|
||||
import React from 'react';
|
||||
import { Icon, Popover } from 'UI';
|
||||
import { Popover as AntPopover } from 'antd';
|
||||
import cn from 'classnames';
|
||||
import { RealReplayTimeConnected, ReduxTime } from '../Time';
|
||||
// @ts-ignore
|
||||
import styles from '../controls.module.css';
|
||||
import { SkipButton } from 'App/player-ui';
|
||||
import { SPEED_OPTIONS } from 'App/player/player/Player';
|
||||
import { Icon } from 'UI';
|
||||
import { Button } from 'antd';
|
||||
import PlayingTime from './PlayingTime';
|
||||
import { JumpBack, IntervalSelector, JumpForward, SpeedOptions } from './ControlsComponents';
|
||||
|
||||
interface Props {
|
||||
skip: boolean;
|
||||
|
|
@ -59,38 +47,12 @@ function PlayerControls(props: Props) {
|
|||
const [timeMode, setTimeMode] = React.useState<ITimeMode>(
|
||||
localStorage.getItem('__or_player_time_mode') as ITimeMode
|
||||
);
|
||||
const speedRef = React.useRef<HTMLButtonElement>(null);
|
||||
const arrowBackRef = React.useRef<HTMLButtonElement>(null);
|
||||
const arrowForwardRef = React.useRef<HTMLButtonElement>(null);
|
||||
const skipRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const saveTimeMode = (mode: ITimeMode) => {
|
||||
localStorage.setItem('__or_player_time_mode', mode);
|
||||
setTimeMode(mode);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleKeyboard = (e: KeyboardEvent) => {
|
||||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
||||
return;
|
||||
}
|
||||
if (e.key === 'ArrowRight') {
|
||||
arrowForwardRef.current?.focus();
|
||||
}
|
||||
if (e.key === 'ArrowLeft') {
|
||||
arrowBackRef.current?.focus();
|
||||
}
|
||||
if (e.key === 'ArrowDown') {
|
||||
speedRef.current?.focus();
|
||||
}
|
||||
if (e.key === 'ArrowUp') {
|
||||
speedRef.current?.focus();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleKeyboard);
|
||||
return () => document.removeEventListener('keydown', handleKeyboard);
|
||||
}, [speedRef, arrowBackRef, arrowForwardRef]);
|
||||
|
||||
const toggleTooltip = () => {
|
||||
setShowTooltip(!showTooltip);
|
||||
};
|
||||
|
|
@ -100,154 +62,42 @@ function PlayerControls(props: Props) {
|
|||
{playButton}
|
||||
<div className="mx-1" />
|
||||
|
||||
<button className={cn(styles.speedButton, 'focus:border focus:border-blue')}>
|
||||
<PlayingTime
|
||||
timeMode={timeMode}
|
||||
setTimeMode={saveTimeMode}
|
||||
startedAt={startedAt}
|
||||
sessionTz={sessionTz}
|
||||
<PlayingTime
|
||||
timeMode={timeMode}
|
||||
setTimeMode={saveTimeMode}
|
||||
startedAt={startedAt}
|
||||
sessionTz={sessionTz}
|
||||
/>
|
||||
|
||||
<div className="rounded ml-2 bg-white border-gray-light flex items-center" style={{ gap: 1 }}>
|
||||
<JumpBack backTenSeconds={backTenSeconds} currentInterval={currentInterval} />
|
||||
<IntervalSelector
|
||||
skipIntervals={skipIntervals}
|
||||
setSkipInterval={setSkipInterval}
|
||||
toggleTooltip={toggleTooltip}
|
||||
currentInterval={currentInterval}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<div className="rounded ml-4 bg-active-blue border border-active-blue-border flex items-stretch">
|
||||
{/* @ts-ignore */}
|
||||
<AntPopover
|
||||
content={
|
||||
<div className={'flex gap-2 items-center'}>
|
||||
<SkipBackwardShortcut />
|
||||
<div>{`Rewind ${currentInterval}s`}</div>
|
||||
</div>
|
||||
}
|
||||
placement="top"
|
||||
>
|
||||
<button style={{ height: 32, background: 'transparent', border: 0 }} ref={arrowBackRef}>
|
||||
<SkipButton
|
||||
size={18}
|
||||
onClick={backTenSeconds}
|
||||
isBackwards={true}
|
||||
customClasses={'hover:bg-active-blue-border color-main h-full flex items-center'}
|
||||
/>
|
||||
</button>
|
||||
</AntPopover>
|
||||
|
||||
<div className="p-1 border-l border-r bg-active-blue-border border-active-blue-border flex items-center">
|
||||
<Popover
|
||||
// @ts-ignore
|
||||
theme="nopadding"
|
||||
animation="none"
|
||||
duration={0}
|
||||
className="cursor-pointer select-none"
|
||||
distance={20}
|
||||
render={({ close }: any) => (
|
||||
<div className="flex flex-col bg-white border border-borderColor-gray-light-shade text-figmaColors-text-primary rounded">
|
||||
<div className="font-semibold py-2 px-4 w-full text-left">
|
||||
Jump <span className="text-disabled-text">(Secs)</span>
|
||||
</div>
|
||||
{Object.keys(skipIntervals).map((interval) => (
|
||||
<div
|
||||
key={interval}
|
||||
onClick={() => {
|
||||
close();
|
||||
setSkipInterval(parseInt(interval, 10));
|
||||
}}
|
||||
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'
|
||||
)}
|
||||
>
|
||||
{interval}
|
||||
<span className="text-disabled-text">s</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div onClick={toggleTooltip} ref={skipRef} className="cursor-pointer select-none">
|
||||
<AntPopover content={<div>Set default skip duration</div>}>
|
||||
{currentInterval}s
|
||||
</AntPopover>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<AntPopover
|
||||
content={
|
||||
<div className={'flex gap-2 items-center'}>
|
||||
<SkipForwardShortcut />
|
||||
<div>{`Forward ${currentInterval}s`}</div>
|
||||
</div>
|
||||
}
|
||||
placement="top"
|
||||
>
|
||||
<button ref={arrowForwardRef} style={{ height: 32, background: 'transparent', border: 0 }}>
|
||||
<SkipButton
|
||||
size={18}
|
||||
onClick={forthTenSeconds}
|
||||
customClasses={'hover:bg-active-blue-border color-main h-full flex items-center'}
|
||||
/>
|
||||
</button>
|
||||
</AntPopover>
|
||||
<JumpForward forthTenSeconds={forthTenSeconds} currentInterval={currentInterval} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<div className="mx-2" />
|
||||
{/* @ts-ignore */}
|
||||
<Popover
|
||||
// @ts-ignore
|
||||
theme="nopadding"
|
||||
animation="none"
|
||||
duration={0}
|
||||
className="cursor-pointer select-none"
|
||||
distance={20}
|
||||
render={({ close }: any) => (
|
||||
<div className="flex flex-col bg-white border border-borderColor-gray-light-shade text-figmaColors-text-primary rounded">
|
||||
<div className="font-semibold py-2 px-4 w-full text-left">Playback speed</div>
|
||||
{Object.keys(SPEED_OPTIONS).map((index: any) => (
|
||||
<div
|
||||
key={SPEED_OPTIONS[index]}
|
||||
onClick={() => {
|
||||
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]}
|
||||
<span className="text-disabled-text">x</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div onClick={toggleTooltip} ref={skipRef} className="cursor-pointer select-none">
|
||||
<AntPopover content={<div className={'flex gap-2 items-center'}>
|
||||
<PlaybackSpeedShortcut />
|
||||
<div>Change playback speed</div>
|
||||
</div>}>
|
||||
<button
|
||||
ref={speedRef}
|
||||
className={cn(styles.speedButton, 'focus:border focus:border-blue')}
|
||||
data-disabled={disabled}
|
||||
>
|
||||
<div>{speed + 'x'}</div>
|
||||
</button>
|
||||
</AntPopover>
|
||||
</div>
|
||||
</Popover>
|
||||
<div className="mx-2" />
|
||||
<button
|
||||
className={cn(styles.skipIntervalButton, {
|
||||
[styles.withCheckIcon]: skip,
|
||||
[styles.active]: skip,
|
||||
})}
|
||||
<div className="mx-1" />
|
||||
<SpeedOptions
|
||||
toggleSpeed={toggleSpeed}
|
||||
disabled={disabled}
|
||||
toggleTooltip={toggleTooltip}
|
||||
speed={speed}
|
||||
/>
|
||||
<div className="mx-1" />
|
||||
<Button
|
||||
onClick={toggleSkip}
|
||||
data-disabled={disabled}
|
||||
disabled={disabled}
|
||||
size={'small'}
|
||||
className={'flex items-center font-semibold'}
|
||||
>
|
||||
{skip && <Icon name="check" size="24" className="mr-1" />}
|
||||
{'Skip Inactivity'}
|
||||
</button>
|
||||
{skip && <Icon name="check" size="24" />}
|
||||
<span>Skip Inactivity</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import {ITimeMode, TimeMode} from "Components/Session_/Player/Controls/components/PlayerControls";
|
||||
import React from 'react'
|
||||
import { Popover } from 'UI'
|
||||
import { RealReplayTimeConnected, ReduxTime, RealUserReplayTimeConnected } from "Components/Session_/Player/Controls/Time";
|
||||
import React from 'react';
|
||||
import { DownOutlined } from '@ant-design/icons';
|
||||
import { Popover } from 'UI';
|
||||
import { ITimeMode, TimeMode } from 'Components/Session_/Player/Controls/components/PlayerControls';
|
||||
import {
|
||||
RealReplayTimeConnected,
|
||||
ReduxTime,
|
||||
RealUserReplayTimeConnected,
|
||||
} from 'Components/Session_/Player/Controls/Time';
|
||||
|
||||
interface Props {
|
||||
timeMode: ITimeMode;
|
||||
|
|
@ -12,59 +17,7 @@ interface Props {
|
|||
|
||||
function PlayingTime({ timeMode, setTimeMode, startedAt, sessionTz }: Props) {
|
||||
return (
|
||||
<Popover
|
||||
// @ts-ignore
|
||||
theme="nopadding"
|
||||
animation="none"
|
||||
duration={0}
|
||||
className="cursor-pointer select-none"
|
||||
distance={20}
|
||||
render={({ close }) => (
|
||||
<div className={'flex flex-col gap-2 bg-white py-2 rounded color-gray-darkest text-left'}>
|
||||
<div className={'font-semibold px-4 cursor-default'}>Playback Time Mode</div>
|
||||
<div className={'flex flex-col cursor-pointer hover:bg-active-blue w-full px-4'}>
|
||||
<div className={'text-sm text-disabled-text text-left'}>Current / Session Duration</div>
|
||||
<div
|
||||
className={'flex items-center text-left'}
|
||||
onClick={() => {
|
||||
setTimeMode(TimeMode.Timestamp);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<ReduxTime isCustom name="time" format="mm:ss" />
|
||||
<span className="px-1">/</span>
|
||||
<ReduxTime isCustom name="endTime" format="mm:ss" />
|
||||
</div>
|
||||
</div>
|
||||
{sessionTz ?
|
||||
<div
|
||||
className={'flex flex-col cursor-pointer hover:bg-active-blue w-full px-4'}
|
||||
onClick={() => {
|
||||
setTimeMode(TimeMode.UserReal);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<div className={'text-sm text-disabled-text text-left'}>User's time</div>
|
||||
<div className={'text-left'}>
|
||||
<RealUserReplayTimeConnected startedAt={startedAt} sessionTz={sessionTz}/>
|
||||
</div>
|
||||
</div>
|
||||
: null}
|
||||
<div
|
||||
className={'flex flex-col cursor-pointer hover:bg-active-blue w-full px-4'}
|
||||
onClick={() => {
|
||||
setTimeMode(TimeMode.Real);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<div className={'text-sm text-disabled-text text-left'}>Based on your settings</div>
|
||||
<div className={'text-left'}>
|
||||
<RealReplayTimeConnected startedAt={startedAt} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<div className="flex items-center font-semibold text-center" style={{ minWidth: 85 }}>
|
||||
{timeMode === TimeMode.Real ? (
|
||||
<RealReplayTimeConnected startedAt={startedAt} />
|
||||
|
|
@ -78,8 +31,65 @@ function PlayingTime({ timeMode, setTimeMode, startedAt, sessionTz }: Props) {
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
</Popover>
|
||||
<Popover
|
||||
// @ts-ignore
|
||||
theme="nopadding"
|
||||
animation="none"
|
||||
duration={0}
|
||||
className="cursor-pointer select-none"
|
||||
distance={20}
|
||||
render={({ close }) => (
|
||||
<div className={'flex flex-col gap-2 bg-white py-2 rounded color-gray-darkest text-left'}>
|
||||
<div className={'font-semibold px-4 cursor-default'}>Playback Time Mode</div>
|
||||
<div className={'flex flex-col cursor-pointer hover:bg-active-blue w-full px-4'}>
|
||||
<div className={'text-sm text-disabled-text text-left'}>
|
||||
Current / Session Duration
|
||||
</div>
|
||||
<div
|
||||
className={'flex items-center text-left'}
|
||||
onClick={() => {
|
||||
setTimeMode(TimeMode.Timestamp);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<ReduxTime isCustom name="time" format="mm:ss" />
|
||||
<span className="px-1">/</span>
|
||||
<ReduxTime isCustom name="endTime" format="mm:ss" />
|
||||
</div>
|
||||
</div>
|
||||
{sessionTz ? (
|
||||
<div
|
||||
className={'flex flex-col cursor-pointer hover:bg-active-blue w-full px-4'}
|
||||
onClick={() => {
|
||||
setTimeMode(TimeMode.UserReal);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<div className={'text-sm text-disabled-text text-left'}>User's time</div>
|
||||
<div className={'text-left'}>
|
||||
<RealUserReplayTimeConnected startedAt={startedAt} sessionTz={sessionTz} />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
className={'flex flex-col cursor-pointer hover:bg-active-blue w-full px-4'}
|
||||
onClick={() => {
|
||||
setTimeMode(TimeMode.Real);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<div className={'text-sm text-disabled-text text-left'}>Based on your settings</div>
|
||||
<div className={'text-left'}>
|
||||
<RealReplayTimeConnected startedAt={startedAt} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<DownOutlined />
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlayingTime
|
||||
export default PlayingTime;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,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 {
|
||||
|
|
@ -24,7 +23,6 @@ interface Props {
|
|||
closedLive?: boolean,
|
||||
isClickmap?: boolean,
|
||||
toggleBottomBlock: (block: number) => void,
|
||||
setCreateNoteTooltip: (args: any) => void,
|
||||
}
|
||||
|
||||
enum ItemKey {
|
||||
|
|
@ -74,7 +72,6 @@ function Overlay({
|
|||
nextId,
|
||||
isClickmap,
|
||||
toggleBottomBlock,
|
||||
setCreateNoteTooltip
|
||||
}: Props) {
|
||||
const {player, store} = React.useContext(PlayerContext)
|
||||
|
||||
|
|
@ -113,7 +110,7 @@ function Overlay({
|
|||
toggleBottomBlock(STORAGE)
|
||||
break;
|
||||
case ItemKey.AddNote:
|
||||
setCreateNoteTooltip({ time: store.get().time, isVisible: true });
|
||||
// TODO setCreateNoteTooltip({ time: store.get().time, isVisible: true });
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
|
|
@ -135,5 +132,4 @@ function Overlay({
|
|||
|
||||
export default connect(null, {
|
||||
toggleBottomBlock,
|
||||
setCreateNoteTooltip
|
||||
})(observer(Overlay));
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@ import React, { useEffect } from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import { setAutoplayValues } from 'Duck/sessions';
|
||||
import { withSiteId, session as sessionRoute } from 'App/routes';
|
||||
import { Link, Icon, Tooltip } from 'UI';
|
||||
import AutoplayToggle from "Shared/AutoplayToggle/AutoplayToggle";
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import cn from 'classnames';
|
||||
import { fetchAutoplaySessions } from 'Duck/search';
|
||||
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
|
||||
import { Button, Popover } from 'antd'
|
||||
|
||||
const PER_PAGE = 10;
|
||||
|
||||
|
|
@ -60,36 +62,41 @@ function QueueControls(props: Props) {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center gap-1">
|
||||
<div
|
||||
onClick={prevHandler}
|
||||
className={cn('p-1 bg-gray-bg group rounded-full color-gray-darkest font-medium', {
|
||||
className={cn('p-1 group rounded-full', {
|
||||
'pointer-events-none opacity-50': !previousId,
|
||||
'cursor-pointer': !!previousId,
|
||||
})}
|
||||
>
|
||||
<Tooltip
|
||||
<Popover
|
||||
placement="bottom"
|
||||
title={<div className="whitespace-nowrap">Play Previous Session</div>}
|
||||
disabled={!previousId}
|
||||
content={<div className="whitespace-nowrap">Play Previous Session</div>}
|
||||
open={previousId ? undefined : false}
|
||||
>
|
||||
<Icon name="prev1" className="group-hover:fill-main" color="inherit" size="16" />
|
||||
</Tooltip>
|
||||
<Button size={'small'} shape={'circle'} disabled={!previousId} className={'flex items-center justify-center'}>
|
||||
<LeftOutlined />
|
||||
</Button>
|
||||
</Popover>
|
||||
</div>
|
||||
<AutoplayToggle />
|
||||
<div
|
||||
onClick={nextHandler}
|
||||
className={cn('p-1 bg-gray-bg group ml-1 rounded-full color-gray-darkest font-medium', {
|
||||
className={cn('p-1 group ml-1 rounded-full', {
|
||||
'pointer-events-none opacity-50': !nextId,
|
||||
'cursor-pointer': !!nextId,
|
||||
})}
|
||||
>
|
||||
<Tooltip
|
||||
<Popover
|
||||
placement="bottom"
|
||||
title={<div className="whitespace-nowrap">Play Next Session</div>}
|
||||
disabled={!nextId}
|
||||
content={<div className="whitespace-nowrap">Play Next Session</div>}
|
||||
open={nextId ? undefined : false}
|
||||
>
|
||||
<Icon name="next1" className="group-hover:fill-main" color="inherit" size="16" />
|
||||
</Tooltip>
|
||||
<Button size={'small'} shape={'circle'} disabled={!previousId} className={'flex items-center justify-center'}>
|
||||
<RightOutlined />
|
||||
</Button>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
import { useStore } from "App/mstore";
|
||||
import React, { useMemo } from 'react';
|
||||
import { Icon, Tooltip, Button } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import KeyboardHelp from 'Components/Session_/Player/Controls/components/KeyboardHelp';
|
||||
import { Icon, Tooltip } from 'UI';
|
||||
import QueueControls from './QueueControls';
|
||||
import Bookmark from 'Shared/Bookmark';
|
||||
import SharePopup from '../shared/SharePopup/SharePopup';
|
||||
import Issues from './Issues/Issues';
|
||||
import NotePopup from './components/NotePopup';
|
||||
import ItemMenu from './components/HeaderMenu';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import BugReportModal from './BugReport/BugReportModal';
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import AutoplayToggle from 'Shared/AutoplayToggle';
|
||||
import { connect } from 'react-redux';
|
||||
import SessionTabs from 'Components/Session/Player/SharedComponents/SessionTabs';
|
||||
import { IFRAME } from 'App/constants/storageKeys';
|
||||
import cn from 'classnames';
|
||||
import { Switch } from 'antd';
|
||||
import { Switch, Button as AntButton, Popover } from 'antd';
|
||||
import { BugOutlined, ShareAltOutlined } from '@ant-design/icons';
|
||||
|
||||
const localhostWarn = (project) => project + '_localhost_warn';
|
||||
const disableDevtools = 'or_devtools_uxt_toggle';
|
||||
|
|
@ -89,7 +89,9 @@ function SubHeader(props) {
|
|||
<>
|
||||
<div
|
||||
className="w-full px-4 flex items-center border-b relative"
|
||||
style={{ background: uxtestingStore.isUxt() ? props.live ? '#F6FFED' : '#EBF4F5' : undefined }}
|
||||
style={{
|
||||
background: uxtestingStore.isUxt() ? (props.live ? '#F6FFED' : '#EBF4F5') : undefined,
|
||||
}}
|
||||
>
|
||||
{showWarning ? (
|
||||
<div
|
||||
|
|
@ -119,14 +121,23 @@ function SubHeader(props) {
|
|||
) : null}
|
||||
<SessionTabs />
|
||||
<div
|
||||
className={cn('ml-auto text-sm flex items-center color-gray-medium gap-2', {
|
||||
'opacity-50 pointer-events-none': hasIframe,
|
||||
})}
|
||||
className={cn(
|
||||
'ml-auto text-sm flex items-center color-gray-medium gap-2',
|
||||
hasIframe ? 'opacity-50 pointer-events-none' : ''
|
||||
)}
|
||||
style={{ width: 'max-content' }}
|
||||
>
|
||||
<Button icon="file-pdf" variant="text" onClick={showReportModal}>
|
||||
Create Bug Report
|
||||
</Button>
|
||||
<KeyboardHelp />
|
||||
<Popover content={'Create Bug Report'}>
|
||||
<AntButton
|
||||
size={'small'}
|
||||
className={'flex items-center justify-center'}
|
||||
onClick={showReportModal}
|
||||
>
|
||||
<BugOutlined />
|
||||
</AntButton>
|
||||
</Popover>
|
||||
<Bookmark sessionId={props.sessionId} />
|
||||
<NotePopup />
|
||||
{enabledIntegration && <Issues sessionId={props.sessionId} />}
|
||||
<SharePopup
|
||||
|
|
@ -135,25 +146,14 @@ function SubHeader(props) {
|
|||
showCopyLink={true}
|
||||
trigger={
|
||||
<div className="relative">
|
||||
<Button icon="share-alt" variant="text" className="relative">
|
||||
Share
|
||||
</Button>
|
||||
<Popover content={'Share Session'}>
|
||||
<AntButton size={'small'} className="flex items-center justify-center">
|
||||
<ShareAltOutlined />
|
||||
</AntButton>
|
||||
</Popover>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<ItemMenu
|
||||
useSc
|
||||
items={[
|
||||
{
|
||||
key: 1,
|
||||
component: <AutoplayToggle />,
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
component: <Bookmark noMargin sessionId={props.sessionId} />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{uxtestingStore.isUxt() ? (
|
||||
<Switch
|
||||
|
|
|
|||
|
|
@ -1,38 +1,42 @@
|
|||
import CreateNote from 'Components/Session_/Player/Controls/components/CreateNote';
|
||||
import React from 'react';
|
||||
import { Button } from 'UI';
|
||||
import { connect } from 'react-redux';
|
||||
import { setCreateNoteTooltip } from 'Duck/sessions';
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
import { Button, Popover } from 'antd';
|
||||
import { MessageOutlined } from '@ant-design/icons';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
|
||||
function NotePopup({
|
||||
setCreateNoteTooltip,
|
||||
tooltipActive,
|
||||
}: {
|
||||
setCreateNoteTooltip: (args: any) => void;
|
||||
tooltipActive: boolean;
|
||||
}) {
|
||||
function NotePopup({ tooltipActive }: { tooltipActive: boolean }) {
|
||||
const { player, store } = React.useContext(PlayerContext);
|
||||
|
||||
const { showModal, hideModal } = useModal();
|
||||
const toggleNotePopup = () => {
|
||||
if (tooltipActive) return;
|
||||
player.pause();
|
||||
setCreateNoteTooltip({ time: Math.round(store.get().time), isVisible: true });
|
||||
showModal(
|
||||
<CreateNote hideModal={hideModal} isEdit={false} time={Math.round(store.get().time)} />,
|
||||
{
|
||||
right: true,
|
||||
width: 380,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => setCreateNoteTooltip({ time: -1, isVisible: false });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Button icon="quotes" variant="text" disabled={tooltipActive} onClick={toggleNotePopup}>
|
||||
Add Note
|
||||
</Button>
|
||||
<Popover content={'Add Note'}>
|
||||
<Button
|
||||
size={'small'}
|
||||
className={'flex items-center justify-center'}
|
||||
onClick={toggleNotePopup}
|
||||
disabled={tooltipActive}
|
||||
>
|
||||
<MessageOutlined />
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
const NotePopupComp = connect(
|
||||
(state: any) => ({ tooltipActive: state.getIn(['sessions', 'createNoteTooltip', 'isVisible']) }),
|
||||
{ setCreateNoteTooltip }
|
||||
)(NotePopup);
|
||||
|
||||
export default React.memo(NotePopupComp);
|
||||
|
|
|
|||
|
|
@ -1,20 +1,14 @@
|
|||
import React from 'react';
|
||||
import { Toggler } from 'UI';
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Switch } from 'antd'
|
||||
|
||||
function AutoplayToggle() {
|
||||
const { player, store } = React.useContext(PlayerContext)
|
||||
const { autoplay } = store.get()
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => player.toggleAutoplay()}
|
||||
className="cursor-pointer flex items-center mr-2 hover:bg-gray-light-shade rounded-md p-2"
|
||||
>
|
||||
<Toggler name="sessionsLive" onChange={() => player.toggleAutoplay()} checked={autoplay} />
|
||||
<span className="ml-2 whitespace-nowrap">Auto-Play</span>
|
||||
</div>
|
||||
<Switch onChange={() => player.toggleAutoplay()} checked={autoplay} checkedChildren="Auto" />
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Tooltip, Button, Icon } from 'UI';
|
||||
import { toggleFavorite } from 'Duck/sessions';
|
||||
import { connect } from 'react-redux';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Button, Popover } from 'antd'
|
||||
import { SaveOutlined } from '@ant-design/icons';
|
||||
|
||||
interface Props {
|
||||
toggleFavorite: (sessionId: string) => Promise<void>;
|
||||
|
|
@ -21,9 +22,6 @@ function Bookmark(props: Props) {
|
|||
const TOOLTIP_TEXT_ADD = isEnterprise ? 'Add to vault' : 'Add to bookmarks';
|
||||
const TOOLTIP_TEXT_REMOVE = isEnterprise ? 'Remove from vault' : 'Remove from bookmarks';
|
||||
|
||||
const ACTIVE_ICON = isEnterprise ? 'safe-fill' : 'star-solid';
|
||||
const INACTIVE_ICON = isEnterprise ? 'safe' : 'star';
|
||||
|
||||
useEffect(() => {
|
||||
setIsFavorite(favorite);
|
||||
}, [favorite]);
|
||||
|
|
@ -37,27 +35,11 @@ function Bookmark(props: Props) {
|
|||
|
||||
return (
|
||||
<div onClick={toggleFavorite} className="w-full">
|
||||
<Tooltip title={isFavorite ? TOOLTIP_TEXT_REMOVE : TOOLTIP_TEXT_ADD}>
|
||||
{noMargin ? (
|
||||
<div className="flex items-center cursor-pointer h-full w-full p-3">
|
||||
<Icon
|
||||
name={isFavorite ? ACTIVE_ICON : INACTIVE_ICON}
|
||||
color={isFavorite ? 'teal' : undefined}
|
||||
size="16"
|
||||
/>
|
||||
<span className="ml-2 text-black">{isEnterprise ? 'Vault' : 'Bookmark'}</span>
|
||||
</div>
|
||||
) : (
|
||||
<Button data-favourite={isFavorite}>
|
||||
<Icon
|
||||
name={isFavorite ? ACTIVE_ICON : INACTIVE_ICON}
|
||||
color={isFavorite ? 'teal' : undefined}
|
||||
size="16"
|
||||
/>
|
||||
<span className="ml-2 text-black">{isEnterprise ? 'Vault' : 'Bookmark'}</span>
|
||||
<Popover content={isFavorite ? TOOLTIP_TEXT_REMOVE : TOOLTIP_TEXT_ADD}>
|
||||
<Button type={isFavorite ? 'primary' : undefined} ghost={isFavorite} size={'small'} className={'flex items-center justify-center'}>
|
||||
<SaveOutlined />
|
||||
</Button>
|
||||
)}
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -552,20 +552,6 @@ export function setTimelineHoverTime(timeLineTooltip) {
|
|||
};
|
||||
}
|
||||
|
||||
export function setCreateNoteTooltip(noteTooltip) {
|
||||
return {
|
||||
type: SET_CREATE_NOTE_TOOLTIP,
|
||||
noteTooltip
|
||||
}
|
||||
}
|
||||
|
||||
export function setEditNoteTooltip(noteTooltip) {
|
||||
return {
|
||||
type: SET_EDIT_NOTE_TOOLTIP,
|
||||
noteTooltip
|
||||
}
|
||||
}
|
||||
|
||||
export function filterOutNote(noteId) {
|
||||
return {
|
||||
type: FILTER_OUT_NOTE,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { makeAutoObservable } from 'mobx';
|
|||
|
||||
export default class AiSummaryStore {
|
||||
text = '';
|
||||
toggleSummary = false;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
|
|
@ -12,6 +13,10 @@ export default class AiSummaryStore {
|
|||
this.text = text;
|
||||
}
|
||||
|
||||
setToggleSummary(toggleSummary: boolean) {
|
||||
this.toggleSummary = toggleSummary;
|
||||
}
|
||||
|
||||
getSummary = async (sessionId: string) => {
|
||||
this.setText('');
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -354,10 +354,6 @@ export default class SessionStore {
|
|||
this.timeLineTooltip = tp
|
||||
}
|
||||
|
||||
setEditNoteTooltip(tp: { time: number, isVisible: boolean, isEdit: boolean, note: any }) {
|
||||
this.createNoteTooltip = tp
|
||||
}
|
||||
|
||||
filterOutNote(noteId: string) {
|
||||
const current = this.current
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
import { PlaySessionInFullscreenShortcut } from 'Components/Session_/Player/Controls/components/KeyboardHelp';
|
||||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import cn from 'classnames';
|
||||
import { Popover } from 'antd';
|
||||
import { Popover, Button } from 'antd';
|
||||
import { FullscreenOutlined } from '@ant-design/icons';
|
||||
import { PlaySessionInFullscreenShortcut } from 'Components/Session_/Player/Controls/components/KeyboardHelp';
|
||||
|
||||
interface IProps {
|
||||
size: number;
|
||||
onClick: () => void;
|
||||
customClasses: string;
|
||||
}
|
||||
export function FullScreenButton({ size = 18, onClick, customClasses }: IProps) {
|
||||
|
||||
export function FullScreenButton({ size = 18, onClick }: IProps) {
|
||||
return (
|
||||
<Popover
|
||||
content={
|
||||
|
|
@ -21,12 +20,13 @@ export function FullScreenButton({ size = 18, onClick, customClasses }: IProps)
|
|||
}
|
||||
placement={"topRight"}
|
||||
>
|
||||
<div
|
||||
<Button
|
||||
onClick={onClick}
|
||||
className={cn('py-1 px-2 hover-main cursor-pointer bg-gray-lightest', customClasses)}
|
||||
>
|
||||
<Icon name="arrows-angle-extend" size={size} color="inherit" />
|
||||
</div>
|
||||
shape="circle"
|
||||
size={'small'}
|
||||
className={'flex items-center justify-center'}
|
||||
icon={<FullscreenOutlined />}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react'
|
||||
import { Icon } from 'UI'
|
||||
import cn from 'classnames'
|
||||
import { ForwardOutlined } from '@ant-design/icons';
|
||||
|
||||
interface IProps {
|
||||
size: number;
|
||||
|
|
@ -9,18 +10,14 @@ interface IProps {
|
|||
customClasses: string;
|
||||
}
|
||||
|
||||
export function SkipButton({ size = 18, onClick, isBackwards, customClasses }: IProps) {
|
||||
export function SkipButton({ onClick, isBackwards, customClasses }: IProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={cn('py-1 px-2 hover-main cursor-pointer bg-gray-lightest', customClasses)}
|
||||
className={cn('py-1 px-2 cursor-pointer', customClasses)}
|
||||
>
|
||||
<Icon
|
||||
name={ isBackwards ? "arrow-counterclockwise" : "arrow-clockwise" }
|
||||
size={size}
|
||||
color="inherit"
|
||||
/>
|
||||
<ForwardOutlined rotate={isBackwards ? 180 : 0} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ export default class AiService extends BaseService {
|
|||
/**
|
||||
* @returns stream of text symbols
|
||||
* */
|
||||
async getSummary(sessionId: string): Promise<ReadableStream | null> {
|
||||
async getSummary(sessionId: string): Promise<string | null> {
|
||||
const r = await this.client.post(
|
||||
`/sessions/${sessionId}/intelligent/summary`,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import APIClient from 'App/api_client';
|
|||
|
||||
|
||||
export const tagProps = {
|
||||
'ISSUE': '#CC0000',
|
||||
'QUERY': '#3EAAAF',
|
||||
'TASK': '#7986CB',
|
||||
'ISSUE': 'red',
|
||||
'QUERY': 'green',
|
||||
'TASK': 'cyan',
|
||||
'OTHER': 'rgba(0, 0, 0, 0.6)',
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue