From 7a3ef9bc2179f30b6c8b3a83c7aad2f1b0a059ae Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Tue, 15 Nov 2022 21:01:40 +0100 Subject: [PATCH 001/252] trfactoring(frontend/player):phase 1 of componnts decomposition; use store per instance --- .../Session_/Player/Controls/Timeline.js | 2 +- .../app/player/MessageDistributor/Lists.ts | 23 - .../StatedScreen/StatedScreen.ts | 152 ------ .../MessageDistributor/StatedScreen/index.ts | 2 - .../app/player/MessageDistributor/index.js | 2 - frontend/app/player/Player.ts | 281 ----------- .../managers => _common}/ListWalker.ts | 27 +- .../app/player/_common/ListWalkerWithMarks.ts | 31 ++ frontend/app/player/_common/SimpleStore.ts | 15 + .../player/{singletone.js => _singletone.ts} | 109 ++--- .../app/player/{store => _store}/connector.js | 0 frontend/app/player/{store => _store}/duck.js | 10 +- .../app/player/{store => _store}/index.js | 0 .../app/player/{store => _store}/selectors.js | 0 .../app/player/{store => _store}/store.js | 0 frontend/app/player/_web/Lists.ts | 71 +++ .../MessageManager.ts} | 149 +++--- .../Screen/BaseScreen.ts | 16 +- .../StatedScreen => _web}/Screen/Cursor.ts | 0 .../StatedScreen => _web}/Screen/Inspector.js | 0 .../Marker.js => _web/Screen/Marker.ts} | 66 +-- .../StatedScreen => _web}/Screen/Screen.ts | 11 +- .../Screen/cursor.module.css | 0 .../StatedScreen => _web}/Screen/index.js | 0 .../Screen/marker.module.css | 0 .../Screen/screen.module.css | 0 .../StatedScreen => _web}/Screen/types.ts | 0 frontend/app/player/_web/WebPlayer.ts | 178 +++++++ .../assist}/AnnotationCanvas.ts | 0 .../managers => _web/assist}/AssistManager.ts | 78 +-- .../managers => _web/assist}/LocalStream.ts | 0 .../managers/ActivityManager.ts | 8 +- .../managers/DOM/DOMManager.ts | 6 +- .../managers/DOM/FocusManager.ts | 2 +- .../managers/DOM/StylesManager.ts | 6 +- .../managers/DOM/VirtualDOM.ts | 0 .../managers/DOM/safeCSSRules.ts | 0 .../managers/MouseMoveManager.ts | 6 +- .../managers/PagesManager.ts | 8 +- .../managers/PerformanceTrackManager.ts | 2 +- .../managers/ReduxStateManager.ts | 2 +- .../managers/WindowNodeCounter.ts | 0 .../messages/JSONRawMessageReader.ts | 0 .../messages/MFileReader.ts | 0 .../messages/MStreamReader.ts | 0 .../messages/PrimitiveReader.ts | 0 .../messages/RawMessageReader.ts | 0 .../messages/index.ts | 0 .../messages/message.ts | 0 .../messages/raw.ts | 0 .../messages/timed.ts | 0 .../messages/tracker-legacy.ts | 0 .../messages/tracker.ts | 0 .../messages/urlResolve.ts | 0 .../network/crypto.ts | 0 .../network/loadFiles.ts | 0 frontend/app/player/create.ts | 25 + frontend/app/player/index.js | 10 +- frontend/app/player/ios/ImagePlayer.js | 454 ------------------ frontend/app/player/ios/Parser.ts | 34 -- frontend/app/player/ios/PerformanceList.js | 73 --- frontend/app/player/ios/ScreenList.ts | 57 --- frontend/app/player/ios/lists.js | 12 - frontend/app/player/ios/state.js | 112 ----- frontend/app/player/lists/ListReader.js | 124 ----- .../app/player/lists/ListReaderWithRed.js | 48 -- frontend/app/player/lists/index.js | 68 --- frontend/app/player/player/Animator.ts | 172 +++++++ frontend/app/player/player/Player.ts | 125 +++++ frontend/app/player/player/_LSCache.ts | 63 +++ frontend/app/player/player/localStorage.ts | 19 + frontend/app/player/player/types.ts | 21 + 72 files changed, 990 insertions(+), 1690 deletions(-) delete mode 100644 frontend/app/player/MessageDistributor/Lists.ts delete mode 100644 frontend/app/player/MessageDistributor/StatedScreen/StatedScreen.ts delete mode 100644 frontend/app/player/MessageDistributor/StatedScreen/index.ts delete mode 100644 frontend/app/player/MessageDistributor/index.js delete mode 100644 frontend/app/player/Player.ts rename frontend/app/player/{MessageDistributor/managers => _common}/ListWalker.ts (86%) create mode 100644 frontend/app/player/_common/ListWalkerWithMarks.ts create mode 100644 frontend/app/player/_common/SimpleStore.ts rename frontend/app/player/{singletone.js => _singletone.ts} (69%) rename frontend/app/player/{store => _store}/connector.js (100%) rename frontend/app/player/{store => _store}/duck.js (68%) rename frontend/app/player/{store => _store}/index.js (100%) rename frontend/app/player/{store => _store}/selectors.js (100%) rename frontend/app/player/{store => _store}/store.js (100%) create mode 100644 frontend/app/player/_web/Lists.ts rename frontend/app/player/{MessageDistributor/MessageDistributor.ts => _web/MessageManager.ts} (86%) rename frontend/app/player/{MessageDistributor/StatedScreen => _web}/Screen/BaseScreen.ts (94%) rename frontend/app/player/{MessageDistributor/StatedScreen => _web}/Screen/Cursor.ts (100%) rename frontend/app/player/{MessageDistributor/StatedScreen => _web}/Screen/Inspector.js (100%) rename frontend/app/player/{MessageDistributor/StatedScreen/Screen/Marker.js => _web/Screen/Marker.ts} (66%) rename frontend/app/player/{MessageDistributor/StatedScreen => _web}/Screen/Screen.ts (91%) rename frontend/app/player/{MessageDistributor/StatedScreen => _web}/Screen/cursor.module.css (100%) rename frontend/app/player/{MessageDistributor/StatedScreen => _web}/Screen/index.js (100%) rename frontend/app/player/{MessageDistributor/StatedScreen => _web}/Screen/marker.module.css (100%) rename frontend/app/player/{MessageDistributor/StatedScreen => _web}/Screen/screen.module.css (100%) rename frontend/app/player/{MessageDistributor/StatedScreen => _web}/Screen/types.ts (100%) create mode 100644 frontend/app/player/_web/WebPlayer.ts rename frontend/app/player/{MessageDistributor/managers => _web/assist}/AnnotationCanvas.ts (100%) rename frontend/app/player/{MessageDistributor/managers => _web/assist}/AssistManager.ts (87%) rename frontend/app/player/{MessageDistributor/managers => _web/assist}/LocalStream.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/managers/ActivityManager.ts (83%) rename frontend/app/player/{MessageDistributor => _web}/managers/DOM/DOMManager.ts (99%) rename frontend/app/player/{MessageDistributor => _web}/managers/DOM/FocusManager.ts (93%) rename frontend/app/player/{MessageDistributor => _web}/managers/DOM/StylesManager.ts (95%) rename frontend/app/player/{MessageDistributor => _web}/managers/DOM/VirtualDOM.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/managers/DOM/safeCSSRules.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/managers/MouseMoveManager.ts (89%) rename frontend/app/player/{MessageDistributor => _web}/managers/PagesManager.ts (85%) rename frontend/app/player/{MessageDistributor => _web}/managers/PerformanceTrackManager.ts (98%) rename frontend/app/player/{MessageDistributor => _web}/managers/ReduxStateManager.ts (96%) rename frontend/app/player/{MessageDistributor => _web}/managers/WindowNodeCounter.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/messages/JSONRawMessageReader.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/messages/MFileReader.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/messages/MStreamReader.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/messages/PrimitiveReader.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/messages/RawMessageReader.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/messages/index.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/messages/message.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/messages/raw.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/messages/timed.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/messages/tracker-legacy.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/messages/tracker.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/messages/urlResolve.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/network/crypto.ts (100%) rename frontend/app/player/{MessageDistributor => _web}/network/loadFiles.ts (100%) create mode 100644 frontend/app/player/create.ts delete mode 100644 frontend/app/player/ios/ImagePlayer.js delete mode 100644 frontend/app/player/ios/Parser.ts delete mode 100644 frontend/app/player/ios/PerformanceList.js delete mode 100644 frontend/app/player/ios/ScreenList.ts delete mode 100644 frontend/app/player/ios/lists.js delete mode 100644 frontend/app/player/ios/state.js delete mode 100644 frontend/app/player/lists/ListReader.js delete mode 100644 frontend/app/player/lists/ListReaderWithRed.js delete mode 100644 frontend/app/player/lists/index.js create mode 100644 frontend/app/player/player/Animator.ts create mode 100644 frontend/app/player/player/Player.ts create mode 100644 frontend/app/player/player/_LSCache.ts create mode 100644 frontend/app/player/player/localStorage.ts create mode 100644 frontend/app/player/player/types.ts diff --git a/frontend/app/components/Session_/Player/Controls/Timeline.js b/frontend/app/components/Session_/Player/Controls/Timeline.js index 3ff810d57..e3ce9788a 100644 --- a/frontend/app/components/Session_/Player/Controls/Timeline.js +++ b/frontend/app/components/Session_/Player/Controls/Timeline.js @@ -30,7 +30,7 @@ let debounceTooltipChange = () => null; disabled: state.cssLoading || state.messagesLoading || state.markedTargets, endTime: state.endTime, live: state.live, - notes: state.notes, + notes: state.notes || [], // TODO: implement notes without interaction with Player state })) @connect( (state) => ({ diff --git a/frontend/app/player/MessageDistributor/Lists.ts b/frontend/app/player/MessageDistributor/Lists.ts deleted file mode 100644 index cb7e4d192..000000000 --- a/frontend/app/player/MessageDistributor/Lists.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { Message } from './messages' -import ListWalker from './managers/ListWalker'; - -export const LIST_NAMES = ["redux", "mobx", "vuex", "zustand", "ngrx", "graphql", "exceptions", "profiles"] as const; - -export const INITIAL_STATE = {} -LIST_NAMES.forEach(name => { - INITIAL_STATE[`${name}ListNow`] = [] - INITIAL_STATE[`${name}List`] = [] -}) - - -type ListsObject = { - [key in typeof LIST_NAMES[number]]: ListWalker -} - -export function initLists(): ListsObject { - const lists: Partial = {}; - for (var i = 0; i < LIST_NAMES.length; i++) { - lists[LIST_NAMES[i]] = new ListWalker(); - } - return lists as ListsObject; -} diff --git a/frontend/app/player/MessageDistributor/StatedScreen/StatedScreen.ts b/frontend/app/player/MessageDistributor/StatedScreen/StatedScreen.ts deleted file mode 100644 index 45028f88f..000000000 --- a/frontend/app/player/MessageDistributor/StatedScreen/StatedScreen.ts +++ /dev/null @@ -1,152 +0,0 @@ -import Screen, { INITIAL_STATE as SUPER_INITIAL_STATE, State as SuperState } from './Screen/Screen'; -import { update, getState } from '../../store'; - -import type { Point } from './Screen/types'; - -function getOffset(el: Element, innerWindow: Window) { - const rect = el.getBoundingClientRect(); - return { - fixedLeft: rect.left + innerWindow.scrollX, - fixedTop: rect.top + innerWindow.scrollY, - rect, - }; -} - -//export interface targetPosition - -interface BoundingRect { - top: number, - left: number, - width: number, - height: number, -} - -export interface MarkedTarget { - boundingRect: BoundingRect, - el: Element, - selector: string, - count: number, - index: number, - active?: boolean, - percent: number -} - -export interface State extends SuperState { - messagesLoading: boolean, - cssLoading: boolean, - markedTargets: MarkedTarget[] | null, - activeTargetIndex: number, -} - -export const INITIAL_STATE: State = { - ...SUPER_INITIAL_STATE, - messagesLoading: false, - cssLoading: false, - markedTargets: null, - activeTargetIndex: 0 -}; - -export default class StatedScreen extends Screen { - constructor() { super(); } - - setMessagesLoading(messagesLoading: boolean) { - this.display(!messagesLoading); - update({ messagesLoading }); - } - - setCSSLoading(cssLoading: boolean) { - this.displayFrame(!cssLoading); - update({ cssLoading }); - } - - setSize({ height, width }: { height: number, width: number }) { - update({ width, height }); - this.scale(); - this.updateMarketTargets() - } - - updateMarketTargets() { - const { markedTargets } = getState(); - if (markedTargets) { - update({ - markedTargets: markedTargets.map((mt: any) => ({ - ...mt, - boundingRect: this.calculateRelativeBoundingRect(mt.el), - })), - }); - } - } - - private calculateRelativeBoundingRect(el: Element): BoundingRect { - if (!this.parentElement) return {top:0, left:0, width:0,height:0} //TODO - const { top, left, width, height } = el.getBoundingClientRect(); - const s = this.getScale(); - const scrinRect = this.screen.getBoundingClientRect(); - const parentRect = this.parentElement.getBoundingClientRect(); - - return { - top: top*s + scrinRect.top - parentRect.top, - left: left*s + scrinRect.left - parentRect.left, - width: width*s, - height: height*s, - } - } - - setActiveTarget(index: number) { - const window = this.window - const markedTargets: MarkedTarget[] | null = getState().markedTargets - const target = markedTargets && markedTargets[index] - if (target && window) { - const { fixedTop, rect } = getOffset(target.el, window) - const scrollToY = fixedTop - window.innerHeight / 1.5 - if (rect.top < 0 || rect.top > window.innerHeight) { - // behavior hack TODO: fix it somehow when they will decide to remove it from browser api - // @ts-ignore - window.scrollTo({ top: scrollToY, behavior: 'instant' }) - setTimeout(() => { - if (!markedTargets) { return } - update({ - markedTargets: markedTargets.map(t => t === target ? { - ...target, - boundingRect: this.calculateRelativeBoundingRect(target.el), - } : t) - }) - }, 0) - } - - } - update({ activeTargetIndex: index }); - } - - private actualScroll: Point | null = null - setMarkedTargets(selections: { selector: string, count: number }[] | null) { - if (selections) { - const totalCount = selections.reduce((a, b) => { - return a + b.count - }, 0); - const markedTargets: MarkedTarget[] = []; - let index = 0; - selections.forEach((s) => { - const el = this.getElementBySelector(s.selector); - if (!el) return; - markedTargets.push({ - ...s, - el, - index: index++, - percent: Math.round((s.count * 100) / totalCount), - boundingRect: this.calculateRelativeBoundingRect(el), - count: s.count, - }) - }); - - this.actualScroll = this.getCurrentScroll() - update({ markedTargets }); - } else { - if (this.actualScroll) { - this.window?.scrollTo(this.actualScroll.x, this.actualScroll.y) - this.actualScroll = null - } - update({ markedTargets: null }); - } - } -} diff --git a/frontend/app/player/MessageDistributor/StatedScreen/index.ts b/frontend/app/player/MessageDistributor/StatedScreen/index.ts deleted file mode 100644 index 0955acffb..000000000 --- a/frontend/app/player/MessageDistributor/StatedScreen/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './StatedScreen'; -export * from './StatedScreen'; \ No newline at end of file diff --git a/frontend/app/player/MessageDistributor/index.js b/frontend/app/player/MessageDistributor/index.js deleted file mode 100644 index 8502aee50..000000000 --- a/frontend/app/player/MessageDistributor/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './MessageDistributor'; -export * from './MessageDistributor'; diff --git a/frontend/app/player/Player.ts b/frontend/app/player/Player.ts deleted file mode 100644 index 320b141ec..000000000 --- a/frontend/app/player/Player.ts +++ /dev/null @@ -1,281 +0,0 @@ -import { goTo as listsGoTo } from './lists'; -import { update, getState } from './store'; -import MessageDistributor, { INITIAL_STATE as SUPER_INITIAL_STATE } from './MessageDistributor/MessageDistributor'; -import { Note } from 'App/services/NotesService'; - -const fps = 60; -const performance = window.performance || { now: Date.now.bind(Date) }; -const requestAnimationFrame = - window.requestAnimationFrame || - // @ts-ignore - window.webkitRequestAnimationFrame || - // @ts-ignore - window.mozRequestAnimationFrame || - // @ts-ignore - window.oRequestAnimationFrame || - // @ts-ignore - window.msRequestAnimationFrame || - ((callback: (args: any) => void) => window.setTimeout(() => { callback(performance.now()); }, 1000 / fps)); -const cancelAnimationFrame = - window.cancelAnimationFrame || - // @ts-ignore - window.mozCancelAnimationFrame || - window.clearTimeout; - -const HIGHEST_SPEED = 16; - - -const SPEED_STORAGE_KEY = "__$player-speed$__"; -const SKIP_STORAGE_KEY = "__$player-skip$__"; -const SKIP_TO_ISSUE_STORAGE_KEY = "__$session-skipToIssue$__"; -const AUTOPLAY_STORAGE_KEY = "__$player-autoplay$__"; -const SHOW_EVENTS_STORAGE_KEY = "__$player-show-events$__"; -const storedSpeed: number = parseInt(localStorage.getItem(SPEED_STORAGE_KEY) || "") ; -const initialSpeed = [1,2,4,8,16].includes(storedSpeed) ? storedSpeed : 1; -const initialSkip = localStorage.getItem(SKIP_STORAGE_KEY) === 'true'; -const initialSkipToIssue = localStorage.getItem(SKIP_TO_ISSUE_STORAGE_KEY) === 'true'; -const initialAutoplay = localStorage.getItem(AUTOPLAY_STORAGE_KEY) === 'true'; -const initialShowEvents = localStorage.getItem(SHOW_EVENTS_STORAGE_KEY) === 'true'; - -export const INITIAL_STATE = { - ...SUPER_INITIAL_STATE, - time: 0, - playing: false, - completed: false, - endTime: 0, - inspectorMode: false, - live: false, - livePlay: false, - liveTimeTravel: false, - notes: [], -} as const; - - -export const INITIAL_NON_RESETABLE_STATE = { - skip: initialSkip, - skipToIssue: initialSkipToIssue, - autoplay: initialAutoplay, - speed: initialSpeed, - showEvents: initialShowEvents, -} - -export default class Player extends MessageDistributor { - private _animationFrameRequestId: number = 0; - - private _setTime(time: number, index?: number) { - update({ - time, - completed: false, - }); - super.move(time, index); - listsGoTo(time, index); - } - - private _startAnimation() { - let prevTime = getState().time; - let animationPrevTime = performance.now(); - - const nextFrame = (animationCurrentTime: number) => { - const { - speed, - skip, - autoplay, - skipIntervals, - endTime, - live, - livePlay, - disconnected, - messagesLoading, - cssLoading, - } = getState(); - - const diffTime = messagesLoading || cssLoading || disconnected - ? 0 - : Math.max(animationCurrentTime - animationPrevTime, 0) * (live ? 1 : speed); - - let time = prevTime + diffTime; - - const skipInterval = !live && skip && skipIntervals.find((si: Node) => si.contains(time)); // TODO: good skip by messages - if (skipInterval) time = skipInterval.end; - - const fmt = super.getFirstMessageTime(); - if (time < fmt) time = fmt; // ? - - const lmt = super.getLastMessageTime(); - if (livePlay && time < lmt) time = lmt; - if (endTime < lmt) { - update({ - endTime: lmt, - }); - } - - prevTime = time; - animationPrevTime = animationCurrentTime; - - const completed = !live && time >= endTime; - if (completed) { - this._setTime(endTime); - return update({ - playing: false, - completed: true, - }); - } - - // throttle store updates - // TODO: make it possible to change frame rate - if (live && time - endTime > 100) { - update({ - endTime: time, - livePlay: endTime - time < 900 - }); - } - this._setTime(time); - this._animationFrameRequestId = requestAnimationFrame(nextFrame); - }; - this._animationFrameRequestId = requestAnimationFrame(nextFrame); - } - - play() { - cancelAnimationFrame(this._animationFrameRequestId); - update({ playing: true }); - this._startAnimation(); - } - - pause() { - cancelAnimationFrame(this._animationFrameRequestId); - update({ playing: false }) - } - - togglePlay() { - const { playing, completed } = getState(); - if (playing) { - this.pause(); - } else if (completed) { - this._setTime(0); - this.play(); - } else { - this.play(); - } - } - - jump(setTime: number, index: number) { - const { live, liveTimeTravel, endTime } = getState(); - if (live && !liveTimeTravel) return; - const time = setTime ? setTime : getState().time - if (getState().playing) { - cancelAnimationFrame(this._animationFrameRequestId); - // this._animationFrameRequestId = requestAnimationFrame(() => { - this._setTime(time, index); - this._startAnimation(); - // throttilg the redux state update from each frame to nearly half a second - // which is better for performance and component rerenders - update({ livePlay: Math.abs(time - endTime) < 500 }); - //}); - } else { - //this._animationFrameRequestId = requestAnimationFrame(() => { - this._setTime(time, index); - update({ livePlay: Math.abs(time - endTime) < 500 }); - //}); - } - } - - toggleSkip() { - const skip = !getState().skip; - localStorage.setItem(SKIP_STORAGE_KEY, `${skip}`); - update({ skip }); - } - - toggleInspectorMode(flag: boolean, clickCallback?: (args: any) => void) { - if (typeof flag !== 'boolean') { - const { inspectorMode } = getState(); - flag = !inspectorMode; - } - - if (flag) { - this.pause(); - update({ inspectorMode: true }); - return super.enableInspector(clickCallback); - } else { - super.disableInspector(); - update({ inspectorMode: false }); - } - } - - markTargets(targets: { selector: string, count: number }[] | null) { - this.pause(); - this.setMarkedTargets(targets); - } - - activeTarget(index: number) { - this.setActiveTarget(index); - } - - toggleSkipToIssue() { - const skipToIssue = !getState().skipToIssue; - localStorage.setItem(SKIP_TO_ISSUE_STORAGE_KEY, `${skipToIssue}`); - update({ skipToIssue }); - } - - toggleAutoplay() { - const autoplay = !getState().autoplay; - localStorage.setItem(AUTOPLAY_STORAGE_KEY, `${autoplay}`); - update({ autoplay }); - } - - toggleEvents(shouldShow?: boolean) { - const showEvents = shouldShow || !getState().showEvents; - localStorage.setItem(SHOW_EVENTS_STORAGE_KEY, `${showEvents}`); - update({ showEvents }); - } - - _updateSpeed(speed: number) { - localStorage.setItem(SPEED_STORAGE_KEY, `${speed}`); - update({ speed }); - } - - toggleSpeed() { - const { speed } = getState(); - this._updateSpeed(speed < HIGHEST_SPEED ? speed * 2 : 1); - } - - speedUp() { - const { speed } = getState(); - this._updateSpeed(Math.min(HIGHEST_SPEED, speed * 2)); - } - - speedDown() { - const { speed } = getState(); - this._updateSpeed(Math.max(1, speed/2)); - } - - async toggleTimetravel() { - if (!getState().liveTimeTravel) { - return await this.reloadWithUnprocessedFile() - } - } - - jumpToLive() { - cancelAnimationFrame(this._animationFrameRequestId); - this._setTime(getState().endTime); - this._startAnimation(); - update({ livePlay: true }); - } - - toggleUserName(name?: string) { - this.cursor.toggleUserName(name) - } - - injectNotes(notes: Note[]) { - update({ notes }) - } - - filterOutNote(noteId: number) { - const { notes } = getState() - update({ notes: notes.filter((note: Note) => note.noteId !== noteId) }) - } - - clean() { - this.pause(); - super.clean(); - } -} diff --git a/frontend/app/player/MessageDistributor/managers/ListWalker.ts b/frontend/app/player/_common/ListWalker.ts similarity index 86% rename from frontend/app/player/MessageDistributor/managers/ListWalker.ts rename to frontend/app/player/_common/ListWalker.ts index e04c5bb83..92f7585c8 100644 --- a/frontend/app/player/MessageDistributor/managers/ListWalker.ts +++ b/frontend/app/player/_common/ListWalker.ts @@ -1,4 +1,4 @@ -import type { Timed } from '../messages/timed'; +import type { Timed } from './messages/timed'; export default class ListWalker { private p = 0 @@ -79,6 +79,23 @@ export default class ListWalker { return this.p; } + private hasNext() { + return this.p < this.length + } + private hasPrev() { + return this.p > 0 + } + protected moveNext(): T | null { + return this.hasNext() + ? this.list[ this.p++ ] + : null + } + protected movePrev(): T | null { + return this.hasPrev() + ? this.list[ --this.p ] + : null + } + /* Returns last message with the time <= t. Assumed that the current message is already handled so @@ -94,11 +111,11 @@ export default class ListWalker { let changed = false; while (this.p < this.length && this.list[this.p][key] <= val) { - this.p++; + this.moveNext() changed = true; } while (this.p > 0 && this.list[ this.p - 1 ][key] > val) { - this.p--; + this.movePrev() changed = true; } return changed ? this.list[ this.p - 1 ] : null; @@ -112,10 +129,10 @@ export default class ListWalker { const list = this.list while (list[this.p] && list[this.p].time <= t) { - fn(list[ this.p++ ]); + fn(this.moveNext()) } while (fnBack && this.p > 0 && list[ this.p - 1 ].time > t) { - fnBack(list[ --this.p ]); + fnBack(this.movePrev()); } } diff --git a/frontend/app/player/_common/ListWalkerWithMarks.ts b/frontend/app/player/_common/ListWalkerWithMarks.ts new file mode 100644 index 000000000..b2a8f5d3d --- /dev/null +++ b/frontend/app/player/_common/ListWalkerWithMarks.ts @@ -0,0 +1,31 @@ +import type { Timed } from './messages/timed'; +import ListWalker from './ListWalker' + + +type CheckFn = (t: T) => boolean + + +export default class ListWalkerWithMarks extends ListWalker { + private _markCountNow: number = 0 + constructor(private isMarked: CheckFn, initialList?: T[]) { + super(initialList) + } + protected moveNext() { + const val = super.moveNext() + if (val && this.isMarked(val)) { + this._markCountNow++ + } + return val + } + protected movePrev() { + const val = super.movePrev() + if (val && this.isMarked(val)) { + this._markCountNow-- + } + return val + } + get markCountNow(): number { + return this._markCountNow + } + +} \ No newline at end of file diff --git a/frontend/app/player/_common/SimpleStore.ts b/frontend/app/player/_common/SimpleStore.ts new file mode 100644 index 000000000..9e81a79cd --- /dev/null +++ b/frontend/app/player/_common/SimpleStore.ts @@ -0,0 +1,15 @@ + +import { State } from './types' + +// (not a type) +export default class SimpleSore implements State { + constructor(private state: G){} + get(): G { + return this.state + } + update(newState: Partial) { + Object.assign(this.state, newState) + } +} + + diff --git a/frontend/app/player/singletone.js b/frontend/app/player/_singletone.ts similarity index 69% rename from frontend/app/player/singletone.js rename to frontend/app/player/_singletone.ts index feb82ec78..71f29a7ee 100644 --- a/frontend/app/player/singletone.js +++ b/frontend/app/player/_singletone.ts @@ -1,11 +1,40 @@ -import Player from './Player'; -import { update, cleanStore, getState } from './store'; -import { clean as cleanLists } from './lists'; +import WebPlayer from './_web/WebPlayer'; +import reduxStore, {update, cleanStore} from './_store'; -/** @type {Player} */ -let instance = null; +import { State as MMState, INITIAL_STATE as MM_INITIAL_STATE } from './_web/MessageManager' +import { State as PState, INITIAL_STATE as PLAYER_INITIAL_STATE } from './player/Player' +import { Store } from './player/types' -const initCheck = method => (...args) => { + +const INIT_STATE = { + ...MM_INITIAL_STATE, + ...PLAYER_INITIAL_STATE, +} + + +const myStore: Store = { + get() { + return reduxStore.getState() + }, + update(s) { + update(s) + } +} + +let instance: WebPlayer | null = null; + +export function init(session, config, live = false) { + instance = new WebPlayer(myStore, session, config, live); +} + +export function clean() { + if (instance === null) return; + instance.clean(); + cleanStore() + instance = null; +} + +const initCheck = (method) => (...args) => { if (instance === null) { console.error("Player method called before Player have been initialized."); return; @@ -13,47 +42,7 @@ const initCheck = method => (...args) => { return method(...args); } - -let autoPlay = true; -document.addEventListener("visibilitychange", function() { - if (instance === null) return; - if (document.hidden) { - const { playing } = getState(); - autoPlay = playing - if (playing) { - instance.pause(); - } - } else if (autoPlay) { - instance.play(); - } -}); - -export function init(session, config, live = false) { - const endTime = !live && session.duration.valueOf(); - - instance = new Player(session, config, live); - update({ - initialized: true, - live, - livePlay: live, - endTime, // : 0, //TODO: through initialState - session, - }); - - if (!document.hidden) { - instance.play(); - } -} - -export function clean() { - if (instance === null) return; - instance.clean(); - cleanStore(); - cleanLists(); - instance = null; -} export const jump = initCheck((...args) => instance.jump(...args)); - export const togglePlay = initCheck((...args) => instance.togglePlay(...args)); export const pause = initCheck((...args) => instance.pause(...args)); export const toggleSkip = initCheck((...args) => instance.toggleSkip(...args)); @@ -64,9 +53,22 @@ export const toggleEvents = initCheck((...args) => instance.toggleEvents(...args export const speedUp = initCheck((...args) => instance.speedUp(...args)); export const speedDown = initCheck((...args) => instance.speedDown(...args)); export const attach = initCheck((...args) => instance.attach(...args)); -export const markElement = initCheck((...args) => instance.marker && instance.marker.mark(...args)); +export const markElement = initCheck((...args) => instance.mark(...args)); export const scale = initCheck(() => instance.scale()); +/** @type {WebPlayer.toggleTimetravel} */ +export const toggleTimetravel = initCheck((...args) => instance.toggleTimetravel(...args)) export const toggleInspectorMode = initCheck((...args) => instance.toggleInspectorMode(...args)); +export const markTargets = initCheck((...args) => instance.markTargets(...args)) +export const activeTarget =initCheck((...args) => instance.setActiveTarget(...args)) + +export const jumpToLive = initCheck((...args) => instance.jumpToLive(...args)) +export const toggleUserName = initCheck((...args) => instance.toggleUserName(...args)) + +// !not related to player, but rather to the OR platform. +export const injectNotes = () => {} // initCheck((...args) => instance.injectNotes(...args)) +export const filterOutNote = () => {} //initCheck((...args) => instance.filterOutNote(...args)) + + /** @type {Player.assistManager.call} */ export const callPeer = initCheck((...args) => instance.assistManager.call(...args)) /** @type {Player.assistManager.setCallArgs} */ @@ -75,17 +77,10 @@ export const setCallArgs = initCheck((...args) => instance.assistManager.setCall export const initiateCallEnd = initCheck((...args) => instance.assistManager.initiateCallEnd(...args)) export const requestReleaseRemoteControl = initCheck((...args) => instance.assistManager.requestReleaseRemoteControl(...args)) export const releaseRemoteControl = initCheck((...args) => instance.assistManager.releaseRemoteControl(...args)) -export const markTargets = initCheck((...args) => instance.markTargets(...args)) -export const activeTarget = initCheck((...args) => instance.activeTarget(...args)) -export const toggleAnnotation = initCheck((...args) => instance.assistManager.toggleAnnotation(...args)) -/** @type {Player.toggleTimetravel} */ -export const toggleTimetravel = initCheck((...args) => instance.toggleTimetravel(...args)) -export const jumpToLive = initCheck((...args) => instance.jumpToLive(...args)) -export const toggleUserName = initCheck((...args) => instance.toggleUserName(...args)) -export const injectNotes = initCheck((...args) => instance.injectNotes(...args)) -export const filterOutNote = initCheck((...args) => instance.filterOutNote(...args)) /** @type {Player.assistManager.toggleVideoLocalStream} */ export const toggleVideoLocalStream = initCheck((...args) => instance.assistManager.toggleVideoLocalStream(...args)) +export const toggleAnnotation = initCheck((...args) => instance.assistManager.toggleAnnotation(...args)) + export const Controls = { jump, @@ -99,4 +94,4 @@ export const Controls = { speedUp, speedDown, callPeer -} +} \ No newline at end of file diff --git a/frontend/app/player/store/connector.js b/frontend/app/player/_store/connector.js similarity index 100% rename from frontend/app/player/store/connector.js rename to frontend/app/player/_store/connector.js diff --git a/frontend/app/player/store/duck.js b/frontend/app/player/_store/duck.js similarity index 68% rename from frontend/app/player/store/duck.js rename to frontend/app/player/_store/duck.js index faf77041c..bcede3b32 100644 --- a/frontend/app/player/store/duck.js +++ b/frontend/app/player/_store/duck.js @@ -1,20 +1,20 @@ import { applyChange, revertChange } from 'deep-diff'; -import { INITIAL_STATE as listsInitialState } from '../lists'; -import { INITIAL_STATE as playerInitialState, INITIAL_NON_RESETABLE_STATE as playerInitialNonResetableState } from '../Player'; + +import { INITIAL_STATE as MM_INITIAL_STATE } from '../_web/MessageManager' +import { INITIAL_STATE as PLAYER_INITIAL_STATE } from '../player/Player' const UPDATE = 'player/UPDATE'; const CLEAN = 'player/CLEAN'; const REDUX = 'player/REDUX'; const resetState = { - ...listsInitialState, - ...playerInitialState, + ...MM_INITIAL_STATE, + ...PLAYER_INITIAL_STATE, initialized: false, }; const initialState = { ...resetState, - ...playerInitialNonResetableState, } export default (state = initialState, action = {}) => { diff --git a/frontend/app/player/store/index.js b/frontend/app/player/_store/index.js similarity index 100% rename from frontend/app/player/store/index.js rename to frontend/app/player/_store/index.js diff --git a/frontend/app/player/store/selectors.js b/frontend/app/player/_store/selectors.js similarity index 100% rename from frontend/app/player/store/selectors.js rename to frontend/app/player/_store/selectors.js diff --git a/frontend/app/player/store/store.js b/frontend/app/player/_store/store.js similarity index 100% rename from frontend/app/player/store/store.js rename to frontend/app/player/_store/store.js diff --git a/frontend/app/player/_web/Lists.ts b/frontend/app/player/_web/Lists.ts new file mode 100644 index 000000000..4f9c236cd --- /dev/null +++ b/frontend/app/player/_web/Lists.ts @@ -0,0 +1,71 @@ +import ListWalker from '../_common/ListWalker'; +import ListWalkerWithMarks from '../_common/ListWalkerWithMarks'; + +import type { Message } from './messages' + +const SIMPLE_LIST_NAMES = [ "event", "redux", "mobx", "vuex", "zustand", "ngrx", "graphql", "exceptions", "profiles"] as const; +const MARKED_LIST_NAMES = [ "log", "resource", "fetch", "stack" ] as const; +//const entityNamesSimple = [ "event", "profile" ]; + +const LIST_NAMES = [...SIMPLE_LIST_NAMES, ...MARKED_LIST_NAMES ]; + +// TODO: provide correct types + +export const INITIAL_STATE = LIST_NAMES.reduce((state, name) => { + state[`${name}List`] = [] + state[`${name}ListNow`] = [] + if (MARKED_LIST_NAMES.includes(name)) { + state[`${name}MarkedCountNow`] = 0 + } + return state +}, {}) + + +type SimpleListsObject = { + [key in typeof SIMPLE_LIST_NAMES[number]]: ListWalker +} +type MarkedListsObject = { + [key in typeof MARKED_LIST_NAMES[number]]: ListWalkerWithMarks +} +type ListsObject = SimpleListsObject & MarkedListsObject + +type InitialLists = { + [key in typeof LIST_NAMES[number]]: any[] +} + +export default class Lists { + lists: ListsObject + constructor(initialLists: Partial = {}) { + const lists: Partial = {} + for (const name of SIMPLE_LIST_NAMES) { + lists[name] = new ListWalker(initialLists[name]) + } + for (const name of MARKED_LIST_NAMES) { + // TODO: provide types + lists[name] = new ListWalkerWithMarks((el) => el.isRed(), initialLists[name]) + } + this.lists = lists as ListsObject + } + + getFullListsState() { + return LIST_NAMES.reduce((state, name) => { + state[`${name}List`] = this.lists[name].list + return state + }, {}) + } + + moveGetState(t: number) { + return LIST_NAMES.reduce((state, name) => { + const lastMsg = this.lists[name].moveGetLast(t) // index: name === 'exceptions' ? undefined : index); + if (lastMsg != null) { + state[`${name}ListNow`] = this.lists[name].listNow + } + return state + }, MARKED_LIST_NAMES.reduce((state, name) => { + state[`${name}RedCountNow`] = this.lists[name].markCountNow // Red --> Marked + return state + }, {}) + ); + } + +} \ No newline at end of file diff --git a/frontend/app/player/MessageDistributor/MessageDistributor.ts b/frontend/app/player/_web/MessageManager.ts similarity index 86% rename from frontend/app/player/MessageDistributor/MessageDistributor.ts rename to frontend/app/player/_web/MessageManager.ts index 85b01e6a3..e6dc9467c 100644 --- a/frontend/app/player/MessageDistributor/MessageDistributor.ts +++ b/frontend/app/player/_web/MessageManager.ts @@ -2,37 +2,31 @@ import { Decoder } from "syncod"; import logger from 'App/logger'; -import Resource, { TYPES } from 'Types/session/resource'; // MBTODO: player types? +import Resource, { TYPES } from 'Types/session/resource'; import { TYPES as EVENT_TYPES } from 'Types/session/event'; import Log from 'Types/session/log'; -import { update } from '../store'; import { toast } from 'react-toastify'; -import { - init as initListsDepr, - append as listAppend, - setStartTime as setListsStartTime -} from '../lists'; +import type { Store } from '../player/types'; +import ListWalker from '../_common/ListWalker'; -import StatedScreen from './StatedScreen/StatedScreen'; +import Screen from './Screen/Screen'; -import ListWalker from './managers/ListWalker'; import PagesManager from './managers/PagesManager'; import MouseMoveManager from './managers/MouseMoveManager'; import PerformanceTrackManager from './managers/PerformanceTrackManager'; import WindowNodeCounter from './managers/WindowNodeCounter'; import ActivityManager from './managers/ActivityManager'; -import AssistManager from './managers/AssistManager'; import MFileReader from './messages/MFileReader'; import { loadFiles, requestEFSDom, requestEFSDevtools } from './network/loadFiles'; import { decryptSessionBytes } from './network/crypto'; -import { INITIAL_STATE as SUPER_INITIAL_STATE, State as SuperState } from './StatedScreen/StatedScreen'; -import { INITIAL_STATE as ASSIST_INITIAL_STATE, State as AssistState } from './managers/AssistManager'; -import { INITIAL_STATE as LISTS_INITIAL_STATE , LIST_NAMES, initLists } from './Lists'; +import { INITIAL_STATE as SUPER_INITIAL_STATE, State as SuperState } from './Screen/Screen'; +import { INITIAL_STATE as ASSIST_INITIAL_STATE, State as AssistState } from './assist/AssistManager'; +import Lists, { INITIAL_STATE as LISTS_INITIAL_STATE } from './Lists'; import type { PerformanceChartPoint } from './managers/PerformanceTrackManager'; import type { SkipInterval } from './managers/ActivityManager'; @@ -50,8 +44,16 @@ export interface State extends SuperState, AssistState { domBuildingTime?: any, loadTime?: any, error: boolean, - devtoolsLoading: boolean + devtoolsLoading: boolean, + + liveTimeTravel: boolean, + messagesLoading: boolean, + cssLoading: boolean, + + ready: boolean, + lastMessageTime: number, } + export const INITIAL_STATE: State = { ...SUPER_INITIAL_STATE, ...LISTS_INITIAL_STATE, @@ -60,6 +62,14 @@ export const INITIAL_STATE: State = { skipIntervals: [], error: false, devtoolsLoading: false, + + liveTimeTravel: false, + messagesLoading: false, + cssLoading: false, + get ready() { + return !this.messagesLoading && !this.cssLoading + }, + lastMessageTime: 0, }; @@ -82,7 +92,7 @@ const visualChanges = [ "set_viewport_scroll", ] -export default class MessageDistributor extends StatedScreen { +export default class MessageManager extends Screen { // TODO: consistent with the other data-lists private locationEventManager: ListWalker/**/ = new ListWalker(); private locationManager: ListWalker = new ListWalker(); @@ -95,12 +105,11 @@ export default class MessageDistributor extends StatedScreen { private resizeManager: ListWalker = new ListWalker([]); private pagesManager: PagesManager; private mouseMoveManager: MouseMoveManager; - private assistManager: AssistManager; private scrollManager: ListWalker = new ListWalker(); private readonly decoder = new Decoder(); - private readonly lists = initLists(); + private readonly lists: Lists; private activityManager: ActivityManager | null = null; @@ -109,37 +118,39 @@ export default class MessageDistributor extends StatedScreen { private lastMessageTime: number = 0; private lastMessageInFileTime: number = 0; - constructor(private readonly session: any /*Session*/, config: any, live: boolean) { + constructor( + private readonly session: any /*Session*/, + private readonly state: Store, + config: any, + live: boolean, + ) { super(); this.pagesManager = new PagesManager(this, this.session.isMobile) this.mouseMoveManager = new MouseMoveManager(this); - this.assistManager = new AssistManager(session, this, config); this.sessionStart = this.session.startedAt; if (live) { - initListsDepr({}) - this.assistManager.connect(this.session.agentToken); + this.lists = new Lists() } else { this.activityManager = new ActivityManager(this.session.duration.milliseconds); /* == REFACTOR_ME == */ - const eventList = this.session.events.toJSON(); - - initListsDepr({ - event: eventList, - stack: this.session.stackEvents.toJSON(), - resource: this.session.resources.toJSON(), - }); - + const eventList = session.events.toJSON(); // TODO: fix types for events, remove immutable js eventList.forEach((e: Record) => { if (e.type === EVENT_TYPES.LOCATION) { //TODO type system this.locationEventManager.append(e); } - }); - this.session.errors.forEach((e: Record) => { - this.lists.exceptions.append(e); - }); + }) + + this.lists = new Lists({ + event: eventList, + stack: session.stackEvents.toJSON(), + resource: session.resources.toJSON(), + exceptions: session.errors, + }) + + /* === */ this.loadMessages(); } @@ -187,23 +198,21 @@ export default class MessageDistributor extends StatedScreen { private waitingForFiles: boolean = false private onFileReadSuccess = () => { - const stateToUpdate: {[key:string]: any} = { + const stateToUpdate = { performanceChartData: this.performanceTrackManager.chartData, performanceAvaliability: this.performanceTrackManager.avaliability, + ...this.lists.getFullListsState() } - LIST_NAMES.forEach(key => { - stateToUpdate[ `${ key }List` ] = this.lists[ key ].list - }) if (this.activityManager) { this.activityManager.end() stateToUpdate.skipIntervals = this.activityManager.list } - update(stateToUpdate) + this.state.update(stateToUpdate) } private onFileReadFailed = (e: any) => { logger.error(e) - update({ error: true }) + this.state.update({ error: true }) toast.error('Error requesting a session file') } private onFileReadFinally = () => { @@ -247,14 +256,14 @@ export default class MessageDistributor extends StatedScreen { // load devtools if (this.session.devtoolsURL.length) { - update({ devtoolsLoading: true }) + this.state.update({ devtoolsLoading: true }) loadFiles(this.session.devtoolsURL, createNewParser()) .catch(() => requestEFSDevtools(this.session.sessionId) .then(createNewParser(false)) ) //.catch() // not able to download the devtools file - .finally(() => update({ devtoolsLoading: false })) + .finally(() => this.state.update({ devtoolsLoading: false })) } } @@ -264,7 +273,7 @@ export default class MessageDistributor extends StatedScreen { this.parseAndDistributeMessages(new MFileReader(byteArray, this.sessionStart), onMessage) } const updateState = () => - update({ + this.state.update({ liveTimeTravel: true, }); @@ -304,7 +313,7 @@ export default class MessageDistributor extends StatedScreen { /* == REFACTOR_ME == */ const lastLoadedLocationMsg = this.loadedLocationManager.moveGetLast(t, index); if (!!lastLoadedLocationMsg) { - setListsStartTime(lastLoadedLocationMsg.time) + // TODO: page-wise resources list // setListsStartTime(lastLoadedLocationMsg.time) this.navigationStartOffset = lastLoadedLocationMsg.navigationStart - this.sessionStart; } const llEvent = this.locationEventManager.moveGetLast(t, index); @@ -340,15 +349,8 @@ export default class MessageDistributor extends StatedScreen { stateToUpdate.performanceChartTime = lastPerformanceTrackMessage.time; } - LIST_NAMES.forEach(key => { - const lastMsg = this.lists[key].moveGetLast(t, key === 'exceptions' ? undefined : index); - if (lastMsg != null) { - // @ts-ignore TODO: fix types - stateToUpdate[`${key}ListNow`] = this.lists[key].listNow; - } - }); - - Object.keys(stateToUpdate).length > 0 && update(stateToUpdate); + this.lists.moveGetState(t) + Object.keys(stateToUpdate).length > 0 && this.state.update(stateToUpdate); /* Sequence of the managers is important here */ // Preparing the size of "screen" @@ -405,6 +407,7 @@ export default class MessageDistributor extends StatedScreen { private distributeMessage(msg: Message, index: number): void { const lastMessageTime = Math.max(msg.time, this.lastMessageTime) this.lastMessageTime = lastMessageTime + this.state.update({ lastMessageTime }) if (visualChanges.includes(msg.tp)) { this.activityManager?.updateAcctivity(msg.time); } @@ -414,15 +417,15 @@ export default class MessageDistributor extends StatedScreen { /* Lists: */ case "console_log": if (msg.level === 'debug') break; - listAppend("log", Log({ + this.lists.lists.log.append(Log({ level: msg.level, value: msg.value, time, index, - })); + })) break; case "fetch": - listAppend("fetch", Resource({ + this.lists.lists.fetch.append(Resource({ method: msg.method, url: msg.url, payload: msg.request, @@ -469,42 +472,42 @@ export default class MessageDistributor extends StatedScreen { decoded = this.decodeStateMessage(msg, ["state", "action"]); logger.log('redux', decoded) if (decoded != null) { - this.lists.redux.append(decoded); + this.lists.lists.redux.append(decoded); } break; case "ng_rx": decoded = this.decodeStateMessage(msg, ["state", "action"]); logger.log('ngrx', decoded) if (decoded != null) { - this.lists.ngrx.append(decoded); + this.lists.lists.ngrx.append(decoded); } break; case "vuex": decoded = this.decodeStateMessage(msg, ["state", "mutation"]); logger.log('vuex', decoded) if (decoded != null) { - this.lists.vuex.append(decoded); + this.lists.lists.vuex.append(decoded); } break; case "zustand": decoded = this.decodeStateMessage(msg, ["state", "mutation"]) logger.log('zustand', decoded) if (decoded != null) { - this.lists.zustand.append(decoded) + this.lists.lists.zustand.append(decoded) } case "mob_x": decoded = this.decodeStateMessage(msg, ["payload"]); logger.log('mobx', decoded) if (decoded != null) { - this.lists.mobx.append(decoded); + this.lists.lists.mobx.append(decoded); } break; case "graph_ql": - this.lists.graphql.append(msg); + this.lists.lists.graphql.append(msg); break; case "profiler": - this.lists.profiles.append(msg); + this.lists.lists.profiles.append(msg); break; default: switch (msg.tp) { @@ -540,11 +543,27 @@ export default class MessageDistributor extends StatedScreen { return this.pagesManager.minTime; } + + setMessagesLoading(messagesLoading: boolean) { + this.display(!messagesLoading); + this.state.update({ messagesLoading }); + } + + setCSSLoading(cssLoading: boolean) { + this.displayFrame(!cssLoading); + this.state.update({ cssLoading }); + } + + private setSize({ height, width }: { height: number, width: number }) { + this.scale({ height, width }); + this.state.update({ width, height }); + + //this.updateMarketTargets() + } + // TODO: clean managers? clean() { - super.clean(); - update(INITIAL_STATE); - this.assistManager.clear(); + this.state.update(INITIAL_STATE); this.incomingMessages.length = 0 } diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/BaseScreen.ts b/frontend/app/player/_web/Screen/BaseScreen.ts similarity index 94% rename from frontend/app/player/MessageDistributor/StatedScreen/Screen/BaseScreen.ts rename to frontend/app/player/_web/Screen/BaseScreen.ts index d86284851..67f527e01 100644 --- a/frontend/app/player/MessageDistributor/StatedScreen/Screen/BaseScreen.ts +++ b/frontend/app/player/_web/Screen/BaseScreen.ts @@ -1,5 +1,4 @@ import styles from './screen.module.css'; -import { getState } from '../../../store'; import type { Point } from './types'; @@ -86,9 +85,6 @@ export default abstract class BaseScreen { parentElement.appendChild(this.screen); this.parentElement = parentElement; - // parentElement.onresize = this.scale; - window.addEventListener('resize', this.scale); - this.scale(); /* == For the Inspecting Document content == */ this.overlay.addEventListener('contextmenu', () => { @@ -197,9 +193,8 @@ export default abstract class BaseScreen { return this.s; } - _scale() { + scale({ height, width }: { height: number, width: number }) { if (!this.parentElement) return; - const { height, width } = getState(); const { offsetWidth, offsetHeight } = this.parentElement; this.s = Math.min(offsetWidth / width, offsetHeight / height); @@ -216,13 +211,4 @@ export default abstract class BaseScreen { this.boundingRect = this.overlay.getBoundingClientRect(); } - - scale = () => { // TODO: solve classes inheritance issues in typescript - this._scale(); - } - - - clean() { - window.removeEventListener('resize', this.scale); - } } diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/Cursor.ts b/frontend/app/player/_web/Screen/Cursor.ts similarity index 100% rename from frontend/app/player/MessageDistributor/StatedScreen/Screen/Cursor.ts rename to frontend/app/player/_web/Screen/Cursor.ts diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/Inspector.js b/frontend/app/player/_web/Screen/Inspector.js similarity index 100% rename from frontend/app/player/MessageDistributor/StatedScreen/Screen/Inspector.js rename to frontend/app/player/_web/Screen/Inspector.js diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/Marker.js b/frontend/app/player/_web/Screen/Marker.ts similarity index 66% rename from frontend/app/player/MessageDistributor/StatedScreen/Screen/Marker.js rename to frontend/app/player/_web/Screen/Marker.ts index ff1476ac3..4d3fab9b1 100644 --- a/frontend/app/player/MessageDistributor/StatedScreen/Screen/Marker.js +++ b/frontend/app/player/_web/Screen/Marker.ts @@ -1,32 +1,32 @@ +import type BaseScreen from './BaseScreen' import styles from './marker.module.css'; -function escapeRegExp(string) { +function escapeRegExp(string: string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } -function escapeHtml(string) { +function escapeHtml(string: string) { return string.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", '''); } -function safeString(string) { +function safeString(string: string) { return (escapeHtml(escapeRegExp(string))) } export default class Marker { - _target = null; - _selector = null; - _tooltip = null; + private _target: Element | null = null; + private selector: string | null = null; + private tooltip: HTMLDivElement + private marker: HTMLDivElement - constructor(overlay, screen) { - this.screen = screen; - - this._tooltip = document.createElement('div'); - this._tooltip.className = styles.tooltip; - this._tooltip.appendChild(document.createElement('div')); + constructor(overlay: HTMLElement, private readonly screen: BaseScreen) { + this.tooltip = document.createElement('div'); + this.tooltip.className = styles.tooltip; + this.tooltip.appendChild(document.createElement('div')); const htmlStr = document.createElement('div'); htmlStr.innerHTML = 'Right-click > Inspect for more details.'; - this._tooltip.appendChild(htmlStr); + this.tooltip.appendChild(htmlStr); const marker = document.createElement('div'); marker.className = styles.marker; @@ -43,34 +43,34 @@ export default class Marker { marker.appendChild(markerT); marker.appendChild(markerB); - marker.appendChild(this._tooltip); + marker.appendChild(this.tooltip); overlay.appendChild(marker); - this._marker = marker; + this.marker = marker; } get target() { return this._target; } - mark(element) { + mark(element: Element | null) { if (this._target === element) { return; } this._target = element; - this._selector = null; + this.selector = null; this.redraw(); } unmark() { - this.mark(null); + this.mark(null) } - _autodefineTarget() { + private autodefineTarget() { // TODO: put to Screen - if (this._selector) { + if (this.selector) { try { - const fitTargets = this.screen.document.querySelectorAll(this._selector); + const fitTargets = this.screen.document.querySelectorAll(this.selector); if (fitTargets.length === 0) { this._target = null; } else { @@ -90,9 +90,9 @@ export default class Marker { } } - markBySelector(selector) { - this._selector = selector; - this._autodefineTarget(); + markBySelector(selector: string) { + this.selector = selector; + this.autodefineTarget(); this.redraw(); } @@ -116,20 +116,20 @@ export default class Marker { } redraw() { - if (this._selector) { - this._autodefineTarget(); + if (this.selector) { + this.autodefineTarget(); } if (!this._target) { - this._marker.style.display = 'none'; + this.marker.style.display = 'none'; return; } const rect = this._target.getBoundingClientRect(); - this._marker.style.display = 'block'; - this._marker.style.left = rect.left + 'px'; - this._marker.style.top = rect.top + 'px'; - this._marker.style.width = rect.width + 'px'; - this._marker.style.height = rect.height + 'px'; + this.marker.style.display = 'block'; + this.marker.style.left = rect.left + 'px'; + this.marker.style.top = rect.top + 'px'; + this.marker.style.width = rect.width + 'px'; + this.marker.style.height = rect.height + 'px'; - this._tooltip.firstChild.innerHTML = this.getTagString(this._target); + this.tooltip.firstChild.innerHTML = this.getTagString(this._target); } } diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/Screen.ts b/frontend/app/player/_web/Screen/Screen.ts similarity index 91% rename from frontend/app/player/MessageDistributor/StatedScreen/Screen/Screen.ts rename to frontend/app/player/_web/Screen/Screen.ts index a0ae8a800..59986423e 100644 --- a/frontend/app/player/MessageDistributor/StatedScreen/Screen/Screen.ts +++ b/frontend/app/player/_web/Screen/Screen.ts @@ -2,7 +2,6 @@ import Marker from './Marker'; import Cursor from './Cursor'; import Inspector from './Inspector'; // import styles from './screen.module.css'; -// import { getState } from '../../../store'; import BaseScreen from './BaseScreen'; export { INITIAL_STATE } from './BaseScreen'; @@ -12,7 +11,7 @@ export default class Screen extends BaseScreen { public readonly cursor: Cursor; private substitutor: BaseScreen | null = null; private inspector: Inspector | null = null; - private marker: Marker | null = null; + public marker: Marker | null = null; constructor() { super(); this.cursor = new Cursor(this.overlay); @@ -26,14 +25,14 @@ export default class Screen extends BaseScreen { return this.getElementsFromInternalPoint(this.cursor.getPosition()); } - _scale() { - super._scale(); + scale(dims: { height: number, width: number }) { + super.scale(dims) if (this.substitutor) { - this.substitutor._scale(); + this.substitutor.scale(dims) } } - enableInspector(clickCallback: ({ target: Element }) => void): Document | null { + enableInspector(clickCallback: (e: { target: Element }) => void): Document | null { if (!this.parentElement) return null; if (!this.substitutor) { this.substitutor = new Screen(); diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/cursor.module.css b/frontend/app/player/_web/Screen/cursor.module.css similarity index 100% rename from frontend/app/player/MessageDistributor/StatedScreen/Screen/cursor.module.css rename to frontend/app/player/_web/Screen/cursor.module.css diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/index.js b/frontend/app/player/_web/Screen/index.js similarity index 100% rename from frontend/app/player/MessageDistributor/StatedScreen/Screen/index.js rename to frontend/app/player/_web/Screen/index.js diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/marker.module.css b/frontend/app/player/_web/Screen/marker.module.css similarity index 100% rename from frontend/app/player/MessageDistributor/StatedScreen/Screen/marker.module.css rename to frontend/app/player/_web/Screen/marker.module.css diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/screen.module.css b/frontend/app/player/_web/Screen/screen.module.css similarity index 100% rename from frontend/app/player/MessageDistributor/StatedScreen/Screen/screen.module.css rename to frontend/app/player/_web/Screen/screen.module.css diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/types.ts b/frontend/app/player/_web/Screen/types.ts similarity index 100% rename from frontend/app/player/MessageDistributor/StatedScreen/Screen/types.ts rename to frontend/app/player/_web/Screen/types.ts diff --git a/frontend/app/player/_web/WebPlayer.ts b/frontend/app/player/_web/WebPlayer.ts new file mode 100644 index 000000000..a48577288 --- /dev/null +++ b/frontend/app/player/_web/WebPlayer.ts @@ -0,0 +1,178 @@ +import type { Store } from '../player/types' +import Player, { State as PlayerState } from '../player/Player' + +import MessageManager from './MessageManager' +import AssistManager from './assist/AssistManager' +import Screen from './Screen/Screen' +import { State as MMState, INITIAL_STATE as MM_INITIAL_STATE } from './MessageManager' + + + +export default class WebPlayer extends Player { + private readonly screen: Screen + private readonly messageManager: MessageManager + + assistManager: AssistManager // public so far + + constructor(private wpState: Store, session, config, live: boolean) { + // TODO: separate screen from manager + const screen = new MessageManager(session, wpState, config, live) + super(wpState, screen) + this.screen = screen + this.messageManager = screen + + // TODO: separate LiveWebPlayer + this.assistManager = new AssistManager(session, this.messageManager, config, wpState) + + + const endTime = !live && session.duration.valueOf() + wpState.update({ + //@ts-ignore + initialized: true, + //@ts-ignore + session, + + live, + livePlay: live, + endTime, // : 0, //TODO: through initialState + }) + + if (live) { + this.assistManager.connect(session.agentToken) + } + } + + attach(parent: HTMLElement) { + this.screen.attach(parent) + window.addEventListener('resize', this.scale) + this.scale() + } + scale = () => { + const { width, height } = this.wpState.get() + this.screen.scale({ width, height }) + } + mark(e: Element) { + this.screen.marker.mark(e) + } + + updateMarketTargets() { + // const { markedTargets } = getState(); + // if (markedTargets) { + // update({ + // markedTargets: markedTargets.map((mt: any) => ({ + // ...mt, + // boundingRect: this.calculateRelativeBoundingRect(mt.el), + // })), + // }); + // } + } + + // private calculateRelativeBoundingRect(el: Element): BoundingRect { + // if (!this.parentElement) return {top:0, left:0, width:0,height:0} //TODO + // const { top, left, width, height } = el.getBoundingClientRect(); + // const s = this.getScale(); + // const scrinRect = this.screen.getBoundingClientRect(); + // const parentRect = this.parentElement.getBoundingClientRect(); + + // return { + // top: top*s + scrinRect.top - parentRect.top, + // left: left*s + scrinRect.left - parentRect.left, + // width: width*s, + // height: height*s, + // } + // } + + setActiveTarget(index: number) { + // const window = this.window + // const markedTargets: MarkedTarget[] | null = getState().markedTargets + // const target = markedTargets && markedTargets[index] + // if (target && window) { + // const { fixedTop, rect } = getOffset(target.el, window) + // const scrollToY = fixedTop - window.innerHeight / 1.5 + // if (rect.top < 0 || rect.top > window.innerHeight) { + // // behavior hack TODO: fix it somehow when they will decide to remove it from browser api + // // @ts-ignore + // window.scrollTo({ top: scrollToY, behavior: 'instant' }) + // setTimeout(() => { + // if (!markedTargets) { return } + // update({ + // markedTargets: markedTargets.map(t => t === target ? { + // ...target, + // boundingRect: this.calculateRelativeBoundingRect(target.el), + // } : t) + // }) + // }, 0) + // } + + // } + // update({ activeTargetIndex: index }); + } + + // private actualScroll: Point | null = null + setMarkedTargets(selections: { selector: string, count: number }[] | null) { + // if (selections) { + // const totalCount = selections.reduce((a, b) => { + // return a + b.count + // }, 0); + // const markedTargets: MarkedTarget[] = []; + // let index = 0; + // selections.forEach((s) => { + // const el = this.getElementBySelector(s.selector); + // if (!el) return; + // markedTargets.push({ + // ...s, + // el, + // index: index++, + // percent: Math.round((s.count * 100) / totalCount), + // boundingRect: this.calculateRelativeBoundingRect(el), + // count: s.count, + // }) + // }); + // this.actualScroll = this.getCurrentScroll() + // update({ markedTargets }); + // } else { + // if (this.actualScroll) { + // this.window?.scrollTo(this.actualScroll.x, this.actualScroll.y) + // this.actualScroll = null + // } + // update({ markedTargets: null }); + // } + } + + markTargets(targets: { selector: string, count: number }[] | null) { + // this.animator.pause(); + // this.setMarkedTargets(targets); + } + + toggleInspectorMode(flag, clickCallback) { + // if (typeof flag !== 'boolean') { + // const { inspectorMode } = getState(); + // flag = !inspectorMode; + // } + + // if (flag) { + // this.pause() + // update({ inspectorMode: true }); + // return super.enableInspector(clickCallback); + // } else { + // super.disableInspector(); + // update({ inspectorMode: false }); + // } + } + + async toggleTimetravel() { + if (!this.wpState.get().liveTimeTravel) { + return await this.messageManager.reloadWithUnprocessedFile() + } + } + + toggleUserName(name?: string) { + this.screen.cursor.toggleUserName(name) + } + clean() { + super.clean() + this.assistManager.clean() + window.removeEventListener('resize', this.scale) + } +} + diff --git a/frontend/app/player/MessageDistributor/managers/AnnotationCanvas.ts b/frontend/app/player/_web/assist/AnnotationCanvas.ts similarity index 100% rename from frontend/app/player/MessageDistributor/managers/AnnotationCanvas.ts rename to frontend/app/player/_web/assist/AnnotationCanvas.ts diff --git a/frontend/app/player/MessageDistributor/managers/AssistManager.ts b/frontend/app/player/_web/assist/AssistManager.ts similarity index 87% rename from frontend/app/player/MessageDistributor/managers/AssistManager.ts rename to frontend/app/player/_web/assist/AssistManager.ts index 897d8dfba..1e1274226 100644 --- a/frontend/app/player/MessageDistributor/managers/AssistManager.ts +++ b/frontend/app/player/_web/assist/AssistManager.ts @@ -1,11 +1,10 @@ import type { Socket } from 'socket.io-client'; import type Peer from 'peerjs'; import type { MediaConnection } from 'peerjs'; -import type MessageDistributor from '../MessageDistributor'; -import store from 'App/store'; +import type MessageManager from '../MessageManager'; +import appStore from 'App/store'; import type { LocalStream } from './LocalStream'; -import { update, getState } from '../../store'; -// import { iceServerConfigFromString } from 'App/utils' +import type { Store } from '../../player/types' import AnnotationCanvas from './AnnotationCanvas'; import MStreamReader from '../messages/MStreamReader'; import JSONRawMessageReader from '../messages/JSONRawMessageReader' @@ -76,10 +75,15 @@ export default class AssistManager { private videoStreams: Record = {} // TODO: Session type - constructor(private session: any, private md: MessageDistributor, private config: any) {} + constructor( + private session: any, + private md: MessageManager, + private config: any, + private store: Store, + ) {} private setStatus(status: ConnectionStatus) { - if (getState().peerConnectionStatus === ConnectionStatus.Disconnected && + if (this.store.get().peerConnectionStatus === ConnectionStatus.Disconnected && status !== ConnectionStatus.Connected) { return } @@ -94,7 +98,7 @@ export default class AssistManager { } else { this.md.display(false); } - update({ peerConnectionStatus: status }); + this.store.update({ peerConnectionStatus: status }); } private get peerID(): string { @@ -106,7 +110,7 @@ export default class AssistManager { this.socketCloseTimeout && clearTimeout(this.socketCloseTimeout) if (document.hidden) { this.socketCloseTimeout = setTimeout(() => { - const state = getState() + const state = this.store.get() if (document.hidden && (state.calling === CallingState.NoCall && state.remoteControl === RemoteControlStatus.Enabled)) { this.socket?.close() @@ -134,7 +138,7 @@ export default class AssistManager { } const now = +new Date() - update({ assistStart: now }) + this.store.update({ assistStart: now }) import('socket.io-client').then(({ default: io }) => { if (this.cleaned) { return } @@ -162,7 +166,7 @@ export default class AssistManager { }) socket.on("disconnect", () => { this.toggleRemoteControl(false) - update({ calling: CallingState.NoCall }) + this.store.update({ calling: CallingState.NoCall }) }) socket.on('messages', messages => { jmr.append(messages) // as RawMessage[] @@ -172,7 +176,7 @@ export default class AssistManager { this.setStatus(ConnectionStatus.Connected) // Call State - if (getState().calling === CallingState.Reconnecting) { + if (this.store.get().calling === CallingState.Reconnecting) { this._callSessionPeer() // reconnecting call (todo improve code separation) } } @@ -222,15 +226,15 @@ export default class AssistManager { this.setStatus(ConnectionStatus.Disconnected) }, 30000) - if (getState().remoteControl === RemoteControlStatus.Requesting) { + if (this.store.get().remoteControl === RemoteControlStatus.Requesting) { this.toggleRemoteControl(false) // else its remaining } // Call State - if (getState().calling === CallingState.OnCall) { - update({ calling: CallingState.Reconnecting }) - } else if (getState().calling === CallingState.Requesting){ - update({ calling: CallingState.NoCall }) + if (this.store.get().calling === CallingState.OnCall) { + this.store.update({ calling: CallingState.Reconnecting }) + } else if (this.store.get().calling === CallingState.Requesting){ + this.store.update({ calling: CallingState.NoCall }) } }) socket.on('error', e => { @@ -263,7 +267,7 @@ export default class AssistManager { private onMouseClick = (e: MouseEvent): void => { if (!this.socket) { return; } - if (getState().annotating) { return; } // ignore clicks while annotating + if (this.store.get().annotating) { return; } // ignore clicks while annotating const data = this.md.getInternalViewportCoordinates(e) // const el = this.md.getElementFromPoint(e); // requires requestiong node_id from domManager @@ -299,31 +303,31 @@ export default class AssistManager { this.md.overlay.addEventListener("click", this.onMouseClick) this.md.overlay.addEventListener("wheel", this.onWheel) this.md.toggleRemoteControlStatus(true) - update({ remoteControl: RemoteControlStatus.Enabled }) + this.store.update({ remoteControl: RemoteControlStatus.Enabled }) } else { this.md.overlay.removeEventListener("mousemove", this.onMouseMove) this.md.overlay.removeEventListener("click", this.onMouseClick) this.md.overlay.removeEventListener("wheel", this.onWheel) this.md.toggleRemoteControlStatus(false) - update({ remoteControl: RemoteControlStatus.Disabled }) + this.store.update({ remoteControl: RemoteControlStatus.Disabled }) this.toggleAnnotation(false) } } requestReleaseRemoteControl = () => { if (!this.socket) { return } - const remoteControl = getState().remoteControl + const remoteControl = this.store.get().remoteControl if (remoteControl === RemoteControlStatus.Requesting) { return } if (remoteControl === RemoteControlStatus.Disabled) { - update({ remoteControl: RemoteControlStatus.Requesting }) + this.store.update({ remoteControl: RemoteControlStatus.Requesting }) this.socket.emit("request_control", JSON.stringify({ ...this.session.agentInfo, query: document.location.search })) // setTimeout(() => { - // if (getState().remoteControl !== RemoteControlStatus.Requesting) { return } + // if (this.store.get().remoteControl !== RemoteControlStatus.Requesting) { return } // this.socket?.emit("release_control") - // update({ remoteControl: RemoteControlStatus.Disabled }) + // this.store.update({ remoteControl: RemoteControlStatus.Disabled }) // }, 8000) } else { this.socket.emit("release_control") @@ -416,15 +420,15 @@ export default class AssistManager { private handleCallEnd() { this.callArgs && this.callArgs.onCallEnd() this.callConnection[0] && this.callConnection[0].close() - update({ calling: CallingState.NoCall }) + this.store.update({ calling: CallingState.NoCall }) this.callArgs = null this.toggleAnnotation(false) } public initiateCallEnd = async () => { - this.socket?.emit("call_end", store.getState().getIn([ 'user', 'account', 'name'])) + this.socket?.emit("call_end", appStore.getState().getIn([ 'user', 'account', 'name'])) this.handleCallEnd() - const remoteControl = getState().remoteControl + const remoteControl = this.store.get().remoteControl if (remoteControl === RemoteControlStatus.Enabled) { this.socket.emit("release_control") this.toggleRemoteControl(false) @@ -432,10 +436,10 @@ export default class AssistManager { } private onRemoteCallEnd = () => { - if (getState().calling === CallingState.Requesting) { + if (this.store.get().calling === CallingState.Requesting) { this.callArgs && this.callArgs.onReject() this.callConnection[0] && this.callConnection[0].close() - update({ calling: CallingState.NoCall }) + this.store.update({ calling: CallingState.NoCall }) this.callArgs = null this.toggleAnnotation(false) } else { @@ -487,10 +491,10 @@ export default class AssistManager { /** Connecting to the app user */ private _callSessionPeer() { - if (![CallingState.NoCall, CallingState.Reconnecting].includes(getState().calling)) { return } - update({ calling: CallingState.Connecting }) + if (![CallingState.NoCall, CallingState.Reconnecting].includes(this.store.get().calling)) { return } + this.store.update({ calling: CallingState.Connecting }) this._peerConnection(this.peerID); - this.socket && this.socket.emit("_agent_name", store.getState().getIn([ 'user', 'account', 'name'])) + this.socket && this.socket.emit("_agent_name", appStore.getState().getIn([ 'user', 'account', 'name'])) } private async _peerConnection(remotePeerId: string) { @@ -509,7 +513,7 @@ export default class AssistManager { }) call.on('stream', stream => { - getState().calling !== CallingState.OnCall && update({ calling: CallingState.OnCall }) + this.store.get().calling !== CallingState.OnCall && this.store.update({ calling: CallingState.OnCall }) this.videoStreams[call.peer] = stream.getVideoTracks()[0] @@ -529,9 +533,9 @@ export default class AssistManager { } toggleAnnotation(enable?: boolean) { - // if (getState().calling !== CallingState.OnCall) { return } + // if (this.store.get().calling !== CallingState.OnCall) { return } if (typeof enable !== "boolean") { - enable = !!getState().annotating + enable = !!this.store.get().annotating } if (enable && !this.annot) { const annot = this.annot = new AnnotationCanvas() @@ -559,11 +563,11 @@ export default class AssistManager { annot.move([ data.x, data.y ]) this.socket.emit("moveAnnotation", [ data.x, data.y ]) }) - update({ annotating: true }) + this.store.update({ annotating: true }) } else if (!enable && !!this.annot) { this.annot.remove() this.annot = null - update({ annotating: false }) + this.store.update({ annotating: false }) } } @@ -577,7 +581,7 @@ export default class AssistManager { /* ==== Cleaning ==== */ private cleaned: boolean = false - clear() { + clean() { this.cleaned = true // sometimes cleaned before modules loaded this.initiateCallEnd(); if (this._peer) { diff --git a/frontend/app/player/MessageDistributor/managers/LocalStream.ts b/frontend/app/player/_web/assist/LocalStream.ts similarity index 100% rename from frontend/app/player/MessageDistributor/managers/LocalStream.ts rename to frontend/app/player/_web/assist/LocalStream.ts diff --git a/frontend/app/player/MessageDistributor/managers/ActivityManager.ts b/frontend/app/player/_web/managers/ActivityManager.ts similarity index 83% rename from frontend/app/player/MessageDistributor/managers/ActivityManager.ts rename to frontend/app/player/_web/managers/ActivityManager.ts index 412fefdce..32265f924 100644 --- a/frontend/app/player/MessageDistributor/managers/ActivityManager.ts +++ b/frontend/app/player/_web/managers/ActivityManager.ts @@ -1,18 +1,18 @@ -import ListWalker from './ListWalker'; +import ListWalker from '../../_common/ListWalker'; class SkipIntervalCls { - constructor(private readonly start = 0, private readonly end = 0) {} + constructor(readonly start = 0, readonly end = 0) {} get time(): number { return this.start; } - contains(ts: number) { + contains(ts) { return ts > this.start && ts < this.end; } } -export type SkipInterval = InstanceType; // exporting only class' type +export type SkipInterval = InstanceType; export default class ActivityManager extends ListWalker { diff --git a/frontend/app/player/MessageDistributor/managers/DOM/DOMManager.ts b/frontend/app/player/_web/managers/DOM/DOMManager.ts similarity index 99% rename from frontend/app/player/MessageDistributor/managers/DOM/DOMManager.ts rename to frontend/app/player/_web/managers/DOM/DOMManager.ts index 2688abb14..370df5c03 100644 --- a/frontend/app/player/MessageDistributor/managers/DOM/DOMManager.ts +++ b/frontend/app/player/_web/managers/DOM/DOMManager.ts @@ -1,9 +1,9 @@ import logger from 'App/logger'; -import type StatedScreen from '../../StatedScreen'; +import type MessageManager from '../../MessageManager'; import type { Message, SetNodeScroll, CreateElementNode } from '../../messages'; -import ListWalker from '../ListWalker'; +import ListWalker from '../../../_common/ListWalker'; import StylesManager, { rewriteNodeStyleSheet } from './StylesManager'; import FocusManager from './FocusManager'; import { @@ -51,7 +51,7 @@ export default class DOMManager extends ListWalker { constructor( - private readonly screen: StatedScreen, + private readonly screen: MessageManager, private readonly isMobile: boolean, public readonly time: number ) { diff --git a/frontend/app/player/MessageDistributor/managers/DOM/FocusManager.ts b/frontend/app/player/_web/managers/DOM/FocusManager.ts similarity index 93% rename from frontend/app/player/MessageDistributor/managers/DOM/FocusManager.ts rename to frontend/app/player/_web/managers/DOM/FocusManager.ts index 6f80ed16c..629f625e3 100644 --- a/frontend/app/player/MessageDistributor/managers/DOM/FocusManager.ts +++ b/frontend/app/player/_web/managers/DOM/FocusManager.ts @@ -1,7 +1,7 @@ import logger from 'App/logger'; import type { SetNodeFocus } from '../../messages'; import type { VElement } from './VirtualDOM'; -import ListWalker from '../ListWalker'; +import ListWalker from '../../../_common/ListWalker'; const FOCUS_CLASS = "-openreplay-focus" diff --git a/frontend/app/player/MessageDistributor/managers/DOM/StylesManager.ts b/frontend/app/player/_web/managers/DOM/StylesManager.ts similarity index 95% rename from frontend/app/player/MessageDistributor/managers/DOM/StylesManager.ts rename to frontend/app/player/_web/managers/DOM/StylesManager.ts index b6dddcdd9..1cb49c5dd 100644 --- a/frontend/app/player/MessageDistributor/managers/DOM/StylesManager.ts +++ b/frontend/app/player/_web/managers/DOM/StylesManager.ts @@ -1,9 +1,9 @@ -import type StatedScreen from '../../StatedScreen'; +import type MessageManager from '../../MessageManager'; import type { CssInsertRule, CssDeleteRule } from '../../messages'; type CSSRuleMessage = CssInsertRule | CssDeleteRule; -import ListWalker from '../ListWalker'; +import ListWalker from '../../../_common/ListWalker'; const HOVER_CN = "-openreplay-hover"; @@ -26,7 +26,7 @@ export default class StylesManager extends ListWalker { private linkLoadPromises: Array> = []; private skipCSSLinks: Array = []; // should be common for all pages - constructor(private readonly screen: StatedScreen) { + constructor(private readonly screen: MessageManager) { super(); } diff --git a/frontend/app/player/MessageDistributor/managers/DOM/VirtualDOM.ts b/frontend/app/player/_web/managers/DOM/VirtualDOM.ts similarity index 100% rename from frontend/app/player/MessageDistributor/managers/DOM/VirtualDOM.ts rename to frontend/app/player/_web/managers/DOM/VirtualDOM.ts diff --git a/frontend/app/player/MessageDistributor/managers/DOM/safeCSSRules.ts b/frontend/app/player/_web/managers/DOM/safeCSSRules.ts similarity index 100% rename from frontend/app/player/MessageDistributor/managers/DOM/safeCSSRules.ts rename to frontend/app/player/_web/managers/DOM/safeCSSRules.ts diff --git a/frontend/app/player/MessageDistributor/managers/MouseMoveManager.ts b/frontend/app/player/_web/managers/MouseMoveManager.ts similarity index 89% rename from frontend/app/player/MessageDistributor/managers/MouseMoveManager.ts rename to frontend/app/player/_web/managers/MouseMoveManager.ts index ca9e3b740..5dd3160a5 100644 --- a/frontend/app/player/MessageDistributor/managers/MouseMoveManager.ts +++ b/frontend/app/player/_web/managers/MouseMoveManager.ts @@ -1,7 +1,7 @@ -import type StatedScreen from '../StatedScreen'; +import type Screen from '../Screen/Screen'; import type { MouseMove } from '../messages'; -import ListWalker from './ListWalker'; +import ListWalker from '../../_common/ListWalker'; const HOVER_CLASS = "-openreplay-hover"; const HOVER_CLASS_DEPR = "-asayer-hover"; @@ -9,7 +9,7 @@ const HOVER_CLASS_DEPR = "-asayer-hover"; export default class MouseMoveManager extends ListWalker { private hoverElements: Array = []; - constructor(private screen: StatedScreen) {super()} + constructor(private screen: Screen) {super()} private updateHover(): void { const curHoverElements = this.screen.getCursorTargets(); diff --git a/frontend/app/player/MessageDistributor/managers/PagesManager.ts b/frontend/app/player/_web/managers/PagesManager.ts similarity index 85% rename from frontend/app/player/MessageDistributor/managers/PagesManager.ts rename to frontend/app/player/_web/managers/PagesManager.ts index 9a4398246..6a3f20f7c 100644 --- a/frontend/app/player/MessageDistributor/managers/PagesManager.ts +++ b/frontend/app/player/_web/managers/PagesManager.ts @@ -1,7 +1,7 @@ -import type StatedScreen from '../StatedScreen'; +import type Screen from '../Screen/Screen'; import type { Message } from '../messages'; -import ListWalker from './ListWalker'; +import ListWalker from '../../_common/ListWalker'; import DOMManager from './DOM/DOMManager'; @@ -9,9 +9,9 @@ export default class PagesManager extends ListWalker { private currentPage: DOMManager | null = null private isMobile: boolean; - private screen: StatedScreen; + private screen: Screen; - constructor(screen: StatedScreen, isMobile: boolean) { + constructor(screen: Screen, isMobile: boolean) { super() this.screen = screen this.isMobile = isMobile diff --git a/frontend/app/player/MessageDistributor/managers/PerformanceTrackManager.ts b/frontend/app/player/_web/managers/PerformanceTrackManager.ts similarity index 98% rename from frontend/app/player/MessageDistributor/managers/PerformanceTrackManager.ts rename to frontend/app/player/_web/managers/PerformanceTrackManager.ts index c4a4a8e63..424466b4a 100644 --- a/frontend/app/player/MessageDistributor/managers/PerformanceTrackManager.ts +++ b/frontend/app/player/_web/managers/PerformanceTrackManager.ts @@ -1,6 +1,6 @@ import type { PerformanceTrack, SetPageVisibility } from '../messages'; -import ListWalker from './ListWalker'; +import ListWalker from '../../_common/ListWalker'; export type PerformanceChartPoint = { time: number, diff --git a/frontend/app/player/MessageDistributor/managers/ReduxStateManager.ts b/frontend/app/player/_web/managers/ReduxStateManager.ts similarity index 96% rename from frontend/app/player/MessageDistributor/managers/ReduxStateManager.ts rename to frontend/app/player/_web/managers/ReduxStateManager.ts index 81638994e..4756b303a 100644 --- a/frontend/app/player/MessageDistributor/managers/ReduxStateManager.ts +++ b/frontend/app/player/_web/managers/ReduxStateManager.ts @@ -1,5 +1,5 @@ // import { applyChange, revertChange } from 'deep-diff'; -// import ListWalker from './ListWalker'; +// import ListWalker from '../../_common/ListWalker'; // import type { Redux } from '../messages'; // export default class ReduxStateManager extends ListWalker { diff --git a/frontend/app/player/MessageDistributor/managers/WindowNodeCounter.ts b/frontend/app/player/_web/managers/WindowNodeCounter.ts similarity index 100% rename from frontend/app/player/MessageDistributor/managers/WindowNodeCounter.ts rename to frontend/app/player/_web/managers/WindowNodeCounter.ts diff --git a/frontend/app/player/MessageDistributor/messages/JSONRawMessageReader.ts b/frontend/app/player/_web/messages/JSONRawMessageReader.ts similarity index 100% rename from frontend/app/player/MessageDistributor/messages/JSONRawMessageReader.ts rename to frontend/app/player/_web/messages/JSONRawMessageReader.ts diff --git a/frontend/app/player/MessageDistributor/messages/MFileReader.ts b/frontend/app/player/_web/messages/MFileReader.ts similarity index 100% rename from frontend/app/player/MessageDistributor/messages/MFileReader.ts rename to frontend/app/player/_web/messages/MFileReader.ts diff --git a/frontend/app/player/MessageDistributor/messages/MStreamReader.ts b/frontend/app/player/_web/messages/MStreamReader.ts similarity index 100% rename from frontend/app/player/MessageDistributor/messages/MStreamReader.ts rename to frontend/app/player/_web/messages/MStreamReader.ts diff --git a/frontend/app/player/MessageDistributor/messages/PrimitiveReader.ts b/frontend/app/player/_web/messages/PrimitiveReader.ts similarity index 100% rename from frontend/app/player/MessageDistributor/messages/PrimitiveReader.ts rename to frontend/app/player/_web/messages/PrimitiveReader.ts diff --git a/frontend/app/player/MessageDistributor/messages/RawMessageReader.ts b/frontend/app/player/_web/messages/RawMessageReader.ts similarity index 100% rename from frontend/app/player/MessageDistributor/messages/RawMessageReader.ts rename to frontend/app/player/_web/messages/RawMessageReader.ts diff --git a/frontend/app/player/MessageDistributor/messages/index.ts b/frontend/app/player/_web/messages/index.ts similarity index 100% rename from frontend/app/player/MessageDistributor/messages/index.ts rename to frontend/app/player/_web/messages/index.ts diff --git a/frontend/app/player/MessageDistributor/messages/message.ts b/frontend/app/player/_web/messages/message.ts similarity index 100% rename from frontend/app/player/MessageDistributor/messages/message.ts rename to frontend/app/player/_web/messages/message.ts diff --git a/frontend/app/player/MessageDistributor/messages/raw.ts b/frontend/app/player/_web/messages/raw.ts similarity index 100% rename from frontend/app/player/MessageDistributor/messages/raw.ts rename to frontend/app/player/_web/messages/raw.ts diff --git a/frontend/app/player/MessageDistributor/messages/timed.ts b/frontend/app/player/_web/messages/timed.ts similarity index 100% rename from frontend/app/player/MessageDistributor/messages/timed.ts rename to frontend/app/player/_web/messages/timed.ts diff --git a/frontend/app/player/MessageDistributor/messages/tracker-legacy.ts b/frontend/app/player/_web/messages/tracker-legacy.ts similarity index 100% rename from frontend/app/player/MessageDistributor/messages/tracker-legacy.ts rename to frontend/app/player/_web/messages/tracker-legacy.ts diff --git a/frontend/app/player/MessageDistributor/messages/tracker.ts b/frontend/app/player/_web/messages/tracker.ts similarity index 100% rename from frontend/app/player/MessageDistributor/messages/tracker.ts rename to frontend/app/player/_web/messages/tracker.ts diff --git a/frontend/app/player/MessageDistributor/messages/urlResolve.ts b/frontend/app/player/_web/messages/urlResolve.ts similarity index 100% rename from frontend/app/player/MessageDistributor/messages/urlResolve.ts rename to frontend/app/player/_web/messages/urlResolve.ts diff --git a/frontend/app/player/MessageDistributor/network/crypto.ts b/frontend/app/player/_web/network/crypto.ts similarity index 100% rename from frontend/app/player/MessageDistributor/network/crypto.ts rename to frontend/app/player/_web/network/crypto.ts diff --git a/frontend/app/player/MessageDistributor/network/loadFiles.ts b/frontend/app/player/_web/network/loadFiles.ts similarity index 100% rename from frontend/app/player/MessageDistributor/network/loadFiles.ts rename to frontend/app/player/_web/network/loadFiles.ts diff --git a/frontend/app/player/create.ts b/frontend/app/player/create.ts new file mode 100644 index 000000000..c42815eae --- /dev/null +++ b/frontend/app/player/create.ts @@ -0,0 +1,25 @@ +import SimpleStore from './_common/SimpleStore' +import type { Store } from './player/types' +import { State as MMState, INITIAL_STATE as MM_INITIAL_STATE } from './_web/MessageManager' +import { State as PState, INITIAL_STATE as PLAYER_INITIAL_STATE } from './player/Player' + +import WebPlayer from './_web/WebPlayer' + +export function createWebPlayer(session, config): [WebPlayer, Store] { + const store = new SimpleStore({ + ...PLAYER_INITIAL_STATE, + ...MM_INITIAL_STATE, + }) + const player = new WebPlayer(store, session, config, false) + return [player, store] +} + + +export function createLiveWebPlayer(session, config): [WebPlayer, Store] { + const store = new SimpleStore({ + ...PLAYER_INITIAL_STATE, + ...MM_INITIAL_STATE, + }) + const player = new WebPlayer(store, session, config, true) + return [player, store] +} \ No newline at end of file diff --git a/frontend/app/player/index.js b/frontend/app/player/index.js index c49d75294..c40e2f26c 100644 --- a/frontend/app/player/index.js +++ b/frontend/app/player/index.js @@ -1,4 +1,6 @@ -export * from './store'; -export * from './singletone'; -export * from './MessageDistributor/managers/AssistManager'; -export * from './MessageDistributor/managers/LocalStream'; \ No newline at end of file +export * from './_store'; +export * from './_web/assist/AssistManager'; +export * from './_web/assist/LocalStream'; +export * from './_singletone'; + +export * from './create'; \ No newline at end of file diff --git a/frontend/app/player/ios/ImagePlayer.js b/frontend/app/player/ios/ImagePlayer.js deleted file mode 100644 index 1dcf2b9cc..000000000 --- a/frontend/app/player/ios/ImagePlayer.js +++ /dev/null @@ -1,454 +0,0 @@ -import { io } from 'socket.io-client'; -import { makeAutoObservable, autorun } from 'mobx'; -import logger from 'App/logger'; -import { - createPlayerState, - createToolPanelState, - createToggleState, - PLAYING, - PAUSED, - COMPLETED, - SOCKET_ERROR, - - CRASHES, - LOGS, - NETWORK, - PERFORMANCE, - CUSTOM, - EVENTS, // last evemt +clicks -} from "./state"; -import { - createListState, - createScreenListState, -} from './lists'; -import Parser from './Parser'; -import PerformanceList from './PerformanceList'; - -const HIGHEST_SPEED = 3; - - -export default class ImagePlayer { - _screen = null - _wrapper = null - _socket = null - toolPanel = createToolPanelState() - fullscreen = createToggleState() - lists = { - [LOGS]: createListState(), - [NETWORK]: createListState(), - [CRASHES]: createListState(), - [EVENTS]: createListState(), - [CUSTOM]: createListState(), - [PERFORMANCE]: new PerformanceList(), - } - _clicks = createListState() - _screens = createScreenListState() - - constructor(session) { - this.state = createPlayerState({ - endTime: session.duration.valueOf(), - }); - //const canvas = document.createElement("canvas"); - // this._context = canvas.getContext('2d'); - // this._img = new Image(); - // this._img..onerror = function(e){ - // logger.log('Error during loading image:', e); - // }; - // wrapper.appendChild(this._img); - session.crashes.forEach(c => this.lists[CRASHES].append(c)); - session.events.forEach(e => this.lists[EVENTS].append(e)); - session.stackEvents.forEach(e => this.lists[CUSTOM].append(e)); - window.fetch(session.mobsUrl) - .then(r => r.arrayBuffer()) - .then(b => { - new Parser(new Uint8Array(b)).parseEach(m => { - m.time = m.timestamp - session.startedAt; - try { - if (m.tp === "ios_log") { - this.lists[LOGS].append(m); - } else if (m.tp === "ios_network_call") { - this.lists[NETWORK].append(m); - // } else if (m.tp === "ios_custom_event") { - // this.lists[CUSTOM].append(m); - } else if (m.tp === "ios_click_event") { - m.time -= 600; //for graphic initiation - this._clicks.append(m); - } else if (m.tp === "ios_performance_event") { - this.lists[PERFORMANCE].append(m); - } - } catch (e) { - logger.error(e); - } - }); - Object.values(this.lists).forEach(list => list.moveGetLast(0)); // In case of negative values - }) - - if (session.socket == null || typeof session.socket.jwt !== "string" || typeof session.socket.url !== "string") { - logger.error("No socket info found fpr session", session); - return - } - - const options = { - extraHeaders: {Authorization: `Bearer ${session.socket.jwt}`}, - reconnectionAttempts: 5, - //transports: ['websocket'], - } - - const socket = this._socket = io(session.socket.url, options); - socket.on("connect", () => { - logger.log("Socket Connected"); - }); - - socket.on('disconnect', (reason) => { - if (reason === 'io client disconnect') { - return; - } - logger.error("Disconnected. Reason: ", reason) - // if (reason === 'io server disconnect') { - // socket.connect(); - // } - }); - socket.on('connect_error', (e) => { - this.state.setState(SOCKET_ERROR); - logger.error(e) - }); - - socket.on('screen', (time, width, height, binary) => { - //logger.log("New Screen!", time, width, height, binary); - this._screens.insertScreen(time, width, height, binary); - }); - socket.on('buffered', (playTime) => { - if (playTime === this.state.time) { - this.state.setBufferingState(false); - } - logger.log("Play ack!", playTime); - }); - - let startPingInterval; - socket.on('start', () => { - logger.log("Started!"); - clearInterval(startPingInterval) - this.state.setBufferingState(true); - socket.emit("speed", this.state.speed); - this.play(); - }); - startPingInterval = setInterval(() => socket.emit("start"), 1000); - socket.emit("start"); - - window.addEventListener("resize", this.scale); - autorun(this.scale); - } - - _click - _getClickElement() { - if (this._click != null) { - return this._click; - } - const click = document.createElement('div'); - click.style.position = "absolute"; - click.style.background = "#ddd"; - click.style.border = "solid 4px #bbb"; - click.style.borderRadius = "50%"; - click.style.width = "32px"; - click.style.height = "32px"; - click.style.transformOrigin = "center"; - return this._click = click; - } - // More sufficient ways? - _animateClick({ x, y }) { - if (this._screen == null) { - return; - } - const click = this._getClickElement(); - if (click.parentElement == null) { - this._screen.appendChild(click); - } - click.style.transition = "none"; - click.style.left = `${x-18}px`; - click.style.top = `${y-18}px`; - click.style.transform = "scale(1)"; - click.style.opacity = "1"; - setTimeout(() => { - click.style.transition = "all ease-in .5s"; - click.style.transform = "scale(0)"; - click.style.opacity = "0"; - }, 0) - } - - _updateFrame({ image, width, height }) { - // const img = new Image(); - // img.onload = () => { - // this._context.drawImage(img); - // }; - // img.onerror = function(e){ - // logger.log('Error during loading image:', e); - // }; - // this._screen.style.backgroundImage = `url(${binaryToDataURL(binaryArray)})`; - this._canvas.getContext('2d').drawImage(image, 0, 0, this._canvas.width, this._canvas.height); - } - - _setTime(ts) { - ts = Math.max(Math.min(ts, this.state.endTime), 0); - this.state.setTime(ts); - Object.values(this.lists).forEach(list => list.moveGetLast(ts)); - const screen = this._screens.moveGetLast(ts); - if (screen != null) { - const { dataURL, width, height } = screen; - this.state.setSize(width, height); - //imagePromise.then(() => this._updateFrame({ image, width, height })); - //this._screen.style.backgroundImage = `url(${screen.dataURL})`; - screen.loadImage.then(() => this._screen.style.backgroundImage = `url(${screen.dataURL})`); - } - const lastClick = this._clicks.moveGetLast(ts); - if (lastClick != null && lastClick.time > ts - 600) { - this._animateClick(lastClick); - } - } - - attach({ wrapperId, screenId }) { - const screen = document.getElementById(screenId); - if (!screen) { - throw new Error(`ImagePlayer: No screen element found with ID "${screenId}" `); - } - const wrapper = document.getElementById(wrapperId); - if (!wrapper) { - throw new Error(`ImagePlayer: No wrapper element found with ID "${wrapperId}" `); - } - screen.style.backgroundSize = "contain"; - screen.style.backgroundPosition = "center"; - wrapper.style.position = "absolute"; - wrapper.style.transformOrigin = "left top"; - wrapper.style.top = "50%"; - wrapper.style.left = "50%"; - // const canvas = document.createElement('canvas'); - // canvas.style.width = "300px"; - // canvas.style.height = "600px"; - // screen.appendChild(canvas); - // this._canvas = canvas; - this._screen = screen; - this._wrapper = wrapper; - this.scale(); - } - - - get loading() { - return this.state.initializing; - } - - get buffering() { - return this.state.buffering; - } - - // get timeTravelDisabled() { - // return this.state.initializing; - // } - - get controlsDisabled() { - return this.state.initializing; //|| this.state.buffering; - } - - _animationFrameRequestId = null - _stopAnimation() { - cancelAnimationFrame(this._animationFrameRequestId); - } - _startAnimation() { - let prevTime = this.state.time; - let animationPrevTime = performance.now(); - const nextFrame = (animationCurrentTime) => { - const { - speed, - //skip, - //skipIntervals, - endTime, - playing, - buffering, - //live, - //livePlay, - //disconnected, - //messagesLoading, - //cssLoading, - } = this.state; - - const diffTime = !playing || buffering - ? 0 - : Math.max(animationCurrentTime - animationPrevTime, 0) * speed; - - let time = prevTime + diffTime; - - //const skipInterval = skip && skipIntervals.find(si => si.contains(time)); // TODO: good skip by messages - //if (skipInterval) time = skipInterval.end; - - //const fmt = this.getFirstMessageTime(); - //if (time < fmt) time = fmt; // ? - - //const lmt = this.getLastMessageTime(); - //if (livePlay && time < lmt) time = lmt; - // if (endTime < lmt) { - // update({ - // endTime: lmt, - // }); - // } - - prevTime = time; - animationPrevTime = animationCurrentTime; - - const completed = time >= endTime; - if (completed) { - this._setComplete(); - } else { - - // if (live && time > endTime) { - // update({ - // endTime: time, - // }); - // } - this._setTime(time); - this._animationFrameRequestId = requestAnimationFrame(nextFrame); - } - }; - this._animationFrameRequestId = requestAnimationFrame(nextFrame); - } - - - scale = () => { - const { height, width } = this.state; // should be before any return for mobx observing - if (this._wrapper === null) return; - const parent = this._wrapper.parentElement; - if (parent === null) return; - let s = 1; - const { offsetWidth, offsetHeight } = parent; - - s = Math.min(offsetWidth / width, (offsetHeight - 20) / height); - if (s > 1) { - s = 1; - } else { - s = Math.round(s * 1e3) / 1e3; - } - - this._wrapper.style.transform = `scale(${ s }) translate(-50%, -50%)`; - this._wrapper.style.width = width + 'px'; - this._wrapper.style.height = height + 'px'; - // this._canvas.style.width = width + 'px'; - // this._canvas.style.height = height + 'px'; - } - - _setComplete() { - this.state.setStatus(COMPLETED); - this._setTime(this.state.endTime); - if (this._socket != null) { - this._socket.emit("pause"); - } - } - - - _pause() { - this._stopAnimation(); - this.state.setStatus(PAUSED); - } - - pause = () => { - this._pause(); - if (this._socket != null) { - this._socket.emit("pause"); - } - } - - _play() { - if (!this.state.playing) { - this._startAnimation(); - } - this.state.setStatus(PLAYING); - } - play = () => { - this._play() - if (this._socket != null) { - this._socket.emit("resume"); - } - } - - _jump(ts) { - if (this.state.playing) { - this._stopAnimation(); - this._setTime(ts); - this._startAnimation(); - } else { - this._setTime(ts); - this.state.setStatus(PAUSED); // for the case when completed - } - } - jump = (ts) => { - ts = Math.round(ts); // Should be integer - this._jump(ts); - if (this._socket != null) { - this.state.setBufferingState(true); - console.log("Send play on jump!", ts) - this._socket.emit("jump", ts); - } - } - - togglePlay = () => { - if (this.state.playing) { - this.pause() - } else { - if (this.state.completed) { - //this.state.time = 0; - this.jump(0) - } - this.play() - } - } - backTenSeconds = () => { - this.jump(Math.max(this.state.time - 10000, 0)); - } - forthTenSeconds = () => { - this.jump(Math.min(this.state.time + 10000, this.state.endTime)); - } - - _setSpeed(speed) { - if (this._socket != null) { - this._socket.emit("speed", speed); - } - this.state.setSpeed(speed) - } - - toggleSpeed = () => { - const speed = this.state.speed; - this._setSpeed(speed < HIGHEST_SPEED ? speed + 1 : 1); - } - - speedUp = () => { - const speed = this.state.speed; - this._setSpeed(Math.min(HIGHEST_SPEED, speed + 1)); - } - - speedDown = () => { - const speed = this.state.speed; - this._setSpeed(Math.max(1, speed - 1)); - } - - togglePanel = (key) => { - this.toolPanel.toggle(key); - setTimeout(() => this.scale(), 0); - } - - closePanel = () => { - this.toolPanel.close(); - setTimeout(() => this.scale(), 0); - } - - toggleFullscreen = (flag = true) => { - this.fullscreen.toggle(flag); - setTimeout(() => this.scale(), 0); - } - - - clean() { - this._stopAnimation(); - if (this._socket != null) { - //this._socket.emit("close"); - this._socket.close(); - } - this._screens.clean(); - } -} - diff --git a/frontend/app/player/ios/Parser.ts b/frontend/app/player/ios/Parser.ts deleted file mode 100644 index 15b750df6..000000000 --- a/frontend/app/player/ios/Parser.ts +++ /dev/null @@ -1,34 +0,0 @@ -import RawMessageReader from '../MessageDistributor/messages/RawMessageReader'; - - -export default class Parser { - private reader: RawMessageReader - private error: boolean = false - constructor(byteArray) { - this.reader = new RawMessageReader(byteArray) - } - - parseEach(cb) { - while (this.hasNext()) { - const msg = this.next(); - if (msg !== null) { - cb(msg); - } - } - } - - hasNext() { - return !this.error && this.reader.hasNextByte(); - } - - next() { - try { - return this.reader.readMessage() - } catch(e) { - console.warn(e) - this.error = true - return null - } - } - -} \ No newline at end of file diff --git a/frontend/app/player/ios/PerformanceList.js b/frontend/app/player/ios/PerformanceList.js deleted file mode 100644 index 47d7918f3..000000000 --- a/frontend/app/player/ios/PerformanceList.js +++ /dev/null @@ -1,73 +0,0 @@ -import { - createListState, -} from './lists'; - -const MIN_INTERVAL = 500; - - -const NAME_MAP = { - "mainThreadCPU": "cpu", - "batteryLevel": "battery", - "memoryUsage": "memory", -} - -export default class PerformanceList { - _list = createListState() - availability = { - cpu: false, - memory: false, - battery: false, - } - - get list() { - return this._list.list; - } - - get count() { - return this._list.count; - } - - moveGetLast(t) { - this._list.moveGetLast(t); - } - - append(m) { - if (!["mainThreadCPU", "memoryUsage", "batteryLevel", "thermalState", "activeProcessorCount", "isLowPowerModeEnabled"].includes(m.name)) { - return; - } - - let lastPoint = Object.assign({ time: 0, cpu: null, battery: null, memory: null }, this._list.last); - if (this._list.length === 0) { - this._list.append(lastPoint); - } - - if (NAME_MAP[m.name] != null) { - this.availability[ NAME_MAP[m.name] ] = true; - if (lastPoint[NAME_MAP[m.name]] === null) { - this._list.forEach(p => p[NAME_MAP[m.name]] = m.value); - lastPoint[NAME_MAP[m.name]] = m.value; - } - } - - - const newPoint = Object.assign({}, lastPoint, { - time: m.time, - [ NAME_MAP[m.name] || m.name ]: m.value, - }); - - const dif = m.time - lastPoint.time; - const insertCount = Math.floor(dif/MIN_INTERVAL); - for (let i = 0; i < insertCount; i++){ - const evalValue = (key) => lastPoint[key] + Math.floor((newPoint[key]-lastPoint[key])/insertCount*(i + 1)) - this._list.append({ - ...lastPoint, - time: evalValue("time"), - cpu: evalValue("cpu") + (Math.floor(5*Math.random())-2), - battery: evalValue("battery"), - memory: evalValue("memory")*(1 + (0.1*Math.random() - 0.05)), - }); - } - - this._list.append(newPoint); - } -} \ No newline at end of file diff --git a/frontend/app/player/ios/ScreenList.ts b/frontend/app/player/ios/ScreenList.ts deleted file mode 100644 index ef6d1e73f..000000000 --- a/frontend/app/player/ios/ScreenList.ts +++ /dev/null @@ -1,57 +0,0 @@ -import ListWalker from '../MessageDistributor/managers/ListWalker'; - -//URL.revokeObjectURL() !! -function binaryToDataURL(arrayBuffer){ - var blob = new Blob([new Uint8Array(arrayBuffer)], {'type' : 'image/jpeg'}); - return URL.createObjectURL(blob); -} - -function prepareImage(width, height, arrayBuffer) { - const dataURL = binaryToDataURL(arrayBuffer); - return { - loadImage: new Promise(resolve => { - const img = new Image(); - img.onload = function() { - //URL.revokeObjectURL(this.src); - resolve(img); - }; - img.src = dataURL; - }).then(), - dataURL, - }; -} - -export default class ScreenList { - _walker = new ListWalker(); - _insertUnique(m) { - let p = this._walker._list.length; - while (p > 0 && this._walker._list[ p - 1 ].time > m.time) { - p--; - } - if (p > 0 && this._walker._list[ p - 1 ].time === m.time) { - return; - } - this._walker._list.splice(p, 0, m); - } - - moveGetLast(time) { - return this._walker.moveGetLast(time); - } - - insertScreen(time, width, height, arrayBuffer): void { - this._insertUnique({ - time, - width, - height, - ...prepareImage(width, height, arrayBuffer), - //image: new ImageData(new Uint8ClampedArray(arrayBuffer), width, height), - // dataURL: binaryToDataURL(arrayBuffer) - }); - } - - clean() { - this._walker.forEach(m => { - URL.revokeObjectURL(m.dataURL); - }); - } -} \ No newline at end of file diff --git a/frontend/app/player/ios/lists.js b/frontend/app/player/ios/lists.js deleted file mode 100644 index b0ddb9d0d..000000000 --- a/frontend/app/player/ios/lists.js +++ /dev/null @@ -1,12 +0,0 @@ -import { makeAutoObservable } from "mobx" -import ListWalker from '../MessageDistributor/managers/ListWalker'; - -import ScreenList from './ScreenList'; - -export function createListState(list) { - return makeAutoObservable(new ListWalker(list)); -} - -export function createScreenListState() { - return makeAutoObservable(new ScreenList()); -} \ No newline at end of file diff --git a/frontend/app/player/ios/state.js b/frontend/app/player/ios/state.js deleted file mode 100644 index 766ba08fe..000000000 --- a/frontend/app/player/ios/state.js +++ /dev/null @@ -1,112 +0,0 @@ -import { makeAutoObservable } from "mobx" - -//configure ({empceActions: true}) - -export const - NONE = 0, - CRASHES = 1, - NETWORK = 2, - LOGS = 3, - EVENTS = 4, - CUSTOM = 5, - PERFORMANCE = 6; - -export function createToolPanelState() { - return makeAutoObservable({ - key: NONE, - toggle(key) { // auto-bind?? - this.key = this.key === key ? NONE : key; - }, - close() { - this.key = NONE; - }, - }); -} - - -export function createToggleState() { - return makeAutoObservable({ - enabled: false, - toggle(flag) { - this.enabled = typeof flag === 'boolean' - ? flag - : !this.enabled; - }, - enable() { - this.enabled = true; - }, - disable() { - this.enabled = false; - }, - }); -} - -const SPEED_STORAGE_KEY = "__$player-speed$__"; -//const SKIP_STORAGE_KEY = "__$player-skip$__"; -//const initialSkip = !!localStorage.getItem(SKIP_STORAGE_KEY); - -export const - INITIALIZING = 0, - PLAYING = 1, - PAUSED = 2, - COMPLETED = 3, - SOCKET_ERROR = 5; - -export const - PORTRAIT = 1, - LANDSCAPE = 2; - -export function createPlayerState(state) { - const storedSpeed = +localStorage.getItem(SPEED_STORAGE_KEY); - const initialSpeed = [1,2,3].includes(storedSpeed) ? storedSpeed : 1; - - return makeAutoObservable({ - status: INITIALIZING, - _statusSaved: null, - setTime(t) { - this.time = t - }, - time: 0, - endTime: 0, - setStatus(status) { - this.status = status; - }, - get initializing() { - return this.status === INITIALIZING; - }, - get playing() { - return this.status === PLAYING; - }, - get completed() { - return this.status === COMPLETED; - }, - _buffering: false, - get buffering() { - return this._buffering; - }, - setBufferingState(flag = true) { - this._buffering = flag; - }, - speed: initialSpeed, - setSpeed(speed) { - localStorage.setItem(SPEED_STORAGE_KEY, speed); - this.speed = speed; - }, - width: 360, - height: 780, - orientation: PORTRAIT, - get orientationLandscape() { - return this.orientation === LANDSCAPE; - }, - setSize(width, height) { - if (height < 0 || width < 0) { - console.log("Player: wrong non-positive size") - return; - } - this.width = width; - this.height = height; - this.orientation = width > height ? LANDSCAPE : PORTRAIT; - }, - ...state, - }); -} diff --git a/frontend/app/player/lists/ListReader.js b/frontend/app/player/lists/ListReader.js deleted file mode 100644 index 641d3341a..000000000 --- a/frontend/app/player/lists/ListReader.js +++ /dev/null @@ -1,124 +0,0 @@ -export default class ListReader { - _callback; - _p = -1; - _list = []; - _offset = 0; - - constructor(callback = Function.prototype) { - if (typeof callback !== 'function') { - return console.error("List Reader: wrong constructor argument. `callback` must be a function."); - } - this._callback = callback; - } - - static checkItem(item) { - if(typeof item !== 'object' || item === null) { - console.error("List Reader: expected item to be not null object but got ", item); - return false; - } - if (typeof item.time !== 'number') { - console.error("List Reader: expected item to have number property 'time', ", item); - return false; - } - // if (typeof item.index !== 'number') { - // console.error("List Reader: expected item to have number property 'index', ", item); - // return false; - // } // future: All will have index - return true; - } - /* EXTENDABLE METHODS */ - _onIncrement() {} - _onDecrement() {} - _onStartTimeChange() {} - - inc() { - const item = this._list[ ++this._p ]; - this._onIncrement(item); - return item; - } - - dec() { - const item = this._list[ this._p-- ]; - this._onDecrement(item); - return item - } - - get _goToReturn() { - return { listNow: this.listNow }; - } - - goTo(time) { - const prevPointer = this._p; - while (!!this._list[ this._p + 1 ] && this._list[ this._p + 1 ].time <= time) { - this.inc(); - } - while (this._p >= 0 && this._list[ this._p ].time > time) { - this.dec(); - } - if (prevPointer !== this._p) { - //this._notify([ "listNow" ]); - return this._goToReturn; - } - } - - goToIndex(index) { // thinkaboutit - const prevPointer = this._p; - while (!!this._list[ this._p + 1 ] && - this._list[ this._p + 1 ].index <= index - ) { - this.inc(); - } - while (this._p >= 0 && this._list[ this._p ].index > index) { - this.dec(); - } - if (prevPointer !== this._p) { - //this._notify([ "listNow" ]); - return this._goToReturn; - } - } - - // happens rare MBTODO only in class ResourceListReader extends ListReaderWithRed - set startTime(time) { - const prevOffset = this._offset; - const prevPointer = this._p; - this._offset = this._list.findIndex(({ time, duration = 0 }) => time + duration >= time); // TODO: strict for duration rrrrr - this._p = Math.max(this._p, this._offset - 1); - if (prevOffset !== this._offset || prevPointer !== this._p) { - this._notify([ "listNow" ]); - } - this._onStartTimeChange(); - } - - get list() { - return this._list; - } - get count() { - return this._list.length; - } - get listNow() { - return this._list.slice(this._offset, this._p + 1); - } - - set list(_list) { - if (!Array.isArray(_list)) { - console.error("List Reader: wrong list value.", _list) - } - const valid = _list.every(this.constructor.checkItem); - if (!valid) return; - this._list = _list; // future: time + index sort - this._notify([ "list", "count" ]); - } - - append(item) { - if (!this.constructor.checkItem(item)) return; - this._list.push(item); // future: time + index sort - this._notify([ "count" ]); // list is the same by ref, CAREFULL - } - - _notify(propertyList) { - const changedState = {}; - propertyList.forEach(p => changedState[ p ] = this[ p ]); - this._callback(changedState); - } - -} \ No newline at end of file diff --git a/frontend/app/player/lists/ListReaderWithRed.js b/frontend/app/player/lists/ListReaderWithRed.js deleted file mode 100644 index 84da42138..000000000 --- a/frontend/app/player/lists/ListReaderWithRed.js +++ /dev/null @@ -1,48 +0,0 @@ -import ListReader from './ListReader'; - -export default class ListReaderWithRed extends ListReader { - _redCountNow = 0; - - static checkItem(item) { - const superCheckResult = super.checkItem(item); - if (typeof item.isRed !== 'function') { - console.error("List Reader With Red: expected item to have method 'isRed', ", item); - return false; - } - return superCheckResult; - } - - get _goToReturn() { - return { - listNow: this.listNow, - redCountNow: this.redCountNow, - } - } - - _onIncrement(item) { - if (item.isRed()) { - this._redCountNow++; - //this._notify([ "redCountNow" ]); - } - } - - _onDecrement(item) { - if (item.isRed()) { - this._redCountNow--; - //this._notify([ "redCountNow" ]); - } - } - - _onStartTimeChange() { - this._redCountNow = this._list - .slice(this._offset, this._p + 1) - .filter(item => item.isRed()) - .length; - this._notify([ "redCountNow" ]); - } - - get redCountNow() { - return this._redCountNow; - } - -} \ No newline at end of file diff --git a/frontend/app/player/lists/index.js b/frontend/app/player/lists/index.js deleted file mode 100644 index edae90b0c..000000000 --- a/frontend/app/player/lists/index.js +++ /dev/null @@ -1,68 +0,0 @@ -import ListReader from './ListReader'; -import ListReaderWithRed from './ListReaderWithRed'; -import { update as updateStore } from '../store'; - -const l = n => `${ n }List`; -const c = n => `${ n }Count`; -const ln = n => `${ n }ListNow`; -const rcn = n => `${ n }RedCountNow`; - -const entityNamesWithRed = [ "log", "resource", "fetch", "stack" ]; -const entityNamesSimple = [ "event", "profile" ]; -const entityNames = /*[ "redux" ].*/entityNamesWithRed.concat(entityNamesSimple); - -const is = {}; -entityNames.forEach(n => { - is[ l(n) ] = []; - is[ c(n) ] = 0; - is[ ln(n) ] = []; - if (entityNamesWithRed.includes(n)) { - is[ rcn(n) ] = 0; - } -}); -//is["reduxState"] = {}; -//is["reduxFinalStates"] = []; - - -const createCallback = n => { - const entityfy = s => `${ n }${ s[ 0 ].toUpperCase() }${ s.slice(1) }`; - return state => { - if (!state) return; - const namedState = {}; - Object.keys(state).forEach(key => { - namedState[ entityfy(key) ] = state[ key ]; - }); - return updateStore(namedState); - } -} - -let readers = null; - -export function init(lists) { - readers = {}; - entityNamesSimple.forEach(n => readers[ n ] = new ListReader(createCallback(n))); - entityNamesWithRed.forEach(n => readers[ n ] = new ListReaderWithRed(createCallback(n))); - - entityNames.forEach(n => readers[ n ].list = lists[ n ] || []); -} -export function append(name, item) { - readers[ name ].append(item); -} -export function setStartTime(time) { - readers.resource.startTime = time; -} -const byTimeNames = [ "event", "stack" ]; // TEMP -const byIndexNames = entityNames.filter(n => !byTimeNames.includes(n)); -export function goTo(time, index) { - if (readers === null) return; - if (typeof index === 'number') { - byTimeNames.forEach(n => readers[ n ] && readers[ n ]._callback(readers[ n ].goTo(time))); - byIndexNames.forEach(n => readers[ n ] && readers[ n ]._callback(readers[ n ].goToIndex(index))); - } else { - entityNames.forEach(n => readers[ n ] && readers[ n ]._callback(readers[ n ].goTo(time))); - } -} -export function clean() { - entityNames.forEach(n => delete readers[ n ]); -} -export const INITIAL_STATE = is; diff --git a/frontend/app/player/player/Animator.ts b/frontend/app/player/player/Animator.ts new file mode 100644 index 000000000..e8b8b7279 --- /dev/null +++ b/frontend/app/player/player/Animator.ts @@ -0,0 +1,172 @@ +import type { Store, Mover, Interval } from './types'; +import * as localStorage from './localStorage'; + +const fps = 60 +const performance: { now: () => number } = window.performance || { now: Date.now.bind(Date) } +const requestAnimationFrame: typeof window.requestAnimationFrame = + window.requestAnimationFrame || + // @ts-ignore + window.webkitRequestAnimationFrame || + // @ts-ignore + window.mozRequestAnimationFrame || + // @ts-ignore + window.oRequestAnimationFrame || + // @ts-ignore + window.msRequestAnimationFrame || + (callback => window.setTimeout(() => { callback(performance.now()) }, 1000 / fps)) +const cancelAnimationFrame = + window.cancelAnimationFrame || + // @ts-ignore + window.mozCancelAnimationFrame || + window.clearTimeout + + +export interface SetState { + time: number + playing: boolean + completed: boolean + endTime: number + live: boolean + livePlay: boolean +} + +export interface GetState extends SetState { + skip: boolean + speed: number + skipIntervals: Interval[] + lastMessageTime: number + ready: boolean +} + +export const INITIAL_STATE: SetState = { + time: 0, + playing: false, + completed: false, + endTime: 0, + live: false, + livePlay: false, +} as const + + +export default class Animator { + private animationFrameRequestId: number = 0 + + constructor(private state: Store, private mm: Mover) {} + + private setTime(time: number) { + this.state.update({ + time, + completed: false, + }) + this.mm.move(time) + } + + private startAnimation() { + let prevTime = this.state.get().time + let animationPrevTime = performance.now() + + const frameHandler = (animationCurrentTime: number) => { + const { + speed, + skip, + skipIntervals, + endTime, + live, + livePlay, + ready, // = messagesLoading || cssLoading || disconnected + lastMessageTime, // should be updated + } = this.state.get() + + const diffTime = !ready + ? 0 + : Math.max(animationCurrentTime - animationPrevTime, 0) * (live ? 1 : speed) + + let time = prevTime + diffTime + + const skipInterval = skip && skipIntervals.find(si => si.contains(time)) // TODO: good skip by messages + if (skipInterval) time = skipInterval.end + + if (time < 0) { time = 0 } // ? + //const fmt = getFirstMessageTime(); + //if (time < fmt) time = fmt; // ? + + + if (livePlay && time < lastMessageTime) { time = lastMessageTime } + if (endTime < lastMessageTime) { + this.state.update({ + endTime: lastMessageTime, + }) + } + + prevTime = time + animationPrevTime = animationCurrentTime + + const completed = !live && time >= endTime + if (completed) { + this.setTime(endTime) + return this.state.update({ + playing: false, + completed: true, + }) + } + + if (live && time > endTime) { + this.state.update({ + endTime: time, + }) + } + this.setTime(time) + this.animationFrameRequestId = requestAnimationFrame(frameHandler) + } + this.animationFrameRequestId = requestAnimationFrame(frameHandler) + } + + play() { + cancelAnimationFrame(this.animationFrameRequestId) + this.state.update({ playing: true }) + this.startAnimation() + } + + pause() { + cancelAnimationFrame(this.animationFrameRequestId) + this.state.update({ playing: false }) + } + + togglePlay() { + const { playing, completed } = this.state.get() + if (playing) { + this.pause() + } else if (completed) { + this.setTime(0) + this.play() + } else { + this.play() + } + } + + // jump by index? + jump(time: number) { + const { live } = this.state.get() + if (live) return + + if (this.state.get().playing) { + cancelAnimationFrame(this.animationFrameRequestId) + this.setTime(time) + this.startAnimation() + this.state.update({ livePlay: time === this.state.get().endTime }) + } else { + this.setTime(time) + this.state.update({ livePlay: time === this.state.get().endTime }) + } + } + + // TODO: clearify logic of live time-travel + jumpToLive() { + cancelAnimationFrame(this.animationFrameRequestId) + this.setTime(this.state.get().endTime) + this.startAnimation() + this.state.update({ livePlay: true }) + } + + +} \ No newline at end of file diff --git a/frontend/app/player/player/Player.ts b/frontend/app/player/player/Player.ts new file mode 100644 index 000000000..057d3752f --- /dev/null +++ b/frontend/app/player/player/Player.ts @@ -0,0 +1,125 @@ +import * as typedLocalStorage from './localStorage'; + +import type { Mover, Cleaner, Store } from './types'; +import Animator from './Animator'; +import { INITIAL_STATE as ANIMATOR_INITIAL_STATE } from './Animator'; +import type { GetState as AnimatorGetState, SetState as AnimatorSetState } from './Animator'; + + +/* == separate this == */ +const HIGHEST_SPEED = 16 +const SPEED_STORAGE_KEY = "__$player-speed$__" +const SKIP_STORAGE_KEY = "__$player-skip$__" +const SKIP_TO_ISSUE_STORAGE_KEY = "__$session-skipToIssue$__" +const AUTOPLAY_STORAGE_KEY = "__$player-autoplay$__" +const SHOW_EVENTS_STORAGE_KEY = "__$player-show-events$__" +const storedSpeed: number = typedLocalStorage.number(SPEED_STORAGE_KEY) +const initialSpeed = [1, 2, 4, 8, 16].includes(storedSpeed) ? storedSpeed : 1 +const initialSkip = typedLocalStorage.boolean(SKIP_STORAGE_KEY) +const initialSkipToIssue = typedLocalStorage.boolean(SKIP_TO_ISSUE_STORAGE_KEY) +const initialAutoplay = typedLocalStorage.boolean(AUTOPLAY_STORAGE_KEY) +const initialShowEvents = typedLocalStorage.boolean(SHOW_EVENTS_STORAGE_KEY) +export const INITIAL_STATE = { + ...ANIMATOR_INITIAL_STATE, + + skipToIssue: initialSkipToIssue, + showEvents: initialShowEvents, + + autoplay: initialAutoplay, + skip: initialSkip, + speed: initialSpeed, +} +export type State = typeof INITIAL_STATE & AnimatorGetState +/* == */ + +export default class Player extends Animator { + constructor(private pState: Store, private manager: Mover & Cleaner) { + super(pState, manager) + + // Autoplay + if (pState.get().autoplay) { + let autoPlay = true; + document.addEventListener("visibilitychange", () => { + if (document.hidden) { + const { playing } = pState.get(); + autoPlay = playing + if (playing) { + this.pause(); + } + } else if (autoPlay) { + this.play(); + } + }) + + if (!document.hidden) { + this.play(); + } + } + } + + + + /* === TODO: incapsulate in LSCache === */ + + toggleAutoplay() { + const autoplay = !this.pState.get().autoplay + localStorage.setItem(AUTOPLAY_STORAGE_KEY, `${autoplay}`); + this.pState.update({ autoplay }) + } + + // Shouldn't it be in the react part as a fully (with localStorage-cache react hook)? + toggleEvents() { + const showEvents = !this.pState.get().showEvents + localStorage.setItem(SHOW_EVENTS_STORAGE_KEY, `${showEvents}`); + this.pState.update({ showEvents }) + } + + // move to React part? + toggleSkipToIssue() { + const skipToIssue = !this.pState.get().skipToIssue + localStorage.setItem(SKIP_TO_ISSUE_STORAGE_KEY, `${skipToIssue}`); + this.pState.update({ skipToIssue }) + } + + toggleSkip() { + const skip = !this.pState.get().skip + localStorage.setItem(SKIP_STORAGE_KEY, `${skip}`); + this.pState.update({ skip }) + } + private updateSpeed(speed: number) { + localStorage.setItem(SPEED_STORAGE_KEY, `${speed}`); + this.pState.update({ speed }) + } + + toggleSpeed() { + const { speed } = this.pState.get() + this.updateSpeed(speed < HIGHEST_SPEED ? speed * 2 : 1) + } + + speedUp() { + const { speed } = this.pState.get() + this.updateSpeed(Math.min(HIGHEST_SPEED, speed * 2)) + } + + speedDown() { + const { speed } = this.pState.get() + this.updateSpeed(Math.max(1, speed / 2)) + } + /* === === */ + + // TODO: move theese to React hooks + // injectNotes(notes: Note[]) { + // update({ notes }) + // } + // filterOutNote(noteId: number) { + // const { notes } = getState() + // update({ notes: notes.filter((note: Note) => note.noteId !== noteId) }) + // } + + + clean() { + this.pause() + this.manager.clean() + } + +} \ No newline at end of file diff --git a/frontend/app/player/player/_LSCache.ts b/frontend/app/player/player/_LSCache.ts new file mode 100644 index 000000000..beb48af46 --- /dev/null +++ b/frontend/app/player/player/_LSCache.ts @@ -0,0 +1,63 @@ +import * as lstore from './localStorage' + +import type { SimpleState } from './PlayerState' + +const SPEED_STORAGE_KEY = "__$player-speed$__"; +const SKIP_STORAGE_KEY = "__$player-skip$__"; +const SKIP_TO_ISSUE_STORAGE_KEY = "__$session-skipToIssue$__"; +const AUTOPLAY_STORAGE_KEY = "__$player-autoplay$__"; +const SHOW_EVENTS_STORAGE_KEY = "__$player-show-events$__"; + + +const storedSpeed = lstore.number(SPEED_STORAGE_KEY, 1) +const initialSpeed = [1,2,4,8,16].includes(storedSpeed) ? storedSpeed : 1; +const initialSkip = lstore.boolean(SKIP_STORAGE_KEY) +const initialSkipToIssue = lstore.boolean(SKIP_TO_ISSUE_STORAGE_KEY) +const initialAutoplay = lstore.boolean(AUTOPLAY_STORAGE_KEY) +const initialShowEvents = lstore.boolean(SHOW_EVENTS_STORAGE_KEY) + +export const INITIAL_STATE = { + skipToIssue: initialSkipToIssue, + autoplay: initialAutoplay, + showEvents: initialShowEvents, + skip: initialSkip, + speed: initialSpeed, +} + +const KEY_MAP = { + speed: SPEED_STORAGE_KEY, + skip: SKIP_STORAGE_KEY, + skipToIssue: SKIP_TO_ISSUE_STORAGE_KEY, + autoplay: AUTOPLAY_STORAGE_KEY, + showEvents: SHOW_EVENTS_STORAGE_KEY, +} + +type KeysOfBoolean = keyof T & keyof { [ K in keyof T as T[K] extends boolean ? K : never ] : K }; + +type Entries = { + [K in keyof T]: [K, T[K]]; +}[keyof T][]; + +export default class LSCache { + constructor(private state: SimpleState, private keyMap: Record, string>) { + } + update(newState: Partial) { + for (let [k, v] of Object.entries(newState) as Entries>) { + if (k in this.keyMap) { + // @ts-ignore TODO: nice typing + //lstore[typeof v](this.keyMap[k], v) + localStorage.setItem(this.keyMap[k], String(v)) + } + } + this.state.update(newState) + } + toggle(key: KeysOfBoolean) { + // @ts-ignore TODO: nice typing + this.update({ + [key]: !this.get()[key] + }) + } + get() { + return this.state.get() + } +} \ No newline at end of file diff --git a/frontend/app/player/player/localStorage.ts b/frontend/app/player/player/localStorage.ts new file mode 100644 index 000000000..03bba1d77 --- /dev/null +++ b/frontend/app/player/player/localStorage.ts @@ -0,0 +1,19 @@ + +export function number(key: string, dflt = 0): number { + const stVal = localStorage.getItem(key) + if (stVal === null) { + return dflt + } + const val = parseInt(stVal) + if (isNaN(val)) { + return dflt + } + return val +} + +export function boolean(key: string, dflt = false): boolean { + return localStorage.getItem(key) === "true" +} +export function string(key: string, dflt = ''): string { + return localStorage.getItem(key) || '' +} \ No newline at end of file diff --git a/frontend/app/player/player/types.ts b/frontend/app/player/player/types.ts new file mode 100644 index 000000000..2ad5032ed --- /dev/null +++ b/frontend/app/player/player/types.ts @@ -0,0 +1,21 @@ + +export interface Mover { + move(time: number): void +} + +export interface Cleaner { + clean(): void +} + +export interface Interval { + contains(t: number): boolean + start: number + end: number +} + +export interface Store { + get(): G + update(state: Partial): void +} + + From 291fe0d4094a819afe1db06a0c7cea6b69ed691e Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Tue, 15 Nov 2022 22:00:49 +0100 Subject: [PATCH 002/252] fix(frontend/player): export WebPlayer type --- frontend/app/player/index.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/app/player/index.js b/frontend/app/player/index.js index c40e2f26c..67d56e19c 100644 --- a/frontend/app/player/index.js +++ b/frontend/app/player/index.js @@ -1,6 +1,7 @@ -export * from './_store'; export * from './_web/assist/AssistManager'; export * from './_web/assist/LocalStream'; -export * from './_singletone'; +export * from './_web/WebPlayer'; +export * from './create'; -export * from './create'; \ No newline at end of file +export * from './_store'; +export * from './_singletone'; From d085b3583d38fc858971483a122509680b86fe59 Mon Sep 17 00:00:00 2001 From: MauricioGarciaS <47052044+MauricioGarciaS@users.noreply.github.com> Date: Thu, 17 Nov 2022 16:34:45 +0100 Subject: [PATCH 003/252] API for signals in chalice ee, added folder for recommendation service --- ee/api/chalicelib/core/signals.py | 16 ++++ ee/api/routers/core_dynamic.py | 12 ++- ee/api/schemas_ee.py | 8 ++ ee/recommendation/Dockerfile | 24 ++++++ ee/recommendation/api.py | 7 ++ ee/recommendation/recommendation.py | 109 ++++++++++++++++++++++++++++ ee/recommendation/requirements.txt | 22 ++++++ ee/recommendation/run.sh | 6 ++ 8 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 ee/api/chalicelib/core/signals.py create mode 100644 ee/recommendation/Dockerfile create mode 100644 ee/recommendation/api.py create mode 100644 ee/recommendation/recommendation.py create mode 100644 ee/recommendation/requirements.txt create mode 100644 ee/recommendation/run.sh diff --git a/ee/api/chalicelib/core/signals.py b/ee/api/chalicelib/core/signals.py new file mode 100644 index 000000000..1c8f76a62 --- /dev/null +++ b/ee/api/chalicelib/core/signals.py @@ -0,0 +1,16 @@ +import json + +import schemas_ee +from chalicelib.utils import helper +from chalicelib.utils import pg_client + + +def handle_frontend_signals(project_id: int, user_id: str, data: schemas_ee.SignalsSchema): + res = {'errors': 'query not executed'} + insights_query = """INSERT INTO public.frontend_signals VALUES ({project_id}, {user_id}, {timestamp}, {action}, {source}, {category}, {data})""" + with pg_client.PostgresClient() as conn: + query = conn.mogrify(insights_query, {project_id=project_id, user_id=user_id, timestamp=data['timestamp'], source=data['source'], + category=data['category'], data=json.dumps(data['data'])}) + conn.execute(query) + res = helper.dict_to_camel_case(conn.fetchone()) + return res diff --git a/ee/api/routers/core_dynamic.py b/ee/api/routers/core_dynamic.py index fb24aec96..a1e9734c4 100644 --- a/ee/api/routers/core_dynamic.py +++ b/ee/api/routers/core_dynamic.py @@ -8,7 +8,7 @@ import schemas import schemas_ee from chalicelib.core import sessions, assist, heatmaps, sessions_favorite, sessions_assignments, errors, errors_viewed, \ errors_favorite, sessions_notes -from chalicelib.core import sessions_viewed +from chalicelib.core import sessions_viewed, signals from chalicelib.core import tenants, users, projects, license from chalicelib.core import webhook from chalicelib.core.collaboration_slack import Slack @@ -435,3 +435,13 @@ def get_all_notes(projectId: int, data: schemas.SearchNoteSchema = Body(...), if "errors" in data: return data return {'data': data} + + +@app.post('/{projectId}/signals', tags=['signals']) +def send_interactions(data: schemas_ee.SignalsSchema = Body(...), + context: schemas.CurrentContext = Depends(OR_context)): + data = signals.handle_frontend_signals(project_id=projectId, user_id=context.user_id, data=data) + + if "errors" in data: + return data + return {'data': data} diff --git a/ee/api/schemas_ee.py b/ee/api/schemas_ee.py index 9690eb334..1fcc14352 100644 --- a/ee/api/schemas_ee.py +++ b/ee/api/schemas_ee.py @@ -31,6 +31,14 @@ class RolePayloadSchema(BaseModel): alias_generator = schemas.attribute_to_camel_case +class SignalsSchema(BaseModel): + timestamp: int = Field(...) + action: str = Field(...) + source: str = Field(...) + category: str = Field(...) + data: dict = Field(default={}) + + class CreateMemberSchema(schemas.CreateMemberSchema): roleId: Optional[int] = Field(None) diff --git a/ee/recommendation/Dockerfile b/ee/recommendation/Dockerfile new file mode 100644 index 000000000..72407160b --- /dev/null +++ b/ee/recommendation/Dockerfile @@ -0,0 +1,24 @@ +FROM python:3.10 +WORKDIR service +COPY tmp/ch_client.py utils/ +COPY tmp/pg_client.py utils/ +COPY recommendation.py . +COPY requirements.txt . +RUN apt update +RUN apt-get install libxml2-dev libxmlsec1-dev libxmlsec1-openssl -y +RUN pip install --upgrade pip +RUN pip install -r requirements.txt +RUN touch utils/__init__.py +ENV pg_host=postgresql.db.svc.cluster.local \ + pg_port=5432 \ + pg_user='postgres' \ + pg_password='8ea158c722fab5976023' \ + pg_dbname='postgres' \ + PG_TIMEOUT=30 \ + ch_host=clickhouse-openreplay-clickhouse.db.svc.cluster.local \ + ch_port=9000 \ + ch_timeout=30 \ + ch_receive_timeout=40 +EXPOSE 8080 +RUN airflow db init +CMD airflow webserver --host 0.0.0.0 --port 8080 diff --git a/ee/recommendation/api.py b/ee/recommendation/api.py new file mode 100644 index 000000000..596e8d0b0 --- /dev/null +++ b/ee/recommendation/api.py @@ -0,0 +1,7 @@ +from fastapi import FastAPI + +app = FastAPI() + +@app.get('/') +def home(): + return '

This is a title

' diff --git a/ee/recommendation/recommendation.py b/ee/recommendation/recommendation.py new file mode 100644 index 000000000..f525bdc98 --- /dev/null +++ b/ee/recommendation/recommendation.py @@ -0,0 +1,109 @@ +from utils.ch_client import ClickHouseClient +from utils.pg_client import PostgresClient + +def get_features_clickhouse(**kwargs): + if 'limit' in kwargs: + limit = kwargs['limit'] + else: + limit = 500 + query = f"""SELECT session_id, project_id, user_id, events_count, errors_count, duration, country, issue_score, device_type, rage, jsexception, badrequest FROM ( + SELECT session_id, project_id, user_id, events_count, errors_count, duration, toInt8(user_country) as country, issue_score, toInt8(user_device_type) as device_type FROM experimental.sessions WHERE user_id IS NOT NULL) as T1 +INNER JOIN (SELECT session_id, project_id, sum(issue_type = 'click_rage') as rage, sum(issue_type = 'js_exception') as jsexception, sum(issue_type = 'bad_request') as badrequest FROM experimental.events WHERE event_type = 'ISSUE' AND session_id > 0 GROUP BY session_id, project_id LIMIT {limit}) as T2 +ON T1.session_id = T2.session_id AND T1.project_id = T2.project_id;""" + with ClickHouseClient() as conn: + res = conn.execute(query) + return res + + +def query_funnels(*kwargs): + # If public.funnel is empty + funnels_query = f"""SELECT project_id, user_id, filter FROM (SELECT project_id, user_id, metric_id FROM public.metrics WHERE metric_type='funnel' + ) as T1 LEFT JOIN (SELECT filter, metric_id FROM public.metric_series) as T2 ON T1.metric_id = T2.metric_id""" + # Else + # funnels_query = "SELECT project_id, user_id, filter FROM public.funnels" + + with PostgresClient() as conn: + conn.execute(funnels_query) + res = conn.fetchall() + return res + + +def query_metrics(*kwargs): + metrics_query = """SELECT metric_type, metric_of, metric_value, metric_format FROM public.metrics""" + with PostgresClient() as conn: + conn.execute(metrics_query) + res = conn.fetchall() + return res + + +def query_with_filters(*kwargs): + filters_query = """SELECT T1.metric_id as metric_id, project_id, name, metric_type, metric_of, filter FROM ( + SELECT metric_id, project_id, name, metric_type, metric_of FROM metric_series WHERE filter != '{}') as T1 INNER JOIN + (SELECT metric_id, filter FROM metrics) as T2 ON T1.metric_id = T2.metric_id""" + with PostgresClient() as conn: + conn.execute(filters_query) + res = conn.fetchall() + return res + + +def transform_funnel(project_id, user_id, data): + res = list() + for k in range(len(data)): + _tmp = data[k] + if _tmp['project_id'] != project_id or _tmp['user_id'] != user_id: + continue + else: + _tmp = _tmp['filter']['events'] + res.append(_tmp) + return res + + +def transform_with_filter(data, *kwargs): + res = list() + for k in range(len(data)): + _tmp = data[k] + jump = False + for _key in kwargs.keys(): + if data[_key] != kwargs[_key]: + jump = True + break + if jump: + continue + _type = data['metric_type'] + if _type == 'funnel': + res.append(['funnel', _tmp['filter']['events']]) + elif _type == 'timeseries': + res.append(['timeseries', _tmp['filter']['filters'], _tmp['filter']['events']]) + elif _type == 'table': + res.append(['table', _tmp['metric_of'], _tmp['filter']['events']]) + return res + + +def transform_data(): + pass + + +def transform(element): + key_ = element.pop('user_id') + secondary_key_ = element.pop('session_id') + context_ = element.pop('project_id') + features_ = element + del element + return {(key_, context_): {secondary_key_: list(features_.values())}} + + +def get_by_project(data, project_id): + head_ = [list(d.keys())[0][1] for d in data] + index_ = [k for k in range(len(head_)) if head_[k] == project_id] + return [data[k] for k in index_] + + +def get_by_user(data, user_id): + head_ = [list(d.keys())[0][0] for d in data] + index_ = [k for k in range(len(head_)) if head_[k] == user_id] + return [data[k] for k in index_] + + +if __name__ == '__main__': + data = get_features_clickhouse() + print('Data length:', len(data)) diff --git a/ee/recommendation/requirements.txt b/ee/recommendation/requirements.txt new file mode 100644 index 000000000..9d2a7872d --- /dev/null +++ b/ee/recommendation/requirements.txt @@ -0,0 +1,22 @@ +requests==2.28.1 +urllib3==1.26.12 +pyjwt==2.5.0 +psycopg2-binary==2.9.3 + +numpy==1.23.4 +threadpoolctl==3.1.0 +joblib==1.2.0 +scipy==1.9.3 +scikit-learn==1.1.3 + +apache-airflow==2.4.3 + +fastapi==0.85.0 +uvicorn[standard]==0.18.3 +python-decouple==3.6 +pydantic[email]==1.10.2 +apscheduler==3.9.1 + +clickhouse-driver==0.2.4 +python3-saml==1.14.0 +python-multipart==0.0.5 diff --git a/ee/recommendation/run.sh b/ee/recommendation/run.sh new file mode 100644 index 000000000..1754c4e99 --- /dev/null +++ b/ee/recommendation/run.sh @@ -0,0 +1,6 @@ +mkdir tmp +cp ../api/chalicelib/utils/ch_client.py tmp +cp ../../api/chalicelib/utils/pg_client.py tmp +docker build -t my_test . +rm tmp/*.py +rmdir tmp From d6fc2ef2eeb2b8656bd9a71014452eb58f0b25d9 Mon Sep 17 00:00:00 2001 From: MauricioGarciaS <47052044+MauricioGarciaS@users.noreply.github.com> Date: Fri, 18 Nov 2022 10:59:18 +0100 Subject: [PATCH 004/252] Fixed error in signals.py --- ee/api/chalicelib/core/signals.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ee/api/chalicelib/core/signals.py b/ee/api/chalicelib/core/signals.py index 1c8f76a62..7a2d9ebd2 100644 --- a/ee/api/chalicelib/core/signals.py +++ b/ee/api/chalicelib/core/signals.py @@ -9,8 +9,8 @@ def handle_frontend_signals(project_id: int, user_id: str, data: schemas_ee.Sign res = {'errors': 'query not executed'} insights_query = """INSERT INTO public.frontend_signals VALUES ({project_id}, {user_id}, {timestamp}, {action}, {source}, {category}, {data})""" with pg_client.PostgresClient() as conn: - query = conn.mogrify(insights_query, {project_id=project_id, user_id=user_id, timestamp=data['timestamp'], source=data['source'], - category=data['category'], data=json.dumps(data['data'])}) + query = conn.mogrify(insights_query, {project_id: project_id, user_id: user_id, timestamp: data['timestamp'], source: data['source'], + category: data['category'], data: json.dumps(data['data'])}) conn.execute(query) res = helper.dict_to_camel_case(conn.fetchone()) return res From c37af6acc27657ebaf005404bae1660821e14b55 Mon Sep 17 00:00:00 2001 From: MauricioGarciaS <47052044+MauricioGarciaS@users.noreply.github.com> Date: Fri, 18 Nov 2022 11:02:14 +0100 Subject: [PATCH 005/252] Fixed dict definition --- ee/api/chalicelib/core/signals.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ee/api/chalicelib/core/signals.py b/ee/api/chalicelib/core/signals.py index 7a2d9ebd2..add0d10af 100644 --- a/ee/api/chalicelib/core/signals.py +++ b/ee/api/chalicelib/core/signals.py @@ -9,8 +9,8 @@ def handle_frontend_signals(project_id: int, user_id: str, data: schemas_ee.Sign res = {'errors': 'query not executed'} insights_query = """INSERT INTO public.frontend_signals VALUES ({project_id}, {user_id}, {timestamp}, {action}, {source}, {category}, {data})""" with pg_client.PostgresClient() as conn: - query = conn.mogrify(insights_query, {project_id: project_id, user_id: user_id, timestamp: data['timestamp'], source: data['source'], - category: data['category'], data: json.dumps(data['data'])}) + query = conn.mogrify(insights_query, {'project_id': project_id, 'user_id': user_id, 'timestamp': data['timestamp'], 'source': data['source'], + 'category': data['category'], 'data': json.dumps(data['data'])}) conn.execute(query) res = helper.dict_to_camel_case(conn.fetchone()) return res From b5bfc32f388540e44c8c45cc6b5b30d2460ca8df Mon Sep 17 00:00:00 2001 From: MauricioGarciaS <47052044+MauricioGarciaS@users.noreply.github.com> Date: Fri, 18 Nov 2022 11:26:51 +0100 Subject: [PATCH 006/252] Testing queue method --- ee/api/chalicelib/core/signals.py | 2 +- ee/api/chalicelib/utils/queue.py | 2 ++ ee/recommendation/Dockerfile | 24 ------------------------ 3 files changed, 3 insertions(+), 25 deletions(-) create mode 100644 ee/api/chalicelib/utils/queue.py delete mode 100644 ee/recommendation/Dockerfile diff --git a/ee/api/chalicelib/core/signals.py b/ee/api/chalicelib/core/signals.py index add0d10af..20f3350aa 100644 --- a/ee/api/chalicelib/core/signals.py +++ b/ee/api/chalicelib/core/signals.py @@ -1,7 +1,7 @@ import json import schemas_ee -from chalicelib.utils import helper +from chalicelib.utils import helper, queue from chalicelib.utils import pg_client diff --git a/ee/api/chalicelib/utils/queue.py b/ee/api/chalicelib/utils/queue.py new file mode 100644 index 000000000..ac5f9c5a2 --- /dev/null +++ b/ee/api/chalicelib/utils/queue.py @@ -0,0 +1,2 @@ +import schemas +import schemas_ee diff --git a/ee/recommendation/Dockerfile b/ee/recommendation/Dockerfile deleted file mode 100644 index 72407160b..000000000 --- a/ee/recommendation/Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -FROM python:3.10 -WORKDIR service -COPY tmp/ch_client.py utils/ -COPY tmp/pg_client.py utils/ -COPY recommendation.py . -COPY requirements.txt . -RUN apt update -RUN apt-get install libxml2-dev libxmlsec1-dev libxmlsec1-openssl -y -RUN pip install --upgrade pip -RUN pip install -r requirements.txt -RUN touch utils/__init__.py -ENV pg_host=postgresql.db.svc.cluster.local \ - pg_port=5432 \ - pg_user='postgres' \ - pg_password='8ea158c722fab5976023' \ - pg_dbname='postgres' \ - PG_TIMEOUT=30 \ - ch_host=clickhouse-openreplay-clickhouse.db.svc.cluster.local \ - ch_port=9000 \ - ch_timeout=30 \ - ch_receive_timeout=40 -EXPOSE 8080 -RUN airflow db init -CMD airflow webserver --host 0.0.0.0 --port 8080 From 1945640ca7bce07198ea4f3a68365950cc97f63e Mon Sep 17 00:00:00 2001 From: MauricioGarciaS <47052044+MauricioGarciaS@users.noreply.github.com> Date: Fri, 18 Nov 2022 12:26:58 +0100 Subject: [PATCH 007/252] Added script to create table frontend_signales in PG --- .../db/init_dbs/postgresql/1.10.0/1.10.0.sql | 20 +++++++++++++++++++ .../db/init_dbs/postgresql/init_schema.sql | 15 +++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 ee/scripts/schema/db/init_dbs/postgresql/1.10.0/1.10.0.sql diff --git a/ee/scripts/schema/db/init_dbs/postgresql/1.10.0/1.10.0.sql b/ee/scripts/schema/db/init_dbs/postgresql/1.10.0/1.10.0.sql new file mode 100644 index 000000000..fa35caa33 --- /dev/null +++ b/ee/scripts/schema/db/init_dbs/postgresql/1.10.0/1.10.0.sql @@ -0,0 +1,20 @@ +BEGIN; +CREATE OR REPLACE FUNCTION openreplay_version() + RETURNS text AS +$$ +SELECT 'v1.10.0-ee' +$$ LANGUAGE sql IMMUTABLE; + +CREATE TABLE IF NOT EXISTS frontend_signals +( + project_id bigint NOT NULL, + user_id text NOT NULL, + timestamp bigint NOT NULL, + action text NOT NULL, + source text NOT NULL, + category text NOT NULL, + data json +); +CREATE INDEX IF NOT EXISTS frontend_signals_user_id_idx ON frontend_signals (user_id); + +COMMIT; diff --git a/ee/scripts/schema/db/init_dbs/postgresql/init_schema.sql b/ee/scripts/schema/db/init_dbs/postgresql/init_schema.sql index f486c731e..b7d8b0491 100644 --- a/ee/scripts/schema/db/init_dbs/postgresql/init_schema.sql +++ b/ee/scripts/schema/db/init_dbs/postgresql/init_schema.sql @@ -646,6 +646,19 @@ $$ CREATE INDEX IF NOT EXISTS user_favorite_sessions_user_id_session_id_idx ON user_favorite_sessions (user_id, session_id); + CREATE TABLE IF NOT EXISTS frontend_signals + ( + project_id bigint NOT NULL, + user_id text NOT NULL, + timestamp bigint NOT NULL, + action text NOT NULL, + source text NOT NULL, + category text NOT NULL, + data json + ); + CREATE INDEX IF NOT EXISTS frontend_signals_user_id_idx ON frontend_signals (user_id); + + CREATE TABLE IF NOT EXISTS assigned_sessions ( session_id bigint NOT NULL REFERENCES sessions (session_id) ON DELETE CASCADE, @@ -1491,4 +1504,4 @@ ON CONFLICT (predefined_key) DO UPDATE metric_type=excluded.metric_type, view_type=excluded.view_type; -COMMIT; \ No newline at end of file +COMMIT; From 9144606b0826eb4b8b6c1f8ba9b195679059825c Mon Sep 17 00:00:00 2001 From: MauricioGarciaS <47052044+MauricioGarciaS@users.noreply.github.com> Date: Mon, 21 Nov 2022 16:20:33 +0100 Subject: [PATCH 008/252] Adding method to handle frontend responses in batches --- ee/api/chalicelib/core/signals.py | 4 +- ee/api/chalicelib/utils/events_queue.py | 59 +++++++++++++++++++++++++ ee/api/chalicelib/utils/queue.py | 2 - ee/recommendation/api.py | 36 +++++++++++++++ ee/recommendation/requirements.txt | 1 + ee/recommendation/run.sh | 2 + 6 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 ee/api/chalicelib/utils/events_queue.py delete mode 100644 ee/api/chalicelib/utils/queue.py diff --git a/ee/api/chalicelib/core/signals.py b/ee/api/chalicelib/core/signals.py index 20f3350aa..702e4f2fa 100644 --- a/ee/api/chalicelib/core/signals.py +++ b/ee/api/chalicelib/core/signals.py @@ -1,13 +1,13 @@ import json import schemas_ee -from chalicelib.utils import helper, queue +from chalicelib.utils import helper from chalicelib.utils import pg_client def handle_frontend_signals(project_id: int, user_id: str, data: schemas_ee.SignalsSchema): res = {'errors': 'query not executed'} - insights_query = """INSERT INTO public.frontend_signals VALUES ({project_id}, {user_id}, {timestamp}, {action}, {source}, {category}, {data})""" + insights_query = """INSERT INTO public.frontend_signals VALUES (%(project_id)s, %(user_id)s, %(timestamp)s, %(action)s, %(source)s, %(category)s, %(data)s)""" with pg_client.PostgresClient() as conn: query = conn.mogrify(insights_query, {'project_id': project_id, 'user_id': user_id, 'timestamp': data['timestamp'], 'source': data['source'], 'category': data['category'], 'data': json.dumps(data['data'])}) diff --git a/ee/api/chalicelib/utils/events_queue.py b/ee/api/chalicelib/utils/events_queue.py new file mode 100644 index 000000000..8a002a8cf --- /dev/null +++ b/ee/api/chalicelib/utils/events_queue.py @@ -0,0 +1,59 @@ +import queue +import logging +# import threading + +# import schemas +# import schemas_ee + +#from utils import helper +from utils import pg_client + +global_queue = None + +class EventQueue(): + + def __init__(self, test=False, queue_max_length=5): + self.events = queue.Queue() + self.events.maxsize = queue_max_length + self.test = test + + def flush(self, conn): + events = list() + while not self.events.empty(): + events.append(self.events.get()) + # self.events.task_done() + if self.test: + print(events) + return 1 + _query = conn.mogrify("""INSERT INTO %(database)s.%(table)s (project_id, user_id, timestamp, action, source, category, data) VALUES %(events)s""", + {'database': 'public', 'table': 'frontend_signals', 'events': "(0, 'test', 0, 'action', 's', 'c', '{}')"}) + logging.info(_query) + res = 'done' + # res = conn.fetchone() + #res = helper.dict_to_camel_case(conn.fetchone()) + return res + + def force_flush(self): + if not self.events.empty(): + with pg_client.PostgreClient() as conn: + self.flush(conn) + + def put(self, element): + if self.events.full(): + with pg_client.PostgresClient() as conn: + self.flush(conn) + self.events.put(element) + self.events.task_done() + +async def init(test=False): + global global_queue + global_queue = EventQueue(test=test) + logging.info("> queue initialized") + +async def terminate(): + global global_queue + if global_queue is not None: + global_queue.force_flush() + logging.info('> queue fulshed') + + diff --git a/ee/api/chalicelib/utils/queue.py b/ee/api/chalicelib/utils/queue.py deleted file mode 100644 index ac5f9c5a2..000000000 --- a/ee/api/chalicelib/utils/queue.py +++ /dev/null @@ -1,2 +0,0 @@ -import schemas -import schemas_ee diff --git a/ee/recommendation/api.py b/ee/recommendation/api.py index 596e8d0b0..b2d47b26d 100644 --- a/ee/recommendation/api.py +++ b/ee/recommendation/api.py @@ -1,7 +1,43 @@ +import logging from fastapi import FastAPI +from fastapi_utils.tasks import repeat_every +from utils import events_queue +from utils import pg_client app = FastAPI() +first_boot=True + @app.get('/') def home(): return '

This is a title

' + + +@app.get('/value/{value}') +@app.put('/value/{value}') +def number(value: int): + logging.info(f'> {value} as input. Testing queue with pg') + events_queue.global_queue.put(value) + + +@app.on_event("startup") +@repeat_every(seconds=60*1) # every 5 mins +async def startup(): + global first_boot + if first_boot: + await pg_client.init() + await events_queue.init(test=False) + first_boot = False + else: + events_queue.global_queue.force_flush() + + +# @repeat_every(seconds=60*5) # 5 min +# def clean_up(): +# events_queue.force_flush() + + +@app.on_event("shutdown") +async def shutdown(): + await events_queue.terminate() + await pg_client.terminate() diff --git a/ee/recommendation/requirements.txt b/ee/recommendation/requirements.txt index 9d2a7872d..8f3f9e958 100644 --- a/ee/recommendation/requirements.txt +++ b/ee/recommendation/requirements.txt @@ -12,6 +12,7 @@ scikit-learn==1.1.3 apache-airflow==2.4.3 fastapi==0.85.0 +fastapi-utils uvicorn[standard]==0.18.3 python-decouple==3.6 pydantic[email]==1.10.2 diff --git a/ee/recommendation/run.sh b/ee/recommendation/run.sh index 1754c4e99..e5f70a5a4 100644 --- a/ee/recommendation/run.sh +++ b/ee/recommendation/run.sh @@ -1,6 +1,8 @@ mkdir tmp cp ../api/chalicelib/utils/ch_client.py tmp +cp ../api/chalicelib/utils/events_queue.py tmp cp ../../api/chalicelib/utils/pg_client.py tmp docker build -t my_test . rm tmp/*.py rmdir tmp +docker run -d -p 8080:8080 my_test From fd0d57cd7b19fc1d7e2f3f2d718527fa8d94189f Mon Sep 17 00:00:00 2001 From: MauricioGarciaS <47052044+MauricioGarciaS@users.noreply.github.com> Date: Mon, 21 Nov 2022 18:02:52 +0100 Subject: [PATCH 009/252] Fixed problem with projectId definition in signals --- ee/api/chalicelib/utils/events_queue.py | 2 +- ee/api/routers/core_dynamic.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ee/api/chalicelib/utils/events_queue.py b/ee/api/chalicelib/utils/events_queue.py index 8a002a8cf..a1a475d39 100644 --- a/ee/api/chalicelib/utils/events_queue.py +++ b/ee/api/chalicelib/utils/events_queue.py @@ -35,7 +35,7 @@ class EventQueue(): def force_flush(self): if not self.events.empty(): - with pg_client.PostgreClient() as conn: + with pg_client.PostgresClient() as conn: self.flush(conn) def put(self, element): diff --git a/ee/api/routers/core_dynamic.py b/ee/api/routers/core_dynamic.py index a1e9734c4..3250df394 100644 --- a/ee/api/routers/core_dynamic.py +++ b/ee/api/routers/core_dynamic.py @@ -438,7 +438,7 @@ def get_all_notes(projectId: int, data: schemas.SearchNoteSchema = Body(...), @app.post('/{projectId}/signals', tags=['signals']) -def send_interactions(data: schemas_ee.SignalsSchema = Body(...), +def send_interactions(projectId:int, data: schemas_ee.SignalsSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): data = signals.handle_frontend_signals(project_id=projectId, user_id=context.user_id, data=data) From fe2c514a0cd4bd52c6c5985de80b27a90506cce0 Mon Sep 17 00:00:00 2001 From: MauricioGarciaS <47052044+MauricioGarciaS@users.noreply.github.com> Date: Mon, 21 Nov 2022 18:45:39 +0100 Subject: [PATCH 010/252] Solved SignalSchema not subscriptable --- ee/api/chalicelib/core/signals.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ee/api/chalicelib/core/signals.py b/ee/api/chalicelib/core/signals.py index 702e4f2fa..d5be6859a 100644 --- a/ee/api/chalicelib/core/signals.py +++ b/ee/api/chalicelib/core/signals.py @@ -9,8 +9,8 @@ def handle_frontend_signals(project_id: int, user_id: str, data: schemas_ee.Sign res = {'errors': 'query not executed'} insights_query = """INSERT INTO public.frontend_signals VALUES (%(project_id)s, %(user_id)s, %(timestamp)s, %(action)s, %(source)s, %(category)s, %(data)s)""" with pg_client.PostgresClient() as conn: - query = conn.mogrify(insights_query, {'project_id': project_id, 'user_id': user_id, 'timestamp': data['timestamp'], 'source': data['source'], - 'category': data['category'], 'data': json.dumps(data['data'])}) + query = conn.mogrify(insights_query, {'project_id': project_id, 'user_id': user_id, 'timestamp': data.timestamp, 'source': data.source, + 'category': data.category, 'data': json.dumps(data.data)}) conn.execute(query) res = helper.dict_to_camel_case(conn.fetchone()) return res From 0dde0d04cd803d967edc523a1c5c1e4ea807c6ae Mon Sep 17 00:00:00 2001 From: MauricioGarciaS <47052044+MauricioGarciaS@users.noreply.github.com> Date: Mon, 21 Nov 2022 19:07:20 +0100 Subject: [PATCH 011/252] Added missing value in query string --- ee/api/chalicelib/core/signals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ee/api/chalicelib/core/signals.py b/ee/api/chalicelib/core/signals.py index d5be6859a..4ad1d640f 100644 --- a/ee/api/chalicelib/core/signals.py +++ b/ee/api/chalicelib/core/signals.py @@ -9,7 +9,7 @@ def handle_frontend_signals(project_id: int, user_id: str, data: schemas_ee.Sign res = {'errors': 'query not executed'} insights_query = """INSERT INTO public.frontend_signals VALUES (%(project_id)s, %(user_id)s, %(timestamp)s, %(action)s, %(source)s, %(category)s, %(data)s)""" with pg_client.PostgresClient() as conn: - query = conn.mogrify(insights_query, {'project_id': project_id, 'user_id': user_id, 'timestamp': data.timestamp, 'source': data.source, + query = conn.mogrify(insights_query, {'project_id': project_id, 'user_id': user_id, 'timestamp': data.timestamp, 'action': data.action, 'source': data.source, 'category': data.category, 'data': json.dumps(data.data)}) conn.execute(query) res = helper.dict_to_camel_case(conn.fetchone()) From 094721684a65f26581404d8f0a480e9ec5f4e9df Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Tue, 22 Nov 2022 11:33:27 +0100 Subject: [PATCH 012/252] refactor(frontend/player):ListWithMarks: add markedCount; import types fix --- frontend/app/player/_common/ListWalker.ts | 8 ++++---- .../app/player/_common/ListWalkerWithMarks.ts | 17 ++++++++++++++--- frontend/app/player/_common/SimpleStore.ts | 4 ++-- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/frontend/app/player/_common/ListWalker.ts b/frontend/app/player/_common/ListWalker.ts index 92f7585c8..3600f31da 100644 --- a/frontend/app/player/_common/ListWalker.ts +++ b/frontend/app/player/_common/ListWalker.ts @@ -1,4 +1,4 @@ -import type { Timed } from './messages/timed'; +import type { Timed } from '../_web/messages/timed'; export default class ListWalker { private p = 0 @@ -80,18 +80,18 @@ export default class ListWalker { } private hasNext() { - return this.p < this.length + return this.p < this.length } private hasPrev() { return this.p > 0 } protected moveNext(): T | null { - return this.hasNext() + return this.hasNext() ? this.list[ this.p++ ] : null } protected movePrev(): T | null { - return this.hasPrev() + return this.hasPrev() ? this.list[ --this.p ] : null } diff --git a/frontend/app/player/_common/ListWalkerWithMarks.ts b/frontend/app/player/_common/ListWalkerWithMarks.ts index b2a8f5d3d..10446d7e0 100644 --- a/frontend/app/player/_common/ListWalkerWithMarks.ts +++ b/frontend/app/player/_common/ListWalkerWithMarks.ts @@ -1,4 +1,4 @@ -import type { Timed } from './messages/timed'; +import type { Timed } from '../_web/messages/timed'; import ListWalker from './ListWalker' @@ -7,9 +7,17 @@ type CheckFn = (t: T) => boolean export default class ListWalkerWithMarks extends ListWalker { private _markCountNow: number = 0 - constructor(private isMarked: CheckFn, initialList?: T[]) { + private _markCount: number = 0 + constructor(private isMarked: CheckFn, initialList: T[] = []) { super(initialList) + this._markCount = initialList.reduce((n, item) => isMarked(item) ? n+1 : n, 0) } + + append(item: T) { + if (this.isMarked(item)) { this._markCount++ } + super.append(item) + } + protected moveNext() { const val = super.moveNext() if (val && this.isMarked(val)) { @@ -24,8 +32,11 @@ export default class ListWalkerWithMarks extends ListWalker } return val } - get markCountNow(): number { + get markedCountNow(): number { return this._markCountNow } + get markedCount(): number { + return this._markCount + } } \ No newline at end of file diff --git a/frontend/app/player/_common/SimpleStore.ts b/frontend/app/player/_common/SimpleStore.ts index 9e81a79cd..2b4a77658 100644 --- a/frontend/app/player/_common/SimpleStore.ts +++ b/frontend/app/player/_common/SimpleStore.ts @@ -1,8 +1,8 @@ -import { State } from './types' +import { Store } from '../player/types' // (not a type) -export default class SimpleSore implements State { +export default class SimpleSore implements Store { constructor(private state: G){} get(): G { return this.state From c5209efd879cb5ac3cbc03d4ce59f8d855252c92 Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Tue, 22 Nov 2022 12:06:31 +0100 Subject: [PATCH 013/252] refactor(frontend/player):inspector responsibility segregation; renamings, types --- frontend/app/player/_singletone.ts | 12 +- frontend/app/player/_store/duck.js | 4 +- .../app/player/_web/InspectorController.ts | 57 ++++ frontend/app/player/_web/Lists.ts | 2 +- frontend/app/player/_web/MessageManager.ts | 14 +- frontend/app/player/_web/Screen/BaseScreen.ts | 214 -------------- frontend/app/player/_web/Screen/Cursor.ts | 29 +- .../Screen/{Inspector.js => Inspector.ts} | 42 ++- frontend/app/player/_web/Screen/Marker.ts | 25 +- frontend/app/player/_web/Screen/Screen.ts | 278 +++++++++++++----- frontend/app/player/_web/Screen/index.js | 2 - frontend/app/player/_web/Screen/types.ts | 9 +- frontend/app/player/_web/WebLivePlayer.ts | 23 ++ frontend/app/player/_web/WebPlayer.ts | 61 ++-- .../app/player/_web/assist/AssistManager.ts | 9 +- .../player/_web/managers/ActivityManager.ts | 2 +- .../player/_web/managers/MouseMoveManager.ts | 39 ++- .../player/_web/managers/ReduxStateManager.ts | 53 ---- frontend/app/player/create.ts | 24 +- frontend/app/player/player/Animator.ts | 69 +++-- frontend/app/player/player/Player.ts | 26 +- frontend/app/player/player/types.ts | 4 +- 22 files changed, 476 insertions(+), 522 deletions(-) create mode 100644 frontend/app/player/_web/InspectorController.ts delete mode 100644 frontend/app/player/_web/Screen/BaseScreen.ts rename frontend/app/player/_web/Screen/{Inspector.js => Inspector.ts} (55%) delete mode 100644 frontend/app/player/_web/Screen/index.js create mode 100644 frontend/app/player/_web/WebLivePlayer.ts delete mode 100644 frontend/app/player/_web/managers/ReduxStateManager.ts diff --git a/frontend/app/player/_singletone.ts b/frontend/app/player/_singletone.ts index 71f29a7ee..c4e0d607b 100644 --- a/frontend/app/player/_singletone.ts +++ b/frontend/app/player/_singletone.ts @@ -1,15 +1,9 @@ import WebPlayer from './_web/WebPlayer'; import reduxStore, {update, cleanStore} from './_store'; -import { State as MMState, INITIAL_STATE as MM_INITIAL_STATE } from './_web/MessageManager' -import { State as PState, INITIAL_STATE as PLAYER_INITIAL_STATE } from './player/Player' -import { Store } from './player/types' - - -const INIT_STATE = { - ...MM_INITIAL_STATE, - ...PLAYER_INITIAL_STATE, -} +import type { State as MMState } from './_web/MessageManager' +import type { State as PState } from './player/Player' +import type { Store } from './player/types' const myStore: Store = { diff --git a/frontend/app/player/_store/duck.js b/frontend/app/player/_store/duck.js index bcede3b32..6cf861ade 100644 --- a/frontend/app/player/_store/duck.js +++ b/frontend/app/player/_store/duck.js @@ -1,7 +1,7 @@ import { applyChange, revertChange } from 'deep-diff'; import { INITIAL_STATE as MM_INITIAL_STATE } from '../_web/MessageManager' -import { INITIAL_STATE as PLAYER_INITIAL_STATE } from '../player/Player' +import Player from '../player/Player' const UPDATE = 'player/UPDATE'; const CLEAN = 'player/CLEAN'; @@ -9,7 +9,7 @@ const REDUX = 'player/REDUX'; const resetState = { ...MM_INITIAL_STATE, - ...PLAYER_INITIAL_STATE, + ...Player.INITIAL_STATE, initialized: false, }; diff --git a/frontend/app/player/_web/InspectorController.ts b/frontend/app/player/_web/InspectorController.ts new file mode 100644 index 000000000..a78bad006 --- /dev/null +++ b/frontend/app/player/_web/InspectorController.ts @@ -0,0 +1,57 @@ +import Marker from './Screen/Marker' +import Inspector from './Screen/Inspector' +import Screen from './Screen/Screen' +import type { Dimensions } from './Screen/types' + +export default class InspectorController { + private substitutor: Screen | null = null + private inspector: Inspector | null = null + marker: Marker | null = null + constructor(private screen: Screen) {} + + scale(dims: Dimensions) { + if (this.substitutor) { + this.substitutor.scale(dims) + } + } + + enableInspector(clickCallback: (e: { target: Element }) => void): Document | null { + if (!this.screen.parentElement) return null; + if (!this.substitutor) { + this.substitutor = new Screen() + this.marker = new Marker(this.substitutor.overlay, this.substitutor) + this.inspector = new Inspector(this.substitutor, this.marker) + //this.inspector.addClickListener(clickCallback, true) + this.substitutor.attach(this.screen.parentElement) + } + + this.substitutor.display(false) + + const docElement = this.screen.document?.documentElement // this.substitutor.document?.importNode( + const doc = this.substitutor.document + if (doc && docElement) { + doc.open() + doc.write(docElement.outerHTML) + doc.close() + + // TODO! : copy stylesheets & cssRules? + } + this.screen.display(false); + this.inspector.enable(clickCallback); + this.substitutor.display(true); + return doc; + } + + disableInspector() { + if (this.substitutor) { + const doc = this.substitutor.document; + if (doc) { + doc.documentElement.innerHTML = ""; + } + this.inspector.clean(); + this.substitutor.display(false); + } + this.screen.display(true); + } + +} \ No newline at end of file diff --git a/frontend/app/player/_web/Lists.ts b/frontend/app/player/_web/Lists.ts index 4f9c236cd..4eb5de6db 100644 --- a/frontend/app/player/_web/Lists.ts +++ b/frontend/app/player/_web/Lists.ts @@ -9,7 +9,7 @@ const MARKED_LIST_NAMES = [ "log", "resource", "fetch", "stack" ] as const; const LIST_NAMES = [...SIMPLE_LIST_NAMES, ...MARKED_LIST_NAMES ]; -// TODO: provide correct types +// TODO: provide correct types; maybe use list object itself inside the store export const INITIAL_STATE = LIST_NAMES.reduce((state, name) => { state[`${name}List`] = [] diff --git a/frontend/app/player/_web/MessageManager.ts b/frontend/app/player/_web/MessageManager.ts index e6dc9467c..14642c3dc 100644 --- a/frontend/app/player/_web/MessageManager.ts +++ b/frontend/app/player/_web/MessageManager.ts @@ -93,7 +93,6 @@ const visualChanges = [ ] export default class MessageManager extends Screen { - // TODO: consistent with the other data-lists private locationEventManager: ListWalker/**/ = new ListWalker(); private locationManager: ListWalker = new ListWalker(); private loadedLocationManager: ListWalker = new ListWalker(); @@ -198,7 +197,7 @@ export default class MessageManager extends Screen { private waitingForFiles: boolean = false private onFileReadSuccess = () => { - const stateToUpdate = { + const stateToUpdate : Partial= { performanceChartData: this.performanceTrackManager.chartData, performanceAvaliability: this.performanceTrackManager.avaliability, ...this.lists.getFullListsState() @@ -349,7 +348,7 @@ export default class MessageManager extends Screen { stateToUpdate.performanceChartTime = lastPerformanceTrackMessage.time; } - this.lists.moveGetState(t) + Object.assign(stateToUpdate, this.lists.moveGetState(t)) Object.keys(stateToUpdate).length > 0 && this.state.update(stateToUpdate); /* Sequence of the managers is important here */ @@ -535,15 +534,6 @@ export default class MessageManager extends Screen { } } - getLastMessageTime(): number { - return this.lastMessageTime; - } - - getFirstMessageTime(): number { - return this.pagesManager.minTime; - } - - setMessagesLoading(messagesLoading: boolean) { this.display(!messagesLoading); this.state.update({ messagesLoading }); diff --git a/frontend/app/player/_web/Screen/BaseScreen.ts b/frontend/app/player/_web/Screen/BaseScreen.ts deleted file mode 100644 index 67f527e01..000000000 --- a/frontend/app/player/_web/Screen/BaseScreen.ts +++ /dev/null @@ -1,214 +0,0 @@ -import styles from './screen.module.css'; - -import type { Point } from './types'; - - -export interface State { - width: number; - height: number; -} - -export const INITIAL_STATE: State = { - width: 0, - height: 0, -} - - -function getElementsFromInternalPoint(doc: Document, { x, y }: Point): Element[] { - // @ts-ignore (IE, Edge) - if (typeof doc.msElementsFromRect === 'function') { - // @ts-ignore - return Array.prototype.slice.call(doc.msElementsFromRect(x,y)) || [] - } - - if (typeof doc.elementsFromPoint === 'function') { - return doc.elementsFromPoint(x, y) - } - const el = doc.elementFromPoint(x, y) - return el ? [ el ] : [] -} - -function getElementsFromInternalPointDeep(doc: Document, point: Point): Element[] { - const elements = getElementsFromInternalPoint(doc, point) - // is it performant though?? - for (let i = 0; i < elements.length; i++) { - const el = elements[i] - if (isIframe(el)){ - const iDoc = el.contentDocument - if (iDoc) { - const iPoint: Point = { - x: point.x - el.clientLeft, - y: point.y - el.clientTop, - } - elements.push(...getElementsFromInternalPointDeep(iDoc, iPoint)) - } - } - } - return elements -} - -function isIframe(el: Element): el is HTMLIFrameElement { - return el.tagName === "IFRAME" -} - -export default abstract class BaseScreen { - public readonly overlay: HTMLDivElement; - - private readonly iframe: HTMLIFrameElement; - protected readonly screen: HTMLDivElement; - protected readonly controlButton: HTMLDivElement; - protected parentElement: HTMLElement | null = null; - - constructor() { - const iframe = document.createElement('iframe'); - iframe.className = styles.iframe; - this.iframe = iframe; - - const overlay = document.createElement('div'); - overlay.className = styles.overlay; - this.overlay = overlay; - - const screen = document.createElement('div'); - - screen.className = styles.screen; - screen.appendChild(iframe); - screen.appendChild(overlay); - this.screen = screen; - } - - attach(parentElement: HTMLElement) { - if (this.parentElement) { - this.parentElement = undefined - console.error("BaseScreen: Trying to attach an attached screen."); - } - - parentElement.appendChild(this.screen); - - this.parentElement = parentElement; - - /* == For the Inspecting Document content == */ - this.overlay.addEventListener('contextmenu', () => { - this.overlay.style.display = 'none' - const doc = this.document - if (!doc) { return } - const returnOverlay = () => { - this.overlay.style.display = 'block' - doc.removeEventListener('mousemove', returnOverlay) - doc.removeEventListener('mouseclick', returnOverlay) // TODO: prevent default in case of input selection - } - doc.addEventListener('mousemove', returnOverlay) - doc.addEventListener('mouseclick', returnOverlay) - }) - } - - toggleRemoteControlStatus(isEnabled: boolean ) { - const styles = isEnabled ? { border: '2px dashed blue' } : { border: 'unset'} - return Object.assign(this.screen.style, styles) - } - - get window(): WindowProxy | null { - return this.iframe.contentWindow; - } - - get document(): Document | null { - return this.iframe.contentDocument; - } - - private boundingRect: DOMRect | null = null; - private getBoundingClientRect(): DOMRect { - if (this.boundingRect === null) { - return this.boundingRect = this.overlay.getBoundingClientRect() // expensive operation? - } - return this.boundingRect - } - - getInternalViewportCoordinates({ x, y }: Point): Point { - const { x: overlayX, y: overlayY, width } = this.getBoundingClientRect(); - - const screenWidth = this.overlay.offsetWidth; - - const scale = screenWidth / width; - const screenX = (x - overlayX) * scale; - const screenY = (y - overlayY) * scale; - - return { x: Math.round(screenX), y: Math.round(screenY) }; - } - - getCurrentScroll(): Point { - const docEl = this.document?.documentElement - const x = docEl ? docEl.scrollLeft : 0 - const y = docEl ? docEl.scrollTop : 0 - return { x, y } - } - - getInternalCoordinates(p: Point): Point { - const { x, y } = this.getInternalViewportCoordinates(p); - - const sc = this.getCurrentScroll() - - return { x: x+sc.x, y: y+sc.y }; - } - - getElementFromInternalPoint({ x, y }: Point): Element | null { - // elementFromPoint && elementFromPoints require viewpoint-related coordinates, - // not document-related - return this.document?.elementFromPoint(x, y) || null; - } - - getElementsFromInternalPoint(point: Point): Element[] { - const doc = this.document - if (!doc) { return [] } - return getElementsFromInternalPointDeep(doc, point) - } - - getElementFromPoint(point: Point): Element | null { - return this.getElementFromInternalPoint(this.getInternalViewportCoordinates(point)); - } - - getElementsFromPoint(point: Point): Element[] { - return this.getElementsFromInternalPoint(this.getInternalViewportCoordinates(point)); - } - - getElementBySelector(selector: string): Element | null { - if (!selector) return null; - try { - const safeSelector = selector.replace(/:/g, '\\\\3A ').replace(/\//g, '\\/'); - return this.document?.querySelector(safeSelector) || null; - } catch (e) { - console.error("Can not select element. ", e) - return null - } - } - - display(flag: boolean = true) { - this.screen.style.display = flag ? '' : 'none'; - } - - displayFrame(flag: boolean = true) { - this.iframe.style.display = flag ? '' : 'none'; - } - - private s: number = 1; - getScale() { - return this.s; - } - - scale({ height, width }: { height: number, width: number }) { - if (!this.parentElement) return; - const { offsetWidth, offsetHeight } = this.parentElement; - - this.s = Math.min(offsetWidth / width, offsetHeight / height); - if (this.s > 1) { - this.s = 1; - } else { - this.s = Math.round(this.s * 1e3) / 1e3; - } - this.screen.style.transform = `scale(${ this.s }) translate(-50%, -50%)`; - this.screen.style.width = width + 'px'; - this.screen.style.height = height + 'px'; - this.iframe.style.width = width + 'px'; - this.iframe.style.height = height + 'px'; - - this.boundingRect = this.overlay.getBoundingClientRect(); - } -} diff --git a/frontend/app/player/_web/Screen/Cursor.ts b/frontend/app/player/_web/Screen/Cursor.ts index 54ea414fd..4d8094b4e 100644 --- a/frontend/app/player/_web/Screen/Cursor.ts +++ b/frontend/app/player/_web/Screen/Cursor.ts @@ -4,8 +4,7 @@ import styles from './cursor.module.css'; export default class Cursor { private readonly cursor: HTMLDivElement; - private nameElement: HTMLDivElement; - private readonly position: Point = { x: -1, y: -1 } + private tagElement: HTMLDivElement; constructor(overlay: HTMLDivElement) { this.cursor = document.createElement('div'); this.cursor.className = styles.cursor; @@ -20,10 +19,10 @@ export default class Cursor { } } - toggleUserName(name?: string) { - if (!this.nameElement) { - this.nameElement = document.createElement('div') - Object.assign(this.nameElement.style, { + showTag(tag?: string) { + if (!this.tagElement) { + this.tagElement = document.createElement('div') + Object.assign(this.tagElement.style, { position: 'absolute', padding: '4px 6px', borderRadius: '8px', @@ -34,21 +33,19 @@ export default class Cursor { fontSize: '12px', whiteSpace: 'nowrap', }) - this.cursor.appendChild(this.nameElement) + this.cursor.appendChild(this.tagElement) } - if (!name) { - this.nameElement.style.display = 'none' + if (!tag) { + this.tagElement.style.display = 'none' } else { - this.nameElement.style.display = 'block' - const nameStr = name ? name.length > 10 ? name.slice(0, 9) + '...' : name : 'User' - this.nameElement.innerHTML = `${nameStr}` + this.tagElement.style.display = 'block' + const nameStr = tag.length > 10 ? tag.slice(0, 9) + '...' : tag + this.tagElement.innerHTML = `${nameStr}` } } move({ x, y }: Point) { - this.position.x = x; - this.position.y = y; this.cursor.style.left = x + 'px'; this.cursor.style.top = y + 'px'; } @@ -64,8 +61,4 @@ export default class Cursor { // transition // setTransitionSpeed() - getPosition(): Point { - return { x: this.position.x, y: this.position.y }; - } - } diff --git a/frontend/app/player/_web/Screen/Inspector.js b/frontend/app/player/_web/Screen/Inspector.ts similarity index 55% rename from frontend/app/player/_web/Screen/Inspector.js rename to frontend/app/player/_web/Screen/Inspector.ts index 98ba5ec0f..b0ea1ca9c 100644 --- a/frontend/app/player/_web/Screen/Inspector.js +++ b/frontend/app/player/_web/Screen/Inspector.ts @@ -1,15 +1,14 @@ +import type Screen from './Screen' +import type Marker from './Marker' + //import { select } from 'optimal-select'; -export default class Inspector { - //private callbacks; - captureCallbacks = []; - bubblingCallbacks = []; - constructor(screen, marker) { - this.screen = screen; - this.marker = marker; - } +export default class Inspector { + // private captureCallbacks = []; + // private bubblingCallbacks = []; + constructor(private screen: Screen, private marker: Marker) {} - _onMouseMove = (e) => { + private onMouseMove = (e: MouseEvent) => { // const { overlay } = this.screen; // if (!overlay.contains(e.target)) { // return; @@ -25,11 +24,11 @@ export default class Inspector { this.marker.mark(target); } - _onOverlayLeave = () => { + private onOverlayLeave = () => { return this.marker.unmark(); } - _onMarkClick = () => { + private onMarkClick = () => { let target = this.marker.target; if (!target) { return @@ -57,16 +56,15 @@ export default class Inspector { // } // } - toggle(flag, clickCallback) { - this.clickCallback = clickCallback; - if (flag) { - this.screen.overlay.addEventListener('mousemove', this._onMouseMove); - this.screen.overlay.addEventListener('mouseleave', this._onOverlayLeave); - this.screen.overlay.addEventListener('click', this._onMarkClick); - } else { - this.screen.overlay.removeEventListener('mousemove', this._onMouseMove); - this.screen.overlay.removeEventListener('mouseleave', this._onOverlayLeave); - this.screen.overlay.removeEventListener('click', this._onMarkClick); - } + private clickCallback: (e: { target: Element }) => void | null = null + enable(clickCallback: Inspector['clickCallback']) { + this.screen.overlay.addEventListener('mousemove', this.onMouseMove) + this.screen.overlay.addEventListener('mouseleave', this.onOverlayLeave) + this.screen.overlay.addEventListener('click', this.onMarkClick) + } + clean() { + this.screen.overlay.removeEventListener('mousemove', this.onMouseMove) + this.screen.overlay.removeEventListener('mouseleave', this.onOverlayLeave) + this.screen.overlay.removeEventListener('click', this.onMarkClick) } } \ No newline at end of file diff --git a/frontend/app/player/_web/Screen/Marker.ts b/frontend/app/player/_web/Screen/Marker.ts index 4d3fab9b1..331995782 100644 --- a/frontend/app/player/_web/Screen/Marker.ts +++ b/frontend/app/player/_web/Screen/Marker.ts @@ -1,4 +1,4 @@ -import type BaseScreen from './BaseScreen' +import type Screen from './Screen' import styles from './marker.module.css'; function escapeRegExp(string: string) { @@ -19,7 +19,7 @@ export default class Marker { private tooltip: HTMLDivElement private marker: HTMLDivElement - constructor(overlay: HTMLElement, private readonly screen: BaseScreen) { + constructor(overlay: HTMLElement, private readonly screen: Screen) { this.tooltip = document.createElement('div'); this.tooltip.className = styles.tooltip; this.tooltip.appendChild(document.createElement('div')); @@ -74,13 +74,14 @@ export default class Marker { if (fitTargets.length === 0) { this._target = null; } else { - this._target = fitTargets[0]; - const cursorTarget = this.screen.getCursorTarget(); - fitTargets.forEach((target) => { - if (target.contains(cursorTarget)) { - this._target = target; - } - }); + // TODO: fix getCursorTarget()? + // this._target = fitTargets[0]; + // const cursorTarget = this.screen.getCursorTarget(); + // fitTargets.forEach((target) => { + // if (target.contains(cursorTarget)) { + // this._target = target; + // } + // }); } } catch (e) { console.info(e); @@ -96,9 +97,9 @@ export default class Marker { this.redraw(); } - getTagString(tag) { - const attrs = tag.attributes; - let str = `${tag.tagName.toLowerCase()}`; + private getTagString(el: Element) { + const attrs = el.attributes; + let str = `${el.tagName.toLowerCase()}`; for (let i = 0; i < attrs.length; i++) { let k = attrs[i]; diff --git a/frontend/app/player/_web/Screen/Screen.ts b/frontend/app/player/_web/Screen/Screen.ts index 59986423e..6d28f06fd 100644 --- a/frontend/app/player/_web/Screen/Screen.ts +++ b/frontend/app/player/_web/Screen/Screen.ts @@ -1,83 +1,215 @@ -import Marker from './Marker'; -import Cursor from './Cursor'; -import Inspector from './Inspector'; -// import styles from './screen.module.css'; -import BaseScreen from './BaseScreen'; +import styles from './screen.module.css' +import Cursor from './Cursor' -export { INITIAL_STATE } from './BaseScreen'; -export type { State } from './BaseScreen'; +import type { Point, Dimensions } from './types'; -export default class Screen extends BaseScreen { - public readonly cursor: Cursor; - private substitutor: BaseScreen | null = null; - private inspector: Inspector | null = null; - public marker: Marker | null = null; - constructor() { - super(); - this.cursor = new Cursor(this.overlay); + +export type State = Dimensions + +export const INITIAL_STATE: State = { + width: 0, + height: 0, +} + + +function getElementsFromInternalPoint(doc: Document, { x, y }: Point): Element[] { + // @ts-ignore (IE, Edge) + if (typeof doc.msElementsFromRect === 'function') { + // @ts-ignore + return Array.prototype.slice.call(doc.msElementsFromRect(x,y)) || [] } - getCursorTarget() { - return this.getElementFromInternalPoint(this.cursor.getPosition()); + if (typeof doc.elementsFromPoint === 'function') { + return doc.elementsFromPoint(x, y) } + const el = doc.elementFromPoint(x, y) + return el ? [ el ] : [] +} - getCursorTargets() { - return this.getElementsFromInternalPoint(this.cursor.getPosition()); - } - - scale(dims: { height: number, width: number }) { - super.scale(dims) - if (this.substitutor) { - this.substitutor.scale(dims) - } - } - - enableInspector(clickCallback: (e: { target: Element }) => void): Document | null { - if (!this.parentElement) return null; - if (!this.substitutor) { - this.substitutor = new Screen(); - this.marker = new Marker(this.substitutor.overlay, this.substitutor); - this.inspector = new Inspector(this.substitutor, this.marker); - //this.inspector.addClickListener(clickCallback, true); - this.substitutor.attach(this.parentElement); - } - - this.substitutor.display(false); - - const docElement = this.document?.documentElement; // this.substitutor.document?.importNode( - const doc = this.substitutor.document; - if (doc && docElement) { - // doc.documentElement.innerHTML = ""; - // // Better way? - // for (let i = 1; i < docElement.attributes.length; i++) { - // const att = docElement.attributes[i]; - // doc.documentElement.setAttribute(att.name, att.value); - // } - // for (let i = 1; i < docElement.childNodes.length; i++) { - // doc.documentElement.appendChild(docElement.childNodes[i].cloneNode(true)); - // } - doc.open(); - doc.write(docElement.outerHTML); // Context will be iframe, so instanceof Element won't work - doc.close(); - - // TODO! : copy stylesheets, check with styles - } - this.display(false); - this.inspector.toggle(true, clickCallback); - this.substitutor.display(true); - return doc; - } - - disableInspector() { - if (this.substitutor) { - const doc = this.substitutor.document; - if (doc) { - doc.documentElement.innerHTML = ""; +function getElementsFromInternalPointDeep(doc: Document, point: Point): Element[] { + const elements = getElementsFromInternalPoint(doc, point) + // is it performant though?? + for (let i = 0; i < elements.length; i++) { + const el = elements[i] + if (isIframe(el)){ + const iDoc = el.contentDocument + if (iDoc) { + const iPoint: Point = { + x: point.x - el.clientLeft, + y: point.y - el.clientTop, + } + elements.push(...getElementsFromInternalPointDeep(iDoc, iPoint)) } - this.inspector.toggle(false); - this.substitutor.display(false); } - this.display(true); + } + return elements +} + +function isIframe(el: Element): el is HTMLIFrameElement { + return el.tagName === "IFRAME" +} + +export default class Screen { + readonly overlay: HTMLDivElement + readonly cursor: Cursor + + private readonly iframe: HTMLIFrameElement; + protected readonly screen: HTMLDivElement; + protected readonly controlButton: HTMLDivElement; + protected parentElement: HTMLElement | null = null; + + constructor() { + const iframe = document.createElement('iframe'); + iframe.className = styles.iframe; + this.iframe = iframe; + + const overlay = document.createElement('div'); + overlay.className = styles.overlay; + this.overlay = overlay; + + const screen = document.createElement('div'); + + screen.className = styles.screen; + screen.appendChild(iframe); + screen.appendChild(overlay); + this.screen = screen; + + this.cursor = new Cursor(this.overlay) // TODO: move outside } -} \ No newline at end of file + attach(parentElement: HTMLElement) { + if (this.parentElement) { + this.parentElement = undefined + console.error("BaseScreen: Trying to attach an attached screen."); + } + + parentElement.appendChild(this.screen); + + this.parentElement = parentElement; + + /* == For the Inspecting Document content == */ + this.overlay.addEventListener('contextmenu', () => { + this.overlay.style.display = 'none' + const doc = this.document + if (!doc) { return } + const returnOverlay = () => { + this.overlay.style.display = 'block' + doc.removeEventListener('mousemove', returnOverlay) + doc.removeEventListener('mouseclick', returnOverlay) // TODO: prevent default in case of input selection + } + doc.addEventListener('mousemove', returnOverlay) + doc.addEventListener('mouseclick', returnOverlay) + }) + } + + toggleBorder(isEnabled: boolean ) { + const styles = isEnabled ? { border: '2px dashed blue' } : { border: 'unset'} + return Object.assign(this.screen.style, styles) + } + + get window(): WindowProxy | null { + return this.iframe.contentWindow; + } + + get document(): Document | null { + return this.iframe.contentDocument; + } + + private boundingRect: DOMRect | null = null; + private getBoundingClientRect(): DOMRect { + if (this.boundingRect === null) { + return this.boundingRect = this.overlay.getBoundingClientRect() // expensive operation? + } + return this.boundingRect + } + + getInternalViewportCoordinates({ x, y }: Point): Point { + const { x: overlayX, y: overlayY, width } = this.getBoundingClientRect(); + + const screenWidth = this.overlay.offsetWidth; + + const scale = screenWidth / width; + const screenX = (x - overlayX) * scale; + const screenY = (y - overlayY) * scale; + + return { x: Math.round(screenX), y: Math.round(screenY) }; + } + + getCurrentScroll(): Point { + const docEl = this.document?.documentElement + const x = docEl ? docEl.scrollLeft : 0 + const y = docEl ? docEl.scrollTop : 0 + return { x, y } + } + + getInternalCoordinates(p: Point): Point { + const { x, y } = this.getInternalViewportCoordinates(p); + + const sc = this.getCurrentScroll() + + return { x: x+sc.x, y: y+sc.y }; + } + + getElementFromInternalPoint({ x, y }: Point): Element | null { + // elementFromPoint && elementFromPoints require viewpoint-related coordinates, + // not document-related + return this.document?.elementFromPoint(x, y) || null; + } + + getElementsFromInternalPoint(point: Point): Element[] { + const doc = this.document + if (!doc) { return [] } + return getElementsFromInternalPointDeep(doc, point) + } + + getElementFromPoint(point: Point): Element | null { + return this.getElementFromInternalPoint(this.getInternalViewportCoordinates(point)); + } + + getElementsFromPoint(point: Point): Element[] { + return this.getElementsFromInternalPoint(this.getInternalViewportCoordinates(point)); + } + + getElementBySelector(selector: string): Element | null { + if (!selector) return null; + try { + const safeSelector = selector.replace(/:/g, '\\\\3A ').replace(/\//g, '\\/'); + return this.document?.querySelector(safeSelector) || null; + } catch (e) { + console.error("Can not select element. ", e) + return null + } + } + + display(flag: boolean = true) { + this.screen.style.display = flag ? '' : 'none'; + } + + displayFrame(flag: boolean = true) { + this.iframe.style.display = flag ? '' : 'none'; + } + + private s: number = 1; + getScale() { + return this.s; + } + + scale({ height, width }: Dimensions) { + if (!this.parentElement) return; + const { offsetWidth, offsetHeight } = this.parentElement; + + this.s = Math.min(offsetWidth / width, offsetHeight / height); + if (this.s > 1) { + this.s = 1; + } else { + this.s = Math.round(this.s * 1e3) / 1e3; + } + this.screen.style.transform = `scale(${ this.s }) translate(-50%, -50%)`; + this.screen.style.width = width + 'px'; + this.screen.style.height = height + 'px'; + this.iframe.style.width = width + 'px'; + this.iframe.style.height = height + 'px'; + + this.boundingRect = this.overlay.getBoundingClientRect(); + } +} diff --git a/frontend/app/player/_web/Screen/index.js b/frontend/app/player/_web/Screen/index.js deleted file mode 100644 index 96f315c68..000000000 --- a/frontend/app/player/_web/Screen/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './Screen'; -export * from './Screen'; diff --git a/frontend/app/player/_web/Screen/types.ts b/frontend/app/player/_web/Screen/types.ts index 0fdac4d8b..2c464fdfc 100644 --- a/frontend/app/player/_web/Screen/types.ts +++ b/frontend/app/player/_web/Screen/types.ts @@ -1,4 +1,9 @@ export interface Point { - x: number; - y: number; + x: number + y: number +} + +export interface Dimensions { + width: number + height: number } \ No newline at end of file diff --git a/frontend/app/player/_web/WebLivePlayer.ts b/frontend/app/player/_web/WebLivePlayer.ts new file mode 100644 index 000000000..d7b707b1c --- /dev/null +++ b/frontend/app/player/_web/WebLivePlayer.ts @@ -0,0 +1,23 @@ +// import WebPlayer from './WebPlayer' +// import AssistManager from './assist/AssistManager' + + +// export default class WebLivePlayer extends WebPlayer { +// assistManager: AssistManager // public so far +// constructor(private wpState: Store, session, config: RTCIceServer[]) { +// super(wpState) +// this.assistManager = new AssistManager(session, this.messageManager, config, wpState) +// const endTime = !live && session.duration.valueOf() +// wpState.update({ +// //@ts-ignore +// initialized: true, +// //@ts-ignore +// session, + +// live: true, +// livePlay: true, +// }) + +// this.assistManager.connect(session.agentToken) +// } +// } \ No newline at end of file diff --git a/frontend/app/player/_web/WebPlayer.ts b/frontend/app/player/_web/WebPlayer.ts index a48577288..de3dcfa49 100644 --- a/frontend/app/player/_web/WebPlayer.ts +++ b/frontend/app/player/_web/WebPlayer.ts @@ -2,19 +2,19 @@ import type { Store } from '../player/types' import Player, { State as PlayerState } from '../player/Player' import MessageManager from './MessageManager' +import InspectorController from './InspectorController' import AssistManager from './assist/AssistManager' import Screen from './Screen/Screen' -import { State as MMState, INITIAL_STATE as MM_INITIAL_STATE } from './MessageManager' - - +import type { State as MMState } from './MessageManager' export default class WebPlayer extends Player { private readonly screen: Screen - private readonly messageManager: MessageManager + private readonly inspectorController: InspectorController + protected readonly messageManager: MessageManager assistManager: AssistManager // public so far - constructor(private wpState: Store, session, config, live: boolean) { + constructor(private wpState: Store, session, config: RTCIceServer[], live: boolean) { // TODO: separate screen from manager const screen = new MessageManager(session, wpState, config, live) super(wpState, screen) @@ -24,6 +24,8 @@ export default class WebPlayer extends Player { // TODO: separate LiveWebPlayer this.assistManager = new AssistManager(session, this.messageManager, config, wpState) + this.inspectorController = new InspectorController(screen) + const endTime = !live && session.duration.valueOf() wpState.update({ @@ -50,9 +52,29 @@ export default class WebPlayer extends Player { scale = () => { const { width, height } = this.wpState.get() this.screen.scale({ width, height }) + this.inspectorController.scale({ width, height }) + + // this.updateMarketTargets() ?? } + + // Inspector & marker mark(e: Element) { - this.screen.marker.mark(e) + this.inspectorController.marker?.mark(e) + } + toggleInspectorMode(flag: boolean, clickCallback) { + if (typeof flag !== 'boolean') { + const { inspectorMode } = this.wpState.get() + flag = !inspectorMode; + } + + if (flag) { + this.pause() + this.wpState.update({ inspectorMode: true }) + return this.inspectorController.enableInspector(clickCallback); + } else { + this.inspectorController.disableInspector(); + this.wpState.update({ inspectorMode: false }); + } } updateMarketTargets() { @@ -109,7 +131,7 @@ export default class WebPlayer extends Player { } // private actualScroll: Point | null = null - setMarkedTargets(selections: { selector: string, count: number }[] | null) { + private setMarkedTargets(selections: { selector: string, count: number }[] | null) { // if (selections) { // const totalCount = selections.reduce((a, b) => { // return a + b.count @@ -132,7 +154,7 @@ export default class WebPlayer extends Player { // update({ markedTargets }); // } else { // if (this.actualScroll) { - // this.window?.scrollTo(this.actualScroll.x, this.actualScroll.y) + // this.screen.window?.scrollTo(this.actualScroll.x, this.actualScroll.y) // this.actualScroll = null // } // update({ markedTargets: null }); @@ -140,26 +162,11 @@ export default class WebPlayer extends Player { } markTargets(targets: { selector: string, count: number }[] | null) { - // this.animator.pause(); - // this.setMarkedTargets(targets); - } - - toggleInspectorMode(flag, clickCallback) { - // if (typeof flag !== 'boolean') { - // const { inspectorMode } = getState(); - // flag = !inspectorMode; - // } - - // if (flag) { - // this.pause() - // update({ inspectorMode: true }); - // return super.enableInspector(clickCallback); - // } else { - // super.disableInspector(); - // update({ inspectorMode: false }); - // } + // this.pause(); + // this.setMarkedTargets(targets); } + // TODO async toggleTimetravel() { if (!this.wpState.get().liveTimeTravel) { return await this.messageManager.reloadWithUnprocessedFile() @@ -167,7 +174,7 @@ export default class WebPlayer extends Player { } toggleUserName(name?: string) { - this.screen.cursor.toggleUserName(name) + this.screen.cursor.showTag(name) } clean() { super.clean() diff --git a/frontend/app/player/_web/assist/AssistManager.ts b/frontend/app/player/_web/assist/AssistManager.ts index 1e1274226..1c9984b51 100644 --- a/frontend/app/player/_web/assist/AssistManager.ts +++ b/frontend/app/player/_web/assist/AssistManager.ts @@ -78,7 +78,7 @@ export default class AssistManager { constructor( private session: any, private md: MessageManager, - private config: any, + private config: RTCIceServer[], private store: Store, ) {} @@ -302,13 +302,13 @@ export default class AssistManager { this.md.overlay.addEventListener("mousemove", this.onMouseMove) this.md.overlay.addEventListener("click", this.onMouseClick) this.md.overlay.addEventListener("wheel", this.onWheel) - this.md.toggleRemoteControlStatus(true) + this.md.toggleBorder(true) this.store.update({ remoteControl: RemoteControlStatus.Enabled }) } else { this.md.overlay.removeEventListener("mousemove", this.onMouseMove) this.md.overlay.removeEventListener("click", this.onMouseClick) this.md.overlay.removeEventListener("wheel", this.onWheel) - this.md.toggleRemoteControlStatus(false) + this.md.toggleBorder(false) this.store.update({ remoteControl: RemoteControlStatus.Disabled }) this.toggleAnnotation(false) } @@ -354,7 +354,7 @@ export default class AssistManager { const urlObject = new URL(window.env.API_EDP || window.location.origin) return import('peerjs').then(({ default: Peer }) => { if (this.cleaned) {return Promise.reject("Already cleaned")} - const peerOpts: any = { + const peerOpts: Peer.PeerJSOption = { host: urlObject.hostname, path: '/assist', port: urlObject.port === "" ? (location.protocol === 'https:' ? 443 : 80 ): parseInt(urlObject.port), @@ -362,6 +362,7 @@ export default class AssistManager { if (this.config) { peerOpts['config'] = { iceServers: this.config, + //@ts-ignore sdpSemantics: 'unified-plan', iceTransportPolicy: 'relay', }; diff --git a/frontend/app/player/_web/managers/ActivityManager.ts b/frontend/app/player/_web/managers/ActivityManager.ts index 32265f924..d5e81f62d 100644 --- a/frontend/app/player/_web/managers/ActivityManager.ts +++ b/frontend/app/player/_web/managers/ActivityManager.ts @@ -7,7 +7,7 @@ class SkipIntervalCls { get time(): number { return this.start; } - contains(ts) { + contains(ts: number) { return ts > this.start && ts < this.end; } } diff --git a/frontend/app/player/_web/managers/MouseMoveManager.ts b/frontend/app/player/_web/managers/MouseMoveManager.ts index 5dd3160a5..63b5ca895 100644 --- a/frontend/app/player/_web/managers/MouseMoveManager.ts +++ b/frontend/app/player/_web/managers/MouseMoveManager.ts @@ -1,41 +1,50 @@ -import type Screen from '../Screen/Screen'; -import type { MouseMove } from '../messages'; +import type Screen from '../Screen/Screen' +import type { Point } from '../Screen/types' +import type { MouseMove } from '../messages' -import ListWalker from '../../_common/ListWalker'; +import ListWalker from '../../_common/ListWalker' const HOVER_CLASS = "-openreplay-hover"; const HOVER_CLASS_DEPR = "-asayer-hover"; export default class MouseMoveManager extends ListWalker { - private hoverElements: Array = []; + private hoverElements: Array = [] constructor(private screen: Screen) {super()} + // private getCursorTarget() { + // return this.screen.getElementFromInternalPoint(this.current) + // } + + private getCursorTargets() { + return this.screen.getElementsFromInternalPoint(this.current) + } + private updateHover(): void { - const curHoverElements = this.screen.getCursorTargets(); - const diffAdd = curHoverElements.filter(elem => !this.hoverElements.includes(elem)); - const diffRemove = this.hoverElements.filter(elem => !curHoverElements.includes(elem)); - this.hoverElements = curHoverElements; + const curHoverElements = this.getCursorTargets() + const diffAdd = curHoverElements.filter(elem => !this.hoverElements.includes(elem)) + const diffRemove = this.hoverElements.filter(elem => !curHoverElements.includes(elem)) + this.hoverElements = curHoverElements diffAdd.forEach(elem => { elem.classList.add(HOVER_CLASS) elem.classList.add(HOVER_CLASS_DEPR) - }); + }) diffRemove.forEach(elem => { elem.classList.remove(HOVER_CLASS) elem.classList.remove(HOVER_CLASS_DEPR) - }); + }) } reset(): void { - this.hoverElements = []; + this.hoverElements.length = 0 } move(t: number) { - const lastMouseMove = this.moveGetLast(t); - if (!!lastMouseMove){ - this.screen.cursor.move(lastMouseMove); + const lastMouseMove = this.moveGetLast(t) + if (!!lastMouseMove) { + this.screen.cursor.move(lastMouseMove) //window.getComputedStyle(this.screen.getCursorTarget()).cursor === 'pointer' // might nfluence performance though - this.updateHover(); + this.updateHover() } } } diff --git a/frontend/app/player/_web/managers/ReduxStateManager.ts b/frontend/app/player/_web/managers/ReduxStateManager.ts deleted file mode 100644 index 4756b303a..000000000 --- a/frontend/app/player/_web/managers/ReduxStateManager.ts +++ /dev/null @@ -1,53 +0,0 @@ -// import { applyChange, revertChange } from 'deep-diff'; -// import ListWalker from '../../_common/ListWalker'; -// import type { Redux } from '../messages'; - -// export default class ReduxStateManager extends ListWalker { -// private state: Object = {} -// private finalStates: Object[] = [] - -// moveWasUpdated(time, index) { -// super.moveApply( -// time, -// this.onIncrement, -// this.onDecrement, -// ) -// } - -// onIncrement = (item) => { -// this.processRedux(item, true); -// } - -// onDecrement = (item) => { -// this.processRedux(item, false); -// } - -// private processRedux(action, forward) { -// if (forward) { -// if (!!action.state) { -// this.finalStates.push(this.state); -// this.state = JSON.parse(JSON.stringify(action.state)); // Deep clone :( -// } else { -// action.diff.forEach(d => { -// try { -// applyChange(this.state, d); -// } catch (e) { -// //console.warn("Deepdiff error") -// } -// }); -// } -// } else { -// if (!!action.state) { -// this.state = this.finalStates.pop(); -// } else { -// action.diff.forEach(d => { -// try { -// revertChange(this.state, 1, d); // bad lib :( TODO: write our own diff -// } catch (e) { -// //console.warn("Deepdiff error") -// } -// }); -// } -// } -// } -// } diff --git a/frontend/app/player/create.ts b/frontend/app/player/create.ts index c42815eae..4a928e20f 100644 --- a/frontend/app/player/create.ts +++ b/frontend/app/player/create.ts @@ -1,25 +1,33 @@ import SimpleStore from './_common/SimpleStore' import type { Store } from './player/types' import { State as MMState, INITIAL_STATE as MM_INITIAL_STATE } from './_web/MessageManager' -import { State as PState, INITIAL_STATE as PLAYER_INITIAL_STATE } from './player/Player' +import Player, { State as PState } from './player/Player' import WebPlayer from './_web/WebPlayer' -export function createWebPlayer(session, config): [WebPlayer, Store] { - const store = new SimpleStore({ - ...PLAYER_INITIAL_STATE, +type WebPlayerStore = Store + +export function createWebPlayer(session, wrapStore?: (s:WebPlayerStore) => WebPlayerStore): [WebPlayer, WebPlayerStore] { + let store: WebPlayerStore = new SimpleStore({ + ...Player.INITIAL_STATE, ...MM_INITIAL_STATE, }) - const player = new WebPlayer(store, session, config, false) + if (wrapStore) { + store = wrapStore(store) + } + const player = new WebPlayer(store, session, null, false) return [player, store] } -export function createLiveWebPlayer(session, config): [WebPlayer, Store] { - const store = new SimpleStore({ - ...PLAYER_INITIAL_STATE, +export function createLiveWebPlayer(session, config: RTCIceServer[], wrapStore?: (s:WebPlayerStore) => WebPlayerStore): [WebPlayer, WebPlayerStore] { + let store: WebPlayerStore = new SimpleStore({ + ...Player.INITIAL_STATE, ...MM_INITIAL_STATE, }) + if (wrapStore) { + store = wrapStore(store) + } const player = new WebPlayer(store, session, config, true) return [player, store] } \ No newline at end of file diff --git a/frontend/app/player/player/Animator.ts b/frontend/app/player/player/Animator.ts index e8b8b7279..871da629a 100644 --- a/frontend/app/player/player/Animator.ts +++ b/frontend/app/player/player/Animator.ts @@ -1,4 +1,4 @@ -import type { Store, Mover, Interval } from './types'; +import type { Store, Moveable, Interval } from './types'; import * as localStorage from './localStorage'; const fps = 60 @@ -25,7 +25,6 @@ export interface SetState { time: number playing: boolean completed: boolean - endTime: number live: boolean livePlay: boolean } @@ -34,27 +33,27 @@ export interface GetState extends SetState { skip: boolean speed: number skipIntervals: Interval[] - lastMessageTime: number + endTime: number ready: boolean + + lastMessageTime: number } -export const INITIAL_STATE: SetState = { - time: 0, - playing: false, - completed: false, - endTime: 0, - live: false, - livePlay: false, -} as const - - export default class Animator { + static INITIAL_STATE: SetState = { + time: 0, + playing: false, + completed: false, + live: false, + livePlay: false, + } as const + private animationFrameRequestId: number = 0 - constructor(private state: Store, private mm: Mover) {} + constructor(private store: Store, private mm: Moveable) {} private setTime(time: number) { - this.state.update({ + this.store.update({ time, completed: false, }) @@ -62,7 +61,7 @@ export default class Animator { } private startAnimation() { - let prevTime = this.state.get().time + let prevTime = this.store.get().time let animationPrevTime = performance.now() const frameHandler = (animationCurrentTime: number) => { @@ -74,8 +73,9 @@ export default class Animator { live, livePlay, ready, // = messagesLoading || cssLoading || disconnected - lastMessageTime, // should be updated - } = this.state.get() + + lastMessageTime, + } = this.store.get() const diffTime = !ready ? 0 @@ -83,20 +83,22 @@ export default class Animator { let time = prevTime + diffTime - const skipInterval = skip && skipIntervals.find(si => si.contains(time)) // TODO: good skip by messages + const skipInterval = skip && skipIntervals.find(si => si.contains(time)) if (skipInterval) time = skipInterval.end if (time < 0) { time = 0 } // ? //const fmt = getFirstMessageTime(); //if (time < fmt) time = fmt; // ? - + // if (livePlay && time < endTime) { time = endTime } + // === live only if (livePlay && time < lastMessageTime) { time = lastMessageTime } if (endTime < lastMessageTime) { - this.state.update({ + this.store.update({ endTime: lastMessageTime, }) } + // === prevTime = time animationPrevTime = animationCurrentTime @@ -104,17 +106,20 @@ export default class Animator { const completed = !live && time >= endTime if (completed) { this.setTime(endTime) - return this.state.update({ + return this.store.update({ playing: false, completed: true, }) } + // === live only if (live && time > endTime) { - this.state.update({ + this.store.update({ endTime: time, }) } + // === + this.setTime(time) this.animationFrameRequestId = requestAnimationFrame(frameHandler) } @@ -123,17 +128,17 @@ export default class Animator { play() { cancelAnimationFrame(this.animationFrameRequestId) - this.state.update({ playing: true }) + this.store.update({ playing: true }) this.startAnimation() } pause() { cancelAnimationFrame(this.animationFrameRequestId) - this.state.update({ playing: false }) + this.store.update({ playing: false }) } togglePlay() { - const { playing, completed } = this.state.get() + const { playing, completed } = this.store.get() if (playing) { this.pause() } else if (completed) { @@ -146,26 +151,26 @@ export default class Animator { // jump by index? jump(time: number) { - const { live } = this.state.get() + const { live } = this.store.get() if (live) return - if (this.state.get().playing) { + if (this.store.get().playing) { cancelAnimationFrame(this.animationFrameRequestId) this.setTime(time) this.startAnimation() - this.state.update({ livePlay: time === this.state.get().endTime }) + this.store.update({ livePlay: time === this.store.get().endTime }) } else { this.setTime(time) - this.state.update({ livePlay: time === this.state.get().endTime }) + this.store.update({ livePlay: time === this.store.get().endTime }) } } // TODO: clearify logic of live time-travel jumpToLive() { cancelAnimationFrame(this.animationFrameRequestId) - this.setTime(this.state.get().endTime) + this.setTime(this.store.get().endTime) this.startAnimation() - this.state.update({ livePlay: true }) + this.store.update({ livePlay: true }) } diff --git a/frontend/app/player/player/Player.ts b/frontend/app/player/player/Player.ts index 057d3752f..9b2b98225 100644 --- a/frontend/app/player/player/Player.ts +++ b/frontend/app/player/player/Player.ts @@ -1,8 +1,7 @@ import * as typedLocalStorage from './localStorage'; -import type { Mover, Cleaner, Store } from './types'; +import type { Moveable, Cleanable, Store } from './types'; import Animator from './Animator'; -import { INITIAL_STATE as ANIMATOR_INITIAL_STATE } from './Animator'; import type { GetState as AnimatorGetState, SetState as AnimatorSetState } from './Animator'; @@ -19,21 +18,22 @@ const initialSkip = typedLocalStorage.boolean(SKIP_STORAGE_KEY) const initialSkipToIssue = typedLocalStorage.boolean(SKIP_TO_ISSUE_STORAGE_KEY) const initialAutoplay = typedLocalStorage.boolean(AUTOPLAY_STORAGE_KEY) const initialShowEvents = typedLocalStorage.boolean(SHOW_EVENTS_STORAGE_KEY) -export const INITIAL_STATE = { - ...ANIMATOR_INITIAL_STATE, - skipToIssue: initialSkipToIssue, - showEvents: initialShowEvents, - - autoplay: initialAutoplay, - skip: initialSkip, - speed: initialSpeed, -} -export type State = typeof INITIAL_STATE & AnimatorGetState +export type State = typeof Player.INITIAL_STATE /* == */ export default class Player extends Animator { - constructor(private pState: Store, private manager: Mover & Cleaner) { + static INITIAL_STATE = { + ...Animator.INITIAL_STATE, + skipToIssue: initialSkipToIssue, + showEvents: initialShowEvents, + + autoplay: initialAutoplay, + skip: initialSkip, + speed: initialSpeed, + } as const + + constructor(private pState: Store, private manager: Moveable & Cleanable) { super(pState, manager) // Autoplay diff --git a/frontend/app/player/player/types.ts b/frontend/app/player/player/types.ts index 2ad5032ed..5deb56b06 100644 --- a/frontend/app/player/player/types.ts +++ b/frontend/app/player/player/types.ts @@ -1,9 +1,9 @@ -export interface Mover { +export interface Moveable { move(time: number): void } -export interface Cleaner { +export interface Cleanable { clean(): void } From f4b1ad1186a6efdc6e4c9ac8f62d13765b0ae174 Mon Sep 17 00:00:00 2001 From: MauricioGarciaS <47052044+MauricioGarciaS@users.noreply.github.com> Date: Tue, 22 Nov 2022 13:08:02 +0100 Subject: [PATCH 014/252] Updated evets_queue.py and signals.py --- ee/api/chalicelib/core/signals.py | 15 ++++++++----- ee/api/chalicelib/utils/events_queue.py | 28 ++++++++++++++++--------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/ee/api/chalicelib/core/signals.py b/ee/api/chalicelib/core/signals.py index 4ad1d640f..1bd8eebc6 100644 --- a/ee/api/chalicelib/core/signals.py +++ b/ee/api/chalicelib/core/signals.py @@ -1,6 +1,7 @@ import json import schemas_ee +import logging from chalicelib.utils import helper from chalicelib.utils import pg_client @@ -8,9 +9,13 @@ from chalicelib.utils import pg_client def handle_frontend_signals(project_id: int, user_id: str, data: schemas_ee.SignalsSchema): res = {'errors': 'query not executed'} insights_query = """INSERT INTO public.frontend_signals VALUES (%(project_id)s, %(user_id)s, %(timestamp)s, %(action)s, %(source)s, %(category)s, %(data)s)""" - with pg_client.PostgresClient() as conn: - query = conn.mogrify(insights_query, {'project_id': project_id, 'user_id': user_id, 'timestamp': data.timestamp, 'action': data.action, 'source': data.source, + try: + with pg_client.PostgresClient() as conn: + query = conn.mogrify(insights_query, {'project_id': project_id, 'user_id': user_id, 'timestamp': data.timestamp, 'action': data.action, 'source': data.source, 'category': data.category, 'data': json.dumps(data.data)}) - conn.execute(query) - res = helper.dict_to_camel_case(conn.fetchone()) - return res + conn.execute(query) + # res = helper.dict_to_camel_case(conn.fetchone()) + return 1 + except Exception as e: + logging.info(f'Error while inserting\n{e}') + return 0 diff --git a/ee/api/chalicelib/utils/events_queue.py b/ee/api/chalicelib/utils/events_queue.py index a1a475d39..a37cec121 100644 --- a/ee/api/chalicelib/utils/events_queue.py +++ b/ee/api/chalicelib/utils/events_queue.py @@ -25,23 +25,31 @@ class EventQueue(): if self.test: print(events) return 1 - _query = conn.mogrify("""INSERT INTO %(database)s.%(table)s (project_id, user_id, timestamp, action, source, category, data) VALUES %(events)s""", - {'database': 'public', 'table': 'frontend_signals', 'events': "(0, 'test', 0, 'action', 's', 'c', '{}')"}) - logging.info(_query) - res = 'done' + _query = """INSERT INTO {database}.{table} (project_id, user_id, timestamp, action, source, category, data) VALUES %(events)s""".format( + database='public', table='frontend_signals') + _query = conn.mogrify(_query, {'events': (0, 'test', 0, 'action', 's', 'c', '{}')}) + conn.execute(_query) + # logging.info(_query) + # res = 'done' # res = conn.fetchone() - #res = helper.dict_to_camel_case(conn.fetchone()) - return res + # res = helper.dict_to_camel_case(conn.fetchone()) + return 1 def force_flush(self): if not self.events.empty(): - with pg_client.PostgresClient() as conn: - self.flush(conn) + try: + with pg_client.PostgresClient() as conn: + self.flush(conn) + except Exception as e: + logging.info(f'Error: {e}') def put(self, element): if self.events.full(): - with pg_client.PostgresClient() as conn: - self.flush(conn) + try: + with pg_client.PostgresClient() as conn: + self.flush(conn) + except Exception as e: + logging.info(f'Error: {e}') self.events.put(element) self.events.task_done() From 649ff85af44d567b2e644c670f4feca074708faf Mon Sep 17 00:00:00 2001 From: MauricioGarciaS <47052044+MauricioGarciaS@users.noreply.github.com> Date: Tue, 22 Nov 2022 13:48:36 +0100 Subject: [PATCH 015/252] Fixed return in signals --- ee/api/chalicelib/core/signals.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ee/api/chalicelib/core/signals.py b/ee/api/chalicelib/core/signals.py index 1bd8eebc6..0311d3259 100644 --- a/ee/api/chalicelib/core/signals.py +++ b/ee/api/chalicelib/core/signals.py @@ -15,7 +15,7 @@ def handle_frontend_signals(project_id: int, user_id: str, data: schemas_ee.Sign 'category': data.category, 'data': json.dumps(data.data)}) conn.execute(query) # res = helper.dict_to_camel_case(conn.fetchone()) - return 1 + return {'success': 1} except Exception as e: - logging.info(f'Error while inserting\n{e}') - return 0 + return {'errors': e} + return {} From 8636fd45dbf4d7dafbeb3c1acd1825e2c3a93bc7 Mon Sep 17 00:00:00 2001 From: MauricioGarciaS <47052044+MauricioGarciaS@users.noreply.github.com> Date: Tue, 22 Nov 2022 14:20:52 +0100 Subject: [PATCH 016/252] changed dict response signals --- ee/api/chalicelib/core/signals.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ee/api/chalicelib/core/signals.py b/ee/api/chalicelib/core/signals.py index 0311d3259..81d2180ff 100644 --- a/ee/api/chalicelib/core/signals.py +++ b/ee/api/chalicelib/core/signals.py @@ -15,7 +15,7 @@ def handle_frontend_signals(project_id: int, user_id: str, data: schemas_ee.Sign 'category': data.category, 'data': json.dumps(data.data)}) conn.execute(query) # res = helper.dict_to_camel_case(conn.fetchone()) - return {'success': 1} + return None except Exception as e: - return {'errors': e} - return {} + return {'errors': [e]} + return None From 1621f73c6959c3697c58687174beb5d647e24567 Mon Sep 17 00:00:00 2001 From: sylenien Date: Tue, 22 Nov 2022 14:23:30 +0100 Subject: [PATCH 017/252] refactor(ui/player): create player context for webplayer components --- .../app/components/Session/PlayerContent.js | 70 +++++++++ frontend/app/components/Session/WebPlayer.js | 139 ------------------ frontend/app/components/Session/WebPlayer.tsx | 127 ++++++++++++++++ .../app/components/Session/playerContext.ts | 12 ++ .../components/Session_/Console/Console.js | 18 --- .../Session_/Console/ConsoleContent.js | 123 ---------------- .../Console/ConsoleRow/ConsoleRow.tsx | 48 ------ .../Session_/Console/ConsoleRow/index.ts | 1 - .../Session_/Console/console.module.css | 38 ----- .../components/Session_/Inspector/index.js | 6 +- .../Session_/LongTasks/LongTasks.js | 8 +- .../app/components/Session_/Player/Player.js | 110 ++++++-------- .../components/Session_/Profiler/Profiler.js | 4 +- frontend/app/player/create.ts | 8 +- 14 files changed, 271 insertions(+), 441 deletions(-) create mode 100644 frontend/app/components/Session/PlayerContent.js delete mode 100644 frontend/app/components/Session/WebPlayer.js create mode 100644 frontend/app/components/Session/WebPlayer.tsx create mode 100644 frontend/app/components/Session/playerContext.ts delete mode 100644 frontend/app/components/Session_/Console/Console.js delete mode 100644 frontend/app/components/Session_/Console/ConsoleContent.js delete mode 100644 frontend/app/components/Session_/Console/ConsoleRow/ConsoleRow.tsx delete mode 100644 frontend/app/components/Session_/Console/ConsoleRow/index.ts delete mode 100644 frontend/app/components/Session_/Console/console.module.css diff --git a/frontend/app/components/Session/PlayerContent.js b/frontend/app/components/Session/PlayerContent.js new file mode 100644 index 000000000..8983ca5fc --- /dev/null +++ b/frontend/app/components/Session/PlayerContent.js @@ -0,0 +1,70 @@ +import React from 'react'; +import { + connectPlayer, +} from 'Player'; +import PlayerBlock from '../Session_/PlayerBlock'; +import styles from '../Session_/session.module.css'; +import { countDaysFrom } from 'App/date'; +import cn from 'classnames'; +import RightBlock from './RightBlock'; + +function PlayerContent({ session, live, fullscreen, activeTab, setActiveTab, hasError }) { + const sessionDays = countDaysFrom(session.startedAt); + return ( +
+ {hasError ? ( +
+
+
+ {sessionDays > 2 ? 'Session not found.' : 'This session is still being processed.'} +
+
+ {sessionDays > 2 + ? 'Please check your data retention policy.' + : 'Please check it again in a few minutes.'} +
+
+
+ ) : ( +
+
+
+ +
+
+ {activeTab !== '' && ( + + )} +
+ )} +
+ ); +} + +function RightMenu({ live, tabs, activeTab, setActiveTab, fullscreen }) { + return ( + !live && + !fullscreen && + ); +} + +export default connectPlayer((state) => ({ + showEvents: !state.showEvents, + hasError: state.error, +}))(PlayerContent); diff --git a/frontend/app/components/Session/WebPlayer.js b/frontend/app/components/Session/WebPlayer.js deleted file mode 100644 index 6e71d3ea7..000000000 --- a/frontend/app/components/Session/WebPlayer.js +++ /dev/null @@ -1,139 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { connect } from 'react-redux'; -import { Loader, Modal } from 'UI'; -import { toggleFullscreen, closeBottomBlock } from 'Duck/components/player'; -import { fetchList } from 'Duck/integrations'; -import { PlayerProvider, injectNotes, connectPlayer, init as initPlayer, clean as cleanPlayer, Controls } from 'Player'; -import cn from 'classnames'; -import RightBlock from './RightBlock'; -import withLocationHandlers from 'HOCs/withLocationHandlers'; -import { useStore } from 'App/mstore' -import PlayerBlockHeader from '../Session_/PlayerBlockHeader'; -import PlayerBlock from '../Session_/PlayerBlock'; -import styles from '../Session_/session.module.css'; -import { countDaysFrom } from 'App/date'; -import ReadNote from '../Session_/Player/Controls/components/ReadNote'; -import { fetchList as fetchMembers } from 'Duck/member'; - -const TABS = { - EVENTS: 'User Steps', - HEATMAPS: 'Click Map', -}; - -const InitLoader = connectPlayer((state) => ({ - loading: !state.initialized, -}))(Loader); - -const PlayerContentConnected = connectPlayer((state) => ({ - showEvents: !state.showEvents, - hasError: state.error, -}))(PlayerContent); - -function PlayerContent({ session, live, fullscreen, activeTab, setActiveTab, hasError }) { - const sessionDays = countDaysFrom(session.startedAt); - return ( -
- {hasError ? ( -
-
-
{sessionDays > 2 ? 'Session not found.' : 'This session is still being processed.'}
-
{sessionDays > 2 ? 'Please check your data retention policy.' : 'Please check it again in a few minutes.'}
-
-
- ) : ( -
-
-
- -
-
- {activeTab !== '' && } -
- )} -
- ); -} - -function RightMenu({ live, tabs, activeTab, setActiveTab, fullscreen }) { - return !live && !fullscreen && ; -} - -function WebPlayer(props) { - const { session, toggleFullscreen, closeBottomBlock, live, fullscreen, jwt, fetchList } = props; - const { notesStore } = useStore() - const [activeTab, setActiveTab] = useState(''); - const [showNoteModal, setShowNote] = useState(false) - const [noteItem, setNoteItem] = useState(null) - - useEffect(() => { - fetchList('issues'); - initPlayer(session, jwt); - props.fetchMembers() - - notesStore.fetchSessionNotes(session.sessionId).then(r => { - injectNotes(r) - const note = props.query.get('note'); - if (note) { - Controls.pause() - setNoteItem(notesStore.getNoteById(parseInt(note, 10), r)) - setShowNote(true) - } - }) - - const jumptTime = props.query.get('jumpto'); - if (jumptTime) { - Controls.jump(parseInt(jumptTime)); - } - - return () => cleanPlayer(); - }, [session.sessionId]); - - // LAYOUT (TODO: local layout state - useContext or something..) - useEffect( - () => () => { - toggleFullscreen(false); - closeBottomBlock(); - }, - [] - ); - - const onNoteClose = () => {setShowNote(false); Controls.togglePlay()} - return ( - - - - - - {showNoteModal ? ( - m.id === noteItem?.userId)?.email || ''} - note={noteItem} - onClose={onNoteClose} - notFound={!noteItem} - /> - ) : null} - - - - ); -} - -export default connect( - (state) => ({ - session: state.getIn(['sessions', 'current']), - jwt: state.get('jwt'), - fullscreen: state.getIn(['components', 'player', 'fullscreen']), - showEvents: state.get('showEvents'), - members: state.getIn(['members', 'list']), - }), - { - toggleFullscreen, - closeBottomBlock, - fetchList, - fetchMembers, - } -)(withLocationHandlers()(WebPlayer)); diff --git a/frontend/app/components/Session/WebPlayer.tsx b/frontend/app/components/Session/WebPlayer.tsx new file mode 100644 index 000000000..811d6b9ba --- /dev/null +++ b/frontend/app/components/Session/WebPlayer.tsx @@ -0,0 +1,127 @@ +import React, { useEffect, useState } from 'react'; +import { connect } from 'react-redux'; +import { Modal } from 'UI'; +import { toggleFullscreen, closeBottomBlock } from 'Duck/components/player'; +import { fetchList } from 'Duck/integrations'; +import { + PlayerProvider, + createWebPlayer, +} from 'Player'; + +import withLocationHandlers from 'HOCs/withLocationHandlers'; +import { useStore } from 'App/mstore'; +import PlayerBlockHeader from '../Session_/PlayerBlockHeader'; +import ReadNote from '../Session_/Player/Controls/components/ReadNote'; +import { fetchList as fetchMembers } from 'Duck/member'; +import PlayerContent from './PlayerContent' +import { + IPlayerContext, + PlayerContext, + defaultContextValue +} from './playerContext' + +const TABS = { + EVENTS: 'User Steps', + HEATMAPS: 'Click Map', +}; + +function WebPlayer(props: any) { + const { session, toggleFullscreen, closeBottomBlock, live, fullscreen, jwt, fetchList } = props; + const { notesStore } = useStore(); + const [activeTab, setActiveTab] = useState(''); + const [showNoteModal, setShowNote] = useState(false); + const [noteItem, setNoteItem] = useState(null); + const [contextValue, setContextValue] = useState(defaultContextValue) + + useEffect(() => { + fetchList('issues'); + const [WebPlayerInst, PlayerStore] = createWebPlayer(session, jwt); + setContextValue({ player: WebPlayerInst, store: PlayerStore }) + + // initPlayer(session, jwt); TODOPlayer + props.fetchMembers(); + + notesStore.fetchSessionNotes(session.sessionId).then((r) => { + // WebPlayerInst.injectNotes(r); + // PlayerStore.update({ notes: r }) + const note = props.query.get('note'); + if (note) { + WebPlayerInst.pause(); + setNoteItem(notesStore.getNoteById(parseInt(note, 10), r)); + setShowNote(true); + } + }); + + const jumptTime = props.query.get('jumpto'); + if (jumptTime) { + WebPlayerInst.jump(parseInt(jumptTime)); + } + + return () => WebPlayerInst.clean(); + }, [session.sessionId]); + + // LAYOUT (TODO: local layout state - useContext or something..) + useEffect( + () => () => { + toggleFullscreen(false); + closeBottomBlock(); + }, + [] + ); + + const onNoteClose = () => { + setShowNote(false); + contextValue.player.togglePlay(); + }; + + if (!contextValue.player) return null; + + return ( + + + <> + + {/* @ts-ignore */} + + + {showNoteModal ? ( + ) => m.id === noteItem?.userId)?.email || ''} + note={noteItem} + onClose={onNoteClose} + notFound={!noteItem} + /> + ) : null} + + + + + ); +} + +export default connect( + (state: any) => ({ + session: state.getIn(['sessions', 'current']), + jwt: state.get('jwt'), + fullscreen: state.getIn(['components', 'player', 'fullscreen']), + showEvents: state.get('showEvents'), + members: state.getIn(['members', 'list']), + }), + { + toggleFullscreen, + closeBottomBlock, + fetchList, + fetchMembers, + } +)(withLocationHandlers()(WebPlayer)); diff --git a/frontend/app/components/Session/playerContext.ts b/frontend/app/components/Session/playerContext.ts new file mode 100644 index 000000000..7068b982f --- /dev/null +++ b/frontend/app/components/Session/playerContext.ts @@ -0,0 +1,12 @@ +import { createContext } from 'react'; +import { + IWebPlayer, + IStore +} from 'Player' + +export interface IPlayerContext { + player: IWebPlayer + store: IStore, +} +export const defaultContextValue: IPlayerContext = { player: undefined, store: undefined} +export const PlayerContext = createContext(defaultContextValue); diff --git a/frontend/app/components/Session_/Console/Console.js b/frontend/app/components/Session_/Console/Console.js deleted file mode 100644 index 3c4a3752c..000000000 --- a/frontend/app/components/Session_/Console/Console.js +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react' -import { connectPlayer, jump } from 'Player'; -import ConsoleContent from './ConsoleContent'; - -@connectPlayer(state => ({ - logs: state.logList, - // time: state.time, - livePlay: state.livePlay, - listNow: state.logListNow, -})) -export default class Console extends React.PureComponent { - render() { - const { logs, time, listNow } = this.props; - return ( - - ); - } -} diff --git a/frontend/app/components/Session_/Console/ConsoleContent.js b/frontend/app/components/Session_/Console/ConsoleContent.js deleted file mode 100644 index a7482b69e..000000000 --- a/frontend/app/components/Session_/Console/ConsoleContent.js +++ /dev/null @@ -1,123 +0,0 @@ -import React from 'react'; -import cn from 'classnames'; -import { getRE } from 'App/utils'; -import { Icon, NoContent, Tabs, Input } from 'UI'; -import { jump } from 'Player'; -import { LEVEL } from 'Types/session/log'; -import Autoscroll from '../Autoscroll'; -import BottomBlock from '../BottomBlock'; -import stl from './console.module.css'; -import ConsoleRow from './ConsoleRow'; -// import { Duration } from 'luxon'; - -const ALL = 'ALL'; -const INFO = 'INFO'; -const WARNINGS = 'WARNINGS'; -const ERRORS = 'ERRORS'; - -const LEVEL_TAB = { - [LEVEL.INFO]: INFO, - [LEVEL.LOG]: INFO, - [LEVEL.WARNING]: WARNINGS, - [LEVEL.ERROR]: ERRORS, - [LEVEL.EXCEPTION]: ERRORS, -}; - -const TABS = [ALL, ERRORS, WARNINGS, INFO].map((tab) => ({ text: tab, key: tab })); - -// eslint-disable-next-line complexity -const getIconProps = (level) => { - switch (level) { - case LEVEL.INFO: - case LEVEL.LOG: - return { - name: 'console/info', - color: 'blue2', - }; - case LEVEL.WARN: - case LEVEL.WARNING: - return { - name: 'console/warning', - color: 'red2', - }; - case LEVEL.ERROR: - return { - name: 'console/error', - color: 'red', - }; - } - return null; -}; - -function renderWithNL(s = '') { - if (typeof s !== 'string') return ''; - return s.split('\n').map((line, i) =>
{line}
); -} - -export default class ConsoleContent extends React.PureComponent { - state = { - filter: '', - activeTab: ALL, - }; - onTabClick = (activeTab) => this.setState({ activeTab }); - onFilterChange = ({ target: { value } }) => this.setState({ filter: value }); - - render() { - const { logs, isResult, additionalHeight, logsNow } = this.props; - const time = logsNow.length > 0 ? logsNow[logsNow.length - 1].time : undefined; - const { filter, activeTab, currentError } = this.state; - const filterRE = getRE(filter, 'i'); - const filtered = logs.filter(({ level, value }) => - activeTab === ALL - ? filterRE.test(value) - : filterRE.test(value) && LEVEL_TAB[level] === activeTab - ); - - const lastIndex = filtered.filter((item) => item.time <= time).length - 1; - - return ( - <> - - -
- Console - -
- -
- - - - No Data - - } - size="small" - show={filtered.length === 0} - > - - {filtered.map((l) => ( - - ))} - - - -
- - ); - } -} diff --git a/frontend/app/components/Session_/Console/ConsoleRow/ConsoleRow.tsx b/frontend/app/components/Session_/Console/ConsoleRow/ConsoleRow.tsx deleted file mode 100644 index c87ff3f9c..000000000 --- a/frontend/app/components/Session_/Console/ConsoleRow/ConsoleRow.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, { useState } from 'react'; -import cn from 'classnames'; -import stl from '../console.module.css'; -import { Icon } from 'UI'; -import JumpButton from 'Shared/DevTools/JumpButton'; - -interface Props { - log: any; - iconProps: any; - jump?: any; - renderWithNL?: any; -} -function ConsoleRow(props: Props) { - const { log, iconProps, jump, renderWithNL } = props; - const [expanded, setExpanded] = useState(false); - const lines = log.value.split('\n').filter((l: any) => !!l); - const canExpand = lines.length > 1; - return ( -
setExpanded(!expanded)} - > -
- -
- {/*
- {Duration.fromMillis(log.time).toFormat('mm:ss.SSS')} -
*/} -
-
- {canExpand && ( - - )} - {renderWithNL(lines.pop())} -
- {canExpand && expanded && lines.map((l: any) =>
{l}
)} -
- jump(log.time)} /> -
- ); -} - -export default ConsoleRow; diff --git a/frontend/app/components/Session_/Console/ConsoleRow/index.ts b/frontend/app/components/Session_/Console/ConsoleRow/index.ts deleted file mode 100644 index c9140d748..000000000 --- a/frontend/app/components/Session_/Console/ConsoleRow/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './ConsoleRow'; diff --git a/frontend/app/components/Session_/Console/console.module.css b/frontend/app/components/Session_/Console/console.module.css deleted file mode 100644 index 2da78f540..000000000 --- a/frontend/app/components/Session_/Console/console.module.css +++ /dev/null @@ -1,38 +0,0 @@ - -.message { - overflow-x: auto; - margin-left: 10px; - font-size: 13px; - overflow-x: auto; - &::-webkit-scrollbar { - height: 2px; - } -} - -.line { - font-family: 'Menlo', 'monaco', 'consolas', monospace; - /* margin-top: -1px; ??? */ - display: flex; - align-items: flex-start; - border-bottom: solid thin $gray-light-shade; - &:hover { - background-coor: $active-blue !important; - } -} - -.timestamp { - -} - -.activeRow { - background-color: $teal-light !important; -} - -.icon { - padding-top: 4px; - margin-right: 7px; -} - -.inactiveRow { - opacity: 0.5; -} \ No newline at end of file diff --git a/frontend/app/components/Session_/Inspector/index.js b/frontend/app/components/Session_/Inspector/index.js index f76834fee..38d44149a 100644 --- a/frontend/app/components/Session_/Inspector/index.js +++ b/frontend/app/components/Session_/Inspector/index.js @@ -53,10 +53,10 @@ export default function Inspector () { if (!doc) return null; return ( - +
markElement(null) } className={stl.wrapper}> - ); -} \ No newline at end of file +} diff --git a/frontend/app/components/Session_/LongTasks/LongTasks.js b/frontend/app/components/Session_/LongTasks/LongTasks.js index fd3b4cc17..2fa80fd01 100644 --- a/frontend/app/components/Session_/LongTasks/LongTasks.js +++ b/frontend/app/components/Session_/LongTasks/LongTasks.js @@ -40,12 +40,12 @@ export default class GraphQL extends React.PureComponent { const { filter, current } = this.state; const filterRE = getRE(filter, 'i'); const filtered = list - .filter(({ containerType, context, containerName = "", containerId = "", containerSrc="" }) => - filterRE.test(containerName) || + .filter(({ containerType, context, containerName = "", containerId = "", containerSrc="" }) => + filterRE.test(containerName) || filterRE.test(containerId) || filterRE.test(containerSrc) || filterRE.test(CONTEXTS[ context ]) || - filterRE.test(CONTAINER_TYPES[ containerType ])); + filterRE.test(CONTAINER_TYPES[ containerType ])); const lastIndex = filtered.filter(item => item.time <= time).length - 1; return ( @@ -64,7 +64,7 @@ export default class GraphQL extends React.PureComponent { - Learn more + Learn more about Long Tasks API } diff --git a/frontend/app/components/Session_/Player/Player.js b/frontend/app/components/Session_/Player/Player.js index 48d881c29..ea3640847 100644 --- a/frontend/app/components/Session_/Player/Player.js +++ b/frontend/app/components/Session_/Player/Player.js @@ -20,21 +20,13 @@ import { OVERVIEW, } from 'Duck/components/player'; import NetworkPanel from 'Shared/DevTools/NetworkPanel'; -import Console from '../Console/Console'; import StackEvents from '../StackEvents/StackEvents'; import Storage from '../Storage'; -import Profiler from '../Profiler'; import { ConnectedPerformance } from '../Performance'; import GraphQL from '../GraphQL'; import Exceptions from '../Exceptions/Exceptions'; import LongTasks from '../LongTasks'; import Inspector from '../Inspector'; -import { - attach as attachPlayer, - Controls as PlayerControls, - scale as scalePlayerScreen, - connectPlayer, -} from 'Player'; import Controls from './Controls'; import Overlay from './Overlay'; import stl from './player.module.css'; @@ -42,70 +34,47 @@ import { updateLastPlayedSession } from 'Duck/sessions'; import OverviewPanel from '../OverviewPanel'; import ConsolePanel from 'Shared/DevTools/ConsolePanel'; import ProfilerPanel from 'Shared/DevTools/ProfilerPanel'; +import { PlayerContext } from 'App/components/Session/playerContext'; -@connectPlayer((state) => ({ - live: state.live, -})) -@connect( - (state) => { - const isAssist = window.location.pathname.includes('/assist/'); - return { - fullscreen: state.getIn(['components', 'player', 'fullscreen']), - nextId: state.getIn(['sessions', 'nextId']), - sessionId: state.getIn(['sessions', 'current', 'sessionId']), - closedLive: - !!state.getIn(['sessions', 'errors']) || - (isAssist && !state.getIn(['sessions', 'current', 'live'])), - }; - }, - { - hideTargetDefiner, +function Player(props) { + const { + className, + bottomBlockIsActive, + fullscreen, fullscreenOff, - updateLastPlayedSession, - } -) -export default class Player extends React.PureComponent { - screenWrapper = React.createRef(); + nextId, + closedLive, + bottomBlock, + activeTab, + } = props; + const playerContext = React.useContext(PlayerContext) + const screenWrapper = React.useRef(); - componentDidUpdate(prevProps) { - if ( - [prevProps.bottomBlock, this.props.bottomBlock].includes(NONE) || - prevProps.fullscreen !== this.props.fullscreen - ) { - scalePlayerScreen(); + React.useEffect(() => { + props.updateLastPlayedSession(props.sessionId); + if (!props.closedLive) { + const parentElement = findDOMNode(screenWrapper.current); //TODO: good architecture + playerContext.player.attach(parentElement); } - } - componentDidMount() { - this.props.updateLastPlayedSession(this.props.sessionId); - if (this.props.closedLive) return; + }, []) - const parentElement = findDOMNode(this.screenWrapper.current); //TODO: good architecture - attachPlayer(parentElement); - } + React.useEffect(() => { + playerContext.player.scale(); + }, [props.bottomBlock, props.fullscreen, playerContext.player]) - render() { - const { - className, - bottomBlockIsActive, - fullscreen, - fullscreenOff, - nextId, - closedLive, - bottomBlock, - activeTab, - } = this.props; + if (!playerContext.player) return null; - const maxWidth = activeTab ? 'calc(100vw - 270px)' : '100vw'; - return ( -
{fullscreen && }
- -
+ +
{!fullscreen && !!bottomBlock && (
@@ -125,8 +94,25 @@ export default class Player extends React.PureComponent { {bottomBlock === INSPECTOR && }
)} - +
- ); - } + ) } + +export default connect((state) => { + const isAssist = window.location.pathname.includes('/assist/'); + return { + fullscreen: state.getIn(['components', 'player', 'fullscreen']), + nextId: state.getIn(['sessions', 'nextId']), + sessionId: state.getIn(['sessions', 'current', 'sessionId']), + closedLive: + !!state.getIn(['sessions', 'errors']) || + (isAssist && !state.getIn(['sessions', 'current', 'live'])), + }; + }, + { + hideTargetDefiner, + fullscreenOff, + updateLastPlayedSession, + } +)(Player) diff --git a/frontend/app/components/Session_/Profiler/Profiler.js b/frontend/app/components/Session_/Profiler/Profiler.js index 398560a3d..9b9e6e42c 100644 --- a/frontend/app/components/Session_/Profiler/Profiler.js +++ b/frontend/app/components/Session_/Profiler/Profiler.js @@ -33,7 +33,7 @@ export default class Profiler extends React.PureComponent { return ( - } @@ -55,7 +55,7 @@ export default class Profiler extends React.PureComponent { /> - +export type IWebPlayer = WebPlayer +export type IWebPlayerStore = Store -export function createWebPlayer(session, wrapStore?: (s:WebPlayerStore) => WebPlayerStore): [WebPlayer, WebPlayerStore] { +export function createWebPlayer(session: Record, wrapStore?: (s:IWebPlayerStore) => IWebPlayerStore): [IWebPlayer, IWebPlayerStore] { let store: WebPlayerStore = new SimpleStore({ ...Player.INITIAL_STATE, ...MM_INITIAL_STATE, @@ -20,7 +22,7 @@ export function createWebPlayer(session, wrapStore?: (s:WebPlayerStore) => WebPl } -export function createLiveWebPlayer(session, config: RTCIceServer[], wrapStore?: (s:WebPlayerStore) => WebPlayerStore): [WebPlayer, WebPlayerStore] { +export function createLiveWebPlayer(session: Record, config: RTCIceServer[], wrapStore?: (s:IWebPlayerStore) => IWebPlayerStore): [IWebPlayer, IWebPlayerStore] { let store: WebPlayerStore = new SimpleStore({ ...Player.INITIAL_STATE, ...MM_INITIAL_STATE, @@ -30,4 +32,4 @@ export function createLiveWebPlayer(session, config: RTCIceServer[], wrapStore?: } const player = new WebPlayer(store, session, config, true) return [player, store] -} \ No newline at end of file +} From 93f65211a59b42fb061ac4a1ed3edb30d2651d86 Mon Sep 17 00:00:00 2001 From: sylenien Date: Tue, 22 Nov 2022 14:27:25 +0100 Subject: [PATCH 018/252] refactor(ui/player): fix store type for context --- frontend/app/components/Session/playerContext.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/app/components/Session/playerContext.ts b/frontend/app/components/Session/playerContext.ts index 7068b982f..1c3ae9ab5 100644 --- a/frontend/app/components/Session/playerContext.ts +++ b/frontend/app/components/Session/playerContext.ts @@ -1,12 +1,12 @@ import { createContext } from 'react'; import { IWebPlayer, - IStore + IWebPlayerStore } from 'Player' export interface IPlayerContext { player: IWebPlayer - store: IStore, + store: IWebPlayerStore, } export const defaultContextValue: IPlayerContext = { player: undefined, store: undefined} export const PlayerContext = createContext(defaultContextValue); From 72d8aa42c39c049cf416c786d28ba57fa1915069 Mon Sep 17 00:00:00 2001 From: MauricioGarciaS <47052044+MauricioGarciaS@users.noreply.github.com> Date: Tue, 22 Nov 2022 15:05:34 +0100 Subject: [PATCH 019/252] Solved INSERT query issue --- ee/api/chalicelib/core/signals.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ee/api/chalicelib/core/signals.py b/ee/api/chalicelib/core/signals.py index 81d2180ff..a1eb26c44 100644 --- a/ee/api/chalicelib/core/signals.py +++ b/ee/api/chalicelib/core/signals.py @@ -8,11 +8,12 @@ from chalicelib.utils import pg_client def handle_frontend_signals(project_id: int, user_id: str, data: schemas_ee.SignalsSchema): res = {'errors': 'query not executed'} - insights_query = """INSERT INTO public.frontend_signals VALUES (%(project_id)s, %(user_id)s, %(timestamp)s, %(action)s, %(source)s, %(category)s, %(data)s)""" + insights_query = """INSERT INTO public.frontend_signals (project_id, user_id, timestamp, action, source, category, data) VALUES (%(project_id)s, %(user_id)s, %(timestamp)s, %(action)s, %(source)s, %(category)s, %(data)s)""" try: with pg_client.PostgresClient() as conn: query = conn.mogrify(insights_query, {'project_id': project_id, 'user_id': user_id, 'timestamp': data.timestamp, 'action': data.action, 'source': data.source, 'category': data.category, 'data': json.dumps(data.data)}) + logging.info(f'QUERY: {query}') conn.execute(query) # res = helper.dict_to_camel_case(conn.fetchone()) return None From 2634d6cbd075c2152880aedcfdd1d1208b88729b Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Tue, 22 Nov 2022 15:07:22 +0100 Subject: [PATCH 020/252] fix(frontend/player): wrap internal state instead of store --- frontend/app/player/create.ts | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/frontend/app/player/create.ts b/frontend/app/player/create.ts index e954fec03..7b665b35e 100644 --- a/frontend/app/player/create.ts +++ b/frontend/app/player/create.ts @@ -5,31 +5,37 @@ import Player, { State as PState } from './player/Player' import WebPlayer from './_web/WebPlayer' -type WebPlayerStore = Store +type WebState = PState & MMState +type WebPlayerStore = Store +export type IWebState = WebState export type IWebPlayer = WebPlayer -export type IWebPlayerStore = Store +export type IWebPlayerStore = WebPlayerStore -export function createWebPlayer(session: Record, wrapStore?: (s:IWebPlayerStore) => IWebPlayerStore): [IWebPlayer, IWebPlayerStore] { - let store: WebPlayerStore = new SimpleStore({ +export function createWebPlayer(session: Record, wrapState?: (s:IWebState) => IWebState): [IWebPlayer, IWebPlayerStore] { + let state: WebState = { ...Player.INITIAL_STATE, ...MM_INITIAL_STATE, - }) - if (wrapStore) { - store = wrapStore(store) } + if (wrapState) { + state = wrapState(state) + } + const store: WebPlayerStore = new SimpleStore(state) + const player = new WebPlayer(store, session, null, false) return [player, store] } -export function createLiveWebPlayer(session: Record, config: RTCIceServer[], wrapStore?: (s:IWebPlayerStore) => IWebPlayerStore): [IWebPlayer, IWebPlayerStore] { - let store: WebPlayerStore = new SimpleStore({ +export function createLiveWebPlayer(session: Record, config: RTCIceServer[], wrapState?: (s:IWebState) => IWebState): [IWebPlayer, IWebPlayerStore] { + let state: WebState = { ...Player.INITIAL_STATE, ...MM_INITIAL_STATE, - }) - if (wrapStore) { - store = wrapStore(store) } + if (wrapState) { + state = wrapState(state) + } + const store: WebPlayerStore = new SimpleStore(state) + const player = new WebPlayer(store, session, config, true) return [player, store] } From 0d57722fecfca5248e4e8ef0c89cfe7626fafa9e Mon Sep 17 00:00:00 2001 From: sylenien Date: Tue, 22 Nov 2022 15:20:22 +0100 Subject: [PATCH 021/252] refactor(ui/player): refactor timeline and overlay --- frontend/app/components/Session/WebPlayer.tsx | 8 +- .../Session_/Player/Controls/Timeline.js | 199 +++++++++--------- .../components/Session_/Player/Overlay.tsx | 72 +++---- .../app/components/Session_/Player/Player.js | 8 +- 4 files changed, 133 insertions(+), 154 deletions(-) diff --git a/frontend/app/components/Session/WebPlayer.tsx b/frontend/app/components/Session/WebPlayer.tsx index 811d6b9ba..c3565321a 100644 --- a/frontend/app/components/Session/WebPlayer.tsx +++ b/frontend/app/components/Session/WebPlayer.tsx @@ -7,7 +7,8 @@ import { PlayerProvider, createWebPlayer, } from 'Player'; - +import { makeAutoObservable } from 'mobx' +import { observer } from "mobx-react-lite" import withLocationHandlers from 'HOCs/withLocationHandlers'; import { useStore } from 'App/mstore'; import PlayerBlockHeader from '../Session_/PlayerBlockHeader'; @@ -35,7 +36,10 @@ function WebPlayer(props: any) { useEffect(() => { fetchList('issues'); - const [WebPlayerInst, PlayerStore] = createWebPlayer(session, jwt); + const [WebPlayerInst, PlayerStore] = createWebPlayer( + session, + (state) => makeAutoObservable(state) + ); setContextValue({ player: WebPlayerInst, store: PlayerStore }) // initPlayer(session, jwt); TODOPlayer diff --git a/frontend/app/components/Session_/Player/Controls/Timeline.js b/frontend/app/components/Session_/Player/Controls/Timeline.js index e3ce9788a..9a4d0595b 100644 --- a/frontend/app/components/Session_/Player/Controls/Timeline.js +++ b/frontend/app/components/Session_/Player/Controls/Timeline.js @@ -1,7 +1,6 @@ import React from 'react'; import { connect } from 'react-redux'; import { Icon } from 'UI' -import { connectPlayer, Controls, toggleTimetravel } from 'Player'; import TimeTracker from './TimeTracker'; import stl from './timeline.module.css'; import { setTimelinePointer, setTimelineHoverTime } from 'Duck/sessions'; @@ -9,6 +8,8 @@ import DraggableCircle from './DraggableCircle'; import CustomDragLayer from './CustomDragLayer'; import { debounce } from 'App/utils'; import TooltipContainer from './components/TooltipContainer'; +import { PlayerContext } from 'App/components/Session/playerContext'; +import { observer } from 'mobx-react-lite'; const BOUNDRY = 0; @@ -20,105 +21,63 @@ function getTimelinePosition(value, scale) { let deboucneJump = () => null; let debounceTooltipChange = () => null; -@connectPlayer((state) => ({ - playing: state.playing, - time: state.time, - skipIntervals: state.skipIntervals, - events: state.eventList, - skip: state.skip, - skipToIssue: state.skipToIssue, - disabled: state.cssLoading || state.messagesLoading || state.markedTargets, - endTime: state.endTime, - live: state.live, - notes: state.notes || [], // TODO: implement notes without interaction with Player state -})) -@connect( - (state) => ({ - issues: state.getIn(['sessions', 'current', 'issues']), - startedAt: state.getIn(['sessions', 'current', 'startedAt']), - tooltipVisible: state.getIn(['sessions', 'timeLineTooltip', 'isVisible']), - }), - { setTimelinePointer, setTimelineHoverTime } -) -export default class Timeline extends React.PureComponent { - progressRef = React.createRef(); - timelineRef = React.createRef(); - wasPlaying = false; - seekProgress = (e) => { - const time = this.getTime(e); - this.props.jump(time); - this.hideTimeTooltip(); - }; - loadAndSeek = async (e) => { - e.persist(); - await toggleTimetravel(); +function Timeline(props) { + const { player, store } = React.useContext(PlayerContext) + const [wasPlaying, setWasPlaying] = React.useState(false); + const { + playing, + time, + skipIntervals, + eventList: events, + skip, + skipToIssue, + disabled, + endTime, + live, + notes = [], + } = store.get() - setTimeout(() => { - this.seekProgress(e); - }); - }; + const progressRef = React.useRef(); + const timelineRef = React.useRef(); - jumpToTime = (e) => { - if (this.props.live && !this.props.liveTimeTravel) { - this.loadAndSeek(e); - } else { - this.seekProgress(e); - } - }; - getTime = (e, customEndTime) => { - const { endTime } = this.props; - const p = e.nativeEvent.offsetX / e.target.offsetWidth; - const targetTime = customEndTime || endTime; - const time = Math.max(Math.round(p * targetTime), 0); + const scale = 100 / endTime; - return time; - }; - - createEventClickHandler = (pointer) => (e) => { - e.stopPropagation(); - this.props.jump(pointer.time); - this.props.setTimelinePointer(pointer); - }; - - componentDidMount() { - const { issues, skipToIssue } = this.props; + React.useEffect(() => { + const { issues } = props; const firstIssue = issues.get(0); - deboucneJump = debounce(this.props.jump, 500); - debounceTooltipChange = debounce(this.props.setTimelineHoverTime, 50); + deboucneJump = debounce(player.jump, 500); + debounceTooltipChange = debounce(props.setTimelineHoverTime, 50); if (firstIssue && skipToIssue) { - this.props.jump(firstIssue.time); + player.jump(firstIssue.time); } - } + }, []) - onDragEnd = () => { - const { live, liveTimeTravel } = this.props; + const onDragEnd = () => { if (live && !liveTimeTravel) return; - if (this.wasPlaying) { - this.props.togglePlay(); + if (wasPlaying) { + player.togglePlay(); } }; - onDrag = (offset) => { - const { endTime, live, liveTimeTravel } = this.props; + const onDrag = (offset) => { if (live && !liveTimeTravel) return; - const p = (offset.x - BOUNDRY) / this.progressRef.current.offsetWidth; + const p = (offset.x - BOUNDRY) / progressRef.current.offsetWidth; const time = Math.max(Math.round(p * endTime), 0); deboucneJump(time); - this.hideTimeTooltip(); - if (this.props.playing) { - this.wasPlaying = true; - this.props.pause(); + hideTimeTooltip(); + if (playing) { + setWasPlaying(true) + player.pause(); } }; - getLiveTime = (e) => { - const { startedAt } = this.props; + const getLiveTime = (e) => { const duration = new Date().getTime() - startedAt; const p = e.nativeEvent.offsetX / e.target.offsetWidth; const time = Math.max(Math.round(p * duration), 0); @@ -126,23 +85,23 @@ export default class Timeline extends React.PureComponent { return [time, duration]; }; - showTimeTooltip = (e) => { - if (e.target !== this.progressRef.current && e.target !== this.timelineRef.current) { - return this.props.tooltipVisible && this.hideTimeTooltip(); + const showTimeTooltip = (e) => { + if (e.target !== progressRef.current && e.target !== timelineRef.current) { + return props.tooltipVisible && hideTimeTooltip(); } - const { live } = this.props; + let timeLineTooltip; if (live) { - const [time, duration] = this.getLiveTime(e); + const [time, duration] = getLiveTime(e); timeLineTooltip = { time: duration - time, offset: e.nativeEvent.offsetX, isVisible: true, }; } else { - const time = this.getTime(e); + const time = getTime(e); timeLineTooltip = { time: time, offset: e.nativeEvent.offsetX, @@ -153,18 +112,44 @@ export default class Timeline extends React.PureComponent { debounceTooltipChange(timeLineTooltip); }; - hideTimeTooltip = () => { + const hideTimeTooltip = () => { const timeLineTooltip = { isVisible: false }; debounceTooltipChange(timeLineTooltip); }; - render() { - const { events, skip, skipIntervals, disabled, endTime, live, notes } = this.props; + const seekProgress = (e) => { + const time = getTime(e); + player.jump(time); + hideTimeTooltip(); + }; - const scale = 100 / endTime; + const loadAndSeek = async (e) => { + e.persist(); + await toggleTimetravel(); - return ( -
{ + seekProgress(e); + }); + }; + + const jumpToTime = (e) => { + if (live && !liveTimeTravel) { + loadAndSeek(e); + } else { + seekProgress(e); + } + }; + + const getTime = (e, customEndTime) => { + const p = e.nativeEvent.offsetX / e.target.offsetWidth; + const targetTime = customEndTime || endTime; + const time = Math.max(Math.round(p * targetTime), 0); + + return time; + }; + + return ( +
{/* custo color is live */} - + {!live && skip ? skipIntervals.map((interval) => ( @@ -208,7 +193,7 @@ export default class Timeline extends React.PureComponent { }} /> )) : null} -
+
{events.map((e) => (
- ); - } + ) } + +export default connect( + (state) => ({ + issues: state.getIn(['sessions', 'current', 'issues']), + startedAt: state.getIn(['sessions', 'current', 'startedAt']), + tooltipVisible: state.getIn(['sessions', 'timeLineTooltip', 'isVisible']), + }), + { setTimelinePointer, setTimelineHoverTime } +)(observer(Timeline)) diff --git a/frontend/app/components/Session_/Player/Overlay.tsx b/frontend/app/components/Session_/Player/Overlay.tsx index 7b610ded1..d9a73c56a 100644 --- a/frontend/app/components/Session_/Player/Overlay.tsx +++ b/frontend/app/components/Session_/Player/Overlay.tsx @@ -1,7 +1,5 @@ import React from 'react'; -import { connectPlayer } from 'Player'; import { getStatusText } from 'Player'; -import type { MarkedTarget } from 'Player'; import { CallingState, ConnectionStatus, RemoteControlStatus } from 'Player'; import AutoplayTimer from './Overlay/AutoplayTimer'; @@ -10,45 +8,39 @@ import LiveStatusText from './Overlay/LiveStatusText'; import Loader from './Overlay/Loader'; import ElementsMarker from './Overlay/ElementsMarker'; import RequestingWindow, { WindowType } from 'App/components/Assist/RequestingWindow'; +import { PlayerContext } from 'App/components/Session/playerContext'; +import { observer } from 'mobx-react-lite'; interface Props { - playing: boolean, - completed: boolean, - inspectorMode: boolean, - loading: boolean, - live: boolean, - liveStatusText: string, - concetionStatus: ConnectionStatus, - autoplay: boolean, - markedTargets: MarkedTarget[] | null, - activeTargetIndex: number, - calling: CallingState, - remoteControl: RemoteControlStatus - nextId: string, - togglePlay: () => void, closedLive?: boolean, - livePlay?: boolean, } function Overlay({ - playing, - completed, - inspectorMode, - loading, - live, - liveStatusText, - concetionStatus, - autoplay, - markedTargets, - activeTargetIndex, nextId, - togglePlay, closedLive, - livePlay, - calling, - remoteControl, }: Props) { + const { player, store } = React.useContext(PlayerContext) + + const { + playing, + messagesLoading, + cssLoading, + completed, + autoplay, + inspectorMode, + live, + peerConnectionStatus, + markedTargets, + activeTargetIndex, + livePlay, + calling, + remoteControl, + } = store.get() + const loading = messagesLoading || cssLoading + const liveStatusText = getStatusText(peerConnectionStatus) + const concetionStatus = peerConnectionStatus + const showAutoplayTimer = !live && completed && autoplay && nextId const showPlayIconLayer = !live && !markedTargets && !inspectorMode && !loading && !showAutoplayTimer; const showLiveStatusText = live && livePlay && liveStatusText && !loading; @@ -65,7 +57,7 @@ function Overlay({ } { loading ? : null } { showPlayIconLayer && - + } { markedTargets && } @@ -74,18 +66,4 @@ function Overlay({ } -export default connectPlayer(state => ({ - playing: state.playing, - loading: state.messagesLoading || state.cssLoading, - completed: state.completed, - autoplay: state.autoplay, - inspectorMode: state.inspectorMode, - live: state.live, - liveStatusText: getStatusText(state.peerConnectionStatus), - concetionStatus: state.peerConnectionStatus, - markedTargets: state.markedTargets, - activeTargetIndex: state.activeTargetIndex, - livePlay: state.livePlay, - calling: state.calling, - remoteControl: state.remoteControl, -}))(Overlay); +export default observer(Overlay); diff --git a/frontend/app/components/Session_/Player/Player.js b/frontend/app/components/Session_/Player/Player.js index ea3640847..a37fca62b 100644 --- a/frontend/app/components/Session_/Player/Player.js +++ b/frontend/app/components/Session_/Player/Player.js @@ -73,7 +73,7 @@ function Player(props) { > {fullscreen && }
- +
{!fullscreen && !!bottomBlock && ( @@ -94,7 +94,11 @@ function Player(props) { {bottomBlock === INSPECTOR && }
)} - +
) } From e317d807f6af0c95930c2f7b89cb4028fb1dd9c0 Mon Sep 17 00:00:00 2001 From: MauricioGarciaS <47052044+MauricioGarciaS@users.noreply.github.com> Date: Tue, 22 Nov 2022 15:31:44 +0100 Subject: [PATCH 022/252] Fixes for API response --- ee/api/chalicelib/core/signals.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ee/api/chalicelib/core/signals.py b/ee/api/chalicelib/core/signals.py index a1eb26c44..ed0664bde 100644 --- a/ee/api/chalicelib/core/signals.py +++ b/ee/api/chalicelib/core/signals.py @@ -13,10 +13,9 @@ def handle_frontend_signals(project_id: int, user_id: str, data: schemas_ee.Sign with pg_client.PostgresClient() as conn: query = conn.mogrify(insights_query, {'project_id': project_id, 'user_id': user_id, 'timestamp': data.timestamp, 'action': data.action, 'source': data.source, 'category': data.category, 'data': json.dumps(data.data)}) - logging.info(f'QUERY: {query}') conn.execute(query) # res = helper.dict_to_camel_case(conn.fetchone()) - return None + return {'data': 'insertion succeded'} except Exception as e: + logging.info(f'Error while inserting: {e}') return {'errors': [e]} - return None From f2bc1e56e3000fb382409e8a4db1e5caabe26859 Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Tue, 22 Nov 2022 16:25:27 +0100 Subject: [PATCH 023/252] refactor(player): remove old store and singletone imports --- frontend/app/player/_store/index.js | 8 ++++---- frontend/app/player/index.js | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/app/player/_store/index.js b/frontend/app/player/_store/index.js index c2a987661..cad6dfaff 100644 --- a/frontend/app/player/_store/index.js +++ b/frontend/app/player/_store/index.js @@ -1,4 +1,4 @@ -export * from './connector'; -export * from './store'; -export * from './selectors'; -export { default } from './store'; \ No newline at end of file +// export * from './connector'; +// export * from './store'; +// export * from './selectors'; +// export { default } from './store'; \ No newline at end of file diff --git a/frontend/app/player/index.js b/frontend/app/player/index.js index 67d56e19c..41f1728c7 100644 --- a/frontend/app/player/index.js +++ b/frontend/app/player/index.js @@ -3,5 +3,5 @@ export * from './_web/assist/LocalStream'; export * from './_web/WebPlayer'; export * from './create'; -export * from './_store'; -export * from './_singletone'; +//export * from './_store'; +//export * from './_singletone'; From 8fd2ce7f8af62c18ec75048cd6536eb5985cc315 Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Tue, 22 Nov 2022 16:28:38 +0100 Subject: [PATCH 024/252] refactor(player):targetMarker functionality segregation; type fixes --- .../app/player/_web/InspectorController.ts | 6 +- frontend/app/player/_web/MessageManager.ts | 42 +++--- frontend/app/player/_web/Screen/Screen.ts | 10 +- frontend/app/player/_web/TargetMarker.ts | 134 ++++++++++++++++++ frontend/app/player/_web/WebPlayer.ts | 116 ++++----------- frontend/app/player/create.ts | 11 +- frontend/app/player/{index.js => index.ts} | 2 + frontend/app/player/player/Animator.ts | 5 +- 8 files changed, 201 insertions(+), 125 deletions(-) create mode 100644 frontend/app/player/_web/TargetMarker.ts rename frontend/app/player/{index.js => index.ts} (78%) diff --git a/frontend/app/player/_web/InspectorController.ts b/frontend/app/player/_web/InspectorController.ts index a78bad006..d2c4b6624 100644 --- a/frontend/app/player/_web/InspectorController.ts +++ b/frontend/app/player/_web/InspectorController.ts @@ -3,6 +3,7 @@ import Inspector from './Screen/Inspector' import Screen from './Screen/Screen' import type { Dimensions } from './Screen/types' + export default class InspectorController { private substitutor: Screen | null = null private inspector: Inspector | null = null @@ -16,13 +17,14 @@ export default class InspectorController { } enableInspector(clickCallback: (e: { target: Element }) => void): Document | null { - if (!this.screen.parentElement) return null; + const parent = this.screen.getParentElement() + if (!parent) return null; if (!this.substitutor) { this.substitutor = new Screen() this.marker = new Marker(this.substitutor.overlay, this.substitutor) this.inspector = new Inspector(this.substitutor, this.marker) //this.inspector.addClickListener(clickCallback, true) - this.substitutor.attach(this.screen.parentElement) + this.substitutor.attach(parent) } this.substitutor.display(false) diff --git a/frontend/app/player/_web/MessageManager.ts b/frontend/app/player/_web/MessageManager.ts index 14642c3dc..9af0a4b0a 100644 --- a/frontend/app/player/_web/MessageManager.ts +++ b/frontend/app/player/_web/MessageManager.ts @@ -24,15 +24,14 @@ import MFileReader from './messages/MFileReader'; import { loadFiles, requestEFSDom, requestEFSDevtools } from './network/loadFiles'; import { decryptSessionBytes } from './network/crypto'; -import { INITIAL_STATE as SUPER_INITIAL_STATE, State as SuperState } from './Screen/Screen'; -import { INITIAL_STATE as ASSIST_INITIAL_STATE, State as AssistState } from './assist/AssistManager'; +import { INITIAL_STATE as SCREEN_INITIAL_STATE, State as SuperState } from './Screen/Screen'; import Lists, { INITIAL_STATE as LISTS_INITIAL_STATE } from './Lists'; import type { PerformanceChartPoint } from './managers/PerformanceTrackManager'; import type { SkipInterval } from './managers/ActivityManager'; -export interface State extends SuperState, AssistState { +export interface State extends SuperState { performanceChartData: PerformanceChartPoint[], skipIntervals: SkipInterval[], connType?: string, @@ -54,24 +53,6 @@ export interface State extends SuperState, AssistState { lastMessageTime: number, } -export const INITIAL_STATE: State = { - ...SUPER_INITIAL_STATE, - ...LISTS_INITIAL_STATE, - ...ASSIST_INITIAL_STATE, - performanceChartData: [], - skipIntervals: [], - error: false, - devtoolsLoading: false, - - liveTimeTravel: false, - messagesLoading: false, - cssLoading: false, - get ready() { - return !this.messagesLoading && !this.cssLoading - }, - lastMessageTime: 0, -}; - import type { Message, @@ -93,6 +74,23 @@ const visualChanges = [ ] export default class MessageManager extends Screen { + static INITIAL_STATE: State = { + ...SCREEN_INITIAL_STATE, + ...LISTS_INITIAL_STATE, + performanceChartData: [], + skipIntervals: [], + error: false, + devtoolsLoading: false, + + liveTimeTravel: false, + messagesLoading: false, + cssLoading: false, + get ready() { + return !this.messagesLoading && !this.cssLoading + }, + lastMessageTime: 0, + } + private locationEventManager: ListWalker/**/ = new ListWalker(); private locationManager: ListWalker = new ListWalker(); private loadedLocationManager: ListWalker = new ListWalker(); @@ -553,7 +551,7 @@ export default class MessageManager extends Screen { // TODO: clean managers? clean() { - this.state.update(INITIAL_STATE); + this.state.update(MessageManager.INITIAL_STATE); this.incomingMessages.length = 0 } diff --git a/frontend/app/player/_web/Screen/Screen.ts b/frontend/app/player/_web/Screen/Screen.ts index 6d28f06fd..aa9e9aea2 100644 --- a/frontend/app/player/_web/Screen/Screen.ts +++ b/frontend/app/player/_web/Screen/Screen.ts @@ -54,9 +54,9 @@ export default class Screen { readonly cursor: Cursor private readonly iframe: HTMLIFrameElement; - protected readonly screen: HTMLDivElement; - protected readonly controlButton: HTMLDivElement; - protected parentElement: HTMLElement | null = null; + private readonly screen: HTMLDivElement; + private readonly controlButton: HTMLDivElement; + private parentElement: HTMLElement | null = null; constructor() { const iframe = document.createElement('iframe'); @@ -102,6 +102,10 @@ export default class Screen { }) } + getParentElement(): HTMLElement | null { + return this.parentElement + } + toggleBorder(isEnabled: boolean ) { const styles = isEnabled ? { border: '2px dashed blue' } : { border: 'unset'} return Object.assign(this.screen.style, styles) diff --git a/frontend/app/player/_web/TargetMarker.ts b/frontend/app/player/_web/TargetMarker.ts new file mode 100644 index 000000000..a2ca99cc2 --- /dev/null +++ b/frontend/app/player/_web/TargetMarker.ts @@ -0,0 +1,134 @@ +import type Screen from './Screen/Screen' +import type { Point } from './Screen/types' +import type { Store } from '../player/types' + + +function getOffset(el: Element, innerWindow: Window) { + const rect = el.getBoundingClientRect(); + return { + fixedLeft: rect.left + innerWindow.scrollX, + fixedTop: rect.top + innerWindow.scrollY, + rect, + }; +} + +interface BoundingRect { + top: number, + left: number, + width: number, + height: number, +} + +export interface MarkedTarget { + boundingRect: BoundingRect, + el: Element, + selector: string, + count: number, + index: number, + active?: boolean, + percent: number +} + +export interface State { + markedTargets: MarkedTarget[] | null, + activeTargetIndex: number, +} + + +export default class TargetMarker { + static INITIAL_STATE: State = { + markedTargets: null, + activeTargetIndex: 0 + } + + constructor( + private readonly screen: Screen, + private readonly store: Store, + ) {} + + updateMarketTargets() { + const { markedTargets } = this.store.get() + if (markedTargets) { + this.store.update({ + markedTargets: markedTargets.map((mt: any) => ({ + ...mt, + boundingRect: this.calculateRelativeBoundingRect(mt.el), + })), + }); + } + } + + private calculateRelativeBoundingRect(el: Element): BoundingRect { + const parentEl = this.screen.getParentElement() + if (!parentEl) return {top:0, left:0, width:0,height:0} //TODO: can be initialized(?) on mounted screen only + const { top, left, width, height } = el.getBoundingClientRect() + const s = this.screen.getScale() + const scrinRect = this.screen.overlay.getBoundingClientRect() //this.screen.getBoundingClientRect() (now private) + const parentRect = parentEl.getBoundingClientRect() + + return { + top: top*s + scrinRect.top - parentRect.top, + left: left*s + scrinRect.left - parentRect.left, + width: width*s, + height: height*s, + } + } + + setActiveTarget(index: number) { + const window = this.screen.window + const markedTargets: MarkedTarget[] | null = this.store.get().markedTargets + const target = markedTargets && markedTargets[index] + if (target && window) { + const { fixedTop, rect } = getOffset(target.el, window) + const scrollToY = fixedTop - window.innerHeight / 1.5 + if (rect.top < 0 || rect.top > window.innerHeight) { + // behavior hack TODO: fix it somehow when they will decide to remove it from browser api + // @ts-ignore + window.scrollTo({ top: scrollToY, behavior: 'instant' }) + setTimeout(() => { + if (!markedTargets) { return } + this.store.update({ + markedTargets: markedTargets.map(t => t === target ? { + ...target, + boundingRect: this.calculateRelativeBoundingRect(target.el), + } : t) + }) + }, 0) + } + + } + this.store.update({ activeTargetIndex: index }); + } + + private actualScroll: Point | null = null + markTargets(selections: { selector: string, count: number }[] | null) { + if (selections) { + const totalCount = selections.reduce((a, b) => { + return a + b.count + }, 0); + const markedTargets: MarkedTarget[] = []; + let index = 0; + selections.forEach((s) => { + const el = this.screen.getElementBySelector(s.selector); + if (!el) return; + markedTargets.push({ + ...s, + el, + index: index++, + percent: Math.round((s.count * 100) / totalCount), + boundingRect: this.calculateRelativeBoundingRect(el), + count: s.count, + }) + }); + this.actualScroll = this.screen.getCurrentScroll() + this.store.update({ markedTargets }); + } else { + if (this.actualScroll) { + this.screen.window?.scrollTo(this.actualScroll.x, this.actualScroll.y) + this.actualScroll = null + } + this.store.update({ markedTargets: null }); + } + } + +} \ No newline at end of file diff --git a/frontend/app/player/_web/WebPlayer.ts b/frontend/app/player/_web/WebPlayer.ts index de3dcfa49..a2da13d4c 100644 --- a/frontend/app/player/_web/WebPlayer.ts +++ b/frontend/app/player/_web/WebPlayer.ts @@ -3,18 +3,33 @@ import Player, { State as PlayerState } from '../player/Player' import MessageManager from './MessageManager' import InspectorController from './InspectorController' -import AssistManager from './assist/AssistManager' +import TargetMarker from './TargetMarker' +import AssistManager, { + INITIAL_STATE as ASSIST_INITIAL_STATE, +} from './assist/AssistManager' import Screen from './Screen/Screen' -import type { State as MMState } from './MessageManager' + +// export type State = typeof WebPlayer.INITIAL_STATE export default class WebPlayer extends Player { + static INITIAL_STATE = { + ...Player.INITIAL_STATE, + ...TargetMarker.INITIAL_STATE, + + ...MessageManager.INITIAL_STATE, + ...ASSIST_INITIAL_STATE, + + inspectorMode: false, + } + private readonly screen: Screen private readonly inspectorController: InspectorController protected readonly messageManager: MessageManager assistManager: AssistManager // public so far + private targetMarker: TargetMarker - constructor(private wpState: Store, session, config: RTCIceServer[], live: boolean) { + constructor(private wpState: Store, session, config: RTCIceServer[], live: boolean) { // TODO: separate screen from manager const screen = new MessageManager(session, wpState, config, live) super(wpState, screen) @@ -24,6 +39,8 @@ export default class WebPlayer extends Player { // TODO: separate LiveWebPlayer this.assistManager = new AssistManager(session, this.messageManager, config, wpState) + this.targetMarker = new TargetMarker(this.screen) + this.inspectorController = new InspectorController(screen) @@ -77,94 +94,14 @@ export default class WebPlayer extends Player { } } - updateMarketTargets() { - // const { markedTargets } = getState(); - // if (markedTargets) { - // update({ - // markedTargets: markedTargets.map((mt: any) => ({ - // ...mt, - // boundingRect: this.calculateRelativeBoundingRect(mt.el), - // })), - // }); - // } + setActiveTarget(args: Parameters) { + this.targetMarker.setActiveTarget(...args) + } + markTargets(args: Parameters) { + this.pause() + this.targetMarker.markTargets(...args) } - // private calculateRelativeBoundingRect(el: Element): BoundingRect { - // if (!this.parentElement) return {top:0, left:0, width:0,height:0} //TODO - // const { top, left, width, height } = el.getBoundingClientRect(); - // const s = this.getScale(); - // const scrinRect = this.screen.getBoundingClientRect(); - // const parentRect = this.parentElement.getBoundingClientRect(); - - // return { - // top: top*s + scrinRect.top - parentRect.top, - // left: left*s + scrinRect.left - parentRect.left, - // width: width*s, - // height: height*s, - // } - // } - - setActiveTarget(index: number) { - // const window = this.window - // const markedTargets: MarkedTarget[] | null = getState().markedTargets - // const target = markedTargets && markedTargets[index] - // if (target && window) { - // const { fixedTop, rect } = getOffset(target.el, window) - // const scrollToY = fixedTop - window.innerHeight / 1.5 - // if (rect.top < 0 || rect.top > window.innerHeight) { - // // behavior hack TODO: fix it somehow when they will decide to remove it from browser api - // // @ts-ignore - // window.scrollTo({ top: scrollToY, behavior: 'instant' }) - // setTimeout(() => { - // if (!markedTargets) { return } - // update({ - // markedTargets: markedTargets.map(t => t === target ? { - // ...target, - // boundingRect: this.calculateRelativeBoundingRect(target.el), - // } : t) - // }) - // }, 0) - // } - - // } - // update({ activeTargetIndex: index }); - } - - // private actualScroll: Point | null = null - private setMarkedTargets(selections: { selector: string, count: number }[] | null) { - // if (selections) { - // const totalCount = selections.reduce((a, b) => { - // return a + b.count - // }, 0); - // const markedTargets: MarkedTarget[] = []; - // let index = 0; - // selections.forEach((s) => { - // const el = this.getElementBySelector(s.selector); - // if (!el) return; - // markedTargets.push({ - // ...s, - // el, - // index: index++, - // percent: Math.round((s.count * 100) / totalCount), - // boundingRect: this.calculateRelativeBoundingRect(el), - // count: s.count, - // }) - // }); - // this.actualScroll = this.getCurrentScroll() - // update({ markedTargets }); - // } else { - // if (this.actualScroll) { - // this.screen.window?.scrollTo(this.actualScroll.x, this.actualScroll.y) - // this.actualScroll = null - // } - // update({ markedTargets: null }); - // } - } - - markTargets(targets: { selector: string, count: number }[] | null) { - // this.pause(); - // this.setMarkedTargets(targets); - } // TODO async toggleTimetravel() { @@ -176,6 +113,7 @@ export default class WebPlayer extends Player { toggleUserName(name?: string) { this.screen.cursor.showTag(name) } + clean() { super.clean() this.assistManager.clean() diff --git a/frontend/app/player/create.ts b/frontend/app/player/create.ts index 7b665b35e..beacd49e6 100644 --- a/frontend/app/player/create.ts +++ b/frontend/app/player/create.ts @@ -1,11 +1,8 @@ import SimpleStore from './_common/SimpleStore' import type { Store } from './player/types' -import { State as MMState, INITIAL_STATE as MM_INITIAL_STATE } from './_web/MessageManager' -import Player, { State as PState } from './player/Player' -import WebPlayer from './_web/WebPlayer' +import WebPlayer, { State as WebState} from './_web/WebPlayer' -type WebState = PState & MMState type WebPlayerStore = Store export type IWebState = WebState export type IWebPlayer = WebPlayer @@ -13,8 +10,7 @@ export type IWebPlayerStore = WebPlayerStore export function createWebPlayer(session: Record, wrapState?: (s:IWebState) => IWebState): [IWebPlayer, IWebPlayerStore] { let state: WebState = { - ...Player.INITIAL_STATE, - ...MM_INITIAL_STATE, + ...WebPlayer.INITIAL_STATE, } if (wrapState) { state = wrapState(state) @@ -28,8 +24,7 @@ export function createWebPlayer(session: Record, wrapState?: (s:IWe export function createLiveWebPlayer(session: Record, config: RTCIceServer[], wrapState?: (s:IWebState) => IWebState): [IWebPlayer, IWebPlayerStore] { let state: WebState = { - ...Player.INITIAL_STATE, - ...MM_INITIAL_STATE, + ...WebPlayer.INITIAL_STATE, } if (wrapState) { state = wrapState(state) diff --git a/frontend/app/player/index.js b/frontend/app/player/index.ts similarity index 78% rename from frontend/app/player/index.js rename to frontend/app/player/index.ts index 41f1728c7..55266aa5f 100644 --- a/frontend/app/player/index.js +++ b/frontend/app/player/index.ts @@ -3,5 +3,7 @@ export * from './_web/assist/LocalStream'; export * from './_web/WebPlayer'; export * from './create'; +export type { MarkedTarget } from './_web/TargetMarker' + //export * from './_store'; //export * from './_singletone'; diff --git a/frontend/app/player/player/Animator.ts b/frontend/app/player/player/Animator.ts index 871da629a..a75290929 100644 --- a/frontend/app/player/player/Animator.ts +++ b/frontend/app/player/player/Animator.ts @@ -27,13 +27,14 @@ export interface SetState { completed: boolean live: boolean livePlay: boolean + + endTime: number } export interface GetState extends SetState { skip: boolean speed: number skipIntervals: Interval[] - endTime: number ready: boolean lastMessageTime: number @@ -46,6 +47,8 @@ export default class Animator { completed: false, live: false, livePlay: false, + + endTime: 0, } as const private animationFrameRequestId: number = 0 From 56327fa90906008e820a454dea367effabf311d5 Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Tue, 22 Nov 2022 17:07:14 +0100 Subject: [PATCH 025/252] refactor(player): decouple live state from MessageManager --- frontend/app/player/_web/Lists.ts | 2 +- frontend/app/player/_web/MessageManager.ts | 47 +++++++--------------- frontend/app/player/_web/WebPlayer.ts | 21 ++++++++-- 3 files changed, 32 insertions(+), 38 deletions(-) diff --git a/frontend/app/player/_web/Lists.ts b/frontend/app/player/_web/Lists.ts index 4eb5de6db..fdf560c93 100644 --- a/frontend/app/player/_web/Lists.ts +++ b/frontend/app/player/_web/Lists.ts @@ -29,7 +29,7 @@ type MarkedListsObject = { } type ListsObject = SimpleListsObject & MarkedListsObject -type InitialLists = { +export type InitialLists = { [key in typeof LIST_NAMES[number]]: any[] } diff --git a/frontend/app/player/_web/MessageManager.ts b/frontend/app/player/_web/MessageManager.ts index 9af0a4b0a..8eeb74f19 100644 --- a/frontend/app/player/_web/MessageManager.ts +++ b/frontend/app/player/_web/MessageManager.ts @@ -27,6 +27,7 @@ import { decryptSessionBytes } from './network/crypto'; import { INITIAL_STATE as SCREEN_INITIAL_STATE, State as SuperState } from './Screen/Screen'; import Lists, { INITIAL_STATE as LISTS_INITIAL_STATE } from './Lists'; +import type { InitialLists } from './Lists' import type { PerformanceChartPoint } from './managers/PerformanceTrackManager'; import type { SkipInterval } from './managers/ActivityManager'; @@ -45,7 +46,6 @@ export interface State extends SuperState { error: boolean, devtoolsLoading: boolean, - liveTimeTravel: boolean, messagesLoading: boolean, cssLoading: boolean, @@ -82,7 +82,6 @@ export default class MessageManager extends Screen { error: false, devtoolsLoading: false, - liveTimeTravel: false, messagesLoading: false, cssLoading: false, get ready() { @@ -118,39 +117,25 @@ export default class MessageManager extends Screen { constructor( private readonly session: any /*Session*/, private readonly state: Store, - config: any, - live: boolean, + initialLists?: Partial ) { super(); this.pagesManager = new PagesManager(this, this.session.isMobile) - this.mouseMoveManager = new MouseMoveManager(this); + this.mouseMoveManager = new MouseMoveManager(this) - this.sessionStart = this.session.startedAt; + this.sessionStart = this.session.startedAt - if (live) { - this.lists = new Lists() - } else { - this.activityManager = new ActivityManager(this.session.duration.milliseconds); - /* == REFACTOR_ME == */ - const eventList = session.events.toJSON(); - // TODO: fix types for events, remove immutable js - eventList.forEach((e: Record) => { - if (e.type === EVENT_TYPES.LOCATION) { //TODO type system - this.locationEventManager.append(e); - } - }) + this.lists = new Lists(initialLists) + initialLists && initialLists.event.forEach((e: Record) => { // TODO: to one of "Moveable" module + if (e.type === EVENT_TYPES.LOCATION) { + this.locationEventManager.append(e); + } + }) - this.lists = new Lists({ - event: eventList, - stack: session.stackEvents.toJSON(), - resource: session.resources.toJSON(), - exceptions: session.errors, - }) + this.activityManager = new ActivityManager(this.session.duration.milliseconds) // only if not-live - /* === */ - this.loadMessages(); - } + this.loadMessages() } private parseAndDistributeMessages(fileReader: MFileReader, onMessage?: (msg: Message) => void) { @@ -264,15 +249,11 @@ export default class MessageManager extends Screen { } } - reloadWithUnprocessedFile() { + reloadWithUnprocessedFile(onSuccess: ()=>void) { const onData = (byteArray: Uint8Array) => { const onMessage = (msg: Message) => { this.lastMessageInFileTime = msg.time } this.parseAndDistributeMessages(new MFileReader(byteArray, this.sessionStart), onMessage) } - const updateState = () => - this.state.update({ - liveTimeTravel: true, - }); // assist will pause and skip messages to prevent timestamp related errors this.reloadMessageManagers() @@ -283,7 +264,7 @@ export default class MessageManager extends Screen { return requestEFSDom(this.session.sessionId) .then(onData) - .then(updateState) + .then(onSuccess) .then(this.onFileReadSuccess) .catch(this.onFileReadFailed) .finally(this.onFileReadFinally) diff --git a/frontend/app/player/_web/WebPlayer.ts b/frontend/app/player/_web/WebPlayer.ts index a2da13d4c..572c9e5b4 100644 --- a/frontend/app/player/_web/WebPlayer.ts +++ b/frontend/app/player/_web/WebPlayer.ts @@ -20,6 +20,7 @@ export default class WebPlayer extends Player { ...ASSIST_INITIAL_STATE, inspectorMode: false, + liveTimeTravel: false, } private readonly screen: Screen @@ -29,9 +30,17 @@ export default class WebPlayer extends Player { assistManager: AssistManager // public so far private targetMarker: TargetMarker - constructor(private wpState: Store, session, config: RTCIceServer[], live: boolean) { + constructor(private wpState: Store, session, config: RTCIceServer[], live: boolean) { + + let initialLists = live ? { + event: session.events.toJSON(), + stack: session.stackEvents.toJSON(), + resource: session.resources.toJSON(), + exceptions: session.errors, + } : {} + // TODO: separate screen from manager - const screen = new MessageManager(session, wpState, config, live) + const screen = new MessageManager(session, wpState, initialLists) super(wpState, screen) this.screen = screen this.messageManager = screen @@ -39,7 +48,7 @@ export default class WebPlayer extends Player { // TODO: separate LiveWebPlayer this.assistManager = new AssistManager(session, this.messageManager, config, wpState) - this.targetMarker = new TargetMarker(this.screen) + this.targetMarker = new TargetMarker(this.screen, wpState) this.inspectorController = new InspectorController(screen) @@ -106,7 +115,11 @@ export default class WebPlayer extends Player { // TODO async toggleTimetravel() { if (!this.wpState.get().liveTimeTravel) { - return await this.messageManager.reloadWithUnprocessedFile() + return await this.messageManager.reloadWithUnprocessedFile(() => + this.wpState.update({ + liveTimeTravel: true, + }) + ) } } From c494f0c0bb6786bd7594ba6904a8570c31fc846f Mon Sep 17 00:00:00 2001 From: sylenien Date: Tue, 22 Nov 2022 17:07:49 +0100 Subject: [PATCH 026/252] refactor(ui/player): refactor connect player for events, autoplay, subheader, player etc --- .../app/components/Session/PlayerContent.js | 20 +++--- .../app/components/Session/RightBlock.tsx | 25 ++++---- frontend/app/components/Session/WebPlayer.tsx | 43 +++++++------ .../components/Session_/Autoplay/Autoplay.js | 22 +++---- .../Session_/EventsBlock/EventsBlock.js | 1 + frontend/app/components/Session_/Subheader.js | 61 ++++++++++--------- frontend/app/player/create.ts | 1 - 7 files changed, 91 insertions(+), 82 deletions(-) diff --git a/frontend/app/components/Session/PlayerContent.js b/frontend/app/components/Session/PlayerContent.js index 8983ca5fc..c0a695173 100644 --- a/frontend/app/components/Session/PlayerContent.js +++ b/frontend/app/components/Session/PlayerContent.js @@ -1,14 +1,21 @@ import React from 'react'; -import { - connectPlayer, -} from 'Player'; import PlayerBlock from '../Session_/PlayerBlock'; import styles from '../Session_/session.module.css'; import { countDaysFrom } from 'App/date'; import cn from 'classnames'; import RightBlock from './RightBlock'; +import { PlayerContext } from 'App/components/Session/playerContext'; +import { observer } from 'mobx-react-lite'; + +function PlayerContent({ session, live, fullscreen, activeTab, setActiveTab }) { + const { store } = React.useContext(PlayerContext) + + const { + error, + } = store.get() + + const hasError = !!error -function PlayerContent({ session, live, fullscreen, activeTab, setActiveTab, hasError }) { const sessionDays = countDaysFrom(session.startedAt); return (
@@ -64,7 +71,4 @@ function RightMenu({ live, tabs, activeTab, setActiveTab, fullscreen }) { ); } -export default connectPlayer((state) => ({ - showEvents: !state.showEvents, - hasError: state.error, -}))(PlayerContent); +export default observer(PlayerContent); diff --git a/frontend/app/components/Session/RightBlock.tsx b/frontend/app/components/Session/RightBlock.tsx index 8ae42b622..66f92a93c 100644 --- a/frontend/app/components/Session/RightBlock.tsx +++ b/frontend/app/components/Session/RightBlock.tsx @@ -1,23 +1,24 @@ -import React, { useState } from 'react' +import React from 'react' import EventsBlock from '../Session_/EventsBlock'; import PageInsightsPanel from '../Session_/PageInsightsPanel/PageInsightsPanel' -import { Controls as PlayerControls } from 'Player'; -import { connectPlayer } from 'Player'; +import { PlayerContext } from 'App/components/Session/playerContext'; +import { observer } from 'mobx-react-lite'; + import cn from 'classnames'; import stl from './rightblock.module.css'; -const EventsBlockConnected = connectPlayer(state => ({ - currentTimeEventIndex: state.eventListNow.length > 0 ? state.eventListNow.length - 1 : 0, - playing: state.playing, -}))(EventsBlock) - -function RightBlock(props) { +function RightBlock(props: any) { const { activeTab } = props; + const { player, store } = React.useContext(PlayerContext) - const renderActiveTab = (tab) => { + const { eventListNow, playing } = store.get() + const currentTimeEventIndex = eventListNow.length > 0 ? eventListNow.length - 1 : 0 + + const EventsBlockConnected = () => + const renderActiveTab = (tab: string) => { switch(tab) { case props.tabs.EVENTS: - return + return case props.tabs.HEATMAPS: return } @@ -29,4 +30,4 @@ function RightBlock(props) { ) } -export default React.memo(RightBlock) +export default observer(RightBlock) diff --git a/frontend/app/components/Session/WebPlayer.tsx b/frontend/app/components/Session/WebPlayer.tsx index c3565321a..480ca0530 100644 --- a/frontend/app/components/Session/WebPlayer.tsx +++ b/frontend/app/components/Session/WebPlayer.tsx @@ -3,23 +3,16 @@ import { connect } from 'react-redux'; import { Modal } from 'UI'; import { toggleFullscreen, closeBottomBlock } from 'Duck/components/player'; import { fetchList } from 'Duck/integrations'; -import { - PlayerProvider, - createWebPlayer, -} from 'Player'; -import { makeAutoObservable } from 'mobx' -import { observer } from "mobx-react-lite" +import { PlayerProvider, createWebPlayer } from 'Player'; +import { makeAutoObservable } from 'mobx'; +import { observer } from 'mobx-react-lite'; import withLocationHandlers from 'HOCs/withLocationHandlers'; import { useStore } from 'App/mstore'; import PlayerBlockHeader from '../Session_/PlayerBlockHeader'; import ReadNote from '../Session_/Player/Controls/components/ReadNote'; import { fetchList as fetchMembers } from 'Duck/member'; -import PlayerContent from './PlayerContent' -import { - IPlayerContext, - PlayerContext, - defaultContextValue -} from './playerContext' +import PlayerContent from './PlayerContent'; +import { IPlayerContext, PlayerContext, defaultContextValue } from './playerContext'; const TABS = { EVENTS: 'User Steps', @@ -27,20 +20,27 @@ const TABS = { }; function WebPlayer(props: any) { - const { session, toggleFullscreen, closeBottomBlock, live, fullscreen, jwt, fetchList } = props; + const { + session, + toggleFullscreen, + closeBottomBlock, + live, + fullscreen, + jwt, + fetchList + } = props; const { notesStore } = useStore(); const [activeTab, setActiveTab] = useState(''); const [showNoteModal, setShowNote] = useState(false); const [noteItem, setNoteItem] = useState(null); - const [contextValue, setContextValue] = useState(defaultContextValue) + const [contextValue, setContextValue] = useState(defaultContextValue); useEffect(() => { fetchList('issues'); - const [WebPlayerInst, PlayerStore] = createWebPlayer( - session, - (state) => makeAutoObservable(state) + const [WebPlayerInst, PlayerStore] = createWebPlayer(session, (state) => + makeAutoObservable(state) ); - setContextValue({ player: WebPlayerInst, store: PlayerStore }) + setContextValue({ player: WebPlayerInst, store: PlayerStore }); // initPlayer(session, jwt); TODOPlayer props.fetchMembers(); @@ -101,14 +101,17 @@ function WebPlayer(props: any) { {showNoteModal ? ( ) => m.id === noteItem?.userId)?.email || ''} + userEmail={ + props.members.find((m: Record) => m.id === noteItem?.userId) + ?.email || '' + } note={noteItem} onClose={onNoteClose} notFound={!noteItem} /> ) : null} - + ); diff --git a/frontend/app/components/Session_/Autoplay/Autoplay.js b/frontend/app/components/Session_/Autoplay/Autoplay.js index 63fdf6841..5510996ef 100644 --- a/frontend/app/components/Session_/Autoplay/Autoplay.js +++ b/frontend/app/components/Session_/Autoplay/Autoplay.js @@ -3,11 +3,16 @@ import { connect } from 'react-redux'; import { setAutoplayValues } from 'Duck/sessions'; import { session as sessionRoute } from 'App/routes'; import { Link, Icon, Toggler, Tooltip } from 'UI'; -import { Controls as PlayerControls, connectPlayer } from 'Player'; import cn from 'classnames'; +import { PlayerContext } from 'App/components/Session/playerContext'; +import { observer } from 'mobx-react-lite'; function Autoplay(props) { - const { previousId, nextId, autoplay, disabled } = props; + const { previousId, nextId, disabled } = props; + const { player, store } = React.useContext(PlayerContext) + + const { autoplay } = store.get() + const { toggleAutoplay } = player useEffect(() => { props.setAutoplayValues(); @@ -16,10 +21,10 @@ function Autoplay(props) { return (
- + Auto-Play
@@ -71,12 +76,5 @@ const connectAutoplay = connect( ); export default connectAutoplay( - connectPlayer( - (state) => ({ - autoplay: state.autoplay, - }), - { - toggleAutoplay: PlayerControls.toggleAutoplay, - } - )(Autoplay) + observer(Autoplay) ); diff --git a/frontend/app/components/Session_/EventsBlock/EventsBlock.js b/frontend/app/components/Session_/EventsBlock/EventsBlock.js index 69ba51996..b3d978347 100644 --- a/frontend/app/components/Session_/EventsBlock/EventsBlock.js +++ b/frontend/app/components/Session_/EventsBlock/EventsBlock.js @@ -141,6 +141,7 @@ export default class EventsBlock extends React.Component { eventsIndex, filterOutNote, } = this.props; + const { query } = this.state; const _events = this.eventsList const isLastEvent = index === _events.size - 1; diff --git a/frontend/app/components/Session_/Subheader.js b/frontend/app/components/Session_/Subheader.js index a1ee8e48d..9d462e0f7 100644 --- a/frontend/app/components/Session_/Subheader.js +++ b/frontend/app/components/Session_/Subheader.js @@ -6,31 +6,50 @@ import SharePopup from '../shared/SharePopup/SharePopup'; import copy from 'copy-to-clipboard'; import Issues from './Issues/Issues'; import NotePopup from './components/NotePopup'; -import { connectPlayer, pause } from 'Player'; 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'; function SubHeader(props) { + const { player, store } = React.useContext(PlayerContext) + const { + width, + height, + location: currentLocation, + fetchList, + graphqlList, + resourceList, + exceptionsList, + eventList: eventsList, + endTime, + } = store.get() + + const mappedResourceList = resourceList + .filter((r) => r.isRed() || r.isYellow()) + .concat(fetchList.filter((i) => parseInt(i.status) >= 400)) + .concat(graphqlList.filter((i) => parseInt(i.status) >= 400)) + const [isCopied, setCopied] = React.useState(false); const { showModal, hideModal } = useModal(); const isAssist = window.location.pathname.includes('/assist/'); const location = - props.currentLocation && props.currentLocation.length > 60 - ? `${props.currentLocation.slice(0, 60)}...` - : props.currentLocation; + currentLocation && currentLocation.length > 60 + ? `${currentLocation.slice(0, 60)}...` + : currentLocation; const showReportModal = () => { - pause(); + player.pause(); const xrayProps = { - currentLocation: props.currentLocation, - resourceList: props.resourceList, - exceptionsList: props.exceptionsList, - eventsList: props.eventsList, - endTime: props.endTime, + currentLocation: currentLocation, + resourceList: mappedResourceList, + exceptionsList: exceptionsList, + eventsList: eventsList, + endTime: endTime, } - showModal(, { right: true }); + showModal(, { right: true }); }; return ( @@ -39,7 +58,7 @@ function SubHeader(props) {
{ - copy(props.currentLocation); + copy(currentLocation); setCopied(true); setTimeout(() => setCopied(false), 5000); }} @@ -102,20 +121,4 @@ function SubHeader(props) { ); } -const SubH = connectPlayer( - (state) => ({ - width: state.width, - height: state.height, - currentLocation: state.location, - resourceList: state.resourceList - .filter((r) => r.isRed() || r.isYellow()) - .concat(state.fetchList.filter((i) => parseInt(i.status) >= 400)) - .concat(state.graphqlList.filter((i) => parseInt(i.status) >= 400)), - exceptionsList: state.exceptionsList, - eventsList: state.eventList, - endTime: state.endTime, - }) - - )(SubHeader); - -export default React.memo(SubH); +export default observer(SubHeader); diff --git a/frontend/app/player/create.ts b/frontend/app/player/create.ts index beacd49e6..c212fd769 100644 --- a/frontend/app/player/create.ts +++ b/frontend/app/player/create.ts @@ -21,7 +21,6 @@ export function createWebPlayer(session: Record, wrapState?: (s:IWe return [player, store] } - export function createLiveWebPlayer(session: Record, config: RTCIceServer[], wrapState?: (s:IWebState) => IWebState): [IWebPlayer, IWebPlayerStore] { let state: WebState = { ...WebPlayer.INITIAL_STATE, From b995fa8441e06dc755fd4b940be23c892ec18e20 Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Tue, 22 Nov 2022 17:27:54 +0100 Subject: [PATCH 027/252] fix(player):actually wrap store instead of state & MessageManager init fix --- frontend/app/player/_web/MessageManager.ts | 2 +- frontend/app/player/create.ts | 24 ++++++++++------------ 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/frontend/app/player/_web/MessageManager.ts b/frontend/app/player/_web/MessageManager.ts index 8eeb74f19..b092483e2 100644 --- a/frontend/app/player/_web/MessageManager.ts +++ b/frontend/app/player/_web/MessageManager.ts @@ -126,7 +126,7 @@ export default class MessageManager extends Screen { this.sessionStart = this.session.startedAt this.lists = new Lists(initialLists) - initialLists && initialLists.event.forEach((e: Record) => { // TODO: to one of "Moveable" module + initialLists?.event?.forEach((e: Record) => { // TODO: to one of "Moveable" module if (e.type === EVENT_TYPES.LOCATION) { this.locationEventManager.append(e); } diff --git a/frontend/app/player/create.ts b/frontend/app/player/create.ts index c212fd769..2bb60f7a9 100644 --- a/frontend/app/player/create.ts +++ b/frontend/app/player/create.ts @@ -4,31 +4,29 @@ import type { Store } from './player/types' import WebPlayer, { State as WebState} from './_web/WebPlayer' type WebPlayerStore = Store -export type IWebState = WebState export type IWebPlayer = WebPlayer export type IWebPlayerStore = WebPlayerStore -export function createWebPlayer(session: Record, wrapState?: (s:IWebState) => IWebState): [IWebPlayer, IWebPlayerStore] { - let state: WebState = { +export function createWebPlayer(session: Record, wrapStore?: (s:IWebPlayerStore) => IWebPlayerStore): [IWebPlayer, IWebPlayerStore] { + let store: WebPlayerStore = new SimpleStore({ ...WebPlayer.INITIAL_STATE, + }) + if (wrapStore) { + store = wrapStore(store) } - if (wrapState) { - state = wrapState(state) - } - const store: WebPlayerStore = new SimpleStore(state) const player = new WebPlayer(store, session, null, false) return [player, store] } -export function createLiveWebPlayer(session: Record, config: RTCIceServer[], wrapState?: (s:IWebState) => IWebState): [IWebPlayer, IWebPlayerStore] { - let state: WebState = { + +export function createLiveWebPlayer(session: Record, config: RTCIceServer[], wrapStore?: (s:IWebPlayerStore) => IWebPlayerStore): [IWebPlayer, IWebPlayerStore] { + let store: WebPlayerStore = new SimpleStore({ ...WebPlayer.INITIAL_STATE, + }) + if (wrapStore) { + store = wrapStore(store) } - if (wrapState) { - state = wrapState(state) - } - const store: WebPlayerStore = new SimpleStore(state) const player = new WebPlayer(store, session, config, true) return [player, store] From af6fd085e208f09a5183d3306421d6d45698adad Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Tue, 22 Nov 2022 17:31:08 +0100 Subject: [PATCH 028/252] fix(player): state type on creation --- frontend/app/player/create.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/app/player/create.ts b/frontend/app/player/create.ts index 2bb60f7a9..410ad6e2a 100644 --- a/frontend/app/player/create.ts +++ b/frontend/app/player/create.ts @@ -1,8 +1,9 @@ import SimpleStore from './_common/SimpleStore' import type { Store } from './player/types' -import WebPlayer, { State as WebState} from './_web/WebPlayer' +import WebPlayer from './_web/WebPlayer' +type WebState = typeof WebPlayer.INITIAL_STATE //? type WebPlayerStore = Store export type IWebPlayer = WebPlayer export type IWebPlayerStore = WebPlayerStore From 1cadd087740bcbeb9dff8dcd341c661110621334 Mon Sep 17 00:00:00 2001 From: MauricioGarciaS <47052044+MauricioGarciaS@users.noreply.github.com> Date: Tue, 22 Nov 2022 18:04:45 +0100 Subject: [PATCH 029/252] Changed scheduler method --- ee/recommendation/api.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/ee/recommendation/api.py b/ee/recommendation/api.py index b2d47b26d..213d797fa 100644 --- a/ee/recommendation/api.py +++ b/ee/recommendation/api.py @@ -1,12 +1,12 @@ import logging +from apscheduler.schedulers.asyncio import AsyncIOScheduler from fastapi import FastAPI -from fastapi_utils.tasks import repeat_every +# from fastapi_utils.tasks import repeat_every from utils import events_queue from utils import pg_client app = FastAPI() -first_boot=True - +app.schedule = AsyncIOScheduler() @app.get('/') def home(): @@ -21,20 +21,15 @@ def number(value: int): @app.on_event("startup") -@repeat_every(seconds=60*1) # every 5 mins async def startup(): - global first_boot - if first_boot: - await pg_client.init() - await events_queue.init(test=False) - first_boot = False - else: - events_queue.global_queue.force_flush() + await pg_client.init() + await events_queue.init(test=False) + app.schedule.start() -# @repeat_every(seconds=60*5) # 5 min -# def clean_up(): -# events_queue.force_flush() +@app.schedule.scheduled_job("interval", seconds=60*1) +def clean_up(): + events_queue.global_queue.force_flush() @app.on_event("shutdown") From daf35a2dfab34291aa02a35cc8f921366926db5c Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Tue, 22 Nov 2022 18:59:55 +0100 Subject: [PATCH 030/252] refactoring(player): separating Screen and MessageManager; renamings --- frontend/app/player/_singletone.ts | 6 +-- frontend/app/player/_store/duck.js | 8 +-- frontend/app/player/_store/index.js | 8 +-- .../player/{_common => common}/ListWalker.ts | 2 +- .../ListWalkerWithMarks.ts | 2 +- .../player/{_common => common}/SimpleStore.ts | 3 +- .../app/player/{player => common}/types.ts | 4 +- frontend/app/player/create.ts | 6 +-- frontend/app/player/index.ts | 12 ++--- frontend/app/player/player/Animator.ts | 2 +- frontend/app/player/player/Player.ts | 17 ++----- .../{_web => web}/InspectorController.ts | 0 frontend/app/player/{_web => web}/Lists.ts | 4 +- .../player/{_web => web}/MessageManager.ts | 40 ++++++++------- .../app/player/{_web => web}/Screen/Cursor.ts | 0 .../player/{_web => web}/Screen/Inspector.ts | 0 .../app/player/{_web => web}/Screen/Marker.ts | 0 .../app/player/{_web => web}/Screen/Screen.ts | 0 .../{_web => web}/Screen/cursor.module.css | 0 .../{_web => web}/Screen/marker.module.css | 0 .../{_web => web}/Screen/screen.module.css | 0 .../app/player/{_web => web}/Screen/types.ts | 0 .../app/player/{_web => web}/TargetMarker.ts | 2 +- .../app/player/{_web => web}/WebLivePlayer.ts | 0 .../app/player/{_web => web}/WebPlayer.ts | 25 ++++++---- .../{_web => web}/assist/AnnotationCanvas.ts | 0 .../{_web => web}/assist/AssistManager.ts | 0 .../{_web => web}/assist/LocalStream.ts | 0 .../{_web => web}/managers/ActivityManager.ts | 2 +- .../{_web => web}/managers/DOM/DOMManager.ts | 11 ++-- .../managers/DOM/FocusManager.ts | 2 +- .../managers/DOM/StylesManager.ts | 50 +++---------------- .../{_web => web}/managers/DOM/VirtualDOM.ts | 0 .../managers/DOM/safeCSSRules.ts | 0 .../managers/MouseMoveManager.ts | 2 +- .../{_web => web}/managers/PagesManager.ts | 15 ++---- .../managers/PerformanceTrackManager.ts | 2 +- .../managers/WindowNodeCounter.ts | 0 .../messages/JSONRawMessageReader.ts | 0 .../{_web => web}/messages/MFileReader.ts | 0 .../{_web => web}/messages/MStreamReader.ts | 0 .../{_web => web}/messages/PrimitiveReader.ts | 0 .../messages/RawMessageReader.ts | 0 .../player/{_web => web}/messages/index.ts | 0 .../player/{_web => web}/messages/message.ts | 0 .../app/player/{_web => web}/messages/raw.ts | 0 .../player/{_web => web}/messages/timed.ts | 0 .../{_web => web}/messages/tracker-legacy.ts | 0 .../player/{_web => web}/messages/tracker.ts | 0 .../{_web => web}/messages/urlResolve.ts | 0 .../player/{_web => web}/network/crypto.ts | 0 .../player/{_web => web}/network/loadFiles.ts | 0 52 files changed, 91 insertions(+), 134 deletions(-) rename frontend/app/player/{_common => common}/ListWalker.ts (98%) rename frontend/app/player/{_common => common}/ListWalkerWithMarks.ts (94%) rename frontend/app/player/{_common => common}/SimpleStore.ts (85%) rename frontend/app/player/{player => common}/types.ts (87%) rename frontend/app/player/{_web => web}/InspectorController.ts (100%) rename frontend/app/player/{_web => web}/Lists.ts (94%) rename frontend/app/player/{_web => web}/MessageManager.ts (94%) rename frontend/app/player/{_web => web}/Screen/Cursor.ts (100%) rename frontend/app/player/{_web => web}/Screen/Inspector.ts (100%) rename frontend/app/player/{_web => web}/Screen/Marker.ts (100%) rename frontend/app/player/{_web => web}/Screen/Screen.ts (100%) rename frontend/app/player/{_web => web}/Screen/cursor.module.css (100%) rename frontend/app/player/{_web => web}/Screen/marker.module.css (100%) rename frontend/app/player/{_web => web}/Screen/screen.module.css (100%) rename frontend/app/player/{_web => web}/Screen/types.ts (100%) rename frontend/app/player/{_web => web}/TargetMarker.ts (98%) rename frontend/app/player/{_web => web}/WebLivePlayer.ts (100%) rename frontend/app/player/{_web => web}/WebPlayer.ts (87%) rename frontend/app/player/{_web => web}/assist/AnnotationCanvas.ts (100%) rename frontend/app/player/{_web => web}/assist/AssistManager.ts (100%) rename frontend/app/player/{_web => web}/assist/LocalStream.ts (100%) rename frontend/app/player/{_web => web}/managers/ActivityManager.ts (94%) rename frontend/app/player/{_web => web}/managers/DOM/DOMManager.ts (98%) rename frontend/app/player/{_web => web}/managers/DOM/FocusManager.ts (93%) rename frontend/app/player/{_web => web}/managers/DOM/StylesManager.ts (54%) rename frontend/app/player/{_web => web}/managers/DOM/VirtualDOM.ts (100%) rename frontend/app/player/{_web => web}/managers/DOM/safeCSSRules.ts (100%) rename frontend/app/player/{_web => web}/managers/MouseMoveManager.ts (96%) rename frontend/app/player/{_web => web}/managers/PagesManager.ts (81%) rename frontend/app/player/{_web => web}/managers/PerformanceTrackManager.ts (98%) rename frontend/app/player/{_web => web}/managers/WindowNodeCounter.ts (100%) rename frontend/app/player/{_web => web}/messages/JSONRawMessageReader.ts (100%) rename frontend/app/player/{_web => web}/messages/MFileReader.ts (100%) rename frontend/app/player/{_web => web}/messages/MStreamReader.ts (100%) rename frontend/app/player/{_web => web}/messages/PrimitiveReader.ts (100%) rename frontend/app/player/{_web => web}/messages/RawMessageReader.ts (100%) rename frontend/app/player/{_web => web}/messages/index.ts (100%) rename frontend/app/player/{_web => web}/messages/message.ts (100%) rename frontend/app/player/{_web => web}/messages/raw.ts (100%) rename frontend/app/player/{_web => web}/messages/timed.ts (100%) rename frontend/app/player/{_web => web}/messages/tracker-legacy.ts (100%) rename frontend/app/player/{_web => web}/messages/tracker.ts (100%) rename frontend/app/player/{_web => web}/messages/urlResolve.ts (100%) rename frontend/app/player/{_web => web}/network/crypto.ts (100%) rename frontend/app/player/{_web => web}/network/loadFiles.ts (100%) diff --git a/frontend/app/player/_singletone.ts b/frontend/app/player/_singletone.ts index c4e0d607b..2449542b6 100644 --- a/frontend/app/player/_singletone.ts +++ b/frontend/app/player/_singletone.ts @@ -1,9 +1,9 @@ -import WebPlayer from './_web/WebPlayer'; +import WebPlayer from './web/WebPlayer'; import reduxStore, {update, cleanStore} from './_store'; -import type { State as MMState } from './_web/MessageManager' +import type { State as MMState } from './web/MessageManager' import type { State as PState } from './player/Player' -import type { Store } from './player/types' +import type { Store } from './common/types' const myStore: Store = { diff --git a/frontend/app/player/_store/duck.js b/frontend/app/player/_store/duck.js index 6cf861ade..6996e8ed1 100644 --- a/frontend/app/player/_store/duck.js +++ b/frontend/app/player/_store/duck.js @@ -1,15 +1,11 @@ -import { applyChange, revertChange } from 'deep-diff'; - -import { INITIAL_STATE as MM_INITIAL_STATE } from '../_web/MessageManager' -import Player from '../player/Player' +import WebPlayer from '../web/WebPlayer' const UPDATE = 'player/UPDATE'; const CLEAN = 'player/CLEAN'; const REDUX = 'player/REDUX'; const resetState = { - ...MM_INITIAL_STATE, - ...Player.INITIAL_STATE, + ...WebPlayer.INITIAL_STATE, initialized: false, }; diff --git a/frontend/app/player/_store/index.js b/frontend/app/player/_store/index.js index cad6dfaff..c2a987661 100644 --- a/frontend/app/player/_store/index.js +++ b/frontend/app/player/_store/index.js @@ -1,4 +1,4 @@ -// export * from './connector'; -// export * from './store'; -// export * from './selectors'; -// export { default } from './store'; \ No newline at end of file +export * from './connector'; +export * from './store'; +export * from './selectors'; +export { default } from './store'; \ No newline at end of file diff --git a/frontend/app/player/_common/ListWalker.ts b/frontend/app/player/common/ListWalker.ts similarity index 98% rename from frontend/app/player/_common/ListWalker.ts rename to frontend/app/player/common/ListWalker.ts index 3600f31da..db847150d 100644 --- a/frontend/app/player/_common/ListWalker.ts +++ b/frontend/app/player/common/ListWalker.ts @@ -1,4 +1,4 @@ -import type { Timed } from '../_web/messages/timed'; +import type { Timed } from './types'; export default class ListWalker { private p = 0 diff --git a/frontend/app/player/_common/ListWalkerWithMarks.ts b/frontend/app/player/common/ListWalkerWithMarks.ts similarity index 94% rename from frontend/app/player/_common/ListWalkerWithMarks.ts rename to frontend/app/player/common/ListWalkerWithMarks.ts index 10446d7e0..a97378d03 100644 --- a/frontend/app/player/_common/ListWalkerWithMarks.ts +++ b/frontend/app/player/common/ListWalkerWithMarks.ts @@ -1,4 +1,4 @@ -import type { Timed } from '../_web/messages/timed'; +import type { Timed } from './types'; import ListWalker from './ListWalker' diff --git a/frontend/app/player/_common/SimpleStore.ts b/frontend/app/player/common/SimpleStore.ts similarity index 85% rename from frontend/app/player/_common/SimpleStore.ts rename to frontend/app/player/common/SimpleStore.ts index 2b4a77658..dd7e1ee7b 100644 --- a/frontend/app/player/_common/SimpleStore.ts +++ b/frontend/app/player/common/SimpleStore.ts @@ -1,5 +1,4 @@ - -import { Store } from '../player/types' +import { Store } from './types' // (not a type) export default class SimpleSore implements Store { diff --git a/frontend/app/player/player/types.ts b/frontend/app/player/common/types.ts similarity index 87% rename from frontend/app/player/player/types.ts rename to frontend/app/player/common/types.ts index 5deb56b06..0e5be2841 100644 --- a/frontend/app/player/player/types.ts +++ b/frontend/app/player/common/types.ts @@ -1,3 +1,6 @@ +export interface Timed { + time: number +} export interface Moveable { move(time: number): void @@ -18,4 +21,3 @@ export interface Store { update(state: Partial): void } - diff --git a/frontend/app/player/create.ts b/frontend/app/player/create.ts index 410ad6e2a..a69d9710e 100644 --- a/frontend/app/player/create.ts +++ b/frontend/app/player/create.ts @@ -1,7 +1,7 @@ -import SimpleStore from './_common/SimpleStore' -import type { Store } from './player/types' +import SimpleStore from './common/SimpleStore' +import type { Store } from './common/types' -import WebPlayer from './_web/WebPlayer' +import WebPlayer from './web/WebPlayer' type WebState = typeof WebPlayer.INITIAL_STATE //? type WebPlayerStore = Store diff --git a/frontend/app/player/index.ts b/frontend/app/player/index.ts index 55266aa5f..949088e43 100644 --- a/frontend/app/player/index.ts +++ b/frontend/app/player/index.ts @@ -1,9 +1,9 @@ -export * from './_web/assist/AssistManager'; -export * from './_web/assist/LocalStream'; -export * from './_web/WebPlayer'; +export * from './web/assist/AssistManager'; +export * from './web/assist/LocalStream'; +export * from './web/WebPlayer'; export * from './create'; -export type { MarkedTarget } from './_web/TargetMarker' +export type { MarkedTarget } from './web/TargetMarker' -//export * from './_store'; -//export * from './_singletone'; +export * from './_store'; +export * from './_singletone'; diff --git a/frontend/app/player/player/Animator.ts b/frontend/app/player/player/Animator.ts index a75290929..7f2e4c5bc 100644 --- a/frontend/app/player/player/Animator.ts +++ b/frontend/app/player/player/Animator.ts @@ -1,4 +1,4 @@ -import type { Store, Moveable, Interval } from './types'; +import type { Store, Moveable, Interval } from '../common/types'; import * as localStorage from './localStorage'; const fps = 60 diff --git a/frontend/app/player/player/Player.ts b/frontend/app/player/player/Player.ts index 9b2b98225..95a3c45de 100644 --- a/frontend/app/player/player/Player.ts +++ b/frontend/app/player/player/Player.ts @@ -1,6 +1,6 @@ import * as typedLocalStorage from './localStorage'; -import type { Moveable, Cleanable, Store } from './types'; +import type { Moveable, Cleanable, Store } from '../common/types'; import Animator from './Animator'; import type { GetState as AnimatorGetState, SetState as AnimatorSetState } from './Animator'; @@ -57,8 +57,6 @@ export default class Player extends Animator { } } - - /* === TODO: incapsulate in LSCache === */ toggleAutoplay() { @@ -67,14 +65,14 @@ export default class Player extends Animator { this.pState.update({ autoplay }) } - // Shouldn't it be in the react part as a fully (with localStorage-cache react hook)? + //TODO: move to react part (with localStorage-cache react hook)? toggleEvents() { const showEvents = !this.pState.get().showEvents localStorage.setItem(SHOW_EVENTS_STORAGE_KEY, `${showEvents}`); this.pState.update({ showEvents }) } - // move to React part? + // TODO: move to React part toggleSkipToIssue() { const skipToIssue = !this.pState.get().skipToIssue localStorage.setItem(SKIP_TO_ISSUE_STORAGE_KEY, `${skipToIssue}`); @@ -107,15 +105,6 @@ export default class Player extends Animator { } /* === === */ - // TODO: move theese to React hooks - // injectNotes(notes: Note[]) { - // update({ notes }) - // } - // filterOutNote(noteId: number) { - // const { notes } = getState() - // update({ notes: notes.filter((note: Note) => note.noteId !== noteId) }) - // } - clean() { this.pause() diff --git a/frontend/app/player/_web/InspectorController.ts b/frontend/app/player/web/InspectorController.ts similarity index 100% rename from frontend/app/player/_web/InspectorController.ts rename to frontend/app/player/web/InspectorController.ts diff --git a/frontend/app/player/_web/Lists.ts b/frontend/app/player/web/Lists.ts similarity index 94% rename from frontend/app/player/_web/Lists.ts rename to frontend/app/player/web/Lists.ts index fdf560c93..dc3a088fd 100644 --- a/frontend/app/player/_web/Lists.ts +++ b/frontend/app/player/web/Lists.ts @@ -1,5 +1,5 @@ -import ListWalker from '../_common/ListWalker'; -import ListWalkerWithMarks from '../_common/ListWalkerWithMarks'; +import ListWalker from '../common/ListWalker'; +import ListWalkerWithMarks from '../common/ListWalkerWithMarks'; import type { Message } from './messages' diff --git a/frontend/app/player/_web/MessageManager.ts b/frontend/app/player/web/MessageManager.ts similarity index 94% rename from frontend/app/player/_web/MessageManager.ts rename to frontend/app/player/web/MessageManager.ts index b092483e2..83a8ea95a 100644 --- a/frontend/app/player/_web/MessageManager.ts +++ b/frontend/app/player/web/MessageManager.ts @@ -8,10 +8,8 @@ import Log from 'Types/session/log'; import { toast } from 'react-toastify'; -import type { Store } from '../player/types'; -import ListWalker from '../_common/ListWalker'; - -import Screen from './Screen/Screen'; +import type { Store } from '../common/types'; +import ListWalker from '../common/ListWalker'; import PagesManager from './managers/PagesManager'; import MouseMoveManager from './managers/MouseMoveManager'; @@ -24,15 +22,19 @@ import MFileReader from './messages/MFileReader'; import { loadFiles, requestEFSDom, requestEFSDevtools } from './network/loadFiles'; import { decryptSessionBytes } from './network/crypto'; -import { INITIAL_STATE as SCREEN_INITIAL_STATE, State as SuperState } from './Screen/Screen'; import Lists, { INITIAL_STATE as LISTS_INITIAL_STATE } from './Lists'; +import Screen, { + INITIAL_STATE as SCREEN_INITIAL_STATE, + State as ScreenState, +} from './Screen/Screen'; + import type { InitialLists } from './Lists' import type { PerformanceChartPoint } from './managers/PerformanceTrackManager'; import type { SkipInterval } from './managers/ActivityManager'; -export interface State extends SuperState { +export interface State extends ScreenState { performanceChartData: PerformanceChartPoint[], skipIntervals: SkipInterval[], connType?: string, @@ -73,7 +75,7 @@ const visualChanges = [ "set_viewport_scroll", ] -export default class MessageManager extends Screen { +export default class MessageManager { static INITIAL_STATE: State = { ...SCREEN_INITIAL_STATE, ...LISTS_INITIAL_STATE, @@ -116,12 +118,12 @@ export default class MessageManager extends Screen { constructor( private readonly session: any /*Session*/, - private readonly state: Store, + private readonly state: Store, + private readonly screen: Screen, initialLists?: Partial ) { - super(); - this.pagesManager = new PagesManager(this, this.session.isMobile) - this.mouseMoveManager = new MouseMoveManager(this) + this.pagesManager = new PagesManager(screen, this.session.isMobile, this) + this.mouseMoveManager = new MouseMoveManager(screen) this.sessionStart = this.session.startedAt @@ -281,8 +283,8 @@ export default class MessageManager extends Screen { this.performanceTrackManager = new PerformanceTrackManager() this.windowNodeCounter = new WindowNodeCounter(); - this.pagesManager = new PagesManager(this, this.session.isMobile) - this.mouseMoveManager = new MouseMoveManager(this); + this.pagesManager = new PagesManager(this.screen, this.session.isMobile, this) + this.mouseMoveManager = new MouseMoveManager(this.screen); this.activityManager = new ActivityManager(this.session.duration.milliseconds); } @@ -339,14 +341,14 @@ export default class MessageManager extends Screen { this.pagesManager.moveReady(t).then(() => { const lastScroll = this.scrollManager.moveGetLast(t, index); - if (!!lastScroll && this.window) { - this.window.scrollTo(lastScroll.x, lastScroll.y); + if (!!lastScroll && this.screen.window) { + this.screen.window.scrollTo(lastScroll.x, lastScroll.y); } // Moving mouse and setting :hover classes on ready view this.mouseMoveManager.move(t); const lastClick = this.clickManager.moveGetLast(t); if (!!lastClick && t - lastClick.time < 600) { // happend during last 600ms - this.cursor.click(); + this.screen.cursor.click(); } // After all changes - redraw the marker //this.marker.redraw(); @@ -514,17 +516,17 @@ export default class MessageManager extends Screen { } setMessagesLoading(messagesLoading: boolean) { - this.display(!messagesLoading); + this.screen.display(!messagesLoading); this.state.update({ messagesLoading }); } setCSSLoading(cssLoading: boolean) { - this.displayFrame(!cssLoading); + this.screen.displayFrame(!cssLoading); this.state.update({ cssLoading }); } private setSize({ height, width }: { height: number, width: number }) { - this.scale({ height, width }); + this.screen.scale({ height, width }); this.state.update({ width, height }); //this.updateMarketTargets() diff --git a/frontend/app/player/_web/Screen/Cursor.ts b/frontend/app/player/web/Screen/Cursor.ts similarity index 100% rename from frontend/app/player/_web/Screen/Cursor.ts rename to frontend/app/player/web/Screen/Cursor.ts diff --git a/frontend/app/player/_web/Screen/Inspector.ts b/frontend/app/player/web/Screen/Inspector.ts similarity index 100% rename from frontend/app/player/_web/Screen/Inspector.ts rename to frontend/app/player/web/Screen/Inspector.ts diff --git a/frontend/app/player/_web/Screen/Marker.ts b/frontend/app/player/web/Screen/Marker.ts similarity index 100% rename from frontend/app/player/_web/Screen/Marker.ts rename to frontend/app/player/web/Screen/Marker.ts diff --git a/frontend/app/player/_web/Screen/Screen.ts b/frontend/app/player/web/Screen/Screen.ts similarity index 100% rename from frontend/app/player/_web/Screen/Screen.ts rename to frontend/app/player/web/Screen/Screen.ts diff --git a/frontend/app/player/_web/Screen/cursor.module.css b/frontend/app/player/web/Screen/cursor.module.css similarity index 100% rename from frontend/app/player/_web/Screen/cursor.module.css rename to frontend/app/player/web/Screen/cursor.module.css diff --git a/frontend/app/player/_web/Screen/marker.module.css b/frontend/app/player/web/Screen/marker.module.css similarity index 100% rename from frontend/app/player/_web/Screen/marker.module.css rename to frontend/app/player/web/Screen/marker.module.css diff --git a/frontend/app/player/_web/Screen/screen.module.css b/frontend/app/player/web/Screen/screen.module.css similarity index 100% rename from frontend/app/player/_web/Screen/screen.module.css rename to frontend/app/player/web/Screen/screen.module.css diff --git a/frontend/app/player/_web/Screen/types.ts b/frontend/app/player/web/Screen/types.ts similarity index 100% rename from frontend/app/player/_web/Screen/types.ts rename to frontend/app/player/web/Screen/types.ts diff --git a/frontend/app/player/_web/TargetMarker.ts b/frontend/app/player/web/TargetMarker.ts similarity index 98% rename from frontend/app/player/_web/TargetMarker.ts rename to frontend/app/player/web/TargetMarker.ts index a2ca99cc2..f3b4d1d7d 100644 --- a/frontend/app/player/_web/TargetMarker.ts +++ b/frontend/app/player/web/TargetMarker.ts @@ -1,6 +1,6 @@ import type Screen from './Screen/Screen' import type { Point } from './Screen/types' -import type { Store } from '../player/types' +import type { Store } from '../common/types' function getOffset(el: Element, innerWindow: Window) { diff --git a/frontend/app/player/_web/WebLivePlayer.ts b/frontend/app/player/web/WebLivePlayer.ts similarity index 100% rename from frontend/app/player/_web/WebLivePlayer.ts rename to frontend/app/player/web/WebLivePlayer.ts diff --git a/frontend/app/player/_web/WebPlayer.ts b/frontend/app/player/web/WebPlayer.ts similarity index 87% rename from frontend/app/player/_web/WebPlayer.ts rename to frontend/app/player/web/WebPlayer.ts index 572c9e5b4..ec132d5f8 100644 --- a/frontend/app/player/_web/WebPlayer.ts +++ b/frontend/app/player/web/WebPlayer.ts @@ -1,4 +1,4 @@ -import type { Store } from '../player/types' +import type { Store } from '../common/types' import Player, { State as PlayerState } from '../player/Player' import MessageManager from './MessageManager' @@ -39,17 +39,13 @@ export default class WebPlayer extends Player { exceptions: session.errors, } : {} - // TODO: separate screen from manager - const screen = new MessageManager(session, wpState, initialLists) - super(wpState, screen) + const screen = new Screen() + const messageManager = new MessageManager(session, wpState, screen, initialLists) + super(wpState, messageManager) this.screen = screen - this.messageManager = screen - - // TODO: separate LiveWebPlayer - this.assistManager = new AssistManager(session, this.messageManager, config, wpState) + this.messageManager = messageManager this.targetMarker = new TargetMarker(this.screen, wpState) - this.inspectorController = new InspectorController(screen) @@ -65,6 +61,8 @@ export default class WebPlayer extends Player { endTime, // : 0, //TODO: through initialState }) + // TODO: separate LiveWebPlayer + this.assistManager = new AssistManager(session, this.messageManager, config, wpState) if (live) { this.assistManager.connect(session.agentToken) } @@ -123,6 +121,15 @@ export default class WebPlayer extends Player { } } + // TODO: restore notes functionality + // injectNotes(notes: Note[]) { + // update({ notes }) + // } + // filterOutNote(noteId: number) { + // const { notes } = getState() + // update({ notes: notes.filter((note: Note) => note.noteId !== noteId) }) + // } + toggleUserName(name?: string) { this.screen.cursor.showTag(name) } diff --git a/frontend/app/player/_web/assist/AnnotationCanvas.ts b/frontend/app/player/web/assist/AnnotationCanvas.ts similarity index 100% rename from frontend/app/player/_web/assist/AnnotationCanvas.ts rename to frontend/app/player/web/assist/AnnotationCanvas.ts diff --git a/frontend/app/player/_web/assist/AssistManager.ts b/frontend/app/player/web/assist/AssistManager.ts similarity index 100% rename from frontend/app/player/_web/assist/AssistManager.ts rename to frontend/app/player/web/assist/AssistManager.ts diff --git a/frontend/app/player/_web/assist/LocalStream.ts b/frontend/app/player/web/assist/LocalStream.ts similarity index 100% rename from frontend/app/player/_web/assist/LocalStream.ts rename to frontend/app/player/web/assist/LocalStream.ts diff --git a/frontend/app/player/_web/managers/ActivityManager.ts b/frontend/app/player/web/managers/ActivityManager.ts similarity index 94% rename from frontend/app/player/_web/managers/ActivityManager.ts rename to frontend/app/player/web/managers/ActivityManager.ts index d5e81f62d..383f29c15 100644 --- a/frontend/app/player/_web/managers/ActivityManager.ts +++ b/frontend/app/player/web/managers/ActivityManager.ts @@ -1,4 +1,4 @@ -import ListWalker from '../../_common/ListWalker'; +import ListWalker from '../../common/ListWalker'; class SkipIntervalCls { diff --git a/frontend/app/player/_web/managers/DOM/DOMManager.ts b/frontend/app/player/web/managers/DOM/DOMManager.ts similarity index 98% rename from frontend/app/player/_web/managers/DOM/DOMManager.ts rename to frontend/app/player/web/managers/DOM/DOMManager.ts index 370df5c03..708129add 100644 --- a/frontend/app/player/_web/managers/DOM/DOMManager.ts +++ b/frontend/app/player/web/managers/DOM/DOMManager.ts @@ -1,9 +1,11 @@ import logger from 'App/logger'; +import type Screen from '../../Screen/Screen'; import type MessageManager from '../../MessageManager'; + import type { Message, SetNodeScroll, CreateElementNode } from '../../messages'; -import ListWalker from '../../../_common/ListWalker'; +import ListWalker from '../../../common/ListWalker'; import StylesManager, { rewriteNodeStyleSheet } from './StylesManager'; import FocusManager from './FocusManager'; import { @@ -51,12 +53,13 @@ export default class DOMManager extends ListWalker { constructor( - private readonly screen: MessageManager, + private readonly screen: Screen, private readonly isMobile: boolean, - public readonly time: number + public readonly time: number, + mm: MessageManager, ) { super() - this.stylesManager = new StylesManager(screen) + this.stylesManager = new StylesManager(screen, mm) } append(m: Message): void { diff --git a/frontend/app/player/_web/managers/DOM/FocusManager.ts b/frontend/app/player/web/managers/DOM/FocusManager.ts similarity index 93% rename from frontend/app/player/_web/managers/DOM/FocusManager.ts rename to frontend/app/player/web/managers/DOM/FocusManager.ts index 629f625e3..174335473 100644 --- a/frontend/app/player/_web/managers/DOM/FocusManager.ts +++ b/frontend/app/player/web/managers/DOM/FocusManager.ts @@ -1,7 +1,7 @@ import logger from 'App/logger'; import type { SetNodeFocus } from '../../messages'; import type { VElement } from './VirtualDOM'; -import ListWalker from '../../../_common/ListWalker'; +import ListWalker from '../../../common/ListWalker'; const FOCUS_CLASS = "-openreplay-focus" diff --git a/frontend/app/player/_web/managers/DOM/StylesManager.ts b/frontend/app/player/web/managers/DOM/StylesManager.ts similarity index 54% rename from frontend/app/player/_web/managers/DOM/StylesManager.ts rename to frontend/app/player/web/managers/DOM/StylesManager.ts index 1cb49c5dd..5cffa9be9 100644 --- a/frontend/app/player/_web/managers/DOM/StylesManager.ts +++ b/frontend/app/player/web/managers/DOM/StylesManager.ts @@ -1,11 +1,9 @@ -import type MessageManager from '../../MessageManager'; +import type Screen from '../../Screen/Screen'; +import type MessageManager from '../../MessageManager' import type { CssInsertRule, CssDeleteRule } from '../../messages'; type CSSRuleMessage = CssInsertRule | CssDeleteRule; -import ListWalker from '../../../_common/ListWalker'; - - const HOVER_CN = "-openreplay-hover"; const HOVER_SELECTOR = `.${HOVER_CN}`; @@ -21,17 +19,14 @@ export function rewriteNodeStyleSheet(doc: Document, node: HTMLLinkElement | HTM } } -export default class StylesManager extends ListWalker { +export default class StylesManager { private linkLoadingCount: number = 0; private linkLoadPromises: Array> = []; private skipCSSLinks: Array = []; // should be common for all pages - constructor(private readonly screen: MessageManager) { - super(); - } + constructor(private readonly screen: Screen, private readonly mm: MessageManager) {} reset():void { - super.reset(); this.linkLoadingCount = 0; this.linkLoadPromises = []; @@ -43,7 +38,7 @@ export default class StylesManager extends ListWalker { const promise = new Promise((resolve) => { if (this.skipCSSLinks.includes(value)) resolve(); this.linkLoadingCount++; - this.screen.setCSSLoading(true); + this.mm.setCSSLoading(true); const addSkipAndResolve = () => { this.skipCSSLinks.push(value); // watch out resolve() @@ -62,44 +57,13 @@ export default class StylesManager extends ListWalker { clearTimeout(timeoutId); this.linkLoadingCount--; if (this.linkLoadingCount === 0) { - this.screen.setCSSLoading(false); + this.mm.setCSSLoading(false); } }); this.linkLoadPromises.push(promise); } - private manageRule = (msg: CSSRuleMessage):void => { - // if (msg.tp === "css_insert_rule") { - // let styleSheet = this.#screen.document.styleSheets[ msg.stylesheetID ]; - // if (!styleSheet) { - // logger.log("No stylesheet with corresponding ID found: ", msg) - // styleSheet = this.#screen.document.styleSheets[0]; - // if (!styleSheet) { - // return; - // } - // } - // try { - // styleSheet.insertRule(msg.rule, msg.index); - // } catch (e) { - // logger.log(e, msg) - // //const index = Math.min(msg.index, styleSheet.cssRules.length); - // styleSheet.insertRule(msg.rule, styleSheet.cssRules.length); - // //styleSheet.ownerNode.innerHTML += msg.rule; - // } - // } - // if (msg.tp === "css_delete_rule") { - // // console.warn('Warning: STYLESHEET_DELETE_RULE msg') - // const styleSheet = this.#screen.document.styleSheets[msg.stylesheetID]; - // if (!styleSheet) { - // logger.log("No stylesheet with corresponding ID found: ", msg) - // return; - // } - // styleSheet.deleteRule(msg.index); - // } - } - - moveReady(t: number): Promise { + moveReady(t: number): Promise { return Promise.all(this.linkLoadPromises) - .then(() => this.moveApply(t, this.manageRule)); } } diff --git a/frontend/app/player/_web/managers/DOM/VirtualDOM.ts b/frontend/app/player/web/managers/DOM/VirtualDOM.ts similarity index 100% rename from frontend/app/player/_web/managers/DOM/VirtualDOM.ts rename to frontend/app/player/web/managers/DOM/VirtualDOM.ts diff --git a/frontend/app/player/_web/managers/DOM/safeCSSRules.ts b/frontend/app/player/web/managers/DOM/safeCSSRules.ts similarity index 100% rename from frontend/app/player/_web/managers/DOM/safeCSSRules.ts rename to frontend/app/player/web/managers/DOM/safeCSSRules.ts diff --git a/frontend/app/player/_web/managers/MouseMoveManager.ts b/frontend/app/player/web/managers/MouseMoveManager.ts similarity index 96% rename from frontend/app/player/_web/managers/MouseMoveManager.ts rename to frontend/app/player/web/managers/MouseMoveManager.ts index 63b5ca895..ea08e1bc9 100644 --- a/frontend/app/player/_web/managers/MouseMoveManager.ts +++ b/frontend/app/player/web/managers/MouseMoveManager.ts @@ -2,7 +2,7 @@ import type Screen from '../Screen/Screen' import type { Point } from '../Screen/types' import type { MouseMove } from '../messages' -import ListWalker from '../../_common/ListWalker' +import ListWalker from '../../common/ListWalker' const HOVER_CLASS = "-openreplay-hover"; const HOVER_CLASS_DEPR = "-asayer-hover"; diff --git a/frontend/app/player/_web/managers/PagesManager.ts b/frontend/app/player/web/managers/PagesManager.ts similarity index 81% rename from frontend/app/player/_web/managers/PagesManager.ts rename to frontend/app/player/web/managers/PagesManager.ts index 6a3f20f7c..b4ddbe28b 100644 --- a/frontend/app/player/_web/managers/PagesManager.ts +++ b/frontend/app/player/web/managers/PagesManager.ts @@ -1,28 +1,23 @@ import type Screen from '../Screen/Screen'; import type { Message } from '../messages'; +import type MessageManager from '../MessageManager'; -import ListWalker from '../../_common/ListWalker'; + +import ListWalker from '../../common/ListWalker'; import DOMManager from './DOM/DOMManager'; export default class PagesManager extends ListWalker { private currentPage: DOMManager | null = null - private isMobile: boolean; - private screen: Screen; - - constructor(screen: Screen, isMobile: boolean) { - super() - this.screen = screen - this.isMobile = isMobile - } + constructor(private screen: Screen, private isMobile: boolean, private mm: MessageManager) { super() } /* Assumed that messages added in a correct time sequence. */ appendMessage(m: Message): void { if (m.tp === "create_document") { - super.append(new DOMManager(this.screen, this.isMobile, m.time)) + super.append(new DOMManager(this.screen, this.isMobile, m.time, this.mm)) } if (this.last === null) { // Log wrong diff --git a/frontend/app/player/_web/managers/PerformanceTrackManager.ts b/frontend/app/player/web/managers/PerformanceTrackManager.ts similarity index 98% rename from frontend/app/player/_web/managers/PerformanceTrackManager.ts rename to frontend/app/player/web/managers/PerformanceTrackManager.ts index 424466b4a..98ce2c0b9 100644 --- a/frontend/app/player/_web/managers/PerformanceTrackManager.ts +++ b/frontend/app/player/web/managers/PerformanceTrackManager.ts @@ -1,6 +1,6 @@ import type { PerformanceTrack, SetPageVisibility } from '../messages'; -import ListWalker from '../../_common/ListWalker'; +import ListWalker from '../../common/ListWalker'; export type PerformanceChartPoint = { time: number, diff --git a/frontend/app/player/_web/managers/WindowNodeCounter.ts b/frontend/app/player/web/managers/WindowNodeCounter.ts similarity index 100% rename from frontend/app/player/_web/managers/WindowNodeCounter.ts rename to frontend/app/player/web/managers/WindowNodeCounter.ts diff --git a/frontend/app/player/_web/messages/JSONRawMessageReader.ts b/frontend/app/player/web/messages/JSONRawMessageReader.ts similarity index 100% rename from frontend/app/player/_web/messages/JSONRawMessageReader.ts rename to frontend/app/player/web/messages/JSONRawMessageReader.ts diff --git a/frontend/app/player/_web/messages/MFileReader.ts b/frontend/app/player/web/messages/MFileReader.ts similarity index 100% rename from frontend/app/player/_web/messages/MFileReader.ts rename to frontend/app/player/web/messages/MFileReader.ts diff --git a/frontend/app/player/_web/messages/MStreamReader.ts b/frontend/app/player/web/messages/MStreamReader.ts similarity index 100% rename from frontend/app/player/_web/messages/MStreamReader.ts rename to frontend/app/player/web/messages/MStreamReader.ts diff --git a/frontend/app/player/_web/messages/PrimitiveReader.ts b/frontend/app/player/web/messages/PrimitiveReader.ts similarity index 100% rename from frontend/app/player/_web/messages/PrimitiveReader.ts rename to frontend/app/player/web/messages/PrimitiveReader.ts diff --git a/frontend/app/player/_web/messages/RawMessageReader.ts b/frontend/app/player/web/messages/RawMessageReader.ts similarity index 100% rename from frontend/app/player/_web/messages/RawMessageReader.ts rename to frontend/app/player/web/messages/RawMessageReader.ts diff --git a/frontend/app/player/_web/messages/index.ts b/frontend/app/player/web/messages/index.ts similarity index 100% rename from frontend/app/player/_web/messages/index.ts rename to frontend/app/player/web/messages/index.ts diff --git a/frontend/app/player/_web/messages/message.ts b/frontend/app/player/web/messages/message.ts similarity index 100% rename from frontend/app/player/_web/messages/message.ts rename to frontend/app/player/web/messages/message.ts diff --git a/frontend/app/player/_web/messages/raw.ts b/frontend/app/player/web/messages/raw.ts similarity index 100% rename from frontend/app/player/_web/messages/raw.ts rename to frontend/app/player/web/messages/raw.ts diff --git a/frontend/app/player/_web/messages/timed.ts b/frontend/app/player/web/messages/timed.ts similarity index 100% rename from frontend/app/player/_web/messages/timed.ts rename to frontend/app/player/web/messages/timed.ts diff --git a/frontend/app/player/_web/messages/tracker-legacy.ts b/frontend/app/player/web/messages/tracker-legacy.ts similarity index 100% rename from frontend/app/player/_web/messages/tracker-legacy.ts rename to frontend/app/player/web/messages/tracker-legacy.ts diff --git a/frontend/app/player/_web/messages/tracker.ts b/frontend/app/player/web/messages/tracker.ts similarity index 100% rename from frontend/app/player/_web/messages/tracker.ts rename to frontend/app/player/web/messages/tracker.ts diff --git a/frontend/app/player/_web/messages/urlResolve.ts b/frontend/app/player/web/messages/urlResolve.ts similarity index 100% rename from frontend/app/player/_web/messages/urlResolve.ts rename to frontend/app/player/web/messages/urlResolve.ts diff --git a/frontend/app/player/_web/network/crypto.ts b/frontend/app/player/web/network/crypto.ts similarity index 100% rename from frontend/app/player/_web/network/crypto.ts rename to frontend/app/player/web/network/crypto.ts diff --git a/frontend/app/player/_web/network/loadFiles.ts b/frontend/app/player/web/network/loadFiles.ts similarity index 100% rename from frontend/app/player/_web/network/loadFiles.ts rename to frontend/app/player/web/network/loadFiles.ts From b00bc4fe9ae3c718695d2090bce82127c4e1e5b0 Mon Sep 17 00:00:00 2001 From: sylenien Date: Tue, 22 Nov 2022 17:29:10 +0100 Subject: [PATCH 031/252] refactor(ui/player): refactor notes popup, time comp --- .../components/Session_/Player/Controls/Time.js | 13 ++++++++----- .../Session_/components/NotePopup.tsx | 17 ++++++++--------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/frontend/app/components/Session_/Player/Controls/Time.js b/frontend/app/components/Session_/Player/Controls/Time.js index e8221b5b8..ab6f5e6b8 100644 --- a/frontend/app/components/Session_/Player/Controls/Time.js +++ b/frontend/app/components/Session_/Player/Controls/Time.js @@ -1,7 +1,8 @@ import React from 'react'; import { Duration } from 'luxon'; -import { connectPlayer } from 'Player'; import styles from './time.module.css'; +import { PlayerContext } from 'App/components/Session/playerContext'; +import { observer } from 'mobx-react-lite'; const Time = ({ time, isCustom, format = 'm:ss', }) => (
@@ -11,10 +12,12 @@ const Time = ({ time, isCustom, format = 'm:ss', }) => ( Time.displayName = "Time"; -const ReduxTime = connectPlayer((state, { name, format }) => ({ - time: state[ name ], - format, -}))(Time); +const ReduxTime = observer(({ format, name }) => { + const { store } = React.useContext(PlayerContext) + const time = store.get()[name] + + return
); From 5b97a0c3cf1755b40167b00ae703c6da61593356 Mon Sep 17 00:00:00 2001 From: sylenien Date: Fri, 25 Nov 2022 11:02:34 +0100 Subject: [PATCH 036/252] refactor(ui/player): refactor more components --- .../RequestingWindow/RequestingWindow.tsx | 28 +- .../AssistActions/AssistActions.tsx | 45 +- frontend/app/components/Session/LivePlayer.js | 42 +- frontend/app/components/Session/WebPlayer.tsx | 2 +- .../Session_/EventsBlock/NoteEvent.tsx | 2 +- .../Session_/Exceptions/Exceptions.js | 12 - .../Session_/OverviewPanel/OverviewPanel.tsx | 2 +- .../components/SelectorCard/SelectorCard.tsx | 8 +- .../Session_/Player/Controls/ControlButton.js | 2 +- .../Session_/Player/Controls/Controls.js | 434 ------------------ .../Session_/Player/Controls/Controls.tsx | 420 +++++++++++++++++ .../Session_/Player/Controls/Time.js | 20 +- .../Controls/components/PlayerControls.tsx | 8 +- .../components/Session_/PlayerBlockHeader.js | 169 ------- .../components/Session_/PlayerBlockHeader.tsx | 161 +++++++ .../components/Session_/Storage/DiffRow.tsx | 4 +- .../components/Session_/Storage/Storage.js | 321 ------------- .../components/Session_/Storage/Storage.tsx | 315 +++++++++++++ .../Session_/components/NotePopup.tsx | 8 +- .../shared/GuidePopup/GuidePopup.tsx | 4 +- .../app/components/ui/Tooltip/Tooltip.tsx | 6 +- frontend/app/player/player/Animator.ts | 22 +- .../app/player/web/InspectorController.ts | 10 +- frontend/app/player/web/Screen/Inspector.ts | 10 +- frontend/app/player/web/WebPlayer.ts | 14 +- .../app/player/web/assist/AssistManager.ts | 4 +- 26 files changed, 1039 insertions(+), 1034 deletions(-) delete mode 100644 frontend/app/components/Session_/Player/Controls/Controls.js create mode 100644 frontend/app/components/Session_/Player/Controls/Controls.tsx delete mode 100644 frontend/app/components/Session_/PlayerBlockHeader.js create mode 100644 frontend/app/components/Session_/PlayerBlockHeader.tsx delete mode 100644 frontend/app/components/Session_/Storage/Storage.js create mode 100644 frontend/app/components/Session_/Storage/Storage.tsx diff --git a/frontend/app/components/Assist/RequestingWindow/RequestingWindow.tsx b/frontend/app/components/Assist/RequestingWindow/RequestingWindow.tsx index 6d702acdb..3c0fce497 100644 --- a/frontend/app/components/Assist/RequestingWindow/RequestingWindow.tsx +++ b/frontend/app/components/Assist/RequestingWindow/RequestingWindow.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { INDEXES } from 'App/constants/zindex'; import { connect } from 'react-redux'; import { Button, Loader, Icon } from 'UI'; -import { initiateCallEnd, releaseRemoteControl } from 'Player'; +import { PlayerContext } from 'App/components/Session/playerContext'; interface Props { userDisplayName: string; @@ -14,20 +14,38 @@ export enum WindowType { Control, } +enum Actions { + CallEnd, + ControlEnd +} + const WIN_VARIANTS = { [WindowType.Call]: { text: 'to accept the call', icon: 'call' as const, - action: initiateCallEnd, + action: Actions.CallEnd, }, [WindowType.Control]: { text: 'to accept remote control request', icon: 'remote-control' as const, - action: releaseRemoteControl, + action: Actions.ControlEnd, }, }; function RequestingWindow({ userDisplayName, type }: Props) { + const { player } = React.useContext(PlayerContext) + + const { + assistManager: { + initiateCallEnd, + releaseRemoteControl, + } + } = player + + const actions = { + [Actions.CallEnd]: initiateCallEnd, + [Actions.ControlEnd]: releaseRemoteControl + } return (
{WIN_VARIANTS[type].text} -
@@ -48,6 +66,6 @@ function RequestingWindow({ userDisplayName, type }: Props) { ); } -export default connect((state) => ({ +export default connect((state: any) => ({ userDisplayName: state.getIn(['sessions', 'current', 'userDisplayName']), }))(RequestingWindow); diff --git a/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx b/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx index e377cd3ba..35afb6721 100644 --- a/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx +++ b/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx @@ -3,15 +3,8 @@ import { Button, Tooltip } from 'UI'; import { connect } from 'react-redux'; import cn from 'classnames'; import { toggleChatWindow } from 'Duck/sessions'; -import { connectPlayer } from 'Player'; import ChatWindow from '../../ChatWindow'; -import { - callPeer, - setCallArgs, - requestReleaseRemoteControl, - toggleAnnotation, - toggleUserName, -} from 'Player'; +// state enums import { CallingState, ConnectionStatus, @@ -19,6 +12,8 @@ import { RequestLocalStream, } from 'Player'; import type { LocalStream } from 'Player'; +import { PlayerContext } from 'App/components/Session/playerContext'; +import { observer } from 'mobx-react-lite'; import { toast } from 'react-toastify'; import { confirm } from 'UI'; import stl from './AassistActions.module.css'; @@ -48,17 +43,31 @@ interface Props { function AssistActions({ userId, - calling, - annotating, - peerConnectionStatus, - remoteControlStatus, hasPermission, isEnterprise, isCallActive, agentIds, - livePlay, userDisplayName, }: Props) { + const { player, store } = React.useContext(PlayerContext) + + const { + assistManager: { + call: callPeer, + setCallArgs, + requestReleaseRemoteControl, + toggleAnnotation, + }, + toggleUserName, + } = player + const { + calling, + annotating, + peerConnectionStatus, + remoteControl: remoteControlStatus, + livePlay, + } = store.get() + const [isPrestart, setPrestart] = useState(false); const [incomeStream, setIncomeStream] = useState([]); const [localStream, setLocalStream] = useState(null); @@ -236,7 +245,7 @@ function AssistActions({ } const con = connect( - (state) => { + (state: any) => { const permissions = state.getIn(['user', 'account', 'permissions']) || []; return { hasPermission: permissions.includes('ASSIST_CALL'), @@ -248,11 +257,5 @@ const con = connect( ); export default con( - connectPlayer((state) => ({ - calling: state.calling, - annotating: state.annotating, - remoteControlStatus: state.remoteControl, - peerConnectionStatus: state.peerConnectionStatus, - livePlay: state.livePlay, - }))(AssistActions) + observer(AssistActions) ); diff --git a/frontend/app/components/Session/LivePlayer.js b/frontend/app/components/Session/LivePlayer.js index 7e0f09145..2168d468f 100644 --- a/frontend/app/components/Session/LivePlayer.js +++ b/frontend/app/components/Session/LivePlayer.js @@ -1,25 +1,20 @@ import React from 'react'; import { useEffect, useState } from 'react'; import { connect } from 'react-redux'; -import { Loader } from 'UI'; import { toggleFullscreen, closeBottomBlock } from 'Duck/components/player'; import { withRequest } from 'HOCs' import { PlayerProvider, - connectPlayer, - init as initPlayer, - clean as cleanPlayer, } from 'Player'; import withPermissions from 'HOCs/withPermissions'; +import { PlayerContext, defaultContextValue } from './playerContext'; +import { createLiveWebPlayer } from 'Player'; +import { makeAutoObservable } from 'mobx'; import PlayerBlockHeader from '../Session_/PlayerBlockHeader'; import PlayerBlock from '../Session_/PlayerBlock'; import styles from '../Session_/session.module.css'; -const InitLoader = connectPlayer(state => ({ - loading: !state.initialized -}))(Loader); - function LivePlayer ({ session, toggleFullscreen, @@ -32,6 +27,8 @@ function LivePlayer ({ userEmail, userName }) { + const [contextValue, setContextValue] = useState(defaultContextValue); + useEffect(() => { if (!loadingCredentials) { @@ -42,9 +39,16 @@ function LivePlayer ({ name: userName, }, } - initPlayer(sessionWithAgentData, assistCredendials, true); + // initPlayer(sessionWithAgentData, assistCredendials, true); + const [LivePlayer, LivePlayerStore] = createLiveWebPlayer( + sessionWithAgentData, + assistCredendials, + (state) => makeAutoObservable(state) + ) + setContextValue({ player: LivePlayer, store: LivePlayerStore }); + } - return () => cleanPlayer() + return () => LivePlayer.clean() }, [ session.sessionId, loadingCredentials, assistCredendials ]); // LAYOUT (TODO: local layout state - useContext or something..) @@ -64,15 +68,17 @@ function LivePlayer ({ } const [activeTab, setActiveTab] = useState(''); + if (!contextValue.player) return null; + return ( - - - -
- -
-
-
+ + + +
+ +
+
+
); }; diff --git a/frontend/app/components/Session/WebPlayer.tsx b/frontend/app/components/Session/WebPlayer.tsx index 480ca0530..d7e8a115b 100644 --- a/frontend/app/components/Session/WebPlayer.tsx +++ b/frontend/app/components/Session/WebPlayer.tsx @@ -5,7 +5,6 @@ import { toggleFullscreen, closeBottomBlock } from 'Duck/components/player'; import { fetchList } from 'Duck/integrations'; import { PlayerProvider, createWebPlayer } from 'Player'; import { makeAutoObservable } from 'mobx'; -import { observer } from 'mobx-react-lite'; import withLocationHandlers from 'HOCs/withLocationHandlers'; import { useStore } from 'App/mstore'; import PlayerBlockHeader from '../Session_/PlayerBlockHeader'; @@ -85,6 +84,7 @@ function WebPlayer(props: any) { <> filterRE.test(e.name) || filterRE.test(e.message)); - // let lastIndex = -1; - // filtered.forEach((item, index) => { - // if ( - // this.props.exceptionsNow.length > 0 && - // item.time <= this.props.exceptionsNow[this.props.exceptionsNow.length - 1].time - // ) { - // lastIndex = index; - // } - // }); - return ( <> jump(e.time)} error={e} key={e.key} - // selected={lastIndex === index} - // inactive={index > lastIndex} onErrorClick={(jsEvent) => { jsEvent.stopPropagation(); jsEvent.preventDefault(); diff --git a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx index 73bd27cd8..667f37d5b 100644 --- a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx +++ b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx @@ -38,7 +38,7 @@ function OverviewPanel({ issuesList }: { issuesList: any[] }) { const resourceList = resourceListUnmap .filter((r: any) => r.isRed() || r.isYellow()) .concat(fetchList.filter((i: any) => parseInt(i.status) >= 400)) - .concat(graphqlList.filter((i: any) => parseInt(i.status) >= 400)), + .concat(graphqlList.filter((i: any) => parseInt(i.status) >= 400)) const resources: any = React.useMemo(() => { return { diff --git a/frontend/app/components/Session_/PageInsightsPanel/components/SelectorCard/SelectorCard.tsx b/frontend/app/components/Session_/PageInsightsPanel/components/SelectorCard/SelectorCard.tsx index 999dae866..39de770b7 100644 --- a/frontend/app/components/Session_/PageInsightsPanel/components/SelectorCard/SelectorCard.tsx +++ b/frontend/app/components/Session_/PageInsightsPanel/components/SelectorCard/SelectorCard.tsx @@ -1,9 +1,9 @@ -import React, { useState } from 'react'; +import React from 'react'; import stl from './SelectorCard.module.css'; import cn from 'classnames'; import type { MarkedTarget } from 'Player'; -import { activeTarget } from 'Player'; import { Tooltip } from 'react-tippy'; +import { PlayerContext } from 'App/components/Session/playerContext'; interface Props { index?: number; @@ -12,7 +12,11 @@ interface Props { } export default function SelectorCard({ index = 1, target, showContent }: Props) { + const { player } = React.useContext(PlayerContext) + const activeTarget = player.setActiveTarget + return ( + // @ts-ignore TODO for Alex
activeTarget(index)}>
{/* @ts-ignore */} diff --git a/frontend/app/components/Session_/Player/Controls/ControlButton.js b/frontend/app/components/Session_/Player/Controls/ControlButton.js index 3c42895b6..1d6391f1f 100644 --- a/frontend/app/components/Session_/Player/Controls/ControlButton.js +++ b/frontend/app/components/Session_/Player/Controls/ControlButton.js @@ -12,7 +12,7 @@ const ControlButton = ({ hasErrors = false, active = false, size = 20, - noLabel, + noLabel = false, labelClassName, containerClassName, noIcon, diff --git a/frontend/app/components/Session_/Player/Controls/Controls.js b/frontend/app/components/Session_/Player/Controls/Controls.js deleted file mode 100644 index e0114926e..000000000 --- a/frontend/app/components/Session_/Player/Controls/Controls.js +++ /dev/null @@ -1,434 +0,0 @@ -import React from 'react'; -import cn from 'classnames'; -import { connect } from 'react-redux'; -import { - connectPlayer, - STORAGE_TYPES, - selectStorageType, - selectStorageListNow, - jumpToLive, - toggleInspectorMode, -} from 'Player'; -import LiveTag from 'Shared/LiveTag'; - -import { Icon, Tooltip } from 'UI'; -import { - fullscreenOn, - fullscreenOff, - toggleBottomBlock, - changeSkipInterval, - OVERVIEW, - CONSOLE, - NETWORK, - STACKEVENTS, - STORAGE, - PROFILER, - PERFORMANCE, - GRAPHQL, - INSPECTOR, -} from 'Duck/components/player'; -import { AssistDuration } from './Time'; -import Timeline from './Timeline'; -import ControlButton from './ControlButton'; -import PlayerControls from './components/PlayerControls'; - -import styles from './controls.module.css'; -import XRayButton from 'Shared/XRayButton'; - -const SKIP_INTERVALS = { - 2: 2e3, - 5: 5e3, - 10: 1e4, - 15: 15e3, - 20: 2e4, - 30: 3e4, - 60: 6e4, -}; - -function getStorageName(type) { - switch (type) { - case STORAGE_TYPES.REDUX: - return 'REDUX'; - case STORAGE_TYPES.MOBX: - return 'MOBX'; - case STORAGE_TYPES.VUEX: - return 'VUEX'; - case STORAGE_TYPES.NGRX: - return 'NGRX'; - case STORAGE_TYPES.ZUSTAND: - return 'ZUSTAND'; - case STORAGE_TYPES.NONE: - return 'STATE'; - } -} - -@connectPlayer((state) => ({ - time: state.time, - endTime: state.endTime, - live: state.live, - livePlay: state.livePlay, - playing: state.playing, - completed: state.completed, - skip: state.skip, - skipToIssue: state.skipToIssue, - speed: state.speed, - disabled: state.cssLoading || state.messagesLoading || state.inspectorMode || state.markedTargets, - inspectorMode: state.inspectorMode, - fullscreenDisabled: state.messagesLoading, - // logCount: state.logList.length, - logRedCount: state.logRedCount, - showExceptions: state.exceptionsList.length > 0, - resourceRedCount: state.resourceRedCount, - fetchRedCount: state.fetchRedCount, - showStack: state.stackList.length > 0, - stackCount: state.stackList.length, - stackRedCount: state.stackRedCount, - profilesCount: state.profilesList.length, - storageCount: selectStorageListNow(state).length, - storageType: selectStorageType(state), - showStorage: selectStorageType(state) !== STORAGE_TYPES.NONE, - showProfiler: state.profilesList.length > 0, - showGraphql: state.graphqlList.length > 0, - showFetch: state.fetchCount > 0, - fetchCount: state.fetchCount, - graphqlCount: state.graphqlList.length, - liveTimeTravel: state.liveTimeTravel, -})) -@connect( - (state, props) => { - const permissions = state.getIn(['user', 'account', 'permissions']) || []; - const isEnterprise = state.getIn(['user', 'account', 'edition']) === 'ee'; - return { - disabled: props.disabled || (isEnterprise && !permissions.includes('DEV_TOOLS')), - fullscreen: state.getIn(['components', 'player', 'fullscreen']), - bottomBlock: state.getIn(['components', 'player', 'bottomBlock']), - showStorage: - props.showStorage || !state.getIn(['components', 'player', 'hiddenHints', 'storage']), - showStack: props.showStack || !state.getIn(['components', 'player', 'hiddenHints', 'stack']), - closedLive: - !!state.getIn(['sessions', 'errors']) || !state.getIn(['sessions', 'current', 'live']), - skipInterval: state.getIn(['components', 'player', 'skipInterval']), - }; - }, - { - fullscreenOn, - fullscreenOff, - toggleBottomBlock, - changeSkipInterval, - } -) -export default class Controls extends React.Component { - componentDidMount() { - document.addEventListener('keydown', this.onKeyDown); - } - - componentWillUnmount() { - document.removeEventListener('keydown', this.onKeyDown); - //this.props.toggleInspectorMode(false); - } - - shouldComponentUpdate(nextProps) { - if ( - nextProps.fullscreen !== this.props.fullscreen || - nextProps.bottomBlock !== this.props.bottomBlock || - nextProps.live !== this.props.live || - nextProps.livePlay !== this.props.livePlay || - nextProps.playing !== this.props.playing || - nextProps.completed !== this.props.completed || - nextProps.skip !== this.props.skip || - nextProps.skipToIssue !== this.props.skipToIssue || - nextProps.speed !== this.props.speed || - nextProps.disabled !== this.props.disabled || - nextProps.fullscreenDisabled !== this.props.fullscreenDisabled || - // nextProps.inspectorMode !== this.props.inspectorMode || - // nextProps.logCount !== this.props.logCount || - nextProps.logRedCount !== this.props.logRedCount || - nextProps.showExceptions !== this.props.showExceptions || - nextProps.resourceRedCount !== this.props.resourceRedCount || - nextProps.fetchRedCount !== this.props.fetchRedCount || - nextProps.showStack !== this.props.showStack || - nextProps.stackCount !== this.props.stackCount || - nextProps.stackRedCount !== this.props.stackRedCount || - nextProps.profilesCount !== this.props.profilesCount || - nextProps.storageCount !== this.props.storageCount || - nextProps.storageType !== this.props.storageType || - nextProps.showStorage !== this.props.showStorage || - nextProps.showProfiler !== this.props.showProfiler || - nextProps.showGraphql !== this.props.showGraphql || - nextProps.showFetch !== this.props.showFetch || - nextProps.fetchCount !== this.props.fetchCount || - nextProps.graphqlCount !== this.props.graphqlCount || - nextProps.liveTimeTravel !== this.props.liveTimeTravel || - nextProps.skipInterval !== this.props.skipInterval - ) - return true; - return false; - } - - onKeyDown = (e) => { - if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { - return; - } - if (this.props.inspectorMode) { - if (e.key === 'Esc' || e.key === 'Escape') { - toggleInspectorMode(false); - } - } - // if (e.key === ' ') { - // document.activeElement.blur(); - // this.props.togglePlay(); - // } - if (e.key === 'Esc' || e.key === 'Escape') { - this.props.fullscreenOff(); - } - if (e.key === 'ArrowRight') { - this.forthTenSeconds(); - } - if (e.key === 'ArrowLeft') { - this.backTenSeconds(); - } - if (e.key === 'ArrowDown') { - this.props.speedDown(); - } - if (e.key === 'ArrowUp') { - this.props.speedUp(); - } - }; - - forthTenSeconds = () => { - const { time, endTime, jump, skipInterval } = this.props; - jump(Math.min(endTime, time + SKIP_INTERVALS[skipInterval])); - }; - - backTenSeconds = () => { - //shouldComponentUpdate - const { time, jump, skipInterval } = this.props; - jump(Math.max(0, time - SKIP_INTERVALS[skipInterval])); - }; - - goLive = () => this.props.jump(this.props.endTime); - - renderPlayBtn = () => { - const { completed, playing } = this.props; - let label; - let icon; - if (completed) { - icon = 'arrow-clockwise'; - label = 'Replay this session'; - } else if (playing) { - icon = 'pause-fill'; - label = 'Pause'; - } else { - icon = 'play-fill-new'; - label = 'Pause'; - label = 'Play'; - } - - return ( - -
- -
-
- ); - }; - - controlIcon = (icon, size, action, isBackwards, additionalClasses) => ( -
- -
- ); - - render() { - const { - bottomBlock, - toggleBottomBlock, - live, - livePlay, - skip, - speed, - disabled, - logRedCount, - showExceptions, - resourceRedCount, - showStack, - stackRedCount, - showStorage, - storageType, - showProfiler, - showGraphql, - fullscreen, - inspectorMode, - closedLive, - toggleSpeed, - toggleSkip, - liveTimeTravel, - changeSkipInterval, - skipInterval, - } = this.props; - - const toggleBottomTools = (blockName) => { - if (blockName === INSPECTOR) { - toggleInspectorMode(); - bottomBlock && toggleBottomBlock(); - } else { - toggleInspectorMode(false); - toggleBottomBlock(blockName); - } - }; - - return ( -
- - {!fullscreen && ( -
-
- {!live && ( - <> - -
- toggleBottomTools(OVERVIEW)} - /> - - )} - - {live && !closedLive && ( -
- (livePlay ? null : jumpToLive())} /> -
- -
-
- )} -
- -
- toggleBottomTools(CONSOLE)} - active={bottomBlock === CONSOLE && !inspectorMode} - label="CONSOLE" - noIcon - labelClassName="!text-base font-semibold" - hasErrors={logRedCount > 0 || showExceptions} - containerClassName="mx-2" - /> - {!live && ( - toggleBottomTools(NETWORK)} - active={bottomBlock === NETWORK && !inspectorMode} - label="NETWORK" - hasErrors={resourceRedCount > 0} - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" - /> - )} - {!live && ( - toggleBottomTools(PERFORMANCE)} - active={bottomBlock === PERFORMANCE && !inspectorMode} - label="PERFORMANCE" - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" - /> - )} - {!live && showGraphql && ( - toggleBottomTools(GRAPHQL)} - active={bottomBlock === GRAPHQL && !inspectorMode} - label="GRAPHQL" - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" - /> - )} - {!live && showStorage && ( - toggleBottomTools(STORAGE)} - active={bottomBlock === STORAGE && !inspectorMode} - label={getStorageName(storageType)} - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" - /> - )} - {!live && ( - toggleBottomTools(STACKEVENTS)} - active={bottomBlock === STACKEVENTS && !inspectorMode} - label="EVENTS" - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" - hasErrors={stackRedCount > 0} - /> - )} - {!live && showProfiler && ( - toggleBottomTools(PROFILER)} - active={bottomBlock === PROFILER && !inspectorMode} - label="PROFILER" - noIcon - labelClassName="!text-base font-semibold" - containerClassName="mx-2" - /> - )} - {!live && ( - - {this.controlIcon( - 'arrows-angle-extend', - 16, - this.props.fullscreenOn, - false, - 'rounded hover:bg-gray-light-shade color-gray-medium' - )} - - )} -
-
- )} -
- ); - } -} diff --git a/frontend/app/components/Session_/Player/Controls/Controls.tsx b/frontend/app/components/Session_/Player/Controls/Controls.tsx new file mode 100644 index 000000000..4e39c72c3 --- /dev/null +++ b/frontend/app/components/Session_/Player/Controls/Controls.tsx @@ -0,0 +1,420 @@ +import React from 'react'; +import cn from 'classnames'; +import { connect } from 'react-redux'; +import { STORAGE_TYPES, selectStorageType, selectStorageListNow } from 'Player'; +import LiveTag from 'Shared/LiveTag'; + +import { Icon, Tooltip } from 'UI'; +import { + fullscreenOn, + fullscreenOff, + toggleBottomBlock, + changeSkipInterval, + OVERVIEW, + CONSOLE, + NETWORK, + STACKEVENTS, + STORAGE, + PROFILER, + PERFORMANCE, + GRAPHQL, + INSPECTOR, +} from 'Duck/components/player'; +import { PlayerContext } from 'App/components/Session/playerContext'; +import { observer } from 'mobx-react-lite'; + +import { AssistDuration } from './Time'; +import Timeline from './Timeline'; +import ControlButton from './ControlButton'; +import PlayerControls from './components/PlayerControls'; + +import styles from './controls.module.css'; +import XRayButton from 'Shared/XRayButton'; + +const SKIP_INTERVALS = { + 2: 2e3, + 5: 5e3, + 10: 1e4, + 15: 15e3, + 20: 2e4, + 30: 3e4, + 60: 6e4, +}; + +function getStorageName(type: any) { + switch (type) { + case STORAGE_TYPES.REDUX: + return 'REDUX'; + case STORAGE_TYPES.MOBX: + return 'MOBX'; + case STORAGE_TYPES.VUEX: + return 'VUEX'; + case STORAGE_TYPES.NGRX: + return 'NGRX'; + case STORAGE_TYPES.ZUSTAND: + return 'ZUSTAND'; + case STORAGE_TYPES.NONE: + return 'STATE'; + } +} + +function Controls(props: any) { + const { player, store } = React.useContext(PlayerContext); + + const { jumpToLive, toggleInspectorMode } = player; + const { + live, + livePlay, + playing, + completed, + skip, + // skipToIssue, UPDATE + speed, + cssLoading, + messagesLoading, + inspectorMode, + markedTargets, + // messagesLoading: fullscreenDisabled, UPDATE + stackList, + exceptionsList, + profilesList, + graphqlList, + fetchList, + liveTimeTravel, + logMarkedCountNow: logRedCount, + resourceMarkedCountNow: resourceRedCount, + stackMarkedCountNow: stackRedCount, + } = store.get(); + // const storageCount = selectStorageListNow(store.get()).length UPDATE + const { + bottomBlock, + toggleBottomBlock, + fullscreen, + closedLive, + changeSkipInterval, + skipInterval, + disabledRedux, + showStorageRedux, + showStackRedux, + } = props; + + const storageType = selectStorageType(store.get()); + const disabled = disabledRedux || cssLoading || messagesLoading || inspectorMode || markedTargets; + const stackCount = stackList.length; + const profilesCount = profilesList.length; + const graphqlCount = graphqlList.length; + const showGraphql = graphqlCount > 0; + const fetchCount = fetchList.length; + const showProfiler = profilesCount > 0; + const showExceptions = exceptionsList.length > 0; + const showStorage = storageType !== STORAGE_TYPES.NONE || showStorageRedux; + // const showStack = stackCount > 0 || showStackRedux UPDATE + // const showFetch = fetchCount > 0 UPDATE + + const onKeyDown = (e: any) => { + if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { + return; + } + if (inspectorMode) { + if (e.key === 'Esc' || e.key === 'Escape') { + player.toggleInspectorMode(false); + } + } + if (e.key === 'Esc' || e.key === 'Escape') { + props.fullscreenOff(); + } + if (e.key === 'ArrowRight') { + forthTenSeconds(); + } + if (e.key === 'ArrowLeft') { + backTenSeconds(); + } + if (e.key === 'ArrowDown') { + props.speedDown(); + } + if (e.key === 'ArrowUp') { + props.speedUp(); + } + }; + + React.useEffect(() => { + document.addEventListener('keydown', onKeyDown); + return () => { + document.removeEventListener('keydown', onKeyDown); + }; + }, []); + + const forthTenSeconds = () => { + // @ts-ignore + player.jumpInterval(SKIP_INTERVALS[skipInterval]) + }; + + const backTenSeconds = () => { + // @ts-ignore + player.jumpInterval(-SKIP_INTERVALS[skipInterval]) + }; + + const renderPlayBtn = () => { + let label; + let icon; + if (completed) { + icon = 'arrow-clockwise' as const; + label = 'Replay this session'; + } else if (playing) { + icon = 'pause-fill' as const; + label = 'Pause'; + } else { + icon = 'play-fill-new' as const; + label = 'Pause'; + label = 'Play'; + } + + return ( + +
player.togglePlay()} + className="hover-main color-main cursor-pointer rounded hover:bg-gray-light-shade" + > + +
+
+ ); + }; + + const controlIcon = ( + icon: string, + size: number, + action: (args: any) => any, + isBackwards: boolean, + additionalClasses: string + ) => ( +
+ +
+ ); + + const toggleBottomTools = (blockName: number) => { + if (blockName === INSPECTOR) { + toggleInspectorMode(false); + bottomBlock && toggleBottomBlock(); + } else { + toggleInspectorMode(false); + toggleBottomBlock(blockName); + } + }; + + return ( +
+ player.jump(t)} + liveTimeTravel={liveTimeTravel} + pause={() => player.pause()} + togglePlay={() => player.togglePlay()} + /> + {!fullscreen && ( +
+
+ {!live && ( + <> + player.toggleSpeed()} + toggleSkip={() => player.toggleSkip()} + playButton={renderPlayBtn()} + controlIcon={controlIcon} + skipIntervals={SKIP_INTERVALS} + setSkipInterval={changeSkipInterval} + currentInterval={skipInterval} + /> +
+ toggleBottomTools(OVERVIEW)} + /> + + )} + + {live && !closedLive && ( +
+ (livePlay ? null : jumpToLive())} /> +
+ +
+
+ )} +
+ +
+ toggleBottomTools(CONSOLE)} + active={bottomBlock === CONSOLE && !inspectorMode} + label="CONSOLE" + noIcon + labelClassName="!text-base font-semibold" + hasErrors={logRedCount > 0 || showExceptions} + containerClassName="mx-2" + /> + {!live && ( + toggleBottomTools(NETWORK)} + active={bottomBlock === NETWORK && !inspectorMode} + label="NETWORK" + hasErrors={resourceRedCount > 0} + noIcon + labelClassName="!text-base font-semibold" + containerClassName="mx-2" + /> + )} + {!live && ( + toggleBottomTools(PERFORMANCE)} + active={bottomBlock === PERFORMANCE && !inspectorMode} + label="PERFORMANCE" + noIcon + labelClassName="!text-base font-semibold" + containerClassName="mx-2" + /> + )} + {!live && showGraphql && ( + toggleBottomTools(GRAPHQL)} + active={bottomBlock === GRAPHQL && !inspectorMode} + label="GRAPHQL" + noIcon + labelClassName="!text-base font-semibold" + containerClassName="mx-2" + /> + )} + {!live && showStorage && ( + toggleBottomTools(STORAGE)} + active={bottomBlock === STORAGE && !inspectorMode} + label={getStorageName(storageType)} + noIcon + labelClassName="!text-base font-semibold" + containerClassName="mx-2" + /> + )} + {!live && ( + toggleBottomTools(STACKEVENTS)} + active={bottomBlock === STACKEVENTS && !inspectorMode} + label="EVENTS" + noIcon + labelClassName="!text-base font-semibold" + containerClassName="mx-2" + hasErrors={stackRedCount > 0} + /> + )} + {!live && showProfiler && ( + toggleBottomTools(PROFILER)} + active={bottomBlock === PROFILER && !inspectorMode} + label="PROFILER" + noIcon + labelClassName="!text-base font-semibold" + containerClassName="mx-2" + /> + )} + {!live && ( + + {controlIcon( + 'arrows-angle-extend', + 16, + props.fullscreenOn, + false, + 'rounded hover:bg-gray-light-shade color-gray-medium' + )} + + )} +
+
+ )} +
+ ); +} + +const ControlPlayer = observer(Controls); + +export default connect( + (state: any) => { + const permissions = state.getIn(['user', 'account', 'permissions']) || []; + const isEnterprise = state.getIn(['user', 'account', 'edition']) === 'ee'; + return { + disabledRedux: isEnterprise && !permissions.includes('DEV_TOOLS'), + fullscreen: state.getIn(['components', 'player', 'fullscreen']), + bottomBlock: state.getIn(['components', 'player', 'bottomBlock']), + showStorageRedux: !state.getIn(['components', 'player', 'hiddenHints', 'storage']), + showStackRedux: !state.getIn(['components', 'player', 'hiddenHints', 'stack']), + closedLive: + !!state.getIn(['sessions', 'errors']) || !state.getIn(['sessions', 'current', 'live']), + skipInterval: state.getIn(['components', 'player', 'skipInterval']), + }; + }, + { + fullscreenOn, + fullscreenOff, + toggleBottomBlock, + changeSkipInterval, + } +)(ControlPlayer); + +// shouldComponentUpdate(nextProps) { +// if ( +// nextProps.fullscreen !== props.fullscreen || +// nextProps.bottomBlock !== props.bottomBlock || +// nextProps.live !== props.live || +// nextProps.livePlay !== props.livePlay || +// nextProps.playing !== props.playing || +// nextProps.completed !== props.completed || +// nextProps.skip !== props.skip || +// nextProps.skipToIssue !== props.skipToIssue || +// nextProps.speed !== props.speed || +// nextProps.disabled !== props.disabled || +// nextProps.fullscreenDisabled !== props.fullscreenDisabled || +// // nextProps.inspectorMode !== props.inspectorMode || +// // nextProps.logCount !== props.logCount || +// nextProps.logRedCount !== props.logRedCount || +// nextProps.showExceptions !== props.showExceptions || +// nextProps.resourceRedCount !== props.resourceRedCount || +// nextProps.fetchRedCount !== props.fetchRedCount || +// nextProps.showStack !== props.showStack || +// nextProps.stackCount !== props.stackCount || +// nextProps.stackRedCount !== props.stackRedCount || +// nextProps.profilesCount !== props.profilesCount || +// nextProps.storageCount !== props.storageCount || +// nextProps.storageType !== props.storageType || +// nextProps.showStorage !== props.showStorage || +// nextProps.showProfiler !== props.showProfiler || +// nextProps.showGraphql !== props.showGraphql || +// nextProps.showFetch !== props.showFetch || +// nextProps.fetchCount !== props.fetchCount || +// nextProps.graphqlCount !== props.graphqlCount || +// nextProps.liveTimeTravel !== props.liveTimeTravel || +// nextProps.skipInterval !== props.skipInterval +// ) +// return true; +// return false; +// } diff --git a/frontend/app/components/Session_/Player/Controls/Time.js b/frontend/app/components/Session_/Player/Controls/Time.js index ab6f5e6b8..db665fc8e 100644 --- a/frontend/app/components/Session_/Player/Controls/Time.js +++ b/frontend/app/components/Session_/Player/Controls/Time.js @@ -12,21 +12,17 @@ const Time = ({ time, isCustom, format = 'm:ss', }) => ( Time.displayName = "Time"; -const ReduxTime = observer(({ format, name }) => { +const ReduxTime = observer(({ format, name, isCustom }) => { const { store } = React.useContext(PlayerContext) const time = store.get()[name] - return
-
-
- ); - } - - render() { - const { type, listNow, list, hintIsHidden } = this.props; - - const showStore = type !== STORAGE_TYPES.MOBX; - return ( - - - {list.length > 0 && ( -
- {showStore &&

{'STATE'}

} - {this.state.showDiffs ? ( -

- DIFFS -

- ) : null} -

{getActionsName(type)}

-

- - TTE - -

-
- )} -
- - - { - 'Inspect your application state while you’re replaying your users sessions. OpenReplay supports ' - } - - Redux - - {', '} - - VueX - - {', '} - - Pinia - - {', '} - - Zustand - - {', '} - - MobX - - {' and '} - - NgRx - - . -
-
- - - ) : null - } - size="small" - show={listNow.length === 0} - > - {showStore && ( -
- {listNow.length === 0 ? ( -
- {'Empty state.'} -
- ) : ( - this.renderTab() - )} -
- )} -
- - {listNow.map((item, i) => - this.renderItem(item, i, i > 0 ? listNow[i - 1] : undefined) - )} - -
-
-
-
- ); - } -} diff --git a/frontend/app/components/Session_/Storage/Storage.tsx b/frontend/app/components/Session_/Storage/Storage.tsx new file mode 100644 index 000000000..0f5e682c5 --- /dev/null +++ b/frontend/app/components/Session_/Storage/Storage.tsx @@ -0,0 +1,315 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { hideHint } from 'Duck/components/player'; +import { PlayerContext } from 'App/components/Session/playerContext'; +import { observer } from 'mobx-react-lite'; +import { JSONTree, NoContent, Tooltip } from 'UI'; +import { formatMs } from 'App/date'; +import { diff } from 'deep-diff'; +import { STORAGE_TYPES, selectStorageList, selectStorageListNow, selectStorageType } from 'Player'; +import Autoscroll from '../Autoscroll'; +import BottomBlock from '../BottomBlock/index'; +import DiffRow from './DiffRow'; +import cn from 'classnames'; +import stl from './storage.module.css'; + +function getActionsName(type: string) { + switch (type) { + case STORAGE_TYPES.MOBX: + case STORAGE_TYPES.VUEX: + return 'MUTATIONS'; + default: + return 'ACTIONS'; + } +} + +interface Props { + hideHint: (args: string) => void; + hintIsHidden: boolean; +} +function Storage(props: Props) { + const lastBtnRef = React.useRef(); + const [showDiffs, setShowDiffs] = React.useState(false); + const { player, store } = React.useContext(PlayerContext); + const state = store.get(); + + const listNow = selectStorageListNow(state); + const list = selectStorageList(state); + const type = selectStorageType(state); + + const focusNextButton = () => { + if (lastBtnRef.current) { + lastBtnRef.current.focus(); + } + }; + + React.useEffect(() => { + focusNextButton(); + }, []); + React.useEffect(() => { + focusNextButton(); + }, [listNow]); + + const renderDiff = (item: Record, prevItem: Record) => { + if (!prevItem) { + // we don't have state before first action + return
; + } + + const stateDiff = diff(prevItem.state, item.state); + + if (!stateDiff) { + return ( +
+ No diff +
+ ); + } + + return ( +
+ {stateDiff.map((d: Record, i: number) => renderDiffs(d, i))} +
+ ); + }; + + const renderDiffs = (diff: Record, i: number) => { + const path = createPath(diff); + return ( + + + + ); + }; + + const createPath = (diff: Record) => { + let path: string[] = []; + + if (diff.path) { + path = path.concat(diff.path); + } + if (typeof diff.index !== 'undefined') { + path.push(diff.index); + } + + const pathStr = path.length ? path.join('.') : ''; + return pathStr; + }; + + const ensureString = (actionType: string) => { + if (typeof actionType === 'string') return actionType; + return 'UNKNOWN'; + }; + + const goNext = () => { + // , list[listNow.length]._index + player.jump(list[listNow.length].time); + }; + + const renderTab = () => { + if (listNow.length === 0) { + return 'Not initialized'; //? + } + return ; + }; + + const renderItem = (item: Record, i: number, prevItem: Record) => { + let src; + let name; + + switch (type) { + case STORAGE_TYPES.REDUX: + case STORAGE_TYPES.NGRX: + src = item.action; + name = src && src.type; + break; + case STORAGE_TYPES.VUEX: + src = item.mutation; + name = src && src.type; + break; + case STORAGE_TYPES.MOBX: + src = item.payload; + name = `@${item.type} ${src && src.type}`; + break; + case STORAGE_TYPES.ZUSTAND: + src = null; + name = item.mutation.join(''); + } + + if (src !== null && !showDiffs) { + setShowDiffs(true); + } + + return ( +
+ {src === null ? ( +
+ {name} +
+ ) : ( + <> + {renderDiff(item, prevItem)} +
+ +
+ + )} +
+ {typeof item.duration === 'number' && ( +
{formatMs(item.duration)}
+ )} +
+ {i + 1 < listNow.length && ( + + )} + {i + 1 === listNow.length && i + 1 < list.length && ( + + )} +
+
+
+ ); + }; + + const { hintIsHidden } = props; + + const showStore = type !== STORAGE_TYPES.MOBX; + return ( + + + {list.length > 0 && ( +
+ {showStore && ( +

+ {'STATE'} +

+ )} + {showDiffs ? ( +

+ DIFFS +

+ ) : null} +

+ {getActionsName(type)} +

+

+ TTE +

+
+ )} +
+ + + { + 'Inspect your application state while you’re replaying your users sessions. OpenReplay supports ' + } + + Redux + + {', '} + + VueX + + {', '} + + Pinia + + {', '} + + Zustand + + {', '} + + MobX + + {' and '} + + NgRx + + . +
+
+ + + ) : null + } + size="small" + show={listNow.length === 0} + > + {showStore && ( +
+ {listNow.length === 0 ? ( +
+ {'Empty state.'} +
+ ) : ( + renderTab() + )} +
+ )} +
+ + {listNow.map((item: Record, i: number) => + renderItem(item, i, i > 0 ? listNow[i - 1] : undefined) + )} + +
+
+
+
+ ); +} + +export default connect( + (state: any) => ({ + hintIsHidden: state.getIn(['components', 'player', 'hiddenHints', 'storage']), + }), + { + hideHint, + } +)(observer(Storage)); diff --git a/frontend/app/components/Session_/components/NotePopup.tsx b/frontend/app/components/Session_/components/NotePopup.tsx index e229d534b..a5b44cfe0 100644 --- a/frontend/app/components/Session_/components/NotePopup.tsx +++ b/frontend/app/components/Session_/components/NotePopup.tsx @@ -4,7 +4,6 @@ import { connect } from 'react-redux'; import { setCreateNoteTooltip } from 'Duck/sessions'; import GuidePopup from 'Shared/GuidePopup'; import { PlayerContext } from 'App/components/Session/playerContext'; -import { observer } from 'mobx-react-lite'; function NotePopup({ setCreateNoteTooltip, @@ -14,12 +13,11 @@ function NotePopup({ tooltipActive: boolean; }) { const { player, store } = React.useContext(PlayerContext) - const { time } = store.get(); const toggleNotePopup = () => { if (tooltipActive) return; player.pause(); - setCreateNoteTooltip({ time: time, isVisible: true }); + setCreateNoteTooltip({ time: store.get().time, isVisible: true }); }; React.useEffect(() => { @@ -42,11 +40,9 @@ function NotePopup({ ); } -const NotePopupPl = observer(NotePopup); - const NotePopupComp = connect( (state: any) => ({ tooltipActive: state.getIn(['sessions', 'createNoteTooltip', 'isVisible']) }), { setCreateNoteTooltip } -)(NotePopupPl); +)(NotePopup); export default React.memo(NotePopupComp); diff --git a/frontend/app/components/shared/GuidePopup/GuidePopup.tsx b/frontend/app/components/shared/GuidePopup/GuidePopup.tsx index c0f0037b2..0b95e7f75 100644 --- a/frontend/app/components/shared/GuidePopup/GuidePopup.tsx +++ b/frontend/app/components/shared/GuidePopup/GuidePopup.tsx @@ -69,6 +69,8 @@ export default function GuidePopup({ children, title, description }: IProps) {
) : ( - children + <> + {children} + ); } diff --git a/frontend/app/components/ui/Tooltip/Tooltip.tsx b/frontend/app/components/ui/Tooltip/Tooltip.tsx index 5cc7a021c..87520b625 100644 --- a/frontend/app/components/ui/Tooltip/Tooltip.tsx +++ b/frontend/app/components/ui/Tooltip/Tooltip.tsx @@ -4,7 +4,7 @@ import type { Placement } from '@floating-ui/react-dom-interactions'; import cn from 'classnames'; interface Props { - title?: any; + title: React.ReactNode; children: any; disabled?: boolean; open?: boolean; @@ -13,6 +13,7 @@ interface Props { delay?: number; style?: any; offset?: number; + anchorClassName?: string; } function Tooltip(props: Props) { const { @@ -21,6 +22,7 @@ function Tooltip(props: Props) { open = false, placement, className = '', + anchorClassName = '', delay = 500, style = {}, offset = 5, @@ -38,7 +40,7 @@ function Tooltip(props: Props) { return (
- {props.children} + {props.children} 0) { + return this.jump( + Math.min( + endTime, + time + interval + ) + ); + } else { + return this.jump( + Math.max( + 0, + time - interval + ) + ); + } + } + // TODO: clearify logic of live time-travel jumpToLive() { cancelAnimationFrame(this.animationFrameRequestId) @@ -177,4 +197,4 @@ export default class Animator { } -} \ No newline at end of file +} diff --git a/frontend/app/player/web/InspectorController.ts b/frontend/app/player/web/InspectorController.ts index d2c4b6624..44f798bea 100644 --- a/frontend/app/player/web/InspectorController.ts +++ b/frontend/app/player/web/InspectorController.ts @@ -16,7 +16,7 @@ export default class InspectorController { } } - enableInspector(clickCallback: (e: { target: Element }) => void): Document | null { + enableInspector(clickCallback?: (e: { target: Element }) => void): Document | null { const parent = this.screen.getParentElement() if (!parent) return null; if (!this.substitutor) { @@ -28,7 +28,7 @@ export default class InspectorController { } this.substitutor.display(false) - + const docElement = this.screen.document?.documentElement // this.substitutor.document?.importNode( const doc = this.substitutor.document if (doc && docElement) { @@ -43,11 +43,11 @@ export default class InspectorController { this.substitutor.display(true); return doc; } - + disableInspector() { if (this.substitutor) { const doc = this.substitutor.document; - if (doc) { + if (doc) { doc.documentElement.innerHTML = ""; } this.inspector.clean(); @@ -56,4 +56,4 @@ export default class InspectorController { this.screen.display(true); } -} \ No newline at end of file +} diff --git a/frontend/app/player/web/Screen/Inspector.ts b/frontend/app/player/web/Screen/Inspector.ts index b0ea1ca9c..028f7f919 100644 --- a/frontend/app/player/web/Screen/Inspector.ts +++ b/frontend/app/player/web/Screen/Inspector.ts @@ -1,14 +1,14 @@ import type Screen from './Screen' import type Marker from './Marker' -//import { select } from 'optimal-select'; +//import { select } from 'optimal-select'; export default class Inspector { // private captureCallbacks = []; // private bubblingCallbacks = []; constructor(private screen: Screen, private marker: Marker) {} - private onMouseMove = (e: MouseEvent) => { + private onMouseMove = (e: MouseEvent) => { // const { overlay } = this.screen; // if (!overlay.contains(e.target)) { // return; @@ -21,7 +21,7 @@ export default class Inspector { return; } - this.marker.mark(target); + this.marker.mark(target); } private onOverlayLeave = () => { @@ -57,7 +57,7 @@ export default class Inspector { // } private clickCallback: (e: { target: Element }) => void | null = null - enable(clickCallback: Inspector['clickCallback']) { + enable(clickCallback?: Inspector['clickCallback']) { this.screen.overlay.addEventListener('mousemove', this.onMouseMove) this.screen.overlay.addEventListener('mouseleave', this.onOverlayLeave) this.screen.overlay.addEventListener('click', this.onMarkClick) @@ -67,4 +67,4 @@ export default class Inspector { this.screen.overlay.removeEventListener('mouseleave', this.onOverlayLeave) this.screen.overlay.removeEventListener('click', this.onMarkClick) } -} \ No newline at end of file +} diff --git a/frontend/app/player/web/WebPlayer.ts b/frontend/app/player/web/WebPlayer.ts index ec132d5f8..4467bdef5 100644 --- a/frontend/app/player/web/WebPlayer.ts +++ b/frontend/app/player/web/WebPlayer.ts @@ -4,7 +4,7 @@ import Player, { State as PlayerState } from '../player/Player' import MessageManager from './MessageManager' import InspectorController from './InspectorController' import TargetMarker from './TargetMarker' -import AssistManager, { +import AssistManager, { INITIAL_STATE as ASSIST_INITIAL_STATE, } from './assist/AssistManager' import Screen from './Screen/Screen' @@ -40,7 +40,7 @@ export default class WebPlayer extends Player { } : {} const screen = new Screen() - const messageManager = new MessageManager(session, wpState, screen, initialLists) + const messageManager = new MessageManager(session, wpState, screen, initialLists) super(wpState, messageManager) this.screen = screen this.messageManager = messageManager @@ -48,14 +48,14 @@ export default class WebPlayer extends Player { this.targetMarker = new TargetMarker(this.screen, wpState) this.inspectorController = new InspectorController(screen) - + const endTime = !live && session.duration.valueOf() wpState.update({ //@ts-ignore initialized: true, //@ts-ignore session, - + live, livePlay: live, endTime, // : 0, //TODO: through initialState @@ -85,7 +85,7 @@ export default class WebPlayer extends Player { mark(e: Element) { this.inspectorController.marker?.mark(e) } - toggleInspectorMode(flag: boolean, clickCallback) { + toggleInspectorMode(flag: boolean, clickCallback?: (args: any) => any) { if (typeof flag !== 'boolean') { const { inspectorMode } = this.wpState.get() flag = !inspectorMode; @@ -104,6 +104,7 @@ export default class WebPlayer extends Player { setActiveTarget(args: Parameters) { this.targetMarker.setActiveTarget(...args) } + markTargets(args: Parameters) { this.pause() this.targetMarker.markTargets(...args) @@ -133,11 +134,10 @@ export default class WebPlayer extends Player { toggleUserName(name?: string) { this.screen.cursor.showTag(name) } - + clean() { super.clean() this.assistManager.clean() window.removeEventListener('resize', this.scale) } } - diff --git a/frontend/app/player/web/assist/AssistManager.ts b/frontend/app/player/web/assist/AssistManager.ts index 1c9984b51..1d2fe08db 100644 --- a/frontend/app/player/web/assist/AssistManager.ts +++ b/frontend/app/player/web/assist/AssistManager.ts @@ -461,7 +461,7 @@ export default class AssistManager { onStream: (s: MediaStream)=>void, onCallEnd: () => void, onReject: () => void, - onError?: ()=> void, + onError?: (e?: any)=> void, ) { this.callArgs = { localStream, @@ -472,7 +472,7 @@ export default class AssistManager { } } - public call(thirdPartyPeers?: string[]): { end: Function } { + public call(thirdPartyPeers?: string[]): { end: () => void } { if (thirdPartyPeers && thirdPartyPeers.length > 0) { this.addPeerCall(thirdPartyPeers) } else { From 2e9ff89976367e3ca022f5b597b128a606c592b8 Mon Sep 17 00:00:00 2001 From: MauricioGarciaS <47052044+MauricioGarciaS@users.noreply.github.com> Date: Fri, 25 Nov 2022 11:48:58 +0100 Subject: [PATCH 037/252] Added queue method to Signals --- ee/api/app.py | 3 +++ ee/api/chalicelib/core/signals.py | 10 +++++++++- ee/api/chalicelib/utils/events_queue.py | 20 ++++++++------------ ee/api/routers/core_dynamic.py | 2 +- ee/api/routers/crons/core_dynamic_crons.py | 7 ++++++- ee/recommendation/api.py | 6 +++++- 6 files changed, 32 insertions(+), 16 deletions(-) diff --git a/ee/api/app.py b/ee/api/app.py index 055706792..f3edcefe3 100644 --- a/ee/api/app.py +++ b/ee/api/app.py @@ -12,6 +12,7 @@ from starlette.responses import StreamingResponse, JSONResponse from chalicelib.core import traces from chalicelib.utils import helper from chalicelib.utils import pg_client +from chalicelib.utils import events_queue from routers import core, core_dynamic, ee, saml from routers.crons import core_crons from routers.crons import core_dynamic_crons @@ -81,6 +82,7 @@ app.queue_system = queue.Queue() async def startup(): logging.info(">>>>> starting up <<<<<") await pg_client.init() + await events_queue.init() app.schedule.start() for job in core_crons.cron_jobs + core_dynamic_crons.cron_jobs + traces.cron_jobs: @@ -96,6 +98,7 @@ async def shutdown(): logging.info(">>>>> shutting down <<<<<") app.schedule.shutdown(wait=True) await traces.process_traces_queue() + await events_queue.terminate() await pg_client.terminate() diff --git a/ee/api/chalicelib/core/signals.py b/ee/api/chalicelib/core/signals.py index ed0664bde..cb4d714c4 100644 --- a/ee/api/chalicelib/core/signals.py +++ b/ee/api/chalicelib/core/signals.py @@ -4,10 +4,10 @@ import schemas_ee import logging from chalicelib.utils import helper from chalicelib.utils import pg_client +from chalicelib.utils import events_queue def handle_frontend_signals(project_id: int, user_id: str, data: schemas_ee.SignalsSchema): - res = {'errors': 'query not executed'} insights_query = """INSERT INTO public.frontend_signals (project_id, user_id, timestamp, action, source, category, data) VALUES (%(project_id)s, %(user_id)s, %(timestamp)s, %(action)s, %(source)s, %(category)s, %(data)s)""" try: with pg_client.PostgresClient() as conn: @@ -19,3 +19,11 @@ def handle_frontend_signals(project_id: int, user_id: str, data: schemas_ee.Sign except Exception as e: logging.info(f'Error while inserting: {e}') return {'errors': [e]} + +def handle_frontend_signals_queued(project_id: int, user_id: str, data: schemas_ee.SignalsSchema): + try: + events_queue.global_queue.put((project_id, user_id, data)) + return {'data': 'insertion succeded'} + except Exception as e: + logging.info(f'Error while inserting: {e}') + return {'errors': [e]} diff --git a/ee/api/chalicelib/utils/events_queue.py b/ee/api/chalicelib/utils/events_queue.py index a37cec121..c6208bc79 100644 --- a/ee/api/chalicelib/utils/events_queue.py +++ b/ee/api/chalicelib/utils/events_queue.py @@ -1,12 +1,7 @@ import queue import logging -# import threading -# import schemas -# import schemas_ee - -#from utils import helper -from utils import pg_client +from chalicelib.utils import pg_client global_queue = None @@ -20,15 +15,16 @@ class EventQueue(): def flush(self, conn): events = list() while not self.events.empty(): - events.append(self.events.get()) - # self.events.task_done() + project_id, user_id, element = self.events.get() + events.append("({project_id}, '{user_id}', {timestamp}, '{action}', '{source}', '{category}', '{data}')".format( + project_id=project_id, user_id=user_id, timestamp=element.timestamp, action=element.action, source=element.source, category=element.category, data=element.data)) + if len(events)==0: + return 0 if self.test: print(events) return 1 - _query = """INSERT INTO {database}.{table} (project_id, user_id, timestamp, action, source, category, data) VALUES %(events)s""".format( - database='public', table='frontend_signals') - _query = conn.mogrify(_query, {'events': (0, 'test', 0, 'action', 's', 'c', '{}')}) - conn.execute(_query) + _base_query = 'INSERT INTO {database}.{table} (project_id, user_id, timestamp, action, source, category, data) VALUES {values_list}' + conn.execute(_base_query.format(database='public', table='frontend_signals', values_list=', '.join(events))) # logging.info(_query) # res = 'done' # res = conn.fetchone() diff --git a/ee/api/routers/core_dynamic.py b/ee/api/routers/core_dynamic.py index 3250df394..addbe749b 100644 --- a/ee/api/routers/core_dynamic.py +++ b/ee/api/routers/core_dynamic.py @@ -440,7 +440,7 @@ def get_all_notes(projectId: int, data: schemas.SearchNoteSchema = Body(...), @app.post('/{projectId}/signals', tags=['signals']) def send_interactions(projectId:int, data: schemas_ee.SignalsSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): - data = signals.handle_frontend_signals(project_id=projectId, user_id=context.user_id, data=data) + data = signals.handle_frontend_signals_queued(project_id=projectId, user_id=context.user_id, data=data) if "errors" in data: return data diff --git a/ee/api/routers/crons/core_dynamic_crons.py b/ee/api/routers/crons/core_dynamic_crons.py index 1d8320eb7..f57b341bb 100644 --- a/ee/api/routers/crons/core_dynamic_crons.py +++ b/ee/api/routers/crons/core_dynamic_crons.py @@ -1,6 +1,7 @@ from chalicelib.core import telemetry, unlock from chalicelib.core import jobs from chalicelib.core import weekly_report as weekly_report_script +from chalicelib.utils import events_queue from decouple import config @@ -22,6 +23,9 @@ def unlock_cron() -> None: print(f"valid: {unlock.is_valid()}") +def pg_events_queue() -> None: + events_queue.global_queue.force_flush() + cron_jobs = [ {"func": unlock_cron, "trigger": "cron", "hour": "*"} ] @@ -29,7 +33,8 @@ cron_jobs = [ SINGLE_CRONS = [{"func": telemetry_cron, "trigger": "cron", "day_of_week": "*"}, {"func": run_scheduled_jobs, "trigger": "interval", "seconds": 60, "misfire_grace_time": 20}, {"func": weekly_report, "trigger": "cron", "day_of_week": "mon", "hour": 5, - "misfire_grace_time": 60 * 60}] + "misfire_grace_time": 60 * 60}, + {"func": pg_events_queue, "trigger": "cron", "interval": 60*5, "misfire_grace_time": 20}] if config("LOCAL_CRONS", default=False, cast=bool): cron_jobs += SINGLE_CRONS diff --git a/ee/recommendation/api.py b/ee/recommendation/api.py index 213d797fa..f9d3e05bf 100644 --- a/ee/recommendation/api.py +++ b/ee/recommendation/api.py @@ -4,6 +4,7 @@ from fastapi import FastAPI # from fastapi_utils.tasks import repeat_every from utils import events_queue from utils import pg_client +from utils import schemas_ee app = FastAPI() app.schedule = AsyncIOScheduler() @@ -17,7 +18,10 @@ def home(): @app.put('/value/{value}') def number(value: int): logging.info(f'> {value} as input. Testing queue with pg') - events_queue.global_queue.put(value) + d = {'timestamp': 23786, 'action': 'action', 'source': 'source', 'category': 'cat', 'data': {}} + events = schemas_ee.SignalsSchema + event = events.parse_obj(d) + events_queue.global_queue.put((value, 0, event)) @app.on_event("startup") From c4dc4ffded40e3cb8f4cf8df201a2e26d39746af Mon Sep 17 00:00:00 2001 From: sylenien Date: Fri, 25 Nov 2022 11:52:27 +0100 Subject: [PATCH 038/252] refactor(ui/player): fix errors after merge --- .../Session_/Exceptions/Exceptions.js | 2 - .../Session_/OverviewPanel/OverviewPanel.tsx | 6 +- .../app/components/Session_/Player/Player.js | 1 + frontend/app/player/_web/MessageManager.ts | 570 ++++++++++++++++++ 4 files changed, 573 insertions(+), 6 deletions(-) create mode 100644 frontend/app/player/_web/MessageManager.ts diff --git a/frontend/app/components/Session_/Exceptions/Exceptions.js b/frontend/app/components/Session_/Exceptions/Exceptions.js index 0eabdf314..2b0c206b4 100644 --- a/frontend/app/components/Session_/Exceptions/Exceptions.js +++ b/frontend/app/components/Session_/Exceptions/Exceptions.js @@ -8,10 +8,8 @@ import { ErrorItem, SlideModal, ErrorDetails, - ErrorHeader, Link, QuestionMarkHint, - Tabs, } from 'UI'; import { fetchErrorStackList } from 'Duck/sessions'; import { connectPlayer, jump } from 'Player'; diff --git a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx index 0b542dce6..81ed0648d 100644 --- a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx +++ b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx @@ -14,10 +14,8 @@ import { NoContent, Icon } from 'UI'; import { observer } from 'mobx-react-lite'; import { PlayerContext } from 'App/components/Session/playerContext'; -function OverviewPanel({ issuesList }: { issuesList: any[] }) { - const { store } = React.useContext(PlayerContext) - function OverviewPanel() { + const { store } = React.useContext(PlayerContext) const [dataLoaded, setDataLoaded] = React.useState(false); const [selectedFeatures, setSelectedFeatures] = React.useState([ 'PERFORMANCE', @@ -140,4 +138,4 @@ export default connect( } )( observer(OverviewPanel) -); +) diff --git a/frontend/app/components/Session_/Player/Player.js b/frontend/app/components/Session_/Player/Player.js index 9312589f8..b283b669a 100644 --- a/frontend/app/components/Session_/Player/Player.js +++ b/frontend/app/components/Session_/Player/Player.js @@ -47,6 +47,7 @@ function Player(props) { closedLive, bottomBlock, activeTab, + fullView, } = props; const playerContext = React.useContext(PlayerContext) const screenWrapper = React.useRef(); diff --git a/frontend/app/player/_web/MessageManager.ts b/frontend/app/player/_web/MessageManager.ts new file mode 100644 index 000000000..49105200c --- /dev/null +++ b/frontend/app/player/_web/MessageManager.ts @@ -0,0 +1,570 @@ +// @ts-ignore +import { Decoder } from "syncod"; +import logger from 'App/logger'; + +import Resource, { TYPES } from 'Types/session/resource'; +import { TYPES as EVENT_TYPES } from 'Types/session/event'; +import Log from 'Types/session/log'; + +import { toast } from 'react-toastify'; + +import type { Store } from '../player/types'; +import ListWalker from '../_common/ListWalker'; + +import Screen from './Screen/Screen'; + +import PagesManager from './managers/PagesManager'; +import MouseMoveManager from './managers/MouseMoveManager'; + +import PerformanceTrackManager from './managers/PerformanceTrackManager'; +import WindowNodeCounter from './managers/WindowNodeCounter'; +import ActivityManager from './managers/ActivityManager'; + +import MFileReader from './messages/MFileReader'; +import { loadFiles, requestEFSDom, requestEFSDevtools } from './network/loadFiles'; +import { decryptSessionBytes } from './network/crypto'; + +import { INITIAL_STATE as SUPER_INITIAL_STATE, State as SuperState } from './Screen/Screen'; +import { INITIAL_STATE as ASSIST_INITIAL_STATE, State as AssistState } from './assist/AssistManager'; +import Lists, { INITIAL_STATE as LISTS_INITIAL_STATE } from './Lists'; + +import type { PerformanceChartPoint } from './managers/PerformanceTrackManager'; +import type { SkipInterval } from './managers/ActivityManager'; + + +export interface State extends SuperState, AssistState { + performanceChartData: PerformanceChartPoint[], + skipIntervals: SkipInterval[], + connType?: string, + connBandwidth?: number, + location?: string, + performanceChartTime?: number, + + domContentLoadedTime?: any, + domBuildingTime?: any, + loadTime?: any, + error: boolean, + devtoolsLoading: boolean, + + liveTimeTravel: boolean, + messagesLoading: boolean, + cssLoading: boolean, + + ready: boolean, + lastMessageTime: number, +} + +export const INITIAL_STATE: State = { + ...SUPER_INITIAL_STATE, + ...LISTS_INITIAL_STATE, + ...ASSIST_INITIAL_STATE, + performanceChartData: [], + skipIntervals: [], + error: false, + devtoolsLoading: false, + + liveTimeTravel: false, + messagesLoading: false, + cssLoading: false, + get ready() { + return !this.messagesLoading && !this.cssLoading + }, + lastMessageTime: 0, +}; + + +import type { + Message, + SetPageLocation, + ConnectionInformation, + SetViewportSize, + SetViewportScroll, + MouseClick, +} from './messages'; + +const visualChanges = [ + "mouse_move", + "mouse_click", + "create_element_node", + "set_input_value", + "set_input_checked", + "set_viewport_size", + "set_viewport_scroll", +] + +export default class MessageManager extends Screen { + // TODO: consistent with the other data-lists + private locationEventManager: ListWalker/**/ = new ListWalker(); + private locationManager: ListWalker = new ListWalker(); + private loadedLocationManager: ListWalker = new ListWalker(); + private connectionInfoManger: ListWalker = new ListWalker(); + private performanceTrackManager: PerformanceTrackManager = new PerformanceTrackManager(); + private windowNodeCounter: WindowNodeCounter = new WindowNodeCounter(); + private clickManager: ListWalker = new ListWalker(); + + private resizeManager: ListWalker = new ListWalker([]); + private pagesManager: PagesManager; + private mouseMoveManager: MouseMoveManager; + + private scrollManager: ListWalker = new ListWalker(); + + private readonly decoder = new Decoder(); + private readonly lists: Lists; + + private activityManager: ActivityManager | null = null; + + private sessionStart: number; + private navigationStartOffset: number = 0; + private lastMessageTime: number = 0; + private lastMessageInFileTime: number = 0; + + constructor( + private readonly session: any /*Session*/, + private readonly state: Store, + config: any, + live: boolean, + ) { + super(); + this.pagesManager = new PagesManager(this, this.session.isMobile) + this.mouseMoveManager = new MouseMoveManager(this); + + this.sessionStart = this.session.startedAt; + + if (live) { + this.lists = new Lists() + } else { + this.activityManager = new ActivityManager(this.session.duration.milliseconds); + /* == REFACTOR_ME == */ + const eventList = session.events.toJSON(); + // TODO: fix types for events, remove immutable js + eventList.forEach((e: Record) => { + if (e.type === EVENT_TYPES.LOCATION) { //TODO type system + this.locationEventManager.append(e); + } + }) + + this.lists = new Lists({ + event: eventList, + stack: session.stackEvents.toJSON(), + resource: session.resources.toJSON(), + exceptions: session.errors, + }) + + + /* === */ + this.loadMessages(); + } + } + + private parseAndDistributeMessages(fileReader: MFileReader, onMessage?: (msg: Message) => void) { + const msgs: Array = [] + let next: ReturnType + while (next = fileReader.next()) { + const [msg, index] = next + this.distributeMessage(msg, index) + msgs.push(msg) + onMessage?.(msg) + } + + logger.info("Messages count: ", msgs.length, msgs) + + + // @ts-ignore Hack for upet (TODO: fix ordering in one mutation in tracker(removes first)) + const headChildrenIds = msgs.filter(m => m.parentID === 1).map(m => m.id); + this.pagesManager.sortPages((m1, m2) => { + if (m1.time === m2.time) { + if (m1.tp === "remove_node" && m2.tp !== "remove_node") { + if (headChildrenIds.includes(m1.id)) { + return -1; + } + } else if (m2.tp === "remove_node" && m1.tp !== "remove_node") { + if (headChildrenIds.includes(m2.id)) { + return 1; + } + } else if (m2.tp === "remove_node" && m1.tp === "remove_node") { + const m1FromHead = headChildrenIds.includes(m1.id); + const m2FromHead = headChildrenIds.includes(m2.id); + if (m1FromHead && !m2FromHead) { + return -1; + } else if (m2FromHead && !m1FromHead) { + return 1; + } + } + } + return 0; + }) + } + + + private waitingForFiles: boolean = false + private onFileReadSuccess = () => { + const stateToUpdate = { + performanceChartData: this.performanceTrackManager.chartData, + performanceAvaliability: this.performanceTrackManager.avaliability, + ...this.lists.getFullListsState() + } + if (this.activityManager) { + this.activityManager.end() + stateToUpdate.skipIntervals = this.activityManager.list + } + + this.state.update(stateToUpdate) + } + private onFileReadFailed = (e: any) => { + logger.error(e) + this.state.update({ error: true }) + toast.error('Error requesting a session file') + } + private onFileReadFinally = () => { + this.incomingMessages + .filter(msg => msg.time >= this.lastMessageInFileTime) + .forEach(msg => this.distributeMessage(msg, 0)) + + this.waitingForFiles = false + this.setMessagesLoading(false) + } + + private loadMessages() { + // TODO: reuseable decryptor instance + const createNewParser = (shouldDecrypt=true) => { + const decrypt = shouldDecrypt && this.session.fileKey + ? (b: Uint8Array) => decryptSessionBytes(b, this.session.fileKey) + : (b: Uint8Array) => Promise.resolve(b) + // Each time called - new fileReader created + const fileReader = new MFileReader(new Uint8Array(), this.sessionStart) + return (b: Uint8Array) => decrypt(b).then(b => { + fileReader.append(b) + this.parseAndDistributeMessages(fileReader) + this.setMessagesLoading(false) + }) + } + this.setMessagesLoading(true) + this.waitingForFiles = true + + loadFiles(this.session.domURL, createNewParser()) + .catch(() => // do if only the first file missing (404) (?) + requestEFSDom(this.session.sessionId) + .then(createNewParser(false)) + // Fallback to back Compatability with mobsUrl + .catch(e => + loadFiles(this.session.mobsUrl, createNewParser(false)) + ) + ) + .then(this.onFileReadSuccess) + .catch(this.onFileReadFailed) + .finally(this.onFileReadFinally) + + // load devtools + if (this.session.devtoolsURL.length) { + this.state.update({ devtoolsLoading: true }) + loadFiles(this.session.devtoolsURL, createNewParser()) + .catch(() => + requestEFSDevtools(this.session.sessionId) + .then(createNewParser(false)) + ) + //.catch() // not able to download the devtools file + .finally(() => this.state.update({ devtoolsLoading: false })) + } + } + + reloadWithUnprocessedFile() { + const onData = (byteArray: Uint8Array) => { + const onMessage = (msg: Message) => { this.lastMessageInFileTime = msg.time } + this.parseAndDistributeMessages(new MFileReader(byteArray, this.sessionStart), onMessage) + } + const updateState = () => + this.state.update({ + liveTimeTravel: true, + }); + + // assist will pause and skip messages to prevent timestamp related errors + this.reloadMessageManagers() + this.windowNodeCounter.reset() + + this.setMessagesLoading(true) + this.waitingForFiles = true + + return requestEFSDom(this.session.sessionId) + .then(onData) + .then(updateState) + .then(this.onFileReadSuccess) + .catch(this.onFileReadFailed) + .finally(this.onFileReadFinally) + } + + private reloadMessageManagers() { + this.locationEventManager = new ListWalker(); + this.locationManager = new ListWalker(); + this.loadedLocationManager = new ListWalker(); + this.connectionInfoManger = new ListWalker(); + this.clickManager = new ListWalker(); + this.scrollManager = new ListWalker(); + this.resizeManager = new ListWalker([]); + + this.performanceTrackManager = new PerformanceTrackManager() + this.windowNodeCounter = new WindowNodeCounter(); + this.pagesManager = new PagesManager(this, this.session.isMobile) + this.mouseMoveManager = new MouseMoveManager(this); + this.activityManager = new ActivityManager(this.session.duration.milliseconds); + } + + move(t: number, index?: number): void { + const stateToUpdate: Partial = {}; + /* == REFACTOR_ME == */ + const lastLoadedLocationMsg = this.loadedLocationManager.moveGetLast(t, index); + if (!!lastLoadedLocationMsg) { + // TODO: page-wise resources list // setListsStartTime(lastLoadedLocationMsg.time) + this.navigationStartOffset = lastLoadedLocationMsg.navigationStart - this.sessionStart; + } + const llEvent = this.locationEventManager.moveGetLast(t, index); + if (!!llEvent) { + if (llEvent.domContentLoadedTime != null) { + stateToUpdate.domContentLoadedTime = { + time: llEvent.domContentLoadedTime + this.navigationStartOffset, //TODO: predefined list of load event for the network tab (merge events & SetPageLocation: add navigationStart to db) + value: llEvent.domContentLoadedTime, + } + } + if (llEvent.loadTime != null) { + stateToUpdate.loadTime = { + time: llEvent.loadTime + this.navigationStartOffset, + value: llEvent.loadTime, + } + } + if (llEvent.domBuildingTime != null) { + stateToUpdate.domBuildingTime = llEvent.domBuildingTime; + } + } + /* === */ + const lastLocationMsg = this.locationManager.moveGetLast(t, index); + if (!!lastLocationMsg) { + stateToUpdate.location = lastLocationMsg.url; + } + const lastConnectionInfoMsg = this.connectionInfoManger.moveGetLast(t, index); + if (!!lastConnectionInfoMsg) { + stateToUpdate.connType = lastConnectionInfoMsg.type; + stateToUpdate.connBandwidth = lastConnectionInfoMsg.downlink; + } + const lastPerformanceTrackMessage = this.performanceTrackManager.moveGetLast(t, index); + if (!!lastPerformanceTrackMessage) { + stateToUpdate.performanceChartTime = lastPerformanceTrackMessage.time; + } + + this.lists.moveGetState(t) + Object.keys(stateToUpdate).length > 0 && this.state.update(stateToUpdate); + + /* Sequence of the managers is important here */ + // Preparing the size of "screen" + const lastResize = this.resizeManager.moveGetLast(t, index); + if (!!lastResize) { + this.setSize(lastResize) + } + this.pagesManager.moveReady(t).then(() => { + + const lastScroll = this.scrollManager.moveGetLast(t, index); + if (!!lastScroll && this.window) { + this.window.scrollTo(lastScroll.x, lastScroll.y); + } + // Moving mouse and setting :hover classes on ready view + this.mouseMoveManager.move(t); + const lastClick = this.clickManager.moveGetLast(t); + if (!!lastClick && t - lastClick.time < 600) { // happend during last 600ms + this.cursor.click(); + } + // After all changes - redraw the marker + //this.marker.redraw(); + }) + + if (this.waitingForFiles && this.lastMessageTime <= t) { + this.setMessagesLoading(true) + } + } + + private decodeStateMessage(msg: any, keys: Array) { + const decoded = {}; + try { + keys.forEach(key => { + // @ts-ignore TODO: types for decoder + decoded[key] = this.decoder.decode(msg[key]); + }); + } catch (e) { + logger.error("Error on message decoding: ", e, msg); + return null; + } + return { ...msg, ...decoded }; + } + + private readonly incomingMessages: Message[] = [] + appendMessage(msg: Message, index: number) { + // @ts-ignore + // msg.time = this.md.getLastRecordedMessageTime() + msg.time\ + //TODO: put index in message type + this.incomingMessages.push(msg) + if (!this.waitingForFiles) { + this.distributeMessage(msg, index) + } + } + + private distributeMessage(msg: Message, index: number): void { + const lastMessageTime = Math.max(msg.time, this.lastMessageTime) + this.lastMessageTime = lastMessageTime + this.state.update({ lastMessageTime }) + if (visualChanges.includes(msg.tp)) { + this.activityManager?.updateAcctivity(msg.time); + } + let decoded; + const time = msg.time; + switch (msg.tp) { + /* Lists: */ + case "console_log": + if (msg.level === 'debug') break; + this.lists.lists.log.append(Log({ + level: msg.level, + value: msg.value, + time, + index, + })) + break; + case "fetch": + this.lists.lists.fetch.append(Resource({ + method: msg.method, + url: msg.url, + payload: msg.request, + response: msg.response, + status: msg.status, + duration: msg.duration, + type: TYPES.FETCH, + time: msg.timestamp - this.sessionStart, //~ + index, + })); + break; + /* */ + case "set_page_location": + this.locationManager.append(msg); + if (msg.navigationStart > 0) { + this.loadedLocationManager.append(msg); + } + break; + case "set_viewport_size": + this.resizeManager.append(msg); + break; + case "mouse_move": + this.mouseMoveManager.append(msg); + break; + case "mouse_click": + this.clickManager.append(msg); + break; + case "set_viewport_scroll": + this.scrollManager.append(msg); + break; + case "performance_track": + this.performanceTrackManager.append(msg); + break; + case "set_page_visibility": + this.performanceTrackManager.handleVisibility(msg) + break; + case "connection_information": + this.connectionInfoManger.append(msg); + break; + case "o_table": + this.decoder.set(msg.key, msg.value); + break; + case "redux": + decoded = this.decodeStateMessage(msg, ["state", "action"]); + logger.log('redux', decoded) + if (decoded != null) { + this.lists.lists.redux.append(decoded); + } + break; + case "ng_rx": + decoded = this.decodeStateMessage(msg, ["state", "action"]); + logger.log('ngrx', decoded) + if (decoded != null) { + this.lists.lists.ngrx.append(decoded); + } + break; + case "vuex": + decoded = this.decodeStateMessage(msg, ["state", "mutation"]); + logger.log('vuex', decoded) + if (decoded != null) { + this.lists.lists.vuex.append(decoded); + } + break; + case "zustand": + decoded = this.decodeStateMessage(msg, ["state", "mutation"]) + logger.log('zustand', decoded) + if (decoded != null) { + this.lists.lists.zustand.append(decoded) + } + case "mob_x": + decoded = this.decodeStateMessage(msg, ["payload"]); + logger.log('mobx', decoded) + + if (decoded != null) { + this.lists.lists.mobx.append(decoded); + } + break; + case "graph_ql": + this.lists.lists.graphql.append(msg); + break; + case "profiler": + this.lists.lists.profiles.append(msg); + break; + default: + switch (msg.tp) { + case "create_document": + this.windowNodeCounter.reset(); + this.performanceTrackManager.setCurrentNodesCount(this.windowNodeCounter.count); + break; + case "create_text_node": + case "create_element_node": + this.windowNodeCounter.addNode(msg.id, msg.parentID); + this.performanceTrackManager.setCurrentNodesCount(this.windowNodeCounter.count); + break; + case "move_node": + this.windowNodeCounter.moveNode(msg.id, msg.parentID); + this.performanceTrackManager.setCurrentNodesCount(this.windowNodeCounter.count); + break; + case "remove_node": + this.windowNodeCounter.removeNode(msg.id); + this.performanceTrackManager.setCurrentNodesCount(this.windowNodeCounter.count); + break; + } + this.performanceTrackManager.addNodeCountPointIfNeed(msg.time) + this.pagesManager.appendMessage(msg); + break; + } + } + + getLastMessageTime(): number { + return this.lastMessageTime; + } + + getFirstMessageTime(): number { + return this.pagesManager.minTime; + } + + + setMessagesLoading(messagesLoading: boolean) { + this.display(!messagesLoading); + this.state.update({ messagesLoading }); + } + + setCSSLoading(cssLoading: boolean) { + this.displayFrame(!cssLoading); + this.state.update({ cssLoading }); + } + + private setSize({ height, width }: { height: number, width: number }) { + this.scale({ height, width }); + this.state.update({ width, height }); + + //this.updateMarketTargets() + } + + // TODO: clean managers? + clean() { + this.state.update(INITIAL_STATE); + this.incomingMessages.length = 0 + } + +} From 99be787a07c70b4346f8f7fb5a66b4f292506664 Mon Sep 17 00:00:00 2001 From: sylenien Date: Fri, 25 Nov 2022 11:56:41 +0100 Subject: [PATCH 039/252] refactor(ui/player): fix errors after merge --- .../components/Session_/OverviewPanel/OverviewPanel.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx index 81ed0648d..ef8598ddc 100644 --- a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx +++ b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx @@ -14,7 +14,7 @@ import { NoContent, Icon } from 'UI'; import { observer } from 'mobx-react-lite'; import { PlayerContext } from 'App/components/Session/playerContext'; -function OverviewPanel() { +function OverviewPanel({ issuesList }: { issuesList: Record[] }) { const { store } = React.useContext(PlayerContext) const [dataLoaded, setDataLoaded] = React.useState(false); const [selectedFeatures, setSelectedFeatures] = React.useState([ @@ -37,9 +37,9 @@ function OverviewPanel() { const fetchPresented = fetchList.length > 0; const resourceList = resourceListUnmap - .filter((r: any) => r.isRed() || r.isYellow()) - .concat(fetchList.filter((i: any) => parseInt(i.status) >= 400)) - .concat(graphqlList.filter((i: any) => parseInt(i.status) >= 400)) + .filter((r: any) => r.isRed() || r.isYellow()) + .concat(fetchList.filter((i: any) => parseInt(i.status) >= 400)) + .concat(graphqlList.filter((i: any) => parseInt(i.status) >= 400)) const resources: any = React.useMemo(() => { return { @@ -68,6 +68,7 @@ function OverviewPanel() { } }, [ resourceList, + issuesList, exceptionsList, eventsList, stackEventList, From c45f5e6d952b0b7c7da5ab7b5fd198a3c7274787 Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Fri, 25 Nov 2022 11:58:46 +0100 Subject: [PATCH 040/252] fix(player):type fixes & delete redundant --- frontend/app/player/_web/MessageManager.ts | 570 ------------------ frontend/app/player/web/MessageManager.ts | 7 +- frontend/app/player/web/WebPlayer.ts | 18 +- .../player/web/assist/ListWalkerWithMarks.ts | 42 -- 4 files changed, 12 insertions(+), 625 deletions(-) delete mode 100644 frontend/app/player/_web/MessageManager.ts delete mode 100644 frontend/app/player/web/assist/ListWalkerWithMarks.ts diff --git a/frontend/app/player/_web/MessageManager.ts b/frontend/app/player/_web/MessageManager.ts deleted file mode 100644 index 49105200c..000000000 --- a/frontend/app/player/_web/MessageManager.ts +++ /dev/null @@ -1,570 +0,0 @@ -// @ts-ignore -import { Decoder } from "syncod"; -import logger from 'App/logger'; - -import Resource, { TYPES } from 'Types/session/resource'; -import { TYPES as EVENT_TYPES } from 'Types/session/event'; -import Log from 'Types/session/log'; - -import { toast } from 'react-toastify'; - -import type { Store } from '../player/types'; -import ListWalker from '../_common/ListWalker'; - -import Screen from './Screen/Screen'; - -import PagesManager from './managers/PagesManager'; -import MouseMoveManager from './managers/MouseMoveManager'; - -import PerformanceTrackManager from './managers/PerformanceTrackManager'; -import WindowNodeCounter from './managers/WindowNodeCounter'; -import ActivityManager from './managers/ActivityManager'; - -import MFileReader from './messages/MFileReader'; -import { loadFiles, requestEFSDom, requestEFSDevtools } from './network/loadFiles'; -import { decryptSessionBytes } from './network/crypto'; - -import { INITIAL_STATE as SUPER_INITIAL_STATE, State as SuperState } from './Screen/Screen'; -import { INITIAL_STATE as ASSIST_INITIAL_STATE, State as AssistState } from './assist/AssistManager'; -import Lists, { INITIAL_STATE as LISTS_INITIAL_STATE } from './Lists'; - -import type { PerformanceChartPoint } from './managers/PerformanceTrackManager'; -import type { SkipInterval } from './managers/ActivityManager'; - - -export interface State extends SuperState, AssistState { - performanceChartData: PerformanceChartPoint[], - skipIntervals: SkipInterval[], - connType?: string, - connBandwidth?: number, - location?: string, - performanceChartTime?: number, - - domContentLoadedTime?: any, - domBuildingTime?: any, - loadTime?: any, - error: boolean, - devtoolsLoading: boolean, - - liveTimeTravel: boolean, - messagesLoading: boolean, - cssLoading: boolean, - - ready: boolean, - lastMessageTime: number, -} - -export const INITIAL_STATE: State = { - ...SUPER_INITIAL_STATE, - ...LISTS_INITIAL_STATE, - ...ASSIST_INITIAL_STATE, - performanceChartData: [], - skipIntervals: [], - error: false, - devtoolsLoading: false, - - liveTimeTravel: false, - messagesLoading: false, - cssLoading: false, - get ready() { - return !this.messagesLoading && !this.cssLoading - }, - lastMessageTime: 0, -}; - - -import type { - Message, - SetPageLocation, - ConnectionInformation, - SetViewportSize, - SetViewportScroll, - MouseClick, -} from './messages'; - -const visualChanges = [ - "mouse_move", - "mouse_click", - "create_element_node", - "set_input_value", - "set_input_checked", - "set_viewport_size", - "set_viewport_scroll", -] - -export default class MessageManager extends Screen { - // TODO: consistent with the other data-lists - private locationEventManager: ListWalker/**/ = new ListWalker(); - private locationManager: ListWalker = new ListWalker(); - private loadedLocationManager: ListWalker = new ListWalker(); - private connectionInfoManger: ListWalker = new ListWalker(); - private performanceTrackManager: PerformanceTrackManager = new PerformanceTrackManager(); - private windowNodeCounter: WindowNodeCounter = new WindowNodeCounter(); - private clickManager: ListWalker = new ListWalker(); - - private resizeManager: ListWalker = new ListWalker([]); - private pagesManager: PagesManager; - private mouseMoveManager: MouseMoveManager; - - private scrollManager: ListWalker = new ListWalker(); - - private readonly decoder = new Decoder(); - private readonly lists: Lists; - - private activityManager: ActivityManager | null = null; - - private sessionStart: number; - private navigationStartOffset: number = 0; - private lastMessageTime: number = 0; - private lastMessageInFileTime: number = 0; - - constructor( - private readonly session: any /*Session*/, - private readonly state: Store, - config: any, - live: boolean, - ) { - super(); - this.pagesManager = new PagesManager(this, this.session.isMobile) - this.mouseMoveManager = new MouseMoveManager(this); - - this.sessionStart = this.session.startedAt; - - if (live) { - this.lists = new Lists() - } else { - this.activityManager = new ActivityManager(this.session.duration.milliseconds); - /* == REFACTOR_ME == */ - const eventList = session.events.toJSON(); - // TODO: fix types for events, remove immutable js - eventList.forEach((e: Record) => { - if (e.type === EVENT_TYPES.LOCATION) { //TODO type system - this.locationEventManager.append(e); - } - }) - - this.lists = new Lists({ - event: eventList, - stack: session.stackEvents.toJSON(), - resource: session.resources.toJSON(), - exceptions: session.errors, - }) - - - /* === */ - this.loadMessages(); - } - } - - private parseAndDistributeMessages(fileReader: MFileReader, onMessage?: (msg: Message) => void) { - const msgs: Array = [] - let next: ReturnType - while (next = fileReader.next()) { - const [msg, index] = next - this.distributeMessage(msg, index) - msgs.push(msg) - onMessage?.(msg) - } - - logger.info("Messages count: ", msgs.length, msgs) - - - // @ts-ignore Hack for upet (TODO: fix ordering in one mutation in tracker(removes first)) - const headChildrenIds = msgs.filter(m => m.parentID === 1).map(m => m.id); - this.pagesManager.sortPages((m1, m2) => { - if (m1.time === m2.time) { - if (m1.tp === "remove_node" && m2.tp !== "remove_node") { - if (headChildrenIds.includes(m1.id)) { - return -1; - } - } else if (m2.tp === "remove_node" && m1.tp !== "remove_node") { - if (headChildrenIds.includes(m2.id)) { - return 1; - } - } else if (m2.tp === "remove_node" && m1.tp === "remove_node") { - const m1FromHead = headChildrenIds.includes(m1.id); - const m2FromHead = headChildrenIds.includes(m2.id); - if (m1FromHead && !m2FromHead) { - return -1; - } else if (m2FromHead && !m1FromHead) { - return 1; - } - } - } - return 0; - }) - } - - - private waitingForFiles: boolean = false - private onFileReadSuccess = () => { - const stateToUpdate = { - performanceChartData: this.performanceTrackManager.chartData, - performanceAvaliability: this.performanceTrackManager.avaliability, - ...this.lists.getFullListsState() - } - if (this.activityManager) { - this.activityManager.end() - stateToUpdate.skipIntervals = this.activityManager.list - } - - this.state.update(stateToUpdate) - } - private onFileReadFailed = (e: any) => { - logger.error(e) - this.state.update({ error: true }) - toast.error('Error requesting a session file') - } - private onFileReadFinally = () => { - this.incomingMessages - .filter(msg => msg.time >= this.lastMessageInFileTime) - .forEach(msg => this.distributeMessage(msg, 0)) - - this.waitingForFiles = false - this.setMessagesLoading(false) - } - - private loadMessages() { - // TODO: reuseable decryptor instance - const createNewParser = (shouldDecrypt=true) => { - const decrypt = shouldDecrypt && this.session.fileKey - ? (b: Uint8Array) => decryptSessionBytes(b, this.session.fileKey) - : (b: Uint8Array) => Promise.resolve(b) - // Each time called - new fileReader created - const fileReader = new MFileReader(new Uint8Array(), this.sessionStart) - return (b: Uint8Array) => decrypt(b).then(b => { - fileReader.append(b) - this.parseAndDistributeMessages(fileReader) - this.setMessagesLoading(false) - }) - } - this.setMessagesLoading(true) - this.waitingForFiles = true - - loadFiles(this.session.domURL, createNewParser()) - .catch(() => // do if only the first file missing (404) (?) - requestEFSDom(this.session.sessionId) - .then(createNewParser(false)) - // Fallback to back Compatability with mobsUrl - .catch(e => - loadFiles(this.session.mobsUrl, createNewParser(false)) - ) - ) - .then(this.onFileReadSuccess) - .catch(this.onFileReadFailed) - .finally(this.onFileReadFinally) - - // load devtools - if (this.session.devtoolsURL.length) { - this.state.update({ devtoolsLoading: true }) - loadFiles(this.session.devtoolsURL, createNewParser()) - .catch(() => - requestEFSDevtools(this.session.sessionId) - .then(createNewParser(false)) - ) - //.catch() // not able to download the devtools file - .finally(() => this.state.update({ devtoolsLoading: false })) - } - } - - reloadWithUnprocessedFile() { - const onData = (byteArray: Uint8Array) => { - const onMessage = (msg: Message) => { this.lastMessageInFileTime = msg.time } - this.parseAndDistributeMessages(new MFileReader(byteArray, this.sessionStart), onMessage) - } - const updateState = () => - this.state.update({ - liveTimeTravel: true, - }); - - // assist will pause and skip messages to prevent timestamp related errors - this.reloadMessageManagers() - this.windowNodeCounter.reset() - - this.setMessagesLoading(true) - this.waitingForFiles = true - - return requestEFSDom(this.session.sessionId) - .then(onData) - .then(updateState) - .then(this.onFileReadSuccess) - .catch(this.onFileReadFailed) - .finally(this.onFileReadFinally) - } - - private reloadMessageManagers() { - this.locationEventManager = new ListWalker(); - this.locationManager = new ListWalker(); - this.loadedLocationManager = new ListWalker(); - this.connectionInfoManger = new ListWalker(); - this.clickManager = new ListWalker(); - this.scrollManager = new ListWalker(); - this.resizeManager = new ListWalker([]); - - this.performanceTrackManager = new PerformanceTrackManager() - this.windowNodeCounter = new WindowNodeCounter(); - this.pagesManager = new PagesManager(this, this.session.isMobile) - this.mouseMoveManager = new MouseMoveManager(this); - this.activityManager = new ActivityManager(this.session.duration.milliseconds); - } - - move(t: number, index?: number): void { - const stateToUpdate: Partial = {}; - /* == REFACTOR_ME == */ - const lastLoadedLocationMsg = this.loadedLocationManager.moveGetLast(t, index); - if (!!lastLoadedLocationMsg) { - // TODO: page-wise resources list // setListsStartTime(lastLoadedLocationMsg.time) - this.navigationStartOffset = lastLoadedLocationMsg.navigationStart - this.sessionStart; - } - const llEvent = this.locationEventManager.moveGetLast(t, index); - if (!!llEvent) { - if (llEvent.domContentLoadedTime != null) { - stateToUpdate.domContentLoadedTime = { - time: llEvent.domContentLoadedTime + this.navigationStartOffset, //TODO: predefined list of load event for the network tab (merge events & SetPageLocation: add navigationStart to db) - value: llEvent.domContentLoadedTime, - } - } - if (llEvent.loadTime != null) { - stateToUpdate.loadTime = { - time: llEvent.loadTime + this.navigationStartOffset, - value: llEvent.loadTime, - } - } - if (llEvent.domBuildingTime != null) { - stateToUpdate.domBuildingTime = llEvent.domBuildingTime; - } - } - /* === */ - const lastLocationMsg = this.locationManager.moveGetLast(t, index); - if (!!lastLocationMsg) { - stateToUpdate.location = lastLocationMsg.url; - } - const lastConnectionInfoMsg = this.connectionInfoManger.moveGetLast(t, index); - if (!!lastConnectionInfoMsg) { - stateToUpdate.connType = lastConnectionInfoMsg.type; - stateToUpdate.connBandwidth = lastConnectionInfoMsg.downlink; - } - const lastPerformanceTrackMessage = this.performanceTrackManager.moveGetLast(t, index); - if (!!lastPerformanceTrackMessage) { - stateToUpdate.performanceChartTime = lastPerformanceTrackMessage.time; - } - - this.lists.moveGetState(t) - Object.keys(stateToUpdate).length > 0 && this.state.update(stateToUpdate); - - /* Sequence of the managers is important here */ - // Preparing the size of "screen" - const lastResize = this.resizeManager.moveGetLast(t, index); - if (!!lastResize) { - this.setSize(lastResize) - } - this.pagesManager.moveReady(t).then(() => { - - const lastScroll = this.scrollManager.moveGetLast(t, index); - if (!!lastScroll && this.window) { - this.window.scrollTo(lastScroll.x, lastScroll.y); - } - // Moving mouse and setting :hover classes on ready view - this.mouseMoveManager.move(t); - const lastClick = this.clickManager.moveGetLast(t); - if (!!lastClick && t - lastClick.time < 600) { // happend during last 600ms - this.cursor.click(); - } - // After all changes - redraw the marker - //this.marker.redraw(); - }) - - if (this.waitingForFiles && this.lastMessageTime <= t) { - this.setMessagesLoading(true) - } - } - - private decodeStateMessage(msg: any, keys: Array) { - const decoded = {}; - try { - keys.forEach(key => { - // @ts-ignore TODO: types for decoder - decoded[key] = this.decoder.decode(msg[key]); - }); - } catch (e) { - logger.error("Error on message decoding: ", e, msg); - return null; - } - return { ...msg, ...decoded }; - } - - private readonly incomingMessages: Message[] = [] - appendMessage(msg: Message, index: number) { - // @ts-ignore - // msg.time = this.md.getLastRecordedMessageTime() + msg.time\ - //TODO: put index in message type - this.incomingMessages.push(msg) - if (!this.waitingForFiles) { - this.distributeMessage(msg, index) - } - } - - private distributeMessage(msg: Message, index: number): void { - const lastMessageTime = Math.max(msg.time, this.lastMessageTime) - this.lastMessageTime = lastMessageTime - this.state.update({ lastMessageTime }) - if (visualChanges.includes(msg.tp)) { - this.activityManager?.updateAcctivity(msg.time); - } - let decoded; - const time = msg.time; - switch (msg.tp) { - /* Lists: */ - case "console_log": - if (msg.level === 'debug') break; - this.lists.lists.log.append(Log({ - level: msg.level, - value: msg.value, - time, - index, - })) - break; - case "fetch": - this.lists.lists.fetch.append(Resource({ - method: msg.method, - url: msg.url, - payload: msg.request, - response: msg.response, - status: msg.status, - duration: msg.duration, - type: TYPES.FETCH, - time: msg.timestamp - this.sessionStart, //~ - index, - })); - break; - /* */ - case "set_page_location": - this.locationManager.append(msg); - if (msg.navigationStart > 0) { - this.loadedLocationManager.append(msg); - } - break; - case "set_viewport_size": - this.resizeManager.append(msg); - break; - case "mouse_move": - this.mouseMoveManager.append(msg); - break; - case "mouse_click": - this.clickManager.append(msg); - break; - case "set_viewport_scroll": - this.scrollManager.append(msg); - break; - case "performance_track": - this.performanceTrackManager.append(msg); - break; - case "set_page_visibility": - this.performanceTrackManager.handleVisibility(msg) - break; - case "connection_information": - this.connectionInfoManger.append(msg); - break; - case "o_table": - this.decoder.set(msg.key, msg.value); - break; - case "redux": - decoded = this.decodeStateMessage(msg, ["state", "action"]); - logger.log('redux', decoded) - if (decoded != null) { - this.lists.lists.redux.append(decoded); - } - break; - case "ng_rx": - decoded = this.decodeStateMessage(msg, ["state", "action"]); - logger.log('ngrx', decoded) - if (decoded != null) { - this.lists.lists.ngrx.append(decoded); - } - break; - case "vuex": - decoded = this.decodeStateMessage(msg, ["state", "mutation"]); - logger.log('vuex', decoded) - if (decoded != null) { - this.lists.lists.vuex.append(decoded); - } - break; - case "zustand": - decoded = this.decodeStateMessage(msg, ["state", "mutation"]) - logger.log('zustand', decoded) - if (decoded != null) { - this.lists.lists.zustand.append(decoded) - } - case "mob_x": - decoded = this.decodeStateMessage(msg, ["payload"]); - logger.log('mobx', decoded) - - if (decoded != null) { - this.lists.lists.mobx.append(decoded); - } - break; - case "graph_ql": - this.lists.lists.graphql.append(msg); - break; - case "profiler": - this.lists.lists.profiles.append(msg); - break; - default: - switch (msg.tp) { - case "create_document": - this.windowNodeCounter.reset(); - this.performanceTrackManager.setCurrentNodesCount(this.windowNodeCounter.count); - break; - case "create_text_node": - case "create_element_node": - this.windowNodeCounter.addNode(msg.id, msg.parentID); - this.performanceTrackManager.setCurrentNodesCount(this.windowNodeCounter.count); - break; - case "move_node": - this.windowNodeCounter.moveNode(msg.id, msg.parentID); - this.performanceTrackManager.setCurrentNodesCount(this.windowNodeCounter.count); - break; - case "remove_node": - this.windowNodeCounter.removeNode(msg.id); - this.performanceTrackManager.setCurrentNodesCount(this.windowNodeCounter.count); - break; - } - this.performanceTrackManager.addNodeCountPointIfNeed(msg.time) - this.pagesManager.appendMessage(msg); - break; - } - } - - getLastMessageTime(): number { - return this.lastMessageTime; - } - - getFirstMessageTime(): number { - return this.pagesManager.minTime; - } - - - setMessagesLoading(messagesLoading: boolean) { - this.display(!messagesLoading); - this.state.update({ messagesLoading }); - } - - setCSSLoading(cssLoading: boolean) { - this.displayFrame(!cssLoading); - this.state.update({ cssLoading }); - } - - private setSize({ height, width }: { height: number, width: number }) { - this.scale({ height, width }); - this.state.update({ width, height }); - - //this.updateMarketTargets() - } - - // TODO: clean managers? - clean() { - this.state.update(INITIAL_STATE); - this.incomingMessages.length = 0 - } - -} diff --git a/frontend/app/player/web/MessageManager.ts b/frontend/app/player/web/MessageManager.ts index 92c67fd7b..19df79c24 100644 --- a/frontend/app/player/web/MessageManager.ts +++ b/frontend/app/player/web/MessageManager.ts @@ -41,6 +41,7 @@ export interface State extends ScreenState, ListsState { connBandwidth?: number, location?: string, performanceChartTime?: number, + performanceAvaliability?: PerformanceTrackManager['avaliability'] domContentLoadedTime?: any, domBuildingTime?: any, @@ -279,7 +280,7 @@ export default class MessageManager { this.connectionInfoManger = new ListWalker(); this.clickManager = new ListWalker(); this.scrollManager = new ListWalker(); - this.resizeManager = new ListWalker([]); + this.resizeManager = new ListWalker(); this.performanceTrackManager = new PerformanceTrackManager() this.windowNodeCounter = new WindowNodeCounter(); @@ -350,8 +351,6 @@ export default class MessageManager { if (!!lastClick && t - lastClick.time < 600) { // happend during last 600ms this.screen.cursor.click(); } - // After all changes - redraw the marker - //this.marker.redraw(); }) if (this.waitingForFiles && this.lastMessageTime <= t) { @@ -375,8 +374,6 @@ export default class MessageManager { private readonly incomingMessages: Message[] = [] appendMessage(msg: Message, index: number) { - // @ts-ignore - // msg.time = this.md.getLastRecordedMessageTime() + msg.time\ //TODO: put index in message type this.incomingMessages.push(msg) if (!this.waitingForFiles) { diff --git a/frontend/app/player/web/WebPlayer.ts b/frontend/app/player/web/WebPlayer.ts index 4467bdef5..6c00b6c65 100644 --- a/frontend/app/player/web/WebPlayer.ts +++ b/frontend/app/player/web/WebPlayer.ts @@ -58,7 +58,7 @@ export default class WebPlayer extends Player { live, livePlay: live, - endTime, // : 0, //TODO: through initialState + endTime, // : 0, }) // TODO: separate LiveWebPlayer @@ -85,22 +85,24 @@ export default class WebPlayer extends Player { mark(e: Element) { this.inspectorController.marker?.mark(e) } - toggleInspectorMode(flag: boolean, clickCallback?: (args: any) => any) { + + toggleInspectorMode(flag: boolean, clickCallback?: Parameters[0]) { if (typeof flag !== 'boolean') { const { inspectorMode } = this.wpState.get() - flag = !inspectorMode; + flag = !inspectorMode } if (flag) { this.pause() this.wpState.update({ inspectorMode: true }) - return this.inspectorController.enableInspector(clickCallback); + return this.inspectorController.enableInspector(clickCallback) } else { - this.inspectorController.disableInspector(); - this.wpState.update({ inspectorMode: false }); + this.inspectorController.disableInspector() + this.wpState.update({ inspectorMode: false }) } } + // Target Marker setActiveTarget(args: Parameters) { this.targetMarker.setActiveTarget(...args) } @@ -111,10 +113,10 @@ export default class WebPlayer extends Player { } - // TODO + // TODO separate message receivers async toggleTimetravel() { if (!this.wpState.get().liveTimeTravel) { - return await this.messageManager.reloadWithUnprocessedFile(() => + await this.messageManager.reloadWithUnprocessedFile(() => this.wpState.update({ liveTimeTravel: true, }) diff --git a/frontend/app/player/web/assist/ListWalkerWithMarks.ts b/frontend/app/player/web/assist/ListWalkerWithMarks.ts deleted file mode 100644 index 43c05382b..000000000 --- a/frontend/app/player/web/assist/ListWalkerWithMarks.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { Timed } from './types'; -import ListWalker from './ListWalker' - - -type CheckFn = (t: T) => boolean - - -export default class ListWalkerWithMarks extends ListWalker { - private _markCountNow: number = 0 - private _markCount: number = 0 - constructor(private isMarked: CheckFn, initialList: T[] = []) { - super(initialList) - this._markCount = initialList.reduce((n, item) => isMarked(item) ? n+1 : n, 0) - } - - append(item: T) { - if (this.isMarked(item)) { this._markCount++ } - super.append(item) - } - - protected moveNext() { - const val = super.moveNext() - if (val && this.isMarked(val)) { - this._markCountNow++ - } - return val - } - protected movePrev() { - const val = super.movePrev() - if (val && this.isMarked(val)) { - this._markCountNow-- - } - return val - } - get markedCountNow(): number { - return this._markCountNow - } - get markedCount(): number { - return this._markCount - } - -} From 37e7f280d9a058ccd614587e293251f3aeabe2bf Mon Sep 17 00:00:00 2001 From: MauricioGarciaS <47052044+MauricioGarciaS@users.noreply.github.com> Date: Fri, 25 Nov 2022 12:22:56 +0100 Subject: [PATCH 041/252] Added signals queue to cron jobs by default --- ee/api/routers/crons/core_dynamic_crons.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ee/api/routers/crons/core_dynamic_crons.py b/ee/api/routers/crons/core_dynamic_crons.py index f57b341bb..88d591269 100644 --- a/ee/api/routers/crons/core_dynamic_crons.py +++ b/ee/api/routers/crons/core_dynamic_crons.py @@ -27,14 +27,15 @@ def pg_events_queue() -> None: events_queue.global_queue.force_flush() cron_jobs = [ - {"func": unlock_cron, "trigger": "cron", "hour": "*"} + {"func": unlock_cron, "trigger": "cron", "hour": "*"}, + {"func": pg_events_queue, "trigger": "interval", "seconds": 60*5, "misfire_grace_time": 20} ] SINGLE_CRONS = [{"func": telemetry_cron, "trigger": "cron", "day_of_week": "*"}, {"func": run_scheduled_jobs, "trigger": "interval", "seconds": 60, "misfire_grace_time": 20}, {"func": weekly_report, "trigger": "cron", "day_of_week": "mon", "hour": 5, - "misfire_grace_time": 60 * 60}, - {"func": pg_events_queue, "trigger": "cron", "interval": 60*5, "misfire_grace_time": 20}] + "misfire_grace_time": 60 * 60} +] if config("LOCAL_CRONS", default=False, cast=bool): cron_jobs += SINGLE_CRONS From 13b602279fc4e32fe7dfe8d1b68314eefb640cd6 Mon Sep 17 00:00:00 2001 From: MauricioGarciaS <47052044+MauricioGarciaS@users.noreply.github.com> Date: Fri, 25 Nov 2022 12:24:52 +0100 Subject: [PATCH 042/252] Changed default queue size of signals to 100 --- ee/api/chalicelib/utils/events_queue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ee/api/chalicelib/utils/events_queue.py b/ee/api/chalicelib/utils/events_queue.py index c6208bc79..1cf344061 100644 --- a/ee/api/chalicelib/utils/events_queue.py +++ b/ee/api/chalicelib/utils/events_queue.py @@ -7,7 +7,7 @@ global_queue = None class EventQueue(): - def __init__(self, test=False, queue_max_length=5): + def __init__(self, test=False, queue_max_length=100): self.events = queue.Queue() self.events.maxsize = queue_max_length self.test = test From 4af4824059405458b9cf72886900a5a58b99b422 Mon Sep 17 00:00:00 2001 From: sylenien Date: Fri, 25 Nov 2022 12:40:32 +0100 Subject: [PATCH 043/252] refactor(ui/player): fix errors after merge --- .../Session_/Player/Controls/Controls.tsx | 18 +++++----- .../Controls/components/PlayerControls.tsx | 34 ++++++++++++------- frontend/package.json | 1 - frontend/webpack.config.ts | 2 +- 4 files changed, 31 insertions(+), 24 deletions(-) diff --git a/frontend/app/components/Session_/Player/Controls/Controls.tsx b/frontend/app/components/Session_/Player/Controls/Controls.tsx index 4e39c72c3..c6f8a7519 100644 --- a/frontend/app/components/Session_/Player/Controls/Controls.tsx +++ b/frontend/app/components/Session_/Player/Controls/Controls.tsx @@ -95,19 +95,19 @@ function Controls(props: any) { skipInterval, disabledRedux, showStorageRedux, - showStackRedux, + // showStackRedux, } = props; const storageType = selectStorageType(store.get()); const disabled = disabledRedux || cssLoading || messagesLoading || inspectorMode || markedTargets; - const stackCount = stackList.length; const profilesCount = profilesList.length; const graphqlCount = graphqlList.length; const showGraphql = graphqlCount > 0; - const fetchCount = fetchList.length; const showProfiler = profilesCount > 0; const showExceptions = exceptionsList.length > 0; const showStorage = storageType !== STORAGE_TYPES.NONE || showStorageRedux; + // const fetchCount = fetchList.length; + // const stackCount = stackList.length; // const showStack = stackCount > 0 || showStackRedux UPDATE // const showFetch = fetchCount > 0 UPDATE @@ -130,17 +130,17 @@ function Controls(props: any) { backTenSeconds(); } if (e.key === 'ArrowDown') { - props.speedDown(); + player.speedDown(); } if (e.key === 'ArrowUp') { - props.speedUp(); + player.speedUp(); } }; React.useEffect(() => { - document.addEventListener('keydown', onKeyDown); + document.addEventListener('keydown', onKeyDown.bind(this)); return () => { - document.removeEventListener('keydown', onKeyDown); + document.removeEventListener('keydown', onKeyDown.bind(this)); }; }, []); @@ -204,10 +204,10 @@ function Controls(props: any) { const toggleBottomTools = (blockName: number) => { if (blockName === INSPECTOR) { - toggleInspectorMode(false); + player.toggleInspectorMode(false); bottomBlock && toggleBottomBlock(); } else { - toggleInspectorMode(false); + player.toggleInspectorMode(false); toggleBottomBlock(blockName); } }; diff --git a/frontend/app/components/Session_/Player/Controls/components/PlayerControls.tsx b/frontend/app/components/Session_/Player/Controls/components/PlayerControls.tsx index d152fad0f..918fdb8b6 100644 --- a/frontend/app/components/Session_/Player/Controls/components/PlayerControls.tsx +++ b/frontend/app/components/Session_/Player/Controls/components/PlayerControls.tsx @@ -94,11 +94,15 @@ function PlayerControls(props: Props) {
{/* @ts-ignore */} - + +
- {/* @ts-ignore */} + {/* @ts-ignore */} {currentInterval}s
- + +
{!live && ( diff --git a/frontend/package.json b/frontend/package.json index c4f0a68de..d036ac7bb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -98,7 +98,6 @@ "babel-loader": "^8.2.4", "babel-plugin-recharts": "^1.2.1", "babel-plugin-transform-decorators-legacy": "^1.3.5", - "circular-dependency-plugin": "^5.2.0", "compression-webpack-plugin": "^10.0.0", "copy-webpack-plugin": "^11.0.0", "country-data": "0.0.31", diff --git a/frontend/webpack.config.ts b/frontend/webpack.config.ts index 404531b55..e2655b67e 100644 --- a/frontend/webpack.config.ts +++ b/frontend/webpack.config.ts @@ -123,7 +123,7 @@ const config: Configuration = { { from: "./app/assets", to: "assets" }, ], }), - new MiniCssExtractPlugin(), + new MiniCssExtractPlugin({ ignoreOrder: true }), ], devtool: isDevelopment ? "inline-source-map" : false, performance: { From e8edb34b9b4a160a09b1628fb0a8e0b4dbd5ae20 Mon Sep 17 00:00:00 2001 From: MauricioGarciaS <47052044+MauricioGarciaS@users.noreply.github.com> Date: Fri, 25 Nov 2022 12:53:36 +0100 Subject: [PATCH 044/252] Fix insert for json format --- ee/api/chalicelib/utils/events_queue.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ee/api/chalicelib/utils/events_queue.py b/ee/api/chalicelib/utils/events_queue.py index 1cf344061..8c44a1690 100644 --- a/ee/api/chalicelib/utils/events_queue.py +++ b/ee/api/chalicelib/utils/events_queue.py @@ -1,3 +1,4 @@ +import json import queue import logging @@ -17,7 +18,7 @@ class EventQueue(): while not self.events.empty(): project_id, user_id, element = self.events.get() events.append("({project_id}, '{user_id}', {timestamp}, '{action}', '{source}', '{category}', '{data}')".format( - project_id=project_id, user_id=user_id, timestamp=element.timestamp, action=element.action, source=element.source, category=element.category, data=element.data)) + project_id=project_id, user_id=user_id, timestamp=element.timestamp, action=element.action, source=element.source, category=element.category, data=json.dumps(element.data))) if len(events)==0: return 0 if self.test: From f9e837c0c879796d29efc20c6e5fd9b5d975a144 Mon Sep 17 00:00:00 2001 From: sylenien Date: Fri, 25 Nov 2022 15:01:46 +0100 Subject: [PATCH 045/252] refactor(ui/player): fix console rerender --- .../components/Session_/Autoplay/Autoplay.js | 5 +- .../Session_/Player/Controls/Controls.tsx | 8 +- .../app/components/Session_/Player/Player.js | 7 +- .../app/components/Session_/PlayerBlock.js | 5 +- .../DevTools/ConsolePanel/ConsolePanel.tsx | 76 ++++++++++--------- frontend/app/utils.ts | 33 ++++++++ 6 files changed, 86 insertions(+), 48 deletions(-) diff --git a/frontend/app/components/Session_/Autoplay/Autoplay.js b/frontend/app/components/Session_/Autoplay/Autoplay.js index 5510996ef..8b094af41 100644 --- a/frontend/app/components/Session_/Autoplay/Autoplay.js +++ b/frontend/app/components/Session_/Autoplay/Autoplay.js @@ -12,7 +12,6 @@ function Autoplay(props) { const { player, store } = React.useContext(PlayerContext) const { autoplay } = store.get() - const { toggleAutoplay } = player useEffect(() => { props.setAutoplayValues(); @@ -21,10 +20,10 @@ function Autoplay(props) { return (
player.toggleAutoplay()} className="cursor-pointer flex items-center mr-2 hover:bg-gray-light-shade rounded-md p-2" > - + player.toggleAutoplay()} checked={autoplay} /> Auto-Play
diff --git a/frontend/app/components/Session_/Player/Controls/Controls.tsx b/frontend/app/components/Session_/Player/Controls/Controls.tsx index c6f8a7519..d88f8eeeb 100644 --- a/frontend/app/components/Session_/Player/Controls/Controls.tsx +++ b/frontend/app/components/Session_/Player/Controls/Controls.tsx @@ -75,11 +75,11 @@ function Controls(props: any) { inspectorMode, markedTargets, // messagesLoading: fullscreenDisabled, UPDATE - stackList, + // stackList, exceptionsList, profilesList, graphqlList, - fetchList, + // fetchList, liveTimeTravel, logMarkedCountNow: logRedCount, resourceMarkedCountNow: resourceRedCount, @@ -204,10 +204,10 @@ function Controls(props: any) { const toggleBottomTools = (blockName: number) => { if (blockName === INSPECTOR) { - player.toggleInspectorMode(false); + // player.toggleInspectorMode(false); bottomBlock && toggleBottomBlock(); } else { - player.toggleInspectorMode(false); + // player.toggleInspectorMode(false); toggleBottomBlock(blockName); } }; diff --git a/frontend/app/components/Session_/Player/Player.js b/frontend/app/components/Session_/Player/Player.js index b283b669a..df6957b6c 100644 --- a/frontend/app/components/Session_/Player/Player.js +++ b/frontend/app/components/Session_/Player/Player.js @@ -20,7 +20,7 @@ import { OVERVIEW, } from 'Duck/components/player'; import NetworkPanel from 'Shared/DevTools/NetworkPanel'; -import StackEvents from '../StackEvents/StackEvents'; +// import StackEvents from '../StackEvents/StackEvents'; import Storage from '../Storage'; import { ConnectedPerformance } from '../Performance'; import GraphQL from '../GraphQL'; @@ -40,7 +40,6 @@ import StackEventPanel from 'Shared/DevTools/StackEventPanel'; function Player(props) { const { className, - bottomBlockIsActive, fullscreen, fullscreenOff, nextId, @@ -51,6 +50,7 @@ function Player(props) { } = props; const playerContext = React.useContext(PlayerContext) const screenWrapper = React.useRef(); + const bottomBlockIsActive = !fullscreen && bottomBlock !== NONE React.useEffect(() => { props.updateLastPlayedSession(props.sessionId); @@ -67,6 +67,8 @@ function Player(props) { if (!playerContext.player) return null; + console.log(bottomBlock) + const maxWidth = activeTab ? 'calc(100vw - 270px)' : '100vw'; return (
{ fullscreen: state.getIn(['components', 'player', 'fullscreen']), nextId: state.getIn(['sessions', 'nextId']), sessionId: state.getIn(['sessions', 'current', 'sessionId']), + bottomBlock: state.getIn(['components', 'player', 'bottomBlock']), closedLive: !!state.getIn(['sessions', 'errors']) || (isAssist && !state.getIn(['sessions', 'current', 'live'])), diff --git a/frontend/app/components/Session_/PlayerBlock.js b/frontend/app/components/Session_/PlayerBlock.js index 54130adf5..d14d8b983 100644 --- a/frontend/app/components/Session_/PlayerBlock.js +++ b/frontend/app/components/Session_/PlayerBlock.js @@ -9,14 +9,13 @@ import styles from './playerBlock.module.css'; @connect((state) => ({ fullscreen: state.getIn(['components', 'player', 'fullscreen']), - bottomBlock: state.getIn(['components', 'player', 'bottomBlock']), sessionId: state.getIn(['sessions', 'current', 'sessionId']), disabled: state.getIn(['components', 'targetDefiner', 'inspectorMode']), jiraConfig: state.getIn(['issues', 'list']).first(), })) export default class PlayerBlock extends React.PureComponent { render() { - const { fullscreen, bottomBlock, sessionId, disabled, activeTab, jiraConfig, fullView = false } = this.props; + const { fullscreen, sessionId, disabled, activeTab, jiraConfig, fullView = false } = this.props; return (
@@ -25,9 +24,7 @@ export default class PlayerBlock extends React.PureComponent { )} ({ text: tab, key: tab })); +let throttledCall = () => 999 function renderWithNL(s = '') { if (typeof s !== 'string') return ''; @@ -64,11 +66,24 @@ const TIMEOUT_DURATION = 5000; function ConsolePanel() { const { player, store } = React.useContext(PlayerContext) + const additionalHeight = 0; + const { + sessionStore: { devTools }, + } = useStore(); + + const filter = devTools[INDEX_KEY].filter; + const activeTab = devTools[INDEX_KEY].activeTab; + const activeIndex = devTools[INDEX_KEY].index; + const [isDetailsModalActive, setIsDetailsModalActive] = useState(false); + const [filteredList, setFilteredList] = useState([]); + const [pauseSync, setPauseSync] = useState(activeIndex > 0); + const synRef: any = useRef({}); + const { showModal } = useModal(); const jump = (t: number) => player.jump(t) const { logList, exceptionsList, time } = store.get() - const logExceptions = exceptionsList.map(({ time, errorId, name, projectId }: any) => + const logExceptions = exceptionsList.map(({ time, errorId, name }: any) => Log({ level: LEVEL.ERROR, value: name, @@ -78,19 +93,6 @@ function ConsolePanel() { ); // @ts-ignore const logs = logList.concat(logExceptions) - const additionalHeight = 0; - const { - sessionStore: { devTools }, - } = useStore(); - - const [isDetailsModalActive, setIsDetailsModalActive] = useState(false); - const [filteredList, setFilteredList] = useState([]); - const filter = useObserver(() => devTools[INDEX_KEY].filter); - const activeTab = useObserver(() => devTools[INDEX_KEY].activeTab); - const activeIndex = useObserver(() => devTools[INDEX_KEY].index); - const [pauseSync, setPauseSync] = useState(activeIndex > 0); - const synRef: any = useRef({}); - const { showModal } = useModal(); const onTabClick = (activeTab: any) => devTools.update(INDEX_KEY, { activeTab }); const onFilterChange = ({ target: { value } }: any) => { @@ -116,30 +118,10 @@ function ConsolePanel() { removePause(); }; - useEffect(() => { - if (pauseSync) { - removePause(); - } - - return () => { - clearTimeout(timeOut); - if (!synRef.current.pauseSync) { - devTools.update(INDEX_KEY, { index: 0 }); - } - }; - }, []); - const getCurrentIndex = () => { return filteredList.filter((item: any) => item.time <= time).length - 1; }; - useEffect(() => { - const currentIndex = getCurrentIndex(); - if (currentIndex !== activeIndex && !pauseSync) { - devTools.update(INDEX_KEY, { index: currentIndex }); - } - }, [time]); - const cache = new CellMeasurerCache({ fixedWidth: true, keyMapper: (index: number) => filteredList[index], @@ -177,6 +159,27 @@ function ConsolePanel() { ); }; + useEffect(() => { + if (pauseSync) { + removePause(); + } + throttledCall = throttle(getCurrentIndex, 500, undefined) + return () => { + clearTimeout(timeOut); + if (!synRef.current.pauseSync) { + devTools.update(INDEX_KEY, { index: 0 }); + } + }; + }, []); + + + useEffect(() => { + const currentIndex = throttledCall() + if (currentIndex !== activeIndex && !pauseSync) { + devTools.update(INDEX_KEY, { index: currentIndex }); + } + }, [time]); + React.useMemo(() => { const filterRE = getRE(filter, 'i'); let list = logs; @@ -186,16 +189,19 @@ function ConsolePanel() { (!!filter ? filterRE.test(value) : true) && (activeTab === ALL || activeTab === LEVEL_TAB[level]) ); + console.log('log filter tab', logs.length, filter, activeTab) setFilteredList(list); - }, [logs, filter, activeTab]); + }, [logs.length, filter, activeTab]); useEffect(() => { if (_list.current) { + console.log('active index') // @ts-ignore _list.current.scrollToRow(activeIndex); } }, [activeIndex]); + console.log('rerender') return ( wait) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + previous = now; + result = func.apply(context, args); + if (!timeout) context = args = null; + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }; From 24d9f758645f8babe87389e8e27effbc240f9f78 Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Fri, 25 Nov 2022 15:27:19 +0100 Subject: [PATCH 046/252] fix(player): fix lists init & bind web-player api (by being making it arrow function) --- frontend/app/player/web/WebPlayer.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/frontend/app/player/web/WebPlayer.ts b/frontend/app/player/web/WebPlayer.ts index 6c00b6c65..5ae1347db 100644 --- a/frontend/app/player/web/WebPlayer.ts +++ b/frontend/app/player/web/WebPlayer.ts @@ -32,12 +32,12 @@ export default class WebPlayer extends Player { constructor(private wpState: Store, session, config: RTCIceServer[], live: boolean) { - let initialLists = live ? { + let initialLists = live ? {} :{ event: session.events.toJSON(), stack: session.stackEvents.toJSON(), resource: session.resources.toJSON(), exceptions: session.errors, - } : {} + } const screen = new Screen() const messageManager = new MessageManager(session, wpState, screen, initialLists) @@ -68,7 +68,7 @@ export default class WebPlayer extends Player { } } - attach(parent: HTMLElement) { + attach = (parent: HTMLElement) => { this.screen.attach(parent) window.addEventListener('resize', this.scale) this.scale() @@ -86,7 +86,7 @@ export default class WebPlayer extends Player { this.inspectorController.marker?.mark(e) } - toggleInspectorMode(flag: boolean, clickCallback?: Parameters[0]) { + toggleInspectorMode = (flag: boolean, clickCallback?: Parameters[0]) => { if (typeof flag !== 'boolean') { const { inspectorMode } = this.wpState.get() flag = !inspectorMode @@ -103,18 +103,18 @@ export default class WebPlayer extends Player { } // Target Marker - setActiveTarget(args: Parameters) { + setActiveTarget = (args: Parameters) => { this.targetMarker.setActiveTarget(...args) } - markTargets(args: Parameters) { + markTargets = (args: Parameters) => { this.pause() this.targetMarker.markTargets(...args) } // TODO separate message receivers - async toggleTimetravel() { + toggleTimetravel = async () => { if (!this.wpState.get().liveTimeTravel) { await this.messageManager.reloadWithUnprocessedFile(() => this.wpState.update({ @@ -133,11 +133,11 @@ export default class WebPlayer extends Player { // update({ notes: notes.filter((note: Note) => note.noteId !== noteId) }) // } - toggleUserName(name?: string) { + toggleUserName = (name?: string) => { this.screen.cursor.showTag(name) } - clean() { + clean = () => { super.clean() this.assistManager.clean() window.removeEventListener('resize', this.scale) From dd8f666ca57eec9d800a19c34d04790c2e6a87df Mon Sep 17 00:00:00 2001 From: sylenien Date: Fri, 25 Nov 2022 16:05:08 +0100 Subject: [PATCH 047/252] fix(ui): remove immutable log --- .../shared/DevTools/ConsolePanel/ConsolePanel.tsx | 5 ++--- .../shared/DevTools/ConsoleRow/ConsoleRow.tsx | 3 ++- frontend/app/player/web/MessageManager.ts | 2 +- frontend/app/types/session/{log.js => log.tsx} | 14 +++++++++++++- 4 files changed, 18 insertions(+), 6 deletions(-) rename frontend/app/types/session/{log.js => log.tsx} (66%) diff --git a/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx b/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx index a39c16f28..023b7c5c4 100644 --- a/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx +++ b/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useRef, useState } from 'react'; -import Log from 'Types/session/log'; +import { Log, LEVEL } from 'Types/session/log'; import BottomBlock from '../BottomBlock'; -import { LEVEL } from 'Types/session/log'; import { Tabs, Input, Icon, NoContent } from 'UI'; import cn from 'classnames'; import ConsoleRow from '../ConsoleRow'; @@ -9,7 +8,6 @@ import { getRE } from 'App/utils'; import { PlayerContext } from 'App/components/Session/playerContext'; import { observer } from 'mobx-react-lite'; import { List, CellMeasurer, CellMeasurerCache, AutoSizer } from 'react-virtualized'; -import { useObserver } from 'mobx-react-lite'; import { useStore } from 'App/mstore'; import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorDetailsModal'; import { useModal } from 'App/components/Modal'; @@ -138,6 +136,7 @@ function ConsolePanel() { const _rowRenderer = ({ index, key, parent, style }: any) => { const item = filteredList[index]; + console.log({ ...filteredList }, { ...item }) return ( // @ts-ignore diff --git a/frontend/app/components/shared/DevTools/ConsoleRow/ConsoleRow.tsx b/frontend/app/components/shared/DevTools/ConsoleRow/ConsoleRow.tsx index 83929cbed..9e4abb3fb 100644 --- a/frontend/app/components/shared/DevTools/ConsoleRow/ConsoleRow.tsx +++ b/frontend/app/components/shared/DevTools/ConsoleRow/ConsoleRow.tsx @@ -15,7 +15,8 @@ interface Props { function ConsoleRow(props: Props) { const { log, iconProps, jump, renderWithNL, style, recalcHeight } = props; const [expanded, setExpanded] = useState(false); - const lines = log.value.split('\n').filter((l: any) => !!l); + console.log(log) + const lines = log.value?.split('\n').filter((l: any) => !!l) || []; const canExpand = lines.length > 1; const clickable = canExpand || !!log.errorId; diff --git a/frontend/app/player/web/MessageManager.ts b/frontend/app/player/web/MessageManager.ts index 19df79c24..bca4dc34a 100644 --- a/frontend/app/player/web/MessageManager.ts +++ b/frontend/app/player/web/MessageManager.ts @@ -4,7 +4,7 @@ import logger from 'App/logger'; import Resource, { TYPES } from 'Types/session/resource'; import { TYPES as EVENT_TYPES } from 'Types/session/event'; -import Log from 'Types/session/log'; +import { Log } from 'Types/session/log'; import { toast } from 'react-toastify'; diff --git a/frontend/app/types/session/log.js b/frontend/app/types/session/log.tsx similarity index 66% rename from frontend/app/types/session/log.js rename to frontend/app/types/session/log.tsx index c1dbe0ef7..28cabf329 100644 --- a/frontend/app/types/session/log.js +++ b/frontend/app/types/session/log.tsx @@ -16,7 +16,7 @@ export const LEVEL = { } export function isRed(log) { - return log.level === EXCEPTION || log.level === ERROR; + return } export default Record({ @@ -36,4 +36,16 @@ export default Record({ } }); +interface ILog { + level: string + value: string + time: number + index?: number + errorId?: string +} +export const Log = (log: ILog) => ({ + isRed: () => log.level === EXCEPTION || log.level === ERROR, + isYellow: () => log.level === WARNING || log.level === WARN, + ...log +}) From cce70179050776168426a99f7d1f65bbc7175c28 Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Fri, 25 Nov 2022 16:09:54 +0100 Subject: [PATCH 048/252] fix(player): non-immutable exception list --- frontend/app/player/web/WebPlayer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/app/player/web/WebPlayer.ts b/frontend/app/player/web/WebPlayer.ts index 5ae1347db..78eaa1435 100644 --- a/frontend/app/player/web/WebPlayer.ts +++ b/frontend/app/player/web/WebPlayer.ts @@ -32,11 +32,11 @@ export default class WebPlayer extends Player { constructor(private wpState: Store, session, config: RTCIceServer[], live: boolean) { - let initialLists = live ? {} :{ + let initialLists = live ? {} : { event: session.events.toJSON(), stack: session.stackEvents.toJSON(), resource: session.resources.toJSON(), - exceptions: session.errors, + exceptions: session.errors.toJSON(), } const screen = new Screen() From 51a5a99fd33893a9d4ca6b482764395535ef22ed Mon Sep 17 00:00:00 2001 From: sylenien Date: Fri, 25 Nov 2022 16:18:25 +0100 Subject: [PATCH 049/252] refactor(ui/player): fix performance tab --- .../Session_/Performance/Performance.tsx | 638 ++++++++---------- .../components/Session_/Performance/index.js | 4 +- .../app/components/Session_/Player/Player.js | 2 - .../DevTools/ConsolePanel/ConsolePanel.tsx | 3 - .../shared/DevTools/ConsoleRow/ConsoleRow.tsx | 1 - 5 files changed, 299 insertions(+), 349 deletions(-) diff --git a/frontend/app/components/Session_/Performance/Performance.tsx b/frontend/app/components/Session_/Performance/Performance.tsx index 994401141..45f944b50 100644 --- a/frontend/app/components/Session_/Performance/Performance.tsx +++ b/frontend/app/components/Session_/Performance/Performance.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; -import { Controls as PlayerControls, connectPlayer } from 'Player'; +import { PlayerContext } from 'App/components/Session/playerContext'; +import { observer } from 'mobx-react-lite'; import { AreaChart, Area, @@ -11,10 +12,8 @@ import { Tooltip, ResponsiveContainer, ReferenceLine, - CartesianGrid, Label, } from 'recharts'; -import { Checkbox } from 'UI'; import { durationFromMsFormatted } from 'App/date'; import { formatBytes } from 'App/utils'; @@ -174,362 +173,319 @@ function addFpsMetadata(data) { }); } -@connect((state) => ({ - userDeviceHeapSize: state.getIn(['sessions', 'current', 'userDeviceHeapSize']), - userDeviceMemorySize: state.getIn(['sessions', 'current', 'userDeviceMemorySize']), -})) -export default class Performance extends React.PureComponent { - _timeTicks = generateTicks(this.props.performanceChartData); - _data = addFpsMetadata(this.props.performanceChartData); - // state = { - // totalHeap: false, - // usedHeap: true, - // fps: true, - // } - // onCheckboxClick = (e, { name, checked }) => this.setState({ [ name ]: checked }) +function Performance({ + userDeviceHeapSize, +}: { + userDeviceHeapSize: number; +}) { + const { player, store } = React.useContext(PlayerContext); - onDotClick = ({ index }) => { - const point = this._data[index]; + const { + performanceChartTime, + performanceChartData, + connType, + connBandwidth, + performanceAvaliability: avaliability, + } = store.get(); + + const _timeTicks = generateTicks(performanceChartData); + const _data = addFpsMetadata(performanceChartData); + + const onDotClick = ({ index: pointer }: { index: number }) => { + const point = _data[pointer]; if (!!point) { - PlayerControls.jump(point.time); + player.jump(point.time); } }; - onChartClick = (e) => { + const onChartClick = (e: any) => { if (e === null) return; const { activeTooltipIndex } = e; - const point = this._data[activeTooltipIndex]; + const point = _data[activeTooltipIndex]; if (!!point) { - PlayerControls.jump(point.time); + player.jump(point.time); } }; - render() { - const { - userDeviceHeapSize, - userDeviceMemorySize, - connType, - connBandwidth, - performanceChartTime, - avaliability = {}, - } = this.props; - const { fps, cpu, heap, nodes } = avaliability; - const avaliableCount = [fps, cpu, heap, nodes].reduce((c, av) => (av ? c + 1 : c), 0); - const height = avaliableCount === 0 ? '0' : `${100 / avaliableCount}%`; + const { fps, cpu, heap, nodes } = avaliability; + const avaliableCount = [fps, cpu, heap, nodes].reduce((c, av) => (av ? c + 1 : c), 0); + const height = avaliableCount === 0 ? '0' : `${100 / avaliableCount}%`; - return ( - - -
-
Performance
- - - {/* */} - - = 1000 ? `${connBandwidth / 1000} Mbps` : `${connBandwidth} Kbps` - } - display={connBandwidth != null} - /> - -
-
- - {fps && ( - - + +
+
Performance
+ + + + = 1000 ? `${connBandwidth / 1000} Mbps` : `${connBandwidth} Kbps` + } + display={connBandwidth != null} + /> + +
+
+ + {fps && ( + + + + + + - - - - {/* */} - {/* */} - - - - {/* */} - - - - - {/* */} - - - - - )} - {cpu && ( - - + + + + + + + + + + + )} + {cpu && ( + + + + + + {/* */} + ''} + domain={[0, 'dataMax']} + ticks={_timeTicks} > - - - - {/* */} - ''} - domain={[0, 'dataMax']} - ticks={this._timeTicks} - > - - - - - - - - - )} +
+
+ )} - {heap && ( - - + + + + + ''} // tick={false} + _timeTicks to cartesian array + domain={[0, 'dataMax']} + ticks={_timeTicks} > - - - - {/* */} - ''} // tick={false} + this._timeTicks to cartesian array - domain={[0, 'dataMax']} - ticks={this._timeTicks} - > - - max * 1.2]} - /> - - - - - - - )} - {nodes && ( - - + + max * 1.2]} + /> + + + + + + + )} + {nodes && ( + + + + + + ''} + domain={[0, 'dataMax']} + ticks={_timeTicks} > - - - - {/* */} - ''} - domain={[0, 'dataMax']} - ticks={this._timeTicks} - > - - max * 1.2]} - /> - - - - - - )} -
-
- ); - } +
+ ); } -export const ConnectedPerformance = connectPlayer((state) => ({ - performanceChartTime: state.performanceChartTime, - performanceChartData: state.performanceChartData, - connType: state.connType, - connBandwidth: state.connBandwidth, - avaliability: state.performanceAvaliability, -}))(Performance); +export const ConnectedPerformance = connect((state: any) => ({ + userDeviceHeapSize: state.getIn(['sessions', 'current', 'userDeviceHeapSize']), +}))(observer(Performance)); diff --git a/frontend/app/components/Session_/Performance/index.js b/frontend/app/components/Session_/Performance/index.js index e0e5ed895..967fe1364 100644 --- a/frontend/app/components/Session_/Performance/index.js +++ b/frontend/app/components/Session_/Performance/index.js @@ -1,2 +1,2 @@ -export { default } from './Performance'; -export * from './Performance'; \ No newline at end of file +export { ConnectedPerformance as default } from './Performance'; +export * from './Performance'; diff --git a/frontend/app/components/Session_/Player/Player.js b/frontend/app/components/Session_/Player/Player.js index df6957b6c..4af1be29e 100644 --- a/frontend/app/components/Session_/Player/Player.js +++ b/frontend/app/components/Session_/Player/Player.js @@ -67,8 +67,6 @@ function Player(props) { if (!playerContext.player) return null; - console.log(bottomBlock) - const maxWidth = activeTab ? 'calc(100vw - 270px)' : '100vw'; return (
{ if (_list.current) { - console.log('active index') // @ts-ignore _list.current.scrollToRow(activeIndex); } }, [activeIndex]); - console.log('rerender') return ( !!l) || []; const canExpand = lines.length > 1; From 80ebc26daa34a0e24b305cece66adeab828008c9 Mon Sep 17 00:00:00 2001 From: sylenien Date: Fri, 25 Nov 2022 16:19:16 +0100 Subject: [PATCH 050/252] refactor(ui/player): remove console log --- .../app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx b/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx index 664ace432..51322cc14 100644 --- a/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx +++ b/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx @@ -136,7 +136,6 @@ function ConsolePanel() { const _rowRenderer = ({ index, key, parent, style }: any) => { const item = filteredList[index]; - console.log({ ...filteredList }, { ...item }) return ( // @ts-ignore From 05c035255306866e17583e3156a84fbaf88c9133 Mon Sep 17 00:00:00 2001 From: sylenien Date: Fri, 25 Nov 2022 16:36:24 +0100 Subject: [PATCH 051/252] refactor(ui/player): fix performance crash loop --- frontend/app/components/Session/PlayerContent.js | 6 ++++++ .../app/components/Session_/Performance/Performance.tsx | 9 +++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/frontend/app/components/Session/PlayerContent.js b/frontend/app/components/Session/PlayerContent.js index c0a695173..15187d15d 100644 --- a/frontend/app/components/Session/PlayerContent.js +++ b/frontend/app/components/Session/PlayerContent.js @@ -7,6 +7,12 @@ import RightBlock from './RightBlock'; import { PlayerContext } from 'App/components/Session/playerContext'; import { observer } from 'mobx-react-lite'; + +const TABS = { + EVENTS: 'User Steps', + HEATMAPS: 'Click Map', +}; + function PlayerContent({ session, live, fullscreen, activeTab, setActiveTab }) { const { store } = React.useContext(PlayerContext) diff --git a/frontend/app/components/Session_/Performance/Performance.tsx b/frontend/app/components/Session_/Performance/Performance.tsx index 45f944b50..eca011fcb 100644 --- a/frontend/app/components/Session_/Performance/Performance.tsx +++ b/frontend/app/components/Session_/Performance/Performance.tsx @@ -179,6 +179,8 @@ function Performance({ userDeviceHeapSize: number; }) { const { player, store } = React.useContext(PlayerContext); + const [_timeTicks, setTicks] = React.useState([]) + const [_data, setData] = React.useState([]) const { performanceChartTime, @@ -188,8 +190,11 @@ function Performance({ performanceAvaliability: avaliability, } = store.get(); - const _timeTicks = generateTicks(performanceChartData); - const _data = addFpsMetadata(performanceChartData); + React.useState(() => { + setTicks(generateTicks(performanceChartData)); + setData(addFpsMetadata(performanceChartData)); + }) + const onDotClick = ({ index: pointer }: { index: number }) => { const point = _data[pointer]; From d37f81190d911649e1d3025f7567f770b0c18677 Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Fri, 25 Nov 2022 16:42:17 +0100 Subject: [PATCH 052/252] refactor(player): move log type to player --- .../DevTools/ConsolePanel/ConsolePanel.tsx | 32 +++++------- frontend/app/player/index.ts | 1 + frontend/app/player/web/MessageManager.ts | 12 ++--- frontend/app/player/web/WebPlayer.ts | 11 +++- frontend/app/player/web/types.ts | 22 ++++++++ frontend/app/types/run/run.js | 4 -- frontend/app/types/session/log.tsx | 51 ------------------- frontend/app/types/session/session.ts | 4 -- 8 files changed, 50 insertions(+), 87 deletions(-) create mode 100644 frontend/app/player/web/types.ts delete mode 100644 frontend/app/types/session/log.tsx diff --git a/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx b/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx index 51322cc14..44b9f3d00 100644 --- a/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx +++ b/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useState } from 'react'; -import { Log, LEVEL } from 'Types/session/log'; +import { LogLevel } from 'Player'; import BottomBlock from '../BottomBlock'; import { Tabs, Input, Icon, NoContent } from 'UI'; import cn from 'classnames'; @@ -19,11 +19,11 @@ const WARNINGS = 'WARNINGS'; const ERRORS = 'ERRORS'; const LEVEL_TAB = { - [LEVEL.INFO]: INFO, - [LEVEL.LOG]: INFO, - [LEVEL.WARNING]: WARNINGS, - [LEVEL.ERROR]: ERRORS, - [LEVEL.EXCEPTION]: ERRORS, + [LogLevel.INFO]: INFO, + [LogLevel.LOG]: INFO, + [LogLevel.WARNING]: WARNINGS, + [LogLevel.ERROR]: ERRORS, + [LogLevel.EXCEPTION]: ERRORS, }; const TABS = [ALL, ERRORS, WARNINGS, INFO].map((tab) => ({ text: tab, key: tab })); @@ -36,19 +36,19 @@ function renderWithNL(s = '') { const getIconProps = (level: any) => { switch (level) { - case LEVEL.INFO: - case LEVEL.LOG: + case LogLevel.INFO: + case LogLevel.LOG: return { name: 'console/info', color: 'blue2', }; - case LEVEL.WARN: - case LEVEL.WARNING: + case LogLevel.WARN: + case LogLevel.WARNING: return { name: 'console/warning', color: 'red2', }; - case LEVEL.ERROR: + case LogLevel.ERROR: return { name: 'console/error', color: 'red', @@ -81,16 +81,8 @@ function ConsolePanel() { const jump = (t: number) => player.jump(t) const { logList, exceptionsList, time } = store.get() - const logExceptions = exceptionsList.map(({ time, errorId, name }: any) => - Log({ - level: LEVEL.ERROR, - value: name, - time, - errorId, - }) - ); // @ts-ignore - const logs = logList.concat(logExceptions) + const logs = logList.concat(exceptionsList) const onTabClick = (activeTab: any) => devTools.update(INDEX_KEY, { activeTab }); const onFilterChange = ({ target: { value } }: any) => { diff --git a/frontend/app/player/index.ts b/frontend/app/player/index.ts index 949088e43..55c67fb0b 100644 --- a/frontend/app/player/index.ts +++ b/frontend/app/player/index.ts @@ -1,6 +1,7 @@ export * from './web/assist/AssistManager'; export * from './web/assist/LocalStream'; export * from './web/WebPlayer'; +export * from './web/types'; export * from './create'; export type { MarkedTarget } from './web/TargetMarker' diff --git a/frontend/app/player/web/MessageManager.ts b/frontend/app/player/web/MessageManager.ts index bca4dc34a..fec4fd9cc 100644 --- a/frontend/app/player/web/MessageManager.ts +++ b/frontend/app/player/web/MessageManager.ts @@ -4,7 +4,7 @@ import logger from 'App/logger'; import Resource, { TYPES } from 'Types/session/resource'; import { TYPES as EVENT_TYPES } from 'Types/session/event'; -import { Log } from 'Types/session/log'; +import { Log } from './types'; import { toast } from 'react-toastify'; @@ -394,12 +394,10 @@ export default class MessageManager { /* Lists: */ case "console_log": if (msg.level === 'debug') break; - this.lists.lists.log.append(Log({ - level: msg.level, - value: msg.value, - time, - index, - })) + this.lists.lists.log.append( + // @ts-ignore : TODO: enums in the message schema + Log(msg) + ) break; case "fetch": this.lists.lists.fetch.append(Resource({ diff --git a/frontend/app/player/web/WebPlayer.ts b/frontend/app/player/web/WebPlayer.ts index 78eaa1435..20115d590 100644 --- a/frontend/app/player/web/WebPlayer.ts +++ b/frontend/app/player/web/WebPlayer.ts @@ -1,3 +1,5 @@ +import { Log, LogLevel } from './types' + import type { Store } from '../common/types' import Player, { State as PlayerState } from '../player/Player' @@ -36,7 +38,14 @@ export default class WebPlayer extends Player { event: session.events.toJSON(), stack: session.stackEvents.toJSON(), resource: session.resources.toJSON(), - exceptions: session.errors.toJSON(), + exceptions: session.errors.toJSON().map(({ time, errorId, name }: any) => + Log({ + level: LogLevel.ERROR, + value: name, + time, + errorId, + }) + ), } const screen = new Screen() diff --git a/frontend/app/player/web/types.ts b/frontend/app/player/web/types.ts new file mode 100644 index 000000000..ee185dfbe --- /dev/null +++ b/frontend/app/player/web/types.ts @@ -0,0 +1,22 @@ +export enum LogLevel { + INFO = 'info', + LOG = 'log', + WARNING = 'warning', + WARN = 'warn', + ERROR = 'error', + EXCEPTION = 'exception', +} + +interface ILog { + level: LogLevel + value: string + time: number + index?: number + errorId?: string +} + +export const Log = (log: ILog) => ({ + isRed: () => log.level === LogLevel.EXCEPTION || log.level === LogLevel.ERROR, + isYellow: () => log.level === LogLevel.WARNING || log.level === LogLevel.WARN, + ...log +}) diff --git a/frontend/app/types/run/run.js b/frontend/app/types/run/run.js index b084a25fe..cfe35488e 100644 --- a/frontend/app/types/run/run.js +++ b/frontend/app/types/run/run.js @@ -4,7 +4,6 @@ import Environment from 'Types/environment'; import stepFromJS from './step'; import seleniumStepFromJS from './seleniumStep'; import Resource from '../session/resource'; -import Log from '../session/log'; export const NOT_FETCHED = undefined; export const QUEUED = 'queued'; @@ -45,7 +44,6 @@ class Run extends Record({ finishedAt: undefined, steps: List(), resources: [], - logs: [], seleniumSteps: List(), url_browser_logs: undefined, url_logs: undefined, @@ -148,7 +146,6 @@ function fromJS(run = {}) { .map(r => r.set("time", r.time - firstResourceTime)) .sort((r1, r2) => r1.time - r2.time).toArray() - const logs = List(run.console).map(Log); const screenshotUrl = run.screenshot_url || seleniumSteps.find(({ screenshotUrl }) => !!screenshotUrl, null, {}).screenshotUrl; @@ -163,7 +160,6 @@ function fromJS(run = {}) { lastExecutedString, steps, resources, - logs, seleniumSteps, tags, environment, diff --git a/frontend/app/types/session/log.tsx b/frontend/app/types/session/log.tsx deleted file mode 100644 index 28cabf329..000000000 --- a/frontend/app/types/session/log.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import Record from 'Types/Record'; - -export const INFO = 'info'; -export const LOG = 'log'; -export const WARNING = 'warning'; -export const WARN = 'warn'; -export const ERROR = 'error'; -export const EXCEPTION = 'exception'; -export const LEVEL = { - INFO, - LOG, - WARNING, - WARN, - ERROR, - EXCEPTION, -} - -export function isRed(log) { - return -} - -export default Record({ - level: '', - value: '', - time: undefined, - index: undefined, - errorId: undefined, -}, { - methods: { - isRed() { - return isRed(this); - }, - isYellow() { - return this.level === WARNING || WARN; - } - } -}); - -interface ILog { - level: string - value: string - time: number - index?: number - errorId?: string -} - -export const Log = (log: ILog) => ({ - isRed: () => log.level === EXCEPTION || log.level === ERROR, - isYellow: () => log.level === WARNING || log.level === WARN, - ...log -}) diff --git a/frontend/app/types/session/session.ts b/frontend/app/types/session/session.ts index 961b34864..5a472b043 100644 --- a/frontend/app/types/session/session.ts +++ b/frontend/app/types/session/session.ts @@ -2,7 +2,6 @@ import Record from 'Types/Record'; import { List, Map } from 'immutable'; import { Duration } from 'luxon'; import SessionEvent, { TYPES } from './event'; -import Log from './log'; import StackEvent from './stackEvent'; import Resource from './resource'; import SessionError from './error'; @@ -32,7 +31,6 @@ export default Record( startedAt: 0, duration: 0, events: List(), - logs: List(), stackEvents: List(), resources: List(), missedResources: List(), @@ -125,7 +123,6 @@ export default Record( .map((r) => r.set('time', r.time - firstResourceTime)) .sort((r1, r2) => r1.time - r2.time); const missedResources = resources.filter(({ success }) => !success); - const logs = List(session.logs).map(Log); const stackEventsList = List(stackEvents) .concat(List(session.userEvents)) @@ -156,7 +153,6 @@ export default Record( errors: exceptions, siteId: projectId, events, - logs, stackEvents: stackEventsList, resources, missedResources, From aa8c3ebbbc399714cdeb261ba8404a9beb8950a1 Mon Sep 17 00:00:00 2001 From: sylenien Date: Fri, 25 Nov 2022 17:08:52 +0100 Subject: [PATCH 053/252] refactor(ui/player): deprecate unused devtools tabs, refactor exceptions --- .../app/components/Session/PlayerContent.js | 1 - .../Session_/Exceptions/Exceptions.js | 145 ------------------ .../Session_/Exceptions/Exceptions.tsx | 126 +++++++++++++++ .../Fetch/{Fetch.js => Fetch.DEPRECATED.js} | 0 .../{LongTasks.js => LongTasks.DEPRECATED.js} | 0 .../{Controls.js => Controls.DEPRECATED.js} | 0 .../app/components/Session_/Player/Player.js | 2 - .../{Storage.js => Storage.DEPRECATED.js} | 0 frontend/app/player/player/Animator.ts | 2 +- 9 files changed, 127 insertions(+), 149 deletions(-) delete mode 100644 frontend/app/components/Session_/Exceptions/Exceptions.js create mode 100644 frontend/app/components/Session_/Exceptions/Exceptions.tsx rename frontend/app/components/Session_/Fetch/{Fetch.js => Fetch.DEPRECATED.js} (100%) rename frontend/app/components/Session_/LongTasks/{LongTasks.js => LongTasks.DEPRECATED.js} (100%) rename frontend/app/components/Session_/Player/Controls/{Controls.js => Controls.DEPRECATED.js} (100%) rename frontend/app/components/Session_/Storage/{Storage.js => Storage.DEPRECATED.js} (100%) diff --git a/frontend/app/components/Session/PlayerContent.js b/frontend/app/components/Session/PlayerContent.js index 15187d15d..64d896455 100644 --- a/frontend/app/components/Session/PlayerContent.js +++ b/frontend/app/components/Session/PlayerContent.js @@ -7,7 +7,6 @@ import RightBlock from './RightBlock'; import { PlayerContext } from 'App/components/Session/playerContext'; import { observer } from 'mobx-react-lite'; - const TABS = { EVENTS: 'User Steps', HEATMAPS: 'Click Map', diff --git a/frontend/app/components/Session_/Exceptions/Exceptions.js b/frontend/app/components/Session_/Exceptions/Exceptions.js deleted file mode 100644 index 2b0c206b4..000000000 --- a/frontend/app/components/Session_/Exceptions/Exceptions.js +++ /dev/null @@ -1,145 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { getRE } from 'App/utils'; -import { - NoContent, - Loader, - Input, - ErrorItem, - SlideModal, - ErrorDetails, - Link, - QuestionMarkHint, -} from 'UI'; -import { fetchErrorStackList } from 'Duck/sessions'; -import { connectPlayer, jump } from 'Player'; -import { error as errorRoute } from 'App/routes'; -import Autoscroll from '../Autoscroll'; -import BottomBlock from '../BottomBlock'; - -@connectPlayer((state) => ({ - logs: state.logListNow, - exceptions: state.exceptionsList, - // exceptionsNow: state.exceptionsListNow, -})) -@connect( - (state) => ({ - session: state.getIn(['sessions', 'current']), - errorStack: state.getIn(['sessions', 'errorStack']), - sourcemapUploaded: state.getIn(['sessions', 'sourcemapUploaded']), - loading: state.getIn(['sessions', 'fetchErrorStackList', 'loading']), - }), - { fetchErrorStackList } -) -export default class Exceptions extends React.PureComponent { - state = { - filter: '', - currentError: null, - }; - - onFilterChange = ({ target: { value } }) => this.setState({ filter: value }); - - setCurrentError = (err) => { - const { session } = this.props; - this.props.fetchErrorStackList(session.sessionId, err.errorId); - this.setState({ currentError: err }); - }; - closeModal = () => this.setState({ currentError: null }); - - render() { - const { exceptions, loading, errorStack, sourcemapUploaded } = this.props; - const { filter, currentError } = this.state; - const filterRE = getRE(filter, 'i'); - - const filtered = exceptions.filter((e) => filterRE.test(e.name) || filterRE.test(e.message)); - - return ( - <> - -
- - {currentError.name} - - {currentError.function} -
-
{currentError.message}
-
- ) - } - isDisplayed={currentError != null} - content={ - currentError && ( -
- - - - - -
- ) - } - onClose={this.closeModal} - /> - - -
- Exceptions -
- -
- - - - Upload Source Maps{' '} - - and see source code context obtained from stack traces in their original form. - - } - /> -
-
- - - - {filtered.map((e, index) => ( - jump(e.time)} - error={e} - key={e.key} - onErrorClick={(jsEvent) => { - jsEvent.stopPropagation(); - jsEvent.preventDefault(); - this.setCurrentError(e); - }} - /> - ))} - - - -
- - ); - } -} diff --git a/frontend/app/components/Session_/Exceptions/Exceptions.tsx b/frontend/app/components/Session_/Exceptions/Exceptions.tsx new file mode 100644 index 000000000..e263c9106 --- /dev/null +++ b/frontend/app/components/Session_/Exceptions/Exceptions.tsx @@ -0,0 +1,126 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { getRE } from 'App/utils'; +import { + NoContent, + Loader, + Input, + ErrorItem, + SlideModal, + ErrorDetails, + Link, + QuestionMarkHint, +} from 'UI'; +import { error as errorRoute } from 'App/routes'; +import Autoscroll from '../Autoscroll'; +import BottomBlock from '../BottomBlock'; +import { PlayerContext } from 'App/components/Session/playerContext'; +import { observer } from 'mobx-react-lite'; + +interface IProps { + loading: boolean; + sourcemapUploaded: boolean; + errorStack: Record; +} + +function Exceptions({ errorStack, sourcemapUploaded, loading }: IProps) { + const { player, store } = React.useContext(PlayerContext); + const { logListNow: logs, exceptionsList: exceptions } = store.get(); + const [filter, setFilter] = React.useState(''); + const [currentError, setCurrentErrorVal] = React.useState(null); + + const onFilterChange = ({ target: { value } }: any) => setFilter(value); + const closeModal = () => setCurrentErrorVal(null); + + const filterRE = getRE(filter, 'i'); + const filtered = exceptions.filter((e: any) => filterRE.test(e.name) || filterRE.test(e.message)); + + return ( + <> + +
+ + {currentError.name} + + {currentError.function} +
+
{currentError.message}
+
+ ) + } + isDisplayed={currentError != null} + content={ + currentError && ( +
+ + + + + +
+ ) + } + onClose={closeModal} + /> + + +
+ Exceptions +
+ +
+ + + + Upload Source Maps{' '} + + and see source code context obtained from stack traces in their original form. + + } + /> +
+
+ + + + {filtered.map((e: any, index) => ( + + player.jump(e.time)} error={e} /> + + ))} + + + +
+ + ); +} + +export default connect((state: any) => ({ + errorStack: state.getIn(['sessions', 'errorStack']), + sourcemapUploaded: state.getIn(['sessions', 'sourcemapUploaded']), + loading: state.getIn(['sessions', 'fetchErrorStackList', 'loading']), +}))(observer(Exceptions)); diff --git a/frontend/app/components/Session_/Fetch/Fetch.js b/frontend/app/components/Session_/Fetch/Fetch.DEPRECATED.js similarity index 100% rename from frontend/app/components/Session_/Fetch/Fetch.js rename to frontend/app/components/Session_/Fetch/Fetch.DEPRECATED.js diff --git a/frontend/app/components/Session_/LongTasks/LongTasks.js b/frontend/app/components/Session_/LongTasks/LongTasks.DEPRECATED.js similarity index 100% rename from frontend/app/components/Session_/LongTasks/LongTasks.js rename to frontend/app/components/Session_/LongTasks/LongTasks.DEPRECATED.js diff --git a/frontend/app/components/Session_/Player/Controls/Controls.js b/frontend/app/components/Session_/Player/Controls/Controls.DEPRECATED.js similarity index 100% rename from frontend/app/components/Session_/Player/Controls/Controls.js rename to frontend/app/components/Session_/Player/Controls/Controls.DEPRECATED.js diff --git a/frontend/app/components/Session_/Player/Player.js b/frontend/app/components/Session_/Player/Player.js index 4af1be29e..b8d295361 100644 --- a/frontend/app/components/Session_/Player/Player.js +++ b/frontend/app/components/Session_/Player/Player.js @@ -15,7 +15,6 @@ import { PERFORMANCE, GRAPHQL, EXCEPTIONS, - LONGTASKS, INSPECTOR, OVERVIEW, } from 'Duck/components/player'; @@ -93,7 +92,6 @@ function Player(props) { {bottomBlock === PERFORMANCE && } {bottomBlock === GRAPHQL && } {bottomBlock === EXCEPTIONS && } - {bottomBlock === LONGTASKS && } {bottomBlock === INSPECTOR && }
)} diff --git a/frontend/app/components/Session_/Storage/Storage.js b/frontend/app/components/Session_/Storage/Storage.DEPRECATED.js similarity index 100% rename from frontend/app/components/Session_/Storage/Storage.js rename to frontend/app/components/Session_/Storage/Storage.DEPRECATED.js diff --git a/frontend/app/player/player/Animator.ts b/frontend/app/player/player/Animator.ts index 83e10f098..787df725b 100644 --- a/frontend/app/player/player/Animator.ts +++ b/frontend/app/player/player/Animator.ts @@ -182,7 +182,7 @@ export default class Animator { return this.jump( Math.max( 0, - time - interval + time + interval ) ); } From 3a6a2dd4a631f768e61bdb0ac8da51f43343849c Mon Sep 17 00:00:00 2001 From: sylenien Date: Fri, 25 Nov 2022 17:59:49 +0100 Subject: [PATCH 054/252] refactor(ui/player): remove key and prop warnings --- frontend/app/components/Errors/List/List.js | 19 +++++---- .../components/Session/IOSPlayer/Crashes.js | 3 +- .../app/components/Session/IOSPlayer/Logs.js | 9 ++--- .../Console/ConsoleRow/ConsoleRow.tsx | 2 +- .../Session_/Exceptions/Exceptions.tsx | 1 - .../components/Session_/GraphQL/GraphQL.js | 1 - .../Session_/Network/NetworkContent.js | 1 - .../DevTools/ConsolePanel/ConsolePanel.tsx | 39 ++++++++++--------- .../shared/DevTools/ConsoleRow/ConsoleRow.tsx | 4 +- .../DevTools/NetworkPanel/NetworkPanel.tsx | 1 - .../StackEventPanel/StackEventPanel.tsx | 1 - .../LiveSessionSearchField.tsx | 3 +- 12 files changed, 38 insertions(+), 46 deletions(-) diff --git a/frontend/app/components/Errors/List/List.js b/frontend/app/components/Errors/List/List.js index 9f379319a..8a22a1f2d 100644 --- a/frontend/app/components/Errors/List/List.js +++ b/frontend/app/components/Errors/List/List.js @@ -25,7 +25,7 @@ const sortOptions = Object.entries(sortOptionsMap) @connect(state => ({ loading: state.getIn([ "errors", "loading" ]), - resolveToggleLoading: state.getIn(["errors", "resolve", "loading"]) || + resolveToggleLoading: state.getIn(["errors", "resolve", "loading"]) || state.getIn(["errors", "unresolve", "loading"]), ignoreLoading: state.getIn([ "errors", "ignore", "loading" ]), mergeLoading: state.getIn([ "errors", "merge", "loading" ]), @@ -54,19 +54,19 @@ export default class List extends React.PureComponent { } this.debounceFetch = debounce(this.props.editOptions, 1000); } - + componentDidMount() { this.props.applyFilter({ }); } check = ({ errorId }) => { const { checkedIds } = this.state; - const newCheckedIds = checkedIds.contains(errorId) - ? checkedIds.remove(errorId) + const newCheckedIds = checkedIds.contains(errorId) + ? checkedIds.remove(errorId) : checkedIds.add(errorId); this.setState({ checkedAll: newCheckedIds.size === this.props.list.size, - checkedIds: newCheckedIds + checkedIds: newCheckedIds }); } @@ -184,7 +184,7 @@ export default class List extends React.PureComponent { onClick={ this.unresolve } disabled={ someLoading || currentCheckedIds.size === 0} /> - } + } { status !== IGNORED && - } + }
- Sort By + Sort By -
- - - - - {[ - { - label: 'Start', - width: 90, - render: renderStart, - }, - { - label: 'Status', - width: 70, - render: renderDefaultStatus, - }, - { - label: 'Type', - dataKey: 'operationKind', - width: 60, - }, - { - label: 'Name', - width: 240, - render: renderName, - }, - ]} - - - - - - ); - } -} diff --git a/frontend/app/components/Session_/GraphQL/GraphQL.tsx b/frontend/app/components/Session_/GraphQL/GraphQL.tsx new file mode 100644 index 000000000..6f89a5ec1 --- /dev/null +++ b/frontend/app/components/Session_/GraphQL/GraphQL.tsx @@ -0,0 +1,174 @@ +import React, { useEffect } from 'react'; +import { NoContent, Input, SlideModal, CloseButton, Button } from 'UI'; +import { getRE } from 'App/utils'; +import BottomBlock from '../BottomBlock'; +import TimeTable from '../TimeTable'; +import GQLDetails from './GQLDetails'; +import { renderStart } from 'Components/Session_/Network/NetworkContent'; +import { PlayerContext } from 'App/components/Session/playerContext'; +import { observer } from 'mobx-react-lite'; + +function renderDefaultStatus() { + return '2xx-3xx'; +} + +export function renderName(r: Record) { + const { player } = React.useContext(PlayerContext); + + return ( +
+
{r.operationName}
+ +
+ ); +} + +function GraphQL() { + const { player, store } = React.useContext(PlayerContext); + + const { graphqlList: list, graphqlListNow: listNow, time, livePlay } = store.get(); + + const defaultState = { + filter: '', + filteredList: list, + filteredListNow: listNow, + // @ts-ignore + current: null, + currentIndex: 0, + showFetchDetails: false, + hasNextError: false, + hasPreviousError: false, + lastActiveItem: 0, + }; + + const [state, setState] = React.useState(defaultState); + + const filterList = (list: any, value: string) => { + const filterRE = getRE(value, 'i'); + + return value + ? list.filter( + (r: any) => + filterRE.test(r.operationKind) || + filterRE.test(r.operationName) || + filterRE.test(r.variables) + ) + : list; + }; + + const onFilterChange = ({ target: { value } }: React.ChangeEvent) => { + const filtered = filterList(list, value); + setState((prevState) => ({ + ...prevState, + filter: value, + filteredList: filtered, + currentIndex: 0, + })); + }; + + const setCurrent = (item: any, index: number) => { + if (!livePlay) { + player.pause(); + player.jump(item.time); + } + setState((prevState) => ({ ...prevState, current: item, currentIndex: index })); + }; + + const closeModal = () => + setState((prevState) => ({ ...prevState, current: null, showFetchDetails: false })); + + useEffect(() => { + const filtered = filterList(listNow, state.filter); + if (filtered.length !== lastActiveItem) { + setState((prevState) => ({ ...prevState, lastActiveItem: listNow.length })); + } + }, [time]); + + const { current, currentIndex, filteredList, lastActiveItem } = state; + + return ( + + +

GraphQL

+
+ +
+
+ } + isDisplayed={current != null} + content={ + current && ( + + ) + } + onClose={closeModal} + /> + + + GraphQL +
+ +
+
+ + + + {[ + { + label: 'Start', + width: 90, + render: renderStart, + }, + { + label: 'Status', + width: 70, + render: renderDefaultStatus, + }, + { + label: 'Type', + dataKey: 'operationKind', + width: 60, + }, + { + label: 'Name', + width: 240, + render: renderName, + }, + ]} + + + +
+ + ); +} + +export default observer(GraphQL); diff --git a/frontend/app/components/Session_/Network/Network.js b/frontend/app/components/Session_/Network/Network.DEPRECATED.js similarity index 100% rename from frontend/app/components/Session_/Network/Network.js rename to frontend/app/components/Session_/Network/Network.DEPRECATED.js diff --git a/frontend/app/components/Session_/Profiler/Profiler.js b/frontend/app/components/Session_/Profiler/Profiler.DEPRECATED.js similarity index 100% rename from frontend/app/components/Session_/Profiler/Profiler.js rename to frontend/app/components/Session_/Profiler/Profiler.DEPRECATED.js diff --git a/frontend/app/components/Session_/StackEvents/StackEvents.js b/frontend/app/components/Session_/StackEvents/StackEvents.DEPRECATED.js similarity index 100% rename from frontend/app/components/Session_/StackEvents/StackEvents.js rename to frontend/app/components/Session_/StackEvents/StackEvents.DEPRECATED.js diff --git a/frontend/app/components/ui/Button/Button.tsx b/frontend/app/components/ui/Button/Button.tsx index dc73f9e60..35e3588fa 100644 --- a/frontend/app/components/ui/Button/Button.tsx +++ b/frontend/app/components/ui/Button/Button.tsx @@ -5,7 +5,7 @@ import { CircularLoader, Icon, Tooltip } from 'UI'; interface Props { className?: string; children?: React.ReactNode; - onClick?: () => void; + onClick?: (e: React.MouseEvent) => void; disabled?: boolean; type?: 'button' | 'submit' | 'reset'; variant?: 'default' | 'primary' | 'text' | 'text-primary' | 'text-red' | 'outline' | 'green'; From 5e89bbbbb6b624a9ad7e717ce02259f56ae05f7d Mon Sep 17 00:00:00 2001 From: sylenien Date: Mon, 28 Nov 2022 11:29:31 +0100 Subject: [PATCH 063/252] refactor(ui/player): remove unused, deprecated etc --- .../Session/Layout/ToolPanel/StackEvents.js | 19 +++++++++---------- .../components/Session_/BottomBlock/Header.js | 2 +- .../components/Session_/BottomBlock/tabs.js | 9 --------- .../Session_/EventsBlock/NoteEvent.tsx | 3 --- .../Session_/Player/Controls/Controls.tsx | 2 +- .../Player/Overlay/ElementsMarker/Marker.tsx | 7 +++++-- .../app/components/Session_/Player/Player.js | 3 +-- .../app/components/Session_/PlayerBlock.js | 1 - .../shared/DevTools/BottomBlock/Header.js | 2 +- .../shared/DevTools/BottomBlock/tabs.js | 9 --------- 10 files changed, 18 insertions(+), 39 deletions(-) delete mode 100644 frontend/app/components/Session_/BottomBlock/tabs.js delete mode 100644 frontend/app/components/shared/DevTools/BottomBlock/tabs.js diff --git a/frontend/app/components/Session/Layout/ToolPanel/StackEvents.js b/frontend/app/components/Session/Layout/ToolPanel/StackEvents.js index cdfccff31..c66c7d7c2 100644 --- a/frontend/app/components/Session/Layout/ToolPanel/StackEvents.js +++ b/frontend/app/components/Session/Layout/ToolPanel/StackEvents.js @@ -2,9 +2,8 @@ import React from 'react'; import { connect } from 'react-redux'; import { useState } from 'react'; import { NoContent, Tabs } from 'UI'; -import withEnumToggle from 'HOCs/withEnumToggle'; import { hideHint } from 'Duck/components/player'; -import { typeList } from 'Types/session/stackEvent'; +import { typeList } from 'Types/session/stackEvent'; import * as PanelLayout from './PanelLayout'; import UserEvent from 'Components/Session_/StackEvents/UserEvent'; @@ -14,7 +13,7 @@ const ALL = 'ALL'; const TABS = [ ALL, ...typeList ].map(tab =>({ text: tab, key: tab })); -function StackEvents({ +function StackEvents({ stackEvents, hintIsHidden, hideHint, @@ -28,10 +27,10 @@ function StackEvents({ return ( <> - @@ -39,12 +38,12 @@ function StackEvents({ Integrations {' and '} - Events + Events { ' make debugging easier. Sync your backend logs and custom events with session replay.' }

@@ -66,8 +65,8 @@ function StackEvents({ } export default connect(state => ({ - hintIsHidden: state.getIn(['components', 'player', 'hiddenHints', 'stack']) || + hintIsHidden: state.getIn(['components', 'player', 'hiddenHints', 'stack']) || !state.getIn([ 'site', 'list' ]).some(s => s.stackIntegrations), }), { hideHint -})(StackEvents); \ No newline at end of file +})(StackEvents); diff --git a/frontend/app/components/Session_/BottomBlock/Header.js b/frontend/app/components/Session_/BottomBlock/Header.js index 15cdf3365..4812305b7 100644 --- a/frontend/app/components/Session_/BottomBlock/Header.js +++ b/frontend/app/components/Session_/BottomBlock/Header.js @@ -2,7 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import cn from 'classnames'; import { closeBottomBlock } from 'Duck/components/player'; -import { Input, CloseButton } from 'UI'; +import { CloseButton } from 'UI'; import stl from './header.module.css'; const Header = ({ diff --git a/frontend/app/components/Session_/BottomBlock/tabs.js b/frontend/app/components/Session_/BottomBlock/tabs.js deleted file mode 100644 index 6addd161e..000000000 --- a/frontend/app/components/Session_/BottomBlock/tabs.js +++ /dev/null @@ -1,9 +0,0 @@ -// import { NONE, CONSOLE, NETWORK, STACKEVENTS, REDUX_STATE, PROFILER, PERFORMANCE, GRAPHQL } from 'Duck/components/player'; -// -// -// export default { -// [NONE]: { -// Component: null, -// -// } -// } \ No newline at end of file diff --git a/frontend/app/components/Session_/EventsBlock/NoteEvent.tsx b/frontend/app/components/Session_/EventsBlock/NoteEvent.tsx index 7ad33cece..676b1f901 100644 --- a/frontend/app/components/Session_/EventsBlock/NoteEvent.tsx +++ b/frontend/app/components/Session_/EventsBlock/NoteEvent.tsx @@ -9,7 +9,6 @@ import copy from 'copy-to-clipboard'; import { toast } from 'react-toastify'; import { session } from 'App/routes'; import { confirm } from 'UI'; -import { filterOutNote as filterOutTimelineNote } from 'Player'; import { TeamBadge } from 'Shared/SessionListContainer/components/Notes'; interface Props { @@ -24,7 +23,6 @@ function NoteEvent(props: Props) { const { settingsStore, notesStore } = useStore(); const { timezone } = settingsStore.sessionSettings; - console.log(props.noEdit); const onEdit = () => { props.onEdit({ isVisible: true, @@ -60,7 +58,6 @@ function NoteEvent(props: Props) { ) { notesStore.deleteNote(props.note.noteId).then((r) => { props.filterOutNote(props.note.noteId); - filterOutTimelineNote(props.note.noteId); toast.success('Note deleted'); }); } diff --git a/frontend/app/components/Session_/Player/Controls/Controls.tsx b/frontend/app/components/Session_/Player/Controls/Controls.tsx index d88f8eeeb..3f2fffd01 100644 --- a/frontend/app/components/Session_/Player/Controls/Controls.tsx +++ b/frontend/app/components/Session_/Player/Controls/Controls.tsx @@ -1,7 +1,7 @@ import React from 'react'; import cn from 'classnames'; import { connect } from 'react-redux'; -import { STORAGE_TYPES, selectStorageType, selectStorageListNow } from 'Player'; +import { STORAGE_TYPES, selectStorageType } from 'Player'; import LiveTag from 'Shared/LiveTag'; import { Icon, Tooltip } from 'UI'; diff --git a/frontend/app/components/Session_/Player/Overlay/ElementsMarker/Marker.tsx b/frontend/app/components/Session_/Player/Overlay/ElementsMarker/Marker.tsx index cbb772020..a81dca109 100644 --- a/frontend/app/components/Session_/Player/Overlay/ElementsMarker/Marker.tsx +++ b/frontend/app/components/Session_/Player/Overlay/ElementsMarker/Marker.tsx @@ -2,8 +2,8 @@ import React from 'react'; import type { MarkedTarget } from 'Player'; import cn from 'classnames'; import stl from './Marker.module.css'; -import { activeTarget } from 'Player'; import { Tooltip } from 'UI'; +import { PlayerContext } from 'App/components/Session/playerContext'; interface Props { target: MarkedTarget; @@ -17,11 +17,14 @@ export default function Marker({ target, active }: Props) { width: `${target.boundingRect.width}px`, height: `${target.boundingRect.height}px`, }; + const { player } = React.useContext(PlayerContext) + return (
activeTarget(target.index)} + // @ts-ignore + onClick={() => player.setActiveTarget(target.index)} >
{target.index + 1}
{target.count} Clicks
}> diff --git a/frontend/app/components/Session_/Player/Player.js b/frontend/app/components/Session_/Player/Player.js index bdcb0e925..d72e53b59 100644 --- a/frontend/app/components/Session_/Player/Player.js +++ b/frontend/app/components/Session_/Player/Player.js @@ -4,7 +4,6 @@ import { findDOMNode } from 'react-dom'; import cn from 'classnames'; import { EscapeButton } from 'UI'; import { hide as hideTargetDefiner } from 'Duck/components/targetDefiner'; -import { fullscreenOff } from 'Duck/components/player'; import { NONE, CONSOLE, @@ -17,9 +16,9 @@ import { EXCEPTIONS, INSPECTOR, OVERVIEW, + fullscreenOff, } from 'Duck/components/player'; import NetworkPanel from 'Shared/DevTools/NetworkPanel'; -// import StackEvents from '../StackEvents/StackEvents'; import Storage from '../Storage'; import { ConnectedPerformance } from '../Performance'; import GraphQL from '../GraphQL'; diff --git a/frontend/app/components/Session_/PlayerBlock.js b/frontend/app/components/Session_/PlayerBlock.js index d14d8b983..173b74e3a 100644 --- a/frontend/app/components/Session_/PlayerBlock.js +++ b/frontend/app/components/Session_/PlayerBlock.js @@ -1,7 +1,6 @@ import React from 'react'; import cn from 'classnames'; import { connect } from 'react-redux'; -import { NONE } from 'Duck/components/player'; import Player from './Player'; import SubHeader from './Subheader'; diff --git a/frontend/app/components/shared/DevTools/BottomBlock/Header.js b/frontend/app/components/shared/DevTools/BottomBlock/Header.js index 15dd7a0c9..f743ab3a5 100644 --- a/frontend/app/components/shared/DevTools/BottomBlock/Header.js +++ b/frontend/app/components/shared/DevTools/BottomBlock/Header.js @@ -2,7 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import cn from 'classnames'; import { closeBottomBlock } from 'Duck/components/player'; -import { Input, CloseButton } from 'UI'; +import { CloseButton } from 'UI'; import stl from './header.module.css'; const Header = ({ diff --git a/frontend/app/components/shared/DevTools/BottomBlock/tabs.js b/frontend/app/components/shared/DevTools/BottomBlock/tabs.js deleted file mode 100644 index 6addd161e..000000000 --- a/frontend/app/components/shared/DevTools/BottomBlock/tabs.js +++ /dev/null @@ -1,9 +0,0 @@ -// import { NONE, CONSOLE, NETWORK, STACKEVENTS, REDUX_STATE, PROFILER, PERFORMANCE, GRAPHQL } from 'Duck/components/player'; -// -// -// export default { -// [NONE]: { -// Component: null, -// -// } -// } \ No newline at end of file From 2662b97401a7bf72bdc1297d578b8a72070caac0 Mon Sep 17 00:00:00 2001 From: sylenien Date: Mon, 28 Nov 2022 11:36:23 +0100 Subject: [PATCH 064/252] refactor(ui/player): connect notes --- frontend/app/components/Session/WebPlayer.tsx | 3 --- .../app/components/Session_/Player/Controls/Timeline.js | 4 +++- frontend/app/mstore/notesStore.ts | 8 ++------ 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/frontend/app/components/Session/WebPlayer.tsx b/frontend/app/components/Session/WebPlayer.tsx index d7e8a115b..779fe3a11 100644 --- a/frontend/app/components/Session/WebPlayer.tsx +++ b/frontend/app/components/Session/WebPlayer.tsx @@ -41,12 +41,9 @@ function WebPlayer(props: any) { ); setContextValue({ player: WebPlayerInst, store: PlayerStore }); - // initPlayer(session, jwt); TODOPlayer props.fetchMembers(); notesStore.fetchSessionNotes(session.sessionId).then((r) => { - // WebPlayerInst.injectNotes(r); - // PlayerStore.update({ notes: r }) const note = props.query.get('note'); if (note) { WebPlayerInst.pause(); diff --git a/frontend/app/components/Session_/Player/Controls/Timeline.js b/frontend/app/components/Session_/Player/Controls/Timeline.js index 9a4d0595b..307076588 100644 --- a/frontend/app/components/Session_/Player/Controls/Timeline.js +++ b/frontend/app/components/Session_/Player/Controls/Timeline.js @@ -10,6 +10,7 @@ import { debounce } from 'App/utils'; import TooltipContainer from './components/TooltipContainer'; import { PlayerContext } from 'App/components/Session/playerContext'; import { observer } from 'mobx-react-lite'; +import { useStore } from 'App/mstore'; const BOUNDRY = 0; @@ -26,6 +27,7 @@ let debounceTooltipChange = () => null; function Timeline(props) { const { player, store } = React.useContext(PlayerContext) const [wasPlaying, setWasPlaying] = React.useState(false); + const { notesStore } = useStore(); const { playing, time, @@ -36,8 +38,8 @@ function Timeline(props) { disabled, endTime, live, - notes = [], } = store.get() + const notes = notesStore.sessionNotes const progressRef = React.useRef(); const timelineRef = React.useRef(); diff --git a/frontend/app/mstore/notesStore.ts b/frontend/app/mstore/notesStore.ts index 46d7fc652..8e1db4aa7 100644 --- a/frontend/app/mstore/notesStore.ts +++ b/frontend/app/mstore/notesStore.ts @@ -2,13 +2,9 @@ import { makeAutoObservable } from "mobx" import { notesService } from "App/services" import { Note, WriteNote, iTag, NotesFilter } from 'App/services/NotesService' -interface SessionNotes { - [sessionId: string]: Note[] -} - export default class NotesStore { notes: Note[] = [] - sessionNotes: SessionNotes = {} + sessionNotes: Note[] = [] loading: boolean page = 1 pageSize = 10 @@ -48,7 +44,7 @@ export default class NotesStore { this.loading = true try { const notes = await notesService.getNotesBySessionId(sessionId) - this.sessionNotes[sessionId] = notes + this.sessionNotes = notes return notes; } catch (e) { console.error(e) From 9d36aefc7c7d2d09c43fefe94c09a0734dd2754a Mon Sep 17 00:00:00 2001 From: sylenien Date: Mon, 28 Nov 2022 12:56:45 +0100 Subject: [PATCH 065/252] fix(ui): fix filterlist method --- frontend/app/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/utils.ts b/frontend/app/utils.ts index e3cd50134..34e6d896e 100644 --- a/frontend/app/utils.ts +++ b/frontend/app/utils.ts @@ -72,7 +72,7 @@ export const filterList = >( if (searchQuery === '') return list; const filterRE = getRE(searchQuery, 'i'); let _list = list.filter((listItem: T) => { - return testKeys.some((key) => filterRE.test(listItem[key]) || searchCb?.(listItem, filterRE)); + return testKeys.some((key) => filterRE.test(listItem[key])) || searchCb?.(listItem, filterRE); }); return _list; } From 9e9452a62d044a07380e310763801ee781172151 Mon Sep 17 00:00:00 2001 From: rjshrjndrn Date: Mon, 28 Nov 2022 14:11:18 +0100 Subject: [PATCH 066/252] chore(helm): Update helm version Signed-off-by: rjshrjndrn --- scripts/helmcharts/init.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/helmcharts/init.sh b/scripts/helmcharts/init.sh index ebbfd60c3..b930efa87 100644 --- a/scripts/helmcharts/init.sh +++ b/scripts/helmcharts/init.sh @@ -59,7 +59,7 @@ usr=`whoami` which helm &> /dev/null if [[ $? -ne 0 ]]; then info "helm not installed. Installing it..." - curl -ssl https://get.helm.sh/helm-v3.10.1-linux-amd64.tar.gz -o /tmp/helm.tar.gz + curl -ssl https://get.helm.sh/helm-v3.10.2-linux-amd64.tar.gz -o /tmp/helm.tar.gz tar -xf /tmp/helm.tar.gz chmod +x linux-amd64/helm sudo cp linux-amd64/helm /usr/local/bin/helm From 70d7210e9a8944479e1eb7a52f38ee151c9f2415 Mon Sep 17 00:00:00 2001 From: Alexander Zavorotynskiy Date: Mon, 28 Nov 2022 14:58:18 +0100 Subject: [PATCH 067/252] feat(backend/sink): write big messages directly to file --- backend/internal/config/sink/config.go | 2 +- backend/internal/sink/sessionwriter/file.go | 23 +++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/backend/internal/config/sink/config.go b/backend/internal/config/sink/config.go index 1a2df142e..53e3517a4 100644 --- a/backend/internal/config/sink/config.go +++ b/backend/internal/config/sink/config.go @@ -9,7 +9,7 @@ type Config struct { common.Config FsDir string `env:"FS_DIR,required"` FsUlimit uint16 `env:"FS_ULIMIT,required"` - FileBuffer int `env:"FILE_BUFFER,default=32768"` + FileBuffer int `env:"FILE_BUFFER,default=16384"` SyncTimeout int `env:"SYNC_TIMEOUT,default=5"` GroupSink string `env:"GROUP_SINK,required"` TopicRawWeb string `env:"TOPIC_RAW_WEB,required"` diff --git a/backend/internal/sink/sessionwriter/file.go b/backend/internal/sink/sessionwriter/file.go index 1ad076d72..37b1664a9 100644 --- a/backend/internal/sink/sessionwriter/file.go +++ b/backend/internal/sink/sessionwriter/file.go @@ -2,6 +2,8 @@ package sessionwriter import ( "bufio" + "io" + "log" "os" ) @@ -24,15 +26,32 @@ func NewFile(path string, bufSize int) (*File, error) { } func (f *File) Write(data []byte) error { + f.updated = true + if len(data) > f.buffer.Available()+f.buffer.Size() { + // Flush buffer to file + for i := 0; i < 3; i++ { + err := f.buffer.Flush() + if err == nil { + break + } + log.Printf("can't flush buffer: %s", err) + } + // Write big message directly to file + return f.write(f.file, data) + } + return f.write(f.buffer, data) +} + +func (f *File) write(w io.Writer, data []byte) error { leftToWrite := len(data) for leftToWrite > 0 { - writtenDown, err := f.buffer.Write(data) + from := len(data) - leftToWrite + writtenDown, err := w.Write(data[from:]) if err != nil { return err } leftToWrite -= writtenDown } - f.updated = true return nil } From 1a377b42da967d06b66a38e026c2d8216499a64e Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Tue, 29 Nov 2022 10:28:52 +0100 Subject: [PATCH 068/252] fix(player): TargetMarker api types fix --- frontend/app/player/web/WebPlayer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/app/player/web/WebPlayer.ts b/frontend/app/player/web/WebPlayer.ts index a07f12867..b9d35d686 100644 --- a/frontend/app/player/web/WebPlayer.ts +++ b/frontend/app/player/web/WebPlayer.ts @@ -112,11 +112,11 @@ export default class WebPlayer extends Player { } // Target Marker - setActiveTarget = (args: Parameters) => { + setActiveTarget = (...args: Parameters) => { this.targetMarker.setActiveTarget(...args) } - markTargets = (args: Parameters) => { + markTargets = (...args: Parameters) => { this.pause() this.targetMarker.markTargets(...args) } From c047ff166a77fdcd836cf8cb1afce60f89cd08bc Mon Sep 17 00:00:00 2001 From: sylenien Date: Mon, 28 Nov 2022 16:51:56 +0100 Subject: [PATCH 069/252] fix(ui): remove old func --- .../shared/DevTools/NetworkPanel/NetworkPanel.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx b/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx index 1193ae86e..17712a295 100644 --- a/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx +++ b/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx @@ -168,13 +168,13 @@ function NetworkPanel() { }, [ showOnlyErrors, list ]) filteredList = useRegExListFilterMemo(filteredList, it => it.status, filter) filteredList = useRegExListFilterMemo(filteredList, it => it.name, filter) - filteredList = useRegExListFilterMemo(filteredList, it => it.type, filter) + filteredList = useRegExListFilterMemo(filteredList, it => it.type, filter) filteredList = useTabListFilterMemo(filteredList, it => TYPE_TO_TAB[it.type], ALL, activeTab) const onTabClick = (activeTab: typeof TAP_KEYS[number]) => devTools.update(INDEX_KEY, { activeTab }) const onFilterChange = ({ target: { value } }: React.ChangeEvent) => devTools.update(INDEX_KEY, { filter: value }) - // AutoScroll + // AutoScroll const autoScrollIndex = fetchListNow.length + resourceListNow.length const { timeoutStartAutoscroll, @@ -190,12 +190,12 @@ function NetworkPanel() { timeoutStartAutoscroll() } - const resourcesSize = useMemo(() => + const resourcesSize = useMemo(() => resourceList.reduce( (sum, { decodedBodySize }) => sum + (decodedBodySize || 0), 0, ), [ resourceList.length ]) - const transferredSize = useMemo(() => + const transferredSize = useMemo(() => resourceList.reduce( (sum, { headerSize, encodedBodySize }) => sum + (headerSize || 0) + (encodedBodySize || 0), @@ -226,12 +226,12 @@ function NetworkPanel() { setIsDetailsModalActive(true) showModal( 0} />, - { + { right: true, onClose: () => { setIsDetailsModalActive(false) timeoutStartAutoscroll() - } + } } ) devTools.update(INDEX_KEY, { index: filteredList.indexOf(item) }) @@ -327,7 +327,6 @@ function NetworkPanel() { sortBy={sortBy} sortAscending={sortAscending} onJump={(row: any) => { - setPauseSync(true); devTools.update(INDEX_KEY, { index: filteredList.indexOf(row) }); player.jump(row.time); }} From efcb317d6b13827cd8eb7967cd1b0113ec738597 Mon Sep 17 00:00:00 2001 From: sylenien Date: Tue, 29 Nov 2022 10:30:02 +0100 Subject: [PATCH 070/252] fix(ui): fix webpack setup --- frontend/webpack.config.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/webpack.config.ts b/frontend/webpack.config.ts index e2655b67e..94df4d579 100644 --- a/frontend/webpack.config.ts +++ b/frontend/webpack.config.ts @@ -132,10 +132,12 @@ const config: Configuration = { devServer: { // static: path.join(__dirname, "public"), historyApiFallback: true, - host: 'localhost', + host: '0.0.0.0', open: true, port: 3333, hot: true, + compress: true, + allowedHosts: "all", }, }; From dcc8ef5fb055d2269364a2fc7e237867410cdaea Mon Sep 17 00:00:00 2001 From: MauricioGarciaS <47052044+MauricioGarciaS@users.noreply.github.com> Date: Tue, 29 Nov 2022 10:38:54 +0100 Subject: [PATCH 071/252] Added airflow worker, scheduler, trigger, webserver --- ee/recommendation/airflow-local/Dockerfile | 14 + .../airflow-local/dags/training_dag.py | 42 +++ .../airflow-local/docker-compose.yaml | 284 ++++++++++++++++++ .../{ => airflow-local}/requirements.txt | 14 +- .../scripts/core}/recommendation.py | 16 +- .../airflow-local/scripts/task.py | 23 ++ .../airflow-local/scripts/utils/ch_client.py | 54 ++++ .../airflow-local/scripts/utils/pg_client.py | 166 ++++++++++ ee/recommendation/run.sh | 8 - ee/recommendation/signals.sql | 11 + 10 files changed, 611 insertions(+), 21 deletions(-) create mode 100644 ee/recommendation/airflow-local/Dockerfile create mode 100644 ee/recommendation/airflow-local/dags/training_dag.py create mode 100644 ee/recommendation/airflow-local/docker-compose.yaml rename ee/recommendation/{ => airflow-local}/requirements.txt (55%) rename ee/recommendation/{ => airflow-local/scripts/core}/recommendation.py (89%) create mode 100644 ee/recommendation/airflow-local/scripts/task.py create mode 100644 ee/recommendation/airflow-local/scripts/utils/ch_client.py create mode 100644 ee/recommendation/airflow-local/scripts/utils/pg_client.py delete mode 100644 ee/recommendation/run.sh create mode 100644 ee/recommendation/signals.sql diff --git a/ee/recommendation/airflow-local/Dockerfile b/ee/recommendation/airflow-local/Dockerfile new file mode 100644 index 000000000..992bcf89a --- /dev/null +++ b/ee/recommendation/airflow-local/Dockerfile @@ -0,0 +1,14 @@ +FROM apache/airflow:2.4.3 +COPY requirements.txt . + +USER root +RUN apt-get update \ + && apt-get install -y \ + vim \ + && apt-get install gcc libc-dev g++ -y \ + && apt-get install -y pkg-config libxml2-dev libxmlsec1-dev libxmlsec1-openssl + + +USER airflow +RUN pip install --upgrade pip +RUN pip install -r requirements.txt diff --git a/ee/recommendation/airflow-local/dags/training_dag.py b/ee/recommendation/airflow-local/dags/training_dag.py new file mode 100644 index 000000000..73c98b4ef --- /dev/null +++ b/ee/recommendation/airflow-local/dags/training_dag.py @@ -0,0 +1,42 @@ +from datetime import datetime, timedelta +from textwrap import dedent + +import pendulum + +from airflow import DAG +from airflow.operators.bash import BashOperator +from airflow.operators.python import PythonOperator +import os +_work_dir = os.getcwd() + +def my_function(): + l = os.listdir('scripts') + print(l) + return l + +dag = DAG( + "first_test", + default_args={ + "depends_on_past": True, + "retries": 1, + "retry_delay": timedelta(minutes=3), + }, + start_date=pendulum.datetime(2015, 12, 1, tz="UTC"), + description="My first test", + schedule="@daily", + catchup=False, +) + + +#assigning the task for our dag to do +with dag: + first_world = PythonOperator( + task_id='FirstTest', + python_callable=my_function, + ) + hello_world = BashOperator( + task_id='OneTest', + bash_command=f'python {_work_dir}/scripts/task.py --mode train --kernel linear', + # provide_context=True + ) + first_world >> hello_world diff --git a/ee/recommendation/airflow-local/docker-compose.yaml b/ee/recommendation/airflow-local/docker-compose.yaml new file mode 100644 index 000000000..bc860831f --- /dev/null +++ b/ee/recommendation/airflow-local/docker-compose.yaml @@ -0,0 +1,284 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +# Basic Airflow cluster configuration for CeleryExecutor with Redis and PostgreSQL. +# +# WARNING: This configuration is for local development. Do not use it in a production deployment. +# +# This configuration supports basic configuration using environment variables or an .env file +# The following variables are supported: +# +# AIRFLOW_IMAGE_NAME - Docker image name used to run Airflow. +# Default: apache/airflow:2.4.3 +# AIRFLOW_UID - User ID in Airflow containers +# Default: 50000 +# Those configurations are useful mostly in case of standalone testing/running Airflow in test/try-out mode +# +# _AIRFLOW_WWW_USER_USERNAME - Username for the administrator account (if requested). +# Default: airflow +# _AIRFLOW_WWW_USER_PASSWORD - Password for the administrator account (if requested). +# Default: airflow +# _PIP_ADDITIONAL_REQUIREMENTS - Additional PIP requirements to add when starting all containers. +# Default: '' +# +# Feel free to modify this file to suit your needs. +--- +version: '3' +x-airflow-common: + &airflow-common + # In order to add custom dependencies or upgrade provider packages you can use your extended image. + # Comment the image line, place your Dockerfile in the directory where you placed the docker-compose.yaml + # and uncomment the "build" line below, Then run `docker-compose build` to build the images. + # image: ${AIRFLOW_IMAGE_NAME:-apache/airflow:2.4.3} + build: . + environment: + &airflow-common-env + AIRFLOW__CORE__EXECUTOR: CeleryExecutor + AIRFLOW__DATABASE__SQL_ALCHEMY_CONN: postgresql+psycopg2://airflow:airflow@postgres/airflow + # For backward compatibility, with Airflow <2.3 + AIRFLOW__CORE__SQL_ALCHEMY_CONN: postgresql+psycopg2://airflow:airflow@postgres/airflow + AIRFLOW__CELERY__RESULT_BACKEND: db+postgresql://airflow:airflow@postgres/airflow + AIRFLOW__CELERY__BROKER_URL: redis://:@redis:6379/0 + AIRFLOW__CORE__FERNET_KEY: '' + AIRFLOW__CORE__DAGS_ARE_PAUSED_AT_CREATION: 'true' + AIRFLOW__CORE__LOAD_EXAMPLES: 'false' + AIRFLOW__API__AUTH_BACKENDS: 'airflow.api.auth.backend.basic_auth' + _PIP_ADDITIONAL_REQUIREMENTS: ${_PIP_ADDITIONAL_REQUIREMENTS:-} + AIRFLOW__CODE_EDITOR__ENABLED: 'true' + AIRFLOW__CODE_EDITOR__GIT_ENABLED: 'false' + AIRFLOW__CODE_EDITOR__STRING_NORMALIZATION: 'true' + AIRFLOW__CODE_EDITOR__MOUNT: '/opt/airflow/dags' + pg_user: airflow + pg_password: airflow + pg_dbname: airflow + pg_host: postgresql+psycopg2://airflow:airflow@postgres/airflow + pg_port: 5432 + PG_TIMEOUT: 30 + PG_POOL: 'true' + volumes: + - ./dags:/opt/airflow/dags + - ./logs:/opt/airflow/logs + - ./plugins:/opt/airflow/plugins + - ./scripts:/opt/airflow/scripts + user: "${AIRFLOW_UID:-50000}:0" + depends_on: + &airflow-common-depends-on + redis: + condition: service_healthy + postgres: + condition: service_healthy + +services: + postgres: + image: postgres:13 + environment: + POSTGRES_USER: airflow + POSTGRES_PASSWORD: airflow + POSTGRES_DB: airflow + volumes: + - postgres-db-volume:/var/lib/postgresql/data + healthcheck: + test: ["CMD", "pg_isready", "-U", "airflow"] + interval: 5s + retries: 5 + restart: always + + redis: + image: redis:latest + expose: + - 6379 + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 30s + retries: 50 + restart: always + + airflow-webserver: + <<: *airflow-common + command: webserver + ports: + - 8080:8080 + healthcheck: + test: ["CMD", "curl", "--fail", "http://localhost:8080/health"] + interval: 10s + timeout: 10s + retries: 5 + restart: always + depends_on: + <<: *airflow-common-depends-on + airflow-init: + condition: service_completed_successfully + + airflow-scheduler: + <<: *airflow-common + command: scheduler + healthcheck: + test: ["CMD-SHELL", 'airflow jobs check --job-type SchedulerJob --hostname "$${HOSTNAME}"'] + interval: 10s + timeout: 10s + retries: 5 + restart: always + depends_on: + <<: *airflow-common-depends-on + airflow-init: + condition: service_completed_successfully + + airflow-worker: + <<: *airflow-common + command: celery worker + healthcheck: + test: + - "CMD-SHELL" + - 'celery --app airflow.executors.celery_executor.app inspect ping -d "celery@$${HOSTNAME}"' + interval: 10s + timeout: 10s + retries: 5 + environment: + <<: *airflow-common-env + # Required to handle warm shutdown of the celery workers properly + # See https://airflow.apache.org/docs/docker-stack/entrypoint.html#signal-propagation + DUMB_INIT_SETSID: "0" + restart: always + depends_on: + <<: *airflow-common-depends-on + airflow-init: + condition: service_completed_successfully + + airflow-triggerer: + <<: *airflow-common + command: triggerer + healthcheck: + test: ["CMD-SHELL", 'airflow jobs check --job-type TriggererJob --hostname "$${HOSTNAME}"'] + interval: 10s + timeout: 10s + retries: 5 + restart: always + depends_on: + <<: *airflow-common-depends-on + airflow-init: + condition: service_completed_successfully + + airflow-init: + <<: *airflow-common + entrypoint: /bin/bash + # yamllint disable rule:line-length + command: + - -c + - | + function ver() { + printf "%04d%04d%04d%04d" $${1//./ } + } + register-python-argcomplete airflow >> ~/.bashrc + airflow_version=$$(AIRFLOW__LOGGING__LOGGING_LEVEL=INFO && gosu airflow airflow version) + airflow_version_comparable=$$(ver $${airflow_version}) + min_airflow_version=2.2.0 + min_airflow_version_comparable=$$(ver $${min_airflow_version}) + if [[ -z "${AIRFLOW_UID}" ]]; then + echo + echo -e "\033[1;33mWARNING!!!: AIRFLOW_UID not set!\e[0m" + echo "If you are on Linux, you SHOULD follow the instructions below to set " + echo "AIRFLOW_UID environment variable, otherwise files will be owned by root." + echo "For other operating systems you can get rid of the warning with manually created .env file:" + echo " See: https://airflow.apache.org/docs/apache-airflow/stable/howto/docker-compose/index.html#setting-the-right-airflow-user" + echo + fi + one_meg=1048576 + mem_available=$$(($$(getconf _PHYS_PAGES) * $$(getconf PAGE_SIZE) / one_meg)) + cpus_available=$$(grep -cE 'cpu[0-9]+' /proc/stat) + disk_available=$$(df / | tail -1 | awk '{print $$4}') + warning_resources="false" + if (( mem_available < 4000 )) ; then + echo + echo -e "\033[1;33mWARNING!!!: Not enough memory available for Docker.\e[0m" + echo "At least 4GB of memory required. You have $$(numfmt --to iec $$((mem_available * one_meg)))" + echo + warning_resources="true" + fi + if (( cpus_available < 2 )); then + echo + echo -e "\033[1;33mWARNING!!!: Not enough CPUS available for Docker.\e[0m" + echo "At least 2 CPUs recommended. You have $${cpus_available}" + echo + warning_resources="true" + fi + if (( disk_available < one_meg * 10 )); then + echo + echo -e "\033[1;33mWARNING!!!: Not enough Disk space available for Docker.\e[0m" + echo "At least 10 GBs recommended. You have $$(numfmt --to iec $$((disk_available * 1024 )))" + echo + warning_resources="true" + fi + if [[ $${warning_resources} == "true" ]]; then + echo + echo -e "\033[1;33mWARNING!!!: You have not enough resources to run Airflow (see above)!\e[0m" + echo "Please follow the instructions to increase amount of resources available:" + echo " https://airflow.apache.org/docs/apache-airflow/stable/howto/docker-compose/index.html#before-you-begin" + echo + fi + mkdir -p /sources/logs /sources/dags /sources/plugins + chown -R "${AIRFLOW_UID}:0" /sources/{logs,dags,plugins} + exec /entrypoint airflow version + # yamllint enable rule:line-length + environment: + <<: *airflow-common-env + _AIRFLOW_DB_UPGRADE: 'true' + _AIRFLOW_WWW_USER_CREATE: 'true' + _AIRFLOW_WWW_USER_USERNAME: ${_AIRFLOW_WWW_USER_USERNAME:-airflow} + _AIRFLOW_WWW_USER_PASSWORD: ${_AIRFLOW_WWW_USER_PASSWORD:-airflow} + _PIP_ADDITIONAL_REQUIREMENTS: '' + user: "0:0" + volumes: + - .:/sources + + airflow-cli: + <<: *airflow-common + profiles: + - debug + environment: + <<: *airflow-common-env + CONNECTION_CHECK_MAX_COUNT: "0" + # Workaround for entrypoint issue. See: https://github.com/apache/airflow/issues/16252 + command: + - bash + - -c + - airflow + + # You can enable flower by adding "--profile flower" option e.g. docker-compose --profile flower up + # or by explicitly targeted on the command line e.g. docker-compose up flower. + # See: https://docs.docker.com/compose/profiles/ + flower: + <<: *airflow-common + command: celery flower + profiles: + - flower + ports: + - 5555:5555 + healthcheck: + test: ["CMD", "curl", "--fail", "http://localhost:5555/"] + interval: 10s + timeout: 10s + retries: 5 + restart: always + depends_on: + <<: *airflow-common-depends-on + airflow-init: + condition: service_completed_successfully + +volumes: + postgres-db-volume: diff --git a/ee/recommendation/requirements.txt b/ee/recommendation/airflow-local/requirements.txt similarity index 55% rename from ee/recommendation/requirements.txt rename to ee/recommendation/airflow-local/requirements.txt index 8f3f9e958..46fe26aa1 100644 --- a/ee/recommendation/requirements.txt +++ b/ee/recommendation/airflow-local/requirements.txt @@ -3,21 +3,17 @@ urllib3==1.26.12 pyjwt==2.5.0 psycopg2-binary==2.9.3 -numpy==1.23.4 +numpy threadpoolctl==3.1.0 joblib==1.2.0 -scipy==1.9.3 -scikit-learn==1.1.3 +scipy +scikit-learn -apache-airflow==2.4.3 +airflow-code-editor -fastapi==0.85.0 -fastapi-utils -uvicorn[standard]==0.18.3 -python-decouple==3.6 pydantic[email]==1.10.2 -apscheduler==3.9.1 clickhouse-driver==0.2.4 python3-saml==1.14.0 python-multipart==0.0.5 +python-decouple diff --git a/ee/recommendation/recommendation.py b/ee/recommendation/airflow-local/scripts/core/recommendation.py similarity index 89% rename from ee/recommendation/recommendation.py rename to ee/recommendation/airflow-local/scripts/core/recommendation.py index f525bdc98..b808c20d5 100644 --- a/ee/recommendation/recommendation.py +++ b/ee/recommendation/airflow-local/scripts/core/recommendation.py @@ -2,6 +2,7 @@ from utils.ch_client import ClickHouseClient from utils.pg_client import PostgresClient def get_features_clickhouse(**kwargs): + """Gets features from ClickHouse database""" if 'limit' in kwargs: limit = kwargs['limit'] else: @@ -16,6 +17,7 @@ ON T1.session_id = T2.session_id AND T1.project_id = T2.project_id;""" def query_funnels(*kwargs): + """Gets Funnels (PG database)""" # If public.funnel is empty funnels_query = f"""SELECT project_id, user_id, filter FROM (SELECT project_id, user_id, metric_id FROM public.metrics WHERE metric_type='funnel' ) as T1 LEFT JOIN (SELECT filter, metric_id FROM public.metric_series) as T2 ON T1.metric_id = T2.metric_id""" @@ -29,6 +31,7 @@ def query_funnels(*kwargs): def query_metrics(*kwargs): + """Gets Metrics (PG_database)""" metrics_query = """SELECT metric_type, metric_of, metric_value, metric_format FROM public.metrics""" with PostgresClient() as conn: conn.execute(metrics_query) @@ -37,9 +40,10 @@ def query_metrics(*kwargs): def query_with_filters(*kwargs): + """Gets Metrics with filters (PG database)""" filters_query = """SELECT T1.metric_id as metric_id, project_id, name, metric_type, metric_of, filter FROM ( - SELECT metric_id, project_id, name, metric_type, metric_of FROM metric_series WHERE filter != '{}') as T1 INNER JOIN - (SELECT metric_id, filter FROM metrics) as T2 ON T1.metric_id = T2.metric_id""" + SELECT metric_id, project_id, name, metric_type, metric_of FROM metrics) as T1 INNER JOIN + (SELECT metric_id, filter FROM metric_series WHERE filter != '{}') as T2 ON T1.metric_id = T2.metric_id""" with PostgresClient() as conn: conn.execute(filters_query) res = conn.fetchall() @@ -104,6 +108,10 @@ def get_by_user(data, user_id): return [data[k] for k in index_] +def test(): + print('One test') + if __name__ == '__main__': - data = get_features_clickhouse() - print('Data length:', len(data)) + print('Just a test') + #data = get_features_clickhouse() + #print('Data length:', len(data)) diff --git a/ee/recommendation/airflow-local/scripts/task.py b/ee/recommendation/airflow-local/scripts/task.py new file mode 100644 index 000000000..b06ebfd0e --- /dev/null +++ b/ee/recommendation/airflow-local/scripts/task.py @@ -0,0 +1,23 @@ +import argparse +from core import recommendation +from utils import pg_client +import asyncio + +#TODO: remove this module +import pandas as pd + +parser = argparse.ArgumentParser(description='Handle machine learning inputs.') +parser.add_argument('--mode', choices=['train', 'test'], required=True, help='--mode sets the model in train or test mode') +parser.add_argument('--kernel', default='linear', help='--kernel set the kernel to be used for SVM') + +args = parser.parse_args() + +if __name__ == '__main__': + asyncio.run(pg_client.init()) + data1 = recommendation.query_funnels() + print(pd.DataFrame(data1)) + data2 = recommendation.query_with_filters() + print(pd.DataFrame(data2)) + data3 = recommendation.query_metrics() + print(pd.DataFrame(data3)) + print(args) diff --git a/ee/recommendation/airflow-local/scripts/utils/ch_client.py b/ee/recommendation/airflow-local/scripts/utils/ch_client.py new file mode 100644 index 000000000..514820212 --- /dev/null +++ b/ee/recommendation/airflow-local/scripts/utils/ch_client.py @@ -0,0 +1,54 @@ +import logging + +import clickhouse_driver +from decouple import config + +logging.basicConfig(level=config("LOGLEVEL", default=logging.INFO)) + +settings = {} +if config('ch_timeout', cast=int, default=-1) > 0: + logging.info(f"CH-max_execution_time set to {config('ch_timeout')}s") + settings = {**settings, "max_execution_time": config('ch_timeout', cast=int)} + +if config('ch_receive_timeout', cast=int, default=-1) > 0: + logging.info(f"CH-receive_timeout set to {config('ch_receive_timeout')}s") + settings = {**settings, "receive_timeout": config('ch_receive_timeout', cast=int)} + + +class ClickHouseClient: + __client = None + + def __init__(self): + self.__client = clickhouse_driver.Client(host=config("ch_host"), + database="default", + port=config("ch_port", cast=int), + settings=settings) \ + if self.__client is None else self.__client + + def __enter__(self): + return self + + def execute(self, query, params=None, **args): + try: + results = self.__client.execute(query=query, params=params, with_column_types=True, **args) + keys = tuple(x for x, y in results[1]) + return [dict(zip(keys, i)) for i in results[0]] + except Exception as err: + logging.error("--------- CH QUERY EXCEPTION -----------") + logging.error(self.format(query=query, params=params)) + logging.error("--------------------") + raise err + + def insert(self, query, params=None, **args): + return self.__client.execute(query=query, params=params, **args) + + def client(self): + return self.__client + + def format(self, query, params): + if params is None: + return query + return self.__client.substitute_params(query, params, self.__client.connection.context) + + def __exit__(self, *args): + pass diff --git a/ee/recommendation/airflow-local/scripts/utils/pg_client.py b/ee/recommendation/airflow-local/scripts/utils/pg_client.py new file mode 100644 index 000000000..69a5b5a8b --- /dev/null +++ b/ee/recommendation/airflow-local/scripts/utils/pg_client.py @@ -0,0 +1,166 @@ +import logging +import time +from threading import Semaphore + +import psycopg2 +import psycopg2.extras +from decouple import config +from psycopg2 import pool + +logging.basicConfig(level=config("LOGLEVEL", default=logging.INFO)) +logging.getLogger('apscheduler').setLevel(config("LOGLEVEL", default=logging.INFO)) + +_PG_CONFIG = {"host": config("pg_host"), + "database": config("pg_dbname"), + "user": config("pg_user"), + "password": config("pg_password"), + "port": config("pg_port", cast=int), + "application_name": config("APP_NAME", default="PY")} +PG_CONFIG = dict(_PG_CONFIG) +if config("PG_TIMEOUT", cast=int, default=0) > 0: + PG_CONFIG["options"] = f"-c statement_timeout={config('PG_TIMEOUT', cast=int) * 1000}" + + +class ORThreadedConnectionPool(psycopg2.pool.ThreadedConnectionPool): + def __init__(self, minconn, maxconn, *args, **kwargs): + self._semaphore = Semaphore(maxconn) + super().__init__(minconn, maxconn, *args, **kwargs) + + def getconn(self, *args, **kwargs): + self._semaphore.acquire() + try: + return super().getconn(*args, **kwargs) + except psycopg2.pool.PoolError as e: + if str(e) == "connection pool is closed": + make_pool() + raise e + + def putconn(self, *args, **kwargs): + try: + super().putconn(*args, **kwargs) + self._semaphore.release() + except psycopg2.pool.PoolError as e: + if str(e) == "trying to put unkeyed connection": + print("!!! trying to put unkeyed connection") + print(f"env-PG_POOL:{config('PG_POOL', default=None)}") + return + raise e + + +postgreSQL_pool: ORThreadedConnectionPool = None + +RETRY_MAX = config("PG_RETRY_MAX", cast=int, default=50) +RETRY_INTERVAL = config("PG_RETRY_INTERVAL", cast=int, default=2) +RETRY = 0 + + +def make_pool(): + if not config('PG_POOL', cast=bool, default=True): + return + global postgreSQL_pool + global RETRY + if postgreSQL_pool is not None: + try: + postgreSQL_pool.closeall() + except (Exception, psycopg2.DatabaseError) as error: + logging.error("Error while closing all connexions to PostgreSQL", error) + try: + postgreSQL_pool = ORThreadedConnectionPool(config("PG_MINCONN", cast=int, default=20), + config("PG_MAXCONN", cast=int, default=80), + **PG_CONFIG) + if (postgreSQL_pool): + logging.info("Connection pool created successfully") + except (Exception, psycopg2.DatabaseError) as error: + logging.error("Error while connecting to PostgreSQL", error) + if RETRY < RETRY_MAX: + RETRY += 1 + logging.info(f"waiting for {RETRY_INTERVAL}s before retry n°{RETRY}") + time.sleep(RETRY_INTERVAL) + make_pool() + else: + raise error + + +class PostgresClient: + connection = None + cursor = None + long_query = False + unlimited_query = False + + def __init__(self, long_query=False, unlimited_query=False): + self.long_query = long_query + self.unlimited_query = unlimited_query + if unlimited_query: + long_config = dict(_PG_CONFIG) + long_config["application_name"] += "-UNLIMITED" + self.connection = psycopg2.connect(**long_config) + elif long_query: + long_config = dict(_PG_CONFIG) + long_config["application_name"] += "-LONG" + long_config["options"] = f"-c statement_timeout=" \ + f"{config('pg_long_timeout', cast=int, default=5 * 60) * 1000}" + self.connection = psycopg2.connect(**long_config) + elif not config('PG_POOL', cast=bool, default=True): + single_config = dict(_PG_CONFIG) + single_config["application_name"] += "-NOPOOL" + single_config["options"] = f"-c statement_timeout={config('PG_TIMEOUT', cast=int, default=30) * 1000}" + self.connection = psycopg2.connect(**single_config) + else: + self.connection = postgreSQL_pool.getconn() + + def __enter__(self): + if self.cursor is None: + self.cursor = self.connection.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + self.cursor.recreate = self.recreate_cursor + return self.cursor + + def __exit__(self, *args): + try: + self.connection.commit() + self.cursor.close() + if self.long_query or self.unlimited_query: + self.connection.close() + except Exception as error: + logging.error("Error while committing/closing PG-connection", error) + if str(error) == "connection already closed" \ + and not self.long_query \ + and not self.unlimited_query \ + and config('PG_POOL', cast=bool, default=True): + logging.info("Recreating the connexion pool") + make_pool() + else: + raise error + finally: + if config('PG_POOL', cast=bool, default=True) \ + and not self.long_query \ + and not self.unlimited_query: + postgreSQL_pool.putconn(self.connection) + + def recreate_cursor(self, rollback=False): + if rollback: + try: + self.connection.rollback() + except Exception as error: + logging.error("Error while rollbacking connection for recreation", error) + try: + self.cursor.close() + except Exception as error: + logging.error("Error while closing cursor for recreation", error) + self.cursor = None + return self.__enter__() + + +async def init(): + logging.info(f">PG_POOL:{config('PG_POOL', default=None)}") + if config('PG_POOL', cast=bool, default=True): + make_pool() + + +async def terminate(): + global postgreSQL_pool + if postgreSQL_pool is not None: + try: + postgreSQL_pool.closeall() + logging.info("Closed all connexions to PostgreSQL") + except (Exception, psycopg2.DatabaseError) as error: + logging.error("Error while closing all connexions to PostgreSQL", error) diff --git a/ee/recommendation/run.sh b/ee/recommendation/run.sh deleted file mode 100644 index e5f70a5a4..000000000 --- a/ee/recommendation/run.sh +++ /dev/null @@ -1,8 +0,0 @@ -mkdir tmp -cp ../api/chalicelib/utils/ch_client.py tmp -cp ../api/chalicelib/utils/events_queue.py tmp -cp ../../api/chalicelib/utils/pg_client.py tmp -docker build -t my_test . -rm tmp/*.py -rmdir tmp -docker run -d -p 8080:8080 my_test diff --git a/ee/recommendation/signals.sql b/ee/recommendation/signals.sql new file mode 100644 index 000000000..5500969ed --- /dev/null +++ b/ee/recommendation/signals.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS frontend_signals +( + project_id bigint NOT NULL, + user_id text NOT NULL, + timestamp bigint NOT NULL, + action text NOT NULL, + source text NOT NULL, + category text NOT NULL, + data json +); +CREATE INDEX IF NOT EXISTS frontend_signals_user_id_idx ON frontend_signals (user_id); From 3699fe5731407eca0276645c0984e47676441a2b Mon Sep 17 00:00:00 2001 From: rjshrjndrn Date: Tue, 29 Nov 2022 10:49:58 +0100 Subject: [PATCH 072/252] chore(cicd): Update frontend build Signed-off-by: rjshrjndrn --- .github/workflows/frontend-dev.yaml | 87 +++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 .github/workflows/frontend-dev.yaml diff --git a/.github/workflows/frontend-dev.yaml b/.github/workflows/frontend-dev.yaml new file mode 100644 index 000000000..17a64df94 --- /dev/null +++ b/.github/workflows/frontend-dev.yaml @@ -0,0 +1,87 @@ +name: Frontend FOSS Deployment +on: + workflow_dispatch: + push: + branches: + - player-refactoring-phase-1 + paths: + - frontend/** +# Disable previous workflows for this action. +concurrency: + group: ${{ github.workflow }} #-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Cache node modules + uses: actions/cache@v1 + with: + path: node_modules + key: ${{ runner.OS }}-build-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.OS }}-build- + ${{ runner.OS }}- + + - name: Docker login + run: | + docker login ${{ secrets.DEV_REGISTRY_URL }} -u ${{ secrets.DEV_DOCKER_USERNAME }} -p "${{ secrets.DEV_REGISTRY_TOKEN }}" + + - uses: azure/k8s-set-context@v1 + with: + method: kubeconfig + kubeconfig: ${{ secrets.DEV_KUBECONFIG }} # Use content of kubeconfig in secret. + id: setcontext + + - name: Building and Pushing frontend image + id: build-image + env: + DOCKER_REPO: ${{ secrets.DEV_REGISTRY_URL }} + IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }} + ENVIRONMENT: staging + run: | + set -x + cd frontend + mv .env.sample .env + docker run --rm -v /etc/passwd:/etc/passwd -u `id -u`:`id -g` -v $(pwd):/home/${USER} -w /home/${USER} --name node_build node:14-stretch-slim /bin/bash -c "yarn && yarn build" + # https://github.com/docker/cli/issues/1134#issuecomment-613516912 + DOCKER_BUILDKIT=1 docker build --target=cicd -t $DOCKER_REPO/frontend:${IMAGE_TAG} . + docker tag $DOCKER_REPO/frontend:${IMAGE_TAG} $DOCKER_REPO/frontend:${IMAGE_TAG}-ee + docker push $DOCKER_REPO/frontend:${IMAGE_TAG} + docker push $DOCKER_REPO/frontend:${IMAGE_TAG}-ee + + - name: Deploy to kubernetes foss + run: | + cd scripts/helmcharts/ + + set -x + cat <>/tmp/image_override.yaml + frontend: + image: + tag: ${IMAGE_TAG} + EOF + + ## Update secerts + sed -i "s#openReplayContainerRegistry.*#openReplayContainerRegistry: \"${{ secrets.OSS_REGISTRY_URL }}\"#g" vars.yaml + sed -i "s/postgresqlPassword: \"changeMePassword\"/postgresqlPassword: \"${{ secrets.DEV_PG_PASSWORD }}\"/g" vars.yaml + sed -i "s/accessKey: \"changeMeMinioAccessKey\"/accessKey: \"${{ secrets.DEV_MINIO_ACCESS_KEY }}\"/g" vars.yaml + sed -i "s/secretKey: \"changeMeMinioPassword\"/secretKey: \"${{ secrets.DEV_MINIO_SECRET_KEY }}\"/g" vars.yaml + sed -i "s/jwt_secret: \"SetARandomStringHere\"/jwt_secret: \"${{ secrets.DEV_JWT_SECRET }}\"/g" vars.yaml + sed -i "s/domainName: \"\"/domainName: \"${{ secrets.DEV_DOMAIN_NAME }}\"/g" vars.yaml + + # Update changed image tag + sed -i "/frontend/{n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml + + cat /tmp/image_override.yaml + # Deploy command + mv openreplay/charts/{ingress-nginx,frontend,quickwit} /tmp + rm -rf openreplay/charts/* + mv /tmp/{ingress-nginx,frontend,quickwit} openreplay/charts/ + helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks | kubectl apply -n app -f - + env: + DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }} + IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }} From cfd24dab5aa322074e009f23b8f812a68ea37036 Mon Sep 17 00:00:00 2001 From: rjshrjndrn Date: Tue, 29 Nov 2022 10:59:41 +0100 Subject: [PATCH 073/252] chore(cicd): Rename workflow Signed-off-by: rjshrjndrn --- .github/workflows/frontend.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/frontend.yaml b/.github/workflows/frontend.yaml index 2f2fd3989..8a9ab31a5 100644 --- a/.github/workflows/frontend.yaml +++ b/.github/workflows/frontend.yaml @@ -1,4 +1,4 @@ -name: Frontend FOSS Deployment +name: Frontend Dev Deployment on: workflow_dispatch: push: From 4715b7a5bbb2b9e2822d0ba980915df13cf5fc1c Mon Sep 17 00:00:00 2001 From: sylenien Date: Tue, 29 Nov 2022 11:05:16 +0100 Subject: [PATCH 074/252] fix(ui): fix store empty state display --- .../app/components/Session_/Storage/Storage.tsx | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/frontend/app/components/Session_/Storage/Storage.tsx b/frontend/app/components/Session_/Storage/Storage.tsx index 0f5e682c5..f955912e7 100644 --- a/frontend/app/components/Session_/Storage/Storage.tsx +++ b/frontend/app/components/Session_/Storage/Storage.tsx @@ -53,7 +53,7 @@ function Storage(props: Props) { const renderDiff = (item: Record, prevItem: Record) => { if (!prevItem) { // we don't have state before first action - return
; + return
; } const stateDiff = diff(prevItem.state, item.state); @@ -106,13 +106,6 @@ function Storage(props: Props) { player.jump(list[listNow.length].time); }; - const renderTab = () => { - if (listNow.length === 0) { - return 'Not initialized'; //? - } - return ; - }; - const renderItem = (item: Record, i: number, prevItem: Record) => { let src; let name; @@ -279,16 +272,16 @@ function Storage(props: Props) { ) : null } size="small" - show={listNow.length === 0} + show={list.length === 0} > {showStore && (
- {listNow.length === 0 ? ( + {list.length === 0 ? (
{'Empty state.'}
) : ( - renderTab() + )}
)} From 7b81fdad5dc61f5bad397b681de5039053402c60 Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Tue, 29 Nov 2022 11:12:41 +0100 Subject: [PATCH 075/252] fix(sourcemap-uploader):3.0.8: upgrade dependencies --- sourcemap-uploader/cli.js | 33 +- sourcemap-uploader/index.js | 2 + sourcemap-uploader/package-lock.json | 1048 -------------------------- sourcemap-uploader/package.json | 8 +- sourcemap-uploader/yarn.lock | 707 +++++++++++++++++ 5 files changed, 730 insertions(+), 1068 deletions(-) delete mode 100644 sourcemap-uploader/package-lock.json create mode 100644 sourcemap-uploader/yarn.lock diff --git a/sourcemap-uploader/cli.js b/sourcemap-uploader/cli.js index 28c167ea3..10977a57a 100755 --- a/sourcemap-uploader/cli.js +++ b/sourcemap-uploader/cli.js @@ -7,48 +7,49 @@ const { version, description } = require('./package.json'); const { uploadFile, uploadDir } = require('./index.js'); const parser = new ArgumentParser({ - version, description, }); -parser.addArgument(['-k', '--api-key'], { +parser.add_argument('-v', '--version', { action: 'version', version }); +parser.add_argument('-k', '--api-key', { help: 'API key', required: true, }); -parser.addArgument(['-p', '-i', '--project-key'], { +parser.add_argument('-p', '-i', '--project-key', { // -i is depricated help: 'Project Key', required: true, }); -parser.addArgument(['-s', '--server'], { +parser.add_argument('-s', '--server', { help: 'OpenReplay API server URL for upload', }); // Should be verbose, but conflicting on npm compilation into bin -parser.addArgument(['-l', '--logs'], { +parser.add_argument('-l', '--logs', { help: 'Log requests information', - action: 'storeTrue', + action: 'store_true', }); -const subparsers = parser.addSubparsers({ +const subparsers = parser.add_subparsers({ title: 'commands', dest: 'command', + required: true, }); -const file = subparsers.addParser('file'); -file.addArgument(['-m', '--sourcemap-file-path'], { +const file = subparsers.add_parser('file'); +file.add_argument('-m', '--sourcemap-file-path', { help: 'Local path to the sourcemap file', required: true, }); -file.addArgument(['-u', '--js-file-url'], { +file.add_argument('-u', '--js-file-url', { help: 'URL to the minified js file', required: true, }); -const dir = subparsers.addParser('dir'); -dir.addArgument(['-m', '--sourcemap-dir-path'], { +const dir = subparsers.add_parser('dir'); +dir.add_argument('-m', '--sourcemap-dir-path', { help: 'Dir with the sourcemap files', required: true, }); -dir.addArgument(['-u', '--js-dir-url'], { +dir.add_argument('-u', '--js-dir-url', { help: 'Base URL where the corresponding dir will be placed', required: true, }); @@ -56,10 +57,10 @@ dir.addArgument(['-u', '--js-dir-url'], { // TODO: exclude in dir const { command, api_key, project_key, server, logs, ...args } = - parser.parseArgs(); + parser.parse_args(); global._VERBOSE = !!logs; - +console.log(command); (command === 'file' ? uploadFile( api_key, @@ -76,7 +77,7 @@ global._VERBOSE = !!logs; server, ) ) - .then((sourceFiles) => + .then((sourceFiles) => console.log('asd') || sourceFiles.length > 0 ? console.log( `Successfully uploaded ${sourceFiles.length} sourcemap file${ diff --git a/sourcemap-uploader/index.js b/sourcemap-uploader/index.js index dabbee434..5874ebd96 100644 --- a/sourcemap-uploader/index.js +++ b/sourcemap-uploader/index.js @@ -21,6 +21,8 @@ module.exports = { server, ) { const sourcemaps = await readDir(sourcemap_dir_path, js_dir_url); + console.log(sourcemaps) + return uploadSourcemaps(api_key, project_key, sourcemaps, server); }, }; diff --git a/sourcemap-uploader/package-lock.json b/sourcemap-uploader/package-lock.json deleted file mode 100644 index 0b38bb293..000000000 --- a/sourcemap-uploader/package-lock.json +++ /dev/null @@ -1,1048 +0,0 @@ -{ - "name": "@openreplay/sourcemap-uploader", - "version": "3.0.6", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.15.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", - "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", - "dev": true - }, - "@babel/highlight": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", - "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.5", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - } - } - }, - "@eslint/eslintrc": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", - "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.1.1", - "espree": "^7.3.0", - "globals": "^13.9.0", - "ignore": "^4.0.6", - "import-fresh": "^3.2.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", - "strip-json-comments": "^3.1.1" - } - }, - "@humanwhocodes/config-array": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", - "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.0", - "debug": "^4.1.1", - "minimatch": "^3.0.4" - } - }, - "@humanwhocodes/object-schema": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz", - "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", - "dev": true - }, - "@types/events": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", - "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==" - }, - "@types/glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", - "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", - "requires": { - "@types/events": "*", - "@types/minimatch": "*", - "@types/node": "*" - } - }, - "@types/minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==" - }, - "@types/node": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.0.tgz", - "integrity": "sha512-GnZbirvmqZUzMgkFn70c74OQpTTUcCzlhQliTzYjQMqg+hVKcDnxdL19Ne3UdYzdMA/+W3eb646FWn/ZaT1NfQ==" - }, - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true - }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "dev": true, - "requires": { - "ansi-colors": "^4.1.1" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true - }, - "eslint": { - "version": "7.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", - "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", - "dev": true, - "requires": { - "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.3", - "@humanwhocodes/config-array": "^0.5.0", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "enquirer": "^2.3.5", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.1", - "esquery": "^1.4.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.1.2", - "globals": "^13.6.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.1.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.0", - "strip-json-comments": "^3.1.0", - "table": "^6.0.9", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - } - }, - "eslint-config-prettier": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz", - "integrity": "sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==", - "dev": true - }, - "eslint-plugin-prettier": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.0.0.tgz", - "integrity": "sha512-98MqmCJ7vJodoQK359bqQWaxOE0CS8paAz/GgjaZLyex4TTk3g9HugoO89EqWCrFiOqn9EVvcoo7gZzONCWVwQ==", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - } - } - }, - "eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true - }, - "espree": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", - "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", - "dev": true, - "requires": { - "acorn": "^7.4.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^1.3.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - } - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true - } - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true - } - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", - "dev": true - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.2.tgz", - "integrity": "sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "glob-promise": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/glob-promise/-/glob-promise-3.4.0.tgz", - "integrity": "sha512-q08RJ6O+eJn+dVanerAndJwIcumgbDdYiUT7zFQl3Wm1xD6fBKtah7H8ZJChj4wP+8C+QfeVy8xautR7rdmKEw==", - "requires": { - "@types/glob": "*" - } - }, - "globals": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.11.0.tgz", - "integrity": "sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", - "dev": true - }, - "lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "lodash.truncate": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true - }, - "prettier": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz", - "integrity": "sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true - }, - "require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - } - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "table": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/table/-/table-6.7.1.tgz", - "integrity": "sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg==", - "dev": true, - "requires": { - "ajv": "^8.0.1", - "lodash.clonedeep": "^4.5.0", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ajv": { - "version": "8.6.3", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.3.tgz", - "integrity": "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - } - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", - "dev": true - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } -} diff --git a/sourcemap-uploader/package.json b/sourcemap-uploader/package.json index ee495605e..5c4efa97c 100644 --- a/sourcemap-uploader/package.json +++ b/sourcemap-uploader/package.json @@ -1,6 +1,6 @@ { "name": "@openreplay/sourcemap-uploader", - "version": "3.0.7", + "version": "3.0.8", "description": "NPM module to upload your JS sourcemaps files to OpenReplay", "bin": "cli.js", "main": "index.js", @@ -13,13 +13,13 @@ ], "license": "MIT", "devDependencies": { - "eslint": "^7.32.0", + "eslint": "^8.28.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^4.0.0", "prettier": "^2.4.1" }, "dependencies": { - "argparse": "^1.0.10", - "glob-promise": "^3.4.0" + "argparse": "^2.0.1", + "glob-promise": "^5.0.0" } } diff --git a/sourcemap-uploader/yarn.lock b/sourcemap-uploader/yarn.lock new file mode 100644 index 000000000..a2f7fdeea --- /dev/null +++ b/sourcemap-uploader/yarn.lock @@ -0,0 +1,707 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@eslint/eslintrc@^1.3.3": + version "1.3.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.3.tgz#2b044ab39fdfa75b4688184f9e573ce3c5b0ff95" + integrity sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.4.0" + globals "^13.15.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@humanwhocodes/config-array@^0.11.6": + version "0.11.7" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.7.tgz#38aec044c6c828f6ed51d5d7ae3d9b9faf6dbb0f" + integrity sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw== + dependencies: + "@humanwhocodes/object-schema" "^1.2.1" + debug "^4.1.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@types/glob@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb" + integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA== + dependencies: + "@types/minimatch" "*" + "@types/node" "*" + +"@types/minimatch@*": + version "5.1.2" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" + integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== + +"@types/node@*": + version "18.11.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.9.tgz#02d013de7058cea16d36168ef2fc653464cfbad4" + integrity sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg== + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.8.0: + version "8.8.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" + integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== + +ajv@^6.10.0, ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +cross-spawn@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +debug@^4.1.1, debug@^4.3.2: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-config-prettier@^8.3.0: + version "8.5.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz#5a81680ec934beca02c7b1a61cf8ca34b66feab1" + integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q== + +eslint-plugin-prettier@^4.0.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz#651cbb88b1dab98bfd42f017a12fa6b2d993f94b" + integrity sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ== + dependencies: + prettier-linter-helpers "^1.0.0" + +eslint-scope@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642" + integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" + integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== + dependencies: + eslint-visitor-keys "^2.0.0" + +eslint-visitor-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" + integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== + +eslint-visitor-keys@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" + integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== + +eslint@^8.28.0: + version "8.28.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.28.0.tgz#81a680732634677cc890134bcdd9fdfea8e63d6e" + integrity sha512-S27Di+EVyMxcHiwDrFzk8dJYAaD+/5SoWKxL1ri/71CRHsnJnRDPNt2Kzj24+MT9FDupf4aqqyqPrvI8MvQ4VQ== + dependencies: + "@eslint/eslintrc" "^1.3.3" + "@humanwhocodes/config-array" "^0.11.6" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.1.1" + eslint-utils "^3.0.0" + eslint-visitor-keys "^3.3.0" + espree "^9.4.0" + esquery "^1.4.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.15.0" + grapheme-splitter "^1.0.4" + ignore "^5.2.0" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-sdsl "^4.1.4" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.1" + regexpp "^3.2.0" + strip-ansi "^6.0.1" + strip-json-comments "^3.1.0" + text-table "^0.2.0" + +espree@^9.4.0: + version "9.4.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.4.1.tgz#51d6092615567a2c2cff7833445e37c28c0065bd" + integrity sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg== + dependencies: + acorn "^8.8.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.3.0" + +esquery@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" + integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-diff@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" + integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fastq@^1.6.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" + integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== + dependencies: + reusify "^1.0.4" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + +flatted@^3.1.0: + version "3.2.7" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" + integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob-promise@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/glob-promise/-/glob-promise-5.0.0.tgz#aee3317ee1c211866d0425e1ac2e9089ded09c28" + integrity sha512-YQ7+beqmmQ3yUzybHoxnj7XMImztIdusIFate36/aB1gEWugR1qZI9a2u1A5mt6gYRqYldipZ7NB9s1UEq3vzw== + dependencies: + "@types/glob" "^7.2.0" + +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^13.15.0: + version "13.18.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.18.0.tgz#fb224daeeb2bb7d254cd2c640f003528b8d0c1dc" + integrity sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A== + dependencies: + type-fest "^0.20.2" + +grapheme-splitter@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" + integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +ignore@^5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.1.tgz#c2b1f76cb999ede1502f3a226a9310fdfe88d46c" + integrity sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA== + +import-fresh@^3.0.0, import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.0, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +js-sdsl@^4.1.4: + version "4.2.0" + resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.2.0.tgz#278e98b7bea589b8baaf048c20aeb19eb7ad09d0" + integrity sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prettier-linter-helpers@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" + integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== + dependencies: + fast-diff "^1.1.2" + +prettier@^2.4.1: + version "2.8.0" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.0.tgz#c7df58393c9ba77d6fba3921ae01faf994fb9dc9" + integrity sha512-9Lmg8hTFZKG0Asr/kW9Bp8tJjRVluO8EJQVfY2T7FMw9T5jy4I/Uvx0Rca/XWf50QQ1/SS48+6IJWnrb+2yemA== + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +regexpp@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" + integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== From 77536c31532b56745104e08cbe2511493146fa60 Mon Sep 17 00:00:00 2001 From: MauricioGarciaS <47052044+MauricioGarciaS@users.noreply.github.com> Date: Tue, 29 Nov 2022 11:17:59 +0100 Subject: [PATCH 076/252] clened recommendation files --- .../{airflow-local => }/Dockerfile | 0 ee/recommendation/api.py | 42 ------------------- ee/recommendation/clean.sh | 1 + .../{airflow-local => }/dags/training_dag.py | 0 .../{airflow-local => }/docker-compose.yaml | 0 .../{airflow-local => }/requirements.txt | 0 ee/recommendation/run.sh | 11 +++++ .../scripts/core/recommendation.py | 0 .../{airflow-local => }/scripts/task.py | 0 .../scripts/utils/ch_client.py | 0 .../scripts/utils/pg_client.py | 0 11 files changed, 12 insertions(+), 42 deletions(-) rename ee/recommendation/{airflow-local => }/Dockerfile (100%) delete mode 100644 ee/recommendation/api.py create mode 100644 ee/recommendation/clean.sh rename ee/recommendation/{airflow-local => }/dags/training_dag.py (100%) rename ee/recommendation/{airflow-local => }/docker-compose.yaml (100%) rename ee/recommendation/{airflow-local => }/requirements.txt (100%) create mode 100644 ee/recommendation/run.sh rename ee/recommendation/{airflow-local => }/scripts/core/recommendation.py (100%) rename ee/recommendation/{airflow-local => }/scripts/task.py (100%) rename ee/recommendation/{airflow-local => }/scripts/utils/ch_client.py (100%) rename ee/recommendation/{airflow-local => }/scripts/utils/pg_client.py (100%) diff --git a/ee/recommendation/airflow-local/Dockerfile b/ee/recommendation/Dockerfile similarity index 100% rename from ee/recommendation/airflow-local/Dockerfile rename to ee/recommendation/Dockerfile diff --git a/ee/recommendation/api.py b/ee/recommendation/api.py deleted file mode 100644 index f9d3e05bf..000000000 --- a/ee/recommendation/api.py +++ /dev/null @@ -1,42 +0,0 @@ -import logging -from apscheduler.schedulers.asyncio import AsyncIOScheduler -from fastapi import FastAPI -# from fastapi_utils.tasks import repeat_every -from utils import events_queue -from utils import pg_client -from utils import schemas_ee - -app = FastAPI() -app.schedule = AsyncIOScheduler() - -@app.get('/') -def home(): - return '

This is a title

' - - -@app.get('/value/{value}') -@app.put('/value/{value}') -def number(value: int): - logging.info(f'> {value} as input. Testing queue with pg') - d = {'timestamp': 23786, 'action': 'action', 'source': 'source', 'category': 'cat', 'data': {}} - events = schemas_ee.SignalsSchema - event = events.parse_obj(d) - events_queue.global_queue.put((value, 0, event)) - - -@app.on_event("startup") -async def startup(): - await pg_client.init() - await events_queue.init(test=False) - app.schedule.start() - - -@app.schedule.scheduled_job("interval", seconds=60*1) -def clean_up(): - events_queue.global_queue.force_flush() - - -@app.on_event("shutdown") -async def shutdown(): - await events_queue.terminate() - await pg_client.terminate() diff --git a/ee/recommendation/clean.sh b/ee/recommendation/clean.sh new file mode 100644 index 000000000..857c8d63d --- /dev/null +++ b/ee/recommendation/clean.sh @@ -0,0 +1 @@ +docker-compose down --volumes --rmi all diff --git a/ee/recommendation/airflow-local/dags/training_dag.py b/ee/recommendation/dags/training_dag.py similarity index 100% rename from ee/recommendation/airflow-local/dags/training_dag.py rename to ee/recommendation/dags/training_dag.py diff --git a/ee/recommendation/airflow-local/docker-compose.yaml b/ee/recommendation/docker-compose.yaml similarity index 100% rename from ee/recommendation/airflow-local/docker-compose.yaml rename to ee/recommendation/docker-compose.yaml diff --git a/ee/recommendation/airflow-local/requirements.txt b/ee/recommendation/requirements.txt similarity index 100% rename from ee/recommendation/airflow-local/requirements.txt rename to ee/recommendation/requirements.txt diff --git a/ee/recommendation/run.sh b/ee/recommendation/run.sh new file mode 100644 index 000000000..0a703bca4 --- /dev/null +++ b/ee/recommendation/run.sh @@ -0,0 +1,11 @@ +echo 'Setting up required modules..' +mkdir scripts +mkdir plugins +mkdir logs +mkdir scripts/utils +cp ../../api/chalicelib/utils/pg_client.py scripts/utils +cp ../api/chalicelib/utils/ch_client.py scripts/utils +echo 'Building containers...' +docker-compose up airflow-init +echo 'Running containers...' +docker-compose up diff --git a/ee/recommendation/airflow-local/scripts/core/recommendation.py b/ee/recommendation/scripts/core/recommendation.py similarity index 100% rename from ee/recommendation/airflow-local/scripts/core/recommendation.py rename to ee/recommendation/scripts/core/recommendation.py diff --git a/ee/recommendation/airflow-local/scripts/task.py b/ee/recommendation/scripts/task.py similarity index 100% rename from ee/recommendation/airflow-local/scripts/task.py rename to ee/recommendation/scripts/task.py diff --git a/ee/recommendation/airflow-local/scripts/utils/ch_client.py b/ee/recommendation/scripts/utils/ch_client.py similarity index 100% rename from ee/recommendation/airflow-local/scripts/utils/ch_client.py rename to ee/recommendation/scripts/utils/ch_client.py diff --git a/ee/recommendation/airflow-local/scripts/utils/pg_client.py b/ee/recommendation/scripts/utils/pg_client.py similarity index 100% rename from ee/recommendation/airflow-local/scripts/utils/pg_client.py rename to ee/recommendation/scripts/utils/pg_client.py From 8a8cf4d9381f07218dfe145d632845964fa240e2 Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 29 Nov 2022 11:34:39 +0100 Subject: [PATCH 077/252] feat(backend/sink): manually sync all files on partitions rebalanced event (#847) --- backend/cmd/sink/main.go | 9 +++ backend/go.mod | 4 +- backend/go.sum | 15 ++--- backend/internal/sink/sessionwriter/writer.go | 16 +++-- backend/pkg/queue/types/types.go | 1 + backend/pkg/redisstream/consumer.go | 6 ++ ee/backend/pkg/kafka/consumer.go | 62 ++++++++++++------- ee/backend/pkg/kafka/log.go | 7 +-- ee/backend/pkg/kafka/producer.go | 2 +- 9 files changed, 76 insertions(+), 46 deletions(-) diff --git a/backend/cmd/sink/main.go b/backend/cmd/sink/main.go index a7e2804c4..fcd78c3aa 100644 --- a/backend/cmd/sink/main.go +++ b/backend/cmd/sink/main.go @@ -147,6 +147,15 @@ func main() { case <-tickInfo: counter.Print() log.Printf("writer: %s", writer.Info()) + case <-consumer.Rebalanced(): + s := time.Now() + // Commit now to avoid duplicate reads + if err := consumer.Commit(); err != nil { + log.Printf("can't commit messages: %s", err) + } + // Sync all files + writer.Sync() + log.Printf("manual sync finished, dur: %d", time.Now().Sub(s).Milliseconds()) default: err := consumer.ConsumeNext() if err != nil { diff --git a/backend/go.mod b/backend/go.mod index b1046b08e..bc0f1896f 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -8,6 +8,7 @@ require ( github.com/Masterminds/semver v1.5.0 github.com/aws/aws-sdk-go v1.44.98 github.com/btcsuite/btcutil v1.0.2 + github.com/confluentinc/confluent-kafka-go v1.8.2 github.com/elastic/go-elasticsearch/v7 v7.13.1 github.com/go-redis/redis v6.15.9+incompatible github.com/google/uuid v1.3.0 @@ -28,7 +29,6 @@ require ( go.opentelemetry.io/otel/sdk/metric v0.30.0 golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 google.golang.org/api v0.81.0 - gopkg.in/confluentinc/confluent-kafka-go.v1 v1.8.2 ) require ( @@ -38,7 +38,6 @@ require ( cloud.google.com/go/storage v1.14.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect - github.com/confluentinc/confluent-kafka-go v1.9.0 // indirect github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -53,7 +52,6 @@ require ( github.com/jackc/puddle v1.2.2-0.20220404125616-4e959849469a // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/klauspost/compress v1.15.7 // indirect - github.com/kr/pretty v0.3.0 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/paulmach/orb v0.7.1 // indirect github.com/pierrec/lz4/v4 v4.1.15 // indirect diff --git a/backend/go.sum b/backend/go.sum index fea2aa1a3..cef80ac2c 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -115,12 +115,11 @@ github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/confluentinc/confluent-kafka-go v1.9.0 h1:d1k62oAuQVxgdMdiDQnpkABbtIWTBwXHpDcyGQUw5QQ= -github.com/confluentinc/confluent-kafka-go v1.9.0/go.mod h1:WDFs+KlhHITEoCzEfHSNgj5aP7vjajyYbZpvTEGs1sE= +github.com/confluentinc/confluent-kafka-go v1.8.2 h1:PBdbvYpyOdFLehj8j+9ba7FL4c4Moxn79gy9cYKxG5E= +github.com/confluentinc/confluent-kafka-go v1.8.2/go.mod h1:u2zNLny2xq+5rWeTQjFHbDzzNuba4P1vo31r9r4uAdg= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -328,14 +327,12 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= @@ -400,8 +397,6 @@ github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0 github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= @@ -941,8 +936,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/confluentinc/confluent-kafka-go.v1 v1.8.2 h1:QAgN6OC0o7dwvyz+HML6GYm+0Pk54O91+oxGqJ/5z8I= -gopkg.in/confluentinc/confluent-kafka-go.v1 v1.8.2/go.mod h1:ZdI3yfYmdNSLQPNCpO1y00EHyWaHG5EnQEyL/ntAegY= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= diff --git a/backend/internal/sink/sessionwriter/writer.go b/backend/internal/sink/sessionwriter/writer.go index f2eb052c7..7da1ae878 100644 --- a/backend/internal/sink/sessionwriter/writer.go +++ b/backend/internal/sink/sessionwriter/writer.go @@ -97,17 +97,21 @@ func (w *SessionWriter) Info() string { return fmt.Sprintf("%d sessions", w.meta.Count()) } +func (w *SessionWriter) Sync() { + w.sessions.Range(func(sid, lockObj any) bool { + if err := w.sync(sid.(uint64)); err != nil { + log.Printf("can't sync file descriptor: %s", err) + } + return true + }) +} + func (w *SessionWriter) synchronizer() { tick := time.Tick(w.syncTimeout) for { select { case <-tick: - w.sessions.Range(func(sid, lockObj any) bool { - if err := w.sync(sid.(uint64)); err != nil { - log.Printf("can't sync file descriptor: %s", err) - } - return true - }) + w.Sync() case <-w.done: w.sessions.Range(func(sid, lockObj any) bool { if err := w.Close(sid.(uint64)); err != nil { diff --git a/backend/pkg/queue/types/types.go b/backend/pkg/queue/types/types.go index 48408ce10..21ee49d60 100644 --- a/backend/pkg/queue/types/types.go +++ b/backend/pkg/queue/types/types.go @@ -6,6 +6,7 @@ type Consumer interface { CommitBack(gap int64) error Commit() error Close() + Rebalanced() <-chan interface{} } // Producer sends batches of session data to queue (redis or kafka) diff --git a/backend/pkg/redisstream/consumer.go b/backend/pkg/redisstream/consumer.go index 228b2c7a0..3c5b6d0a4 100644 --- a/backend/pkg/redisstream/consumer.go +++ b/backend/pkg/redisstream/consumer.go @@ -27,6 +27,7 @@ type Consumer struct { idsPending streamPendingIDsMap lastTs int64 autoCommit bool + event chan interface{} } func NewConsumer(group string, streams []string, messageIterator messages.MessageIterator) *Consumer { @@ -57,11 +58,16 @@ func NewConsumer(group string, streams []string, messageIterator messages.Messag group: group, autoCommit: true, idsPending: idsPending, + event: make(chan interface{}, 4), } } const READ_COUNT = 10 +func (c *Consumer) Rebalanced() <-chan interface{} { + return c.event +} + func (c *Consumer) ConsumeNext() error { // MBTODO: read in go routine, send messages to channel res, err := c.redis.XReadGroup(&_redis.XReadGroupArgs{ diff --git a/ee/backend/pkg/kafka/consumer.go b/ee/backend/pkg/kafka/consumer.go index 14f8d5a68..bea1f0604 100644 --- a/ee/backend/pkg/kafka/consumer.go +++ b/ee/backend/pkg/kafka/consumer.go @@ -2,24 +2,24 @@ package kafka import ( "log" - "openreplay/backend/pkg/messages" "os" "time" - "github.com/pkg/errors" - - "gopkg.in/confluentinc/confluent-kafka-go.v1/kafka" "openreplay/backend/pkg/env" + "openreplay/backend/pkg/messages" + + "github.com/confluentinc/confluent-kafka-go/kafka" + "github.com/pkg/errors" ) type Message = kafka.Message type Consumer struct { - c *kafka.Consumer - messageIterator messages.MessageIterator - commitTicker *time.Ticker - pollTimeout uint - + c *kafka.Consumer + messageIterator messages.MessageIterator + commitTicker *time.Ticker + pollTimeout uint + events chan interface{} lastReceivedPrtTs map[int32]int64 } @@ -48,7 +48,7 @@ func NewConsumer( kafkaConfig.SetKey("ssl.certificate.location", os.Getenv("KAFKA_SSL_CERT")) } - // Apply Kerberos configuration + // Apply Kerberos configuration if env.Bool("KAFKA_USE_KERBEROS") { kafkaConfig.SetKey("security.protocol", "sasl_plaintext") kafkaConfig.SetKey("sasl.mechanisms", "GSSAPI") @@ -61,6 +61,21 @@ func NewConsumer( if err != nil { log.Fatalln(err) } + + var commitTicker *time.Ticker + if autoCommit { + commitTicker = time.NewTicker(2 * time.Minute) + } + + consumer := &Consumer{ + c: c, + messageIterator: messageIterator, + commitTicker: commitTicker, + pollTimeout: 200, + events: make(chan interface{}, 4), + lastReceivedPrtTs: make(map[int32]int64, 16), + } + subREx := "^(" for i, t := range topics { if i != 0 { @@ -69,22 +84,27 @@ func NewConsumer( subREx += t } subREx += ")$" - if err := c.Subscribe(subREx, nil); err != nil { + if err := c.Subscribe(subREx, consumer.reBalanceCallback); err != nil { log.Fatalln(err) } - var commitTicker *time.Ticker - if autoCommit { - commitTicker = time.NewTicker(2 * time.Minute) - } + return consumer +} - return &Consumer{ - c: c, - messageIterator: messageIterator, - commitTicker: commitTicker, - pollTimeout: 200, - lastReceivedPrtTs: make(map[int32]int64), +func (consumer *Consumer) reBalanceCallback(_ *kafka.Consumer, e kafka.Event) error { + switch evt := e.(type) { + case kafka.RevokedPartitions: + // receive before re-balancing partitions; stop consuming messages and commit current state + consumer.events <- evt.String() + case kafka.AssignedPartitions: + // receive after re-balancing partitions; continue consuming messages + //consumer.events <- evt.String() } + return nil +} + +func (consumer *Consumer) Rebalanced() <-chan interface{} { + return consumer.events } func (consumer *Consumer) Commit() error { diff --git a/ee/backend/pkg/kafka/log.go b/ee/backend/pkg/kafka/log.go index 0cd80cb6d..c71c6d2bd 100644 --- a/ee/backend/pkg/kafka/log.go +++ b/ee/backend/pkg/kafka/log.go @@ -1,16 +1,15 @@ package kafka import ( - "log" "fmt" + "log" - "gopkg.in/confluentinc/confluent-kafka-go.v1/kafka" + "github.com/confluentinc/confluent-kafka-go/kafka" ) - func logPartitions(s string, prts []kafka.TopicPartition) { for _, p := range prts { s = fmt.Sprintf("%v | %v", s, p.Partition) } log.Println(s) -} \ No newline at end of file +} diff --git a/ee/backend/pkg/kafka/producer.go b/ee/backend/pkg/kafka/producer.go index f895241a7..1ec241b8a 100644 --- a/ee/backend/pkg/kafka/producer.go +++ b/ee/backend/pkg/kafka/producer.go @@ -5,7 +5,7 @@ import ( "log" "os" - "gopkg.in/confluentinc/confluent-kafka-go.v1/kafka" + "github.com/confluentinc/confluent-kafka-go/kafka" "openreplay/backend/pkg/env" ) From 90f88abdb487c5c0fdda8c7387951d60e114bf2b Mon Sep 17 00:00:00 2001 From: Alexander Zavorotynskiy Date: Tue, 29 Nov 2022 11:44:58 +0100 Subject: [PATCH 078/252] feat(backend): fix vulnerability in /x/net --- backend/cmd/sink/main.go | 3 --- backend/go.mod | 4 ++-- backend/go.sum | 7 ++++--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/backend/cmd/sink/main.go b/backend/cmd/sink/main.go index fcd78c3aa..03f11b200 100644 --- a/backend/cmd/sink/main.go +++ b/backend/cmd/sink/main.go @@ -14,14 +14,11 @@ import ( "openreplay/backend/internal/storage" "openreplay/backend/pkg/messages" "openreplay/backend/pkg/monitoring" - "openreplay/backend/pkg/pprof" "openreplay/backend/pkg/queue" "openreplay/backend/pkg/url/assets" ) func main() { - pprof.StartProfilingServer() - metrics := monitoring.New("sink") log.SetFlags(log.LstdFlags | log.LUTC | log.Llongfile) diff --git a/backend/go.mod b/backend/go.mod index bc0f1896f..61d644a17 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -27,7 +27,7 @@ require ( go.opentelemetry.io/otel/exporters/prometheus v0.30.0 go.opentelemetry.io/otel/metric v0.30.0 go.opentelemetry.io/otel/sdk/metric v0.30.0 - golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 + golang.org/x/net v0.0.0-20220906165146-f3363e06e74c google.golang.org/api v0.81.0 ) @@ -67,7 +67,7 @@ require ( golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 // indirect - golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect + golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect golang.org/x/text v0.4.0 // indirect golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect google.golang.org/appengine v1.6.7 // indirect diff --git a/backend/go.sum b/backend/go.sum index cef80ac2c..c7abea25e 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -561,8 +561,9 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 h1:NWy5+hlRbC7HK+PmcXVUmW1IMyFce7to56IUvhUFm7Y= golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220906165146-f3363e06e74c h1:yKufUcDwucU5urd+50/Opbt4AYpqthk7wHpHok8f1lo= +golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -674,8 +675,8 @@ golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220429233432-b5fbb4746d32/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From d44af97efda5ec927af4ad6a3e3caa9c17732ea2 Mon Sep 17 00:00:00 2001 From: sylenien Date: Tue, 29 Nov 2022 12:07:57 +0100 Subject: [PATCH 079/252] fix(ui): fix console row expand method --- .../app/components/shared/DevTools/ConsoleRow/ConsoleRow.tsx | 4 ++-- .../components/shared/DevTools/NetworkPanel/NetworkPanel.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/app/components/shared/DevTools/ConsoleRow/ConsoleRow.tsx b/frontend/app/components/shared/DevTools/ConsoleRow/ConsoleRow.tsx index 0aa9b801f..777771de9 100644 --- a/frontend/app/components/shared/DevTools/ConsoleRow/ConsoleRow.tsx +++ b/frontend/app/components/shared/DevTools/ConsoleRow/ConsoleRow.tsx @@ -49,13 +49,13 @@ function ConsoleRow(props: Props) { )} {renderWithNL(lines.pop())}
- {/* {canExpand && + {canExpand && expanded && lines.map((l: string, i: number) => (
{l}
- ))} */} + ))}
jump(log.time)} />
diff --git a/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx b/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx index 17712a295..1eef37f87 100644 --- a/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx +++ b/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { observer } from 'mobx-react-lite'; import { Duration } from 'luxon'; From f6ee3f2292ec97488b40a934fd247ccf90a440df Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Tue, 29 Nov 2022 12:27:50 +0100 Subject: [PATCH 080/252] fix(frontend): autoscroll index: use filtered list --- .../DevTools/ConsolePanel/ConsolePanel.tsx | 11 ++-- .../DevTools/NetworkPanel/NetworkPanel.tsx | 20 +++--- .../StackEventPanel/StackEventPanel.tsx | 7 ++- .../shared/DevTools/useAutoscroll.ts | 61 ++++++++++++------- frontend/app/player/index.ts | 1 + 5 files changed, 64 insertions(+), 36 deletions(-) diff --git a/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx b/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx index ee4a4b78b..0d59f6648 100644 --- a/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx +++ b/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx @@ -86,13 +86,14 @@ function ConsolePanel() { const onFilterChange = ({ target: { value } }: any) => devTools.update(INDEX_KEY, { filter: value }) // AutoScroll - const autoScrollIndex = logListNow.length + exceptionsListNow.length - const { + const countNow = logListNow.length + exceptionsListNow.length + const [ timeoutStartAutoscroll, stopAutoscroll, - } = useAutoscroll( + ] = useAutoscroll( + filteredList, + list[countNow].time, activeIndex, - autoScrollIndex, index => devTools.update(INDEX_KEY, { index }) ) const onMouseEnter = stopAutoscroll @@ -101,7 +102,7 @@ function ConsolePanel() { timeoutStartAutoscroll() } - const _list = useRef(); + const _list = useRef(); // TODO: fix react-virtualized types & incapsulate scrollToRow logic useEffect(() => { if (_list.current) { // @ts-ignore diff --git a/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx b/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx index 1eef37f87..7b0c01230 100644 --- a/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx +++ b/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx @@ -152,16 +152,20 @@ function NetworkPanel() { const activeTab = devTools[INDEX_KEY].activeTab; const activeIndex = devTools[INDEX_KEY].index; - const list = useMemo(() => - resourceList.filter(res => !fetchList.some(ft => { + const { list, intersectedCount } = useMemo(() => { + let intersectedCount = 0 + const list = resourceList.filter(res => !fetchList.some(ft => { if (res.url !== ft.url) { return false } if (Math.abs(res.time - ft.time) > 200) { return false } // TODO: find good epsilons if (Math.abs(res.duration - ft.duration) > 100) { return false } + intersectedCount++ return true })) .concat(fetchList) .sort((a, b) => a.time - b.time) - , [ resourceList.length, fetchList.length ]) + return { list, intersectedCount } + }, [ resourceList.length, fetchList.length ]) + let filteredList = useMemo(() => { if (!showOnlyErrors) { return list } return list.filter(it => parseInt(it.status) >= 400 || !it.success) @@ -174,14 +178,16 @@ function NetworkPanel() { const onTabClick = (activeTab: typeof TAP_KEYS[number]) => devTools.update(INDEX_KEY, { activeTab }) const onFilterChange = ({ target: { value } }: React.ChangeEvent) => devTools.update(INDEX_KEY, { filter: value }) + // AutoScroll - const autoScrollIndex = fetchListNow.length + resourceListNow.length - const { + const countNow = fetchListNow.length + resourceListNow.length - intersectedCount + const [ timeoutStartAutoscroll, stopAutoscroll, - } = useAutoscroll( + ] = useAutoscroll( + filteredList, + list[countNow].time, activeIndex, - autoScrollIndex, index => devTools.update(INDEX_KEY, { index }) ) const onMouseEnter = stopAutoscroll diff --git a/frontend/app/components/shared/DevTools/StackEventPanel/StackEventPanel.tsx b/frontend/app/components/shared/DevTools/StackEventPanel/StackEventPanel.tsx index 90dcdb218..70916bcb5 100644 --- a/frontend/app/components/shared/DevTools/StackEventPanel/StackEventPanel.tsx +++ b/frontend/app/components/shared/DevTools/StackEventPanel/StackEventPanel.tsx @@ -44,12 +44,13 @@ function StackEventPanel() { [ list.length ], ) - const { + const [ timeoutStartAutoscroll, stopAutoscroll, - } = useAutoscroll( + ] = useAutoscroll( + filteredList, + listNow[listNow.length-1].time, activeIndex, - listNow.length, index => devTools.update(INDEX_KEY, { index }) ) const onMouseEnter = stopAutoscroll diff --git a/frontend/app/components/shared/DevTools/useAutoscroll.ts b/frontend/app/components/shared/DevTools/useAutoscroll.ts index 64883beec..5414edf42 100644 --- a/frontend/app/components/shared/DevTools/useAutoscroll.ts +++ b/frontend/app/components/shared/DevTools/useAutoscroll.ts @@ -1,42 +1,61 @@ -import { useEffect, useState, useRef } from 'react' +import { useEffect, useState, useRef, useMemo } from 'react' +import { Timed } from 'Player' import useLatestRef from 'App/hooks/useLatestRef' import useCancelableTimeout from 'App/hooks/useCancelableTimeout' const TIMEOUT_DURATION = 5000; -export default function useAutoscroll( - savedIndex: number, - autoscrollIndex: number, - updadteIndex: (index: number) => void, + +function useAutoupdate( + savedValue: T, + actualValue: T, + resetValue: T, + updadteValue: (value: T) => void, ) { - const [ autoscroll, setAutoscroll ] = useState(savedIndex === 0) + const [ autoupdate, setAutoupdate ] = useState(savedValue === resetValue) - const [ timeoutStartAutoscroll, stopAutoscroll ] = useCancelableTimeout( - () => setAutoscroll(true), - () => setAutoscroll(false), + const [ timeoutStartAutoupdate, stopAutoupdate ] = useCancelableTimeout( + () => setAutoupdate(true), + () => setAutoupdate(false), TIMEOUT_DURATION, ) useEffect(() => { - if (autoscroll && autoscrollIndex !== savedIndex) { - updadteIndex(autoscrollIndex) + if (autoupdate && actualValue !== savedValue) { + updadteValue(actualValue) } - }, [ autoscroll, autoscrollIndex ]) + }, [ autoupdate, actualValue ]) - const autoScrollRef = useLatestRef(autoscroll) + const autoScrollRef = useLatestRef(autoupdate) useEffect(() => { - if (!autoscroll) { - timeoutStartAutoscroll() + if (!autoupdate) { + timeoutStartAutoupdate() } return () => { if (autoScrollRef.current) { - updadteIndex(0) // index:0 means autoscroll is active + updadteValue(resetValue) } } }, []) - return { - autoscrollIndex, - timeoutStartAutoscroll, - stopAutoscroll, - } + return [ timeoutStartAutoupdate, stopAutoupdate ] +} + +// That might be simplified by removing index from devTools[INDEX_KEY] store... +export default function useAutoscroll( + filteredList: Timed[], + time: number, + savedIndex: number, + updadteIndex: (index: number) => void, +) { + const filteredIndexNow = useMemo(() => { + // Should use findLastIndex here + for (let i=0; i < filteredList.length; i++) { + if (filteredList[i].time > time) { + return i-1 + } + } + return filteredList.length + }, [ time, filteredList ]) + + return useAutoupdate(savedIndex, filteredIndexNow, 0, updadteIndex) } \ No newline at end of file diff --git a/frontend/app/player/index.ts b/frontend/app/player/index.ts index 55c67fb0b..cf7fe3244 100644 --- a/frontend/app/player/index.ts +++ b/frontend/app/player/index.ts @@ -2,6 +2,7 @@ export * from './web/assist/AssistManager'; export * from './web/assist/LocalStream'; export * from './web/WebPlayer'; export * from './web/types'; +export * from './common/types'; export * from './create'; export type { MarkedTarget } from './web/TargetMarker' From aa3e7cde061d3ac6de3bbf3676beea468dfb4970 Mon Sep 17 00:00:00 2001 From: Mehdi Osman Date: Tue, 29 Nov 2022 12:39:21 +0100 Subject: [PATCH 081/252] Update .env.sample --- frontend/.env.sample | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/.env.sample b/frontend/.env.sample index 33c67be52..d5337deee 100644 --- a/frontend/.env.sample +++ b/frontend/.env.sample @@ -23,4 +23,4 @@ MINIO_SECRET_KEY = '' # APP and TRACKER VERSIONS VERSION = '1.9.0' -TRACKER_VERSION = '4.1.7' +TRACKER_VERSION = '4.1.9' From e9e5a9399952a6919eef721e0c084f7cacce2ccb Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 28 Nov 2022 13:08:52 +0100 Subject: [PATCH 082/252] change(ui) - popover behaviour --- frontend/app/components/ui/Popover/Popover.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/components/ui/Popover/Popover.tsx b/frontend/app/components/ui/Popover/Popover.tsx index 1c1e2a67e..d8a0e260f 100644 --- a/frontend/app/components/ui/Popover/Popover.tsx +++ b/frontend/app/components/ui/Popover/Popover.tsx @@ -59,7 +59,7 @@ const Popover = ({ children, render, placement, onOpen = () => {} }: Props) => { {open && ( From e2b8601390dec25437e9e8b20f801698fe381c62 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 28 Nov 2022 13:42:20 +0100 Subject: [PATCH 083/252] change(ui) - popover dropdown, and replayer subheader buttons --- .../components/Session_/Autoplay/Autoplay.tsx | 25 +----- .../app/components/Session_/Issues/Issues.js | 45 +++++----- .../Session_/Player/Overlay/AutoplayTimer.tsx | 50 ++++++----- frontend/app/components/Session_/Subheader.js | 88 +++++++++---------- .../shared/AutoplayToggle/AutoplayToggle.tsx | 29 ++++++ .../components/shared/AutoplayToggle/index.ts | 1 + .../shared/SharePopup/SharePopup.js | 2 +- 7 files changed, 124 insertions(+), 116 deletions(-) create mode 100644 frontend/app/components/shared/AutoplayToggle/AutoplayToggle.tsx create mode 100644 frontend/app/components/shared/AutoplayToggle/index.ts diff --git a/frontend/app/components/Session_/Autoplay/Autoplay.tsx b/frontend/app/components/Session_/Autoplay/Autoplay.tsx index a83aa3997..5f54b4a8a 100644 --- a/frontend/app/components/Session_/Autoplay/Autoplay.tsx +++ b/frontend/app/components/Session_/Autoplay/Autoplay.tsx @@ -2,8 +2,7 @@ import React, { useEffect } from 'react'; import { connect } from 'react-redux'; import { setAutoplayValues } from 'Duck/sessions'; import { session as sessionRoute } from 'App/routes'; -import { Link, Icon, Toggler, Tooltip } from 'UI'; -import { Controls as PlayerControls, connectPlayer } from 'Player'; +import { Link, Icon, Tooltip } from 'UI'; import { withRouter, RouteComponentProps } from 'react-router-dom'; import cn from 'classnames'; import { fetchAutoplaySessions } from 'Duck/search'; @@ -13,12 +12,10 @@ const PER_PAGE = 10; interface Props extends RouteComponentProps { previousId: string; nextId: string; - autoplay: boolean; defaultList: any; currentPage: number; total: number; setAutoplayValues?: () => void; - toggleAutoplay?: () => void; latestRequestTime: any; sessionIds: any; fetchAutoplaySessions?: (page: number) => Promise; @@ -29,7 +26,6 @@ function Autoplay(props: Props) { nextId, currentPage, total, - autoplay, sessionIds, latestRequestTime, match: { @@ -54,14 +50,6 @@ function Autoplay(props: Props) { return (
-
- - Auto-Play -
- Play Previous Session
} @@ -111,13 +99,4 @@ export default connect( latestRequestTime: state.getIn(['search', 'latestRequestTime']), }), { setAutoplayValues, fetchAutoplaySessions } -)( - connectPlayer( - (state: any) => ({ - autoplay: state.autoplay, - }), - { - toggleAutoplay: PlayerControls.toggleAutoplay, - } - )(withRouter(Autoplay)) -); +)(withRouter(Autoplay)); diff --git a/frontend/app/components/Session_/Issues/Issues.js b/frontend/app/components/Session_/Issues/Issues.js index 3c037a019..2cc309624 100644 --- a/frontend/app/components/Session_/Issues/Issues.js +++ b/frontend/app/components/Session_/Issues/Issues.js @@ -1,6 +1,6 @@ import React from 'react'; import { connect } from 'react-redux'; -import { Icon, Popover } from 'UI'; +import { Icon, Popover, Button } from 'UI'; import IssuesModal from './IssuesModal'; import { fetchProjects, fetchMeta } from 'Duck/assignments'; import stl from './issues.module.css'; @@ -67,30 +67,27 @@ class Issues extends React.Component { const provider = issuesIntegration.provider; return ( -
-
- ( -
- -
- )} - > -
- - Create Issue -
-
+ ( +
+ +
+ )} + > +
+
-
+ {/*
+ + Create Issue +
*/} + ); } } diff --git a/frontend/app/components/Session_/Player/Overlay/AutoplayTimer.tsx b/frontend/app/components/Session_/Player/Overlay/AutoplayTimer.tsx index a99633bb4..83d993e6b 100644 --- a/frontend/app/components/Session_/Player/Overlay/AutoplayTimer.tsx +++ b/frontend/app/components/Session_/Player/Overlay/AutoplayTimer.tsx @@ -1,9 +1,9 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState } from 'react'; import cn from 'classnames'; -import { connect } from 'react-redux' +import { connect } from 'react-redux'; import { withRouter, RouteComponentProps } from 'react-router-dom'; -import { Button, Link } from 'UI' -import { session as sessionRoute, withSiteId } from 'App/routes' +import { Button, Link } from 'UI'; +import { session as sessionRoute, withSiteId } from 'App/routes'; import stl from './AutoplayTimer.module.css'; import clsOv from './overlay.module.css'; @@ -13,49 +13,53 @@ interface IProps extends RouteComponentProps { } function AutoplayTimer({ nextId, siteId, history }: IProps) { - let timer: NodeJS.Timer + let timer: NodeJS.Timer; const [cancelled, setCancelled] = useState(false); const [counter, setCounter] = useState(5); useEffect(() => { - if(counter > 0) { + if (counter > 0) { timer = setTimeout(() => { - setCounter(counter - 1) - }, 1000) + setCounter(counter - 1); + }, 1000); } if (counter === 0) { - history.push(withSiteId(sessionRoute(nextId), siteId)) + history.push(withSiteId(sessionRoute(nextId), siteId)); } return () => clearTimeout(timer); - }, [counter]) + }, [counter]); const cancel = () => { - clearTimeout(timer) - setCancelled(true) - } + clearTimeout(timer); + setCancelled(true); + }; - if (cancelled) - return null + if (cancelled) return null; return ( -
+
Next recording will be played in {counter}s
- +
- +
+
Turn on/off auto-replay in: More options
- ) + ); } -export default withRouter(connect(state => ({ - siteId: state.getIn([ 'site', 'siteId' ]), - nextId: parseInt(state.getIn([ 'sessions', 'nextId' ])), -}))(AutoplayTimer)) +export default withRouter( + connect((state: any) => ({ + siteId: state.getIn(['site', 'siteId']), + nextId: parseInt(state.getIn(['sessions', 'nextId'])), + }))(AutoplayTimer) +); diff --git a/frontend/app/components/Session_/Subheader.js b/frontend/app/components/Session_/Subheader.js index a1ee8e48d..b45c49e00 100644 --- a/frontend/app/components/Session_/Subheader.js +++ b/frontend/app/components/Session_/Subheader.js @@ -10,6 +10,7 @@ import { connectPlayer, pause } from 'Player'; import ItemMenu from './components/HeaderMenu'; import { useModal } from 'App/components/Modal'; import BugReportModal from './BugReport/BugReportModal'; +import AutoplayToggle from 'Shared/AutoplayToggle'; function SubHeader(props) { const [isCopied, setCopied] = React.useState(false); @@ -29,8 +30,16 @@ function SubHeader(props) { exceptionsList: props.exceptionsList, eventsList: props.eventsList, endTime: props.endTime, - } - showModal(, { right: true }); + }; + showModal( + , + { right: true } + ); }; return ( @@ -55,39 +64,31 @@ function SubHeader(props) { className="ml-auto text-sm flex items-center color-gray-medium gap-2" style={{ width: 'max-content' }} > - + + + + +
+ } + /> , + }, { key: 2, - component: props.jiraConfig && props.jiraConfig.token && ( - - ), - }, - { - key: 3, - component: ( - - - Share -
- } - /> - ), - }, - { - key: 4, component: , }, ]} @@ -102,20 +103,17 @@ function SubHeader(props) { ); } -const SubH = connectPlayer( - (state) => ({ - width: state.width, - height: state.height, - currentLocation: state.location, - resourceList: state.resourceList - .filter((r) => r.isRed() || r.isYellow()) - .concat(state.fetchList.filter((i) => parseInt(i.status) >= 400)) - .concat(state.graphqlList.filter((i) => parseInt(i.status) >= 400)), - exceptionsList: state.exceptionsList, - eventsList: state.eventList, - endTime: state.endTime, - }) - - )(SubHeader); +const SubH = connectPlayer((state) => ({ + width: state.width, + height: state.height, + currentLocation: state.location, + resourceList: state.resourceList + .filter((r) => r.isRed() || r.isYellow()) + .concat(state.fetchList.filter((i) => parseInt(i.status) >= 400)) + .concat(state.graphqlList.filter((i) => parseInt(i.status) >= 400)), + exceptionsList: state.exceptionsList, + eventsList: state.eventList, + endTime: state.endTime, +}))(SubHeader); export default React.memo(SubH); diff --git a/frontend/app/components/shared/AutoplayToggle/AutoplayToggle.tsx b/frontend/app/components/shared/AutoplayToggle/AutoplayToggle.tsx new file mode 100644 index 000000000..3f25fd525 --- /dev/null +++ b/frontend/app/components/shared/AutoplayToggle/AutoplayToggle.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Controls as PlayerControls, connectPlayer } from 'Player'; +import { Toggler } from 'UI'; + +interface Props { + toggleAutoplay: () => void; + autoplay: boolean; +} +function AutoplayToggle(props: Props) { + const { autoplay } = props; + return ( +
+ + Auto-Play +
+ ); +} + +export default connectPlayer( + (state: any) => ({ + autoplay: state.autoplay, + }), + { + toggleAutoplay: PlayerControls.toggleAutoplay, + } +)(AutoplayToggle); diff --git a/frontend/app/components/shared/AutoplayToggle/index.ts b/frontend/app/components/shared/AutoplayToggle/index.ts new file mode 100644 index 000000000..a170180b0 --- /dev/null +++ b/frontend/app/components/shared/AutoplayToggle/index.ts @@ -0,0 +1 @@ +export { default } from './AutoplayToggle'; diff --git a/frontend/app/components/shared/SharePopup/SharePopup.js b/frontend/app/components/shared/SharePopup/SharePopup.js index 9fef76843..4c75df64b 100644 --- a/frontend/app/components/shared/SharePopup/SharePopup.js +++ b/frontend/app/components/shared/SharePopup/SharePopup.js @@ -128,7 +128,7 @@ export default class SharePopup extends React.PureComponent {
)} > -
{trigger}
+ {trigger} ); } From f31ee245e2da9dd1f64584bf4fefe51b57c67345 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 28 Nov 2022 16:22:50 +0100 Subject: [PATCH 084/252] change(ui) - text color and icon --- .../Session_/Player/Overlay/AutoplayTimer.tsx | 7 ++- .../Session_/components/HeaderMenu.tsx | 50 +++++++------------ frontend/app/svg/icons/quotes.svg | 2 +- 3 files changed, 24 insertions(+), 35 deletions(-) diff --git a/frontend/app/components/Session_/Player/Overlay/AutoplayTimer.tsx b/frontend/app/components/Session_/Player/Overlay/AutoplayTimer.tsx index 83d993e6b..045b7492b 100644 --- a/frontend/app/components/Session_/Player/Overlay/AutoplayTimer.tsx +++ b/frontend/app/components/Session_/Player/Overlay/AutoplayTimer.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import cn from 'classnames'; import { connect } from 'react-redux'; import { withRouter, RouteComponentProps } from 'react-router-dom'; -import { Button, Link } from 'UI'; +import { Button, Link, Icon } from 'UI'; import { session as sessionRoute, withSiteId } from 'App/routes'; import stl from './AutoplayTimer.module.css'; import clsOv from './overlay.module.css'; @@ -51,7 +51,10 @@ function AutoplayTimer({ nextId, siteId, history }: IProps) {
-
Turn on/off auto-replay in: More options
+
+ + Turn on/off auto-replay in: More options +
); diff --git a/frontend/app/components/Session_/components/HeaderMenu.tsx b/frontend/app/components/Session_/components/HeaderMenu.tsx index 08c875f96..003ca5ab3 100644 --- a/frontend/app/components/Session_/components/HeaderMenu.tsx +++ b/frontend/app/components/Session_/components/HeaderMenu.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Icon } from 'UI'; +import { Icon, Button } from 'UI'; import styles from './menu.module.css'; import cn from 'classnames'; import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv'; @@ -32,8 +32,8 @@ export default class ItemMenu extends React.PureComponent { }; closeMenu = () => { - this.setState({ displayed: false }) - } + this.setState({ displayed: false }); + }; render() { const { items } = this.props; @@ -42,36 +42,22 @@ export default class ItemMenu extends React.PureComponent { return (
-
-
- + +
+ {items.map((item) => + item.component ? ( +
+ {item.component} +
+ ) : null + )}
- More -
-
- {items.map((item) => - item.component ? ( -
- {item.component} -
- ) : null - )} -
); diff --git a/frontend/app/svg/icons/quotes.svg b/frontend/app/svg/icons/quotes.svg index 252b3b50f..5989049be 100644 --- a/frontend/app/svg/icons/quotes.svg +++ b/frontend/app/svg/icons/quotes.svg @@ -1,3 +1,3 @@ - + From aa52f3c6fbc5d50622f6d015f4a6a8e7ac9ddabd Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 28 Nov 2022 16:23:07 +0100 Subject: [PATCH 085/252] fix(ui) - dev tools sync check --- .../Session_/Player/Overlay/AutoplayTimer.tsx | 3 +-- .../shared/DevTools/ConsolePanel/ConsolePanel.tsx | 8 +++----- .../shared/DevTools/NetworkPanel/NetworkPanel.tsx | 13 ++++++------- .../DevTools/StackEventPanel/StackEventPanel.tsx | 10 ++++------ 4 files changed, 14 insertions(+), 20 deletions(-) diff --git a/frontend/app/components/Session_/Player/Overlay/AutoplayTimer.tsx b/frontend/app/components/Session_/Player/Overlay/AutoplayTimer.tsx index 045b7492b..fc292e737 100644 --- a/frontend/app/components/Session_/Player/Overlay/AutoplayTimer.tsx +++ b/frontend/app/components/Session_/Player/Overlay/AutoplayTimer.tsx @@ -52,8 +52,7 @@ function AutoplayTimer({ nextId, siteId, history }: IProps) {
- - Turn on/off auto-replay in: More options + Turn on/off auto-replay in More options
diff --git a/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx b/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx index 38714b92d..c9f040c50 100644 --- a/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx +++ b/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx @@ -72,14 +72,13 @@ function ConsolePanel(props: Props) { const { sessionStore: { devTools }, } = useStore(); - const [isDetailsModalActive, setIsDetailsModalActive] = useState(false); const [filteredList, setFilteredList] = useState([]); const filter = useObserver(() => devTools[INDEX_KEY].filter); const activeTab = useObserver(() => devTools[INDEX_KEY].activeTab); const activeIndex = useObserver(() => devTools[INDEX_KEY].index); const [pauseSync, setPauseSync] = useState(activeIndex > 0); const synRef: any = useRef({}); - const { showModal } = useModal(); + const { showModal, component: modalActive } = useModal(); const onTabClick = (activeTab: any) => devTools.update(INDEX_KEY, { activeTab }); const onFilterChange = ({ target: { value } }: any) => { @@ -92,7 +91,7 @@ function ConsolePanel(props: Props) { }; const removePause = () => { - setIsDetailsModalActive(false); + if (!!modalActive) return; clearTimeout(timeOut); timeOut = setTimeout(() => { devTools.update(INDEX_KEY, { index: getCurrentIndex() }); @@ -101,7 +100,6 @@ function ConsolePanel(props: Props) { }; const onMouseLeave = () => { - if (isDetailsModalActive) return; removePause(); }; @@ -136,7 +134,7 @@ function ConsolePanel(props: Props) { const _list = React.useRef(); const showDetails = (log: any) => { - setIsDetailsModalActive(true); + clearTimeout(timeOut); showModal(, { right: true, onClose: removePause }); devTools.update(INDEX_KEY, { index: filteredList.indexOf(log) }); setPauseSync(true); diff --git a/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx b/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx index 634aa9bae..416dd24eb 100644 --- a/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx +++ b/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx @@ -140,11 +140,9 @@ interface Props { } function NetworkPanel(props: Props) { const { resources, time, domContentLoadedTime, loadTime, domBuildingTime, fetchList } = props; - const { showModal } = useModal(); - + const { showModal, component: modalActive } = useModal(); const [filteredList, setFilteredList] = useState([]); const [showOnlyErrors, setShowOnlyErrors] = useState(false); - const [isDetailsModalActive, setIsDetailsModalActive] = useState(false); const additionalHeight = 0; const fetchPresented = fetchList.length > 0; const { @@ -169,7 +167,7 @@ function NetworkPanel(props: Props) { }; const removePause = () => { - setIsDetailsModalActive(false); + if (!!modalActive) return; clearTimeout(timeOut); timeOut = setTimeout(() => { devTools.update(INDEX_KEY, { index: getCurrentIndex() }); @@ -178,7 +176,7 @@ function NetworkPanel(props: Props) { }; const onMouseLeave = () => { - if (isDetailsModalActive) return; + if (!!modalActive) return; removePause(); }; @@ -261,7 +259,8 @@ function NetworkPanel(props: Props) { }, []); const showDetailsModal = (row: any) => { - setIsDetailsModalActive(true); + clearTimeout(timeOut); + setPauseSync(true); showModal( , { @@ -270,7 +269,6 @@ function NetworkPanel(props: Props) { } ); devTools.update(INDEX_KEY, { index: filteredList.indexOf(row) }); - setPauseSync(true); }; useEffect(() => { @@ -296,6 +294,7 @@ function NetworkPanel(props: Props) { border={false} />
+ {pauseSync &&
pause
} devTools[INDEX_KEY].filter); const activeTab = useObserver(() => devTools[INDEX_KEY].activeTab); @@ -55,8 +54,8 @@ function StackEventPanel(props: Props) { }; const removePause = () => { + if (!!modalActive) return; clearTimeout(timeOut); - setIsDetailsModalActive(false); timeOut = setTimeout(() => { devTools.update(INDEX_KEY, { index: getCurrentIndex() }); setPauseSync(false); @@ -71,7 +70,6 @@ function StackEventPanel(props: Props) { }, [time]); const onMouseLeave = () => { - if (isDetailsModalActive) return; removePause(); }; @@ -97,7 +95,7 @@ function StackEventPanel(props: Props) { }); const showDetails = (item: any) => { - setIsDetailsModalActive(true); + clearTimeout(timeOut); showModal(, { right: true, onClose: removePause }); devTools.update(INDEX_KEY, { index: filteredList.indexOf(item) }); setPauseSync(true); From 7844d4388af51c8b3cf7e3a83b050a143899f9c7 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 28 Nov 2022 16:34:05 +0100 Subject: [PATCH 086/252] fix(ui) - dev tools sync check --- .../app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx b/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx index 416dd24eb..4a7e71ea2 100644 --- a/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx +++ b/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx @@ -294,7 +294,6 @@ function NetworkPanel(props: Props) { border={false} />
- {pauseSync &&
pause
} Date: Mon, 28 Nov 2022 16:36:00 +0100 Subject: [PATCH 087/252] fix(ui) - audit sort value --- frontend/app/components/Client/Audit/AuditView/AuditView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/components/Client/Audit/AuditView/AuditView.tsx b/frontend/app/components/Client/Audit/AuditView/AuditView.tsx index b93c26d08..df30e64ff 100644 --- a/frontend/app/components/Client/Audit/AuditView/AuditView.tsx +++ b/frontend/app/components/Client/Audit/AuditView/AuditView.tsx @@ -46,7 +46,7 @@ function AuditView(props) { ]} defaultValue={order} plain - onChange={({ value }) => auditStore.updateKey('order', value)} + onChange={({ value }) => auditStore.updateKey('order', value.value)} />
auditStore.updateKey('searchQuery', value) }/> From 0b9d5fd303492a694a2c443d04352824b0a34ee4 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 28 Nov 2022 16:43:58 +0100 Subject: [PATCH 088/252] fix(ui) - note overflow auto --- .../components/Session_/Player/Controls/components/ReadNote.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/components/Session_/Player/Controls/components/ReadNote.tsx b/frontend/app/components/Session_/Player/Controls/components/ReadNote.tsx index edc58aa9f..7a9f54bcf 100644 --- a/frontend/app/components/Session_/Player/Controls/components/ReadNote.tsx +++ b/frontend/app/components/Session_/Player/Controls/components/ReadNote.tsx @@ -67,7 +67,7 @@ function ReadNote(props: Props) {
-
+
{props.note.message}
From 74ee93f357064ac45ccaeb3db01c99750aaf3478 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 28 Nov 2022 17:21:11 +0100 Subject: [PATCH 089/252] fix(ui) - autoplay modal --- .../app/components/Session_/Player/Overlay/AutoplayTimer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/components/Session_/Player/Overlay/AutoplayTimer.tsx b/frontend/app/components/Session_/Player/Overlay/AutoplayTimer.tsx index fc292e737..955e01441 100644 --- a/frontend/app/components/Session_/Player/Overlay/AutoplayTimer.tsx +++ b/frontend/app/components/Session_/Player/Overlay/AutoplayTimer.tsx @@ -52,7 +52,7 @@ function AutoplayTimer({ nextId, siteId, history }: IProps) {
- Turn on/off auto-replay in More options + Turn on/off auto-replay in More options
From e61a9f87d7933ae727e204e2a3947b746a89fa34 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 28 Nov 2022 17:22:13 +0100 Subject: [PATCH 090/252] fix(ui) - autoplay modal --- .../app/components/Session_/Player/Overlay/AutoplayTimer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/components/Session_/Player/Overlay/AutoplayTimer.tsx b/frontend/app/components/Session_/Player/Overlay/AutoplayTimer.tsx index 955e01441..d79a64b5c 100644 --- a/frontend/app/components/Session_/Player/Overlay/AutoplayTimer.tsx +++ b/frontend/app/components/Session_/Player/Overlay/AutoplayTimer.tsx @@ -51,7 +51,7 @@ function AutoplayTimer({ nextId, siteId, history }: IProps) { -
+
Turn on/off auto-replay in More options
From bdd54a185c24e97429bbe687b8f1b5500b293467 Mon Sep 17 00:00:00 2001 From: Taha Yassine Kraiem Date: Tue, 29 Nov 2022 09:55:01 +0100 Subject: [PATCH 091/252] feat(chalice): fixed jira-integration issue-types list --- api/chalicelib/utils/jira_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/chalicelib/utils/jira_client.py b/api/chalicelib/utils/jira_client.py index a820d4aa9..ee8196a46 100644 --- a/api/chalicelib/utils/jira_client.py +++ b/api/chalicelib/utils/jira_client.py @@ -242,7 +242,7 @@ class JiraManager: def get_issue_types(self): try: - types = self._jira.issue_types() + types = self._jira.project(self._config['JIRA_PROJECT_ID']).issueTypes except JIRAError as e: self.retries -= 1 if (e.status_code // 100) == 4 and self.retries > 0: From f95fff2112068e1af67f1c072fdb06f5b54a5523 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Tue, 29 Nov 2022 10:42:10 +0100 Subject: [PATCH 092/252] fix(ui) - github/jira issue form data fetch --- frontend/app/components/Session_/Issues/IssueForm.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/frontend/app/components/Session_/Issues/IssueForm.js b/frontend/app/components/Session_/Issues/IssueForm.js index 4b6585985..43a7f382a 100644 --- a/frontend/app/components/Session_/Issues/IssueForm.js +++ b/frontend/app/components/Session_/Issues/IssueForm.js @@ -18,7 +18,6 @@ const SelectedValue = ({ icon, text }) => { class IssueForm extends React.PureComponent { componentDidMount() { const { projects, issueTypes } = this.props; - this.props.init({ projectId: projects[0] ? projects[0].id : '', issueType: issueTypes[0] ? issueTypes[0].id : '', @@ -27,8 +26,8 @@ class IssueForm extends React.PureComponent { componentWillReceiveProps(newProps) { const { instance } = this.props; - if (instance.projectId && newProps.instance.projectId != instance.projectId) { - this.props.fetchMeta(instance.projectId).then(() => { + if (newProps.instance.projectId && newProps.instance.projectId != instance.projectId) { + this.props.fetchMeta(newProps.instance.projectId).then(() => { this.props.edit({ issueType: '', assignee: '', projectId: newProps.instance.projectId }); }); } @@ -87,7 +86,7 @@ class IssueForm extends React.PureComponent { + + ); +} + +export default observer(RecordingsSearch); diff --git a/frontend/app/components/Assist/RecordingsList/RecordsListItem.tsx b/frontend/app/components/Assist/RecordingsList/RecordsListItem.tsx new file mode 100644 index 000000000..132282fcf --- /dev/null +++ b/frontend/app/components/Assist/RecordingsList/RecordsListItem.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { Icon, ItemMenu } from 'UI'; +import { durationFromMs, checkForRecent, getDateFromMill } from 'App/date' +import { IRecord } from 'App/services/RecordingsService' +import { useStore } from 'App/mstore' +import { toast } from 'react-toastify' + +interface Props { + record: IRecord; +} + +function RecordsListItem(props: Props) { + const { record } = props; + const { recordingsStore } = useStore() + + const onRecordClick = () => { + recordingsStore.fetchRecordingUrl(record.recordId).then(url => { + window.open(url, "_blank"); + }) + } + + const onDelete = () => { + recordingsStore.deleteRecording(record.recordId).then(() => { + toast.success("Recording deleted") + }) + } + + const menuItems = [ + { icon: 'trash', text: 'Delete', onClick: onDelete }, + ] + return ( +
+
+
+
+
+ +
+
+
{record.name}
+
+ {durationFromMs(record.duration)} +
+
+
+
+
+
+
+ {record.createdBy} +
+
{checkForRecent(getDateFromMill(record.createdAt), 'LLL dd, yyyy, hh:mm a')}
+
+
+
+
+ + +
Play Video
+
+ +
+
+
+ ); +} + +export default RecordsListItem; diff --git a/frontend/app/components/Session_/ScreenRecorder/ScreenRecorder.tsx b/frontend/app/components/Session_/ScreenRecorder/ScreenRecorder.tsx index 30d5ac08e..e7e75d979 100644 --- a/frontend/app/components/Session_/ScreenRecorder/ScreenRecorder.tsx +++ b/frontend/app/components/Session_/ScreenRecorder/ScreenRecorder.tsx @@ -44,6 +44,7 @@ function ScreenRecorder({ React.useEffect(() => { return () => stopRecorderCb?.(); }, []); + const onSave = async (saveObj: { name: string; duration: number }, blob: Blob) => { try { const url = await recordingsService.reserveUrl(siteId, saveObj); diff --git a/frontend/app/date.ts b/frontend/app/date.ts index 3403abfd6..a6162d671 100644 --- a/frontend/app/date.ts +++ b/frontend/app/date.ts @@ -1,7 +1,6 @@ // @flow import { DateTime, Duration } from 'luxon'; // TODO -import { toJS } from 'mobx'; import { Timezone } from 'MOBX/types/sessionSettings'; export const durationFormatted = (duration: Duration):string => { diff --git a/frontend/app/mstore/index.tsx b/frontend/app/mstore/index.tsx index 1d6d758a1..fe21a11ab 100644 --- a/frontend/app/mstore/index.tsx +++ b/frontend/app/mstore/index.tsx @@ -14,6 +14,7 @@ import { funnelService, errorService, notesService, + recordingsService, } from 'App/services'; import SettingsStore from './settingsStore'; import AuditStore from './auditStore'; @@ -22,6 +23,7 @@ import ErrorStore from './errorStore'; import SessionStore from './sessionStore'; import NotesStore from './notesStore'; import BugReportStore from './bugReportStore' +import RecordingsStore from './recordingsStore' export class RootStore { dashboardStore: DashboardStore; @@ -35,7 +37,8 @@ export class RootStore { notificationStore: NotificationStore; sessionStore: SessionStore; notesStore: NotesStore; - bugReportStore: BugReportStore + bugReportStore: BugReportStore; + recordingsStore: RecordingsStore; constructor() { this.dashboardStore = new DashboardStore(); @@ -50,6 +53,7 @@ export class RootStore { this.sessionStore = new SessionStore(); this.notesStore = new NotesStore(); this.bugReportStore = new BugReportStore(); + this.recordingsStore = new RecordingsStore(); } initClient() { @@ -62,6 +66,7 @@ export class RootStore { auditService.initClient(client); errorService.initClient(client); notesService.initClient(client) + recordingsService.initClient(client); } } diff --git a/frontend/app/mstore/recordingsStore.ts b/frontend/app/mstore/recordingsStore.ts new file mode 100644 index 000000000..0f581a931 --- /dev/null +++ b/frontend/app/mstore/recordingsStore.ts @@ -0,0 +1,74 @@ +import { makeAutoObservable } from "mobx" +import { recordingsService } from "App/services" +import { IRecord } from 'App/services/RecordingsService' + +export default class RecordingsStore { + recordings: IRecord[] = [] + loading: boolean + + page = 1 + pageSize = 15 + order: 'desc' | 'asc' = 'desc' + search = '' + // not later we will add search by user id + userId: number + + constructor() { + makeAutoObservable(this) + } + + updateSearch(val: string) { + this.search = val + } + updatePage(page: number) { + this.page = page + } + + async fetchRecordings() { + const filter = { + page: this.page, + limit: this.pageSize, + order: this.order, + search: this.search, + } + + this.loading = true + try { + const recordings = await recordingsService.fetchRecordings(filter) + this.recordings = recordings; + this.fetchRecordingUrl(recordings[0].recordId) + return recordings; + } catch (e) { + console.error(e) + } finally { + this.loading = false + } + } + + async fetchRecordingUrl(id: number): Promise { + this.loading = true + try { + const recording = await recordingsService.fetchRecording(id) + return recording.URL; + } catch (e) { + console.error(e) + } finally { + this.loading = false + } + } + + async deleteRecording(id: number) { + this.loading = true + try { + const recording = await recordingsService.deleteRecording(id) + console.log(recording) + return recording + } catch (e) { + console.error(e) + } finally { + this.loading = false + } + } + + +} diff --git a/frontend/app/player/web/assist/AssistManager.ts b/frontend/app/player/web/assist/AssistManager.ts index 7d65a988d..8c30d9876 100644 --- a/frontend/app/player/web/assist/AssistManager.ts +++ b/frontend/app/player/web/assist/AssistManager.ts @@ -283,7 +283,6 @@ export default class AssistManager { const recordingState = getState().recordingState if (!this.socket || recordingState === SessionRecordingStatus.Off) return; - console.log('stop rec') this.socket.emit("stop_recording") this.toggleRecording(false) } @@ -292,7 +291,6 @@ export default class AssistManager { this.md.toggleRecordingStatus(isAccepted) update({ recordingState: isAccepted ? SessionRecordingStatus.Recording : SessionRecordingStatus.Off }) - console.log('Im here', isAccepted, getState().recordingState) } /* ==== Remote Control ==== */ diff --git a/frontend/app/player/web/network/loadFiles.ts b/frontend/app/player/web/network/loadFiles.ts index 6b34c3261..79e591aac 100644 --- a/frontend/app/player/web/network/loadFiles.ts +++ b/frontend/app/player/web/network/loadFiles.ts @@ -17,8 +17,8 @@ export const loadFiles = ( return processAPIStreamResponse(r, true) }) .then(onData) - .then(() => - urls.reduce((p, url) => + .then(() => + urls.reduce((p, url) => p.then(() => window.fetch(url) .then(r => { @@ -62,7 +62,7 @@ const processAPIStreamResponse = (response: Response, isMainFile: boolean) => { } if (response.status >= 400) { return rej( - isMainFile ? `no start file. status code ${ response.status }` + isMainFile ? `no start file. status code ${ response.status }` : `Bad endfile status code ${response.status}` ) } diff --git a/frontend/app/routes.js b/frontend/app/routes.js index 90de53012..139994fc0 100644 --- a/frontend/app/routes.js +++ b/frontend/app/routes.js @@ -84,6 +84,7 @@ export const onboarding = (tab = routerOBTabString) => `/onboarding/${ tab }`; export const sessions = params => queried('/sessions', params); export const assist = params => queried('/assist', params); +export const recordings = params => queried("/recordings", params); export const session = (sessionId = ':sessionId', hash) => hashed(`/session/${ sessionId }`, hash); export const liveSession = (sessionId = ':sessionId', params, hash) => hashed(queried(`/assist/${ sessionId }`, params), hash); @@ -123,7 +124,8 @@ const REQUIRED_SITE_ID_ROUTES = [ session(''), sessions(), assist(), - + recordings(), + metrics(), metricDetails(''), metricDetailsSub(''), @@ -172,6 +174,7 @@ const SITE_CHANGE_AVALIABLE_ROUTES = [ sessions(), funnels(), assist(), + recordings(), dashboard(), dashboardSelected(), metrics(), diff --git a/frontend/app/services/RecordingsService.ts b/frontend/app/services/RecordingsService.ts index 0b50fb943..64cbe21f8 100644 --- a/frontend/app/services/RecordingsService.ts +++ b/frontend/app/services/RecordingsService.ts @@ -5,6 +5,24 @@ interface RecordingData { duration: number; } +interface FetchFilter { + page: number + limit: number + order: 'asc' | 'desc' + search: string +} + +export interface IRecord { + createdAt: number + createdBy: string + duration: number + name: string + recordId: number + sessionId: number + userId: number + URL?: string +} + export default class RecordingsService { private client: APIClient; @@ -38,4 +56,37 @@ export default class RecordingsService { }) } + fetchRecordings(filters: FetchFilter): Promise { + return this.client.post(`/assist/records`, filters) + .then(r => { + if (r.ok) { + return r.json().then(j => j.data) + } else { + throw new Error("Can't get recordings: " + r.status); + } + }) + } + + fetchRecording(id: number): Promise { + return this.client.get(`/assist/records/${id}`) + .then(r => { + if (r.ok) { + return r.json().then(j => j.data) + } else { + throw new Error("Can't get recordings: " + r.status); + } + }) + } + + deleteRecording(id: number): Promise { + return this.client.delete(`/assist/records/${id}`) + .then(r => { + if (r.ok) { + return r.json().then(j => j.data) + } else { + throw new Error("Can't get recordings: " + r.status); + } + }) + } + } diff --git a/frontend/app/utils/screenRecorder.ts b/frontend/app/utils/screenRecorder.ts index fe68df4a5..6c7985a04 100644 --- a/frontend/app/utils/screenRecorder.ts +++ b/frontend/app/utils/screenRecorder.ts @@ -95,7 +95,11 @@ export async function screenRecorder(recName: string, sessionId: string, saveCb: const stream = await recordScreen(); const mediaRecorder = createFileRecorder(stream, FILE_TYPE, recName, sessionId, saveCb); - return () => mediaRecorder.stop(); + return () => { + if (mediaRecorder.state !== 'inactive') { + mediaRecorder.stop(); + } + } } catch (e) { console.log(e); } From 7bd5d13e61d8a326a8f27c5d095828dfd3e2eb58 Mon Sep 17 00:00:00 2001 From: sylenien Date: Thu, 17 Nov 2022 17:54:36 +0100 Subject: [PATCH 120/252] change(ui): add editing etc --- .../Assist/RecordingsList/RecordingsList.tsx | 4 +- .../Assist/RecordingsList/RecordsListItem.tsx | 152 +++++++++++------- frontend/app/mstore/recordingsStore.ts | 74 +++++---- frontend/app/services/RecordingsService.ts | 144 +++++++++-------- 4 files changed, 218 insertions(+), 156 deletions(-) diff --git a/frontend/app/components/Assist/RecordingsList/RecordingsList.tsx b/frontend/app/components/Assist/RecordingsList/RecordingsList.tsx index 7f71d157d..5f9eb68f9 100644 --- a/frontend/app/components/Assist/RecordingsList/RecordingsList.tsx +++ b/frontend/app/components/Assist/RecordingsList/RecordingsList.tsx @@ -13,8 +13,8 @@ function RecordingsList() { const recordsSearch = recordingsStore.search; React.useEffect(() => { - recordingsStore.fetchRecordings() - }, []) + recordingsStore.fetchRecordings(); + }, []); React.useEffect(() => { setRecordings(filterList(recordings, recordsSearch, ['createdBy', 'name'])); diff --git a/frontend/app/components/Assist/RecordingsList/RecordsListItem.tsx b/frontend/app/components/Assist/RecordingsList/RecordsListItem.tsx index 132282fcf..e6996d1fc 100644 --- a/frontend/app/components/Assist/RecordingsList/RecordsListItem.tsx +++ b/frontend/app/components/Assist/RecordingsList/RecordsListItem.tsx @@ -1,71 +1,113 @@ import React from 'react'; import { Icon, ItemMenu } from 'UI'; -import { durationFromMs, checkForRecent, getDateFromMill } from 'App/date' -import { IRecord } from 'App/services/RecordingsService' -import { useStore } from 'App/mstore' -import { toast } from 'react-toastify' +import { durationFromMs, checkForRecent, getDateFromMill } from 'App/date'; +import { IRecord } from 'App/services/RecordingsService'; +import { useStore } from 'App/mstore'; +import { toast } from 'react-toastify'; +import cn from 'classnames'; interface Props { - record: IRecord; + record: IRecord; } function RecordsListItem(props: Props) { - const { record } = props; - const { recordingsStore } = useStore() + const { record } = props; + const { recordingsStore } = useStore(); + const [isEdit, setEdit] = React.useState(false); + const [recordingTitle, setRecordingTitle] = React.useState(record.name); + const inputRef = React.useRef(null); - const onRecordClick = () => { - recordingsStore.fetchRecordingUrl(record.recordId).then(url => { - window.open(url, "_blank"); - }) + const onRecordClick = () => { + recordingsStore.fetchRecordingUrl(record.recordId).then((url) => { + window.open(url, '_blank'); + }); + }; + + React.useEffect(() => { + if (inputRef.current) { + inputRef.current.style.width = `${record.name.length}ch`; } + }, [isEdit, inputRef.current]); - const onDelete = () => { - recordingsStore.deleteRecording(record.recordId).then(() => { - toast.success("Recording deleted") - }) - } + const onDelete = () => { + recordingsStore.deleteRecording(record.recordId).then(() => { + recordingsStore.setRecordings(recordingsStore.recordings.filter(rec => rec.recordId !== record.recordId)) + toast.success('Recording deleted'); + }); + }; - const menuItems = [ - { icon: 'trash', text: 'Delete', onClick: onDelete }, - ] - return ( -
-
-
-
-
- -
-
-
{record.name}
-
- {durationFromMs(record.duration)} -
-
-
-
-
-
-
- {record.createdBy} -
-
{checkForRecent(getDateFromMill(record.createdAt), 'LLL dd, yyyy, hh:mm a')}
-
-
-
-
- - -
Play Video
-
- -
+ const menuItems = [{ icon: 'trash', text: 'Delete', onClick: onDelete }]; + + const onSave = () => { + recordingsStore.updateRecordingName(record.recordId, recordingTitle); + setEdit(false); + }; + + return ( +
+
+
+
+
+
+
+ {isEdit ? ( + setRecordingTitle(e.target.value)} + onBlur={onSave} + onFocus={() => setEdit(true)} + /> + ) : ( +
setEdit(true)} + className={cn( + 'border-dotted border-gray-medium', + 'pt-1 w-fit -mt-2', + 'cursor-pointer select-none border-b' + )} + > + {recordingTitle} +
+ )} +
{durationFromMs(record.duration)}
+
+
- ); +
+
+
{record.createdBy}
+
+ {checkForRecent(getDateFromMill(record.createdAt), 'LLL dd, yyyy, hh:mm a')} +
+
+
+
+
+ + +
Play Video
+
+ +
+
+
+ ); } export default RecordsListItem; diff --git a/frontend/app/mstore/recordingsStore.ts b/frontend/app/mstore/recordingsStore.ts index 0f581a931..783e580d1 100644 --- a/frontend/app/mstore/recordingsStore.ts +++ b/frontend/app/mstore/recordingsStore.ts @@ -1,27 +1,31 @@ -import { makeAutoObservable } from "mobx" -import { recordingsService } from "App/services" -import { IRecord } from 'App/services/RecordingsService' +import { makeAutoObservable } from 'mobx'; +import { recordingsService } from 'App/services'; +import { IRecord } from 'App/services/RecordingsService'; export default class RecordingsStore { - recordings: IRecord[] = [] - loading: boolean + recordings: IRecord[] = []; + loading: boolean; - page = 1 - pageSize = 15 - order: 'desc' | 'asc' = 'desc' - search = '' + page = 1; + pageSize = 15; + order: 'desc' | 'asc' = 'desc'; + search = ''; // not later we will add search by user id - userId: number + userId: number; constructor() { - makeAutoObservable(this) + makeAutoObservable(this); + } + + setRecordings(records: IRecord[]) { + this.recordings = records; } updateSearch(val: string) { - this.search = val + this.search = val; } updatePage(page: number) { - this.page = page + this.page = page; } async fetchRecordings() { @@ -30,45 +34,53 @@ export default class RecordingsStore { limit: this.pageSize, order: this.order, search: this.search, - } + }; - this.loading = true + this.loading = true; try { - const recordings = await recordingsService.fetchRecordings(filter) - this.recordings = recordings; - this.fetchRecordingUrl(recordings[0].recordId) + const recordings = await recordingsService.fetchRecordings(filter); + this.setRecordings(recordings); return recordings; } catch (e) { - console.error(e) + console.error(e); } finally { - this.loading = false + this.loading = false; } } async fetchRecordingUrl(id: number): Promise { - this.loading = true + this.loading = true; try { - const recording = await recordingsService.fetchRecording(id) + const recording = await recordingsService.fetchRecording(id); return recording.URL; } catch (e) { - console.error(e) + console.error(e); } finally { - this.loading = false + this.loading = false; } } async deleteRecording(id: number) { - this.loading = true + this.loading = true; try { - const recording = await recordingsService.deleteRecording(id) - console.log(recording) - return recording + const recording = await recordingsService.deleteRecording(id); + return recording; } catch (e) { - console.error(e) + console.error(e); } finally { - this.loading = false + this.loading = false; } } - + async updateRecordingName(id: number, name: string) { + this.loading = true; + try { + const recording = await recordingsService.updateRecordingName(id, name); + return recording; + } catch (e) { + console.error(e); + } finally { + this.loading = false; + } + } } diff --git a/frontend/app/services/RecordingsService.ts b/frontend/app/services/RecordingsService.ts index 64cbe21f8..352a22e3b 100644 --- a/frontend/app/services/RecordingsService.ts +++ b/frontend/app/services/RecordingsService.ts @@ -6,87 +6,95 @@ interface RecordingData { } interface FetchFilter { - page: number - limit: number - order: 'asc' | 'desc' - search: string + page: number; + limit: number; + order: 'asc' | 'desc'; + search: string; } export interface IRecord { - createdAt: number - createdBy: string - duration: number - name: string - recordId: number - sessionId: number - userId: number - URL?: string + createdAt: number; + createdBy: string; + duration: number; + name: string; + recordId: number; + sessionId: number; + userId: number; + URL?: string; } export default class RecordingsService { private client: APIClient; - constructor(client?: APIClient) { - this.client = client ? client : new APIClient(); - } + constructor(client?: APIClient) { + this.client = client ? client : new APIClient(); + } - initClient(client?: APIClient) { - this.client = client || new APIClient(); - } + initClient(client?: APIClient) { + this.client = client || new APIClient(); + } - reserveUrl(siteId: string, recordingData: RecordingData): Promise { - return this.client.put(`/${siteId}/assist/save`, recordingData) - .then(r => { - if (r.ok) { - return r.json().then(j => j.data.URL) - } else { - throw new Error("Can't reserve space for recording: " + r.status); - } - }) - } + reserveUrl(siteId: string, recordingData: RecordingData): Promise { + return this.client.put(`/${siteId}/assist/save`, recordingData).then((r) => { + if (r.ok) { + return r.json().then((j) => j.data.URL); + } else { + throw new Error("Can't reserve space for recording: " + r.status); + } + }); + } - saveFile(url: string, file: Blob) { - return fetch(url, { method: 'PUT', headers: { 'Content-Type': 'video/webm' }, body: file }) - .then(r => { - if (r.ok) { - return true - } else { - throw new Error("Can't upload file: " + r.status) - } - }) - } + saveFile(url: string, file: Blob) { + return fetch(url, { + method: 'PUT', + headers: { 'Content-Type': 'video/webm' }, + body: file, + }).then((r) => { + if (r.ok) { + return true; + } else { + throw new Error("Can't upload file: " + r.status); + } + }); + } - fetchRecordings(filters: FetchFilter): Promise { - return this.client.post(`/assist/records`, filters) - .then(r => { - if (r.ok) { - return r.json().then(j => j.data) - } else { - throw new Error("Can't get recordings: " + r.status); - } - }) - } + fetchRecordings(filters: FetchFilter): Promise { + return this.client.post(`/assist/records`, filters).then((r) => { + if (r.ok) { + return r.json().then((j) => j.data); + } else { + throw new Error("Can't get recordings: " + r.status); + } + }); + } - fetchRecording(id: number): Promise { - return this.client.get(`/assist/records/${id}`) - .then(r => { - if (r.ok) { - return r.json().then(j => j.data) - } else { - throw new Error("Can't get recordings: " + r.status); - } - }) - } + fetchRecording(id: number): Promise { + return this.client.get(`/assist/records/${id}`).then((r) => { + if (r.ok) { + return r.json().then((j) => j.data); + } else { + throw new Error("Can't get recordings: " + r.status); + } + }); + } - deleteRecording(id: number): Promise { - return this.client.delete(`/assist/records/${id}`) - .then(r => { - if (r.ok) { - return r.json().then(j => j.data) - } else { - throw new Error("Can't get recordings: " + r.status); - } - }) - } + updateRecordingName(id: number, name: string): Promise { + return this.client.post(`/assist/records/${id}`, { name }).then((r) => { + if (r.ok) { + return r.json().then((j) => j.data); + } else { + throw new Error("Can't get recordings: " + r.status); + } + }); + } + deleteRecording(id: number): Promise { + return this.client.delete(`/assist/records/${id}`).then((r) => { + if (r.ok) { + return r.json().then((j) => j.data); + } else { + throw new Error("Can't get recordings: " + r.status); + } + }); + } } From 3e66c0680be4c303ff1ee282308166c00555c9bc Mon Sep 17 00:00:00 2001 From: sylenien Date: Fri, 18 Nov 2022 12:11:57 +0100 Subject: [PATCH 121/252] change(ui): change time presentation --- .../components/Assist/RecordingsList/RecordingsList.tsx | 2 +- .../components/Assist/RecordingsList/RecordsListItem.tsx | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/app/components/Assist/RecordingsList/RecordingsList.tsx b/frontend/app/components/Assist/RecordingsList/RecordingsList.tsx index 5f9eb68f9..d2916a9eb 100644 --- a/frontend/app/components/Assist/RecordingsList/RecordingsList.tsx +++ b/frontend/app/components/Assist/RecordingsList/RecordingsList.tsx @@ -39,7 +39,7 @@ function RecordingsList() { >
-
Title
+
Name
By
diff --git a/frontend/app/components/Assist/RecordingsList/RecordsListItem.tsx b/frontend/app/components/Assist/RecordingsList/RecordsListItem.tsx index e6996d1fc..6af11e1fc 100644 --- a/frontend/app/components/Assist/RecordingsList/RecordsListItem.tsx +++ b/frontend/app/components/Assist/RecordingsList/RecordsListItem.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Icon, ItemMenu } from 'UI'; -import { durationFromMs, checkForRecent, getDateFromMill } from 'App/date'; +import { durationFromMs, formatTimeOrDate, getDateFromMill } from 'App/date'; import { IRecord } from 'App/services/RecordingsService'; import { useStore } from 'App/mstore'; import { toast } from 'react-toastify'; @@ -12,7 +12,8 @@ interface Props { function RecordsListItem(props: Props) { const { record } = props; - const { recordingsStore } = useStore(); + const { recordingsStore, settingsStore } = useStore(); + const { timezone } = settingsStore.sessionSettings; const [isEdit, setEdit] = React.useState(false); const [recordingTitle, setRecordingTitle] = React.useState(record.name); const inputRef = React.useRef(null); @@ -85,7 +86,7 @@ function RecordsListItem(props: Props) {
{record.createdBy}
- {checkForRecent(getDateFromMill(record.createdAt), 'LLL dd, yyyy, hh:mm a')} + {formatTimeOrDate(record.createdAt, timezone, true)}
From e606fe108fe4d595d4acd6e3b9809025627ca252 Mon Sep 17 00:00:00 2001 From: sylenien Date: Fri, 18 Nov 2022 12:21:32 +0100 Subject: [PATCH 122/252] change(ui): fix tooltip type, change rec name, add tooltip to recording name --- .../Assist/RecordingsList/RecordsListItem.tsx | 24 ++++++++++--------- .../DashboardView/DashboardView.tsx | 2 +- .../components/WidgetName/WidgetName.tsx | 10 ++++---- .../BugReport/components/ReportTitle.tsx | 2 +- .../ScreenRecorder/ScreenRecorder.tsx | 5 ++-- .../app/components/ui/Tooltip/Tooltip.tsx | 4 ++-- 6 files changed, 25 insertions(+), 22 deletions(-) diff --git a/frontend/app/components/Assist/RecordingsList/RecordsListItem.tsx b/frontend/app/components/Assist/RecordingsList/RecordsListItem.tsx index 6af11e1fc..ec004221a 100644 --- a/frontend/app/components/Assist/RecordingsList/RecordsListItem.tsx +++ b/frontend/app/components/Assist/RecordingsList/RecordsListItem.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Icon, ItemMenu } from 'UI'; +import { Icon, ItemMenu, Tooltip } from 'UI'; import { durationFromMs, formatTimeOrDate, getDateFromMill } from 'App/date'; import { IRecord } from 'App/services/RecordingsService'; import { useStore } from 'App/mstore'; @@ -67,16 +67,18 @@ function RecordsListItem(props: Props) { onFocus={() => setEdit(true)} /> ) : ( -
setEdit(true)} - className={cn( - 'border-dotted border-gray-medium', - 'pt-1 w-fit -mt-2', - 'cursor-pointer select-none border-b' - )} - > - {recordingTitle} -
+ +
setEdit(true)} + className={cn( + 'border-dotted border-gray-medium', + 'pt-1 w-fit -mt-2', + 'cursor-pointer select-none border-b' + )} + > + {recordingTitle} +
+
)}
{durationFromMs(record.duration)}
diff --git a/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx b/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx index e610d040c..e1653c947 100644 --- a/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx +++ b/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx @@ -125,7 +125,7 @@ function DashboardView(props: Props) { + {dashboard?.name} } diff --git a/frontend/app/components/Dashboard/components/WidgetName/WidgetName.tsx b/frontend/app/components/Dashboard/components/WidgetName/WidgetName.tsx index 3f352165a..a560235e9 100644 --- a/frontend/app/components/Dashboard/components/WidgetName/WidgetName.tsx +++ b/frontend/app/components/Dashboard/components/WidgetName/WidgetName.tsx @@ -64,12 +64,12 @@ function WidgetName(props: Props) { /> ) : ( // @ts-ignore - -
setEditing(true)} + +
setEditing(true)} className={ cn( - "text-2xl h-8 flex items-center border-transparent", + "text-2xl h-8 flex items-center border-transparent", canEdit && 'cursor-pointer select-none border-b border-b-borderColor-transparent hover:border-dotted hover:border-gray-medium' ) } @@ -77,7 +77,7 @@ function WidgetName(props: Props) { { name }
- + )} { canEdit &&
setEditing(true)}>
}
diff --git a/frontend/app/components/Session_/BugReport/components/ReportTitle.tsx b/frontend/app/components/Session_/BugReport/components/ReportTitle.tsx index 6e00d397a..93eb4148a 100644 --- a/frontend/app/components/Session_/BugReport/components/ReportTitle.tsx +++ b/frontend/app/components/Session_/BugReport/components/ReportTitle.tsx @@ -45,7 +45,7 @@ function ReportTitle() { /> ) : ( // @ts-ignore - +
{ try { + toast.warn('Uploading the recording...'); const url = await recordingsService.reserveUrl(siteId, saveObj); const status = recordingsService.saveFile(url, blob); if (status) { - toast.success('Session recording saved'); + toast.success('Session recording uploaded'); } } catch (e) { console.error(e); @@ -68,7 +69,7 @@ function ScreenRecorder({ }, [recordingState, isRecording]); const startRecording = async () => { - const stop = await screenRecorder('test rec_' + new Date().getTime(), sessionId, onSave); + const stop = await screenRecorder(`${sessionId}_${new Date().getTime()}`, sessionId, onSave); stopRecorderCb = stop; setRecording(true); }; diff --git a/frontend/app/components/ui/Tooltip/Tooltip.tsx b/frontend/app/components/ui/Tooltip/Tooltip.tsx index 87520b625..eb1c16c2d 100644 --- a/frontend/app/components/ui/Tooltip/Tooltip.tsx +++ b/frontend/app/components/ui/Tooltip/Tooltip.tsx @@ -4,8 +4,8 @@ import type { Placement } from '@floating-ui/react-dom-interactions'; import cn from 'classnames'; interface Props { - title: React.ReactNode; - children: any; + title?: React.ReactNode; + children: React.ReactNode; disabled?: boolean; open?: boolean; placement?: Placement; From 332970b89335b5f3cce0f1a89a237f5db0845840 Mon Sep 17 00:00:00 2001 From: sylenien Date: Fri, 18 Nov 2022 12:22:46 +0100 Subject: [PATCH 123/252] change(ui): change icon for no content --- .../app/components/Assist/RecordingsList/RecordingsList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/components/Assist/RecordingsList/RecordingsList.tsx b/frontend/app/components/Assist/RecordingsList/RecordingsList.tsx index d2916a9eb..9bb57e66c 100644 --- a/frontend/app/components/Assist/RecordingsList/RecordingsList.tsx +++ b/frontend/app/components/Assist/RecordingsList/RecordingsList.tsx @@ -28,7 +28,7 @@ function RecordingsList() { show={lenth === 0} title={
- +
{recordsSearch !== '' ? 'No matching results' From 72048cc1062dec15b771ad2092c8b54fe28c26a0 Mon Sep 17 00:00:00 2001 From: sylenien Date: Fri, 18 Nov 2022 16:29:30 +0100 Subject: [PATCH 124/252] change(ui): change file upl confirmation --- .../Assist/RecordingsList/RecordsListItem.tsx | 13 ++++++++++--- .../components/AssistActions/AssistActions.tsx | 2 +- .../Session_/ScreenRecorder/ScreenRecorder.tsx | 13 ++++++++++--- frontend/app/services/RecordingsService.ts | 15 +++++++++++++-- .../tracker-assist/src/ScreenRecordingState.ts | 7 +++++++ 5 files changed, 41 insertions(+), 9 deletions(-) diff --git a/frontend/app/components/Assist/RecordingsList/RecordsListItem.tsx b/frontend/app/components/Assist/RecordingsList/RecordsListItem.tsx index ec004221a..e4a0fe1cb 100644 --- a/frontend/app/components/Assist/RecordingsList/RecordsListItem.tsx +++ b/frontend/app/components/Assist/RecordingsList/RecordsListItem.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Icon, ItemMenu, Tooltip } from 'UI'; -import { durationFromMs, formatTimeOrDate, getDateFromMill } from 'App/date'; +import { durationFromMs, formatTimeOrDate } from 'App/date'; import { IRecord } from 'App/services/RecordingsService'; import { useStore } from 'App/mstore'; import { toast } from 'react-toastify'; @@ -32,7 +32,9 @@ function RecordsListItem(props: Props) { const onDelete = () => { recordingsStore.deleteRecording(record.recordId).then(() => { - recordingsStore.setRecordings(recordingsStore.recordings.filter(rec => rec.recordId !== record.recordId)) + recordingsStore.setRecordings( + recordingsStore.recordings.filter((rec) => rec.recordId !== record.recordId) + ); toast.success('Recording deleted'); }); }; @@ -40,7 +42,12 @@ function RecordsListItem(props: Props) { const menuItems = [{ icon: 'trash', text: 'Delete', onClick: onDelete }]; const onSave = () => { - recordingsStore.updateRecordingName(record.recordId, recordingTitle); + recordingsStore + .updateRecordingName(record.recordId, recordingTitle) + .then(() => { + toast.success('Name updated'); + }) + .catch(() => toast.error("Couldn't update name")); setEdit(false); }; diff --git a/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx b/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx index 0f5ea510f..a474c9cc8 100644 --- a/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx +++ b/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx @@ -186,7 +186,7 @@ function AssistActions({ )} {/* @ts-ignore wtf? */} - + {isEnterprise ? : null}
{/* @ts-ignore */} diff --git a/frontend/app/components/Session_/ScreenRecorder/ScreenRecorder.tsx b/frontend/app/components/Session_/ScreenRecorder/ScreenRecorder.tsx index 78c311266..345584e16 100644 --- a/frontend/app/components/Session_/ScreenRecorder/ScreenRecorder.tsx +++ b/frontend/app/components/Session_/ScreenRecorder/ScreenRecorder.tsx @@ -8,6 +8,7 @@ import { SessionRecordingStatus } from 'Player/MessageDistributor/managers/Assis let stopRecorderCb: () => void; import { recordingsService } from 'App/services'; import { toast } from 'react-toastify'; +import { durationFromMs, formatTimeOrDate } from 'App/date'; /** * "edge" || "edg/" chromium based edge (dev or canary) @@ -48,14 +49,16 @@ function ScreenRecorder({ const onSave = async (saveObj: { name: string; duration: number }, blob: Blob) => { try { toast.warn('Uploading the recording...'); - const url = await recordingsService.reserveUrl(siteId, saveObj); - const status = recordingsService.saveFile(url, blob); + const { URL, key } = await recordingsService.reserveUrl(siteId, { ...saveObj, sessionId }); + const status = recordingsService.saveFile(URL, blob); if (status) { + await recordingsService.confirmFile(siteId, { ...saveObj, sessionId }, key); toast.success('Session recording uploaded'); } } catch (e) { console.error(e); + toast.error("Couldn't upload the file"); } }; @@ -69,7 +72,11 @@ function ScreenRecorder({ }, [recordingState, isRecording]); const startRecording = async () => { - const stop = await screenRecorder(`${sessionId}_${new Date().getTime()}`, sessionId, onSave); + const stop = await screenRecorder( + `${formatTimeOrDate(new Date().getTime(), undefined, true)}_${sessionId}`, + sessionId, + onSave + ); stopRecorderCb = stop; setRecording(true); }; diff --git a/frontend/app/services/RecordingsService.ts b/frontend/app/services/RecordingsService.ts index 352a22e3b..f7885a36b 100644 --- a/frontend/app/services/RecordingsService.ts +++ b/frontend/app/services/RecordingsService.ts @@ -3,6 +3,7 @@ import APIClient from 'App/api_client'; interface RecordingData { name: string; duration: number; + sessionId: string; } interface FetchFilter { @@ -34,10 +35,10 @@ export default class RecordingsService { this.client = client || new APIClient(); } - reserveUrl(siteId: string, recordingData: RecordingData): Promise { + reserveUrl(siteId: string, recordingData: RecordingData): Promise<{ URL: string; key: string }> { return this.client.put(`/${siteId}/assist/save`, recordingData).then((r) => { if (r.ok) { - return r.json().then((j) => j.data.URL); + return r.json().then((j) => j.data); } else { throw new Error("Can't reserve space for recording: " + r.status); } @@ -58,6 +59,16 @@ export default class RecordingsService { }); } + confirmFile(siteId: string, recordingData: RecordingData, key: string): Promise { + return this.client.put(`/${siteId}/assist/save/done`, { ...recordingData, key }).then((r) => { + if (r.ok) { + return r.json().then((j) => j.data); + } else { + throw new Error("Can't confirm file saving: " + r.status); + } + }); + } + fetchRecordings(filters: FetchFilter): Promise { return this.client.post(`/assist/records`, filters).then((r) => { if (r.ok) { diff --git a/tracker/tracker-assist/src/ScreenRecordingState.ts b/tracker/tracker-assist/src/ScreenRecordingState.ts index 521a80370..8ceadec50 100644 --- a/tracker/tracker-assist/src/ScreenRecordingState.ts +++ b/tracker/tracker-assist/src/ScreenRecordingState.ts @@ -105,11 +105,18 @@ export default class ScreenRecordingState { Object.assign(stopButton.style, styles) stopButton.textContent = 'Stop Recording' stopButton.id = 'or-recording-border' + stopButton.setAttribute('data-openreplay-obscured', '') + stopButton.setAttribute('data-openreplay-hidden', '') + stopButton.setAttribute('data-openreplay-ignore', '') window.document.body.appendChild(stopButton) Object.entries(borderEmulationStyles).forEach(([key, style,]) => { Object.assign(borders[key].style, style) borders[key].id = 'or-recording-border' + + borders[key].setAttribute('data-openreplay-obscured', '') + borders[key].setAttribute('data-openreplay-hidden', '') + borders[key].setAttribute('data-openreplay-ignore', '') window.document.body.appendChild(borders[key]) }) From ee10c75e8a9bf3a68f96f75243769e615a579db8 Mon Sep 17 00:00:00 2001 From: sylenien Date: Mon, 21 Nov 2022 14:15:33 +0100 Subject: [PATCH 125/252] change(ui): some cleanup --- frontend/app/api_client.js | 4 ++-- frontend/app/components/Assist/Assist.tsx | 6 +++--- frontend/app/components/Assist/AssistView.tsx | 3 +-- .../components/Assist/RecordingsList/RecordsListItem.tsx | 4 ++-- .../components/Session_/ScreenRecorder/ScreenRecorder.tsx | 7 ++++--- frontend/app/mstore/recordingsStore.ts | 2 +- tracker/tracker-assist/src/ScreenRecordingState.ts | 5 +++-- 7 files changed, 16 insertions(+), 15 deletions(-) diff --git a/frontend/app/api_client.js b/frontend/app/api_client.js index 8b279b281..0e4699359 100644 --- a/frontend/app/api_client.js +++ b/frontend/app/api_client.js @@ -104,12 +104,12 @@ export default class APIClient { post(path, params, options) { this.init.method = 'POST'; - return this.fetch(path, params, undefined); + return this.fetch(path, params); } put(path, params, options) { this.init.method = 'PUT'; - return this.fetch(path, params, undefined); + return this.fetch(path, params); } delete(path, params, options) { diff --git a/frontend/app/components/Assist/Assist.tsx b/frontend/app/components/Assist/Assist.tsx index 7aa559d43..650b64d0e 100644 --- a/frontend/app/components/Assist/Assist.tsx +++ b/frontend/app/components/Assist/Assist.tsx @@ -19,7 +19,7 @@ function Assist(props: Props) { const isRecords = history.location.pathname.includes('recordings'); const redirect = (path: string) => { - history.push(path); + history.push(withSiteId(path, siteId)); }; return (
@@ -30,14 +30,14 @@ function Assist(props: Props) { id="menu-assist" title="Live Sessions" iconName="play-circle-light" - onClick={() => redirect(withSiteId(assist(), siteId))} + onClick={() => redirect(assist())} /> redirect(withSiteId(recordings(), siteId))} + onClick={() => redirect(recordings())} />
diff --git a/frontend/app/components/Assist/AssistView.tsx b/frontend/app/components/Assist/AssistView.tsx index 700820cbd..36e7b7137 100644 --- a/frontend/app/components/Assist/AssistView.tsx +++ b/frontend/app/components/Assist/AssistView.tsx @@ -1,12 +1,11 @@ import React from 'react'; import LiveSessionList from 'Shared/LiveSessionList'; import LiveSessionSearch from 'Shared/LiveSessionSearch'; -import cn from 'classnames' import AssistSearchField from './AssistSearchField'; function AssistView() { return ( -
+
diff --git a/frontend/app/components/Assist/RecordingsList/RecordsListItem.tsx b/frontend/app/components/Assist/RecordingsList/RecordsListItem.tsx index e4a0fe1cb..595e6241e 100644 --- a/frontend/app/components/Assist/RecordingsList/RecordsListItem.tsx +++ b/frontend/app/components/Assist/RecordingsList/RecordsListItem.tsx @@ -45,9 +45,9 @@ function RecordsListItem(props: Props) { recordingsStore .updateRecordingName(record.recordId, recordingTitle) .then(() => { - toast.success('Name updated'); + toast.success('Recording name updated'); }) - .catch(() => toast.error("Couldn't update name")); + .catch(() => toast.error("Couldn't update recording name")); setEdit(false); }; diff --git a/frontend/app/components/Session_/ScreenRecorder/ScreenRecorder.tsx b/frontend/app/components/Session_/ScreenRecorder/ScreenRecorder.tsx index 345584e16..f1f01e5b1 100644 --- a/frontend/app/components/Session_/ScreenRecorder/ScreenRecorder.tsx +++ b/frontend/app/components/Session_/ScreenRecorder/ScreenRecorder.tsx @@ -89,20 +89,21 @@ function ScreenRecorder({ const recordingRequest = () => { requestRecording(); - // startRecording() }; - if (!isSupported()) + if (!isSupported()) { return (
{/* @ts-ignore */} -
); + } + return (
+ + +
+ + +
+ + + {errors && ( +
+ {errors.map((error) => ( + + {error} + + ))} +
+ )} +
+ ); + } +} + +export default connect( + (state: any) => ({ + instance: state.getIn(['slack', 'instance']), + saving: state.getIn(['slack', 'saveRequest', 'loading']), + errors: state.getIn(['slack', 'saveRequest', 'errors']), + }), + { edit, save, init, remove, update } +)(TeamsAddForm); diff --git a/frontend/app/components/Client/Integrations/Teams/TeamsChannelList.tsx b/frontend/app/components/Client/Integrations/Teams/TeamsChannelList.tsx new file mode 100644 index 000000000..9c5189705 --- /dev/null +++ b/frontend/app/components/Client/Integrations/Teams/TeamsChannelList.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { NoContent } from 'UI'; +import { remove, edit, init } from 'Duck/integrations/teams'; +import DocLink from 'Shared/DocLink/DocLink'; + +function TeamsChannelList(props: { list: any, edit: (inst: any) => any, onEdit: () => void }) { + const { list } = props; + + const onEdit = (instance: Record) => { + props.edit(instance); + props.onEdit(); + }; + + return ( +
+ +
+ Integrate MS Teams with OpenReplay and share insights with the rest of the team, directly from the recording page. +
+ +
+ } + size="small" + show={list.size === 0} + > + {list.map((c: any) => ( +
onEdit(c)} + > +
+
{c.name}
+
{c.endpoint}
+
+
+ ))} + +
+ ); +} + +export default connect( + (state: any) => ({ + list: state.getIn(['teams', 'list']), + }), + { remove, edit, init } +)(TeamsChannelList); diff --git a/frontend/app/components/Client/Integrations/Teams/index.tsx b/frontend/app/components/Client/Integrations/Teams/index.tsx new file mode 100644 index 000000000..d5fdba6fc --- /dev/null +++ b/frontend/app/components/Client/Integrations/Teams/index.tsx @@ -0,0 +1,55 @@ +import React, { useEffect } from 'react'; +import TeamsChannelList from './TeamsChannelList'; +import { fetchList, init } from 'Duck/integrations/teams'; +import { connect } from 'react-redux'; +import SlackAddForm from './SlackAddForm'; +import { Button } from 'UI'; + +interface Props { + onEdit?: (integration: any) => void; + istance: any; + fetchList: any; + init: any; +} +const MSTeams = (props: Props) => { + const [active, setActive] = React.useState(false); + + const onEdit = () => { + setActive(true); + }; + + const onNew = () => { + setActive(true); + props.init({}); + } + + useEffect(() => { + props.fetchList(); + }, []); + + return ( +
+ {active && ( +
+ setActive(false)} /> +
+ )} +
+
+

Microsoft Teams

+
+ +
+
+ ); +}; + +MSTeams.displayName = 'MSTeams'; + +export default connect( + (state: any) => ({ + istance: state.getIn(['teams', 'instance']), + }), + { fetchList, init } +)(MSTeams); diff --git a/frontend/app/components/ui/SVG.tsx b/frontend/app/components/ui/SVG.tsx index 3a23f63b8..a04c041a7 100644 --- a/frontend/app/components/ui/SVG.tsx +++ b/frontend/app/components/ui/SVG.tsx @@ -1,7 +1,7 @@ import React from 'react'; -export type IconNames = 'alarm-clock' | 'alarm-plus' | 'all-sessions' | 'analytics' | 'anchor' | 'arrow-alt-square-right' | 'arrow-bar-left' | 'arrow-clockwise' | 'arrow-down-short' | 'arrow-down' | 'arrow-repeat' | 'arrow-right-short' | 'arrow-square-left' | 'arrow-square-right' | 'arrow-up-short' | 'arrow-up' | 'arrows-angle-extend' | 'avatar/icn_bear' | 'avatar/icn_beaver' | 'avatar/icn_bird' | 'avatar/icn_bison' | 'avatar/icn_camel' | 'avatar/icn_chameleon' | 'avatar/icn_deer' | 'avatar/icn_dog' | 'avatar/icn_dolphin' | 'avatar/icn_elephant' | 'avatar/icn_fish' | 'avatar/icn_fox' | 'avatar/icn_gorilla' | 'avatar/icn_hippo' | 'avatar/icn_horse' | 'avatar/icn_hyena' | 'avatar/icn_kangaroo' | 'avatar/icn_lemur' | 'avatar/icn_mammel' | 'avatar/icn_monkey' | 'avatar/icn_moose' | 'avatar/icn_panda' | 'avatar/icn_penguin' | 'avatar/icn_porcupine' | 'avatar/icn_quail' | 'avatar/icn_rabbit' | 'avatar/icn_rhino' | 'avatar/icn_sea_horse' | 'avatar/icn_sheep' | 'avatar/icn_snake' | 'avatar/icn_squirrel' | 'avatar/icn_tapir' | 'avatar/icn_turtle' | 'avatar/icn_vulture' | 'avatar/icn_wild1' | 'avatar/icn_wild_bore' | 'ban' | 'bar-chart-line' | 'bar-pencil' | 'bell-fill' | 'bell-plus' | 'bell-slash' | 'bell' | 'binoculars' | 'book' | 'browser/browser' | 'browser/chrome' | 'browser/edge' | 'browser/electron' | 'browser/facebook' | 'browser/firefox' | 'browser/ie' | 'browser/opera' | 'browser/safari' | 'bullhorn' | 'business-time' | 'calendar-alt' | 'calendar-check' | 'calendar-day' | 'calendar' | 'call' | 'camera-alt' | 'camera-video-off' | 'camera-video' | 'camera' | 'caret-down-fill' | 'caret-left-fill' | 'caret-right-fill' | 'caret-up-fill' | 'chat-dots' | 'chat-right-text' | 'chat-square-quote' | 'check-circle-fill' | 'check-circle' | 'check' | 'chevron-double-left' | 'chevron-double-right' | 'chevron-down' | 'chevron-left' | 'chevron-right' | 'chevron-up' | 'circle-fill' | 'circle' | 'clipboard-list-check' | 'clock' | 'close' | 'cloud-fog2-fill' | 'code' | 'cog' | 'cogs' | 'collection' | 'columns-gap-filled' | 'columns-gap' | 'console/error' | 'console/exception' | 'console/info' | 'console/warning' | 'console' | 'controller' | 'cookies' | 'copy' | 'credit-card-front' | 'cubes' | 'dash' | 'dashboard-icn' | 'desktop' | 'device' | 'diagram-3' | 'dizzy' | 'door-closed' | 'doublecheck' | 'download' | 'drag' | 'edit' | 'ellipsis-v' | 'enter' | 'envelope' | 'errors-icon' | 'event/click' | 'event/clickrage' | 'event/code' | 'event/i-cursor' | 'event/input' | 'event/link' | 'event/location' | 'event/resize' | 'event/view' | 'exclamation-circle' | 'expand-wide' | 'explosion' | 'external-link-alt' | 'eye-slash-fill' | 'eye-slash' | 'eye' | 'fetch' | 'file-code' | 'file-medical-alt' | 'file-pdf' | 'file' | 'filter' | 'filters/arrow-return-right' | 'filters/browser' | 'filters/click' | 'filters/clickrage' | 'filters/code' | 'filters/console' | 'filters/country' | 'filters/cpu-load' | 'filters/custom' | 'filters/device' | 'filters/dom-complete' | 'filters/duration' | 'filters/error' | 'filters/fetch-failed' | 'filters/fetch' | 'filters/file-code' | 'filters/graphql' | 'filters/i-cursor' | 'filters/input' | 'filters/lcpt' | 'filters/link' | 'filters/location' | 'filters/memory-load' | 'filters/metadata' | 'filters/os' | 'filters/perfromance-network-request' | 'filters/platform' | 'filters/referrer' | 'filters/resize' | 'filters/rev-id' | 'filters/state-action' | 'filters/ttfb' | 'filters/user-alt' | 'filters/userid' | 'filters/view' | 'flag-na' | 'folder-plus' | 'folder2' | 'fullscreen' | 'funnel/cpu-fill' | 'funnel/cpu' | 'funnel/dizzy' | 'funnel/emoji-angry-fill' | 'funnel/emoji-angry' | 'funnel/emoji-dizzy-fill' | 'funnel/exclamation-circle-fill' | 'funnel/exclamation-circle' | 'funnel/file-earmark-break-fill' | 'funnel/file-earmark-break' | 'funnel/file-earmark-minus-fill' | 'funnel/file-earmark-minus' | 'funnel/file-medical-alt' | 'funnel/file-x' | 'funnel/hdd-fill' | 'funnel/hourglass-top' | 'funnel/image-fill' | 'funnel/image' | 'funnel/microchip' | 'funnel/mouse' | 'funnel/patch-exclamation-fill' | 'funnel/sd-card' | 'funnel-fill' | 'funnel-new' | 'funnel' | 'gear-fill' | 'gear' | 'geo-alt-fill-custom' | 'github' | 'graph-up-arrow' | 'graph-up' | 'grid-3x3' | 'grid-check' | 'grid-horizontal' | 'grip-horizontal' | 'hash' | 'hdd-stack' | 'headset' | 'heart-rate' | 'high-engagement' | 'history' | 'hourglass-start' | 'id-card' | 'image' | 'info-circle-fill' | 'info-circle' | 'info-square' | 'info' | 'inspect' | 'integrations/assist' | 'integrations/bugsnag-text' | 'integrations/bugsnag' | 'integrations/cloudwatch-text' | 'integrations/cloudwatch' | 'integrations/datadog' | 'integrations/elasticsearch-text' | 'integrations/elasticsearch' | 'integrations/github' | 'integrations/graphql' | 'integrations/jira-text' | 'integrations/jira' | 'integrations/mobx' | 'integrations/newrelic-text' | 'integrations/newrelic' | 'integrations/ngrx' | 'integrations/openreplay-text' | 'integrations/openreplay' | 'integrations/redux' | 'integrations/rollbar-text' | 'integrations/rollbar' | 'integrations/segment' | 'integrations/sentry-text' | 'integrations/sentry' | 'integrations/slack-bw' | 'integrations/slack' | 'integrations/stackdriver' | 'integrations/sumologic-text' | 'integrations/sumologic' | 'integrations/vuejs' | 'journal-code' | 'layer-group' | 'lightbulb-on' | 'lightbulb' | 'link-45deg' | 'list-alt' | 'list-arrow' | 'list-ul' | 'list' | 'lock-alt' | 'magic' | 'map-marker-alt' | 'memory' | 'mic-mute' | 'mic' | 'minus' | 'mobile' | 'mouse-alt' | 'network' | 'next1' | 'no-dashboard' | 'no-metrics-chart' | 'no-metrics' | 'os/android' | 'os/chrome_os' | 'os/fedora' | 'os/ios' | 'os/linux' | 'os/mac_os_x' | 'os/other' | 'os/ubuntu' | 'os/windows' | 'os' | 'pause-fill' | 'pause' | 'pdf-download' | 'pencil-stop' | 'pencil' | 'percent' | 'performance-icon' | 'person-fill' | 'person' | 'pie-chart-fill' | 'pin-fill' | 'play-circle-light' | 'play-circle' | 'play-fill-new' | 'play-fill' | 'play-hover' | 'play' | 'plus-circle' | 'plus' | 'prev1' | 'puzzle-piece' | 'puzzle' | 'question-circle' | 'question-lg' | 'quote-left' | 'quote-right' | 'quotes' | 'record-circle' | 'redo-back' | 'redo' | 'remote-control' | 'replay-10' | 'resources-icon' | 'safe-fill' | 'safe' | 'sandglass' | 'search' | 'search_notification' | 'server' | 'share-alt' | 'shield-lock' | 'signup' | 'skip-forward-fill' | 'skip-forward' | 'slack' | 'slash-circle' | 'sliders' | 'social/slack' | 'social/trello' | 'spinner' | 'star-solid' | 'star' | 'step-forward' | 'stop-record-circle' | 'stopwatch' | 'store' | 'sync-alt' | 'table-new' | 'table' | 'tablet-android' | 'tachometer-slow' | 'tachometer-slowest' | 'tags' | 'team-funnel' | 'telephone-fill' | 'telephone' | 'text-paragraph' | 'tools' | 'trash' | 'turtle' | 'user-alt' | 'user-circle' | 'user-friends' | 'users' | 'vendors/graphql' | 'vendors/mobx' | 'vendors/ngrx' | 'vendors/redux' | 'vendors/vuex' | 'web-vitals' | 'wifi' | 'window-alt' | 'window-restore' | 'window-x' | 'window' | 'zoom-in'; +export type IconNames = 'alarm-clock' | 'alarm-plus' | 'all-sessions' | 'analytics' | 'anchor' | 'arrow-alt-square-right' | 'arrow-bar-left' | 'arrow-clockwise' | 'arrow-down-short' | 'arrow-down' | 'arrow-repeat' | 'arrow-right-short' | 'arrow-square-left' | 'arrow-square-right' | 'arrow-up-short' | 'arrow-up' | 'arrows-angle-extend' | 'avatar/icn_bear' | 'avatar/icn_beaver' | 'avatar/icn_bird' | 'avatar/icn_bison' | 'avatar/icn_camel' | 'avatar/icn_chameleon' | 'avatar/icn_deer' | 'avatar/icn_dog' | 'avatar/icn_dolphin' | 'avatar/icn_elephant' | 'avatar/icn_fish' | 'avatar/icn_fox' | 'avatar/icn_gorilla' | 'avatar/icn_hippo' | 'avatar/icn_horse' | 'avatar/icn_hyena' | 'avatar/icn_kangaroo' | 'avatar/icn_lemur' | 'avatar/icn_mammel' | 'avatar/icn_monkey' | 'avatar/icn_moose' | 'avatar/icn_panda' | 'avatar/icn_penguin' | 'avatar/icn_porcupine' | 'avatar/icn_quail' | 'avatar/icn_rabbit' | 'avatar/icn_rhino' | 'avatar/icn_sea_horse' | 'avatar/icn_sheep' | 'avatar/icn_snake' | 'avatar/icn_squirrel' | 'avatar/icn_tapir' | 'avatar/icn_turtle' | 'avatar/icn_vulture' | 'avatar/icn_wild1' | 'avatar/icn_wild_bore' | 'ban' | 'bar-chart-line' | 'bar-pencil' | 'bell-fill' | 'bell-plus' | 'bell-slash' | 'bell' | 'binoculars' | 'book' | 'browser/browser' | 'browser/chrome' | 'browser/edge' | 'browser/electron' | 'browser/facebook' | 'browser/firefox' | 'browser/ie' | 'browser/opera' | 'browser/safari' | 'bullhorn' | 'business-time' | 'calendar-alt' | 'calendar-check' | 'calendar-day' | 'calendar' | 'call' | 'camera-alt' | 'camera-video-off' | 'camera-video' | 'camera' | 'caret-down-fill' | 'caret-left-fill' | 'caret-right-fill' | 'caret-up-fill' | 'chat-dots' | 'chat-right-text' | 'chat-square-quote' | 'check-circle-fill' | 'check-circle' | 'check' | 'chevron-double-left' | 'chevron-double-right' | 'chevron-down' | 'chevron-left' | 'chevron-right' | 'chevron-up' | 'circle-fill' | 'circle' | 'clipboard-list-check' | 'clock' | 'close' | 'cloud-fog2-fill' | 'code' | 'cog' | 'cogs' | 'collection' | 'columns-gap-filled' | 'columns-gap' | 'console/error' | 'console/exception' | 'console/info' | 'console/warning' | 'console' | 'controller' | 'cookies' | 'copy' | 'credit-card-front' | 'cubes' | 'dash' | 'dashboard-icn' | 'desktop' | 'device' | 'diagram-3' | 'dizzy' | 'door-closed' | 'doublecheck' | 'download' | 'drag' | 'edit' | 'ellipsis-v' | 'enter' | 'envelope' | 'errors-icon' | 'event/click' | 'event/clickrage' | 'event/code' | 'event/i-cursor' | 'event/input' | 'event/link' | 'event/location' | 'event/resize' | 'event/view' | 'exclamation-circle' | 'expand-wide' | 'explosion' | 'external-link-alt' | 'eye-slash-fill' | 'eye-slash' | 'eye' | 'fetch' | 'file-code' | 'file-medical-alt' | 'file-pdf' | 'file' | 'filter' | 'filters/arrow-return-right' | 'filters/browser' | 'filters/click' | 'filters/clickrage' | 'filters/code' | 'filters/console' | 'filters/country' | 'filters/cpu-load' | 'filters/custom' | 'filters/device' | 'filters/dom-complete' | 'filters/duration' | 'filters/error' | 'filters/fetch-failed' | 'filters/fetch' | 'filters/file-code' | 'filters/graphql' | 'filters/i-cursor' | 'filters/input' | 'filters/lcpt' | 'filters/link' | 'filters/location' | 'filters/memory-load' | 'filters/metadata' | 'filters/os' | 'filters/perfromance-network-request' | 'filters/platform' | 'filters/referrer' | 'filters/resize' | 'filters/rev-id' | 'filters/state-action' | 'filters/ttfb' | 'filters/user-alt' | 'filters/userid' | 'filters/view' | 'flag-na' | 'folder-plus' | 'folder2' | 'fullscreen' | 'funnel/cpu-fill' | 'funnel/cpu' | 'funnel/dizzy' | 'funnel/emoji-angry-fill' | 'funnel/emoji-angry' | 'funnel/emoji-dizzy-fill' | 'funnel/exclamation-circle-fill' | 'funnel/exclamation-circle' | 'funnel/file-earmark-break-fill' | 'funnel/file-earmark-break' | 'funnel/file-earmark-minus-fill' | 'funnel/file-earmark-minus' | 'funnel/file-medical-alt' | 'funnel/file-x' | 'funnel/hdd-fill' | 'funnel/hourglass-top' | 'funnel/image-fill' | 'funnel/image' | 'funnel/microchip' | 'funnel/mouse' | 'funnel/patch-exclamation-fill' | 'funnel/sd-card' | 'funnel-fill' | 'funnel-new' | 'funnel' | 'gear-fill' | 'gear' | 'geo-alt-fill-custom' | 'github' | 'graph-up-arrow' | 'graph-up' | 'grid-3x3' | 'grid-check' | 'grid-horizontal' | 'grip-horizontal' | 'hash' | 'hdd-stack' | 'headset' | 'heart-rate' | 'high-engagement' | 'history' | 'hourglass-start' | 'id-card' | 'image' | 'info-circle-fill' | 'info-circle' | 'info-square' | 'info' | 'inspect' | 'integrations/assist' | 'integrations/bugsnag-text' | 'integrations/bugsnag' | 'integrations/cloudwatch-text' | 'integrations/cloudwatch' | 'integrations/datadog' | 'integrations/elasticsearch-text' | 'integrations/elasticsearch' | 'integrations/github' | 'integrations/graphql' | 'integrations/jira-text' | 'integrations/jira' | 'integrations/mobx' | 'integrations/newrelic-text' | 'integrations/newrelic' | 'integrations/ngrx' | 'integrations/openreplay-text' | 'integrations/openreplay' | 'integrations/redux' | 'integrations/rollbar-text' | 'integrations/rollbar' | 'integrations/segment' | 'integrations/sentry-text' | 'integrations/sentry' | 'integrations/slack-bw' | 'integrations/slack' | 'integrations/stackdriver' | 'integrations/sumologic-text' | 'integrations/sumologic' | 'integrations/teams' | 'integrations/vuejs' | 'journal-code' | 'layer-group' | 'lightbulb-on' | 'lightbulb' | 'link-45deg' | 'list-alt' | 'list-arrow' | 'list-ul' | 'list' | 'lock-alt' | 'magic' | 'map-marker-alt' | 'memory' | 'mic-mute' | 'mic' | 'minus' | 'mobile' | 'mouse-alt' | 'network' | 'next1' | 'no-dashboard' | 'no-metrics-chart' | 'no-metrics' | 'os/android' | 'os/chrome_os' | 'os/fedora' | 'os/ios' | 'os/linux' | 'os/mac_os_x' | 'os/other' | 'os/ubuntu' | 'os/windows' | 'os' | 'pause-fill' | 'pause' | 'pdf-download' | 'pencil-stop' | 'pencil' | 'percent' | 'performance-icon' | 'person-fill' | 'person' | 'pie-chart-fill' | 'pin-fill' | 'play-circle-light' | 'play-circle' | 'play-fill-new' | 'play-fill' | 'play-hover' | 'play' | 'plus-circle' | 'plus' | 'prev1' | 'puzzle-piece' | 'puzzle' | 'question-circle' | 'question-lg' | 'quote-left' | 'quote-right' | 'quotes' | 'record-circle' | 'redo-back' | 'redo' | 'remote-control' | 'replay-10' | 'resources-icon' | 'safe-fill' | 'safe' | 'sandglass' | 'search' | 'search_notification' | 'server' | 'share-alt' | 'shield-lock' | 'signup' | 'skip-forward-fill' | 'skip-forward' | 'slack' | 'slash-circle' | 'sliders' | 'social/slack' | 'social/trello' | 'spinner' | 'star-solid' | 'star' | 'step-forward' | 'stop-record-circle' | 'stopwatch' | 'store' | 'sync-alt' | 'table-new' | 'table' | 'tablet-android' | 'tachometer-slow' | 'tachometer-slowest' | 'tags' | 'team-funnel' | 'telephone-fill' | 'telephone' | 'text-paragraph' | 'tools' | 'trash' | 'turtle' | 'user-alt' | 'user-circle' | 'user-friends' | 'users' | 'vendors/graphql' | 'vendors/mobx' | 'vendors/ngrx' | 'vendors/redux' | 'vendors/vuex' | 'web-vitals' | 'wifi' | 'window-alt' | 'window-restore' | 'window-x' | 'window' | 'zoom-in'; interface Props { name: IconNames; @@ -289,6 +289,7 @@ const SVG = (props: Props) => { case 'integrations/stackdriver': return ; case 'integrations/sumologic-text': return ; case 'integrations/sumologic': return ; + case 'integrations/teams': return ; case 'integrations/vuejs': return ; case 'journal-code': return ; case 'layer-group': return ; diff --git a/frontend/app/duck/integrations/index.js b/frontend/app/duck/integrations/index.js index 0274f7c80..dec847235 100644 --- a/frontend/app/duck/integrations/index.js +++ b/frontend/app/duck/integrations/index.js @@ -12,6 +12,7 @@ import GithubConfig from 'Types/integrations/githubConfig'; import IssueTracker from 'Types/integrations/issueTracker'; import slack from './slack'; import integrations from './integrations'; +import teams from './teams' import { createIntegrationReducer } from './reducer'; @@ -29,6 +30,7 @@ export default { github: createIntegrationReducer('github', GithubConfig), issues: createIntegrationReducer('issues', IssueTracker), slack, + teams, integrations, }; diff --git a/frontend/app/duck/integrations/teams.js b/frontend/app/duck/integrations/teams.js new file mode 100644 index 000000000..9d9add27a --- /dev/null +++ b/frontend/app/duck/integrations/teams.js @@ -0,0 +1,88 @@ +import { Map, List } from 'immutable'; +import withRequestState, { RequestTypes } from 'Duck/requestStateCreator'; +import Config from 'Types/integrations/slackConfig'; +import { createItemInListUpdater } from '../funcTools/tools'; + +const SAVE = new RequestTypes('msteams/SAVE'); +const UPDATE = new RequestTypes('msteams/UPDATE'); +const REMOVE = new RequestTypes('msteams/REMOVE'); +const FETCH_LIST = new RequestTypes('msteams/FETCH_LIST'); +const EDIT = 'msteams/EDIT'; +const INIT = 'msteams/INIT'; +const idKey = 'webhookId'; +const itemInListUpdater = createItemInListUpdater(idKey); + +const initialState = Map({ + instance: Config(), + list: List(), +}); + +const reducer = (state = initialState, action = {}) => { + switch (action.type) { + case FETCH_LIST.SUCCESS: + return state.set('list', List(action.data).map(Config)); + case UPDATE.SUCCESS: + case SAVE.SUCCESS: + const config = Config(action.data); + return state.update('list', itemInListUpdater(config)).set('instance', config); + case REMOVE.SUCCESS: + return state.update('list', (list) => list.filter((item) => item.webhookId !== action.id)).set('instance', Config()); + case EDIT: + return state.mergeIn(['instance'], action.instance); + case INIT: + return state.set('instance', Config(action.instance)); + } + return state; +}; + +export default withRequestState( + { + fetchRequest: FETCH_LIST, + saveRequest: SAVE, + removeRequest: REMOVE, + }, + reducer +); + +export function fetchList() { + return { + types: FETCH_LIST.toArray(), + call: (client) => client.get('/integrations/msteams/channels'), + }; +} + +export function save(instance) { + return { + types: SAVE.toArray(), + call: (client) => client.post(`/integrations/msteams`, instance.toData()), + }; +} + +export function update(instance) { + return { + types: UPDATE.toArray(), + call: (client) => client.put(`/integrations/msteams/${instance.webhookId}`, instance.toData()), + }; +} + +export function edit(instance) { + return { + type: EDIT, + instance, + }; +} + +export function init(instance) { + return { + type: INIT, + instance, + }; +} + +export function remove(id) { + return { + types: REMOVE.toArray(), + call: (client) => client.delete(`/integrations/msteams/${id}`), + id, + }; +} diff --git a/frontend/app/svg/icons/integrations/teams.svg b/frontend/app/svg/icons/integrations/teams.svg new file mode 100644 index 000000000..e93adb2b8 --- /dev/null +++ b/frontend/app/svg/icons/integrations/teams.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + From 02027da02b46ec49e540f2881ae9e0d3ab8af76f Mon Sep 17 00:00:00 2001 From: sylenien Date: Tue, 6 Dec 2022 16:31:20 +0100 Subject: [PATCH 158/252] change(ui): set up msteams for share popup and note creation --- frontend/app/Router.js | 6 + frontend/app/api_client.js | 1 + frontend/app/components/Alerts/AlertForm.js | 27 ++- frontend/app/components/Alerts/AlertItem.js | 4 +- .../Integrations/Teams/TeamsAddForm.tsx | 15 +- .../Client/Integrations/Teams/index.tsx | 4 +- .../components/Client/Webhooks/Webhooks.js | 2 +- .../Alerts/AlertForm/NotifyHooks.tsx | 24 ++- .../components/Alerts/AlertListItem.tsx | 3 + .../Player/Controls/components/CreateNote.tsx | 115 +++++++++---- .../Controls/components/styles.module.css | 3 +- .../IntegrateSlackButton.js | 19 +- .../shared/SharePopup/SharePopup.js | 162 ++++++++++++------ .../shared/SharePopup/sharePopup.module.css | 7 +- frontend/app/components/ui/Message/Message.js | 2 +- frontend/app/components/ui/SVG.tsx | 3 +- frontend/app/duck/integrations/slack.js | 12 +- frontend/app/duck/integrations/teams.js | 11 ++ frontend/app/mstore/notesStore.ts | 9 + frontend/app/services/NotesService.ts | 11 ++ .../svg/icons/integrations/teams-white.svg | 4 + 21 files changed, 328 insertions(+), 116 deletions(-) create mode 100644 frontend/app/svg/icons/integrations/teams-white.svg diff --git a/frontend/app/Router.js b/frontend/app/Router.js index d1be5a569..86d878a1c 100644 --- a/frontend/app/Router.js +++ b/frontend/app/Router.js @@ -195,6 +195,12 @@ class Router extends React.Component { state: tenantId, }); break; + case '/integrations/msteams': + client.post('integrations/msteams/add', { + code: location.search.split('=')[1], + state: tenantId, + }); + break; } return ; }} diff --git a/frontend/app/api_client.js b/frontend/app/api_client.js index 0e4699359..8985d0686 100644 --- a/frontend/app/api_client.js +++ b/frontend/app/api_client.js @@ -11,6 +11,7 @@ const siteIdRequiredPaths = [ '/metadata', '/integrations/sentry/events', '/integrations/slack/notify', + '/integrations/msteams/notify', '/assignments', '/integration/sources', '/issue_types', diff --git a/frontend/app/components/Alerts/AlertForm.js b/frontend/app/components/Alerts/AlertForm.js index 6604574e0..9046b4069 100644 --- a/frontend/app/components/Alerts/AlertForm.js +++ b/frontend/app/components/Alerts/AlertForm.js @@ -1,6 +1,5 @@ import React, { useEffect } from 'react'; -import { Button, Form, Input, SegmentSelection, Checkbox, Message, Link, Icon } from 'UI'; -import { alertMetrics as metrics } from 'App/constants'; +import { Button, Form, Input, SegmentSelection, Checkbox, Icon } from 'UI'; import { alertConditions as conditions } from 'App/constants'; import { client, CLIENT_TABS } from 'App/routes'; import { connect } from 'react-redux'; @@ -47,12 +46,12 @@ const AlertForm = (props) => { const { instance, slackChannels, + msTeamsChannels, webhooks, loading, onDelete, deleting, triggerOptions, - metricId, style = { width: '580px', height: '100vh' }, } = props; const write = ({ target: { value, name } }) => props.edit({ [name]: value }); @@ -241,6 +240,14 @@ const AlertForm = (props) => { onClick={onChangeCheck} label="Slack" /> + {
)} + {instance.msteams && ( +
+ +
+ props.edit({ msTeamsInput: selected })} + /> +
+
+ )} {instance.email && (
diff --git a/frontend/app/components/Alerts/AlertItem.js b/frontend/app/components/Alerts/AlertItem.js index 9dbb204b8..76431bf77 100644 --- a/frontend/app/components/Alerts/AlertItem.js +++ b/frontend/app/components/Alerts/AlertItem.js @@ -17,6 +17,8 @@ const AlertItem = props => { const getNotifyChannel = alert => { let str = ''; + if (alert.msteams) + str = 'MS Teams' if (alert.slack) str = 'Slack'; if (alert.email) @@ -36,7 +38,7 @@ const AlertItem = props => { className={cn(stl.wrapper, 'p-4 py-6 relative group cursor-pointer', { [stl.active]: active })} onClick={onEdit} id="alert-item" - > + >
{alert.name}
diff --git a/frontend/app/components/Client/Integrations/Teams/TeamsAddForm.tsx b/frontend/app/components/Client/Integrations/Teams/TeamsAddForm.tsx index 018a5f0ca..04b8e9451 100644 --- a/frontend/app/components/Client/Integrations/Teams/TeamsAddForm.tsx +++ b/frontend/app/components/Client/Integrations/Teams/TeamsAddForm.tsx @@ -10,6 +10,7 @@ interface Props { init: (inst: any) => void; update: (inst: any) => void; remove: (id: string) => void; + onClose: () => void; instance: any; saving: boolean; errors: any; @@ -29,7 +30,7 @@ class TeamsAddForm extends React.PureComponent { } }; - remove = async (id) => { + remove = async (id: string) => { if ( await confirm({ header: 'Confirm', @@ -41,7 +42,7 @@ class TeamsAddForm extends React.PureComponent { } }; - write = ({ target: { name, value } }) => this.props.edit({ [name]: value }); + write = ({ target: { name, value } }: { target: { name: string, value: string }}) => this.props.edit({ [name]: value }); render() { const { instance, saving, errors, onClose } = this.props; @@ -91,8 +92,8 @@ class TeamsAddForm extends React.PureComponent { {errors && (
- {errors.map((error) => ( - + {errors.map((error: any) => ( + {error} ))} @@ -105,9 +106,9 @@ class TeamsAddForm extends React.PureComponent { export default connect( (state: any) => ({ - instance: state.getIn(['slack', 'instance']), - saving: state.getIn(['slack', 'saveRequest', 'loading']), - errors: state.getIn(['slack', 'saveRequest', 'errors']), + instance: state.getIn(['teams', 'instance']), + saving: state.getIn(['teams', 'saveRequest', 'loading']), + errors: state.getIn(['teams', 'saveRequest', 'errors']), }), { edit, save, init, remove, update } )(TeamsAddForm); diff --git a/frontend/app/components/Client/Integrations/Teams/index.tsx b/frontend/app/components/Client/Integrations/Teams/index.tsx index d5fdba6fc..4814697b8 100644 --- a/frontend/app/components/Client/Integrations/Teams/index.tsx +++ b/frontend/app/components/Client/Integrations/Teams/index.tsx @@ -2,7 +2,7 @@ import React, { useEffect } from 'react'; import TeamsChannelList from './TeamsChannelList'; import { fetchList, init } from 'Duck/integrations/teams'; import { connect } from 'react-redux'; -import SlackAddForm from './SlackAddForm'; +import TeamsAddForm from './TeamsAddForm'; import { Button } from 'UI'; interface Props { @@ -31,7 +31,7 @@ const MSTeams = (props: Props) => {
{active && (
- setActive(false)} /> + setActive(false)} />
)}
diff --git a/frontend/app/components/Client/Webhooks/Webhooks.js b/frontend/app/components/Client/Webhooks/Webhooks.js index e005a893a..a87ac2298 100644 --- a/frontend/app/components/Client/Webhooks/Webhooks.js +++ b/frontend/app/components/Client/Webhooks/Webhooks.js @@ -16,7 +16,7 @@ function Webhooks(props) { const { webhooks, loading } = props; const { showModal, hideModal } = useModal(); - const noSlackWebhooks = webhooks.filter((hook) => hook.type !== 'slack'); + const noSlackWebhooks = webhooks.filter((hook) => hook.type === 'webhook'); useEffect(() => { props.fetchList(); }, []); diff --git a/frontend/app/components/Dashboard/components/Alerts/AlertForm/NotifyHooks.tsx b/frontend/app/components/Dashboard/components/Alerts/AlertForm/NotifyHooks.tsx index 921c7ba9b..76d99dc6b 100644 --- a/frontend/app/components/Dashboard/components/Alerts/AlertForm/NotifyHooks.tsx +++ b/frontend/app/components/Dashboard/components/Alerts/AlertForm/NotifyHooks.tsx @@ -6,6 +6,7 @@ interface INotifyHooks { instance: Alert; onChangeCheck: (e: React.ChangeEvent) => void; slackChannels: Array; + msTeamsChannels: Array; validateEmail: (value: string) => boolean; edit: (data: any) => void; hooks: Array; @@ -16,6 +17,7 @@ function NotifyHooks({ onChangeCheck, slackChannels, validateEmail, + msTeamsChannels, hooks, edit, }: INotifyHooks) { @@ -49,7 +51,7 @@ function NotifyHooks({ {instance.slack && (
- +
)} + {instance.msteams && ( +
+ +
+ edit({ msteamsInput: selected })} + /> +
+
+ )} + {instance.email && (
- +
- +
, webhooks: Array) => { str = 'Slack'; str += alert.slackInput.length > 0 ? getSlackChannels() : ''; } + if (alert.msteams) { + str = 'MS Teams' + } if (alert.email) { str += (str === '' ? '' : ' and ') + (alert.emailInput.length > 1 ? 'Emails' : 'Email'); str += alert.emailInput.length > 0 ? ' (' + alert.emailInput.join(', ') + ')' : ''; diff --git a/frontend/app/components/Session_/Player/Controls/components/CreateNote.tsx b/frontend/app/components/Session_/Player/Controls/components/CreateNote.tsx index 8ac9d93f4..68f510de6 100644 --- a/frontend/app/components/Session_/Player/Controls/components/CreateNote.tsx +++ b/frontend/app/components/Session_/Player/Controls/components/CreateNote.tsx @@ -8,8 +8,10 @@ import stl from './styles.module.css'; 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 Select from 'Shared/Select'; -import { TeamBadge } from 'Shared/SessionListContainer/components/Notes' +import { TeamBadge } from 'Shared/SessionListContainer/components/Notes'; import { List } from 'immutable'; interface Props { @@ -22,7 +24,9 @@ interface Props { isEdit: string; editNote: WriteNote; slackChannels: List>; + teamsChannels: List>; fetchSlack: () => void; + fetchTeams: () => void; } function CreateNote({ @@ -36,12 +40,18 @@ function CreateNote({ updateNote, slackChannels, fetchSlack, + teamsChannels, + fetchTeams, }: Props) { const [text, setText] = React.useState(''); - const [channel, setChannel] = React.useState(''); + const [slackChannel, setSlackChannel] = React.useState(''); + const [teamsChannel, setTeamsChannel] = React.useState(''); const [isPublic, setPublic] = React.useState(false); const [tag, setTag] = React.useState(TAGS[0]); const [useTimestamp, setUseTs] = React.useState(true); + const [useSlack, setSlack] = React.useState(false); + const [useTeams, setTeams] = React.useState(false); + const inputRef = React.createRef(); const { notesStore } = useStore(); @@ -59,6 +69,7 @@ function CreateNote({ React.useEffect(() => { if (inputRef.current && isVisible) { fetchSlack(); + fetchTeams(); inputRef.current.focus(); } }, [isVisible]); @@ -75,17 +86,20 @@ function CreateNote({ isPublic, }; const onSuccess = (noteId: string) => { - if (channel) { - notesStore.sendSlackNotification(noteId, channel) + if (slackChannel) { + notesStore.sendSlackNotification(noteId, slackChannel); } - } + if (teamsChannel) { + notesStore.sendMsTeamsNotification(noteId, teamsChannel); + } + }; if (isEdit) { return notesStore .updateNote(editNote.noteId, note) .then((r) => { toast.success('Note updated'); notesStore.fetchSessionNotes(sessionId).then((notes) => { - onSuccess(editNote.noteId) + onSuccess(editNote.noteId); updateNote(r); }); }) @@ -103,7 +117,7 @@ function CreateNote({ return notesStore .addNote(sessionId, note) .then((r) => { - onSuccess(r.noteId as unknown as string) + onSuccess(r.noteId as unknown as string); toast.success('Note added'); notesStore.fetchSessionNotes(sessionId).then((notes) => { addNote(r); @@ -130,27 +144,41 @@ function CreateNote({ setTag(tag); }; - const slackChannelsOptions = slackChannels.map(({ webhookId, name }) => ({ - value: webhookId, - label: name, - })).toJS() as unknown as { value: string, label: string }[] + const slackChannelsOptions = slackChannels + .map(({ webhookId, name }) => ({ + value: webhookId, + label: name, + })) + .toJS() as unknown as { value: string; label: string }[]; + const teamsChannelsOptions = teamsChannels + .map(({ webhookId, name }) => ({ + value: webhookId, + label: name, + })) + .toJS() as unknown as { value: string; label: string }[]; - slackChannelsOptions.unshift({ value: null, label: 'Share to slack?' }) + slackChannelsOptions.unshift({ value: null, label: 'Pick a channel' }); + teamsChannelsOptions.unshift({ value: null, label: 'Pick a channel' }); - const changeChannel = ({ value, name }: { value: Record; name: string }) => { - setChannel(value.value); + const changeSlackChannel = ({ value, name }: { value: Record; name: string }) => { + setSlackChannel(value.value); + }; + + const changeTeamsChannel = ({ value, name }: { value: Record; name: string }) => { + setTeamsChannel(value.value); }; return (
0 ? -310 : 255, width: 350, left: 'calc(50% - 175px)', display: isVisible ? 'flex' : 'none', flexDirection: 'column', gap: '1rem', + bottom: '15vh', + zIndex: 110, }} onClick={(e) => e.stopPropagation()} > @@ -206,15 +234,44 @@ function CreateNote({
{slackChannelsOptions.length > 0 ? ( -
- +
+ )} +
+ ) : null} + + {teamsChannelsOptions.length > 0 ? ( +
+
setTeams(!useTeams)}> + + Send to teams? +
+ + {useTeams && ( +
+