* ui: use enum state for spot ready checker

* ui: force worker for hls

* ui: fix spot list header behavior, change spot login flow?

* ui: bump spot v

* ui: spot signup fixes
This commit is contained in:
Delirium 2024-09-13 18:13:15 +02:00 committed by GitHub
parent cbe2d62def
commit 9ed207abb1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 114 additions and 88 deletions

View file

@ -119,6 +119,7 @@ interface Props {
function PrivateRoutes(props: Props) { function PrivateRoutes(props: Props) {
const { onboarding, sites, siteId } = props; const { onboarding, sites, siteId } = props;
const hasRecordings = sites.some(s => s.recorded); const hasRecordings = sites.some(s => s.recorded);
const redirectToSetup = props.scope === 0;
const redirectToOnboarding = const redirectToOnboarding =
!onboarding && (localStorage.getItem(GLOBAL_HAS_NO_RECORDINGS) === 'true' || !hasRecordings) && props.scope > 0; !onboarding && (localStorage.getItem(GLOBAL_HAS_NO_RECORDINGS) === 'true' || !hasRecordings) && props.scope > 0;
const siteIdList: any = sites.map(({ id }) => id).toJS(); const siteIdList: any = sites.map(({ id }) => id).toJS();
@ -126,6 +127,13 @@ function PrivateRoutes(props: Props) {
return ( return (
<Suspense fallback={<Loader loading={true} className="flex-1" />}> <Suspense fallback={<Loader loading={true} className="flex-1" />}>
<Switch key="content"> <Switch key="content">
<Route
exact
strict
path={SCOPE_SETUP}
component={enhancedComponents.ScopeSetup}
/>
{redirectToSetup ? <Redirect to={SCOPE_SETUP} /> : null}
<Route path={CLIENT_PATH} component={enhancedComponents.Client} /> <Route path={CLIENT_PATH} component={enhancedComponents.Client} />
<Route <Route
path={withSiteId(ONBOARDING_PATH, siteIdList)} path={withSiteId(ONBOARDING_PATH, siteIdList)}
@ -143,12 +151,6 @@ function PrivateRoutes(props: Props) {
path={SPOT_PATH} path={SPOT_PATH}
component={enhancedComponents.Spot} component={enhancedComponents.Spot}
/> />
<Route
exact
strict
path={SCOPE_SETUP}
component={enhancedComponents.ScopeSetup}
/>
{props.scope === 1 ? <Redirect to={SPOTS_LIST_PATH} /> : null} {props.scope === 1 ? <Redirect to={SPOTS_LIST_PATH} /> : null}
<Route <Route
path="/integrations/" path="/integrations/"

View file

@ -10,21 +10,19 @@ import {
GLOBAL_DESTINATION_PATH, GLOBAL_DESTINATION_PATH,
IFRAME, IFRAME,
JWT_PARAM, JWT_PARAM,
SPOT_ONBOARDING SPOT_ONBOARDING,
} from "App/constants/storageKeys"; } from 'App/constants/storageKeys';
import Layout from 'App/layout/Layout'; import Layout from 'App/layout/Layout';
import { withStore } from "App/mstore"; import { withStore } from 'App/mstore';
import { checkParam, handleSpotJWT, isTokenExpired } from "App/utils"; import { checkParam, handleSpotJWT, isTokenExpired } from 'App/utils';
import { ModalProvider } from 'Components/Modal'; import { ModalProvider } from 'Components/Modal';
import { ModalProvider as NewModalProvider } from 'Components/ModalContext'; import { ModalProvider as NewModalProvider } from 'Components/ModalContext';
import { fetchListActive as fetchMetadata } from 'Duck/customField'; import { fetchListActive as fetchMetadata } from 'Duck/customField';
import { setSessionPath } from 'Duck/sessions'; import { setSessionPath } from 'Duck/sessions';
import { fetchList as fetchSiteList } from 'Duck/site'; import { fetchList as fetchSiteList } from 'Duck/site';
import { init as initSite } from 'Duck/site'; import { init as initSite } from 'Duck/site';
import { fetchUserInfo, getScope, setJwt, logout } from "Duck/user"; import { fetchUserInfo, getScope, logout, setJwt } from 'Duck/user';
import { fetchTenants } from 'Duck/user';
import { Loader } from 'UI'; import { Loader } from 'UI';
import { spotsList } from "./routes";
import * as routes from './routes'; import * as routes from './routes';
interface RouterProps interface RouterProps
@ -36,7 +34,6 @@ interface RouterProps
changePassword: boolean; changePassword: boolean;
isEnterprise: boolean; isEnterprise: boolean;
fetchUserInfo: () => any; fetchUserInfo: () => any;
fetchTenants: () => any;
setSessionPath: (path: any) => any; setSessionPath: (path: any) => any;
fetchSiteList: (siteId?: number) => any; fetchSiteList: (siteId?: number) => any;
match: { match: {
@ -45,7 +42,7 @@ interface RouterProps
}; };
}; };
mstore: any; mstore: any;
setJwt: (params: { jwt: string, spotJwt: string | null }) => any; setJwt: (params: { jwt: string; spotJwt: string | null }) => any;
fetchMetadata: (siteId: string) => void; fetchMetadata: (siteId: string) => void;
initSite: (site: any) => void; initSite: (site: any) => void;
scopeSetup: boolean; scopeSetup: boolean;
@ -68,15 +65,16 @@ const Router: React.FC<RouterProps> = (props) => {
logout, logout,
} = props; } = props;
const params = new URLSearchParams(location.search) const params = new URLSearchParams(location.search);
const spotCb = params.get('spotCallback'); const spotCb = params.get('spotCallback');
const spotReqSent = React.useRef(false) const spotReqSent = React.useRef(false);
const [isSpotCb, setIsSpotCb] = React.useState(false); const [isSpotCb, setIsSpotCb] = React.useState(false);
const [isSignup, setIsSignup] = React.useState(false);
const [isIframe, setIsIframe] = React.useState(false); const [isIframe, setIsIframe] = React.useState(false);
const [isJwt, setIsJwt] = React.useState(false); const [isJwt, setIsJwt] = React.useState(false);
const handleJwtFromUrl = () => { const handleJwtFromUrl = () => {
const params = new URLSearchParams(location.search) const params = new URLSearchParams(location.search);
const urlJWT = params.get('jwt'); const urlJWT = params.get('jwt');
const spotJwt = params.get('spotJwt'); const spotJwt = params.get('spotJwt');
if (spotJwt) { if (spotJwt) {
@ -92,6 +90,7 @@ const Router: React.FC<RouterProps> = (props) => {
return; return;
} else { } else {
spotReqSent.current = true; spotReqSent.current = true;
setIsSpotCb(false);
} }
handleSpotJWT(jwt); handleSpotJWT(jwt);
}; };
@ -107,13 +106,17 @@ const Router: React.FC<RouterProps> = (props) => {
const handleUserLogin = async () => { const handleUserLogin = async () => {
if (isSpotCb) { if (isSpotCb) {
localStorage.setItem(SPOT_ONBOARDING, 'true') localStorage.setItem(SPOT_ONBOARDING, 'true');
} }
await fetchUserInfo(); await fetchUserInfo();
const siteIdFromPath = parseInt(location.pathname.split('/')[1]); const siteIdFromPath = parseInt(location.pathname.split('/')[1]);
await fetchSiteList(siteIdFromPath); await fetchSiteList(siteIdFromPath);
props.mstore.initClient(); props.mstore.initClient();
if (localSpotJwt && !isTokenExpired(localSpotJwt)) {
handleSpotLogin(localSpotJwt);
}
const destinationPath = localStorage.getItem(GLOBAL_DESTINATION_PATH); const destinationPath = localStorage.getItem(GLOBAL_DESTINATION_PATH);
if ( if (
destinationPath && destinationPath &&
@ -144,7 +147,10 @@ const Router: React.FC<RouterProps> = (props) => {
if (spotCb) { if (spotCb) {
setIsSpotCb(true); setIsSpotCb(true);
} }
}, [spotCb]) if (location.pathname.includes('signup')) {
setIsSignup(true);
}
}, [spotCb]);
useEffect(() => { useEffect(() => {
handleDestinationPath(); handleDestinationPath();
@ -159,22 +165,14 @@ const Router: React.FC<RouterProps> = (props) => {
}, [isLoggedIn]); }, [isLoggedIn]);
useEffect(() => { useEffect(() => {
if (scopeSetup) { if (isLoggedIn && isSpotCb && !isSignup) {
history.push(routes.scopeSetup()) if (localSpotJwt && !isTokenExpired(localSpotJwt)) {
} handleSpotLogin(localSpotJwt);
}, [scopeSetup]) } else {
logout();
useEffect(() => {
if (isLoggedIn && (location.pathname.includes('login') || isSpotCb)) {
if (localSpotJwt) {
if (!isTokenExpired(localSpotJwt)) {
handleSpotLogin(localSpotJwt);
} else {
logout();
}
} }
} }
}, [isSpotCb, location, isLoggedIn, localSpotJwt]) }, [isSpotCb, isLoggedIn, localSpotJwt, isSignup]);
useEffect(() => { useEffect(() => {
if (siteId && siteId !== lastFetchedSiteIdRef.current) { if (siteId && siteId !== lastFetchedSiteIdRef.current) {
@ -204,8 +202,7 @@ const Router: React.FC<RouterProps> = (props) => {
location.pathname.includes('multiview') || location.pathname.includes('multiview') ||
location.pathname.includes('/view-spot/') || location.pathname.includes('/view-spot/') ||
location.pathname.includes('/spots/') || location.pathname.includes('/spots/') ||
location.pathname.includes('/scope-setup') location.pathname.includes('/scope-setup');
if (isIframe) { if (isIframe) {
return ( return (
@ -238,8 +235,11 @@ const mapStateToProps = (state: Map<string, any>) => {
'loading', 'loading',
]); ]);
const sitesLoading = state.getIn(['site', 'fetchListRequest', 'loading']); const sitesLoading = state.getIn(['site', 'fetchListRequest', 'loading']);
const scopeSetup = getScope(state) === 0 const scopeSetup = getScope(state) === 0;
const loading = Boolean(userInfoLoading) || Boolean(sitesLoading) || (!scopeSetup && !siteId); const loading =
Boolean(userInfoLoading) ||
Boolean(sitesLoading) ||
(!scopeSetup && !siteId);
return { return {
siteId, siteId,
changePassword, changePassword,
@ -262,7 +262,6 @@ const mapStateToProps = (state: Map<string, any>) => {
const mapDispatchToProps = { const mapDispatchToProps = {
fetchUserInfo, fetchUserInfo,
fetchTenants,
setSessionPath, setSessionPath,
fetchSiteList, fetchSiteList,
setJwt, setJwt,

View file

@ -2,7 +2,7 @@ import { ArrowRightOutlined } from '@ant-design/icons';
import { Button, Card, Radio } from 'antd'; import { Button, Card, Radio } from 'antd';
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { upgradeScope, downgradeScope } from "App/duck/user"; import { upgradeScope, downgradeScope, getScope } from 'App/duck/user';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import * as routes from 'App/routes' import * as routes from 'App/routes'
import { SPOT_ONBOARDING } from "../../constants/storageKeys"; import { SPOT_ONBOARDING } from "../../constants/storageKeys";
@ -15,8 +15,18 @@ const Scope = {
function ScopeForm({ function ScopeForm({
upgradeScope, upgradeScope,
downgradeScope, downgradeScope,
scopeState,
}: any) { }: any) {
const [scope, setScope] = React.useState(Scope.FULL); const [scope, setScope] = React.useState(Scope.FULL);
React.useEffect(() => {
if (scopeState !== 0) {
if (scopeState === 2) {
history.replace(routes.onboarding())
} else {
history.replace(routes.spotsList())
}
}
}, [scopeState])
React.useEffect(() => { React.useEffect(() => {
const isSpotSetup = localStorage.getItem(SPOT_ONBOARDING) const isSpotSetup = localStorage.getItem(SPOT_ONBOARDING)
if (isSpotSetup) { if (isSpotSetup) {
@ -36,50 +46,52 @@ function ScopeForm({
}; };
return ( return (
<div className={'flex items-center justify-center w-screen h-screen'}> <div className={'flex items-center justify-center w-screen h-screen'}>
<Card <Card
style={{ width: 540 }} style={{ width: 540 }}
title={'👋 Welcome to OpenReplay'} title={'👋 Welcome to OpenReplay'}
classNames={{ classNames={{
header: 'text-2xl font-semibold text-center', header: 'text-2xl font-semibold text-center',
body: 'flex flex-col gap-2', body: 'flex flex-col gap-2',
}} }}
>
<div className={'font-semibold'}>
How will you primarily use OpenReplay?{' '}
</div>
<div className={'text-disabled-text'}>
<div>
You will have access to all OpenReplay features regardless of your
choice.
</div>
<div>
Your preference will simply help us tailor your onboarding experience.
</div>
</div>
<Radio.Group
value={scope}
onChange={(e) => setScope(e.target.value)}
className={'flex flex-col gap-2 mt-4 '}
> >
<Radio value={'full'}> <div className={'font-semibold'}>
Session Replay & Debugging, Customer Support and more How will you primarily use OpenReplay?{' '}
</Radio> </div>
<Radio value={'spot'}>Report bugs via Spot</Radio> <div className={'text-disabled-text'}>
</Radio.Group> <div>
You will have access to all OpenReplay features regardless of your
<div className={'self-end'}> choice.
<Button </div>
type={'primary'} <div>
onClick={() => onContinue()} Your preference will simply help us tailor your onboarding experience.
icon={<ArrowRightOutlined />} </div>
iconPosition={'end'} </div>
<Radio.Group
value={scope}
onChange={(e) => setScope(e.target.value)}
className={'flex flex-col gap-2 mt-4 '}
> >
Continue <Radio value={'full'}>
</Button> Session Replay & Debugging, Customer Support and more
</div> </Radio>
</Card> <Radio value={'spot'}>Report bugs via Spot</Radio>
</Radio.Group>
<div className={'self-end'}>
<Button
type={'primary'}
onClick={() => onContinue()}
icon={<ArrowRightOutlined />}
iconPosition={'end'}
>
Continue
</Button>
</div>
</Card>
</div> </div>
); );
} }
export default connect(null, { upgradeScope, downgradeScope })(ScopeForm); export default connect((state) => ({
scopeState: getScope(state),
}), { upgradeScope, downgradeScope })(ScopeForm);

View file

@ -10,7 +10,7 @@ const SpotsListHeader = observer(
onDelete, onDelete,
selectedCount, selectedCount,
onClearSelection, onClearSelection,
isEmpty, tenantHasSpots,
onRefresh, onRefresh,
}: { }: {
onDelete: () => void; onDelete: () => void;
@ -18,6 +18,7 @@ const SpotsListHeader = observer(
onClearSelection: () => void; onClearSelection: () => void;
onRefresh: () => void; onRefresh: () => void;
isEmpty?: boolean; isEmpty?: boolean;
tenantHasSpots: boolean;
}) => { }) => {
const { spotStore } = useStore(); const { spotStore } = useStore();
@ -52,7 +53,7 @@ const SpotsListHeader = observer(
<ReloadButton buttonSize={'small'} onClick={onRefresh} iconSize={16} /> <ReloadButton buttonSize={'small'} onClick={onRefresh} iconSize={16} />
</div> </div>
{isEmpty ? null : ( {tenantHasSpots ? null : (
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<div className={'ml-auto'}> <div className={'ml-auto'}>
{selectedCount > 0 && ( {selectedCount > 0 && (

View file

@ -89,6 +89,7 @@ function SpotsList() {
selectedCount={selectedSpots.length} selectedCount={selectedSpots.length}
onClearSelection={clearSelection} onClearSelection={clearSelection}
isEmpty={isEmpty} isEmpty={isEmpty}
tenantHasSpots={spotStore.tenantHasSpots}
/> />
</div> </div>

View file

@ -1,10 +1,15 @@
import { makeAutoObservable } from 'mobx'; import { makeAutoObservable } from 'mobx';
import { spotService } from 'App/services'; import { spotService } from 'App/services';
import { UpdateSpotRequest } from 'App/services/spotService'; import { UpdateSpotRequest } from 'App/services/spotService';
import { Spot } from './types/spot'; import { Spot } from './types/spot';
export default class SpotStore { export default class SpotStore {
isLoading: boolean = false; isLoading: boolean = false;
spots: Spot[] = []; spots: Spot[] = [];
@ -18,6 +23,7 @@ export default class SpotStore {
pubKey: { value: string; expiration: number } | null = null; pubKey: { value: string; expiration: number } | null = null;
readonly order = 'desc'; readonly order = 'desc';
accessError = false; accessError = false;
tenantHasSpots = false;
constructor() { constructor() {
makeAutoObservable(this); makeAutoObservable(this);
@ -81,13 +87,18 @@ export default class SpotStore {
limit: this.limit, limit: this.limit,
} as const; } as const;
const response = await this.withLoader(() => const { spots, tenantHasSpots, total } = await this.withLoader(() =>
spotService.fetchSpots(filters) spotService.fetchSpots(filters)
); );
this.setSpots(response.spots.map((spot: any) => new Spot(spot))); this.setSpots(spots.map((spot: any) => new Spot(spot)));
this.setTotal(response.total); this.setTotal(total);
this.setTenantHasSpots(tenantHasSpots);
}; };
setTenantHasSpots(hasSpots: boolean) {
this.tenantHasSpots = hasSpots;
}
async fetchSpotById(id: string) { async fetchSpotById(id: string) {
try { try {
const response = await this.withLoader(() => const response = await this.withLoader(() =>

View file

@ -33,6 +33,7 @@ interface AddCommentRequest {
interface GetSpotsResponse { interface GetSpotsResponse {
spots: SpotInfo[]; spots: SpotInfo[];
total: number; total: number;
tenantHasSpots: boolean;
} }
interface GetSpotsRequest { interface GetSpotsRequest {

View file

@ -504,7 +504,6 @@ export function truncateStringToFit(string: string, screenWidth: number, charWid
let sendingRequest = false; let sendingRequest = false;
export const handleSpotJWT = (jwt: string) => { export const handleSpotJWT = (jwt: string) => {
console.log(jwt, sendingRequest)
let tries = 0; let tries = 0;
if (!jwt || sendingRequest) { if (!jwt || sendingRequest) {
return; return;

View file

@ -2,7 +2,7 @@
"name": "wxt-starter", "name": "wxt-starter",
"description": "manifest.json description", "description": "manifest.json description",
"private": true, "private": true,
"version": "1.0.5", "version": "1.0.6",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "wxt", "dev": "wxt",