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:
Delirium 2024-03-21 10:40:36 +01:00 committed by GitHub
parent 1f1e4703c2
commit 96453e96e5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 891 additions and 804 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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}&note=${props.note.noteId}` : `?note=${props.note.noteId}`}`
)}${
props.note.timestamp > 0
? `?jumpto=${props.note.timestamp}&note=${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" />

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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