From df20cd5333e95b487b1ede59f1087c127b75c997 Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Wed, 18 Sep 2024 15:39:33 +0200 Subject: [PATCH] move api and "few" files to new project store --- frontend/app/PrivateRoutes.tsx | 20 +- frontend/app/Router.tsx | 59 +++--- frontend/app/api_client.ts | 11 +- .../Client/CustomFields/CustomFields.tsx | 22 +-- .../Client/Integrations/IntegrationForm.tsx | 11 +- .../Client/Integrations/Integrations.tsx | 12 +- .../Client/Integrations/PiniaDoc/PiniaDoc.tsx | 14 +- .../AddProjectButton/AddProjectButton.tsx | 18 +- .../app/components/Client/Sites/GDPRForm.js | 184 +++++++++--------- .../app/components/Client/Sites/Sites.tsx | 27 ++- .../app/components/hocs/withSiteIdRouter.js | 39 ++-- .../app/components/hocs/withSiteIdUpdater.js | 61 +++--- .../ProjectDropdown/ProjectDropdown.tsx | 31 ++- frontend/app/layout/Layout.tsx | 22 +-- frontend/app/layout/TopHeader.tsx | 14 +- frontend/app/layout/TopRight.tsx | 10 +- frontend/app/mstore/index.tsx | 1 + frontend/app/services/DashboardService.ts | 30 +-- 18 files changed, 255 insertions(+), 331 deletions(-) diff --git a/frontend/app/PrivateRoutes.tsx b/frontend/app/PrivateRoutes.tsx index 97f7fac83..0d562bff9 100644 --- a/frontend/app/PrivateRoutes.tsx +++ b/frontend/app/PrivateRoutes.tsx @@ -3,8 +3,8 @@ import { Map } from 'immutable'; import React, { Suspense, lazy } from 'react'; import { connect } from 'react-redux'; import { Redirect, Route, Switch } from 'react-router-dom'; - -import AdditionalRoutes from 'App/AdditionalRoutes'; +import { observer } from 'mobx-react-lite' +import { useStore } from "./mstore"; import { GLOBAL_HAS_NO_RECORDINGS } from 'App/constants/storageKeys'; import { OB_DEFAULT_TAB } from 'App/routes'; import { Loader } from 'UI'; @@ -110,20 +110,20 @@ const SCOPE_SETUP = routes.scopeSetup(); interface Props { tenantId: string; - siteId: string; - sites: Map; onboarding: boolean; scope: number; } function PrivateRoutes(props: Props) { - const { onboarding, sites, siteId } = props; + const { projectsStore } = useStore(); + const sites = projectsStore.list; + const siteId = projectsStore.siteId; + const { onboarding } = props; const hasRecordings = sites.some(s => s.recorded); const redirectToSetup = props.scope === 0; const redirectToOnboarding = - !onboarding && (localStorage.getItem(GLOBAL_HAS_NO_RECORDINGS) === 'true' || !hasRecordings) && props.scope > 0; - const siteIdList: any = sites.map(({ id }) => id).toJS(); - + !onboarding && (localStorage.getItem(GLOBAL_HAS_NO_RECORDINGS) === 'true' || (sites.length > 0 && !hasRecordings)) && props.scope > 0; + const siteIdList: any = sites.map(({ id }) => id); return ( }> @@ -292,7 +292,5 @@ function PrivateRoutes(props: Props) { export default connect((state: any) => ({ onboarding: state.getIn(['user', 'onboarding']), scope: getScope(state), - sites: state.getIn(['site', 'list']), - siteId: state.getIn(['site', 'siteId']), tenantId: state.getIn(['user', 'account', 'tenantId']), -}))(PrivateRoutes); +}))(observer(PrivateRoutes)); diff --git a/frontend/app/Router.tsx b/frontend/app/Router.tsx index 8c5801dc9..b3986e35b 100644 --- a/frontend/app/Router.tsx +++ b/frontend/app/Router.tsx @@ -13,56 +13,55 @@ import { SPOT_ONBOARDING } from 'App/constants/storageKeys'; import Layout from 'App/layout/Layout'; -import { useStore, withStore } from 'App/mstore'; +import { useStore } from 'App/mstore'; import { checkParam, handleSpotJWT, isTokenExpired } from 'App/utils'; import { ModalProvider } from 'Components/Modal'; import { ModalProvider as NewModalProvider } from 'Components/ModalContext'; import { setSessionPath } from 'Duck/sessions'; -import { fetchList as fetchSiteList } from 'Duck/site'; -import { init as initSite } from 'Duck/site'; import { fetchUserInfo, getScope, logout, setJwt } from 'Duck/user'; import { Loader } from 'UI'; import * as routes from './routes'; +import { observer } from 'mobx-react-lite' interface RouterProps extends RouteComponentProps, ConnectedProps { isLoggedIn: boolean; - sites: Map; - loading: boolean; changePassword: boolean; isEnterprise: boolean; fetchUserInfo: () => any; setSessionPath: (path: any) => any; - fetchSiteList: (siteId?: number) => any; match: { params: { siteId: string; }; }; - mstore: any; setJwt: (params: { jwt: string; spotJwt: string | null }) => any; - initSite: (site: any) => void; - scopeSetup: boolean; localSpotJwt: string | null; } const Router: React.FC = (props) => { const { isLoggedIn, - siteId, - sites, - loading, + userInfoLoading, location, fetchUserInfo, - fetchSiteList, history, setSessionPath, - scopeSetup, localSpotJwt, - logout + logout, + scopeSetup, + setJwt, } = props; - const { customFieldStore } = useStore(); + const mstore = useStore(); + const { customFieldStore, projectsStore } = mstore; + + const siteId = projectsStore.siteId; + const sitesLoading = projectsStore.sitesLoading; + const sites = projectsStore.list; + const loading = Boolean(userInfoLoading || (!scopeSetup && !siteId) || sitesLoading); + const initSite = projectsStore.initProject; + const fetchSiteList = projectsStore.fetchList; const params = new URLSearchParams(location.search); const spotCb = params.get('spotCallback'); @@ -80,7 +79,7 @@ const Router: React.FC = (props) => { handleSpotLogin(spotJwt); } if (urlJWT) { - props.setJwt({ jwt: urlJWT, spotJwt: spotJwt ?? null }); + setJwt({ jwt: urlJWT, spotJwt: spotJwt ?? null }); } }; @@ -108,9 +107,9 @@ const Router: React.FC = (props) => { localStorage.setItem(SPOT_ONBOARDING, 'true'); } await fetchUserInfo(); - const siteIdFromPath = parseInt(location.pathname.split('/')[1]); + const siteIdFromPath = location.pathname.split('/')[1]; await fetchSiteList(siteIdFromPath); - props.mstore.initClient(); + mstore.initClient(); if (localSpotJwt && !isTokenExpired(localSpotJwt)) { handleSpotLogin(localSpotJwt); @@ -177,13 +176,13 @@ const Router: React.FC = (props) => { const fetchData = async () => { if (siteId && siteId !== lastFetchedSiteIdRef.current) { const activeSite = sites.find((s) => s.id == siteId); - props.initSite(activeSite); - lastFetchedSiteIdRef.current = activeSite.id; + initSite(activeSite ?? {}); + lastFetchedSiteIdRef.current = activeSite?.id; await customFieldStore.fetchListActive(siteId + ''); } }; - fetchData(); + void fetchData(); }, [siteId]); const lastFetchedSiteIdRef = useRef(null); @@ -229,7 +228,6 @@ const Router: React.FC = (props) => { }; const mapStateToProps = (state: Map) => { - const siteId = state.getIn(['site', 'siteId']); const jwt = state.getIn(['user', 'jwt']); const changePassword = state.getIn(['user', 'account', 'changePassword']); const userInfoLoading = state.getIn([ @@ -237,21 +235,14 @@ const mapStateToProps = (state: Map) => { 'fetchUserInfoRequest', 'loading' ]); - const sitesLoading = state.getIn(['site', 'fetchListRequest', 'loading']); const scopeSetup = getScope(state) === 0; - const loading = - Boolean(userInfoLoading) || - Boolean(sitesLoading) || - (!scopeSetup && !siteId); return { - siteId, changePassword, - sites: state.getIn(['site', 'list']), jwt, + scopeSetup, localSpotJwt: state.getIn(['user', 'spotJwt']), isLoggedIn: jwt !== null && !changePassword, - scopeSetup, - loading, + userInfoLoading, email: state.getIn(['user', 'account', 'email']), account: state.getIn(['user', 'account']), organisation: state.getIn(['user', 'account', 'name']), @@ -266,12 +257,10 @@ const mapStateToProps = (state: Map) => { const mapDispatchToProps = { fetchUserInfo, setSessionPath, - fetchSiteList, setJwt, - initSite, logout }; const connector = connect(mapStateToProps, mapDispatchToProps); -export default withStore(withRouter(connector(Router))); +export default withRouter(connector(observer(Router))); diff --git a/frontend/app/api_client.ts b/frontend/app/api_client.ts index c967e9c0f..2ea7f80d9 100644 --- a/frontend/app/api_client.ts +++ b/frontend/app/api_client.ts @@ -1,7 +1,6 @@ import store from 'App/store'; import { queried } from './routes'; import { setJwt } from 'Duck/user'; -import { projectStore } from 'App/mstore'; const siteIdRequiredPaths: string[] = [ '/dashboard', @@ -55,12 +54,12 @@ export const clean = (obj: any, forbiddenValues: any[] = [undefined, '']): any = export default class APIClient { private init: RequestInit; - private readonly siteId: string | undefined; + private siteId: string | undefined; + private siteIdCheck: (() => { siteId: string | null }) | undefined; private refreshingTokenPromise: Promise | null = null; constructor() { const jwt = store.getState().getIn(['user', 'jwt']); - const { siteId } = projectStore.getSiteId(); this.init = { headers: new Headers({ Accept: 'application/json', @@ -70,7 +69,10 @@ export default class APIClient { if (jwt !== null) { (this.init.headers as Headers).set('Authorization', `Bearer ${jwt}`); } - this.siteId = siteId || undefined; + } + + setSiteIdCheck(checker: () => { siteId: string | null }): void { + this.siteIdCheck = checker } private getInit(method: string = 'GET', params?: any, reqHeaders?: Record): RequestInit { @@ -102,6 +104,7 @@ export default class APIClient { delete init.body; // GET requests shouldn't have a body } + this.siteId = this.siteIdCheck?.().siteId ?? undefined; return init; } diff --git a/frontend/app/components/Client/CustomFields/CustomFields.tsx b/frontend/app/components/Client/CustomFields/CustomFields.tsx index dd42911e4..e4152911e 100644 --- a/frontend/app/components/Client/CustomFields/CustomFields.tsx +++ b/frontend/app/components/Client/CustomFields/CustomFields.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from 'react'; import cn from 'classnames'; -import { connect } from 'react-redux'; import withPageTitle from 'HOCs/withPageTitle'; import { Button, Loader, NoContent, Icon, Tooltip, Divider } from 'UI'; import SiteDropdown from 'Shared/SiteDropdown'; @@ -12,20 +11,17 @@ import { useModal } from 'App/components/Modal'; import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; -interface CustomFieldsProps { - sites: any; -} - -const CustomFields: React.FC = (props) => { - const [currentSite, setCurrentSite] = useState(props.sites.get(0)); +const CustomFields = () => { + const { customFieldStore: store, projectsStore } = useStore(); + const sites = projectsStore.list; + const [currentSite, setCurrentSite] = useState(sites[0]); const [deletingItem, setDeletingItem] = useState(null); const { showModal, hideModal } = useModal(); - const { customFieldStore: store } = useStore(); const fields = store.list; const [loading, setLoading] = useState(false); useEffect(() => { - const activeSite = props.sites.get(0); + const activeSite = sites[0]; if (!activeSite) return; setCurrentSite(activeSite); @@ -34,7 +30,7 @@ const CustomFields: React.FC = (props) => { store.fetchList(activeSite.id).finally(() => { setLoading(false); }); - }, [props.sites]); + }, [sites]); const handleInit = (field?: any) => { console.log('field', field); @@ -45,7 +41,7 @@ const CustomFields: React.FC = (props) => { }; const onChangeSelect = ({ value }: { value: { value: number } }) => { - const site = props.sites.find((s: any) => s.id === value.value); + const site = sites.find((s: any) => s.id === value.value); setCurrentSite(site); setLoading(true); @@ -109,6 +105,4 @@ const CustomFields: React.FC = (props) => { ); }; -export default connect((state: any) => ({ - sites: state.getIn(['site', 'list']) -}))(withPageTitle('Metadata - OpenReplay Preferences')(observer(CustomFields))); +export default withPageTitle('Metadata - OpenReplay Preferences')(observer(CustomFields)); diff --git a/frontend/app/components/Client/Integrations/IntegrationForm.tsx b/frontend/app/components/Client/Integrations/IntegrationForm.tsx index 905b41d70..a290e91f1 100644 --- a/frontend/app/components/Client/Integrations/IntegrationForm.tsx +++ b/frontend/app/components/Client/Integrations/IntegrationForm.tsx @@ -8,7 +8,9 @@ import { Button, Checkbox, Form, Input, Loader } from 'UI'; function IntegrationForm(props: any) { const { formFields, name, integrated } = props; - const { integrationsStore } = useStore(); + const { integrationsStore, projectsStore } = useStore(); + const sites = projectsStore.list; + const initialSiteId = projectsStore.siteId; const integrationStore = integrationsStore[name as unknown as namedStore]; const config = integrationStore.instance; const loading = integrationStore.loading; @@ -18,7 +20,7 @@ function IntegrationForm(props: any) { const fetchIntegrationList = integrationsStore.integrations.fetchIntegrations; const fetchList = () => { - void fetchIntegrationList(props.initialSiteId); + void fetchIntegrationList(initialSiteId); }; const write = ({ target: { value, name: key, type, checked } }) => { @@ -104,7 +106,4 @@ function IntegrationForm(props: any) { ); } -export default connect((state: any) => ({ - sites: state.getIn(['site', 'list']), - initialSiteId: state.getIn(['site', 'siteId']), -}))(observer(IntegrationForm)); +export default observer(IntegrationForm); diff --git a/frontend/app/components/Client/Integrations/Integrations.tsx b/frontend/app/components/Client/Integrations/Integrations.tsx index b3cf4050f..2509e883b 100644 --- a/frontend/app/components/Client/Integrations/Integrations.tsx +++ b/frontend/app/components/Client/Integrations/Integrations.tsx @@ -41,11 +41,11 @@ interface Props { } function Integrations(props: Props) { - const { integrationsStore } = useStore(); - + const { integrationsStore, projectsStore } = useStore(); + const siteId = projectsStore.siteId; const fetchIntegrationList = integrationsStore.integrations.fetchIntegrations; const storeIntegratedList = integrationsStore.integrations.list; - const { siteId, hideHeader = false } = props; + const { hideHeader = false } = props; const { showModal } = useModal(); const [integratedList, setIntegratedList] = useState([]); const [activeFilter, setActiveFilter] = useState('all'); @@ -162,11 +162,7 @@ function Integrations(props: Props) { ); } -export default connect((state: any) => ({ - siteId: state.getIn(['site', 'siteId']), -}))( - withPageTitle('Integrations - OpenReplay Preferences')(observer(Integrations)) -); +export default withPageTitle('Integrations - OpenReplay Preferences')(observer(Integrations)) const integrations = [ { diff --git a/frontend/app/components/Client/Integrations/PiniaDoc/PiniaDoc.tsx b/frontend/app/components/Client/Integrations/PiniaDoc/PiniaDoc.tsx index 4184b4293..71a9cbccb 100644 --- a/frontend/app/components/Client/Integrations/PiniaDoc/PiniaDoc.tsx +++ b/frontend/app/components/Client/Integrations/PiniaDoc/PiniaDoc.tsx @@ -1,6 +1,5 @@ import { observer } from 'mobx-react-lite'; import React from 'react'; -import { connect } from 'react-redux'; import { useStore } from 'App/mstore'; import ToggleContent from 'Components/shared/ToggleContent'; @@ -8,9 +7,9 @@ import { CodeBlock } from 'UI'; import DocLink from 'Shared/DocLink/DocLink'; -const PiniaDoc = (props) => { - const { integrationsStore } = useStore(); - const sites = props.sites ? props.sites.toJS() : []; +const PiniaDoc = () => { + const { integrationsStore, projectsStore } = useStore(); + const sites = projectsStore.list; const siteId = integrationsStore.integrations.siteId; const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey @@ -110,9 +109,4 @@ piniaStorePlugin(examplePiniaStore) PiniaDoc.displayName = 'PiniaDoc'; -export default connect((state: any) => { - const sites = state.getIn(['site', 'list']); - return { - sites, - }; -})(observer(PiniaDoc)); +export default observer(PiniaDoc); diff --git a/frontend/app/components/Client/Sites/AddProjectButton/AddProjectButton.tsx b/frontend/app/components/Client/Sites/AddProjectButton/AddProjectButton.tsx index 0a2712462..5f0df0bce 100644 --- a/frontend/app/components/Client/Sites/AddProjectButton/AddProjectButton.tsx +++ b/frontend/app/components/Client/Sites/AddProjectButton/AddProjectButton.tsx @@ -1,8 +1,7 @@ import React from 'react'; import { Tooltip, Button } from 'UI'; import { useStore } from 'App/mstore'; -import { useObserver } from 'mobx-react-lite'; -import { init, remove, fetchGDPR } from 'Duck/site'; +import { observer } from 'mobx-react-lite'; import { connect } from 'react-redux'; import { useModal } from 'App/components/Modal'; import NewSiteForm from '../NewSiteForm'; @@ -10,16 +9,15 @@ import NewSiteForm from '../NewSiteForm'; const PERMISSION_WARNING = 'You don’t have the permissions to perform this action.'; const LIMIT_WARNING = 'You have reached site limit.'; -function AddProjectButton({ isAdmin = false, init = () => {} }: any) { - const { userStore } = useStore(); +function AddProjectButton({ isAdmin = false }: any) { + const { userStore, projectsStore } = useStore(); + const init = projectsStore.initProject; const { showModal, hideModal } = useModal(); - const limtis = useObserver(() => userStore.limits); - const canAddProject = useObserver( - () => isAdmin && (limtis.projects === -1 || limtis.projects > 0) - ); + const limits = userStore.limits; + const canAddProject = isAdmin && (limits.projects === -1 || limits.projects > 0) const onClick = () => { - init(); + init({}); showModal(, { right: true }); }; return ( @@ -34,4 +32,4 @@ function AddProjectButton({ isAdmin = false, init = () => {} }: any) { ); } -export default connect(null, { init, remove, fetchGDPR })(AddProjectButton); +export default observer(AddProjectButton); diff --git a/frontend/app/components/Client/Sites/GDPRForm.js b/frontend/app/components/Client/Sites/GDPRForm.js index c65559cd3..fa54f73be 100644 --- a/frontend/app/components/Client/Sites/GDPRForm.js +++ b/frontend/app/components/Client/Sites/GDPRForm.js @@ -1,7 +1,7 @@ import React from 'react'; -import { connect } from 'react-redux'; +import { observer } from 'mobx-react-lite'; +import { useStore } from "App/mstore"; import { Form, Button, Input, Icon } from 'UI'; -import { editGDPR, saveGDPR } from 'Duck/site'; import { validateNumber } from 'App/validate'; import styles from './siteForm.module.css'; import Select from 'Shared/Select'; @@ -12,124 +12,118 @@ const inputModeOptions = [ { label: 'Obscure all inputs', value: 'hidden' }, ]; -@connect(state => ({ - site: state.getIn([ 'site', 'instance' ]), - gdpr: state.getIn([ 'site', 'instance', 'gdpr' ]), - saving: state.getIn([ 'site', 'saveGDPR', 'loading' ]), -}), { - editGDPR, - saveGDPR, -}) -export default class GDPRForm extends React.PureComponent { - onChange = ({ target: { name, value } }) => { +function GDPRForm(props) { + const { projectsStore } = useStore(); + const site = projectsStore.instance; + const gdpr = site.gdpr; + const saving = false //projectsStore.; + const editGDPR = projectsStore.editGDPR; + const saveGDPR = projectsStore.saveGDPR; + + + const onChange = ({ target: { name, value } }) => { if (name === "sampleRate") { if (!validateNumber(value, { min: 0, max: 100 })) return; if (value.length > 1 && value[0] === "0") { value = value.slice(1); } } - this.props.editGDPR({ [ name ]: value }); + editGDPR({ [ name ]: value }); } - onSampleRateBlur = ({ target: { name, value } }) => { //TODO: editState hoc + const onSampleRateBlur = ({ target: { name, value } }) => { //TODO: editState hoc if (value === ''){ - this.props.editGDPR({ sampleRate: 100 }); + editGDPR({ sampleRate: 100 }); } } - onChangeSelect = ({ name, value }) => { - this.props.editGDPR({ [ name ]: value }); + const onChangeSelect = ({ name, value }) => { + props.editGDPR({ [ name ]: value }); }; - onChangeOption = ({ target: { checked, name } }) => { - this.props.editGDPR({ [ name ]: checked }); + const onChangeOption = ({ target: { checked, name } }) => { + editGDPR({ [ name ]: checked }); } - onSubmit = (e) => { + const onSubmit = (e) => { e.preventDefault(); - const { site, gdpr } = this.props; - this.props.saveGDPR(site.id, gdpr); + void saveGDPR(site.id); } + + return ( +
+
+ + +
{ site.host }
+
+ + + + - render() { - const { - site, onClose, saving, gdpr, - } = this.props; + + + + + { 'Do not record any numeric text' } +
{ 'If enabled, OpenReplay will not record or store any numeric text for all sessions.' }
+ + - - - - + { 'Do not record email addresses ' } +
{ 'If enabled, OpenReplay will not record or store any email address for all sessions.' }
+ + - - - - - - - - -
-
- { 'Block IP' } -
+
+
+ { 'Block IP' }
+
-
-
- - ); - } +
+
+ + ) } + +export default observer(GDPRForm); \ No newline at end of file diff --git a/frontend/app/components/Client/Sites/Sites.tsx b/frontend/app/components/Client/Sites/Sites.tsx index 0b716fb4c..f804bede4 100644 --- a/frontend/app/components/Client/Sites/Sites.tsx +++ b/frontend/app/components/Client/Sites/Sites.tsx @@ -3,7 +3,6 @@ import { connect, ConnectedProps } from 'react-redux'; import { Tag } from 'antd'; import cn from 'classnames'; import { Loader, Button, TextLink, NoContent, Pagination, PageTitle, Divider, Icon } from 'UI'; -import { init, remove, fetchGDPR, setSiteId } from 'Duck/site'; import withPageTitle from 'HOCs/withPageTitle'; import stl from './sites.module.css'; import NewSiteForm from './NewSiteForm'; @@ -16,9 +15,11 @@ import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; import { useModal } from 'App/components/Modal'; import CaptureRate from 'Shared/SessionSettings/components/CaptureRate'; import { BranchesOutlined } from '@ant-design/icons'; +import { observer } from 'mobx-react-lite'; +import { useStore } from 'App/mstore' type Project = { - id: number; + id: string; name: string; conditionsCount: number; platform: 'web' | 'mobile'; @@ -29,7 +30,11 @@ type Project = { type PropsFromRedux = ConnectedProps; -const Sites = ({ loading, sites, user, init }: PropsFromRedux) => { +const Sites = ({ user }: PropsFromRedux) => { + const { projectsStore } = useStore(); + const sites = projectsStore.list; + const loading = projectsStore.sitesLoading; + const init = projectsStore.initProject const [searchQuery, setSearchQuery] = useState(''); const [showCaptureRate, setShowCaptureRate] = useState(true); const [activeProject, setActiveProject] = useState(null); @@ -140,7 +145,7 @@ const Sites = ({ loading, sites, user, init }: PropsFromRedux) => {
} size="small" - show={!loading && filteredSites.size === 0} + show={!loading && filteredSites.length === 0} >
Project Name
@@ -160,7 +165,7 @@ const Sites = ({ loading, sites, user, init }: PropsFromRedux) => {
updatePage(page)} limit={pageSize} /> @@ -181,18 +186,10 @@ const Sites = ({ loading, sites, user, init }: PropsFromRedux) => { }; const mapStateToProps = (state: any) => ({ - site: state.getIn(['site', 'instance']), - sites: state.getIn(['site', 'list']), - loading: state.getIn(['site', 'loading']), user: state.getIn(['user', 'account']), account: state.getIn(['user', 'account']), }); -const connector = connect(mapStateToProps, { - init, - remove, - fetchGDPR, - setSiteId, -}); +const connector = connect(mapStateToProps, null); -export default connector(withPageTitle('Projects - OpenReplay Preferences')(Sites)); +export default connector(withPageTitle('Projects - OpenReplay Preferences')(observer(Sites))); diff --git a/frontend/app/components/hocs/withSiteIdRouter.js b/frontend/app/components/hocs/withSiteIdRouter.js index ee41610ce..756a5d280 100644 --- a/frontend/app/components/hocs/withSiteIdRouter.js +++ b/frontend/app/components/hocs/withSiteIdRouter.js @@ -1,32 +1,21 @@ import React from 'react'; import { withRouter } from 'react-router-dom'; -import { connect } from 'react-redux'; import { withSiteId } from 'App/routes'; -import { setSiteId } from 'Duck/site'; +import { observer } from 'mobx-react-lite' +import { useStore } from "App/mstore"; -export default BaseComponent => { - @withRouter - @connect((state, props) => ({ - urlSiteId: props.match.params.siteId, - siteId: state.getIn(['site', 'siteId']), - }), { - setSiteId, - }) - class WrappedClass extends React.PureComponent { - push = (location) => { - const { history, siteId } = this.props; - if (typeof location === 'string') { - history.push(withSiteId(location, siteId)); - } else if (typeof location === 'object') { - history.push({ ...location, pathname: withSiteId(location.pathname, siteId) }); - } - } +export default BaseComponent => withRouter(observer((props) => { + const { history, ...other } = props + const { projectsStore } = useStore(); + const siteId = projectsStore.siteId - render() { - const { history, ...other } = this.props - - return + const push = (location) => { + if (typeof location === 'string') { + history.push(withSiteId(location, siteId)); + } else if (typeof location === 'object') { + history.push({ ...location, pathname: withSiteId(location.pathname, siteId) }); } } - return WrappedClass -} \ No newline at end of file + + return +})) \ No newline at end of file diff --git a/frontend/app/components/hocs/withSiteIdUpdater.js b/frontend/app/components/hocs/withSiteIdUpdater.js index 0849bbd84..2bf0acb44 100644 --- a/frontend/app/components/hocs/withSiteIdUpdater.js +++ b/frontend/app/components/hocs/withSiteIdUpdater.js @@ -1,40 +1,39 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { setSiteId } from 'Duck/site'; +import React, { useEffect, useRef } from 'react'; +import { useStore } from "App/mstore"; +import { observer } from 'mobx-react-lite' -export default (BaseComponent) => { - @connect((state, props) => ({ - urlSiteId: props.match.params.siteId, - siteId: state.getIn(['site', 'siteId']), - }), { - setSiteId, - }) - class WrapperClass extends React.PureComponent { - state = { load: false } - constructor(props) { - super(props); - if (props.urlSiteId && props.urlSiteId !== props.siteId) { - props.setSiteId(props.urlSiteId); +const withSiteIdUpdater = (BaseComponent) => { + const WrapperComponent = (props) => { + const { projectsStore } = useStore(); + const siteId = projectsStore.siteId; + const setSiteId = projectsStore.setSiteId; + const urlSiteId = props.match.params.siteId + const prevSiteIdRef = useRef(props.siteId); + + useEffect(() => { + if (urlSiteId && urlSiteId !== siteId) { + props.setSiteId(urlSiteId); } - } - componentDidUpdate(prevProps) { - const { urlSiteId, siteId, location: { pathname }, history } = this.props; + }, []); + + useEffect(() => { + const { location: { pathname }, history } = props; + const shouldUrlUpdate = urlSiteId && parseInt(urlSiteId, 10) !== parseInt(siteId, 10); if (shouldUrlUpdate) { const path = ['', siteId].concat(pathname.split('/').slice(2)).join('/'); history.push(path); } - const shouldBaseComponentReload = shouldUrlUpdate || siteId !== prevProps.siteId; - if (shouldBaseComponentReload) { - this.setState({ load: true }); - setTimeout(() => this.setState({ load: false }), 0); - } - } + prevSiteIdRef.current = siteId; + }, [urlSiteId, siteId, props.location.pathname, props.history]); - render() { - return this.state.load ? null : ; - } - } + const key = props.siteId; - return WrapperClass -} + const passedProps = { ...props, siteId, setSiteId, urlSiteId }; + return ; + }; + + return observer(WrapperComponent); +}; + +export default withSiteIdUpdater; diff --git a/frontend/app/components/shared/ProjectDropdown/ProjectDropdown.tsx b/frontend/app/components/shared/ProjectDropdown/ProjectDropdown.tsx index de96ad67e..f15493212 100644 --- a/frontend/app/components/shared/ProjectDropdown/ProjectDropdown.tsx +++ b/frontend/app/components/shared/ProjectDropdown/ProjectDropdown.tsx @@ -8,7 +8,8 @@ import React from 'react'; import { connect } from 'react-redux'; import { RouteComponentProps, withRouter } from 'react-router-dom'; -import { useStore, withStore } from 'App/mstore'; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; import { hasSiteId, siteChangeAvailable } from 'App/routes'; import NewSiteForm from 'Components/Client/Sites/NewSiteForm'; import { useModal } from 'Components/Modal'; @@ -27,18 +28,19 @@ interface Site { } interface Props extends RouteComponentProps { - sites: Site[]; - siteId: string; - setSiteId: (siteId: string) => void; clearSearch: (isSession: boolean) => void; clearSearchLive: () => void; - initProject: (data: any) => void; - mstore: any; account: any; } function ProjectDropdown(props: Props) { - const { sites, siteId, location, account } = props; + const mstore = useStore(); + const { projectsStore } = mstore; + const sites = projectsStore.list; + const siteId = projectsStore.siteId; + const setSiteId = projectsStore.setSiteId; + const initProject = projectsStore.initProject; + const { location, account } = props; const isAdmin = account.admin || account.superAdmin; const activeSite = sites.find((s) => s.id === siteId); const showCurrent = @@ -47,21 +49,20 @@ function ProjectDropdown(props: Props) { const { customFieldStore } = useStore(); const handleSiteChange = async (newSiteId: string) => { - props.setSiteId(newSiteId); // Fixed: should set the new siteId, not the existing one + setSiteId(newSiteId); // Fixed: should set the new siteId, not the existing one await customFieldStore.fetchList(newSiteId) props.clearSearch(location.pathname.includes('/sessions')); props.clearSearchLive(); - props.mstore.initClient(); + mstore.initClient(); }; const addProjectClickHandler = () => { - props.initProject({}); + initProject({}); showModal(, { right: true }); }; - // @ts-ignore immutable - const menuItems = sites.toJS().map((site) => ({ + const menuItems = sites.map((site) => ({ key: site.id, label: (
({ - sites: state.getIn(['site', 'list']), - siteId: state.getIn(['site', 'siteId']), account: state.getIn(['user', 'account']), }); export default withRouter( connect(mapStateToProps, { - setSiteId, clearSearch, clearSearchLive, - initProject, - })(withStore(ProjectDropdown)) + })(observer(ProjectDropdown)) ); diff --git a/frontend/app/layout/Layout.tsx b/frontend/app/layout/Layout.tsx index 7b9370072..49a96df2c 100644 --- a/frontend/app/layout/Layout.tsx +++ b/frontend/app/layout/Layout.tsx @@ -4,24 +4,19 @@ import SideMenu from 'App/layout/SideMenu'; import TopHeader from 'App/layout/TopHeader'; import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; -import { init as initSite } from 'Duck/site'; -import { connect } from 'react-redux'; - const { Sider, Content } = AntLayout; interface Props { children: React.ReactNode; hideHeader?: boolean; - siteId?: string; - initSite: (site: any) => void; - sites: any[]; } function Layout(props: Props) { - const { hideHeader, siteId } = props; + const { hideHeader } = props; const isPlayer = /\/(session|assist|view-spot)\//.test(window.location.pathname); - const { settingsStore } = useStore(); + const { settingsStore, projectsStore } = useStore(); + const siteId = projectsStore.siteId; return ( @@ -29,7 +24,7 @@ function Layout(props: Props) { )} - {!hideHeader && !window.location.pathname.includes('/onboarding/') && ( + {!hideHeader && !window.location.pathname.includes('/onboarding/') ? ( - + - )} + ) : null} {props.children} @@ -52,7 +47,4 @@ function Layout(props: Props) { ); } -export default connect((state: any) => ({ - siteId: state.getIn(['site', 'siteId']), - sites: state.getIn(['site', 'list']) -}), { initSite })(observer(Layout)); +export default observer(Layout); diff --git a/frontend/app/layout/TopHeader.tsx b/frontend/app/layout/TopHeader.tsx index e82a4df33..3f2e38b3c 100644 --- a/frontend/app/layout/TopHeader.tsx +++ b/frontend/app/layout/TopHeader.tsx @@ -1,28 +1,26 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import Logo from 'App/layout/Logo'; import TopRight from 'App/layout/TopRight'; import { Layout, Space, Tooltip } from 'antd'; import { useStore } from 'App/mstore'; import { Icon } from 'UI'; -import { observer, useObserver } from 'mobx-react-lite'; +import { observer } from 'mobx-react-lite'; import { INDEXES } from 'App/constants/zindex'; import { connect } from 'react-redux'; import { logout } from 'Duck/user'; -import { init as initSite } from 'Duck/site'; const { Header } = Layout; interface Props { account: any; - siteId: string; - initSite: (site: any) => void; } function TopHeader(props: Props) { const { settingsStore } = useStore(); - const { account, siteId } = props; - const { userStore, notificationStore } = useStore(); + const { account } = props; + const { userStore, notificationStore, projectsStore } = useStore(); + const siteId = projectsStore.siteId; const initialDataFetched = userStore.initialDataFetched; useEffect(() => { @@ -74,12 +72,10 @@ function TopHeader(props: Props) { const mapStateToProps = (state: any) => ({ account: state.getIn(['user', 'account']), - siteId: state.getIn(['site', 'siteId']) }); const mapDispatchToProps = { onLogoutClick: logout, - initSite }; export default connect( diff --git a/frontend/app/layout/TopRight.tsx b/frontend/app/layout/TopRight.tsx index 9beab1bb5..c47902e03 100644 --- a/frontend/app/layout/TopRight.tsx +++ b/frontend/app/layout/TopRight.tsx @@ -10,17 +10,17 @@ import UserMenu from 'Components/Header/UserMenu/UserMenu'; import GettingStartedProgress from 'Shared/GettingStarted/GettingStartedProgress'; import ProjectDropdown from 'Shared/ProjectDropdown'; import { getScope } from "../duck/user"; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; interface Props { account: any; - siteId: any; - sites: any; spotOnly?: boolean; } function TopRight(props: Props) { + const { projectsStore } = useStore(); const { account } = props; - // @ts-ignore return ( {props.spotOnly ? null : ( @@ -52,9 +52,7 @@ function mapStateToProps(state: any) { return { account: state.getIn(['user', 'account']), spotOnly: getScope(state) === 1, - siteId: state.getIn(['site', 'siteId']), - sites: state.getIn(['site', 'list']), }; } -export default connect(mapStateToProps)(TopRight); +export default connect(mapStateToProps)(observer(TopRight)); diff --git a/frontend/app/mstore/index.tsx b/frontend/app/mstore/index.tsx index 3101f8a36..70e009e6c 100644 --- a/frontend/app/mstore/index.tsx +++ b/frontend/app/mstore/index.tsx @@ -98,6 +98,7 @@ export class RootStore { services.forEach((service) => { service.initClient(client); }); + client.setSiteIdCheck(this.projectsStore.getSiteId) } } diff --git a/frontend/app/services/DashboardService.ts b/frontend/app/services/DashboardService.ts index 4c16c4e76..0b8e2a07d 100644 --- a/frontend/app/services/DashboardService.ts +++ b/frontend/app/services/DashboardService.ts @@ -1,22 +1,12 @@ import Dashboard from "App/mstore/types/dashboard"; -import APIClient from 'App/api_client'; +import BaseService from "./BaseService"; import Widget from "App/mstore/types/widget"; -export default class DashboardService { - private client: APIClient; - - constructor(client?: APIClient) { - this.client = client ? client : new APIClient(); - } - - initClient(client?: APIClient) { - this.client = client || new APIClient(); - } - +export default class DashboardService extends BaseService { /** * Get all widgets from a dashboard. * @param dashboardId Required - * @returns + * @returns */ getWidgets(dashboardId: string): Promise { return this.client.get(`/dashboards/${dashboardId}/widgets`) @@ -34,10 +24,10 @@ export default class DashboardService { .then(response => response.json()) .then(response => response.data || []); } - + /** * Get a dashboard by dashboardId. - * @param dashboardId + * @param dashboardId * @returns {Promise} */ getDashboard(dashboardId: string): Promise { @@ -66,9 +56,9 @@ export default class DashboardService { /** * Add a widget to a dashboard. - * @param dashboard - * @param metricIds - * @returns + * @param dashboard + * @param metricIds + * @returns */ addWidget(dashboard: Dashboard, metricIds: any): Promise { const data = dashboard.toJson() @@ -80,7 +70,7 @@ export default class DashboardService { /** * Delete a dashboard. - * @param dashboardId + * @param dashboardId * @returns {Promise} */ deleteDashboard(dashboardId: string): Promise { @@ -89,7 +79,7 @@ export default class DashboardService { /** - * Create a new Meitrc, if the dashboardId is not provided, + * Create a new Meitrc, if the dashboardId is not provided, * it will add the metric to the dashboard. * @param metric Required * @param dashboardId Optional