From 1326bb2eae228fe6aed990bc5aadfd12a4d49f4f Mon Sep 17 00:00:00 2001 From: Delirium Date: Thu, 29 Aug 2024 13:35:58 +0200 Subject: [PATCH] feat spot: init commit for extension (#2452) * feat spot: init commit for extension * nvmrc * fix login flow * Spots Gridview Updates (#2422) * feat ui: login flow for spot extension * spot list, store and service created * some fixing for header * start work on single spot * spot player start * header for player, comments, icons, etc * split stuff into compoennts, create player state manager * player controls, activity panel etc etc * comments, empty page, rename and stuff * interval buttons etc * access modal * pubkey support * fix tooltip * limit 10 -> 9 * hls lib instead of videojs * some warnings * fix date display for exp * change public links * display more client data * fix cleaning, init comment * map network to replay player network ev * stream support, console panel, close panels on X * fixing streaming, destroy on leave * fix autoplay * show notification on spot login * fix spot login * backup player added, fix audio issue * show thumbnail when no video, add spot roles * add poster thumbnail * some fixes to video check * fix events jump * fix play btn * try catch over pubkey * feat ui: login flow for spot extension * spot list, store and service created * some fixing for header * start work on single spot * spot player start * header for player, comments, icons, etc * split stuff into compoennts, create player state manager * player controls, activity panel etc etc * comments, empty page, rename and stuff * interval buttons etc * access modal * pubkey support * fix tooltip * limit 10 -> 9 * hls lib instead of videojs * some warnings * fix date display for exp * change public links * display more client data * fix cleaning, init comment * map network to replay player network ev * stream support, console panel, close panels on X * fixing streaming, destroy on leave * fix autoplay * show notification on spot login * fix spot login * backup player added, fix audio issue * show thumbnail when no video, add spot roles * add poster thumbnail * some fixes to video check * fix events jump * fix play btn * try catch over pubkey * icons * Various updates * Update SVG.tsx * Update SideMenu.tsx * SpotList & Menu updates * feat ui: login flow for spot extension * spot list, store and service created * some fixing for header * start work on single spot * spot player start * header for player, comments, icons, etc * split stuff into compoennts, create player state manager * player controls, activity panel etc etc * comments, empty page, rename and stuff * interval buttons etc * access modal * pubkey support * fix tooltip * limit 10 -> 9 * hls lib instead of videojs * some warnings * fix date display for exp * change public links * display more client data * fix cleaning, init comment * map network to replay player network ev * stream support, console panel, close panels on X * fixing streaming, destroy on leave * fix autoplay * show notification on spot login * fix spot login * backup player added, fix audio issue * show thumbnail when no video, add spot roles * add poster thumbnail * some fixes to video check * fix events jump * fix play btn * try catch over pubkey * icons * spot login pinging * Spot List & Player Updates * move spot login flow to login comp, use separate spot login path for unique jwt * invalidate spot jwt on logout * add visual data on page load event * typo fix * Spot Listing improvements post review. * Update SpotListItem.tsx * Improved Spot List and Item Details * Minor improvements * More improvements * Public player header improvements * Moved formatExpirationTime to utils * fixes after merge --------- Co-authored-by: nick-delirium * set sso link to ? * some small perf fixes * login duck reformat... * Update frontend.yaml * add observer to spot list header * split list header * update spotjwt param in router * fix toast in router * fix async fetch, move ctx * capture space btn ev * fix header link * public sharing error msg * fix err msg for unsuccessful rec start * fix list alignment * Caching assets. Finally!!! * fix typing in comment field * add pubkey to comments, fix console jump btn * no content comp * change refresh token logic * move thumbnail ts * move thumbnail ts * fix tab change * switch up toggler * early exit if no jwt present * regenerate icons * fix location str * fix ctx * change thumnail res, return autoplay for video player * parse links in console rows, fix injected method parse? * remove ts from js * fix console parsing order? * fixes for autoplay * xray for spot player * move to spot list after login; esc to cancel; fix signup link; move ux commit * kb sc for skipping; xray for spot ext * track aborted requests * tooltip for readability * fixing empty state * New blank state + various minor improvements (#2471) * New blank state + various minor improvements * apres merge --------- Co-authored-by: nick-delirium * rm temp v * init or card * empty state debug * empty state debug * empty state debug * fix initor img * spotonly scope support * Improved Spot dead-end pages (#2475) * Improved Spot dead-end pages * Initiate OpenReplay Setup and some more * get scope changes * fix crash * scope upgrade/downgrade * scope setup flow * ping for backend * upgrade wxt deps * cancel ping int on expiration * check rec status * fix ping * check video processing state * check video processing state * fix xray close, network highlight, fcp rounding * update wxt, move open spot stuff to settings * fix some history issues * fix spot login flow * fix spot login again * fix spot login again * don't send two requests * limit messages for logged users * limit messages for logged users * fix public ignore * microphone stuff * microphone stuff * Various improvements (#2509) * Various improvements - Updated icons in mic settings - Included prefix in Spot title - Save recording notification has been updated - Other minor UI improvements * Inline declaration of spot name field, and settings UI * str f --------- Co-authored-by: nick-delirium * UI changes in player header, spot list (#2510) * Added UI elements in player page - Badge with counts for comments - Download and Delete dropdown in player - Spot selection -- UI improvement * Minor copy updates * completing changes --------- Co-authored-by: nick-delirium * rm cmt * fix cellmeasurer * thumbnail dur * fix download * Minor fixes (#2512) - Spot delete confirmation - Spot comments UI update - Minor copy updates * limit number of notif messages * add spot title to doc title, add cache groups for webpack * drop mic controls from recording popup view * fix for webpack compress * fix for auto mic pickup * change status banners * move svgs around, remove undefined check * refactor svgs * fix timetable scaling * fix error popup * self contain css * pre-select spot on spot onboarding --------- Co-authored-by: Sudheer Salavadi Co-authored-by: Rajesh Rajendran --- .github/workflows/frontend.yaml | 8 +- frontend/app/PrivateRoutes.tsx | 279 +- frontend/app/PublicRoutes.tsx | 2 +- frontend/app/Router.tsx | 93 +- frontend/app/api_client.ts | 4 +- frontend/app/assets/img/chrome.svg | 16 + frontend/app/assets/img/chromeStore.svg | 13 + frontend/app/assets/img/init-or.png | Bin 0 -> 13076 bytes frontend/app/assets/img/spot1.svg | 55 + frontend/app/assets/img/spot2.svg | 25 + frontend/app/assets/img/spotThumbBg.svg | 55 + frontend/app/assets/img/videoProcessing.svg | 27 + frontend/app/components/Login/Login.tsx | 61 +- .../app/components/ScopeForm/ScopeForm.tsx | 85 + frontend/app/components/ScopeForm/index.ts | 1 + .../Player/LivePlayer/LivePlayerSubHeader.tsx | 5 +- .../components/Session_/BottomBlock/Header.js | 3 +- .../Session_/Issues/IssueCommentForm.js | 6 +- .../Session_/OverviewPanel/OverviewPanel.tsx | 57 +- .../VerticalPointerLine.tsx | 6 +- .../components/VerticalPointerLine/index.ts | 2 +- frontend/app/components/Session_/Subheader.js | 5 +- .../Spots/SpotPlayer/SpotPlayer.tsx | 102 +- .../SpotPlayer/components/AccessModal.tsx | 165 +- .../SpotPlayer/components/CommentsSection.tsx | 138 +- .../components/Panels/SpotConsole.tsx | 10 +- .../components/Panels/SpotNetwork.tsx | 2 +- .../SpotPlayer/components/SpotActivity.tsx | 13 +- .../SpotPlayer/components/SpotLocation.tsx | 8 +- .../components/SpotPlayerControls.tsx | 5 + .../components/SpotPlayerHeader.tsx | 166 +- .../components/SpotVideoContainer.tsx | 189 +- .../Spots/SpotPlayer/spotPlayerStore.ts | 16 +- .../components/Spots/SpotsList/EmptyPage.tsx | 92 + .../Spots/SpotsList/SpotListItem.tsx | 176 +- .../Spots/SpotsList/SpotsListHeader.tsx | 114 + .../app/components/Spots/SpotsList/index.tsx | 252 +- .../shared/DevTools/BottomBlock/Header.tsx | 26 +- .../DevTools/ConsolePanel/ConsolePanel.tsx | 29 +- .../shared/DevTools/ConsoleRow/ConsoleRow.tsx | 27 +- .../shared/DevTools/JumpButton/JumpButton.tsx | 2 +- .../DevTools/NetworkPanel/NetworkPanel.tsx | 398 +- .../shared/DevTools/TimeTable/TimeTable.tsx | 4 +- .../LiveSessionList/LiveSessionList.tsx | 2 +- .../SortOrderButton/SortOrderButton.tsx | 2 +- .../ui/Icons/color_browser_whale.tsx | 4 +- .../app/components/ui/Icons/dashboard_icn.tsx | 2 +- frontend/app/components/ui/Icons/orSpot.tsx | 2 +- .../app/components/ui/NoContent/NoContent.tsx | 9 +- frontend/app/constants/storageKeys.ts | 3 +- frontend/app/duck/user.js | 181 +- frontend/app/layout/InitORCard.tsx | 38 + frontend/app/layout/SideMenu.tsx | 314 +- .../app/layout/SpotToOpenReplayPrompt.tsx | 74 + frontend/app/layout/TopRight.tsx | 38 +- frontend/app/layout/data.ts | 11 + frontend/app/mstore/loginStore.ts | 2 +- frontend/app/mstore/spotStore.ts | 72 +- frontend/app/player/web/types/resource.ts | 5 +- frontend/app/routes.ts | 1 + frontend/app/services/loginService.ts | 7 +- frontend/app/services/spotService.ts | 12 +- frontend/app/store.js | 6 +- frontend/app/utils/index.ts | 14 +- frontend/scripts/colors.js | 3 - frontend/scripts/icons.js | 138 +- spot/.gitignore | 28 + spot/.nvmrc | 1 + spot/.prettierrc | 1 + spot/README.md | 2 + spot/assets/Setting.svg | 10 + spot/assets/arrow-left.svg | 1 + spot/assets/circle-help.svg | 12 + spot/assets/desktop.svg | 10 + spot/assets/main.css | 3 + spot/assets/mic-off-red.svg | 15 + spot/assets/mic-off.svg | 15 + spot/assets/mic-on-animated-dark.svg | 14 + spot/assets/mic-on-animated.svg | 14 + spot/assets/mic-on-dark.svg | 1 + spot/assets/mic-on-red.svg | 1 + spot/assets/mic-on.svg | 1 + spot/assets/orSpot.svg | 22 + spot/assets/recording-animated.svg | 6 + spot/assets/solid.svg | 1 + spot/assets/tab.svg | 1 + spot/entrypoints/audio/audio.js | 17 + spot/entrypoints/audio/index.html | 38 + spot/entrypoints/background.ts | 1372 ++ spot/entrypoints/content/ControlsBox.tsx | 99 + spot/entrypoints/content/Countdown.tsx | 121 + .../entrypoints/content/RecordingControls.tsx | 291 + spot/entrypoints/content/SavingControls.tsx | 504 + spot/entrypoints/content/dragControls.css | 50 + spot/entrypoints/content/eventTrackers.ts | 100 + spot/entrypoints/content/index.tsx | 367 + spot/entrypoints/content/style.css | 307 + spot/entrypoints/content/utils.ts | 56 + spot/entrypoints/injected.js | 153 + spot/entrypoints/notifications.js | 111 + spot/entrypoints/offscreen/index.html | 8 + spot/entrypoints/offscreen/main.js | 398 + spot/entrypoints/popup/App.tsx | 339 + spot/entrypoints/popup/Button.tsx | 23 + spot/entrypoints/popup/Dropdown.tsx | 52 + spot/entrypoints/popup/Icons.tsx | 135 + spot/entrypoints/popup/Login.tsx | 49 + spot/entrypoints/popup/Settings.tsx | 214 + spot/entrypoints/popup/index.html | 14 + spot/entrypoints/popup/init.css | 17 + spot/entrypoints/popup/main.tsx | 5 + spot/entrypoints/welcome.html | 11 + spot/package-lock.json | 12033 ++++++++++++++++ spot/package.json | 37 + spot/postcss.config.js | 7 + spot/public/_locales/en/messages.json | 11 + spot/public/icon/128.png | Bin 0 -> 1952 bytes spot/public/icon/16.png | Bin 0 -> 503 bytes spot/public/icon/32.png | Bin 0 -> 797 bytes spot/public/icon/48.png | Bin 0 -> 1067 bytes spot/public/icon/96.png | Bin 0 -> 1571 bytes spot/public/wxt.svg | 15 + spot/tailwind.config.js | 14 + spot/tsconfig.json | 7 + spot/wxt.config.ts | 38 + spot/yarn.lock | 5296 +++++++ 126 files changed, 25036 insertions(+), 1114 deletions(-) create mode 100644 frontend/app/assets/img/chrome.svg create mode 100644 frontend/app/assets/img/chromeStore.svg create mode 100644 frontend/app/assets/img/init-or.png create mode 100644 frontend/app/assets/img/spot1.svg create mode 100644 frontend/app/assets/img/spot2.svg create mode 100644 frontend/app/assets/img/spotThumbBg.svg create mode 100644 frontend/app/assets/img/videoProcessing.svg create mode 100644 frontend/app/components/ScopeForm/ScopeForm.tsx create mode 100644 frontend/app/components/ScopeForm/index.ts create mode 100644 frontend/app/components/Spots/SpotsList/EmptyPage.tsx create mode 100644 frontend/app/components/Spots/SpotsList/SpotsListHeader.tsx create mode 100644 frontend/app/layout/InitORCard.tsx create mode 100644 frontend/app/layout/SpotToOpenReplayPrompt.tsx create mode 100644 spot/.gitignore create mode 100644 spot/.nvmrc create mode 100644 spot/.prettierrc create mode 100644 spot/README.md create mode 100644 spot/assets/Setting.svg create mode 100644 spot/assets/arrow-left.svg create mode 100644 spot/assets/circle-help.svg create mode 100644 spot/assets/desktop.svg create mode 100644 spot/assets/main.css create mode 100644 spot/assets/mic-off-red.svg create mode 100644 spot/assets/mic-off.svg create mode 100644 spot/assets/mic-on-animated-dark.svg create mode 100644 spot/assets/mic-on-animated.svg create mode 100644 spot/assets/mic-on-dark.svg create mode 100644 spot/assets/mic-on-red.svg create mode 100644 spot/assets/mic-on.svg create mode 100644 spot/assets/orSpot.svg create mode 100644 spot/assets/recording-animated.svg create mode 100644 spot/assets/solid.svg create mode 100644 spot/assets/tab.svg create mode 100644 spot/entrypoints/audio/audio.js create mode 100644 spot/entrypoints/audio/index.html create mode 100644 spot/entrypoints/background.ts create mode 100644 spot/entrypoints/content/ControlsBox.tsx create mode 100644 spot/entrypoints/content/Countdown.tsx create mode 100644 spot/entrypoints/content/RecordingControls.tsx create mode 100644 spot/entrypoints/content/SavingControls.tsx create mode 100644 spot/entrypoints/content/dragControls.css create mode 100644 spot/entrypoints/content/eventTrackers.ts create mode 100644 spot/entrypoints/content/index.tsx create mode 100644 spot/entrypoints/content/style.css create mode 100644 spot/entrypoints/content/utils.ts create mode 100644 spot/entrypoints/injected.js create mode 100644 spot/entrypoints/notifications.js create mode 100644 spot/entrypoints/offscreen/index.html create mode 100644 spot/entrypoints/offscreen/main.js create mode 100644 spot/entrypoints/popup/App.tsx create mode 100644 spot/entrypoints/popup/Button.tsx create mode 100644 spot/entrypoints/popup/Dropdown.tsx create mode 100644 spot/entrypoints/popup/Icons.tsx create mode 100644 spot/entrypoints/popup/Login.tsx create mode 100644 spot/entrypoints/popup/Settings.tsx create mode 100644 spot/entrypoints/popup/index.html create mode 100644 spot/entrypoints/popup/init.css create mode 100644 spot/entrypoints/popup/main.tsx create mode 100644 spot/entrypoints/welcome.html create mode 100644 spot/package-lock.json create mode 100644 spot/package.json create mode 100644 spot/postcss.config.js create mode 100644 spot/public/_locales/en/messages.json create mode 100644 spot/public/icon/128.png create mode 100644 spot/public/icon/16.png create mode 100644 spot/public/icon/32.png create mode 100644 spot/public/icon/48.png create mode 100644 spot/public/icon/96.png create mode 100644 spot/public/wxt.svg create mode 100644 spot/tailwind.config.js create mode 100644 spot/tsconfig.json create mode 100644 spot/wxt.config.ts create mode 100644 spot/yarn.lock diff --git a/.github/workflows/frontend.yaml b/.github/workflows/frontend.yaml index b13f88f37..836e63391 100644 --- a/.github/workflows/frontend.yaml +++ b/.github/workflows/frontend.yaml @@ -19,10 +19,12 @@ jobs: uses: actions/checkout@v2 - name: Cache node modules - uses: actions/cache@v1 + uses: actions/cache@v4 with: - path: node_modules - key: ${{ runner.OS }}-build-${{ hashFiles('**/package-lock.json') }} + path: | + /home/runner/work/openreplay/openreplay/frontend/node_modules + /home/runner/work/openreplay/openreplay/frontend/.yarn + key: ${{ runner.OS }}-build-${{ hashFiles('frontend/yarn.lock') }} restore-keys: | ${{ runner.OS }}-build- ${{ runner.OS }}- diff --git a/frontend/app/PrivateRoutes.tsx b/frontend/app/PrivateRoutes.tsx index 68793ec69..09729bd4d 100644 --- a/frontend/app/PrivateRoutes.tsx +++ b/frontend/app/PrivateRoutes.tsx @@ -5,6 +5,7 @@ import { Loader } from 'UI'; import withSiteIdUpdater from 'HOCs/withSiteIdUpdater'; import APIClient from './api_client'; +import { getScope } from "./duck/user"; import * as routes from './routes'; import { OB_DEFAULT_TAB } from 'App/routes'; import { GLOBAL_HAS_NO_RECORDINGS } from 'App/constants/storageKeys'; @@ -28,6 +29,7 @@ const components: any = { UsabilityTestOverviewPure: lazy(() => import('Components/UsabilityTesting/TestOverview')), SpotsListPure: lazy(() => import('Components/Spots/SpotsList')), SpotPure: lazy(() => import('Components/Spots/SpotPlayer')), + ScopeSetup: lazy(() => import('Components/ScopeForm')), }; const enhancedComponents: any = { @@ -47,6 +49,7 @@ const enhancedComponents: any = { UsabilityTestOverview: withSiteIdUpdater(components.UsabilityTestOverviewPure), SpotsList: withSiteIdUpdater(components.SpotsListPure), Spot: components.SpotPure, + ScopeSetup: components.ScopeSetup }; const withSiteId = routes.withSiteId; @@ -92,6 +95,7 @@ const USABILITY_TESTING_VIEW_PATH = routes.usabilityTestingView(); const SPOTS_LIST_PATH = routes.spotsList(); const SPOT_PATH = routes.spot(); +const SCOPE_SETUP = routes.scopeSetup(); interface Props { isEnterprise: boolean; @@ -100,6 +104,7 @@ interface Props { jwt: string; sites: Map; onboarding: boolean; + spotOnly?: boolean; } function PrivateRoutes(props: Props) { @@ -107,6 +112,7 @@ function PrivateRoutes(props: Props) { const redirectToOnboarding = !onboarding && localStorage.getItem(GLOBAL_HAS_NO_RECORDINGS) === 'true'; const siteIdList: any = sites.map(({ id }) => id).toJS(); + return ( }> @@ -115,149 +121,157 @@ function PrivateRoutes(props: Props) { path={withSiteId(ONBOARDING_PATH, siteIdList)} component={enhancedComponents.Onboarding} /> - { - const client = new APIClient(); - switch (location.pathname) { - case '/integrations/slack': - client.post('integrations/slack/add', { - code: location.search.split('=')[1], - state: props.tenantId, - }); - break; - case '/integrations/msteams': - client.post('integrations/msteams/add', { - code: location.search.split('=')[1], - state: props.tenantId, - }); - break; - } - return ; - }} - /> - {redirectToOnboarding && } - - {/* DASHBOARD and Metrics */} - - - - - - - - - - - - - - - - + + {props.spotOnly ? null : <> + { + const client = new APIClient(); + switch (location.pathname) { + case "/integrations/slack": + client.post("integrations/slack/add", { + code: location.search.split("=")[1], + state: props.tenantId + }); + break; + case "/integrations/msteams": + client.post("integrations/msteams/add", { + code: location.search.split("=")[1], + state: props.tenantId + }); + break; + } + return ; + }} + /> + {redirectToOnboarding && } - {Object.entries(routes.redirects).map(([fr, to]) => ( - - ))} - + {/* DASHBOARD and Metrics */} + + + + + + + + + + + + + + + + + {Object.entries(routes.redirects).map(([fr, to]) => ( + + ))} + + } + {props.spotOnly ? : null} ); @@ -269,6 +283,7 @@ export default connect((state: any) => ({ sites: state.getIn(['site', 'list']), siteId: state.getIn(['site', 'siteId']), jwt: state.getIn(['user', 'jwt']), + spotOnly: getScope(state) === 'spot', tenantId: state.getIn(['user', 'account', 'tenantId']), isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee' || diff --git a/frontend/app/PublicRoutes.tsx b/frontend/app/PublicRoutes.tsx index 5099363f1..a1f8e72ed 100644 --- a/frontend/app/PublicRoutes.tsx +++ b/frontend/app/PublicRoutes.tsx @@ -29,7 +29,7 @@ function PublicRoutes(props: Props) { - + diff --git a/frontend/app/Router.tsx b/frontend/app/Router.tsx index 82d0c7c2b..b728dbc2b 100644 --- a/frontend/app/Router.tsx +++ b/frontend/app/Router.tsx @@ -9,8 +9,8 @@ import PublicRoutes from 'App/PublicRoutes'; import { GLOBAL_DESTINATION_PATH, IFRAME, - JWT_PARAM, -} from 'App/constants/storageKeys'; + JWT_PARAM, SPOT_ONBOARDING +} from "App/constants/storageKeys"; import Layout from 'App/layout/Layout'; import { withStore } from "App/mstore"; import { checkParam } from 'App/utils'; @@ -23,7 +23,9 @@ import { init as initSite } from 'Duck/site'; import { fetchUserInfo, setJwt } from 'Duck/user'; import { fetchTenants } from 'Duck/user'; import { Loader } from 'UI'; +import { spotsList } from "./routes"; import * as routes from './routes'; +import { toast } from 'react-toastify' interface RouterProps extends RouteComponentProps, @@ -43,9 +45,11 @@ interface RouterProps }; }; mstore: any; - setJwt: (jwt: string) => any; + setJwt: (params: { jwt: string, spotJwt: string | null }) => any; fetchMetadata: (siteId: string) => void; initSite: (site: any) => void; + scopeSetup: boolean; + localSpotJwt: string | null; } const Router: React.FC = (props) => { @@ -58,18 +62,62 @@ const Router: React.FC = (props) => { fetchUserInfo, fetchSiteList, history, - match: { - params: { siteId: siteIdFromPath }, - }, setSessionPath, + scopeSetup, + localSpotJwt, } = props; + const params = new URLSearchParams(location.search) + const spotCb = params.get('spotCallback'); + const spotReqSent = React.useRef(false) const [isIframe, setIsIframe] = React.useState(false); const [isJwt, setIsJwt] = React.useState(false); const handleJwtFromUrl = () => { - const urlJWT = new URLSearchParams(location.search).get('jwt'); - if (urlJWT) { - props.setJwt(urlJWT); + const params = new URLSearchParams(location.search) + const urlJWT = params.get('jwt'); + const spotJwt = params.get('spotJwt'); + if (spotJwt) { + handleSpotLogin(spotJwt); } + if (urlJWT) { + props.setJwt({ jwt: urlJWT, spotJwt: spotJwt ?? null }); + } + }; + + const handleSpotLogin = (jwt: string) => { + if (spotReqSent.current) { + return; + } else { + spotReqSent.current = true; + } + let tries = 0; + if (!jwt) { + return; + } + let int: ReturnType; + + const onSpotMsg = (event: any) => { + if (event.data.type === 'orspot:logged') { + clearInterval(int); + window.removeEventListener('message', onSpotMsg); + } + }; + window.addEventListener('message', onSpotMsg); + + int = setInterval(() => { + if (tries > 20) { + clearInterval(int); + window.removeEventListener('message', onSpotMsg); + return; + } + window.postMessage( + { + type: 'orspot:token', + token: jwt, + }, + '*' + ); + tries += 1; + }, 250); }; const handleDestinationPath = () => { @@ -123,8 +171,20 @@ const Router: React.FC = (props) => { useEffect(() => { if (prevIsLoggedIn !== isLoggedIn && isLoggedIn) { handleUserLogin(); + if (scopeSetup) { + history.push(routes.scopeSetup()) + } else if (spotCb) { + history.push(spotsList()) + localStorage.setItem(SPOT_ONBOARDING, 'true') + } } - }, [isLoggedIn]); + }, [isLoggedIn, scopeSetup]); + + useEffect(() => { + if (isLoggedIn && location.pathname.includes('login') && localSpotJwt) { + handleSpotLogin(localSpotJwt); + } + }, [location, isLoggedIn, localSpotJwt]) useEffect(() => { if (siteId && siteId !== lastFetchedSiteIdRef.current) { @@ -153,7 +213,9 @@ const Router: React.FC = (props) => { location.pathname.includes('/assist/') || location.pathname.includes('multiview') || location.pathname.includes('/view-spot/') || - location.pathname.includes('/spots/'); + location.pathname.includes('/spots/') || + location.pathname.includes('/scope-setup') + if (isIframe) { return ( @@ -164,7 +226,7 @@ const Router: React.FC = (props) => { return isLoggedIn ? ( - + @@ -186,14 +248,17 @@ const mapStateToProps = (state: Map) => { 'loading', ]); const sitesLoading = state.getIn(['site', 'fetchListRequest', 'loading']); - + const scopeSetup = state.getIn(['user', 'scopeSetup']) + const loading = Boolean(userInfoLoading) || Boolean(sitesLoading) return { siteId, changePassword, sites: state.getIn(['site', 'list']), jwt, + localSpotJwt: state.getIn(['user', 'spotJwt']), isLoggedIn: jwt !== null && !changePassword, - loading: siteId === null || userInfoLoading || sitesLoading, + scopeSetup, + loading, email: state.getIn(['user', 'account', 'email']), account: state.getIn(['user', 'account']), organisation: state.getIn(['user', 'account', 'name']), diff --git a/frontend/app/api_client.ts b/frontend/app/api_client.ts index 7c3e65152..f6bb4dc49 100644 --- a/frontend/app/api_client.ts +++ b/frontend/app/api_client.ts @@ -201,11 +201,11 @@ export default class APIClient { const data = await response.json(); const refreshedJwt = data.jwt; - store.dispatch(setJwt(refreshedJwt)); + store.dispatch(setJwt({ jwt: refreshedJwt, })); return refreshedJwt; } catch (error) { console.error('Error refreshing token:', error); - store.dispatch(setJwt(null)); + store.dispatch(setJwt({ jwt: null })); throw error; } } diff --git a/frontend/app/assets/img/chrome.svg b/frontend/app/assets/img/chrome.svg new file mode 100644 index 000000000..45c46e8f3 --- /dev/null +++ b/frontend/app/assets/img/chrome.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/app/assets/img/chromeStore.svg b/frontend/app/assets/img/chromeStore.svg new file mode 100644 index 000000000..ee9b6df42 --- /dev/null +++ b/frontend/app/assets/img/chromeStore.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/frontend/app/assets/img/init-or.png b/frontend/app/assets/img/init-or.png new file mode 100644 index 0000000000000000000000000000000000000000..a2b14bb9fb847c2a395431c5af88e5b0fbd7f2da GIT binary patch literal 13076 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 Pv0$jZ)KaQcFbnx#`(~70 literal 0 HcmV?d00001 diff --git a/frontend/app/assets/img/spot1.svg b/frontend/app/assets/img/spot1.svg new file mode 100644 index 000000000..244e64a9f --- /dev/null +++ b/frontend/app/assets/img/spot1.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app/assets/img/spot2.svg b/frontend/app/assets/img/spot2.svg new file mode 100644 index 000000000..2fa9a1b29 --- /dev/null +++ b/frontend/app/assets/img/spot2.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app/assets/img/spotThumbBg.svg b/frontend/app/assets/img/spotThumbBg.svg new file mode 100644 index 000000000..b347b90c0 --- /dev/null +++ b/frontend/app/assets/img/spotThumbBg.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app/assets/img/videoProcessing.svg b/frontend/app/assets/img/videoProcessing.svg new file mode 100644 index 000000000..86557a928 --- /dev/null +++ b/frontend/app/assets/img/videoProcessing.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app/components/Login/Login.tsx b/frontend/app/components/Login/Login.tsx index a9362e533..7a9a1ee36 100644 --- a/frontend/app/components/Login/Login.tsx +++ b/frontend/app/components/Login/Login.tsx @@ -9,8 +9,14 @@ import { toast } from 'react-toastify'; import { ENTERPRISE_REQUEIRED } from 'App/constants'; import { useStore } from 'App/mstore'; -import { forgotPassword, signup } from 'App/routes'; -import { fetchTenants, loginSuccess, setJwt } from 'Duck/user'; +import { forgotPassword, signup, spotsList } from 'App/routes'; +import { + fetchTenants, + loadingLogin, + loginFailure, + loginSuccess, + setJwt, +} from 'Duck/user'; import { Button, Form, Icon, Input, Link, Loader, Tooltip } from 'UI'; import Copyright from 'Shared/Copyright'; @@ -27,6 +33,8 @@ interface LoginProps { loginSuccess: typeof loginSuccess; setJwt: typeof setJwt; fetchTenants: typeof fetchTenants; + loadingLogin: typeof loadingLogin; + loginFailure: typeof loginFailure; location: Location; } @@ -38,6 +46,8 @@ const Login: React.FC = ({ setJwt, fetchTenants, location, + loadingLogin, + loginFailure, }) => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); @@ -60,8 +70,12 @@ const Login: React.FC = ({ useEffect(() => { fetchTenants(); const jwt = params.get('jwt'); + const spotJwt = params.get('spotJwt'); + if (spotJwt) { + handleSpotLogin(spotJwt); + } if (jwt) { - setJwt(jwt); + setJwt({ jwt, spotJwt }); } }, []); @@ -99,18 +113,27 @@ const Login: React.FC = ({ }; const handleSubmit = (token?: string) => { + if (!email || !password) { + return; + } + loadingLogin(); loginStore.setEmail(email.trim()); loginStore.setPassword(password); if (token) { loginStore.setCaptchaResponse(token); } - loginStore.generateJWT().then((resp) => { - if (resp) { - handleSpotLogin(resp.spotJwt); - } - loginSuccess(resp) - setJwt(resp.jwt) - }) + loginStore + .generateJWT() + .then((resp) => { + if (resp) { + loginSuccess({ ...resp, spotJwt: resp.spotJwt ?? null }); + setJwt({ jwt: resp.jwt, spotJwt: resp.spotJwt ?? null }); + handleSpotLogin(resp.spotJwt); + } + }) + .catch((e) => { + loginFailure(e); + }); }; const onSubmit = (e: React.FormEvent) => { @@ -122,14 +145,10 @@ const Login: React.FC = ({ } }; - const onSSOClick = () => { - if (window !== window.top) { - // if in iframe - window.parent.location.href = `${window.location.origin}/api/sso/saml2?iFrame=true`; - } else { - window.location.href = `${window.location.origin}/api/sso/saml2`; - } - }; + const ssoLink = + window !== window.top + ? `${window.location.origin}/api/sso/saml2?iFrame=true&spot=true` + : `${window.location.origin}/api/sso/saml2?spot=true`; return (
@@ -223,7 +242,7 @@ const Login: React.FC = ({
{authDetails.sso ? ( - + @@ -294,6 +313,8 @@ const mapDispatchToProps = { loginSuccess, setJwt, fetchTenants, + loadingLogin, + loginFailure, }; export default withPageTitle('Login - OpenReplay')( diff --git a/frontend/app/components/ScopeForm/ScopeForm.tsx b/frontend/app/components/ScopeForm/ScopeForm.tsx new file mode 100644 index 000000000..6e762ea94 --- /dev/null +++ b/frontend/app/components/ScopeForm/ScopeForm.tsx @@ -0,0 +1,85 @@ +import { ArrowRightOutlined } from '@ant-design/icons'; +import { Button, Card, Radio } from 'antd'; +import React from 'react'; +import { connect } from 'react-redux'; +import { upgradeScope, downgradeScope } from "App/duck/user"; +import { useHistory } from 'react-router-dom'; +import * as routes from 'App/routes' +import { SPOT_ONBOARDING } from "../../constants/storageKeys"; + +const Scope = { + FULL: 'full', + SPOT: 'spot', +}; + +function ScopeForm({ + upgradeScope, + downgradeScope, +}: any) { + const [scope, setScope] = React.useState(Scope.FULL); + React.useEffect(() => { + const isSpotSetup = localStorage.getItem(SPOT_ONBOARDING) + if (isSpotSetup) { + setScope(Scope.SPOT) + localStorage.removeItem(SPOT_ONBOARDING) + } + }, []) + const history = useHistory(); + const onContinue = () => { + if (scope === Scope.FULL) { + upgradeScope(); + history.replace(routes.onboarding()) + } else { + downgradeScope(); + history.replace(routes.spotsList()) + } + }; + return ( +
+ +
+ How will you primarily use OpenReplay?{' '} +
+
+
+ You will have access to all OpenReplay features regardless of your + choice. +
+
+ Your preference will simply help us tailor your onboarding experience. +
+
+ setScope(e.target.value)} + className={'flex flex-col gap-2 mt-4 '} + > + + Session Replay & Debugging, Customer Support and more + + Report bugs via Spot + + +
+ +
+
+
+ ); +} + +export default connect(null, { upgradeScope, downgradeScope })(ScopeForm); diff --git a/frontend/app/components/ScopeForm/index.ts b/frontend/app/components/ScopeForm/index.ts new file mode 100644 index 000000000..89021071e --- /dev/null +++ b/frontend/app/components/ScopeForm/index.ts @@ -0,0 +1 @@ +export { default } from './ScopeForm'; \ No newline at end of file diff --git a/frontend/app/components/Session/Player/LivePlayer/LivePlayerSubHeader.tsx b/frontend/app/components/Session/Player/LivePlayer/LivePlayerSubHeader.tsx index 8f3b73157..837dd94ee 100644 --- a/frontend/app/components/Session/Player/LivePlayer/LivePlayerSubHeader.tsx +++ b/frontend/app/components/Session/Player/LivePlayer/LivePlayerSubHeader.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Icon, Tooltip } from 'UI'; +import {Link2} from 'lucide-react'; import { PlayerContext } from 'App/components/Session/playerContext'; import { observer } from 'mobx-react-lite'; import SessionTabs from 'Components/Session/Player/SharedComponents/SessionTabs'; @@ -19,8 +20,8 @@ function SubHeader() { {location && (
- - + + {location} diff --git a/frontend/app/components/Session_/BottomBlock/Header.js b/frontend/app/components/Session_/BottomBlock/Header.js index 4812305b7..05b361bee 100644 --- a/frontend/app/components/Session_/BottomBlock/Header.js +++ b/frontend/app/components/Session_/BottomBlock/Header.js @@ -12,12 +12,13 @@ const Header = ({ onFilterChange, showClose = true, customStyle, + customClose, ...props }) => (
{ children }
- { showClose && } + { showClose && }
); diff --git a/frontend/app/components/Session_/Issues/IssueCommentForm.js b/frontend/app/components/Session_/Issues/IssueCommentForm.js index ad11efd1a..ac688205b 100644 --- a/frontend/app/components/Session_/Issues/IssueCommentForm.js +++ b/frontend/app/components/Session_/Issues/IssueCommentForm.js @@ -6,7 +6,11 @@ import { addMessage } from 'Duck/assignments'; class IssueCommentForm extends React.PureComponent { state = { comment: '' } - write = ({ target: { name, value } }) => this.setState({ comment: value }); + write = (e) => { + e.stopPropagation(); + const { target: { name, value } } = e + this.setState({ comment: value }); + } addComment = () => { const { comment } = this.state; diff --git a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx index 42aa27542..d3fade552 100644 --- a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx +++ b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx @@ -23,7 +23,7 @@ import FeatureSelection, { import OverviewPanelContainer from './components/OverviewPanelContainer'; import TimelinePointer from './components/TimelinePointer'; import TimelineScale from './components/TimelineScale'; -import VerticalPointerLine from './components/VerticalPointerLine'; +import VerticalPointerLine, { VerticalPointerLineComp } from './components/VerticalPointerLine'; function MobileOverviewPanelCont({ issuesList, @@ -210,6 +210,35 @@ function WebOverviewPanelCont({ ); } +export function SpotOverviewPanelCont({ + resourceList, + exceptionsList, + spotTime, + spotEndTime, + onClose, +}: any) { + const selectedFeatures = ['ERRORS', 'NETWORK']; + const fetchPresented = false; // TODO + const endTime = 0; // TODO + const resources = { + NETWORK: resourceList, + ERRORS: exceptionsList, + }; + + return ( + + ); +} + function PanelComponent({ selectedFeatures, endTime, @@ -224,11 +253,15 @@ function PanelComponent({ sessionId, zoomTab, setZoomTab, + isSpot, + spotTime, + spotEndTime, + onClose, }: any) { return ( - +
X-Ray {showSummary ? ( @@ -265,13 +298,15 @@ function PanelComponent({ ) : null}
-
- - -
+ {isSpot ? null : ( +
+ + +
+ )}
{summaryChecked ? : null} @@ -291,7 +326,7 @@ function PanelComponent({
} > - + {isSpot ? : } {selectedFeatures.map((feature: any, index: number) => (
)} - endTime={endTime} + endTime={isSpot ? spotEndTime : endTime} message={HELP_MESSAGE[feature]} /> {isMobile && feature === 'PERFORMANCE' ? ( diff --git a/frontend/app/components/Session_/OverviewPanel/components/VerticalPointerLine/VerticalPointerLine.tsx b/frontend/app/components/Session_/OverviewPanel/components/VerticalPointerLine/VerticalPointerLine.tsx index 5f815efa6..f0dec1d1c 100644 --- a/frontend/app/components/Session_/OverviewPanel/components/VerticalPointerLine/VerticalPointerLine.tsx +++ b/frontend/app/components/Session_/OverviewPanel/components/VerticalPointerLine/VerticalPointerLine.tsx @@ -7,9 +7,13 @@ function VerticalPointerLine() { const { store } = React.useContext(PlayerContext) const { time, endTime } = store.get(); - const scale = 100 / endTime; + return +} +export function VerticalPointerLineComp ({ time, endTime }: { time: number, endTime: number }) { + const scale = 100 / endTime; const left = time * scale; + return ; } diff --git a/frontend/app/components/Session_/OverviewPanel/components/VerticalPointerLine/index.ts b/frontend/app/components/Session_/OverviewPanel/components/VerticalPointerLine/index.ts index 4a75fc048..51c155c2f 100644 --- a/frontend/app/components/Session_/OverviewPanel/components/VerticalPointerLine/index.ts +++ b/frontend/app/components/Session_/OverviewPanel/components/VerticalPointerLine/index.ts @@ -1 +1 @@ -export { default } from './VerticalPointerLine' \ No newline at end of file +export { default, VerticalPointerLineComp } from './VerticalPointerLine' \ No newline at end of file diff --git a/frontend/app/components/Session_/Subheader.js b/frontend/app/components/Session_/Subheader.js index 7fdb149be..a7fa9a45f 100644 --- a/frontend/app/components/Session_/Subheader.js +++ b/frontend/app/components/Session_/Subheader.js @@ -2,6 +2,7 @@ import React, { useMemo } from 'react'; import { useStore } from 'App/mstore'; import KeyboardHelp from 'Components/Session_/Player/Controls/components/KeyboardHelp'; import { Icon } from 'UI'; +import {Link2} from 'lucide-react'; import QueueControls from './QueueControls'; import Bookmark from 'Shared/Bookmark'; import SharePopup from '../shared/SharePopup/SharePopup'; @@ -141,8 +142,8 @@ function SubHeader(props) { {locationTruncated && (
- - + + {locationTruncated} diff --git a/frontend/app/components/Spots/SpotPlayer/SpotPlayer.tsx b/frontend/app/components/Spots/SpotPlayer/SpotPlayer.tsx index d9698a631..11b445333 100644 --- a/frontend/app/components/Spots/SpotPlayer/SpotPlayer.tsx +++ b/frontend/app/components/Spots/SpotPlayer/SpotPlayer.tsx @@ -1,3 +1,4 @@ +import { Button, Card } from 'antd'; import cn from 'classnames'; import { observer } from 'mobx-react-lite'; import React from 'react'; @@ -5,12 +6,15 @@ import { connect } from 'react-redux'; import { useHistory, useParams } from 'react-router-dom'; import { useStore } from 'App/mstore'; -import { EscapeButton, Loader } from 'UI'; +import { EscapeButton, Icon, Loader } from 'UI'; + +import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; import { debounceUpdate, getDefaultPanelHeight, } from '../../Session/Player/ReplayPlayer/PlayerInst'; +import { SpotOverviewPanelCont } from '../../Session_/OverviewPanel/OverviewPanel'; import withPermissions from '../../hocs/withPermissions'; import SpotConsole from './components/Panels/SpotConsole'; import SpotNetwork from './components/Panels/SpotNetwork'; @@ -32,6 +36,11 @@ function SpotPlayer({ loggedIn }: { loggedIn: boolean }) { const { spotId } = useParams<{ spotId: string }>(); const [activeTab, setActiveTab] = React.useState(null); + React.useEffect(() => { + if (spotStore.currentSpot) { + document.title = spotStore.currentSpot.title + ' - OpenReplay' + } + }, [spotStore.currentSpot]) React.useEffect(() => { if (!loggedIn) { const query = new URLSearchParams(window.location.search); @@ -98,6 +107,12 @@ function SpotPlayer({ loggedIn }: { loggedIn: boolean }) { }); const ev = (e: KeyboardEvent) => { + if ( + e.target instanceof HTMLInputElement || + e.target instanceof HTMLTextAreaElement + ) { + return false; + } if (e.key === 'Escape') { spotPlayerStore.setIsFullScreen(false); } @@ -116,6 +131,19 @@ function SpotPlayer({ loggedIn }: { loggedIn: boolean }) { const highest = 16; spotPlayerStore.setPlaybackRate(Math.min(highest, current * 2)); } + if (e.key === 'ArrowRight') { + spotPlayerStore.setTime( + Math.min( + spotPlayerStore.duration, + spotPlayerStore.time + spotPlayerStore.skipInterval + ) + ); + } + if (e.key === 'ArrowLeft') { + spotPlayerStore.setTime( + Math.max(0, spotPlayerStore.time - spotPlayerStore.skipInterval) + ); + } }; document.addEventListener('keydown', ev); @@ -127,8 +155,47 @@ function SpotPlayer({ loggedIn }: { loggedIn: boolean }) { }, []); if (!spotStore.currentSpot) { return ( -
- +
+ {spotStore.accessError ? ( + <> +
+ + +
+ The Spot link has expired. +
+

+ Contact the person who shared it to re-spot. +

+
+
+ +
+
+ + ) : ( + + )}
); } @@ -166,7 +233,6 @@ function SpotPlayer({ loggedIn }: { loggedIn: boolean }) { // }] // }; - console.log(spotStore.currentSpot) return (
spotStore.checkIsProcessed(spotId)} />
{!isFullScreen && spotPlayerStore.activePanel ? ( @@ -227,6 +294,9 @@ function SpotPlayer({ loggedIn }: { loggedIn: boolean }) { panelHeight={panelHeight} /> ) : null} + {spotPlayerStore.activePanel === PANELS.OVERVIEW ? ( + + ) : null}
) : null}
@@ -244,6 +314,30 @@ function SpotPlayer({ loggedIn }: { loggedIn: boolean }) { ); } +const SpotOverviewConnector = observer(() => { + const endTime = spotPlayerStore.duration * 1000; + const time = spotPlayerStore.time * 1000; + const resourceList = spotPlayerStore.network + .filter((r: any) => r.isRed || r.isYellow || (r.status && r.status >= 400)) + .filter((i: any) => i.type === 'xhr'); + const exceptionsList = spotPlayerStore.logs.filter( + (l) => l.level === 'error' + ); + + const onClose = () => { + spotPlayerStore.setActivePanel(null); + } + return ( + + ); +}); + function mapStateToProps(state: any) { const userEmail = state.getIn(['user', 'account', 'name']); const loggedIn = !!userEmail; diff --git a/frontend/app/components/Spots/SpotPlayer/components/AccessModal.tsx b/frontend/app/components/Spots/SpotPlayer/components/AccessModal.tsx index af32ce6ed..551c2848c 100644 --- a/frontend/app/components/Spots/SpotPlayer/components/AccessModal.tsx +++ b/frontend/app/components/Spots/SpotPlayer/components/AccessModal.tsx @@ -1,15 +1,10 @@ -import { DownOutlined, LinkOutlined, StopOutlined } from '@ant-design/icons'; -import { Button, Dropdown, Segmented } from 'antd'; +import { DownOutlined, CopyOutlined, StopOutlined } from '@ant-design/icons'; +import { Button, Dropdown, Menu, Segmented, Modal } from 'antd'; import copy from 'copy-to-clipboard'; -import React from 'react'; - +import React, { useState } from 'react'; +import { observer } from 'mobx-react-lite'; import { useStore } from 'App/mstore'; -import { confirm } from 'UI'; -import { durationFormatted } from "../../../../date"; - -const HOUR_SECS = 60 * 60; -const DAY_SECS = 24 * HOUR_SECS; -const WEEK_SECS = 7 * DAY_SECS; +import { formatExpirationTime, HOUR_SECS, DAY_SECS, WEEK_SECS } from 'App/utils/index'; enum Intervals { hour, @@ -20,9 +15,11 @@ enum Intervals { function AccessModal() { const { spotStore } = useStore(); - const [isCopied, setIsCopied] = React.useState(false); - const [isPublic, setIsPublic] = React.useState(!!spotStore.pubKey); - const [generated, setGenerated] = React.useState(!!spotStore.pubKey); + const [isCopied, setIsCopied] = useState(false); + const [isPublic, setIsPublic] = useState(!!spotStore.pubKey); + const [generated, setGenerated] = useState(!!spotStore.pubKey); + const [selectedInterval, setSelectedInterval] = useState(Intervals.hour); + const [loadingKey, setLoadingKey] = useState(false); const expirationValues = { [Intervals.hour]: HOUR_SECS, @@ -37,68 +34,52 @@ function AccessModal() { const menuItems = [ { - key: Intervals.hour, - label:
One Hour
, + key: Intervals.hour.toString(), + label:
1 Hour
, }, { - key: Intervals.threeHours, - label:
Three Hours
, + key: Intervals.threeHours.toString(), + label:
3 Hours
, }, { - key: Intervals.day, - label:
One Day
, + key: Intervals.day.toString(), + label:
1 Day
, }, { - key: Intervals.week, - label:
One Week
, + key: Intervals.week.toString(), + label:
1 Week
, }, ]; - const onMenuClick = ({ key }: { key: Intervals }) => { - const val = expirationValues[key]; - if ( - spotStore.pubKey?.expiration && - Math.abs(spotStore.pubKey?.expiration - val) / val < 0.1 - ) { - return; - } - void spotStore.generateKey(spotId, val); + + const onMenuClick = async (info: { key: string }) => { + const val = expirationValues[Number(info.key) as Intervals]; + setSelectedInterval(Number(info.key) as Intervals); + await spotStore.generateKey(spotId, val); }; const changeAccess = async (toPublic: boolean) => { if (isPublic && !toPublic && spotStore.pubKey) { - if ( - await confirm({ - header: 'Confirm', - confirmButton: 'Disable', - confirmation: - 'Are you sure you want to disable public sharing for this spot?', - }) - ) { - void spotStore.generateKey(spotId, 0); - } + await spotStore.generateKey(spotId, 0); + setIsPublic(toPublic); + } else { + setIsPublic(toPublic); } - setIsPublic(toPublic); }; + const revokeKey = async () => { - if ( - await confirm({ - header: 'Confirm', - confirmButton: 'Disable', - confirmation: - 'Are you sure you want to disable public sharing for this spot?', - }) - ) { - void spotStore.generateKey(spotId, 0); - setGenerated(false); - setIsPublic(false); - } + await spotStore.generateKey(spotId, 0); + setGenerated(false); + setIsPublic(false); }; + const generateInitial = async () => { + setLoadingKey(true); const k = await spotStore.generateKey( spotId, expirationValues[Intervals.hour] ); setGenerated(!!k); + setLoadingKey(false); }; const onCopy = () => { @@ -108,12 +89,8 @@ function AccessModal() { }; return ( -
+
-
Who can access this Spot
-
- All team members in your project will able to view this Spot +
+ Link for internal team members
-
+
{spotLink}
@@ -143,56 +120,60 @@ function AccessModal() {
) : !generated ? ( -
+
) : ( <> -
-
Anyone with following link will be able to view this spot
-
- {spotLink} -
-
-
-
Link expires in
- -
- {spotStore.isLoading ? 'Loading' : durationFormatted(spotStore.pubKey!.expiration * 1000)} - +
+
+
Anyone with the following link can access this Spot
+
+ {spotLink}
- -
-
-
-
+ +
+
Link expires in
+ }> +
+ {loadingKey ? 'Loading' : formatExpirationTime(expirationValues[selectedInterval])} + +
+
+
+
+
+ +
+
-
)} @@ -200,4 +181,4 @@ function AccessModal() { ); } -export default AccessModal; +export default observer(AccessModal); diff --git a/frontend/app/components/Spots/SpotPlayer/components/CommentsSection.tsx b/frontend/app/components/Spots/SpotPlayer/components/CommentsSection.tsx index 5dc336251..1cc924407 100644 --- a/frontend/app/components/Spots/SpotPlayer/components/CommentsSection.tsx +++ b/frontend/app/components/Spots/SpotPlayer/components/CommentsSection.tsx @@ -1,109 +1,143 @@ +import { CloseOutlined } from '@ant-design/icons'; +import { SendOutlined } from '@ant-design/icons'; import { Button, Input, Tooltip } from 'antd'; import cn from 'classnames'; -import { X } from 'lucide-react'; +import { observer } from 'mobx-react-lite'; import React from 'react'; import { connect } from 'react-redux'; +import { toast } from 'react-toastify'; + import { resentOrDate } from 'App/date'; import { useStore } from 'App/mstore'; -import { observer } from 'mobx-react-lite'; -import { SendOutlined } from '@ant-design/icons'; -function CommentsSection({ - onClose, -}: { - onClose?: () => void; -}) { +function CommentsSection({ onClose }: { onClose?: () => void }) { const { spotStore } = useStore(); const comments = spotStore.currentSpot?.comments ?? []; return (
-
-
Comments
-
- -
+
+
Comments
+
{comments.map((comment) => ( -
+
{comment.user[0]}
-
{comment.user}
+
+ {comment.user} +
+ {resentOrDate(new Date(comment.createdAt).getTime())} +
+
{comment.text}
-
- {resentOrDate(new Date(comment.createdAt).getTime())} -
))} - 5} /> + 5} + loggedLimit={comments.length > 25} + />
); } -function BottomSection({ loggedIn, userEmail, disableComments }: { disableComments: boolean, loggedIn?: boolean, userEmail?: string }) { +function BottomSection({ + loggedIn, + userEmail, + unloggedLimit, + loggedLimit, +}: { + loggedLimit: boolean; + unloggedLimit: boolean; + loggedIn?: boolean; + userEmail?: string; +}) { const [commentText, setCommentText] = React.useState(''); const [userName, setUserName] = React.useState(userEmail ?? ''); const { spotStore } = useStore(); const addComment = async () => { - await spotStore.addComment( - spotStore.currentSpot!.spotId, - commentText, - userName - ); - setCommentText(''); + try { + await spotStore.addComment( + spotStore.currentSpot!.spotId, + commentText, + userName + ); + setCommentText(''); + } catch (e) { + toast.error('Failed to add comment; Try again later'); + } }; - const disableSubmit = commentText.trim().length === 0 || userName.trim().length === 0 || disableComments + const unlogged = userName.trim().length === 0 && unloggedLimit + const disableSubmit = + commentText.trim().length === 0 || + unlogged || loggedLimit; return (
-
- setUserName(e.target.value)} - /> - setCommentText(e.target.value)} - /> -
- +
+ setUserName(e.target.value)} + /> + { + e.preventDefault(); + e.stopPropagation(); + setCommentText(e.target.value); + }} + placeholder="Add a comment..." + /> +
+
diff --git a/frontend/app/components/Spots/SpotPlayer/components/Panels/SpotConsole.tsx b/frontend/app/components/Spots/SpotPlayer/components/Panels/SpotConsole.tsx index 2bbf6aa49..b5cec283c 100644 --- a/frontend/app/components/Spots/SpotPlayer/components/Panels/SpotConsole.tsx +++ b/frontend/app/components/Spots/SpotPlayer/components/Panels/SpotConsole.tsx @@ -16,8 +16,10 @@ import spotPlayerStore from '../../spotPlayerStore'; function SpotConsole({ onClose }: { onClose: () => void }) { const [activeTab, setActiveTab] = React.useState(TABS[0]); const _list = React.useRef(null); - const onTabClick = (tab: any) => { - setActiveTab(tab); + + const onTabClick = (tab: string) => { + const newTab = TABS.find((t) => t.text === tab); + setActiveTab(newTab); }; const logs = spotPlayerStore.logs; const filteredList = React.useMemo(() => { @@ -29,7 +31,7 @@ function SpotConsole({ onClose }: { onClose: () => void }) { }, [activeTab]); const jump = (t: number) => { - spotPlayerStore.setTime(t); + spotPlayerStore.setTime(t / 1000); }; return ( @@ -48,7 +50,7 @@ function SpotConsole({ onClose }: { onClose: () => void }) { +
No Data
diff --git a/frontend/app/components/Spots/SpotPlayer/components/Panels/SpotNetwork.tsx b/frontend/app/components/Spots/SpotPlayer/components/Panels/SpotNetwork.tsx index 0a5bf49f5..0f346999c 100644 --- a/frontend/app/components/Spots/SpotPlayer/components/Panels/SpotNetwork.tsx +++ b/frontend/app/components/Spots/SpotPlayer/components/Panels/SpotNetwork.tsx @@ -26,7 +26,7 @@ function SpotNetwork({ panelHeight, onClose }: { panelHeight: number, onClose: ( websocketListNow={[]} /* @ts-ignore */ player={{ jump: (t) => spotPlayerStore.setTime(t) }} - activeIndex={index} + activeOutsideIndex={index} onClose={onClose} /> ); diff --git a/frontend/app/components/Spots/SpotPlayer/components/SpotActivity.tsx b/frontend/app/components/Spots/SpotPlayer/components/SpotActivity.tsx index 3f1f4689b..bd61eee10 100644 --- a/frontend/app/components/Spots/SpotPlayer/components/SpotActivity.tsx +++ b/frontend/app/components/Spots/SpotPlayer/components/SpotActivity.tsx @@ -1,5 +1,6 @@ +import { CloseOutlined } from '@ant-design/icons'; import { TYPES } from 'Types/session/event'; -import { X } from 'lucide-react'; +import { Button } from 'antd'; import { observer } from 'mobx-react-lite'; import React from 'react'; @@ -28,14 +29,14 @@ function SpotActivity({ onClose }: { onClose: () => void }) { }; return (
-
Activity
-
- -
+
Activity
+
170 ? `${currUrl.slice(0, 170)}...` : currUrl; return (
diff --git a/frontend/app/components/Spots/SpotPlayer/components/SpotPlayerControls.tsx b/frontend/app/components/Spots/SpotPlayer/components/SpotPlayerControls.tsx index 55fab6655..771678aae 100644 --- a/frontend/app/components/Spots/SpotPlayer/components/SpotPlayerControls.tsx +++ b/frontend/app/components/Spots/SpotPlayer/components/SpotPlayerControls.tsx @@ -94,6 +94,11 @@ function SpotPlayerControls() {
+ togglePanel(PANELS.OVERVIEW)} + active={spotPlayerStore.activePanel === PANELS.OVERVIEW} + /> togglePanel(PANELS.CONSOLE)} diff --git a/frontend/app/components/Spots/SpotPlayer/components/SpotPlayerHeader.tsx b/frontend/app/components/Spots/SpotPlayer/components/SpotPlayerHeader.tsx index 3c6537760..47c5e49e6 100644 --- a/frontend/app/components/Spots/SpotPlayer/components/SpotPlayerHeader.tsx +++ b/frontend/app/components/Spots/SpotPlayer/components/SpotPlayerHeader.tsx @@ -1,23 +1,24 @@ -import { - ArrowLeftOutlined, - CommentOutlined, - LinkOutlined, - SettingOutlined, - UserSwitchOutlined, -} from '@ant-design/icons'; -import { Button, Popover } from 'antd'; +import { ArrowLeftOutlined, CommentOutlined, CopyOutlined, DeleteOutlined, DownloadOutlined, MoreOutlined, SettingOutlined, UserSwitchOutlined } from '@ant-design/icons'; +import { Badge, Button, Dropdown, MenuProps, Popover, Tooltip, message } from 'antd'; import copy from 'copy-to-clipboard'; -import React from 'react'; +import { observer } from 'mobx-react-lite'; +import React, { useState } from 'react'; import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; + + +import { useStore } from 'App/mstore'; import { spotsList } from 'App/routes'; import { hashString } from 'App/types/session/session'; import { Avatar, Icon } from 'UI'; + + import { TABS, Tab } from '../consts'; import AccessModal from './AccessModal'; + const spotLink = spotsList(); function SpotPlayerHeader({ @@ -43,53 +44,96 @@ function SpotPlayerHeader({ platform: string | null; hasShareAccess: boolean; }) { - const [isCopied, setIsCopied] = React.useState(false); - const [dropdownOpen, setDropdownOpen] = React.useState(false); + const { spotStore } = useStore(); + const comments = spotStore.currentSpot?.comments ?? []; + + const [dropdownOpen, setDropdownOpen] = useState(false); + const history = useHistory(); + const onCopy = () => { - setIsCopied(true); copy(window.location.href); - setTimeout(() => setIsCopied(false), 2000); + message.success('Internal sharing link copied to clipboard'); }; + + const navigateToSpotsList = () => { + history.push(spotLink); + }; + + const items: MenuProps['items'] = [ + { + key: '1', + icon: , + label: 'Download Video', + }, + { + key: '2', + icon: , + label: 'Delete', + }, + ]; + + const onMenuClick = async ({ key }: { key: string }) => { + if (key === '1') { + const { url } = await spotStore.getVideo(spotStore.currentSpot!.spotId); + await downloadFile(url, `${spotStore.currentSpot!.title}.webm`) + } else if (key === '2') { + spotStore.deleteSpot([spotStore.currentSpot!.spotId]).then(() => { + history.push(spotsList()); + message.success('Spot deleted successfully'); + }); + } + }; + return (
{isLoggedIn ? ( - -
- -
All Spots
-
- + ) : ( <> -
- -
Spot
-
-
by OpenReplay
+ + + )}
-
+
-
{title}
-
+ +
+ {title} +
+
+
{user}
·
-
{date}
+
{date}
{browserVersion && ( <>
·
-
Chrome v{browserVersion}
+
Chrome v{browserVersion}
)} {resolution && ( @@ -101,7 +145,7 @@ function SpotPlayerHeader({ {platform && ( <>
·
-
{platform}
+
{platform}
)}
@@ -113,24 +157,30 @@ function SpotPlayerHeader({ {hasShareAccess ? ( - }> + }> ) : null} + + +
@@ -143,18 +193,44 @@ function SpotPlayerHeader({ > Activity +
); } +async function downloadFile(url: string, fileName: string) { + try { + const response = await fetch(url); + + if (!response.ok) { + throw new Error('Network response was not ok'); + } + + const blob = await response.blob(); + const blobUrl = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = blobUrl; + a.download = fileName; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(blobUrl); + } catch (error) { + console.error('Error downloading file:', error); + } +} + export default connect((state: any) => { const jwt = state.getIn(['user', 'jwt']); const isEE = state.getIn(['user', 'account', 'edition']) === 'ee'; @@ -163,4 +239,4 @@ export default connect((state: any) => { const hasShareAccess = isEE ? permissions.includes('SPOT_PUBLIC') : true; return { isLoggedIn: !!jwt, hasShareAccess }; -})(SpotPlayerHeader); +})(observer(SpotPlayerHeader)); diff --git a/frontend/app/components/Spots/SpotPlayer/components/SpotVideoContainer.tsx b/frontend/app/components/Spots/SpotPlayer/components/SpotVideoContainer.tsx index 2b44f4eb4..2d03c204c 100644 --- a/frontend/app/components/Spots/SpotPlayer/components/SpotVideoContainer.tsx +++ b/frontend/app/components/Spots/SpotPlayer/components/SpotVideoContainer.tsx @@ -1,3 +1,4 @@ +import Hls from 'hls.js'; import { observer } from 'mobx-react-lite'; import React from 'react'; @@ -19,84 +20,109 @@ function SpotVideoContainer({ videoURL, streamFile, thumbnail, + checkReady, }: { videoURL: string; streamFile?: string; thumbnail?: string; + checkReady: () => Promise; }) { - const [videoLink, setVideoLink] = React.useState(videoURL); - - const { spotStore } = useStore(); + const [prevIsProcessing, setPrevIsProcessing] = React.useState(false); + const [isProcessing, setIsProcessing] = React.useState(false); const [isLoaded, setLoaded] = React.useState(false); const videoRef = React.useRef(null); const playbackTime = React.useRef(0); const hlsRef = React.useRef(null); React.useEffect(() => { - import('hls.js').then(({ default: Hls }) => { - if (Hls.isSupported() && videoRef.current) { - videoRef.current.addEventListener('loadeddata', () => { - setLoaded(true); - }); - if (streamFile) { - const hls = new Hls({ - enableWorker: false, - // workerPath: '/hls-worker.js', - // 1MB buffer -- we have small videos anyways - maxBufferSize: 1000 * 1000, + const startPlaying = () => { + if (spotPlayerStore.isPlaying && videoRef.current) { + videoRef.current + .play() + .then(() => { + console.debug('playing'); + }) + .catch((e) => { + console.error(e); + spotPlayerStore.setIsPlaying(false); + const onClick = () => { + spotPlayerStore.setIsPlaying(true); + document.removeEventListener('click', onClick); + }; + document.addEventListener('click', onClick); }); - const url = URL.createObjectURL(base64toblob(streamFile)); - if (url && videoRef.current) { - hls.loadSource(url); - hls.attachMedia(videoRef.current); - if (spotPlayerStore.isPlaying) { - void videoRef.current.play(); + } + }; + checkReady().then((isReady) => { + if (!isReady) { + setIsProcessing(true); + setPrevIsProcessing(true); + const int = setInterval(() => { + checkReady().then((r) => { + if (r) { + setIsProcessing(false); + clearInterval(int); } - hlsRef.current = hls; - } else { - if (videoRef.current) { - videoRef.current.src = videoURL; - if (spotPlayerStore.isPlaying) { - void videoRef.current.play(); - } - } - } - } else { - const check = () => { - fetch(videoLink).then((r) => { - if (r.ok && r.status === 200) { - if (videoRef.current) { - videoRef.current.src = ''; - setTimeout(() => { - videoRef.current!.src = videoURL; - }, 0); - } - - return true; - } else { - setTimeout(() => { - check(); - }, 1000); - } - }); - }; - check(); - videoRef.current.src = videoURL; - if (spotPlayerStore.isPlaying) { - void videoRef.current.play(); - } - } - } else { - if (videoRef.current) { + }); + }, 5000) + } + import('hls.js').then(({ default: Hls }) => { + if (Hls.isSupported() && videoRef.current) { videoRef.current.addEventListener('loadeddata', () => { setLoaded(true); }); - videoRef.current.src = videoURL; - if (spotPlayerStore.isPlaying) { - void videoRef.current.play(); + if (streamFile) { + const hls = new Hls({ + // not needed for small videos (we have 3 min limit and 720 quality with half kbps) + enableWorker: false, + // = 1MB, should be enough + maxBufferSize: 1000 * 1000, + }); + const url = URL.createObjectURL(base64toblob(streamFile)); + if (url && videoRef.current) { + hls.loadSource(url); + hls.attachMedia(videoRef.current); + startPlaying(); + hlsRef.current = hls; + } else { + if (videoRef.current) { + videoRef.current.src = videoURL; + startPlaying(); + } + } + } else { + const check = () => { + fetch(videoURL).then((r) => { + if (r.ok && r.status === 200) { + if (videoRef.current) { + videoRef.current.src = ''; + setTimeout(() => { + videoRef.current!.src = videoURL; + }, 0); + } + + return true; + } else { + setTimeout(() => { + check(); + }, 1000); + } + }); + }; + check(); + videoRef.current.src = videoURL; + startPlaying(); + } + } else { + if (videoRef.current) { + videoRef.current.addEventListener('loadeddata', () => { + setLoaded(true); + }); + videoRef.current.src = videoURL; + startPlaying(); } } - } + }); }); return () => { hlsRef.current?.destroy(); @@ -143,29 +169,46 @@ function SpotVideoContainer({ videoRef.current.playbackRate = spotPlayerStore.playbackRate; } }, [spotPlayerStore.playbackRate]); + + const warnText = isProcessing ? 'You’re viewing the entire recording. The trimmed Spot is on its way.' : 'Your trimmed Spot is ready! Please reload the page.' return ( <> + {isProcessing || prevIsProcessing + ?
+ {warnText} +
+ : null} + {!isLoaded && ( +
+ {'Processing +
Loading Spot Recording
+
+ )}