feat(ui): timeline zoom (#1982)
* feat(ui): timeline zoom * stable draggable markers * integrate zoom into panels, ready up ai stuff for zoom * tabs for ai, slider styles * fixes for zoom tabs * code style
This commit is contained in:
parent
d2cabcdb54
commit
80f0005362
17 changed files with 1035 additions and 288 deletions
|
|
@ -2,5 +2,8 @@
|
|||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"printWidth": 100,
|
||||
"singleQuote": true
|
||||
"singleQuote": true,
|
||||
"importOrderSeparation": true,
|
||||
"importOrderSortSpecifiers": true,
|
||||
"importOrder": ["^Components|^App|^UI|^Duck", "^Shared", "^[./]"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +1,107 @@
|
|||
import React from 'react';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { useStore } from 'App/mstore';
|
||||
import { debounce } from 'App/utils';
|
||||
import { IResourceRequest, IResourceTiming } from "../../../../../player";
|
||||
import { WsChannel } from "../../../../../player/web/messages";
|
||||
import { PlayerContext } from "../../../playerContext";
|
||||
|
||||
let debounceUpdate: any = () => {};
|
||||
|
||||
const userBehaviorRegex = /User\s+(\w+\s+)?Behavior/i;
|
||||
const issuesErrorsRegex = /Issues\s+(and\s+|,?\s+)?(\w+\s+)?Errors/i;
|
||||
|
||||
function testLine(line: string): boolean {
|
||||
function isTitleLine(line: string): boolean {
|
||||
return userBehaviorRegex.test(line) || issuesErrorsRegex.test(line);
|
||||
}
|
||||
|
||||
function SummaryBlock({ sessionId }: { sessionId: string }) {
|
||||
function SummaryBlock({
|
||||
sessionId,
|
||||
zoomEnabled,
|
||||
zoomStartTs,
|
||||
zoomEndTs,
|
||||
zoomTab,
|
||||
duration,
|
||||
}: {
|
||||
sessionId: string;
|
||||
zoomEnabled: boolean;
|
||||
zoomStartTs: number;
|
||||
zoomEndTs: number;
|
||||
zoomTab: 'overview' | 'journey' | 'issues' | 'errors';
|
||||
duration: any;
|
||||
}) {
|
||||
const { store } = React.useContext(PlayerContext)
|
||||
const { tabStates } = store.get();
|
||||
const { aiSummaryStore } = useStore();
|
||||
|
||||
React.useEffect(() => {
|
||||
void aiSummaryStore.getSummary(sessionId);
|
||||
debounceUpdate = debounce(
|
||||
(
|
||||
sessionId: string,
|
||||
events: any[],
|
||||
feat: 'journey' | 'issues' | 'errors',
|
||||
startTs: number,
|
||||
endTs: number
|
||||
) => aiSummaryStore.getDetailedSummary(sessionId, events, feat, startTs, endTs),
|
||||
500
|
||||
);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (zoomTab === 'overview') {
|
||||
void aiSummaryStore.getSummary(sessionId);
|
||||
} else {
|
||||
const totalFetchList: IResourceRequest[] = [];
|
||||
const totalResourceList: IResourceTiming[] = [];
|
||||
const totalWebsocketList: WsChannel[] = [];
|
||||
Object.values(tabStates).forEach(({
|
||||
fetchList,
|
||||
resourceList,
|
||||
websocketList,
|
||||
}) => {
|
||||
totalFetchList.push(...fetchList);
|
||||
totalResourceList.push(...resourceList);
|
||||
totalWebsocketList.push(...websocketList);
|
||||
})
|
||||
const resultingEvents = [
|
||||
...totalFetchList,
|
||||
...totalResourceList,
|
||||
...totalWebsocketList,
|
||||
]
|
||||
const range = !zoomEnabled ? [0, duration] : [zoomStartTs, zoomEndTs];
|
||||
void debounceUpdate(sessionId, resultingEvents, zoomTab, range[0], range[1]);
|
||||
}
|
||||
}, [zoomTab]);
|
||||
|
||||
const formattedText = aiSummaryStore.text.split('\n').map((line) => {
|
||||
if (testLine(line)) {
|
||||
if (isTitleLine(line)) {
|
||||
return <div className={'font-semibold mt-2'}>{line}</div>;
|
||||
}
|
||||
if (line.startsWith('*')) {
|
||||
return <li className={'ml-1 marker:mr-1'}>{line.replace('* ', '')}</li>;
|
||||
return (
|
||||
<li className={'ml-1 marker:mr-1 flex items-center gap-1'}>
|
||||
<CodeStringFormatter text={line.replace('* ', '')} />
|
||||
</li>
|
||||
);
|
||||
}
|
||||
return <div>{line}</div>;
|
||||
return (
|
||||
<div className={'flex items-center gap-1'}>
|
||||
<CodeStringFormatter text={line} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={summaryBlockStyle}>
|
||||
<div className={'flex items-center gap-2 px-2 py-1 rounded border border-gray-light bg-white w-fit'}>
|
||||
User Behavior Analysis
|
||||
</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'}>
|
||||
|
|
@ -66,6 +137,20 @@ function TextPlaceholder() {
|
|||
);
|
||||
}
|
||||
|
||||
const CodeStringFormatter = ({ text }: { text: string }) => {
|
||||
const parts = text.split(/(`[^`]*`)/).map((part, index) =>
|
||||
part.startsWith('`') && part.endsWith('`') ? (
|
||||
<div key={index} className="whitespace-nowrap bg-gray-lightest font-mono mx-1 px-1 border">
|
||||
{part.substring(1, part.length - 1)}
|
||||
</div>
|
||||
) : (
|
||||
<span key={index}>{part}</span>
|
||||
)
|
||||
);
|
||||
|
||||
return <>{parts}</>;
|
||||
};
|
||||
|
||||
const summaryBlockStyle: React.CSSProperties = {
|
||||
background: 'linear-gradient(180deg, #E8EBFF -24.14%, rgba(236, 254, 255, 0.00) 100%)',
|
||||
width: '100%',
|
||||
|
|
@ -77,4 +162,10 @@ const summaryBlockStyle: React.CSSProperties = {
|
|||
padding: '1rem',
|
||||
};
|
||||
|
||||
export default observer(SummaryBlock);
|
||||
export default connect((state: Record<string, any>) => ({
|
||||
zoomEnabled: state.getIn(['components', 'player']).timelineZoom.enabled,
|
||||
zoomStartTs: state.getIn(['components', 'player']).timelineZoom.startTs,
|
||||
zoomEndTs: state.getIn(['components', 'player']).timelineZoom.endTs,
|
||||
zoomTab: state.getIn(['components', 'player']).zoomTab,
|
||||
duration: state.getIn(['sessions', 'current']).durationSeconds,
|
||||
}))(observer(SummaryBlock));
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ interface IProps {
|
|||
filterOutNote: (id: string) => void;
|
||||
eventsIndex: number[];
|
||||
uxtVideo: string;
|
||||
zoomEnabled: boolean;
|
||||
zoomStartTs: number;
|
||||
zoomEndTs: number;
|
||||
}
|
||||
|
||||
function EventsBlock(props: IProps) {
|
||||
|
|
@ -82,8 +85,21 @@ function EventsBlock(props: IProps) {
|
|||
return mergeEventLists(
|
||||
filteredLength > 0 ? filteredEvents : eventsWithMobxNotes,
|
||||
tabChangeEvents
|
||||
).filter((e) =>
|
||||
props.zoomEnabled
|
||||
? 'time' in e
|
||||
? e.time >= props.zoomStartTs && e.time <= props.zoomEndTs
|
||||
: false
|
||||
: true
|
||||
);
|
||||
}, [filteredLength, notesWithEvtsLength, notesLength]);
|
||||
}, [
|
||||
filteredLength,
|
||||
notesWithEvtsLength,
|
||||
notesLength,
|
||||
props.zoomEnabled,
|
||||
props.zoomStartTs,
|
||||
props.zoomEndTs,
|
||||
]);
|
||||
|
||||
const write = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
props.setEventFilter({ query: value });
|
||||
|
|
@ -180,8 +196,20 @@ function EventsBlock(props: IProps) {
|
|||
<div className={cn(styles.header, 'p-4')}>
|
||||
{uxtestingStore.isUxt() ? (
|
||||
<div style={{ width: 240, height: 130 }} className={'relative'}>
|
||||
<video className={'z-20 fixed'} muted autoPlay controls src={props.uxtVideo} width={240} />
|
||||
<div style={{ top: '40%', left: '50%', transform: 'translate(-50%, -50%)' }} className={'absolute z-10'}>No video</div>
|
||||
<video
|
||||
className={'z-20 fixed'}
|
||||
muted
|
||||
autoPlay
|
||||
controls
|
||||
src={props.uxtVideo}
|
||||
width={240}
|
||||
/>
|
||||
<div
|
||||
style={{ top: '40%', left: '50%', transform: 'translate(-50%, -50%)' }}
|
||||
className={'absolute z-10'}
|
||||
>
|
||||
No video
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className={cn(styles.hAndProgress, 'mt-3')}>
|
||||
|
|
@ -233,6 +261,9 @@ export default connect(
|
|||
filteredEvents: state.getIn(['sessions', 'filteredEvents']),
|
||||
query: state.getIn(['sessions', 'eventsQuery']),
|
||||
eventsIndex: state.getIn(['sessions', 'eventsIndex']),
|
||||
zoomEnabled: state.getIn(['components', 'player']).timelineZoom.enabled,
|
||||
zoomStartTs: state.getIn(['components', 'player']).timelineZoom.startTs,
|
||||
zoomEndTs: state.getIn(['components', 'player']).timelineZoom.endTs,
|
||||
}),
|
||||
{
|
||||
setEventFilter,
|
||||
|
|
|
|||
|
|
@ -1,27 +1,40 @@
|
|||
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';
|
||||
import EventRow from './components/EventRow';
|
||||
import { connect } from 'react-redux';
|
||||
import TimelineScale from './components/TimelineScale';
|
||||
import FeatureSelection, { HELP_MESSAGE } from './components/FeatureSelection/FeatureSelection';
|
||||
import TimelinePointer from './components/TimelinePointer';
|
||||
import VerticalPointerLine from './components/VerticalPointerLine';
|
||||
import cn from 'classnames';
|
||||
import OverviewPanelContainer from './components/OverviewPanelContainer';
|
||||
import { NoContent, Icon } from 'UI';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Segmented } from 'antd'
|
||||
|
||||
import { MobilePlayerContext, PlayerContext } from 'App/components/Session/playerContext';
|
||||
import { useStore } from 'App/mstore';
|
||||
import SummaryBlock from 'Components/Session/Player/ReplayPlayer/SummaryBlock';
|
||||
import { SummaryButton } from 'Components/Session_/Player/Controls/Controls';
|
||||
import { toggleBottomBlock, setZoomTab } from 'Duck/components/player';
|
||||
import { Icon, NoContent } from 'UI';
|
||||
|
||||
import BottomBlock from '../BottomBlock';
|
||||
import EventRow from './components/EventRow';
|
||||
import FeatureSelection, { HELP_MESSAGE } from './components/FeatureSelection/FeatureSelection';
|
||||
import OverviewPanelContainer from './components/OverviewPanelContainer';
|
||||
import TimelinePointer from './components/TimelinePointer';
|
||||
import TimelineScale from './components/TimelineScale';
|
||||
import VerticalPointerLine from './components/VerticalPointerLine';
|
||||
|
||||
function MobileOverviewPanelCont({
|
||||
issuesList,
|
||||
sessionId,
|
||||
zoomEnabled,
|
||||
zoomStartTs,
|
||||
zoomEndTs,
|
||||
setZoomTab,
|
||||
zoomTab
|
||||
}: {
|
||||
issuesList: Record<string, any>[];
|
||||
sessionId: string;
|
||||
zoomEnabled: boolean;
|
||||
zoomStartTs: number;
|
||||
zoomEndTs: number;
|
||||
setZoomTab: (tab: string) => void;
|
||||
zoomTab: 'overview' | 'journey' | 'issues' | 'errors'
|
||||
}) {
|
||||
const { aiSummaryStore } = useStore();
|
||||
const { store, player } = React.useContext(MobilePlayerContext);
|
||||
|
|
@ -45,12 +58,18 @@ function MobileOverviewPanelCont({
|
|||
|
||||
const fetchPresented = fetchList.length > 0;
|
||||
|
||||
const checkInZoomRange = (list: any[]) => {
|
||||
return list.filter((i) => (zoomEnabled ? i.time >= zoomStartTs && i.time <= zoomEndTs : true));
|
||||
};
|
||||
|
||||
const resources = {
|
||||
NETWORK: fetchList.filter((r: any) => r.status >= 400 || r.isRed || r.isYellow),
|
||||
ERRORS: exceptionsList,
|
||||
EVENTS: eventsList,
|
||||
PERFORMANCE: performanceChartData,
|
||||
FRUSTRATIONS: frustrationsList,
|
||||
NETWORK: checkInZoomRange(
|
||||
fetchList.filter((r: any) => r.status >= 400 || r.isRed || r.isYellow)
|
||||
),
|
||||
ERRORS: checkInZoomRange(exceptionsList),
|
||||
EVENTS: checkInZoomRange(eventsList),
|
||||
PERFORMANCE: checkInZoomRange(performanceChartData),
|
||||
FRUSTRATIONS: checkInZoomRange(frustrationsList),
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -88,11 +107,27 @@ function MobileOverviewPanelCont({
|
|||
showSummary={isSaas}
|
||||
toggleSummary={() => aiSummaryStore.setToggleSummary(!aiSummaryStore.toggleSummary)}
|
||||
summaryChecked={aiSummaryStore.toggleSummary}
|
||||
setZoomTab={setZoomTab}
|
||||
zoomTab={zoomTab}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function WebOverviewPanelCont({ sessionId }: { sessionId: string }) {
|
||||
function WebOverviewPanelCont({
|
||||
sessionId,
|
||||
zoomEnabled,
|
||||
zoomStartTs,
|
||||
zoomEndTs,
|
||||
setZoomTab,
|
||||
zoomTab,
|
||||
}: {
|
||||
sessionId: string;
|
||||
zoomEnabled: boolean;
|
||||
zoomStartTs: number;
|
||||
zoomEndTs: number;
|
||||
setZoomTab: (tab: string) => void;
|
||||
zoomTab: 'overview' | 'journey' | 'issues' | 'errors'
|
||||
}) {
|
||||
const { aiSummaryStore } = useStore();
|
||||
const { store } = React.useContext(PlayerContext);
|
||||
const [selectedFeatures, setSelectedFeatures] = React.useState([
|
||||
|
|
@ -121,15 +156,19 @@ function WebOverviewPanelCont({ sessionId }: { sessionId: string }) {
|
|||
.concat(graphqlList.filter((i: any) => parseInt(i.status) >= 400))
|
||||
.filter((i: any) => i.type === 'fetch');
|
||||
|
||||
const checkInZoomRange = (list: any[]) => {
|
||||
return list.filter((i) => (zoomEnabled ? i.time >= zoomStartTs && i.time <= zoomEndTs : true));
|
||||
};
|
||||
|
||||
const resources: any = React.useMemo(() => {
|
||||
return {
|
||||
NETWORK: resourceList,
|
||||
ERRORS: exceptionsList,
|
||||
EVENTS: stackEventList,
|
||||
PERFORMANCE: performanceChartData,
|
||||
FRUSTRATIONS: frustrationsList,
|
||||
NETWORK: checkInZoomRange(resourceList),
|
||||
ERRORS: checkInZoomRange(exceptionsList),
|
||||
EVENTS: checkInZoomRange(stackEventList),
|
||||
PERFORMANCE: checkInZoomRange(performanceChartData),
|
||||
FRUSTRATIONS: checkInZoomRange(frustrationsList),
|
||||
};
|
||||
}, [tabStates, currentTab]);
|
||||
}, [tabStates, currentTab, zoomEnabled, zoomStartTs, zoomEndTs]);
|
||||
|
||||
const originStr = window.env.ORIGIN || window.location.origin;
|
||||
const isSaas = /app\.openreplay\.com/.test(originStr);
|
||||
|
|
@ -144,6 +183,8 @@ function WebOverviewPanelCont({ sessionId }: { sessionId: string }) {
|
|||
toggleSummary={() => aiSummaryStore.setToggleSummary(!aiSummaryStore.toggleSummary)}
|
||||
summaryChecked={aiSummaryStore.toggleSummary}
|
||||
sessionId={sessionId}
|
||||
setZoomTab={setZoomTab}
|
||||
zoomTab={zoomTab}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -160,6 +201,8 @@ function PanelComponent({
|
|||
toggleSummary,
|
||||
summaryChecked,
|
||||
sessionId,
|
||||
zoomTab,
|
||||
setZoomTab,
|
||||
}: any) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
|
|
@ -168,7 +211,31 @@ function PanelComponent({
|
|||
<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} />
|
||||
<>
|
||||
<SummaryButton withToggle onClick={toggleSummary} toggleValue={summaryChecked} />
|
||||
<Segmented
|
||||
value={zoomTab}
|
||||
onChange={(val) => setZoomTab(val)}
|
||||
options={[
|
||||
{
|
||||
label: 'Overview',
|
||||
value: 'overview',
|
||||
},
|
||||
{
|
||||
label: 'User Journey',
|
||||
value: 'journey',
|
||||
},
|
||||
{
|
||||
label: 'Issues',
|
||||
value: 'issues',
|
||||
},
|
||||
{
|
||||
label: 'Suggestions',
|
||||
value: 'errors',
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center h-20 mr-4">
|
||||
|
|
@ -247,9 +314,12 @@ export const OverviewPanel = connect(
|
|||
(state: Record<string, any>) => ({
|
||||
issuesList: state.getIn(['sessions', 'current']).issues,
|
||||
sessionId: state.getIn(['sessions', 'current']).sessionId,
|
||||
zoomEnabled: state.getIn(['components', 'player']).timelineZoom.enabled,
|
||||
zoomStartTs: state.getIn(['components', 'player']).timelineZoom.startTs,
|
||||
zoomEndTs: state.getIn(['components', 'player']).timelineZoom.endTs,
|
||||
}),
|
||||
{
|
||||
toggleBottomBlock,
|
||||
toggleBottomBlock, setZoomTab
|
||||
}
|
||||
)(observer(WebOverviewPanelCont));
|
||||
|
||||
|
|
@ -257,8 +327,12 @@ export const MobileOverviewPanel = connect(
|
|||
(state: Record<string, any>) => ({
|
||||
issuesList: state.getIn(['sessions', 'current']).issues,
|
||||
sessionId: state.getIn(['sessions', 'current']).sessionId,
|
||||
zoomEnabled: state.getIn(['components', 'player']).timelineZoom.enabled,
|
||||
zoomStartTs: state.getIn(['components', 'player']).timelineZoom.startTs,
|
||||
zoomEndTs: state.getIn(['components', 'player']).timelineZoom.endTs,
|
||||
zoomTab: state.getIn(['components', 'player']).zoomTab
|
||||
}),
|
||||
{
|
||||
toggleBottomBlock,
|
||||
toggleBottomBlock, setZoomTab,
|
||||
}
|
||||
)(observer(MobileOverviewPanelCont));
|
||||
|
|
|
|||
|
|
@ -75,8 +75,15 @@ function Controls(props: any) {
|
|||
const { player, store } = React.useContext(PlayerContext);
|
||||
const { uxtestingStore } = useStore();
|
||||
|
||||
const { playing, completed, skip, speed, messagesLoading, markedTargets, inspectorMode } =
|
||||
store.get();
|
||||
const {
|
||||
playing,
|
||||
completed,
|
||||
skip,
|
||||
speed,
|
||||
messagesLoading,
|
||||
markedTargets,
|
||||
inspectorMode,
|
||||
} = store.get();
|
||||
|
||||
const {
|
||||
bottomBlock,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import DraggableMarkers from 'Components/Session_/Player/Controls/components/ZoomDragLayer';
|
||||
import React, { useEffect, useMemo, useContext, useState, useRef } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import TimeTracker from './TimeTracker';
|
||||
import stl from './timeline.module.css';
|
||||
import { setTimelinePointer, setTimelineHoverTime } from 'Duck/sessions';
|
||||
import DraggableCircle from './components/DraggableCircle';
|
||||
import CustomDragLayer, { OnDragCallback } from './components/CustomDragLayer';
|
||||
import { debounce } from 'App/utils';
|
||||
import TooltipContainer from './components/TooltipContainer';
|
||||
|
|
@ -11,37 +10,33 @@ import { PlayerContext } from 'App/components/Session/playerContext';
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import Issue from "Types/session/issue";
|
||||
import Issue from 'Types/session/issue';
|
||||
import { WebEventsList, MobEventsList } from './EventsList';
|
||||
import NotesList from './NotesList';
|
||||
import SkipIntervalsList from './SkipIntervalsList'
|
||||
import TimelineTracker from "Components/Session_/Player/Controls/TimelineTracker";
|
||||
import SkipIntervalsList from './SkipIntervalsList';
|
||||
import TimelineTracker from 'Components/Session_/Player/Controls/TimelineTracker';
|
||||
|
||||
interface IProps {
|
||||
issues: Issue[]
|
||||
setTimelineHoverTime: (t: number) => void
|
||||
startedAt: number
|
||||
tooltipVisible: boolean
|
||||
timezone?: string
|
||||
isMobile?: boolean
|
||||
issues: Issue[];
|
||||
setTimelineHoverTime: (t: number) => void;
|
||||
startedAt: number;
|
||||
tooltipVisible: boolean;
|
||||
timezone?: string;
|
||||
isMobile?: boolean;
|
||||
timelineZoomEnabled: boolean;
|
||||
timelineZoomStartTs: number;
|
||||
timelineZoomEndTs: number;
|
||||
}
|
||||
|
||||
function Timeline(props: IProps) {
|
||||
const { player, store } = useContext(PlayerContext)
|
||||
const [wasPlaying, setWasPlaying] = useState(false)
|
||||
const { player, store } = useContext(PlayerContext);
|
||||
const [wasPlaying, setWasPlaying] = useState(false);
|
||||
const { settingsStore } = useStore();
|
||||
const {
|
||||
playing,
|
||||
skipToIssue,
|
||||
ready,
|
||||
endTime,
|
||||
devtoolsLoading,
|
||||
domLoading,
|
||||
} = store.get()
|
||||
const { issues, timezone } = props;
|
||||
const { playing, skipToIssue, ready, endTime, devtoolsLoading, domLoading } = store.get();
|
||||
const { issues, timezone, timelineZoomEnabled } = props;
|
||||
|
||||
const progressRef = useRef<HTMLDivElement>(null)
|
||||
const timelineRef = useRef<HTMLDivElement>(null)
|
||||
const progressRef = useRef<HTMLDivElement>(null);
|
||||
const timelineRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const scale = 100 / endTime;
|
||||
|
||||
|
|
@ -51,10 +46,10 @@ function Timeline(props: IProps) {
|
|||
if (firstIssue && skipToIssue) {
|
||||
player.jump(firstIssue.time);
|
||||
}
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
const debouncedJump = useMemo(() => debounce(player.jump, 500), [])
|
||||
const debouncedTooltipChange = useMemo(() => debounce(props.setTimelineHoverTime, 50), [])
|
||||
const debouncedJump = useMemo(() => debounce(player.jump, 500), []);
|
||||
const debouncedTooltipChange = useMemo(() => debounce(props.setTimelineHoverTime, 50), []);
|
||||
|
||||
const onDragEnd = () => {
|
||||
if (wasPlaying) {
|
||||
|
|
@ -64,31 +59,37 @@ function Timeline(props: IProps) {
|
|||
|
||||
const onDrag: OnDragCallback = (offset) => {
|
||||
// @ts-ignore react mismatch
|
||||
const p = (offset.x) / progressRef.current.offsetWidth;
|
||||
const p = offset.x / progressRef.current.offsetWidth;
|
||||
const time = Math.max(Math.round(p * endTime), 0);
|
||||
debouncedJump(time);
|
||||
hideTimeTooltip();
|
||||
if (playing) {
|
||||
setWasPlaying(true)
|
||||
setWasPlaying(true);
|
||||
player.pause();
|
||||
}
|
||||
};
|
||||
|
||||
const showTimeTooltip = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (
|
||||
e.target !== progressRef.current
|
||||
&& e.target !== timelineRef.current
|
||||
e.target !== progressRef.current &&
|
||||
e.target !== timelineRef.current &&
|
||||
// @ts-ignore black magic
|
||||
&& !progressRef.current.contains(e.target)
|
||||
!progressRef.current.contains(e.target)
|
||||
) {
|
||||
return props.tooltipVisible && hideTimeTooltip();
|
||||
}
|
||||
|
||||
const time = getTime(e);
|
||||
if (!time) return;
|
||||
const tz = settingsStore.sessionSettings.timezone.value
|
||||
const timeStr = DateTime.fromMillis(props.startedAt + time).setZone(tz).toFormat(`hh:mm:ss a`)
|
||||
const userTimeStr = timezone ? DateTime.fromMillis(props.startedAt + time).setZone(timezone).toFormat(`hh:mm:ss a`) : undefined
|
||||
const tz = settingsStore.sessionSettings.timezone.value;
|
||||
const timeStr = DateTime.fromMillis(props.startedAt + time)
|
||||
.setZone(tz)
|
||||
.toFormat(`hh:mm:ss a`);
|
||||
const userTimeStr = timezone
|
||||
? DateTime.fromMillis(props.startedAt + time)
|
||||
.setZone(timezone)
|
||||
.toFormat(`hh:mm:ss a`)
|
||||
: undefined;
|
||||
|
||||
const timeLineTooltip = {
|
||||
time: Duration.fromMillis(time).toFormat(`mm:ss`),
|
||||
|
|
@ -99,7 +100,7 @@ function Timeline(props: IProps) {
|
|||
};
|
||||
|
||||
debouncedTooltipChange(timeLineTooltip);
|
||||
}
|
||||
};
|
||||
|
||||
const hideTimeTooltip = () => {
|
||||
const timeLineTooltip = { isVisible: false };
|
||||
|
|
@ -134,6 +135,7 @@ function Timeline(props: IProps) {
|
|||
left: '0.5rem',
|
||||
}}
|
||||
>
|
||||
{timelineZoomEnabled ? <DraggableMarkers scale={scale} /> : null}
|
||||
<div
|
||||
className={stl.progress}
|
||||
onClick={ready ? jumpToTime : undefined}
|
||||
|
|
@ -179,6 +181,7 @@ export default connect(
|
|||
startedAt: state.getIn(['sessions', 'current']).startedAt || 0,
|
||||
timezone: state.getIn(['sessions', 'current']).timezone,
|
||||
tooltipVisible: state.getIn(['sessions', 'timeLineTooltip', 'isVisible']),
|
||||
timelineZoomEnabled: state.getIn(['components', 'player']).timelineZoom.enabled,
|
||||
}),
|
||||
{ setTimelinePointer, setTimelineHoverTime }
|
||||
)(observer(Timeline))
|
||||
)(observer(Timeline));
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Icon } from 'UI';
|
|||
import { Button } from 'antd';
|
||||
import PlayingTime from './PlayingTime';
|
||||
import { JumpBack, IntervalSelector, JumpForward, SpeedOptions } from './ControlsComponents';
|
||||
import TimelineZoomButton from 'Components/Session_/Player/Controls/components/TimelineZoomButton';
|
||||
|
||||
interface Props {
|
||||
skip: boolean;
|
||||
|
|
@ -80,15 +81,14 @@ function PlayerControls(props: Props) {
|
|||
<JumpForward forthTenSeconds={forthTenSeconds} currentInterval={currentInterval} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<div className="mx-1" />
|
||||
<div className="flex items-center gap-2 ml-2">
|
||||
<TimelineZoomButton />
|
||||
<SpeedOptions
|
||||
toggleSpeed={toggleSpeed}
|
||||
disabled={disabled}
|
||||
toggleTooltip={toggleTooltip}
|
||||
speed={speed}
|
||||
/>
|
||||
<div className="mx-1" />
|
||||
<Button
|
||||
onClick={toggleSkip}
|
||||
disabled={disabled}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button } from 'antd';
|
||||
import { toggleZoom } from 'Duck/components/player';
|
||||
import { PlayerContext } from 'Components/Session/playerContext';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
interface Props {
|
||||
enabled: boolean;
|
||||
startTs: number;
|
||||
endTs: number;
|
||||
toggleZoom: typeof toggleZoom;
|
||||
}
|
||||
|
||||
function TimelineZoomButton({ enabled, toggleZoom }: Props) {
|
||||
const { store } = React.useContext(PlayerContext);
|
||||
|
||||
const onClickHandler = () => {
|
||||
// 2% of the timeline * 2 as initial zoom range
|
||||
const distance = store.get().endTime / 50;
|
||||
toggleZoom({
|
||||
enabled: !enabled,
|
||||
range: [
|
||||
Math.max(store.get().time - distance, 0),
|
||||
Math.min(store.get().time + distance, store.get().endTime),
|
||||
],
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Button onClick={onClickHandler} size={'small'} className={'flex items-center font-semibold'}>
|
||||
Timeline Zoom {enabled ? 'On' : 'Off'}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state: Record<string, any>) => ({
|
||||
enabled: state.getIn(['components', 'player']).timelineZoom.enabled,
|
||||
startTs: state.getIn(['components', 'player']).timelineZoom.startTs,
|
||||
endTs: state.getIn(['components', 'player']).timelineZoom.endTs,
|
||||
}),
|
||||
{ toggleZoom }
|
||||
)(observer(TimelineZoomButton));
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
|
||||
import { getTimelinePosition } from 'Components/Session_/Player/Controls/getTimelinePosition';
|
||||
import { toggleZoom } from 'Duck/components/player';
|
||||
|
||||
|
||||
interface Props {
|
||||
timelineZoomStartTs: number;
|
||||
timelineZoomEndTs: number;
|
||||
scale: number;
|
||||
toggleZoom: typeof toggleZoom;
|
||||
}
|
||||
|
||||
const DraggableMarkers = ({ timelineZoomStartTs, timelineZoomEndTs, scale, toggleZoom }: Props) => {
|
||||
const [startPos, setStartPos] = useState(getTimelinePosition(timelineZoomStartTs, scale));
|
||||
const [endPos, setEndPos] = useState(getTimelinePosition(timelineZoomEndTs, scale));
|
||||
const [dragging, setDragging] = useState<string | null>(null);
|
||||
|
||||
const convertToPercentage = useCallback((clientX: number, element: HTMLElement) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const x = clientX - rect.left;
|
||||
return (x / rect.width) * 100;
|
||||
}, []);
|
||||
|
||||
const startDrag = useCallback(
|
||||
(marker: 'start' | 'end' | 'body') => (event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
setDragging(marker);
|
||||
},
|
||||
[convertToPercentage, startPos]
|
||||
);
|
||||
|
||||
const minDistance = 1.5;
|
||||
const onDrag = useCallback(
|
||||
(event: any) => {
|
||||
event.stopPropagation();
|
||||
if (dragging && event.clientX !== 0) {
|
||||
const newPos = convertToPercentage(event.clientX, event.currentTarget);
|
||||
if (dragging === 'start') {
|
||||
setStartPos(newPos);
|
||||
if (endPos - newPos <= minDistance) {
|
||||
setEndPos(newPos + minDistance);
|
||||
}
|
||||
toggleZoom({ enabled: true, range: [newPos / scale, endPos / scale] });
|
||||
} else if (dragging === 'end') {
|
||||
setEndPos(newPos);
|
||||
if (newPos - startPos <= minDistance) {
|
||||
setStartPos(newPos - minDistance);
|
||||
}
|
||||
toggleZoom({ enabled: true, range: [startPos / scale, newPos / scale] });
|
||||
} else if (dragging === 'body') {
|
||||
const offset = (endPos - startPos) / 2;
|
||||
let newStartPos = newPos - offset;
|
||||
let newEndPos = newStartPos + (endPos - startPos);
|
||||
if (newStartPos < 0) {
|
||||
newStartPos = 0;
|
||||
newEndPos = endPos - startPos;
|
||||
} else if (newEndPos > 100) {
|
||||
newEndPos = 100;
|
||||
newStartPos = 100 - (endPos - startPos);
|
||||
}
|
||||
setStartPos(newStartPos);
|
||||
setEndPos(newEndPos);
|
||||
toggleZoom({ enabled: true, range: [newStartPos / scale, newEndPos / scale] });
|
||||
}
|
||||
}
|
||||
},
|
||||
[dragging, startPos, endPos, scale, toggleZoom]
|
||||
);
|
||||
|
||||
const endDrag = useCallback(() => {
|
||||
setDragging(null);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseMove={onDrag}
|
||||
onMouseLeave={endDrag}
|
||||
onMouseUp={endDrag}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '24px',
|
||||
left: 0,
|
||||
top: '-4px',
|
||||
zIndex: 100,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="marker start"
|
||||
onMouseDown={startDrag("start")}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: `${startPos}%`,
|
||||
height: "100%",
|
||||
background: "#FCC100",
|
||||
cursor: "ew-resize",
|
||||
borderTopLeftRadius: 3,
|
||||
borderBottomLeftRadius: 3,
|
||||
zIndex: 100,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
paddingRight: 3,
|
||||
paddingLeft: 6,
|
||||
width: 18,
|
||||
}}
|
||||
>
|
||||
<div className={"bg-black rounded-xl"} style={{ zIndex: 101, height: 18, width: 2, marginRight: 3, overflow: "hidden" }} />
|
||||
<div className={"bg-black rounded-xl"} style={{ zIndex: 101, height: 18, width: 2, overflow: "hidden" }} />
|
||||
</div>
|
||||
<div
|
||||
className="slider-body"
|
||||
onMouseDown={startDrag("body")}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: `calc(${startPos}% + 18px)`,
|
||||
width: `calc(${endPos - startPos}% - 18px)`,
|
||||
height: '100%',
|
||||
background: 'rgba(252, 193, 0, 0.10)',
|
||||
borderTop: '2px solid #FCC100',
|
||||
borderBottom: '2px solid #FCC100',
|
||||
cursor: 'grab',
|
||||
zIndex: 100,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="marker end"
|
||||
onMouseDown={startDrag('end')}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${endPos}%`,
|
||||
height: '100%',
|
||||
background: '#FCC100',
|
||||
cursor: 'ew-resize',
|
||||
borderTopRightRadius: 3,
|
||||
borderBottomRightRadius: 3,
|
||||
zIndex: 100,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
paddingLeft: 3,
|
||||
paddingRight: 6,
|
||||
width: 18,
|
||||
}}
|
||||
>
|
||||
<div className={'bg-black rounded-xl'} style={{ zIndex: 101, height: 18, width: 2, marginRight: 3, overflow: 'hidden' }} />
|
||||
<div className={'bg-black rounded-xl'} style={{ zIndex: 101, height: 18, width: 2, overflow: 'hidden' }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(
|
||||
(state: Record<string, any>) => ({
|
||||
timelineZoomStartTs: state.getIn(['components', 'player']).timelineZoom.startTs,
|
||||
timelineZoomEndTs: state.getIn(['components', 'player']).timelineZoom.endTs,
|
||||
}),
|
||||
{ toggleZoom }
|
||||
)(DraggableMarkers);
|
||||
|
|
@ -11,8 +11,9 @@ import { useStore } from 'App/mstore';
|
|||
import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorDetailsModal';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
|
||||
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter'
|
||||
import useCellMeasurerCache from 'App/hooks/useCellMeasurerCache'
|
||||
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter';
|
||||
import useCellMeasurerCache from 'App/hooks/useCellMeasurerCache';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
const ALL = 'ALL';
|
||||
const INFO = 'INFO';
|
||||
|
|
@ -25,13 +26,17 @@ const LEVEL_TAB = {
|
|||
[LogLevel.WARN]: WARNINGS,
|
||||
[LogLevel.ERROR]: ERRORS,
|
||||
[LogLevel.EXCEPTION]: ERRORS,
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
const TABS = [ALL, ERRORS, WARNINGS, INFO].map((tab) => ({ text: tab, key: tab }));
|
||||
|
||||
function renderWithNL(s: string | null = '') {
|
||||
if (typeof s !== 'string') return '';
|
||||
return s.split('\n').map((line, i) => <div key={i + line.slice(0, 6)} className={cn({ 'ml-20': i !== 0 })}>{line}</div>);
|
||||
return s.split('\n').map((line, i) => (
|
||||
<div key={i + line.slice(0, 6)} className={cn({ 'ml-20': i !== 0 })}>
|
||||
{line}
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
const getIconProps = (level: any) => {
|
||||
|
|
@ -56,62 +61,80 @@ const getIconProps = (level: any) => {
|
|||
return null;
|
||||
};
|
||||
|
||||
|
||||
const INDEX_KEY = 'console';
|
||||
|
||||
function ConsolePanel({ isLive }: { isLive?: boolean }) {
|
||||
function ConsolePanel({
|
||||
isLive,
|
||||
zoomEnabled,
|
||||
zoomStartTs,
|
||||
zoomEndTs,
|
||||
}: {
|
||||
isLive?: boolean;
|
||||
zoomEnabled: boolean;
|
||||
zoomStartTs: number;
|
||||
zoomEndTs: number;
|
||||
}) {
|
||||
const {
|
||||
sessionStore: { devTools },
|
||||
} = useStore()
|
||||
} = useStore();
|
||||
|
||||
const filter = devTools[INDEX_KEY].filter;
|
||||
const activeTab = devTools[INDEX_KEY].activeTab;
|
||||
// Why do we need to keep index in the store? if we could get read of it it would simplify the code
|
||||
const activeIndex = devTools[INDEX_KEY].index;
|
||||
const [ isDetailsModalActive, setIsDetailsModalActive ] = useState(false);
|
||||
const [isDetailsModalActive, setIsDetailsModalActive] = useState(false);
|
||||
const { showModal } = useModal();
|
||||
|
||||
const { player, store } = React.useContext(PlayerContext)
|
||||
const jump = (t: number) => player.jump(t)
|
||||
const { player, store } = React.useContext(PlayerContext);
|
||||
const jump = (t: number) => player.jump(t);
|
||||
|
||||
const { currentTab, tabStates } = store.get()
|
||||
const { logList = [], exceptionsList = [], logListNow = [], exceptionsListNow = [] } = tabStates[currentTab]
|
||||
const { currentTab, tabStates } = store.get();
|
||||
const {
|
||||
logList = [],
|
||||
exceptionsList = [],
|
||||
logListNow = [],
|
||||
exceptionsListNow = [],
|
||||
} = tabStates[currentTab];
|
||||
|
||||
const list = isLive ?
|
||||
useMemo(() => logListNow.concat(exceptionsListNow).sort((a, b) => a.time - b.time),
|
||||
[logListNow.length, exceptionsListNow.length]
|
||||
) as ILog[]
|
||||
: useMemo(() => logList.concat(exceptionsList).sort((a, b) => a.time - b.time),
|
||||
[ logList.length, exceptionsList.length ],
|
||||
) as ILog[]
|
||||
let filteredList = useRegExListFilterMemo(list, l => l.value, filter)
|
||||
filteredList = useTabListFilterMemo(filteredList, l => LEVEL_TAB[l.level], ALL, activeTab)
|
||||
const list = isLive
|
||||
? (useMemo(
|
||||
() => logListNow.concat(exceptionsListNow).sort((a, b) => a.time - b.time),
|
||||
[logListNow.length, exceptionsListNow.length]
|
||||
) as ILog[])
|
||||
: (useMemo(
|
||||
() => logList.concat(exceptionsList).sort((a, b) => a.time - b.time),
|
||||
[logList.length, exceptionsList.length]
|
||||
).filter((l) =>
|
||||
zoomEnabled ? l.time >= zoomStartTs && l.time <= zoomEndTs : true
|
||||
) as ILog[]);
|
||||
let filteredList = useRegExListFilterMemo(list, (l) => l.value, filter);
|
||||
filteredList = useTabListFilterMemo(filteredList, (l) => LEVEL_TAB[l.level], ALL, activeTab);
|
||||
|
||||
React.useEffect(() => {
|
||||
setTimeout(() => {
|
||||
cache.clearAll();
|
||||
_list.current?.recomputeRowHeights();
|
||||
}, 0)
|
||||
}, [activeTab, filter])
|
||||
const onTabClick = (activeTab: any) => devTools.update(INDEX_KEY, { activeTab })
|
||||
const onFilterChange = ({ target: { value } }: any) => devTools.update(INDEX_KEY, { filter: value })
|
||||
}, 0);
|
||||
}, [activeTab, filter]);
|
||||
const onTabClick = (activeTab: any) => devTools.update(INDEX_KEY, { activeTab });
|
||||
const onFilterChange = ({ target: { value } }: any) =>
|
||||
devTools.update(INDEX_KEY, { filter: value });
|
||||
|
||||
// AutoScroll
|
||||
const [
|
||||
timeoutStartAutoscroll,
|
||||
stopAutoscroll,
|
||||
] = useAutoscroll(
|
||||
// AutoScroll
|
||||
const [timeoutStartAutoscroll, stopAutoscroll] = useAutoscroll(
|
||||
filteredList,
|
||||
getLastItemTime(logListNow, exceptionsListNow),
|
||||
activeIndex,
|
||||
index => devTools.update(INDEX_KEY, { index })
|
||||
)
|
||||
const onMouseEnter = stopAutoscroll
|
||||
(index) => devTools.update(INDEX_KEY, { index })
|
||||
);
|
||||
const onMouseEnter = stopAutoscroll;
|
||||
const onMouseLeave = () => {
|
||||
if (isDetailsModalActive) { return }
|
||||
timeoutStartAutoscroll()
|
||||
}
|
||||
|
||||
if (isDetailsModalActive) {
|
||||
return;
|
||||
}
|
||||
timeoutStartAutoscroll();
|
||||
};
|
||||
|
||||
const _list = useRef<List>(null); // TODO: fix react-virtualized types & encapsulate scrollToRow logic
|
||||
useEffect(() => {
|
||||
if (_list.current) {
|
||||
|
|
@ -120,51 +143,45 @@ function ConsolePanel({ isLive }: { isLive?: boolean }) {
|
|||
}
|
||||
}, [activeIndex]);
|
||||
|
||||
const cache = useCellMeasurerCache()
|
||||
const cache = useCellMeasurerCache();
|
||||
|
||||
const showDetails = (log: any) => {
|
||||
setIsDetailsModalActive(true);
|
||||
showModal(
|
||||
<ErrorDetailsModal errorId={log.errorId} />,
|
||||
{
|
||||
right: true,
|
||||
width: 1200,
|
||||
onClose: () => {
|
||||
setIsDetailsModalActive(false)
|
||||
timeoutStartAutoscroll()
|
||||
}
|
||||
});
|
||||
showModal(<ErrorDetailsModal errorId={log.errorId} />, {
|
||||
right: true,
|
||||
width: 1200,
|
||||
onClose: () => {
|
||||
setIsDetailsModalActive(false);
|
||||
timeoutStartAutoscroll();
|
||||
},
|
||||
});
|
||||
devTools.update(INDEX_KEY, { index: filteredList.indexOf(log) });
|
||||
stopAutoscroll()
|
||||
}
|
||||
stopAutoscroll();
|
||||
};
|
||||
const _rowRenderer = ({ index, key, parent, style }: any) => {
|
||||
const item = filteredList[index];
|
||||
|
||||
return (
|
||||
// @ts-ignore
|
||||
<CellMeasurer cache={cache} columnIndex={0} key={key} rowIndex={index} parent={parent}>
|
||||
{({ measure, registerChild }) => (
|
||||
<div ref={registerChild} style={style}>
|
||||
<ConsoleRow
|
||||
log={item}
|
||||
jump={jump}
|
||||
iconProps={getIconProps(item.level)}
|
||||
renderWithNL={renderWithNL}
|
||||
onClick={() => showDetails(item)}
|
||||
recalcHeight={measure}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CellMeasurer>
|
||||
)
|
||||
}
|
||||
// @ts-ignore
|
||||
<CellMeasurer cache={cache} columnIndex={0} key={key} rowIndex={index} parent={parent}>
|
||||
{({ measure, registerChild }) => (
|
||||
<div ref={registerChild} style={style}>
|
||||
<ConsoleRow
|
||||
log={item}
|
||||
jump={jump}
|
||||
iconProps={getIconProps(item.level)}
|
||||
renderWithNL={renderWithNL}
|
||||
onClick={() => showDetails(item)}
|
||||
recalcHeight={measure}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CellMeasurer>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<BottomBlock
|
||||
style={{ height: '100%' }}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<BottomBlock style={{ height: '100%' }} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||
{/* @ts-ignore */}
|
||||
<BottomBlock.Header>
|
||||
<div className="flex items-center">
|
||||
|
|
@ -220,4 +237,8 @@ function ConsolePanel({ isLive }: { isLive?: boolean }) {
|
|||
);
|
||||
}
|
||||
|
||||
export default observer(ConsolePanel);
|
||||
export default connect((state: Record<string, any>) => ({
|
||||
zoomEnabled: state.getIn(['components', 'player']).timelineZoom.enabled,
|
||||
zoomStartTs: state.getIn(['components', 'player']).timelineZoom.startTs,
|
||||
zoomEndTs: state.getIn(['components', 'player']).timelineZoom.endTs,
|
||||
}))(observer(ConsolePanel));
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import BottomBlock from '../BottomBlock';
|
|||
import InfoLine from '../BottomBlock/InfoLine';
|
||||
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
|
||||
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter';
|
||||
import WSModal from './WSModal'
|
||||
import WSModal from './WSModal';
|
||||
|
||||
const INDEX_KEY = 'network';
|
||||
|
||||
|
|
@ -150,7 +150,19 @@ function renderStatus({ status, cached }: { status: string; cached: boolean }) {
|
|||
);
|
||||
}
|
||||
|
||||
function NetworkPanelCont({ startedAt, panelHeight }: { startedAt: number; panelHeight: number }) {
|
||||
function NetworkPanelCont({
|
||||
startedAt,
|
||||
panelHeight,
|
||||
zoomEnabled,
|
||||
zoomStartTs,
|
||||
zoomEndTs,
|
||||
}: {
|
||||
startedAt: number;
|
||||
panelHeight: number;
|
||||
zoomEnabled: boolean;
|
||||
zoomStartTs: number;
|
||||
zoomEndTs: number;
|
||||
}) {
|
||||
const { player, store } = React.useContext(PlayerContext);
|
||||
|
||||
const { domContentLoadedTime, loadTime, domBuildingTime, tabStates, currentTab } = store.get();
|
||||
|
|
@ -184,9 +196,15 @@ function NetworkPanelCont({ startedAt, panelHeight }: { startedAt: number; panel
|
|||
function MobileNetworkPanelCont({
|
||||
startedAt,
|
||||
panelHeight,
|
||||
zoomEnabled,
|
||||
zoomStartTs,
|
||||
zoomEndTs,
|
||||
}: {
|
||||
startedAt: number;
|
||||
panelHeight: number;
|
||||
zoomEnabled: boolean;
|
||||
zoomStartTs: number;
|
||||
zoomEndTs: number;
|
||||
}) {
|
||||
const { player, store } = React.useContext(MobilePlayerContext);
|
||||
|
||||
|
|
@ -219,6 +237,9 @@ function MobileNetworkPanelCont({
|
|||
websocketList={websocketList}
|
||||
// @ts-ignore
|
||||
websocketListNow={websocketListNow}
|
||||
zoomEnabled={zoomEnabled}
|
||||
zoomStartTs={zoomStartTs}
|
||||
zoomEndTs={zoomEndTs}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -229,7 +250,7 @@ type WSMessage = Timed & {
|
|||
timestamp: number;
|
||||
dir: 'up' | 'down';
|
||||
messageType: string;
|
||||
}
|
||||
};
|
||||
|
||||
interface Props {
|
||||
domContentLoadedTime?: {
|
||||
|
|
@ -250,6 +271,9 @@ interface Props {
|
|||
player: WebPlayer | MobilePlayer;
|
||||
startedAt: number;
|
||||
isMobile?: boolean;
|
||||
zoomEnabled: boolean;
|
||||
zoomStartTs: number;
|
||||
zoomEndTs: number;
|
||||
panelHeight: number;
|
||||
}
|
||||
|
||||
|
|
@ -267,6 +291,9 @@ const NetworkPanelComp = observer(
|
|||
isMobile,
|
||||
panelHeight,
|
||||
websocketList,
|
||||
zoomEnabled,
|
||||
zoomStartTs,
|
||||
zoomEndTs,
|
||||
}: Props) => {
|
||||
const { showModal } = useModal();
|
||||
const [sortBy, setSortBy] = useState('time');
|
||||
|
|
@ -333,6 +360,7 @@ const NetworkPanelComp = observer(
|
|||
transferredBodySize: 0,
|
||||
}))
|
||||
)
|
||||
.filter((req) => (zoomEnabled ? req.time >= zoomStartTs && req.time <= zoomEndTs : true))
|
||||
.sort((a, b) => a.time - b.time),
|
||||
[resourceList.length, fetchList.length, socketList]
|
||||
);
|
||||
|
|
@ -407,13 +435,10 @@ const NetworkPanelComp = observer(
|
|||
if (item.type === 'websocket') {
|
||||
const socketMsgList = websocketList.filter((ws) => ws.channelName === item.channelName);
|
||||
|
||||
return showModal(
|
||||
<WSModal
|
||||
socketMsgList={socketMsgList}
|
||||
/>, {
|
||||
right: true, width: 700,
|
||||
}
|
||||
)
|
||||
return showModal(<WSModal socketMsgList={socketMsgList} />, {
|
||||
right: true,
|
||||
width: 700,
|
||||
});
|
||||
}
|
||||
setIsDetailsModalActive(true);
|
||||
showModal(
|
||||
|
|
@ -583,10 +608,16 @@ const NetworkPanelComp = observer(
|
|||
|
||||
const WebNetworkPanel = connect((state: any) => ({
|
||||
startedAt: state.getIn(['sessions', 'current']).startedAt,
|
||||
zoomEnabled: state.getIn(['components', 'player']).timelineZoom.enabled,
|
||||
zoomStartTs: state.getIn(['components', 'player']).timelineZoom.startTs,
|
||||
zoomEndTs: state.getIn(['components', 'player']).timelineZoom.endTs,
|
||||
}))(observer(NetworkPanelCont));
|
||||
|
||||
const MobileNetworkPanel = connect((state: any) => ({
|
||||
startedAt: state.getIn(['sessions', 'current']).startedAt,
|
||||
zoomEnabled: state.getIn(['components', 'player']).timelineZoom.enabled,
|
||||
zoomStartTs: state.getIn(['components', 'player']).timelineZoom.startTs,
|
||||
zoomEndTs: state.getIn(['components', 'player']).timelineZoom.endTs,
|
||||
}))(observer(MobileNetworkPanelCont));
|
||||
|
||||
export { WebNetworkPanel, MobileNetworkPanel };
|
||||
|
|
|
|||
|
|
@ -14,45 +14,102 @@ import StackEventModal from '../StackEventModal';
|
|||
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
|
||||
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter';
|
||||
import useCellMeasurerCache from 'App/hooks/useCellMeasurerCache';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
const mapNames = (type: string) => {
|
||||
if (type === 'openreplay') return 'OpenReplay';
|
||||
return type
|
||||
}
|
||||
return type;
|
||||
};
|
||||
|
||||
const INDEX_KEY = 'stackEvent';
|
||||
const ALL = 'ALL';
|
||||
const TAB_KEYS = [ALL, ...typeList] as const;
|
||||
const TABS = TAB_KEYS.map((tab) => ({ text: tab, key: tab }));
|
||||
|
||||
type EventsList = Array<Timed & { name: string; source: string, key: string }>
|
||||
type EventsList = Array<Timed & { name: string; source: string; key: string }>;
|
||||
|
||||
export const WebStackEventPanel = observer(() => {
|
||||
const { player, store } = React.useContext(PlayerContext);
|
||||
const jump = (t: number) => player.jump(t);
|
||||
const { currentTab, tabStates } = store.get();
|
||||
const WebStackEventPanelComp = observer(
|
||||
({
|
||||
zoomEnabled,
|
||||
zoomStartTs,
|
||||
zoomEndTs,
|
||||
}: {
|
||||
zoomEnabled: boolean;
|
||||
zoomStartTs: number;
|
||||
zoomEndTs: number;
|
||||
}) => {
|
||||
const { player, store } = React.useContext(PlayerContext);
|
||||
const jump = (t: number) => player.jump(t);
|
||||
const { currentTab, tabStates } = store.get();
|
||||
|
||||
const { stackList: list = [], stackListNow: listNow = [] } = tabStates[currentTab];
|
||||
const { stackList: list = [], stackListNow: listNow = [] } = tabStates[currentTab];
|
||||
|
||||
return <EventsPanel list={list as EventsList} listNow={listNow as EventsList} jump={jump} />;
|
||||
});
|
||||
return (
|
||||
<EventsPanel
|
||||
list={list as EventsList}
|
||||
listNow={listNow as EventsList}
|
||||
jump={jump}
|
||||
zoomEnabled={zoomEnabled}
|
||||
zoomStartTs={zoomStartTs}
|
||||
zoomEndTs={zoomEndTs}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const MobileStackEventPanel = observer(() => {
|
||||
const { player, store } = React.useContext(MobilePlayerContext);
|
||||
const jump = (t: number) => player.jump(t);
|
||||
const { eventList: list = [], eventListNow: listNow = [] } = store.get();
|
||||
export const WebStackEventPanel = connect((state: Record<string, any>) => ({
|
||||
zoomEnabled: state.getIn(['components', 'player']).timelineZoom.enabled,
|
||||
zoomStartTs: state.getIn(['components', 'player']).timelineZoom.startTs,
|
||||
zoomEndTs: state.getIn(['components', 'player']).timelineZoom.endTs,
|
||||
}))(WebStackEventPanelComp);
|
||||
|
||||
return <EventsPanel list={list as EventsList} listNow={listNow as EventsList} jump={jump} />;
|
||||
});
|
||||
const MobileStackEventPanelComp = observer(
|
||||
({
|
||||
zoomEnabled,
|
||||
zoomStartTs,
|
||||
zoomEndTs,
|
||||
}: {
|
||||
zoomEnabled: boolean;
|
||||
zoomStartTs: number;
|
||||
zoomEndTs: number;
|
||||
}) => {
|
||||
const { player, store } = React.useContext(MobilePlayerContext);
|
||||
const jump = (t: number) => player.jump(t);
|
||||
const { eventList: list = [], eventListNow: listNow = [] } = store.get();
|
||||
|
||||
return (
|
||||
<EventsPanel
|
||||
list={list as EventsList}
|
||||
listNow={listNow as EventsList}
|
||||
jump={jump}
|
||||
zoomEnabled={zoomEnabled}
|
||||
zoomStartTs={zoomStartTs}
|
||||
zoomEndTs={zoomEndTs}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const MobileStackEventPanel = connect((state: Record<string, any>) => ({
|
||||
zoomEnabled: state.getIn(['components', 'player']).timelineZoom.enabled,
|
||||
zoomStartTs: state.getIn(['components', 'player']).timelineZoom.startTs,
|
||||
zoomEndTs: state.getIn(['components', 'player']).timelineZoom.endTs,
|
||||
}))(MobileStackEventPanelComp);
|
||||
|
||||
function EventsPanel({
|
||||
list,
|
||||
listNow,
|
||||
jump,
|
||||
zoomEnabled,
|
||||
zoomStartTs,
|
||||
zoomEndTs,
|
||||
}: {
|
||||
list: EventsList;
|
||||
listNow: EventsList;
|
||||
jump: (t: number) => void;
|
||||
zoomEnabled: boolean;
|
||||
zoomStartTs: number;
|
||||
zoomEndTs: number;
|
||||
}) {
|
||||
const {
|
||||
sessionStore: { devTools },
|
||||
|
|
@ -63,7 +120,14 @@ function EventsPanel({
|
|||
const activeTab = devTools[INDEX_KEY].activeTab;
|
||||
const activeIndex = devTools[INDEX_KEY].index;
|
||||
|
||||
let filteredList = useRegExListFilterMemo(list, (it) => it.name, filter);
|
||||
const inZoomRangeList = list.filter(({ time }) =>
|
||||
zoomEnabled ? zoomStartTs <= time && time <= zoomEndTs : true
|
||||
);
|
||||
const inZoomRangeListNow = listNow.filter(({ time }) =>
|
||||
zoomEnabled ? zoomStartTs <= time && time <= zoomEndTs : true
|
||||
);
|
||||
|
||||
let filteredList = useRegExListFilterMemo(inZoomRangeList, (it) => it.name, filter);
|
||||
filteredList = useTabListFilterMemo(filteredList, (it) => it.source, ALL, activeTab);
|
||||
|
||||
const onTabClick = (activeTab: (typeof TAB_KEYS)[number]) =>
|
||||
|
|
@ -71,13 +135,13 @@ function EventsPanel({
|
|||
const onFilterChange = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) =>
|
||||
devTools.update(INDEX_KEY, { filter: value });
|
||||
const tabs = useMemo(
|
||||
() => TABS.filter(({ key }) => key === ALL || list.some(({ source }) => key === source)),
|
||||
[list.length]
|
||||
() => TABS.filter(({ key }) => key === ALL || inZoomRangeList.some(({ source }) => key === source)),
|
||||
[inZoomRangeList.length]
|
||||
);
|
||||
|
||||
const [timeoutStartAutoscroll, stopAutoscroll] = useAutoscroll(
|
||||
filteredList,
|
||||
getLastItemTime(listNow),
|
||||
getLastItemTime(inZoomRangeListNow),
|
||||
activeIndex,
|
||||
(index) => devTools.update(INDEX_KEY, { index })
|
||||
);
|
||||
|
|
@ -138,15 +202,17 @@ function EventsPanel({
|
|||
};
|
||||
|
||||
return (
|
||||
<BottomBlock
|
||||
style={{ height: '100%' }}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<BottomBlock style={{ height: '100%' }} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||
<BottomBlock.Header>
|
||||
<div className="flex items-center">
|
||||
<span className="font-semibold color-gray-medium mr-4">Stack Events</span>
|
||||
<Tabs renameTab={mapNames} tabs={tabs} active={activeTab} onClick={onTabClick} border={false} />
|
||||
<Tabs
|
||||
renameTab={mapNames}
|
||||
tabs={tabs}
|
||||
active={activeTab}
|
||||
onClick={onTabClick}
|
||||
border={false}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
className="input-small h-8"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,80 @@
|
|||
import { Map } from 'immutable';
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
interface PlayerState {
|
||||
fullscreen: boolean;
|
||||
bottomBlock: number;
|
||||
hiddenHints: {
|
||||
storage?: string;
|
||||
stack?: string;
|
||||
};
|
||||
skipInterval: number;
|
||||
timelineZoom: {
|
||||
enabled: boolean;
|
||||
startTs: number;
|
||||
endTs: number;
|
||||
}
|
||||
zoomTab: 'overview' | 'journey' | 'issues' | 'errors',
|
||||
}
|
||||
|
||||
const initialState: PlayerState = {
|
||||
fullscreen: false,
|
||||
bottomBlock: 0,
|
||||
hiddenHints: {
|
||||
storage: localStorage.getItem('storageHideHint') || undefined,
|
||||
stack: localStorage.getItem('stackHideHint') || undefined,
|
||||
},
|
||||
skipInterval: parseInt(localStorage.getItem('CHANGE_SKIP_INTERVAL') || '10', 10),
|
||||
timelineZoom: {
|
||||
enabled: false,
|
||||
startTs: 0,
|
||||
endTs: 0,
|
||||
},
|
||||
zoomTab: 'overview',
|
||||
};
|
||||
|
||||
export const playerSlice = createSlice({
|
||||
name: 'player',
|
||||
initialState,
|
||||
reducers: {
|
||||
toggleFullscreen: (state, action: PayloadAction<boolean | undefined>) => {
|
||||
state.fullscreen = action.payload !== undefined ? action.payload : !state.fullscreen;
|
||||
},
|
||||
toggleBottomBlock: (state, action: PayloadAction<number>) => {
|
||||
state.bottomBlock = state.bottomBlock !== action.payload && action.payload !== 0 ? action.payload : 0;
|
||||
},
|
||||
closeBottomBlock: (state) => {
|
||||
state.bottomBlock = 0;
|
||||
},
|
||||
changeSkipInterval: (state, action: PayloadAction<number>) => {
|
||||
const skipInterval = action.payload;
|
||||
localStorage.setItem('CHANGE_SKIP_INTERVAL', skipInterval.toString());
|
||||
state.skipInterval = skipInterval;
|
||||
},
|
||||
hideHint: (state, action: PayloadAction<'storage' | 'stack'>) => {
|
||||
const name = action.payload;
|
||||
localStorage.setItem(`${name}HideHint`, 'true');
|
||||
state.hiddenHints[name] = 'true';
|
||||
state.bottomBlock = 0;
|
||||
},
|
||||
toggleZoom: (state, action: PayloadAction<ToggleZoomPayload>) => {
|
||||
const { enabled, range } = action.payload;
|
||||
state.timelineZoom = {
|
||||
enabled,
|
||||
startTs: range?.[0] || 0,
|
||||
endTs: range?.[1] || 0,
|
||||
};
|
||||
},
|
||||
setZoomTab: (state, action: PayloadAction<'overview' | 'journey' | 'issues' | 'errors'>) => {
|
||||
state.zoomTab = action.payload;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
interface ToggleZoomPayload { enabled: boolean, range?: [number, number]}
|
||||
|
||||
export const { toggleFullscreen, toggleBottomBlock, changeSkipInterval, hideHint, toggleZoom, setZoomTab, closeBottomBlock } = playerSlice.actions;
|
||||
|
||||
export default playerSlice.reducer;
|
||||
|
||||
export const NONE = 0;
|
||||
export const CONSOLE = 1;
|
||||
|
|
@ -26,7 +102,7 @@ export const blocks = {
|
|||
exceptions: EXCEPTIONS,
|
||||
inspector: INSPECTOR,
|
||||
overview: OVERVIEW,
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
export const blockValues = [
|
||||
NONE,
|
||||
|
|
@ -41,84 +117,4 @@ export const blockValues = [
|
|||
EXCEPTIONS,
|
||||
INSPECTOR,
|
||||
OVERVIEW,
|
||||
] as const
|
||||
|
||||
const TOGGLE_FULLSCREEN = 'player/TOGGLE_FS';
|
||||
const TOGGLE_BOTTOM_BLOCK = 'player/SET_BOTTOM_BLOCK';
|
||||
const HIDE_HINT = 'player/HIDE_HINT';
|
||||
const CHANGE_INTERVAL = 'player/CHANGE_SKIP_INTERVAL'
|
||||
|
||||
const initialState = Map({
|
||||
fullscreen: false,
|
||||
bottomBlock: NONE,
|
||||
hiddenHints: Map({
|
||||
storage: localStorage.getItem('storageHideHint'),
|
||||
stack: localStorage.getItem('stackHideHint')
|
||||
}),
|
||||
skipInterval: localStorage.getItem(CHANGE_INTERVAL) || 10,
|
||||
});
|
||||
|
||||
const reducer = (state = initialState, action: any = {}) => {
|
||||
switch (action.type) {
|
||||
case TOGGLE_FULLSCREEN:
|
||||
const { flag } = action
|
||||
return state.update('fullscreen', fs => typeof flag === 'boolean' ? flag : !fs);
|
||||
case TOGGLE_BOTTOM_BLOCK:
|
||||
const { bottomBlock } = action;
|
||||
if (state.get('bottomBlock') !== bottomBlock && bottomBlock !== NONE) {
|
||||
}
|
||||
return state.update('bottomBlock', bb => bb === bottomBlock ? NONE : bottomBlock);
|
||||
case CHANGE_INTERVAL:
|
||||
const { skipInterval } = action;
|
||||
localStorage.setItem(CHANGE_INTERVAL, skipInterval);
|
||||
return state.update('skipInterval', () => skipInterval);
|
||||
case HIDE_HINT:
|
||||
const { name } = action;
|
||||
localStorage.setItem(`${name}HideHint`, 'true');
|
||||
return state
|
||||
.setIn([ "hiddenHints", name ], true)
|
||||
.set('bottomBlock', NONE);
|
||||
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export default reducer;
|
||||
|
||||
export function toggleFullscreen(flag: any) {
|
||||
return {
|
||||
type: TOGGLE_FULLSCREEN,
|
||||
flag,
|
||||
};
|
||||
}
|
||||
export function fullscreenOff() {
|
||||
return toggleFullscreen(false);
|
||||
}
|
||||
export function fullscreenOn() {
|
||||
return toggleFullscreen(true);
|
||||
}
|
||||
|
||||
export function toggleBottomBlock(bottomBlock = NONE) {
|
||||
return {
|
||||
bottomBlock,
|
||||
type: TOGGLE_BOTTOM_BLOCK,
|
||||
};
|
||||
}
|
||||
|
||||
export function closeBottomBlock() {
|
||||
return toggleBottomBlock();
|
||||
}
|
||||
|
||||
export function changeSkipInterval(skipInterval: number) {
|
||||
return {
|
||||
skipInterval,
|
||||
type: CHANGE_INTERVAL,
|
||||
};
|
||||
}
|
||||
|
||||
export function hideHint(name: string) {
|
||||
return {
|
||||
name,
|
||||
type: HIDE_HINT,
|
||||
}
|
||||
}
|
||||
] as const;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { aiService } from 'App/services';
|
||||
import { makeAutoObservable } from 'mobx';
|
||||
|
||||
import { aiService } from 'App/services';
|
||||
|
||||
export default class AiSummaryStore {
|
||||
text = '';
|
||||
toggleSummary = false;
|
||||
isLoading = false;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
|
|
@ -17,7 +19,14 @@ export default class AiSummaryStore {
|
|||
this.toggleSummary = toggleSummary;
|
||||
}
|
||||
|
||||
setLoading(loading: boolean) {
|
||||
this.isLoading = loading;
|
||||
}
|
||||
|
||||
getSummary = async (sessionId: string) => {
|
||||
if (this.isLoading) return;
|
||||
|
||||
this.setLoading(true);
|
||||
this.setText('');
|
||||
try {
|
||||
const respText = await aiService.getSummary(sessionId);
|
||||
|
|
@ -26,6 +35,25 @@ export default class AiSummaryStore {
|
|||
this.setText(respText);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
this.setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
getDetailedSummary = async (sessionId: string, networkEvents: any[], feat: 'errors' | 'issues' | 'journey', startTs: number, endTs: number) => {
|
||||
if (this.isLoading) return;
|
||||
|
||||
this.setLoading(true);
|
||||
this.setText('');
|
||||
try {
|
||||
const respText = await aiService.getDetailedSummary(sessionId, networkEvents,feat, startTs, endTs);
|
||||
if (!respText) return;
|
||||
|
||||
this.setText(respText);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
this.setLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,19 +4,31 @@ export default class AiService extends BaseService {
|
|||
/**
|
||||
* @returns stream of text symbols
|
||||
* */
|
||||
async getSummary(sessionId: string): Promise<string | null> {
|
||||
const r = await this.client.post(
|
||||
`/sessions/${sessionId}/intelligent/summary`,
|
||||
);
|
||||
async getSummary(sessionId: string, start?: number, end?: number): Promise<string | null> {
|
||||
const r = await this.client.post(`/sessions/${sessionId}/intelligent/summary`, {
|
||||
frameStartTimestamp: start,
|
||||
frameEndTimestamp: end,
|
||||
});
|
||||
|
||||
return r.json()
|
||||
return r.json();
|
||||
}
|
||||
|
||||
async getDetailedSummary(sessionId: string, networkEvents: any[], feat: 'errors' | 'issues' | 'journey', start: number, end: number): Promise<string | null> {
|
||||
const r = await this.client.post(`/sessions/${sessionId}/intelligent/detailed-summary`, {
|
||||
event: feat,
|
||||
frameStartTimestamp: start,
|
||||
frameEndTimestamp: end,
|
||||
devtoolsEvents: networkEvents,
|
||||
});
|
||||
|
||||
return r.json();
|
||||
}
|
||||
|
||||
async getSearchFilters(query: string): Promise<Record<string, any>> {
|
||||
const r = await this.client.post('/intelligent/search', {
|
||||
question: query
|
||||
})
|
||||
question: query,
|
||||
});
|
||||
const { data } = await r.json();
|
||||
return data
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
"@babel/plugin-transform-private-methods": "^7.23.3",
|
||||
"@floating-ui/react-dom-interactions": "^0.10.3",
|
||||
"@medv/finder": "^3.1.0",
|
||||
"@reduxjs/toolkit": "^2.2.2",
|
||||
"@sentry/browser": "^5.21.1",
|
||||
"@svg-maps/world": "^1.0.1",
|
||||
"@svgr/webpack": "^6.2.1",
|
||||
|
|
@ -112,6 +113,7 @@
|
|||
"@storybook/manager-webpack5": "^6.5.12",
|
||||
"@storybook/react": "^6.5.12",
|
||||
"@storybook/testing-library": "^0.0.13",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@types/luxon": "^3.0.0",
|
||||
"@types/react": "^18.0.9",
|
||||
"@types/react-dom": "^18.0.4",
|
||||
|
|
|
|||
|
|
@ -198,6 +198,17 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/generator@npm:7.17.7":
|
||||
version: 7.17.7
|
||||
resolution: "@babel/generator@npm:7.17.7"
|
||||
dependencies:
|
||||
"@babel/types": ^7.17.0
|
||||
jsesc: ^2.5.1
|
||||
source-map: ^0.5.0
|
||||
checksum: 8088453c4418e0ee6528506fbd5847bbdfd56327a0025ca9496a259261e162c594ffd08be0d63e74c32feced795616772f38acc5f5e493a86a45fd439fd9feb0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/generator@npm:^7.12.11, @babel/generator@npm:^7.12.5, @babel/generator@npm:^7.23.3, @babel/generator@npm:^7.23.4, @babel/generator@npm:^7.7.2":
|
||||
version: 7.23.4
|
||||
resolution: "@babel/generator@npm:7.23.4"
|
||||
|
|
@ -210,6 +221,18 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/generator@npm:^7.23.0":
|
||||
version: 7.24.1
|
||||
resolution: "@babel/generator@npm:7.24.1"
|
||||
dependencies:
|
||||
"@babel/types": ^7.24.0
|
||||
"@jridgewell/gen-mapping": ^0.3.5
|
||||
"@jridgewell/trace-mapping": ^0.3.25
|
||||
jsesc: ^2.5.1
|
||||
checksum: f0eea7497657cdf68cfb4b7d181588e1498eefd1f303d73b0d8ca9b21a6db27136a6f5beb8f988b6bdcd4249870826080950450fd310951de42ecf36df274881
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-annotate-as-pure@npm:^7.18.6, @babel/helper-annotate-as-pure@npm:^7.22.5":
|
||||
version: 7.22.5
|
||||
resolution: "@babel/helper-annotate-as-pure@npm:7.22.5"
|
||||
|
|
@ -448,7 +471,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-validator-identifier@npm:^7.22.20":
|
||||
"@babel/helper-validator-identifier@npm:^7.16.7, @babel/helper-validator-identifier@npm:^7.22.20":
|
||||
version: 7.22.20
|
||||
resolution: "@babel/helper-validator-identifier@npm:7.22.20"
|
||||
checksum: dcad63db345fb110e032de46c3688384b0008a42a4845180ce7cd62b1a9c0507a1bed727c4d1060ed1a03ae57b4d918570259f81724aaac1a5b776056f37504e
|
||||
|
|
@ -522,6 +545,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/parser@npm:^7.20.5, @babel/parser@npm:^7.23.0":
|
||||
version: 7.24.1
|
||||
resolution: "@babel/parser@npm:7.24.1"
|
||||
bin:
|
||||
parser: ./bin/babel-parser.js
|
||||
checksum: d2a8b99aa5f33182b69d5569367403a40e7c027ae3b03a1f81fd8ac9b06ceb85b31f6ee4267fb90726dc2ac99909c6bdaa9cf16c379efab73d8dfe85cee32c50
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.23.3":
|
||||
version: 7.23.3
|
||||
resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.23.3"
|
||||
|
|
@ -1850,6 +1882,24 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/traverse@npm:7.23.2":
|
||||
version: 7.23.2
|
||||
resolution: "@babel/traverse@npm:7.23.2"
|
||||
dependencies:
|
||||
"@babel/code-frame": ^7.22.13
|
||||
"@babel/generator": ^7.23.0
|
||||
"@babel/helper-environment-visitor": ^7.22.20
|
||||
"@babel/helper-function-name": ^7.23.0
|
||||
"@babel/helper-hoist-variables": ^7.22.5
|
||||
"@babel/helper-split-export-declaration": ^7.22.6
|
||||
"@babel/parser": ^7.23.0
|
||||
"@babel/types": ^7.23.0
|
||||
debug: ^4.1.0
|
||||
globals: ^11.1.0
|
||||
checksum: d096c7c4bab9262a2f658298a3c630ae4a15a10755bb257ae91d5ab3e3b2877438934859c8d34018b7727379fe6b26c4fa2efc81cf4c462a7fe00caf79fa02ff
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/traverse@npm:^7.1.6, @babel/traverse@npm:^7.12.11, @babel/traverse@npm:^7.12.9, @babel/traverse@npm:^7.13.0, @babel/traverse@npm:^7.23.3, @babel/traverse@npm:^7.23.4":
|
||||
version: 7.23.4
|
||||
resolution: "@babel/traverse@npm:7.23.4"
|
||||
|
|
@ -1868,6 +1918,16 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/types@npm:7.17.0":
|
||||
version: 7.17.0
|
||||
resolution: "@babel/types@npm:7.17.0"
|
||||
dependencies:
|
||||
"@babel/helper-validator-identifier": ^7.16.7
|
||||
to-fast-properties: ^2.0.0
|
||||
checksum: ad09224272b40fedb00b262677d12b6838f5b5df5c47d67059ba1181bd4805439993393a8de32459dae137b536d60ebfcaf39ae84d8b3873f1e81cc75f5aeae8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.7, @babel/types@npm:^7.13.0, @babel/types@npm:^7.2.0, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.3, @babel/types@npm:^7.23.4, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3":
|
||||
version: 7.23.4
|
||||
resolution: "@babel/types@npm:7.23.4"
|
||||
|
|
@ -1879,6 +1939,17 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/types@npm:^7.17.0, @babel/types@npm:^7.24.0":
|
||||
version: 7.24.0
|
||||
resolution: "@babel/types@npm:7.24.0"
|
||||
dependencies:
|
||||
"@babel/helper-string-parser": ^7.23.4
|
||||
"@babel/helper-validator-identifier": ^7.22.20
|
||||
to-fast-properties: ^2.0.0
|
||||
checksum: 777a0bb5dbe038ca4c905fdafb1cdb6bdd10fe9d63ce13eca0bd91909363cbad554a53dc1f902004b78c1dcbc742056f877f2c99eeedff647333b1fadf51235d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@base2/pretty-print-object@npm:1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "@base2/pretty-print-object@npm:1.0.1"
|
||||
|
|
@ -2661,6 +2732,17 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jridgewell/gen-mapping@npm:^0.3.5":
|
||||
version: 0.3.5
|
||||
resolution: "@jridgewell/gen-mapping@npm:0.3.5"
|
||||
dependencies:
|
||||
"@jridgewell/set-array": ^1.2.1
|
||||
"@jridgewell/sourcemap-codec": ^1.4.10
|
||||
"@jridgewell/trace-mapping": ^0.3.24
|
||||
checksum: 1be4fd4a6b0f41337c4f5fdf4afc3bd19e39c3691924817108b82ffcb9c9e609c273f936932b9fba4b3a298ce2eb06d9bff4eb1cc3bd81c4f4ee1b4917e25feb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jridgewell/resolve-uri@npm:^3.0.3, @jridgewell/resolve-uri@npm:^3.1.0":
|
||||
version: 3.1.1
|
||||
resolution: "@jridgewell/resolve-uri@npm:3.1.1"
|
||||
|
|
@ -2675,6 +2757,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jridgewell/set-array@npm:^1.2.1":
|
||||
version: 1.2.1
|
||||
resolution: "@jridgewell/set-array@npm:1.2.1"
|
||||
checksum: 2a5aa7b4b5c3464c895c802d8ae3f3d2b92fcbe84ad12f8d0bfbb1f5ad006717e7577ee1fd2eac00c088abe486c7adb27976f45d2941ff6b0b92b2c3302c60f4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jridgewell/source-map@npm:^0.3.3":
|
||||
version: 0.3.5
|
||||
resolution: "@jridgewell/source-map@npm:0.3.5"
|
||||
|
|
@ -2712,6 +2801,16 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25":
|
||||
version: 0.3.25
|
||||
resolution: "@jridgewell/trace-mapping@npm:0.3.25"
|
||||
dependencies:
|
||||
"@jridgewell/resolve-uri": ^3.1.0
|
||||
"@jridgewell/sourcemap-codec": ^1.4.14
|
||||
checksum: 3d1ce6ebc69df9682a5a8896b414c6537e428a1d68b02fcc8363b04284a8ca0df04d0ee3013132252ab14f2527bc13bea6526a912ecb5658f0e39fd2860b4df4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@leichtgewicht/ip-codec@npm:^2.0.1":
|
||||
version: 2.0.4
|
||||
resolution: "@leichtgewicht/ip-codec@npm:2.0.4"
|
||||
|
|
@ -3087,6 +3186,26 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@reduxjs/toolkit@npm:^2.2.2":
|
||||
version: 2.2.2
|
||||
resolution: "@reduxjs/toolkit@npm:2.2.2"
|
||||
dependencies:
|
||||
immer: ^10.0.3
|
||||
redux: ^5.0.1
|
||||
redux-thunk: ^3.1.0
|
||||
reselect: ^5.0.1
|
||||
peerDependencies:
|
||||
react: ^16.9.0 || ^17.0.0 || ^18
|
||||
react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
|
||||
peerDependenciesMeta:
|
||||
react:
|
||||
optional: true
|
||||
react-redux:
|
||||
optional: true
|
||||
checksum: d749181b1bc071698517cba7ce05c42ddfe99363019249722b4dfa3afc71b3a6e4cb9885af574cf81c5d6515f68201ebfedddb5c14b262c941a45112fdc66ce3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rollup/plugin-babel@npm:^5.2.0":
|
||||
version: 5.3.1
|
||||
resolution: "@rollup/plugin-babel@npm:5.3.1"
|
||||
|
|
@ -4733,6 +4852,26 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@trivago/prettier-plugin-sort-imports@npm:^4.3.0":
|
||||
version: 4.3.0
|
||||
resolution: "@trivago/prettier-plugin-sort-imports@npm:4.3.0"
|
||||
dependencies:
|
||||
"@babel/generator": 7.17.7
|
||||
"@babel/parser": ^7.20.5
|
||||
"@babel/traverse": 7.23.2
|
||||
"@babel/types": 7.17.0
|
||||
javascript-natural-sort: 0.7.1
|
||||
lodash: ^4.17.21
|
||||
peerDependencies:
|
||||
"@vue/compiler-sfc": 3.x
|
||||
prettier: 2.x - 3.x
|
||||
peerDependenciesMeta:
|
||||
"@vue/compiler-sfc":
|
||||
optional: true
|
||||
checksum: 42270fb9c89e54a3f8b6ac8c43e6d0e03350e2857e902cdad4de22c78ef1864da600525595311bc7e94e51c16c7dd3882c2e048a162fdab59761ffa893756aa2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@trysound/sax@npm:0.2.0":
|
||||
version: 0.2.0
|
||||
resolution: "@trysound/sax@npm:0.2.0"
|
||||
|
|
@ -13568,6 +13707,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"immer@npm:^10.0.3":
|
||||
version: 10.0.4
|
||||
resolution: "immer@npm:10.0.4"
|
||||
checksum: da9da59d6e71cf3f2875024b5cfb58874baef3eefec6425483e53163e31ed0ab24bae85cd2829cb7812acb9723075eedb5946654f8bd47ecf38663388e04d3bd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"immutable@npm:^3.7.2":
|
||||
version: 3.8.2
|
||||
resolution: "immutable@npm:3.8.2"
|
||||
|
|
@ -14657,6 +14803,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"javascript-natural-sort@npm:0.7.1":
|
||||
version: 0.7.1
|
||||
resolution: "javascript-natural-sort@npm:0.7.1"
|
||||
checksum: 340f8ffc5d30fb516e06dc540e8fa9e0b93c865cf49d791fed3eac3bdc5fc71f0066fc81d44ec1433edc87caecaf9f13eec4a1fce8c5beafc709a71eaedae6fe
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jest-changed-files@npm:^29.7.0":
|
||||
version: 29.7.0
|
||||
resolution: "jest-changed-files@npm:29.7.0"
|
||||
|
|
@ -18107,6 +18260,7 @@ __metadata:
|
|||
"@jest/globals": ^29.7.0
|
||||
"@medv/finder": ^3.1.0
|
||||
"@openreplay/sourcemap-uploader": ^3.0.8
|
||||
"@reduxjs/toolkit": ^2.2.2
|
||||
"@sentry/browser": ^5.21.1
|
||||
"@storybook/addon-actions": ^6.5.12
|
||||
"@storybook/addon-docs": ^6.5.12
|
||||
|
|
@ -18119,6 +18273,7 @@ __metadata:
|
|||
"@storybook/testing-library": ^0.0.13
|
||||
"@svg-maps/world": ^1.0.1
|
||||
"@svgr/webpack": ^6.2.1
|
||||
"@trivago/prettier-plugin-sort-imports": ^4.3.0
|
||||
"@types/luxon": ^3.0.0
|
||||
"@types/react": ^18.0.9
|
||||
"@types/react-dom": ^18.0.4
|
||||
|
|
@ -21415,6 +21570,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"redux-thunk@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "redux-thunk@npm:3.1.0"
|
||||
peerDependencies:
|
||||
redux: ^5.0.0
|
||||
checksum: 21557f6a30e1b2e3e470933247e51749be7f1d5a9620069a3125778675ce4d178d84bdee3e2a0903427a5c429e3aeec6d4df57897faf93eb83455bc1ef7b66fd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"redux@npm:^4.0.0, redux@npm:^4.0.5, redux@npm:^4.1.2, redux@npm:^4.2.0":
|
||||
version: 4.2.1
|
||||
resolution: "redux@npm:4.2.1"
|
||||
|
|
@ -21424,6 +21588,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"redux@npm:^5.0.1":
|
||||
version: 5.0.1
|
||||
resolution: "redux@npm:5.0.1"
|
||||
checksum: b10c28357194f38e7d53b760ed5e64faa317cc63de1fb95bc5d9e127fab956392344368c357b8e7a9bedb0c35b111e7efa522210cfdc3b3c75e5074718e9069c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"reflect.getprototypeof@npm:^1.0.4":
|
||||
version: 1.0.4
|
||||
resolution: "reflect.getprototypeof@npm:1.0.4"
|
||||
|
|
@ -21757,6 +21928,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"reselect@npm:^5.0.1":
|
||||
version: 5.1.0
|
||||
resolution: "reselect@npm:5.1.0"
|
||||
checksum: b0ed789f4f6f10dfbd23741823726793384932969aa7ce8f584c882ad87620a02b09b5d1146cd2ea6eaa0953b3fd9f7df22f113893af73f35f28432a8a4294de
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"resize-observer-polyfill@npm:^1.5.1":
|
||||
version: 1.5.1
|
||||
resolution: "resize-observer-polyfill@npm:1.5.1"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue