diff --git a/frontend/app/IFrameRoutes.tsx b/frontend/app/IFrameRoutes.tsx index 0973ccd59..5ba0b0b09 100644 --- a/frontend/app/IFrameRoutes.tsx +++ b/frontend/app/IFrameRoutes.tsx @@ -10,6 +10,8 @@ import NotFoundPage from 'Shared/NotFoundPage'; import { ModalProvider } from 'Components/Modal'; import Layout from 'App/layout/Layout'; import PublicRoutes from 'App/PublicRoutes'; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; const components: any = { SessionPure: lazy(() => import('Components/Session/Session')), @@ -41,8 +43,11 @@ interface Props { } function IFrameRoutes(props: Props) { - const { isJwt = false, isLoggedIn = false, loading, onboarding, sites, siteId, jwt } = props; - const siteIdList: any = sites.map(({ id }) => id).toJS(); + const { projectsStore } = useStore(); + const sites = projectsStore.list; + const siteId = projectsStore.siteId; + const { isJwt = false, isLoggedIn = false, loading, onboarding, jwt } = props; + const siteIdList: any = sites.map(({ id }) => id); if (isLoggedIn) { return ( @@ -75,11 +80,9 @@ function IFrameRoutes(props: Props) { export default connect((state: any) => ({ changePassword: state.getIn(['user', 'account', 'changePassword']), onboarding: state.getIn(['user', 'onboarding']), - sites: state.getIn(['site', 'list']), - siteId: state.getIn(['site', 'siteId']), jwt: state.getIn(['user', 'jwt']), tenantId: state.getIn(['user', 'account', 'tenantId']), isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee' || state.getIn(['user', 'authDetails', 'edition']) === 'ee' -}))(IFrameRoutes); \ No newline at end of file +}))(observer(IFrameRoutes)); 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..5020b1cf8 100644 --- a/frontend/app/Router.tsx +++ b/frontend/app/Router.tsx @@ -13,56 +13,53 @@ 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, sessionStore } = mstore; + + const setSessionPath = sessionStore.setSessionPath; + 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 +77,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 +105,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 +174,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 +226,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 +233,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']), @@ -265,13 +254,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 2a9394687..2ea7f80d9 100644 --- a/frontend/app/api_client.ts +++ b/frontend/app/api_client.ts @@ -54,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 = store.getState().getIn(['site', 'siteId']); this.init = { headers: new Headers({ Accept: 'application/json', @@ -69,7 +69,10 @@ export default class APIClient { if (jwt !== null) { (this.init.headers as Headers).set('Authorization', `Bearer ${jwt}`); } - this.siteId = siteId; + } + + setSiteIdCheck(checker: () => { siteId: string | null }): void { + this.siteIdCheck = checker } private getInit(method: string = 'GET', params?: any, reqHeaders?: Record): RequestInit { @@ -101,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/Assist/components/SessionList/SessionList.tsx b/frontend/app/components/Assist/components/SessionList/SessionList.tsx index 6fd78da60..2bf5d2ecc 100644 --- a/frontend/app/components/Assist/components/SessionList/SessionList.tsx +++ b/frontend/app/components/Assist/components/SessionList/SessionList.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from 'react'; -import { connect } from 'react-redux'; -import { fetchLiveList } from 'Duck/sessions'; +import { observer } from 'mobx-react-lite' +import { useStore } from 'App/mstore'; import { Loader, NoContent, Label } from 'UI'; import SessionItem from 'Shared/SessionItem'; import { useModal } from 'App/components/Modal'; @@ -11,16 +11,20 @@ interface Props { list: any; session: any; userId: any; - fetchLiveList: (params: any) => void; } function SessionList(props: Props) { const { hideModal } = useModal(); + const { sessionStore } = useStore(); + const fetchLiveList = sessionStore.fetchLiveSessions; + const session = sessionStore.current; + const list = sessionStore.liveSessions.filter((i: any) => i.userId === session.userId && i.sessionId !== session.sessionId); + const loading = sessionStore.loadingLiveSessions; useEffect(() => { const params: any = {}; if (props.session.userId) { params.userId = props.session.userId; } - props.fetchLiveList(params); + void fetchLiveList(params); }, []); return ( @@ -33,9 +37,9 @@ function SessionList(props: Props) { {props.userId}'s Live Sessions{' '} - + @@ -45,7 +49,7 @@ function SessionList(props: Props) { } >
- {props.list.map((session: any) => ( + {list.map((session: any) => (
{session.pageTitle && session.pageTitle !== '' && (
@@ -65,14 +69,4 @@ function SessionList(props: Props) { ); } -export default connect( - (state: any) => { - const session = state.getIn(['sessions', 'current']); - return { - session, - list: state.getIn(['sessions', 'liveSessions']).filter((i: any) => i.userId === session.userId && i.sessionId !== session.sessionId), - loading: state.getIn(['sessions', 'fetchLiveListRequest', 'loading']), - }; - }, - { fetchLiveList } -)(SessionList); +export default observer(SessionList); 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/AssistDoc/AssistDoc.js b/frontend/app/components/Client/Integrations/AssistDoc/AssistDoc.js index dd94f90e6..1165af6ec 100644 --- a/frontend/app/components/Client/Integrations/AssistDoc/AssistDoc.js +++ b/frontend/app/components/Client/Integrations/AssistDoc/AssistDoc.js @@ -1,10 +1,11 @@ +import { useStore } from "App/mstore"; import React from 'react'; import DocLink from 'Shared/DocLink/DocLink'; import AssistScript from './AssistScript'; import AssistNpm from './AssistNpm'; import { Tabs, CodeBlock } from 'UI'; import { useState } from 'react'; -import { connect } from 'react-redux'; +import { observer } from 'mobx-react-lite' const NPM = 'NPM'; const SCRIPT = 'SCRIPT'; @@ -13,8 +14,11 @@ const TABS = [ { key: NPM, text: NPM }, ]; -const AssistDoc = (props) => { - const { projectKey } = props; +const AssistDoc = () => { + const { integrationsStore, projectsStore } = useStore(); + const sites = projectsStore.list; + const siteId = integrationsStore.integrations.siteId + const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey const [activeTab, setActiveTab] = useState(SCRIPT); const renderActiveTab = () => { @@ -53,10 +57,4 @@ const AssistDoc = (props) => { AssistDoc.displayName = 'AssistDoc'; -export default connect((state) => { - const siteId = state.getIn(['integrations', 'siteId']); - const sites = state.getIn(['site', 'list']); - return { - projectKey: sites.find((site) => site.get('id') === siteId).get('projectKey'), - }; -})(AssistDoc); +export default observer(AssistDoc); diff --git a/frontend/app/components/Client/Integrations/BugsnagForm/ProjectListDropdown.js b/frontend/app/components/Client/Integrations/BugsnagForm/ProjectListDropdown.js index c30b57953..8fb9d5df2 100644 --- a/frontend/app/components/Client/Integrations/BugsnagForm/ProjectListDropdown.js +++ b/frontend/app/components/Client/Integrations/BugsnagForm/ProjectListDropdown.js @@ -1,17 +1,16 @@ import React from 'react'; import { connect } from 'react-redux'; import { tokenRE } from 'Types/integrations/bugsnagConfig'; -import { edit } from 'Duck/integrations/actions'; import Select from 'Shared/Select'; import { withRequest } from 'HOCs'; @connect(state => ({ token: state.getIn([ 'bugsnag', 'instance', 'authorizationToken' ]) -}), { edit }) +})) @withRequest({ dataName: "projects", initialData: [], - dataWrapper: (data = [], prevData) => { + dataWrapper: (data = []) => { if (!Array.isArray(data)) throw new Error('Wrong responce format.'); const withOrgName = data.length > 1; return data.reduce((accum, { name: orgName, projects }) => { @@ -35,15 +34,7 @@ export default class ProjectListDropdown extends React.PureComponent { if (!tokenRE.test(token)) return; this.props.fetchProjectList({ authorizationToken: token, - }).then(() => { - const { value, projects } = this.props; - const values = projects.map(p => p.id); - if (!values.includes(value) && values.length > 0) { - this.props.edit("bugsnag", { - projectId: values[0], - }); - } - }); + }) } componentDidUpdate(prevProps) { if (prevProps.token !== this.props.token) { diff --git a/frontend/app/components/Client/Integrations/CloudwatchForm/CloudwatchForm.js b/frontend/app/components/Client/Integrations/CloudwatchForm/CloudwatchForm.js index ca4e6ae3b..003545e23 100644 --- a/frontend/app/components/Client/Integrations/CloudwatchForm/CloudwatchForm.js +++ b/frontend/app/components/Client/Integrations/CloudwatchForm/CloudwatchForm.js @@ -1,41 +1,53 @@ +import { + ACCESS_KEY_ID_LENGTH, + SECRET_ACCESS_KEY_LENGTH, +} from 'Types/integrations/cloudwatchConfig'; import React from 'react'; -import { ACCESS_KEY_ID_LENGTH, SECRET_ACCESS_KEY_LENGTH } from 'Types/integrations/cloudwatchConfig'; + +import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModalCard'; + +import DocLink from 'Shared/DocLink/DocLink'; + import IntegrationForm from '../IntegrationForm'; import LogGroupDropdown from './LogGroupDropdown'; import RegionDropdown from './RegionDropdown'; -import DocLink from 'Shared/DocLink/DocLink'; -import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModalCard'; const CloudwatchForm = (props) => ( -
- -
-
How it works?
+
+ +
+
How it works?
  1. Create a Service Account
  2. Enter the details below
  3. Propagate openReplaySessionToken
- +
( checkIfDisplayed: (config) => config.awsSecretAccessKey.length === SECRET_ACCESS_KEY_LENGTH && config.region !== '' && - config.awsAccessKeyId.length === ACCESS_KEY_ID_LENGTH - } + config.awsAccessKeyId.length === ACCESS_KEY_ID_LENGTH, + }, ]} />
diff --git a/frontend/app/components/Client/Integrations/CloudwatchForm/LogGroupDropdown.js b/frontend/app/components/Client/Integrations/CloudwatchForm/LogGroupDropdown.js index d1d306244..ce2c85e3a 100644 --- a/frontend/app/components/Client/Integrations/CloudwatchForm/LogGroupDropdown.js +++ b/frontend/app/components/Client/Integrations/CloudwatchForm/LogGroupDropdown.js @@ -1,77 +1,93 @@ -import React from 'react'; -import { connect } from 'react-redux'; +import React, { useState, useEffect, useCallback } from 'react'; import { ACCESS_KEY_ID_LENGTH, SECRET_ACCESS_KEY_LENGTH } from 'Types/integrations/cloudwatchConfig'; -import { edit } from 'Duck/integrations/actions'; +import { observer } from 'mobx-react-lite'; +import { useStore } from 'App/mstore'; import Select from 'Shared/Select'; -import { withRequest } from 'HOCs'; +import { integrationsService } from "App/services"; -@connect(state => ({ - config: state.getIn([ 'cloudwatch', 'instance' ]) -}), { edit }) -@withRequest({ - dataName: "values", - initialData: [], - resetBeforeRequest: true, - requestName: "fetchLogGroups", - endpoint: '/integrations/cloudwatch/list_groups', - method: 'POST', -}) -export default class LogGroupDropdown extends React.PureComponent { - constructor(props) { - super(props); - this.fetchLogGroups() - } - fetchLogGroups() { - const { config } = this.props; - if (config.region === "" || - config.awsSecretAccessKey.length !== SECRET_ACCESS_KEY_LENGTH || - config.awsAccessKeyId.length !== ACCESS_KEY_ID_LENGTH - ) return; - this.props.fetchLogGroups({ - region: config.region, - awsSecretAccessKey: config.awsSecretAccessKey, - awsAccessKeyId: config.awsAccessKeyId, - }).then(() => { - const { value, values, name } = this.props; - if (!values.includes(value) && values.length > 0) { - this.props.edit("cloudwatch", { - [ name ]: values[0], - }); - } - }); - } - componentDidUpdate(prevProps) { - const { config } = this.props; - if (prevProps.config.region !== config.region || - prevProps.config.awsSecretAccessKey !== config.awsSecretAccessKey || - prevProps.config.awsAccessKeyId !== config.awsAccessKeyId) { - this.fetchLogGroups(); +const LogGroupDropdown = (props) => { + const { integrationsStore } = useStore(); + const config = integrationsStore.cloudwatch.instance; + const edit = integrationsStore.cloudwatch.edit; + const { + value, + name, + placeholder, + onChange, + } = props; + + const [values, setValues] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(false); + + const { region, awsSecretAccessKey, awsAccessKeyId } = config; + + const fetchLogGroups = useCallback(() => { + if ( + region === '' || + awsSecretAccessKey.length !== SECRET_ACCESS_KEY_LENGTH || + awsAccessKeyId.length !== ACCESS_KEY_ID_LENGTH + ) { + return; } - } - onChange = (target) => { - if (typeof this.props.onChange === 'function') { - this.props.onChange({ target }); + + setLoading(true); + setError(false); + setValues([]); // Reset values before request + + const params = { + region: region, + awsSecretAccessKey: awsSecretAccessKey, + awsAccessKeyId: awsAccessKeyId, + }; + + integrationsService.client + .post('/integrations/cloudwatch/list_groups', params) + .then((response) => response.json()) + .then(({ errors, data }) => { + if (errors) { + setError(true); + setLoading(false); + return; + } + setValues(data); + setLoading(false); + + // If current value is not in the new values list, update it + if (!data.includes(value) && data.length > 0) { + edit({ + [name]: data[0], + }); + } + }) + .catch(() => { + setError(true); + setLoading(false); + }); + }, [region, awsSecretAccessKey, awsAccessKeyId, value, name, edit]); + + // Fetch log groups on mount and when config changes + useEffect(() => { + fetchLogGroups(); + }, [fetchLogGroups]); + + const handleChange = (target) => { + if (typeof onChange === 'function') { + onChange({ target }); } - } - render() { - const { - values, - name, - value, - placeholder, - loading, - } = this.props; - const options = values.map(g => ({ text: g, value: g })); - return ( - o.value === value)} + placeholder={placeholder} + onChange={handleChange} + loading={loading} + /> + ); +}; + +export default observer(LogGroupDropdown); diff --git a/frontend/app/components/Client/Integrations/ElasticsearchForm.js b/frontend/app/components/Client/Integrations/ElasticsearchForm.js index 2c30cea47..8c5c3d7f6 100644 --- a/frontend/app/components/Client/Integrations/ElasticsearchForm.js +++ b/frontend/app/components/Client/Integrations/ElasticsearchForm.js @@ -1,97 +1,64 @@ import React from 'react'; -import { connect } from 'react-redux'; -import IntegrationForm from './IntegrationForm'; -import { withRequest } from 'HOCs'; -import { edit } from 'Duck/integrations/actions'; -import DocLink from 'Shared/DocLink/DocLink'; import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModalCard'; -@connect( - (state) => ({ - config: state.getIn(['elasticsearch', 'instance']) - }), - { edit } -) -@withRequest({ - dataName: 'isValid', - initialData: false, - dataWrapper: (data) => data.state, - requestName: 'validateConfig', - endpoint: '/integrations/elasticsearch/test', - method: 'POST' -}) -export default class ElasticsearchForm extends React.PureComponent { - componentWillReceiveProps(newProps) { - const { - config: { host, port, apiKeyId, apiKey } - } = this.props; - const { loading, config } = newProps; - const valuesChanged = host !== config.host || port !== config.port || apiKeyId !== config.apiKeyId || apiKey !== config.apiKey; - if (!loading && valuesChanged && newProps.config.validateKeys() && newProps) { - this.validateConfig(newProps); - } - } +import DocLink from 'Shared/DocLink/DocLink'; - validateConfig = (newProps) => { - const { config } = newProps; - this.props - .validateConfig({ - host: config.host, - port: config.port, - apiKeyId: config.apiKeyId, - apiKey: config.apiKey - }) - .then((res) => { - const { isValid } = this.props; - this.props.edit('elasticsearch', { isValid: isValid }); - }); - }; +import IntegrationForm from './IntegrationForm'; - render() { - const props = this.props; - return ( -
- +const ElasticsearchForm = (props) => { + return ( +
+ -
-
How it works?
-
    -
  1. Create a new Elastic API key
  2. -
  3. Enter the API key below
  4. -
  5. Propagate openReplaySessionToken
  6. -
- -
- +
How it works?
+
    +
  1. Create a new Elastic API key
  2. +
  3. Enter the API key below
  4. +
  5. Propagate openReplaySessionToken
  6. +
+
- ); - } -} + +
+ ); +}; + +export default ElasticsearchForm; diff --git a/frontend/app/components/Client/Integrations/GraphQLDoc/GraphQLDoc.js b/frontend/app/components/Client/Integrations/GraphQLDoc/GraphQLDoc.js index 18c78d368..2b7bb916a 100644 --- a/frontend/app/components/Client/Integrations/GraphQLDoc/GraphQLDoc.js +++ b/frontend/app/components/Client/Integrations/GraphQLDoc/GraphQLDoc.js @@ -1,11 +1,15 @@ +import { useStore } from "App/mstore"; import React from 'react'; import { CodeBlock } from "UI"; import DocLink from 'Shared/DocLink/DocLink'; import ToggleContent from 'Shared/ToggleContent'; -import { connect } from 'react-redux'; +import { observer } from 'mobx-react-lite' -const GraphQLDoc = (props) => { - const { projectKey } = props; +const GraphQLDoc = () => { + const { integrationsStore, projectsStore } = useStore(); + const sites = projectsStore.list; + const siteId = integrationsStore.integrations.siteId + const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey const usage = `import OpenReplay from '@openreplay/tracker'; import trackerGraphQL from '@openreplay/tracker-graphql'; //... @@ -70,10 +74,4 @@ export const recordGraphQL = tracker.use(trackerGraphQL());` GraphQLDoc.displayName = 'GraphQLDoc'; -export default connect((state) => { - const siteId = state.getIn(['integrations', 'siteId']); - const sites = state.getIn(['site', 'list']); - return { - projectKey: sites.find((site) => site.get('id') === siteId).get('projectKey'), - }; -})(GraphQLDoc); +export default observer(GraphQLDoc); diff --git a/frontend/app/components/Client/Integrations/IntegrationForm.js b/frontend/app/components/Client/Integrations/IntegrationForm.js deleted file mode 100644 index c4d634562..000000000 --- a/frontend/app/components/Client/Integrations/IntegrationForm.js +++ /dev/null @@ -1,142 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { Input, Form, Button, Checkbox, Loader } from 'UI'; -import { save, init, edit, remove } from 'Duck/integrations/actions'; -import { fetchIntegrationList } from 'Duck/integrations/integrations'; - -@connect( - (state, { name, customPath }) => ({ - sites: state.getIn(['site', 'list']), - initialSiteId: state.getIn(['site', 'siteId']), - list: state.getIn([name, 'list']), - config: state.getIn([name, 'instance']), - loading: state.getIn([name, 'fetchRequest', 'loading']), - saving: state.getIn([customPath || name, 'saveRequest', 'loading']), - removing: state.getIn([name, 'removeRequest', 'loading']), - siteId: state.getIn(['integrations', 'siteId']), - }), - { - save, - init, - edit, - remove, - // fetchList, - fetchIntegrationList, - } -) -export default class IntegrationForm extends React.PureComponent { - constructor(props) { - super(props); - } - - fetchList = () => { - const { siteId, initialSiteId } = this.props; - if (!siteId) { - this.props.fetchIntegrationList(initialSiteId); - } else { - this.props.fetchIntegrationList(siteId); - } - } - - write = ({ target: { value, name: key, type, checked } }) => { - if (type === 'checkbox') this.props.edit(this.props.name, { [key]: checked }); - else this.props.edit(this.props.name, { [key]: value }); - }; - - // onChangeSelect = ({ value }) => { - // const { sites, list, name } = this.props; - // const site = sites.find((s) => s.id === value.value); - // this.setState({ currentSiteId: site.id }); - // this.init(value.value); - // }; - - // init = (siteId) => { - // const { list, name } = this.props; - // const config = parseInt(siteId) > 0 ? list.find((s) => s.projectId === siteId) : undefined; - // this.props.init(name, config ? config : list.first()); - // }; - - save = () => { - const { config, name, customPath, ignoreProject } = this.props; - const isExists = config.exists(); - // const { currentSiteId } = this.state; - this.props.save(customPath || name, !ignoreProject ? this.props.siteId : null, config).then(() => { - // this.props.fetchList(name); - this.fetchList(); - this.props.onClose(); - if (isExists) return; - }); - }; - - remove = () => { - const { name, config, ignoreProject } = this.props; - this.props.remove(name, !ignoreProject ? config.projectId : null).then(() => { - this.props.onClose(); - this.fetchList(); - }); - }; - - render() { - const { config, saving, removing, formFields, name, loading, integrated } = this.props; - return ( - -
-
- {formFields.map( - ({ - key, - label, - placeholder = label, - component: Component = 'input', - type = 'text', - checkIfDisplayed, - autoFocus = false, - }) => - (typeof checkIfDisplayed !== 'function' || checkIfDisplayed(config)) && - (type === 'checkbox' ? ( - - - - ) : ( - - - - - )) - )} - - - - {integrated && ( - - )} -
-
-
- ); - } -} diff --git a/frontend/app/components/Client/Integrations/IntegrationForm.tsx b/frontend/app/components/Client/Integrations/IntegrationForm.tsx new file mode 100644 index 000000000..a290e91f1 --- /dev/null +++ b/frontend/app/components/Client/Integrations/IntegrationForm.tsx @@ -0,0 +1,109 @@ +import { observer } from 'mobx-react-lite'; +import React from 'react'; +import { connect } from 'react-redux'; + +import { useStore } from 'App/mstore'; +import { namedStore } from 'App/mstore/integrationsStore'; +import { Button, Checkbox, Form, Input, Loader } from 'UI'; + +function IntegrationForm(props: any) { + const { formFields, name, integrated } = props; + 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; + const onSave = integrationStore.saveIntegration; + const onRemove = integrationStore.deleteIntegration; + const edit = integrationStore.edit; + const fetchIntegrationList = integrationsStore.integrations.fetchIntegrations; + + const fetchList = () => { + void fetchIntegrationList(initialSiteId); + }; + + const write = ({ target: { value, name: key, type, checked } }) => { + if (type === 'checkbox') edit({ [key]: checked }); + else edit({ [key]: value }); + }; + + const save = () => { + const { name, customPath } = props; + onSave(customPath || name).then(() => { + fetchList(); + props.onClose(); + }); + }; + + const remove = () => { + onRemove().then(() => { + props.onClose(); + fetchList(); + }); + }; + + return ( + +
+
+ {formFields.map( + ({ + key, + label, + placeholder = label, + component: Component = 'input', + type = 'text', + checkIfDisplayed, + autoFocus = false, + }) => + (typeof checkIfDisplayed !== 'function' || + checkIfDisplayed(config)) && + (type === 'checkbox' ? ( + + + + ) : ( + + + + + )) + )} + + + + {integrated && ( + + )} +
+
+
+ ); +} + +export default observer(IntegrationForm); diff --git a/frontend/app/components/Client/Integrations/Integrations.tsx b/frontend/app/components/Client/Integrations/Integrations.tsx index e81a7038a..2509e883b 100644 --- a/frontend/app/components/Client/Integrations/Integrations.tsx +++ b/frontend/app/components/Client/Integrations/Integrations.tsx @@ -1,88 +1,95 @@ +import withPageTitle from 'HOCs/withPageTitle'; +import cn from 'classnames'; +import { observer } from 'mobx-react-lite'; import React, { useEffect, useState } from 'react'; import { connect } from 'react-redux'; + import { useModal } from 'App/components/Modal'; -import cn from 'classnames'; +import { useStore } from 'App/mstore'; +import IntegrationFilters from 'Components/Client/Integrations/IntegrationFilters'; +import { PageTitle } from 'UI'; -import { fetch, init } from 'Duck/integrations/actions'; -import { fetchIntegrationList, setSiteId } from 'Duck/integrations/integrations'; -import SiteDropdown from 'Shared/SiteDropdown'; -import ReduxDoc from './ReduxDoc'; -import VueDoc from './VueDoc'; -import GraphQLDoc from './GraphQLDoc'; -import NgRxDoc from './NgRxDoc'; -import MobxDoc from './MobxDoc'; -import ProfilerDoc from './ProfilerDoc'; -import AssistDoc from './AssistDoc'; -import PiniaDoc from './PiniaDoc'; -import ZustandDoc from './ZustandDoc'; -import MSTeams from './Teams'; import DocCard from 'Shared/DocCard/DocCard'; -import { PageTitle, Tooltip } from 'UI'; -import withPageTitle from 'HOCs/withPageTitle'; +import AssistDoc from './AssistDoc'; import BugsnagForm from './BugsnagForm'; import CloudwatchForm from './CloudwatchForm'; import DatadogForm from './DatadogForm'; import ElasticsearchForm from './ElasticsearchForm'; import GithubForm from './GithubForm'; +import GraphQLDoc from './GraphQLDoc'; import IntegrationItem from './IntegrationItem'; import JiraForm from './JiraForm'; +import MobxDoc from './MobxDoc'; import NewrelicForm from './NewrelicForm'; +import NgRxDoc from './NgRxDoc'; +import PiniaDoc from './PiniaDoc'; +import ProfilerDoc from './ProfilerDoc'; +import ReduxDoc from './ReduxDoc'; import RollbarForm from './RollbarForm'; import SentryForm from './SentryForm'; import SlackForm from './SlackForm'; import StackdriverForm from './StackdriverForm'; import SumoLogicForm from './SumoLogicForm'; -import IntegrationFilters from 'Components/Client/Integrations/IntegrationFilters'; +import MSTeams from './Teams'; +import VueDoc from './VueDoc'; +import ZustandDoc from './ZustandDoc'; interface Props { - fetch: (name: string, siteId: string) => void; - init: () => void; - fetchIntegrationList: (siteId: any) => void; - integratedList: any; - initialSiteId: string; - setSiteId: (siteId: string) => void; siteId: string; hideHeader?: boolean; - loading?: boolean; } function Integrations(props: Props) { - const { initialSiteId, hideHeader = false, loading = false } = props; + const { integrationsStore, projectsStore } = useStore(); + const siteId = projectsStore.siteId; + const fetchIntegrationList = integrationsStore.integrations.fetchIntegrations; + const storeIntegratedList = integrationsStore.integrations.list; + const { hideHeader = false } = props; const { showModal } = useModal(); const [integratedList, setIntegratedList] = useState([]); const [activeFilter, setActiveFilter] = useState('all'); useEffect(() => { - const list = props.integratedList + const list = storeIntegratedList .filter((item: any) => item.integrated) .map((item: any) => item.name); setIntegratedList(list); - }, [props.integratedList]); + }, [storeIntegratedList]); useEffect(() => { - props.fetchIntegrationList(initialSiteId); - props.setSiteId(initialSiteId); - }, []); + void fetchIntegrationList(siteId); + }, [siteId]); const onClick = (integration: any, width: number) => { - if (integration.slug && integration.slug !== 'slack' && integration.slug !== 'msteams') { - props.fetch(integration.slug, props.siteId); + if ( + integration.slug && + integration.slug !== 'slack' && + integration.slug !== 'msteams' + ) { + const intName = integration.slug as + | 'sentry' + | 'bugsnag' + | 'rollbar' + | 'elasticsearch' + | 'datadog' + | 'sumologic' + | 'stackdriver' + | 'cloudwatch' + | 'newrelic'; + if (integrationsStore[intName]) { + void integrationsStore[intName].fetchIntegration(siteId); + } } showModal( React.cloneElement(integration.component, { - integrated: integratedList.includes(integration.slug) + integrated: integratedList.includes(integration.slug), }), { right: true, width } ); }; - const onChangeSelect = ({ value }: any) => { - props.setSiteId(value.value); - props.fetchIntegrationList(value.value); - }; - const onChange = (key: string) => { setActiveFilter(key); }; @@ -99,83 +106,92 @@ function Integrations(props: Props) { key: cat.key, title: cat.title, label: cat.title, - icon: cat.icon - })) - - - const allIntegrations = filteredIntegrations.flatMap(cat => cat.integrations); + icon: cat.icon, + })); + const allIntegrations = filteredIntegrations.flatMap( + (cat) => cat.integrations + ); + console.log( + allIntegrations, + integratedList + ) return ( <> -
+
{!hideHeader && Integrations
} />} - +
-
+
-
+`)} + > {allIntegrations.map((integration: any) => ( - onClick(integration, filteredIntegrations.find(cat => cat.integrations.includes(integration)).title === 'Plugins' ? 500 : 350) + onClick( + integration, + filteredIntegrations.find((cat) => + cat.integrations.includes(integration) + ).title === 'Plugins' + ? 500 + : 350 + ) } hide={ (integration.slug === 'github' && integratedList.includes('jira')) || - (integration.slug === 'jira' && - integratedList.includes('github')) + (integration.slug === 'jira' && integratedList.includes('github')) } /> ))}
- ); } -export default connect( - (state: any) => ({ - initialSiteId: state.getIn(['site', 'siteId']), - integratedList: state.getIn(['integrations', 'list']) || [], - loading: state.getIn(['integrations', 'fetchRequest', 'loading']), - siteId: state.getIn(['integrations', 'siteId']) - }), - { fetch, init, fetchIntegrationList, setSiteId } -)(withPageTitle('Integrations - OpenReplay Preferences')(Integrations)); - +export default withPageTitle('Integrations - OpenReplay Preferences')(observer(Integrations)) const integrations = [ { title: 'Issue Reporting', key: 'issue-reporting', - description: 'Seamlessly report issues or share issues with your team right from OpenReplay.', + description: + 'Seamlessly report issues or share issues with your team right from OpenReplay.', isProject: false, icon: 'exclamation-triangle', integrations: [ { title: 'Jira', - subtitle: 'Integrate Jira with OpenReplay to enable the creation of a new ticket directly from a session.', + subtitle: + 'Integrate Jira with OpenReplay to enable the creation of a new ticket directly from a session.', slug: 'jira', category: 'Errors', icon: 'integrations/jira', - component: + component: , }, { title: 'Github', - subtitle: 'Integrate GitHub with OpenReplay to enable the direct creation of a new issue from a session.', + subtitle: + 'Integrate GitHub with OpenReplay to enable the direct creation of a new issue from a session.', slug: 'github', category: 'Errors', icon: 'integrations/github', - component: - } - ] + component: , + }, + ], }, { title: 'Backend Logging', @@ -186,106 +202,119 @@ const integrations = [ 'Sync your backend errors with sessions replays and see what happened front-to-back.', docs: () => ( - Sync your backend errors with sessions replays and see what happened front-to-back. + Sync your backend errors with sessions replays and see what happened + front-to-back. ), integrations: [ { title: 'Sentry', - subtitle: 'Integrate Sentry with session replays to seamlessly observe backend errors.', + subtitle: + 'Integrate Sentry with session replays to seamlessly observe backend errors.', slug: 'sentry', icon: 'integrations/sentry', - component: + component: , }, { title: 'Bugsnag', - subtitle: 'Integrate Bugsnag to access the OpenReplay session linked to the JS exception within its interface.', + subtitle: + 'Integrate Bugsnag to access the OpenReplay session linked to the JS exception within its interface.', slug: 'bugsnag', icon: 'integrations/bugsnag', - component: + component: , }, { title: 'Rollbar', - subtitle: 'Integrate Rollbar with session replays to seamlessly observe backend errors.', + subtitle: + 'Integrate Rollbar with session replays to seamlessly observe backend errors.', slug: 'rollbar', icon: 'integrations/rollbar', - component: + component: , }, { title: 'Elasticsearch', - subtitle: 'Integrate Elasticsearch with session replays to seamlessly observe backend errors.', + subtitle: + 'Integrate Elasticsearch with session replays to seamlessly observe backend errors.', slug: 'elasticsearch', icon: 'integrations/elasticsearch', - component: + component: , }, { title: 'Datadog', - subtitle: 'Incorporate DataDog to visualize backend errors alongside session replay, for easy troubleshooting.', + subtitle: + 'Incorporate DataDog to visualize backend errors alongside session replay, for easy troubleshooting.', slug: 'datadog', icon: 'integrations/datadog', - component: + component: , }, { title: 'Sumo Logic', - subtitle: 'Integrate Sumo Logic with session replays to seamlessly observe backend errors.', + subtitle: + 'Integrate Sumo Logic with session replays to seamlessly observe backend errors.', slug: 'sumologic', icon: 'integrations/sumologic', - component: + component: , }, { title: 'Google Cloud', - subtitle: 'Integrate Google Cloud to view backend logs and errors in conjunction with session replay', + subtitle: + 'Integrate Google Cloud to view backend logs and errors in conjunction with session replay', slug: 'stackdriver', icon: 'integrations/google-cloud', - component: + component: , }, { title: 'CloudWatch', - subtitle: 'Integrate CloudWatch to see backend logs and errors alongside session replay.', + subtitle: + 'Integrate CloudWatch to see backend logs and errors alongside session replay.', slug: 'cloudwatch', icon: 'integrations/aws', - component: + component: , }, { title: 'Newrelic', - subtitle: 'Integrate NewRelic with session replays to seamlessly observe backend errors.', + subtitle: + 'Integrate NewRelic with session replays to seamlessly observe backend errors.', slug: 'newrelic', icon: 'integrations/newrelic', - component: - } - ] + component: , + }, + ], }, { title: 'Collaboration', key: 'collaboration', isProject: false, icon: 'file-code', - description: 'Share your sessions with your team and collaborate on issues.', + description: + 'Share your sessions with your team and collaborate on issues.', integrations: [ { title: 'Slack', - subtitle: 'Integrate Slack to empower every user in your org with the ability to send sessions to any Slack channel.', + subtitle: + 'Integrate Slack to empower every user in your org with the ability to send sessions to any Slack channel.', slug: 'slack', category: 'Errors', icon: 'integrations/slack', component: , - shared: true + shared: true, }, { title: 'MS Teams', - subtitle: 'Integrate MS Teams to empower every user in your org with the ability to send sessions to any MS Teams channel.', + subtitle: + 'Integrate MS Teams to empower every user in your org with the ability to send sessions to any MS Teams channel.', slug: 'msteams', category: 'Errors', icon: 'integrations/teams', component: , - shared: true - } - ] + shared: true, + }, + ], }, // { // title: 'State Management', @@ -302,72 +331,82 @@ const integrations = [ icon: 'chat-left-text', docs: () => ( - Plugins capture your application’s store, monitor queries, track performance issues and even - assist your end user through live sessions. + Plugins capture your application’s store, monitor queries, track + performance issues and even assist your end user through live sessions. ), description: - 'Reproduce issues as if they happened in your own browser. Plugins help capture your application\'s store, HTTP requeets, GraphQL queries, and more.', + "Reproduce issues as if they happened in your own browser. Plugins help capture your application's store, HTTP requeets, GraphQL queries, and more.", integrations: [ { title: 'Redux', - subtitle: 'Capture Redux actions/state and inspect them later on while replaying session recordings.', - icon: 'integrations/redux', component: + subtitle: + 'Capture Redux actions/state and inspect them later on while replaying session recordings.', + icon: 'integrations/redux', + component: , }, { title: 'VueX', - subtitle: 'Capture VueX mutations/state and inspect them later on while replaying session recordings.', + subtitle: + 'Capture VueX mutations/state and inspect them later on while replaying session recordings.', icon: 'integrations/vuejs', - component: + component: , }, { title: 'Pinia', - subtitle: 'Capture Pinia mutations/state and inspect them later on while replaying session recordings.', + subtitle: + 'Capture Pinia mutations/state and inspect them later on while replaying session recordings.', icon: 'integrations/pinia', - component: + component: , }, { title: 'GraphQL', - subtitle: 'Capture GraphQL requests and inspect them later on while replaying session recordings. This plugin is compatible with Apollo and Relay implementations.', + subtitle: + 'Capture GraphQL requests and inspect them later on while replaying session recordings. This plugin is compatible with Apollo and Relay implementations.', icon: 'integrations/graphql', - component: + component: , }, { title: 'NgRx', - subtitle: 'Capture NgRx actions/state and inspect them later on while replaying session recordings.\n', + subtitle: + 'Capture NgRx actions/state and inspect them later on while replaying session recordings.\n', icon: 'integrations/ngrx', - component: + component: , }, { title: 'MobX', - subtitle: 'Capture MobX mutations and inspect them later on while replaying session recordings.', + subtitle: + 'Capture MobX mutations and inspect them later on while replaying session recordings.', icon: 'integrations/mobx', - component: + component: , }, { title: 'Profiler', - subtitle: 'Plugin allows you to measure your JS functions performance and capture both arguments and result for each call.', + subtitle: + 'Plugin allows you to measure your JS functions performance and capture both arguments and result for each call.', icon: 'integrations/openreplay', - component: + component: , }, { title: 'Assist', - subtitle: 'OpenReplay Assist allows you to support your users by seeing their live screen and instantly hopping on call (WebRTC) with them without requiring any 3rd-party screen sharing software.\n', + subtitle: + 'OpenReplay Assist allows you to support your users by seeing their live screen and instantly hopping on call (WebRTC) with them without requiring any 3rd-party screen sharing software.\n', icon: 'integrations/openreplay', - component: + component: , }, { title: 'Zustand', - subtitle: 'Capture Zustand mutations/state and inspect them later on while replaying session recordings.', + subtitle: + 'Capture Zustand mutations/state and inspect them later on while replaying session recordings.', icon: 'integrations/zustand', // header: '🐻', - component: - } - ] - } + component: , + }, + ], + }, ]; diff --git a/frontend/app/components/Client/Integrations/MobxDoc/MobxDoc.js b/frontend/app/components/Client/Integrations/MobxDoc/MobxDoc.js index 65705b1e0..9c917f876 100644 --- a/frontend/app/components/Client/Integrations/MobxDoc/MobxDoc.js +++ b/frontend/app/components/Client/Integrations/MobxDoc/MobxDoc.js @@ -1,11 +1,15 @@ import React from 'react'; import ToggleContent from 'Shared/ToggleContent'; import DocLink from 'Shared/DocLink/DocLink'; -import { connect } from 'react-redux'; import { CodeBlock } from "UI"; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; -const MobxDoc = (props) => { - const { projectKey } = props; +const MobxDoc = () => { + const { integrationsStore, projectsStore } = useStore(); + const sites = projectsStore.list; + const siteId = integrationsStore.integrations.siteId + const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey const mobxUsage = `import OpenReplay from '@openreplay/tracker'; import trackerMobX from '@openreplay/tracker-mobx'; @@ -67,10 +71,4 @@ function SomeFunctionalComponent() { MobxDoc.displayName = 'MobxDoc'; -export default connect((state) => { - const siteId = state.getIn(['integrations', 'siteId']); - const sites = state.getIn(['site', 'list']); - return { - projectKey: sites.find((site) => site.get('id') === siteId).get('projectKey'), - }; -})(MobxDoc); +export default observer(MobxDoc) diff --git a/frontend/app/components/Client/Integrations/NgRxDoc/NgRxDoc.js b/frontend/app/components/Client/Integrations/NgRxDoc/NgRxDoc.js index 8097a4618..e4d199f5b 100644 --- a/frontend/app/components/Client/Integrations/NgRxDoc/NgRxDoc.js +++ b/frontend/app/components/Client/Integrations/NgRxDoc/NgRxDoc.js @@ -1,11 +1,15 @@ +import { useStore } from "App/mstore"; import React from 'react'; import { CodeBlock } from "UI"; import ToggleContent from 'Shared/ToggleContent'; import DocLink from 'Shared/DocLink/DocLink'; -import { connect } from 'react-redux'; +import { observer } from 'mobx-react-lite' -const NgRxDoc = (props) => { - const { projectKey } = props; +const NgRxDoc = () => { + const { integrationsStore, projectsStore } = useStore(); + const sites = projectsStore.list; + const siteId = integrationsStore.integrations.siteId + const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey const usage = `import { StoreModule } from '@ngrx/store'; import { reducers } from './reducers'; import OpenReplay from '@openreplay/tracker'; @@ -80,10 +84,4 @@ const metaReducers = [tracker.use(trackerNgRx())]; // check list of ava NgRxDoc.displayName = 'NgRxDoc'; -export default connect((state) => { - const siteId = state.getIn(['integrations', 'siteId']); - const sites = state.getIn(['site', 'list']); - return { - projectKey: sites.find((site) => site.get('id') === siteId).get('projectKey'), - }; -})(NgRxDoc); +export default observer(NgRxDoc); diff --git a/frontend/app/components/Client/Integrations/PiniaDoc/PiniaDoc.tsx b/frontend/app/components/Client/Integrations/PiniaDoc/PiniaDoc.tsx index a0a9c157e..71a9cbccb 100644 --- a/frontend/app/components/Client/Integrations/PiniaDoc/PiniaDoc.tsx +++ b/frontend/app/components/Client/Integrations/PiniaDoc/PiniaDoc.tsx @@ -1,11 +1,19 @@ +import { observer } from 'mobx-react-lite'; import React from 'react'; -import { CodeBlock } from "UI"; -import ToggleContent from '../../../shared/ToggleContent'; -import DocLink from 'Shared/DocLink/DocLink'; -import { connect } from 'react-redux'; -const PiniaDoc = (props) => { - const { projectKey } = props; +import { useStore } from 'App/mstore'; +import ToggleContent from 'Components/shared/ToggleContent'; +import { CodeBlock } from 'UI'; + +import DocLink from 'Shared/DocLink/DocLink'; + +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 + : sites[0]?.projectKey; const usage = `import Vuex from 'vuex' import OpenReplay from '@openreplay/tracker'; import trackerVuex from '@openreplay/tracker-vuex'; @@ -28,7 +36,7 @@ piniaStorePlugin(examplePiniaStore) // now you can use examplePiniaStore as // usual pinia store // (destructure values or return it as a whole etc) -` +`; const usageCjs = `import Vuex from 'vuex' import OpenReplay from '@openreplay/tracker/cjs'; import trackerVuex from '@openreplay/tracker-vuex/cjs'; @@ -55,34 +63,38 @@ piniaStorePlugin(examplePiniaStore) // now you can use examplePiniaStore as // usual pinia store // (destructure values or return it as a whole etc) -}` +}`; return ( -
+

VueX

- This plugin allows you to capture Pinia mutations + state and inspect them later on while - replaying session recordings. This is very useful for understanding and fixing issues. + This plugin allows you to capture Pinia mutations + state and inspect + them later on while replaying session recordings. This is very useful + for understanding and fixing issues.
Installation
- +
Usage

- Initialize the @openreplay/tracker package as usual and load the plugin into it. Then put - the generated plugin into your plugins field of your store. + Initialize the @openreplay/tracker package as usual and load the + plugin into it. Then put the generated plugin into your plugins field + of your store.

- } - second={ - - } + first={} + second={} /> { - const siteId = state.getIn(['integrations', 'siteId']); - const sites = state.getIn(['site', 'list']); - return { - projectKey: sites.find((site: any) => site.get('id') === siteId).get('projectKey'), - }; -})(PiniaDoc); +export default observer(PiniaDoc); diff --git a/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js b/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js index eb7ad3999..35abc1eb0 100644 --- a/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js +++ b/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js @@ -1,13 +1,16 @@ +import { useStore } from "App/mstore"; import React from 'react'; -import { connect } from 'react-redux'; - +import { observer } from 'mobx-react-lite'; import { CodeBlock } from 'UI'; import DocLink from 'Shared/DocLink/DocLink'; import ToggleContent from 'Shared/ToggleContent'; -const ProfilerDoc = (props) => { - const { projectKey } = props; +const ProfilerDoc = () => { + const { integrationsStore, projectsStore } = useStore(); + const sites = projectsStore.list; + const siteId = integrationsStore.integrations.siteId + const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey const usage = `import OpenReplay from '@openreplay/tracker'; import trackerProfiler from '@openreplay/tracker-profiler'; @@ -87,12 +90,4 @@ const fn = profiler('call_name')(() => { ProfilerDoc.displayName = 'ProfilerDoc'; -export default connect((state) => { - const siteId = state.getIn(['integrations', 'siteId']); - const sites = state.getIn(['site', 'list']); - return { - projectKey: sites - .find((site) => site.get('id') === siteId) - .get('projectKey'), - }; -})(ProfilerDoc); +export default observer(ProfilerDoc); diff --git a/frontend/app/components/Client/Integrations/ReduxDoc/ReduxDoc.js b/frontend/app/components/Client/Integrations/ReduxDoc/ReduxDoc.js index 3566bb82d..e782a2298 100644 --- a/frontend/app/components/Client/Integrations/ReduxDoc/ReduxDoc.js +++ b/frontend/app/components/Client/Integrations/ReduxDoc/ReduxDoc.js @@ -1,11 +1,15 @@ +import { useStore } from "App/mstore"; import React from 'react'; import { CodeBlock } from 'UI' -import ToggleContent from '../../../shared/ToggleContent'; +import ToggleContent from 'Components/shared/ToggleContent'; import DocLink from 'Shared/DocLink/DocLink'; -import { connect } from 'react-redux'; +import { observer } from 'mobx-react-lite' -const ReduxDoc = (props) => { - const { projectKey } = props; +const ReduxDoc = () => { + const { integrationsStore, projectsStore } = useStore(); + const sites = projectsStore.list; + const siteId = integrationsStore.integrations.siteId + const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey const usage = `import { applyMiddleware, createStore } from 'redux'; import OpenReplay from '@openreplay/tracker'; @@ -74,10 +78,4 @@ const store = createStore( ReduxDoc.displayName = 'ReduxDoc'; -export default connect((state) => { - const siteId = state.getIn(['integrations', 'siteId']); - const sites = state.getIn(['site', 'list']); - return { - projectKey: sites.find((site) => site.get('id') === siteId).get('projectKey'), - }; -})(ReduxDoc); +export default observer(ReduxDoc); diff --git a/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js b/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js index 26cfc9520..60edc363d 100644 --- a/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js +++ b/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js @@ -1,25 +1,36 @@ import React from 'react'; import { connect } from 'react-redux'; -import { edit, save, init, update } from 'Duck/integrations/slack'; import { Form, Input, Button, Message } from 'UI'; import { confirm } from 'UI'; -import { remove } from 'Duck/integrations/slack'; +import { observer } from 'mobx-react-lite' +import { useStore } from 'App/mstore' -class SlackAddForm extends React.PureComponent { - componentWillUnmount() { - this.props.init({}); - } +function SlackAddForm(props) { + const { onClose } = props; + const { integrationsStore } = useStore(); + const instance = integrationsStore.slack.instance; + const saving = integrationsStore.slack.loading; + const errors = integrationsStore.slack.errors; + const edit = integrationsStore.slack.edit; + const onSave = integrationsStore.slack.saveIntegration; + const update = integrationsStore.slack.update; + const init = integrationsStore.slack.init; + const onRemove = integrationsStore.slack.removeInt; + + React.useEffect(() => { + return () => init({}) + }, []) - save = () => { - const instance = this.props.instance; + + const save = () => { if (instance.exists()) { - this.props.update(this.props.instance); + void update(instance); } else { - this.props.save(this.props.instance); + void onSave(instance); } }; - remove = async (id) => { + const remove = async (id) => { if ( await confirm({ header: 'Confirm', @@ -27,79 +38,68 @@ class SlackAddForm extends React.PureComponent { confirmation: `Are you sure you want to permanently delete this channel?`, }) ) { - this.props.remove(id); + await onRemove(id); + onClose(); } }; - write = ({ target: { name, value } }) => this.props.edit({ [name]: value }); - - render() { - const { instance, saving, errors, onClose } = this.props; - return ( -
-
- - - - - - - - -
-
- - - -
- - -
-
- {errors && ( -
- {errors.map((error) => ( - - {error} - - ))} +
- )} -
- ); - } + + +
+ + + {errors && ( +
+ {errors.map((error) => ( + + {error} + + ))} +
+ )} +
+ ); } -export default connect( - (state) => ({ - instance: state.getIn(['slack', 'instance']), - saving: - state.getIn(['slack', 'saveRequest', 'loading']) || - state.getIn(['slack', 'updateRequest', 'loading']), - errors: state.getIn(['slack', 'saveRequest', 'errors']), - }), - { edit, save, init, remove, update } -)(SlackAddForm); +export default observer(SlackAddForm); diff --git a/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js b/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js index 8d25b4454..db53d3100 100644 --- a/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js +++ b/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js @@ -1,14 +1,16 @@ import React from 'react'; -import { connect } from 'react-redux'; import { NoContent } from 'UI'; -import { remove, edit, init } from 'Duck/integrations/slack'; import DocLink from 'Shared/DocLink/DocLink'; +import { observer } from 'mobx-react-lite' +import { useStore } from 'App/mstore' function SlackChannelList(props) { - const { list } = props; + const { integrationsStore } = useStore(); + const list = integrationsStore.slack.list; + const edit = integrationsStore.slack.edit; const onEdit = (instance) => { - props.edit(instance); + edit(instance.toData()); props.onEdit(); }; @@ -24,7 +26,7 @@ function SlackChannelList(props) {
} size="small" - show={list.size === 0} + show={list.length === 0} > {list.map((c) => (
({ - list: state.getIn(['slack', 'list']), - }), - { remove, edit, init } -)(SlackChannelList); +export default observer(SlackChannelList); diff --git a/frontend/app/components/Client/Integrations/SlackForm.tsx b/frontend/app/components/Client/Integrations/SlackForm.tsx index 018dbe885..43a720da4 100644 --- a/frontend/app/components/Client/Integrations/SlackForm.tsx +++ b/frontend/app/components/Client/Integrations/SlackForm.tsx @@ -1,17 +1,14 @@ import React, { useEffect } from 'react'; import SlackChannelList from './SlackChannelList/SlackChannelList'; -import { fetchList, init } from 'Duck/integrations/slack'; -import { connect } from 'react-redux'; import SlackAddForm from './SlackAddForm'; import { Button } from 'UI'; +import { observer } from 'mobx-react-lite' +import { useStore } from 'App/mstore' -interface Props { - onEdit?: (integration: any) => void; - istance: any; - fetchList: any; - init: any; -} -const SlackForm = (props: Props) => { +const SlackForm = () => { + const { integrationsStore } = useStore(); + const init = integrationsStore.slack.init; + const fetchList = integrationsStore.slack.fetchIntegrations; const [active, setActive] = React.useState(false); const onEdit = () => { @@ -20,11 +17,11 @@ const SlackForm = (props: Props) => { const onNew = () => { setActive(true); - props.init({}); + init({}); } useEffect(() => { - props.fetchList(); + void fetchList(); }, []); return ( @@ -47,9 +44,4 @@ const SlackForm = (props: Props) => { SlackForm.displayName = 'SlackForm'; -export default connect( - (state: any) => ({ - istance: state.getIn(['slack', 'instance']), - }), - { fetchList, init } -)(SlackForm); +export default observer(SlackForm); \ No newline at end of file diff --git a/frontend/app/components/Client/Integrations/Teams/TeamsAddForm.tsx b/frontend/app/components/Client/Integrations/Teams/TeamsAddForm.tsx index f13efc535..e45d6d7b1 100644 --- a/frontend/app/components/Client/Integrations/Teams/TeamsAddForm.tsx +++ b/frontend/app/components/Client/Integrations/Teams/TeamsAddForm.tsx @@ -1,36 +1,38 @@ +import { observer } from 'mobx-react-lite'; import React from 'react'; -import { connect } from 'react-redux'; -import { edit, save, init, update, remove } from 'Duck/integrations/teams'; -import { Form, Input, Button, Message } from 'UI'; + +import { useStore } from 'App/mstore'; +import { Button, Form, Input, Message } from 'UI'; import { confirm } from 'UI'; interface Props { - edit: (inst: any) => void; - save: (inst: any) => void; - init: (inst: any) => void; - update: (inst: any) => void; - remove: (id: string) => void; onClose: () => void; - instance: any; - saving: boolean; - errors: any; } -class TeamsAddForm extends React.PureComponent { - componentWillUnmount() { - this.props.init({}); - } +function TeamsAddForm({ onClose }: Props) { + const { integrationsStore } = useStore(); + const instance = integrationsStore.msteams.instance; + const saving = integrationsStore.msteams.loading; + const errors = integrationsStore.msteams.errors; + const edit = integrationsStore.msteams.edit; + const onSave = integrationsStore.msteams.saveIntegration; + const init = integrationsStore.msteams.init; + const onRemove = integrationsStore.msteams.removeInt; + const update = integrationsStore.msteams.update; - save = () => { - const instance = this.props.instance; - if (instance.exists()) { - this.props.update(this.props.instance); + React.useEffect(() => { + return () => init({}); + }, []); + + const save = () => { + if (instance?.exists()) { + void update(); } else { - this.props.save(this.props.instance); + void onSave(); } }; - remove = async (id: string) => { + const remove = async (id: string) => { if ( await confirm({ header: 'Confirm', @@ -38,80 +40,74 @@ class TeamsAddForm extends React.PureComponent { confirmation: `Are you sure you want to permanently delete this channel?`, }) ) { - this.props.remove(id); + void onRemove(id); } }; - write = ({ target: { name, value } }: { target: { name: string; value: string } }) => - this.props.edit({ [name]: value }); + const write = ({ + target: { name, value }, + }: { + target: { name: string; value: string }; + }) => edit({ [name]: value }); - render() { - const { instance, saving, errors, onClose } = this.props; - return ( -
-
- - - - - - - - -
-
- - - -
- - -
-
- {errors && ( -
- {errors.map((error: any) => ( - - {error} - - ))} +
- )} -
- ); - } + + +
+ + + {errors && ( +
+ {errors.map((error: any) => ( + + {error} + + ))} +
+ )} +
+ ); } -export default connect( - (state: any) => ({ - instance: state.getIn(['teams', 'instance']), - saving: - state.getIn(['teams', 'saveRequest', 'loading']) || - state.getIn(['teams', 'updateRequest', 'loading']), - errors: state.getIn(['teams', 'saveRequest', 'errors']), - }), - { edit, save, init, remove, update } -)(TeamsAddForm); +export default observer(TeamsAddForm); diff --git a/frontend/app/components/Client/Integrations/Teams/TeamsChannelList.tsx b/frontend/app/components/Client/Integrations/Teams/TeamsChannelList.tsx index 942e1e32c..131a404c8 100644 --- a/frontend/app/components/Client/Integrations/Teams/TeamsChannelList.tsx +++ b/frontend/app/components/Client/Integrations/Teams/TeamsChannelList.tsx @@ -1,51 +1,57 @@ +import { observer } from 'mobx-react-lite'; import React from 'react'; -import { connect } from 'react-redux'; + +import { useStore } from 'App/mstore'; import { NoContent } from 'UI'; -import { remove, edit, init } from 'Duck/integrations/teams'; + import DocLink from 'Shared/DocLink/DocLink'; -function TeamsChannelList(props: { list: any, edit: (inst: any) => any, onEdit: () => void }) { - const { list } = props; +function TeamsChannelList(props: { onEdit: () => void }) { + const { integrationsStore } = useStore(); + const list = integrationsStore.msteams.list; + const edit = integrationsStore.msteams.edit; - const onEdit = (instance: Record) => { - props.edit(instance); - props.onEdit(); - }; + const onEdit = (instance: Record) => { + edit(instance); + props.onEdit(); + }; - return ( -
- -
- Integrate MS Teams with OpenReplay and share insights with the rest of the team, directly from the recording page. -
- -
- } - size="small" - show={list.size === 0} - > - {list.map((c: any) => ( -
onEdit(c)} - > -
-
{c.name}
-
{c.endpoint}
-
-
- ))} - -
- ); + return ( +
+ +
+ Integrate MS Teams with OpenReplay and share insights with the + rest of the team, directly from the recording page. +
+ +
+ } + size="small" + show={list.length === 0} + > + {list.map((c: any) => ( +
onEdit(c)} + > +
+
{c.name}
+
+ {c.endpoint} +
+
+
+ ))} + +
+ ); } -export default connect( - (state: any) => ({ - list: state.getIn(['teams', 'list']), - }), - { remove, edit, init } -)(TeamsChannelList); +export default observer(TeamsChannelList); diff --git a/frontend/app/components/Client/Integrations/Teams/index.tsx b/frontend/app/components/Client/Integrations/Teams/index.tsx index e51bd64b1..b85a4f010 100644 --- a/frontend/app/components/Client/Integrations/Teams/index.tsx +++ b/frontend/app/components/Client/Integrations/Teams/index.tsx @@ -1,17 +1,15 @@ import React, { useEffect } from 'react'; import TeamsChannelList from './TeamsChannelList'; -import { fetchList, init } from 'Duck/integrations/teams'; -import { connect } from 'react-redux'; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; + import TeamsAddForm from './TeamsAddForm'; import { Button } from 'UI'; -interface Props { - onEdit?: (integration: any) => void; - istance: any; - fetchList: any; - init: any; -} -const MSTeams = (props: Props) => { +const MSTeams = () => { + const { integrationsStore } = useStore(); + const fetchList = integrationsStore.msteams.fetchIntegrations; + const init = integrationsStore.msteams.init; const [active, setActive] = React.useState(false); const onEdit = () => { @@ -20,11 +18,11 @@ const MSTeams = (props: Props) => { const onNew = () => { setActive(true); - props.init({}); + init({}); } useEffect(() => { - props.fetchList(); + void fetchList(); }, []); return ( @@ -47,9 +45,4 @@ const MSTeams = (props: Props) => { MSTeams.displayName = 'MSTeams'; -export default connect( - (state: any) => ({ - istance: state.getIn(['teams', 'instance']), - }), - { fetchList, init } -)(MSTeams); +export default observer(MSTeams); diff --git a/frontend/app/components/Client/Integrations/VueDoc/VueDoc.js b/frontend/app/components/Client/Integrations/VueDoc/VueDoc.js index 2a9e2e2a6..1c2ff19c6 100644 --- a/frontend/app/components/Client/Integrations/VueDoc/VueDoc.js +++ b/frontend/app/components/Client/Integrations/VueDoc/VueDoc.js @@ -1,11 +1,15 @@ +import { useStore } from "App/mstore"; import React from 'react'; import { CodeBlock } from "UI"; -import ToggleContent from '../../../shared/ToggleContent'; +import ToggleContent from 'Components/shared/ToggleContent'; import DocLink from 'Shared/DocLink/DocLink'; -import { connect } from 'react-redux'; +import { observer } from 'mobx-react-lite'; -const VueDoc = (props) => { - const { projectKey, siteId } = props; +const VueDoc = () => { + const { integrationsStore, projectsStore } = useStore(); + const sites = projectsStore.list; + const siteId = integrationsStore.integrations.siteId + const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey const usage = `import Vuex from 'vuex' import OpenReplay from '@openreplay/tracker'; @@ -81,10 +85,4 @@ const store = new Vuex.Store({ VueDoc.displayName = 'VueDoc'; -export default connect((state) => { - const siteId = state.getIn(['integrations', 'siteId']); - const sites = state.getIn(['site', 'list']); - return { - projectKey: sites.find((site) => site.get('id') === siteId).get('projectKey'), - }; -})(VueDoc); +export default observer(VueDoc); diff --git a/frontend/app/components/Client/Integrations/ZustandDoc/ZustandDoc.js b/frontend/app/components/Client/Integrations/ZustandDoc/ZustandDoc.js index 9ed446cdc..eb7dcd091 100644 --- a/frontend/app/components/Client/Integrations/ZustandDoc/ZustandDoc.js +++ b/frontend/app/components/Client/Integrations/ZustandDoc/ZustandDoc.js @@ -1,11 +1,15 @@ +import { useStore } from "App/mstore"; import React from 'react'; import { CodeBlock } from "UI"; -import ToggleContent from '../../../shared/ToggleContent'; +import ToggleContent from 'Components//shared/ToggleContent'; import DocLink from 'Shared/DocLink/DocLink'; -import { connect } from 'react-redux'; +import { observer } from 'mobx-react-lite' const ZustandDoc = (props) => { - const { projectKey } = props; + const { integrationsStore, projectsStore } = useStore(); + const sites = projectsStore.list; + const siteId = integrationsStore.integrations.siteId + const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey const usage = `import create from "zustand"; import Tracker from '@openreplay/tracker'; @@ -97,10 +101,4 @@ const useBearStore = create( ZustandDoc.displayName = 'ZustandDoc'; -export default connect((state) => { - const siteId = state.getIn(['integrations', 'siteId']); - const sites = state.getIn(['site', 'list']); - return { - projectKey: sites.find((site) => site.get('id') === siteId).get('projectKey'), - }; -})(ZustandDoc); +export default observer(ZustandDoc); diff --git a/frontend/app/components/Client/Roles/Roles.tsx b/frontend/app/components/Client/Roles/Roles.tsx index a5ef62264..1ea6e9685 100644 --- a/frontend/app/components/Client/Roles/Roles.tsx +++ b/frontend/app/components/Client/Roles/Roles.tsx @@ -23,11 +23,14 @@ interface Props { permissionsMap: any; removeErrors: any; resetErrors: () => void; - projectsMap: any; } function Roles(props: Props) { - const { roleStore } = useStore(); + const { roleStore, projectsStore } = useStore(); + const projectsMap = projectsStore.list.reduce((acc: any, p: any) => { + acc[p.id] = p.name; + return acc; + }, {}) const roles = roleStore.list; const loading = roleStore.loading; const init = roleStore.init; @@ -36,7 +39,7 @@ function Roles(props: Props) { roleStore.permissions.forEach((p: any) => { permissionsMap[p.value] = p.text; }); - const { account, projectsMap } = props; + const { account } = props; const { showModal, hideModal } = useModal(); const isAdmin = account.admin || account.superAdmin; @@ -108,13 +111,8 @@ function Roles(props: Props) { export default connect( (state: any) => { - const projects = state.getIn(['site', 'list']); return { account: state.getIn(['user', 'account']), - projectsMap: projects.reduce((acc: any, p: any) => { - acc[p.id] = p.name; - return acc; - }, {}), }; } )(withPageTitle('Roles & Access - OpenReplay Preferences')(observer(Roles))); diff --git a/frontend/app/components/Client/Roles/components/RoleForm/RoleForm.tsx b/frontend/app/components/Client/Roles/components/RoleForm/RoleForm.tsx index b8287dd42..e83bf2993 100644 --- a/frontend/app/components/Client/Roles/components/RoleForm/RoleForm.tsx +++ b/frontend/app/components/Client/Roles/components/RoleForm/RoleForm.tsx @@ -23,16 +23,16 @@ interface Permission { interface Props { closeModal: (toastMessage?: string) => void; - projects: any[]; permissionsMap: any; deleteHandler: (id: any) => Promise; } const RoleForm = (props: Props) => { - const { roleStore } = useStore(); + const { roleStore, projectsStore } = useStore(); + const projects = projectsStore.list; const role = roleStore.instance; const saving = roleStore.loading; - const { closeModal, permissionsMap, projects } = props; + const { closeModal, permissionsMap } = props; const projectOptions = projects .filter(({ value }) => !role.projects.includes(value)) .map((p: any) => ({ @@ -217,12 +217,7 @@ const RoleForm = (props: Props) => { ); }; -export default connect((state: any) => { - const projects = state.getIn(['site', 'list']); - return { - projects, - }; -})(observer(RoleForm)); +export default observer(RoleForm); function OptionLabel(nameMap: any, p: any, onChangeOption: (e: any) => void) { return ( 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/NewSiteForm.tsx b/frontend/app/components/Client/Sites/NewSiteForm.tsx index c4beec33b..1caf34c16 100644 --- a/frontend/app/components/Client/Sites/NewSiteForm.tsx +++ b/frontend/app/components/Client/Sites/NewSiteForm.tsx @@ -3,21 +3,17 @@ import React, { ChangeEvent, FormEvent, useEffect, useState } from 'react'; import { ConnectedProps, connect } from 'react-redux'; import { RouteComponentProps, withRouter } from 'react-router-dom'; import { toast } from 'react-toastify'; - -import { useStore, withStore } from 'App/mstore'; +import { useStore } from 'App/mstore'; import { clearSearch as clearSearchLive } from 'Duck/liveSearch'; -import { edit, fetchList, remove, save, update } from 'Duck/site'; -import { setSiteId } from 'Duck/site'; import { pushNewSite } from 'Duck/user'; import { Button, Form, Icon, Input, SegmentSelection } from 'UI'; import { confirm } from 'UI'; +import { observer } from 'mobx-react-lite'; import styles from './siteForm.module.css'; type OwnProps = { onClose: (arg: any) => void; - mstore: any; - canDelete: boolean; }; type PropsFromRedux = ConnectedProps; @@ -25,36 +21,34 @@ type PropsFromRedux = ConnectedProps; type Props = PropsFromRedux & RouteComponentProps & OwnProps; const NewSiteForm = ({ - site, - loading, - save, - remove, - edit, - update, - pushNewSite, - fetchList, - setSiteId, clearSearchLive, location: { pathname }, onClose, - mstore, - activeSiteId, - canDelete, }: Props) => { + const mstore = useStore(); + const { projectsStore } = mstore; + const activeSiteId = projectsStore.active?.id + const site = projectsStore.instance; + const siteList = projectsStore.list; + const loading = projectsStore.loading; + const canDelete = siteList.length > 1; + const setSiteId = projectsStore.setSiteId; + const saveProject = projectsStore.save; + const fetchList = projectsStore.fetchList; const [existsError, setExistsError] = useState(false); const { searchStore } = useStore(); useEffect(() => { - if (pathname.includes('onboarding')) { + if (pathname.includes('onboarding') && site?.id) { setSiteId(site.id); } + if (!site) projectsStore.initProject({}); }, []); const onSubmit = (e: FormEvent) => { e.preventDefault(); - - if (site.exists()) { - update(site, site.id).then((response: any) => { + if (site?.id && site.exists()) { + projectsStore.updateProject( site.id, site.toData()).then((response: any) => { if (!response || !response.errors || response.errors.size === 0) { onClose(null); if (!pathname.includes('onboarding')) { @@ -66,7 +60,7 @@ const NewSiteForm = ({ } }); } else { - save(site).then((response: any) => { + saveProject(site!).then((response: any) => { if (!response || !response.errors || response.errors.size === 0) { onClose(null); searchStore.clearSearch(); @@ -88,8 +82,9 @@ const NewSiteForm = ({ confirmButton: 'Yes, delete', cancelButton: 'Cancel', }) + && site?.id ) { - remove(site.id).then(() => { + projectsStore.removeProject(site.id).then(() => { onClose(null); if (site.id === activeSiteId) { setSiteId(null); @@ -102,9 +97,12 @@ const NewSiteForm = ({ target: { name, value }, }: ChangeEvent) => { setExistsError(false); - edit({ [name]: value }); + projectsStore.editInstance({ [name]: value }); }; + if (!site) { + return null + } return (
@@ -145,7 +143,7 @@ const NewSiteForm = ({ ]} value={site.platform} onChange={(value) => { - edit({ platform: value }); + projectsStore.editInstance({ platform: value }); }} />
@@ -156,9 +154,9 @@ const NewSiteForm = ({ type="submit" className="float-left mr-2" loading={loading} - disabled={!site.validate()} + disabled={!site.validate} > - {site.exists() ? 'Update' : 'Add'} + {site?.exists() ? 'Update' : 'Add'} {site.exists() && (
} 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/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx index 3a1b4ebfc..81a1d00cf 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx @@ -81,7 +81,6 @@ export default connect( insightsFilters: state.getIn(['sessions', 'insightFilters']), visitedEvents: state.getIn(['sessions', 'visitedEvents']), insights: state.getIn(['sessions', 'insights']), - host: state.getIn(['sessions', 'host']), }), { fetchInsights, } ) diff --git a/frontend/app/components/Dashboard/components/DashboardList/DashboardList.tsx b/frontend/app/components/Dashboard/components/DashboardList/DashboardList.tsx index 1588248a2..2e70a543e 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/DashboardList.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/DashboardList.tsx @@ -24,11 +24,12 @@ import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; import DashboardEditModal from '../DashboardEditModal'; -function DashboardList({ siteId }: { siteId: string }) { +function DashboardList() { + const { dashboardStore, projectsStore } = useStore(); + const siteId = projectsStore.siteId; const [focusTitle, setFocusedInput] = React.useState(true); const [showEditModal, setShowEditModal] = React.useState(false); - const { dashboardStore } = useStore(); const list = dashboardStore.filteredList; const dashboardsSearch = dashboardStore.filter.query; const history = useHistory(); @@ -219,6 +220,4 @@ function DashboardList({ siteId }: { siteId: string }) { ); } -export default connect((state: any) => ({ - siteId: state.getIn(['site', 'siteId']), -}))(observer(DashboardList)); +export default observer(DashboardList); diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/NewDashboardModal.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/NewDashboardModal.tsx index d5a22f1fb..274c4c5e4 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/NewDashboardModal.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/NewDashboardModal.tsx @@ -2,7 +2,8 @@ import { Modal } from 'antd'; import React, { useEffect } from 'react'; import { connect } from 'react-redux'; import colors from 'tailwindcss/colors'; - +import { observer } from 'mobx-react-lite'; +import { useStore } from 'App/mstore'; import CreateCard from 'Components/Dashboard/components/DashboardList/NewDashModal/CreateCard'; import SelectCard from './SelectCard'; @@ -12,7 +13,6 @@ interface NewDashboardModalProps { open: boolean; isAddingFromLibrary?: boolean; isEnterprise?: boolean; - isMobile?: boolean; } const NewDashboardModal: React.FC = ({ @@ -20,8 +20,9 @@ const NewDashboardModal: React.FC = ({ open, isAddingFromLibrary = false, isEnterprise = false, - isMobile = false, }) => { + const { projectsStore } = useStore(); + const isMobile = projectsStore.isMobile; const [step, setStep] = React.useState(0); const [selectedCategory, setSelectedCategory] = React.useState('product-analytics'); @@ -75,10 +76,9 @@ const NewDashboardModal: React.FC = ({ }; const mapStateToProps = (state: any) => ({ - isMobile: state.getIn(['site', 'instance', 'platform']) === 'ios', isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee' || state.getIn(['user', 'account', 'edition']) === 'msaas', }); -export default connect(mapStateToProps)(NewDashboardModal); +export default connect(mapStateToProps)(observer(NewDashboardModal)); diff --git a/frontend/app/components/Header/NewProjectButton/NewProjectButton.tsx b/frontend/app/components/Header/NewProjectButton/NewProjectButton.tsx index 2ac9e95ae..241136a63 100644 --- a/frontend/app/components/Header/NewProjectButton/NewProjectButton.tsx +++ b/frontend/app/components/Header/NewProjectButton/NewProjectButton.tsx @@ -1,40 +1,26 @@ +import { observer } from 'mobx-react-lite'; import React from 'react'; -import { Icon } from 'UI'; -import { useStore } from 'App/mstore'; -import { useObserver } from 'mobx-react-lite'; -import { useModal } from 'App/components/Modal'; + import NewSiteForm from 'App/components/Client/Sites/NewSiteForm'; -import { init } from 'Duck/site'; -import { connect } from 'react-redux'; -interface Props { - isAdmin?: boolean; - init?: (data: any) => void; -} -function NewProjectButton(props: Props) { - const { isAdmin = false } = props; - const { userStore } = useStore(); - const limtis = useObserver(() => userStore.limits); - const canAddProject = useObserver(() => isAdmin && (limtis.projects === -1 || limtis.projects > 0)); - const { showModal, hideModal } = useModal(); +import { useModal } from 'App/components/Modal'; +import { useStore } from 'App/mstore'; +import { Icon } from 'UI'; - const onClick = () => { - props.init({}); - showModal(, { right: true }); - }; +function NewProjectButton() { + const { projectsStore } = useStore(); + const { showModal, hideModal } = useModal(); - return ( -
  • - - Add Project -
  • - //
    - // - // Add New Project - //
    - ); + const onClick = () => { + projectsStore.initProject({}); + showModal(, { right: true }); + }; + + return ( +
  • + + Add Project +
  • + ); } -export default connect(null, { init })(NewProjectButton); +export default observer(NewProjectButton); diff --git a/frontend/app/components/Onboarding/components/MetadataList/MetadataList.tsx b/frontend/app/components/Onboarding/components/MetadataList/MetadataList.tsx index 536442bd9..da9cc1c62 100644 --- a/frontend/app/components/Onboarding/components/MetadataList/MetadataList.tsx +++ b/frontend/app/components/Onboarding/components/MetadataList/MetadataList.tsx @@ -1,29 +1,26 @@ import React, { useEffect } from 'react'; import { Button, TagBadge } from 'UI'; -import { connect } from 'react-redux'; import CustomFieldForm from '../../../Client/CustomFields/CustomFieldForm'; import { confirm } from 'UI'; import { useModal } from 'App/components/Modal'; import { toast } from 'react-toastify'; import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; -interface MetadataListProps { - site: { id: string }; -} - -const MetadataList: React.FC = (props) => { - const { site } = props; - const { customFieldStore } = useStore(); +const MetadataList = () => { + const { customFieldStore, projectsStore } = useStore(); + const site = projectsStore.instance; const fields = customFieldStore.list; const { showModal, hideModal } = useModal(); useEffect(() => { - customFieldStore.fetchList(site.id); - }, [site.id]); + customFieldStore.fetchList(site?.id); + }, [site?.id]); const save = (field: any) => { - customFieldStore.save(site.id, field).then((response) => { + if (!site) return; + customFieldStore.save(site.id!, field).then((response) => { if (!response || !response.errors || response.errors.size === 0) { hideModal(); toast.success('Metadata added successfully!'); @@ -62,11 +59,4 @@ const MetadataList: React.FC = (props) => { ); }; -export default connect( - (state: any) => ({ - site: state.getIn(['site', 'instance']), - fields: state.getIn(['customFields', 'list']).sortBy((i: any) => i.index), - field: state.getIn(['customFields', 'instance']), - loading: state.getIn(['customFields', 'fetchRequest', 'loading']) - }) -)(MetadataList); +export default observer(MetadataList); diff --git a/frontend/app/components/Onboarding/components/OnboardingTabs/ProjectCodeSnippet/ProjectCodeSnippet.js b/frontend/app/components/Onboarding/components/OnboardingTabs/ProjectCodeSnippet/ProjectCodeSnippet.js index 831f962b6..e4ee5e2c4 100644 --- a/frontend/app/components/Onboarding/components/OnboardingTabs/ProjectCodeSnippet/ProjectCodeSnippet.js +++ b/frontend/app/components/Onboarding/components/OnboardingTabs/ProjectCodeSnippet/ProjectCodeSnippet.js @@ -1,8 +1,8 @@ import React, { useEffect, useState } from 'react'; -import { connect } from 'react-redux'; -import { editGDPR, saveGDPR, init } from 'Duck/site'; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; import { Checkbox, Loader, Toggler } from 'UI'; -import GDPR from 'Types/site/gdpr'; +import GDPR from 'App/mstore/types/gdpr'; import cn from 'classnames'; import stl from './projectCodeSnippet.module.css'; import CircleNumber from '../../CircleNumber'; @@ -18,37 +18,39 @@ const inputModeOptions = [ const inputModeOptionsMap = {}; inputModeOptions.forEach((o, i) => (inputModeOptionsMap[o.value] = i)); -const ProjectCodeSnippet = (props) => { - const { site } = props; - const { gdpr } = props.site; +const ProjectCodeSnippet = () => { + const { projectsStore } = useStore(); + const siteId = projectsStore.siteId; + const site = projectsStore.instance; + const gdpr = site.gdpr; + const sites = projectsStore.list; + const editGDPR = projectsStore.editGDPR; + const onSaveGDPR = projectsStore.saveGDPR; + const init = projectsStore.initProject; const [changed, setChanged] = useState(false); const [isAssistEnabled, setAssistEnabled] = useState(false); const [showLoader, setShowLoader] = useState(false); useEffect(() => { - const site = props.sites.find((s) => s.id === props.siteId); + const site = sites.find((s) => s.id === siteId); if (site) { - props.init(site); + init(site); } }, []); - const saveGDPR = (value) => { + const saveGDPR = () => { setChanged(true); - props.saveGDPR(site.id, GDPR({ ...value })); + void onSaveGDPR(site.id); }; const onChangeSelect = ({ name, value }) => { - const _gdpr = { ...gdpr.toData() }; - _gdpr[name] = value; - props.editGDPR({ [name]: value }); - saveGDPR(_gdpr); + editGDPR({ [name]: value }); + saveGDPR(); }; const onChangeOption = ({ target: { name, checked } }) => { - const _gdpr = { ...gdpr.toData() }; - _gdpr[name] = checked; - props.editGDPR({ [name]: checked }); - saveGDPR(_gdpr); + editGDPR({ [name]: checked }); + saveGDPR(); }; useEffect(() => { @@ -159,12 +161,4 @@ const ProjectCodeSnippet = (props) => { ); }; -export default connect( - (state) => ({ - siteId: state.getIn(['site', 'siteId']), - site: state.getIn(['site', 'instance']), - sites: state.getIn(['site', 'list']), - saving: state.getIn(['site', 'saveGDPR', 'loading']), - }), - { editGDPR, saveGDPR, init } -)(ProjectCodeSnippet); +export default observer(ProjectCodeSnippet); diff --git a/frontend/app/components/Onboarding/components/ProjectFormButton/ProjectFormButton.js b/frontend/app/components/Onboarding/components/ProjectFormButton/ProjectFormButton.js index da9f4211b..22405aead 100644 --- a/frontend/app/components/Onboarding/components/ProjectFormButton/ProjectFormButton.js +++ b/frontend/app/components/Onboarding/components/ProjectFormButton/ProjectFormButton.js @@ -1,10 +1,14 @@ import React from 'react'; -import { connect } from 'react-redux'; import NewSiteForm from '../../../Client/Sites/NewSiteForm'; -import { init } from 'Duck/site'; import { useModal } from 'App/components/Modal'; +import { observer } from 'mobx-react-lite' +import { useStore } from 'App/mstore'; -const ProjectFormButton = ({ sites, siteId, init }) => { +const ProjectFormButton = () => { + const { projectsStore } = useStore(); + const sites = projectsStore.list; + const siteId = projectsStore.siteId; + const init = projectsStore.initProject; const site = sites.find(({ id }) => id === siteId); const { showModal, hideModal } = useModal(); const openModal = (e) => { @@ -26,10 +30,4 @@ const ProjectFormButton = ({ sites, siteId, init }) => { ); }; -export default connect( - (state) => ({ - siteId: state.getIn(['site', 'siteId']), - sites: state.getIn(['site', 'list']), - }), - { init } -)(ProjectFormButton); +export default observer(ProjectFormButton); diff --git a/frontend/app/components/Onboarding/components/withOnboarding.tsx b/frontend/app/components/Onboarding/components/withOnboarding.tsx index 931609428..ad87a358e 100644 --- a/frontend/app/components/Onboarding/components/withOnboarding.tsx +++ b/frontend/app/components/Onboarding/components/withOnboarding.tsx @@ -3,6 +3,8 @@ import { withRouter, RouteComponentProps } from 'react-router-dom'; import { connect, ConnectedProps } from 'react-redux'; import { setOnboarding } from 'Duck/user'; import { sessions, withSiteId, onboarding as onboardingRoute } from 'App/routes'; +import { observer } from 'mobx-react-lite'; +import { useStore } from 'App/mstore'; export interface WithOnboardingProps { history: RouteComponentProps['history']; @@ -18,10 +20,7 @@ export interface WithOnboardingProps { } const connector = connect( - (state: any) => ({ - siteId: state.getIn(['site', 'siteId']), - sites: state.getIn(['site', 'list']), - }), + null, { setOnboarding } ); @@ -31,8 +30,9 @@ const withOnboarding =

    ( Component: React.ComponentType

    ) => { const WithOnboarding: React.FC

    = (props) => { + const { projectsStore } = useStore(); + const sites = projectsStore.list; const { - sites, match: { params: { siteId }, }, @@ -43,7 +43,7 @@ const withOnboarding =

    ( props.setOnboarding(true); props.history.push(withSiteId(sessions(), siteId)); }; - + const navTo = (tab: string) => { props.history.push(withSiteId(onboardingRoute(tab), siteId)); }; @@ -51,7 +51,7 @@ const withOnboarding =

    ( return ; }; - return withRouter(connector(WithOnboarding as React.ComponentType)); + return withRouter(connector(observer(WithOnboarding as React.ComponentType))); }; export default withOnboarding; diff --git a/frontend/app/components/Session/LivePlayer.tsx b/frontend/app/components/Session/LivePlayer.tsx index 516427d11..7e9f0963e 100644 --- a/frontend/app/components/Session/LivePlayer.tsx +++ b/frontend/app/components/Session/LivePlayer.tsx @@ -14,6 +14,7 @@ import withLocationHandlers from 'HOCs/withLocationHandlers'; import APIClient from 'App/api_client'; import { useLocation } from 'react-router-dom'; import { toast } from 'react-toastify'; +import { useStore } from 'App/mstore'; interface Props { session: Session; @@ -26,7 +27,6 @@ interface Props { query?: Record any>; request: () => void; userId: number; - siteId: number; } let playerInst: ILivePlayerContext['player'] | undefined; @@ -40,7 +40,6 @@ function LivePlayer({ query, isEnterprise, userId, - siteId, }: Props) { // @ts-ignore const [contextValue, setContextValue] = useState(defaultContextValue); @@ -48,8 +47,10 @@ function LivePlayer({ const openedFromMultiview = query?.get('multi') === 'true'; const usedSession = isMultiview ? customSession! : session; const location = useLocation(); + const { projectsStore } = useStore(); useEffect(() => { + const projectId = projectsStore.getSiteId(); playerInst = undefined; if (!usedSession.sessionId || contextValue.player !== undefined) return; console.debug('creating live player for', usedSession.sessionId); @@ -69,7 +70,7 @@ function LivePlayer({ sessionWithAgentData, data, userId, - siteId, + projectId, (state) => makeAutoObservable(state), toast ); @@ -81,7 +82,7 @@ function LivePlayer({ sessionWithAgentData, null, userId, - siteId, + projectId, (state) => makeAutoObservable(state), toast ); @@ -140,9 +141,7 @@ function LivePlayer({ export default withPermissions(['ASSIST_LIVE', 'SERVICE_ASSIST_LIVE'], '', true, false)( connect((state: any) => { return { - siteId: state.getIn([ 'site', 'siteId' ]), session: state.getIn(['sessions', 'current']), - showAssist: state.getIn(['sessions', 'showChatWindow']), isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee', userEmail: state.getIn(['user', 'account', 'email']), userName: state.getIn(['user', 'account', 'name']), diff --git a/frontend/app/components/Session/LiveSession.js b/frontend/app/components/Session/LiveSession.js index a9bba11b9..eec861e04 100644 --- a/frontend/app/components/Session/LiveSession.js +++ b/frontend/app/components/Session/LiveSession.js @@ -3,28 +3,29 @@ import { useEffect } from 'react'; import { connect } from 'react-redux'; import usePageTitle from 'App/hooks/usePageTitle'; import { fetch as fetchSession, clearCurrentSession } from 'Duck/sessions'; -import { fetchList as fetchSlackList } from 'Duck/integrations/slack'; import { Loader } from 'UI'; import withPermissions from 'HOCs/withPermissions'; import LivePlayer from './LivePlayer'; import { clearLogs } from 'App/dev/console'; import { toast } from 'react-toastify'; +import { useStore } from 'App/mstore' function LiveSession({ sessionId, fetchSession, - fetchSlackList, hasSessionsPath, session, fetchFailed, clearCurrentSession, }) { + const { integrationsStore } = useStore(); + const fetchSlackList = integrationsStore.slack.fetchIntegrations; const [initialLoading, setInitialLoading] = React.useState(true); usePageTitle('OpenReplay Assist'); useEffect(() => { clearLogs(); - fetchSlackList(); + void fetchSlackList(); return () => { clearCurrentSession() @@ -77,7 +78,6 @@ export default withPermissions(['ASSIST_LIVE', 'SERVICE_ASSIST_LIVE'], '', true, }, { fetchSession, - fetchSlackList, clearCurrentSession, } )(LiveSession) diff --git a/frontend/app/components/Session/MobilePlayer.tsx b/frontend/app/components/Session/MobilePlayer.tsx index 16f4a9178..c03efefa9 100644 --- a/frontend/app/components/Session/MobilePlayer.tsx +++ b/frontend/app/components/Session/MobilePlayer.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useState } from 'react'; import { connect } from 'react-redux'; import { Modal, Loader } from 'UI'; -import { fetchList } from 'Duck/integrations'; import { createIOSPlayer } from 'Player'; import { makeAutoObservable } from 'mobx'; import withLocationHandlers from 'HOCs/withLocationHandlers'; @@ -24,7 +23,7 @@ let playerInst: IOSPlayerContext['player'] | undefined; function MobilePlayer(props: any) { const { session, fetchList } = props; - const { notesStore, sessionStore, uiPlayerStore } = useStore(); + const { notesStore, sessionStore, uiPlayerStore, integrationsStore } = useStore(); const [activeTab, setActiveTab] = useState(''); const [noteItem, setNoteItem] = useState(undefined); // @ts-ignore @@ -37,7 +36,7 @@ function MobilePlayer(props: any) { useEffect(() => { playerInst = undefined; if (!session.sessionId || contextValue.player !== undefined) return; - fetchList('issues'); + void integrationsStore.issues.fetchIntegrations(); sessionStore.setUserTimezone(session.timezone); const [IOSPlayerInst, PlayerStore] = createIOSPlayer( session, diff --git a/frontend/app/components/Session/Player/LivePlayer/AssistSessionsTabs/AssistSessionsTabs.tsx b/frontend/app/components/Session/Player/LivePlayer/AssistSessionsTabs/AssistSessionsTabs.tsx index b156ee3f4..28b3945dd 100644 --- a/frontend/app/components/Session/Player/LivePlayer/AssistSessionsTabs/AssistSessionsTabs.tsx +++ b/frontend/app/components/Session/Player/LivePlayer/AssistSessionsTabs/AssistSessionsTabs.tsx @@ -5,7 +5,6 @@ import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; import { useHistory } from 'react-router-dom'; import { multiview, liveSession, withSiteId } from 'App/routes'; -import { connect } from 'react-redux'; import { PlayerContext, ILivePlayerContext } from 'App/components/Session/playerContext'; interface ITab { @@ -44,13 +43,14 @@ const CurrentTab = React.memo(() => ( )); -function AssistTabs({ session, siteId }: { session: Record; siteId: string }) { +function AssistTabs({ session }: { session: Record }) { const history = useHistory(); const { store } = React.useContext(PlayerContext) as unknown as ILivePlayerContext const { recordingState, calling, remoteControl } = store.get() const isDisabled = recordingState !== 0 || calling !== 0 || remoteControl !== 0 - const { assistMultiviewStore } = useStore(); + const { assistMultiviewStore, projectsStore } = useStore(); + const siteId = projectsStore.siteId!; const placeholder = new Array(4 - assistMultiviewStore.sessions.length).fill(0); @@ -91,6 +91,4 @@ function AssistTabs({ session, siteId }: { session: Record; siteId: ); } -export default connect((state: any) => ({ siteId: state.getIn(['site', 'siteId']) }))( - observer(AssistTabs) -); +export default observer(AssistTabs); diff --git a/frontend/app/components/Session/Player/LivePlayer/LivePlayerBlockHeader.tsx b/frontend/app/components/Session/Player/LivePlayer/LivePlayerBlockHeader.tsx index df37e1684..96b7751b4 100644 --- a/frontend/app/components/Session/Player/LivePlayer/LivePlayerBlockHeader.tsx +++ b/frontend/app/components/Session/Player/LivePlayer/LivePlayerBlockHeader.tsx @@ -22,7 +22,8 @@ const ASSIST_ROUTE = assistRoute(); function LivePlayerBlockHeader(props: any) { const [hideBack, setHideBack] = React.useState(false); const { store } = React.useContext(PlayerContext); - const { assistMultiviewStore } = useStore(); + const { assistMultiviewStore, projectsStore } = useStore(); + const siteId = projectsStore.siteId; const history = useHistory(); const { width, height } = store.get(); @@ -30,7 +31,6 @@ function LivePlayerBlockHeader(props: any) { session, metaList, closedLive = false, - siteId, isMultiview, } = props; @@ -109,7 +109,6 @@ const PlayerHeaderCont = connect( isAssist, session, sessionPath: state.getIn(['sessions', 'sessionPath']), - siteId: state.getIn(['site', 'siteId']), metaList: state.getIn(['customFields', 'list']).map((i: any) => i.key), closedLive: !!state.getIn(['sessions', 'errors']) || (isAssist && !session.live), }; diff --git a/frontend/app/components/Session/Player/MobilePlayer/MobileControls.tsx b/frontend/app/components/Session/Player/MobilePlayer/MobileControls.tsx index 5bc1ab3b5..386532833 100644 --- a/frontend/app/components/Session/Player/MobilePlayer/MobileControls.tsx +++ b/frontend/app/components/Session/Player/MobilePlayer/MobileControls.tsx @@ -46,7 +46,7 @@ function Controls(props: any) { const { player, store } = React.useContext(MobilePlayerContext); const history = useHistory(); const { playing, completed, skip, speed, messagesLoading } = store.get(); - const { uiPlayerStore } = useStore(); + const { uiPlayerStore, projectsStore } = useStore(); const fullscreen = uiPlayerStore.fullscreen; const bottomBlock = uiPlayerStore.bottomBlock; const toggleBottomBlock = uiPlayerStore.toggleBottomBlock @@ -54,12 +54,12 @@ function Controls(props: any) { const fullscreenOff = uiPlayerStore.fullscreenOff; const changeSkipInterval = uiPlayerStore.changeSkipInterval; const skipInterval = uiPlayerStore.skipInterval; + const siteId = projectsStore.siteId; const { session, setActiveTab, previousSessionId, nextSessionId, - siteId, disableDevtools, } = props; @@ -289,7 +289,6 @@ export default connect( totalAssistSessions: state.getIn(['liveSearch', 'total']), previousSessionId: state.getIn(['sessions', 'previousId']), nextSessionId: state.getIn(['sessions', 'nextId']), - siteId: state.getIn(['site', 'siteId']), }; }, { diff --git a/frontend/app/components/Session/Player/MobilePlayer/MobilePlayerHeader.tsx b/frontend/app/components/Session/Player/MobilePlayer/MobilePlayerHeader.tsx index 183c419f1..a6a243e77 100644 --- a/frontend/app/components/Session/Player/MobilePlayer/MobilePlayerHeader.tsx +++ b/frontend/app/components/Session/Player/MobilePlayer/MobilePlayerHeader.tsx @@ -23,13 +23,12 @@ function PlayerBlockHeader(props: any) { const playerState = store?.get?.() || { width: 0, height: 0, showEvents: false }; const { width = 0, height = 0, showEvents = false } = playerState; - const { customFieldStore } = useStore(); - + const { customFieldStore, projectsStore } = useStore(); + const siteId = projectsStore.siteId!; const { session, fullscreen, metaList, - siteId, setActiveTab, activeTab, history, @@ -106,9 +105,7 @@ const PlayerHeaderCont = connect( return { session, sessionPath: state.getIn(['sessions', 'sessionPath']), - local: state.getIn(['sessions', 'timezone']), funnelRef: state.getIn(['funnels', 'navRef']), - siteId: state.getIn(['site', 'siteId']), metaList: state.getIn(['customFields', 'list']).map((i: any) => i.key), }; }, diff --git a/frontend/app/components/Session/Player/MobilePlayer/MobilePlayerSubheader.tsx b/frontend/app/components/Session/Player/MobilePlayer/MobilePlayerSubheader.tsx index c07e903fd..df068843f 100644 --- a/frontend/app/components/Session/Player/MobilePlayer/MobilePlayerSubheader.tsx +++ b/frontend/app/components/Session/Player/MobilePlayer/MobilePlayerSubheader.tsx @@ -3,7 +3,6 @@ import QueueControls from 'Components/Session_/QueueControls'; import Bookmark from 'Shared/Bookmark'; import Issues from 'Components/Session_/Issues/Issues'; import NotePopup from 'Components/Session_/components/NotePopup'; -import { observer } from 'mobx-react-lite'; import { connect } from 'react-redux'; import { Tag } from 'antd' import { ShareAltOutlined } from '@ant-design/icons'; @@ -56,8 +55,7 @@ function SubHeader(props: any) { } export default connect((state: any) => ({ - siteId: state.getIn(['site', 'siteId']), modules: state.getIn(['user', 'account', 'modules']) || [], integrations: state.getIn(['issues', 'list']), isIOS: state.getIn(['sessions', 'current']).platform === 'ios', -}))(observer(SubHeader)); +}))(SubHeader); diff --git a/frontend/app/components/Session/Player/ReplayPlayer/PlayerBlockHeader.tsx b/frontend/app/components/Session/Player/ReplayPlayer/PlayerBlockHeader.tsx index c4ffefc89..6b7c899d3 100644 --- a/frontend/app/components/Session/Player/ReplayPlayer/PlayerBlockHeader.tsx +++ b/frontend/app/components/Session/Player/ReplayPlayer/PlayerBlockHeader.tsx @@ -24,7 +24,8 @@ const SESSIONS_ROUTE = sessionsRoute(); function PlayerBlockHeader(props: any) { const [hideBack, setHideBack] = React.useState(false); const { player, store } = React.useContext(PlayerContext); - const { uxtestingStore, customFieldStore } = useStore() + const { uxtestingStore, customFieldStore, projectsStore } = useStore() + const siteId = projectsStore.siteId!; const playerState = store?.get?.() || { width: 0, height: 0, showEvents: false } const { width = 0, height = 0, showEvents = false } = playerState @@ -33,7 +34,6 @@ function PlayerBlockHeader(props: any) { fullscreen, metaList, closedLive = false, - siteId, setActiveTab, activeTab, history, @@ -135,9 +135,7 @@ const PlayerHeaderCont = connect( return { session, sessionPath: state.getIn(['sessions', 'sessionPath']), - local: state.getIn(['sessions', 'timezone']), funnelRef: state.getIn(['funnels', 'navRef']), - siteId: state.getIn(['site', 'siteId']), metaList: state.getIn(['customFields', 'list']).map((i: any) => i.key), }; }, diff --git a/frontend/app/components/Session/Player/TagWatch/SaveModal.tsx b/frontend/app/components/Session/Player/TagWatch/SaveModal.tsx index 3fa29e4ef..d612d94d7 100644 --- a/frontend/app/components/Session/Player/TagWatch/SaveModal.tsx +++ b/frontend/app/components/Session/Player/TagWatch/SaveModal.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Button, Checkbox, Input } from 'antd'; import { useHistory } from 'react-router-dom'; import { withSiteId, sessions } from 'App/routes'; -import store from 'App/store'; +import { useStore } from 'App/mstore'; interface Props { onSave: (name: string, ignoreClRage: boolean, ignoreDeadCl: boolean) => Promise; @@ -11,6 +11,7 @@ interface Props { function SaveModal({ onSave, hideModal }: Props) { const history = useHistory(); + const { projectsStore } = useStore(); const [name, setName] = React.useState(''); const [ignoreClRage, setIgnoreClRage] = React.useState(false); const [ignoreDeadCl, setIgnoreDeadCl] = React.useState(false); @@ -22,7 +23,7 @@ function SaveModal({ onSave, hideModal }: Props) { const saveAndOpen = () => { onSave(name, ignoreClRage, ignoreDeadCl).then((tagId) => { hideModal(); - const siteId = store.getState().getIn(['site', 'siteId']); + const siteId = projectsStore.getSiteId() as unknown as string; history.push(withSiteId(sessions({ tnw: `is|${tagId}`, range: 'LAST_24_HOURS' }), siteId)); }); }; diff --git a/frontend/app/components/Session/Session.tsx b/frontend/app/components/Session/Session.tsx index 026ae0c7b..02ab206a5 100644 --- a/frontend/app/components/Session/Session.tsx +++ b/frontend/app/components/Session/Session.tsx @@ -8,7 +8,6 @@ import usePageTitle from 'App/hooks/usePageTitle'; import { useStore } from 'App/mstore'; import { sessions as sessionsRoute } from 'App/routes'; import MobilePlayer from 'Components/Session/MobilePlayer'; -import { fetchList as fetchSlackList } from 'Duck/integrations/slack'; import { clearCurrentSession, fetchV2 } from 'Duck/sessions'; import { Link, Loader, NoContent } from 'UI'; @@ -89,7 +88,6 @@ export default withPermissions( }; }, { - fetchSlackList, fetchV2, clearCurrentSession, } diff --git a/frontend/app/components/Session/WebPlayer.tsx b/frontend/app/components/Session/WebPlayer.tsx index 93970d515..8ccd9f49e 100644 --- a/frontend/app/components/Session/WebPlayer.tsx +++ b/frontend/app/components/Session/WebPlayer.tsx @@ -9,7 +9,6 @@ import { toast } from 'react-toastify'; import { useStore } from 'App/mstore'; import { Note } from 'App/services/NotesService'; -import { fetchList } from 'Duck/integrations'; import { Loader, Modal } from 'UI'; import ReadNote from '../Session_/Player/Controls/components/ReadNote'; @@ -36,10 +35,9 @@ let playerInst: IPlayerContext['player'] | undefined; function WebPlayer(props: any) { const { session, - fetchList, startedAt, } = props; - const { notesStore, sessionStore, uxtestingStore, uiPlayerStore } = useStore(); + const { notesStore, sessionStore, uxtestingStore, uiPlayerStore, integrationsStore } = useStore(); const fullscreen = uiPlayerStore.fullscreen; const toggleFullscreen = uiPlayerStore.toggleFullscreen; const closeBottomBlock = uiPlayerStore.closeBottomBlock; @@ -72,7 +70,7 @@ function WebPlayer(props: any) { | Record | undefined; const usePrefetched = props.prefetched && mobData?.data; - fetchList('issues'); + void integrationsStore.issues.fetchIntegrations(); sessionStore.setUserTimezone(session.timezone); const [WebPlayerInst, PlayerStore] = createWebPlayer( session, @@ -256,7 +254,5 @@ export default connect( jwt: state.getIn(['user', 'jwt']), startedAt: state.getIn(['sessions', 'current']).startedAt || 0, }), - { - fetchList, - } + )(withLocationHandlers()(observer(WebPlayer))); diff --git a/frontend/app/components/Session_/Multiview/Multiview.tsx b/frontend/app/components/Session_/Multiview/Multiview.tsx index de5ab0865..583e0d421 100644 --- a/frontend/app/components/Session_/Multiview/Multiview.tsx +++ b/frontend/app/components/Session_/Multiview/Multiview.tsx @@ -15,20 +15,19 @@ import SessionTileFooter from './SessionTileFooter' function Multiview({ total, fetchSessions, - siteId, assistCredentials, customSetSessions, }: { total: number; customSetSessions: (data: any) => void; fetchSessions: (filter: any) => void; - siteId: string; assistCredentials: any; list: Record[]; }) { const { showModal, hideModal } = useModal(); - const { assistMultiviewStore } = useStore(); + const { assistMultiviewStore, projectsStore } = useStore(); + const siteId = projectsStore.siteId!; const history = useHistory(); // @ts-ignore const { sessionsquery } = useParams(); @@ -128,7 +127,6 @@ function Multiview({ export default connect( (state: any) => ({ total: state.getIn(['liveSearch', 'total']), - siteId: state.getIn(['site', 'siteId']), }), { fetchSessions, diff --git a/frontend/app/components/Session_/Player/Controls/AssistSessionsTabs/AssistSessionsTabs.tsx b/frontend/app/components/Session_/Player/Controls/AssistSessionsTabs/AssistSessionsTabs.tsx index 07311665a..d6e249998 100644 --- a/frontend/app/components/Session_/Player/Controls/AssistSessionsTabs/AssistSessionsTabs.tsx +++ b/frontend/app/components/Session_/Player/Controls/AssistSessionsTabs/AssistSessionsTabs.tsx @@ -42,9 +42,10 @@ const CurrentTab = React.memo(() => ( )); -function AssistTabs({ session, siteId }: { session: Record; siteId: string }) { +function AssistTabs({ session }: { session: Record }) { const history = useHistory(); - const { assistMultiviewStore } = useStore(); + const { assistMultiviewStore, projectsStore } = useStore(); + const siteId = projectsStore.siteId!; const placeholder = new Array(4 - assistMultiviewStore.sessions.length).fill(0); @@ -83,6 +84,4 @@ function AssistTabs({ session, siteId }: { session: Record; siteId: ); } -export default connect((state: any) => ({ siteId: state.getIn(['site', 'siteId']) }))( - observer(AssistTabs) -); +export default observer(AssistTabs) diff --git a/frontend/app/components/Session_/Player/Controls/Controls.tsx b/frontend/app/components/Session_/Player/Controls/Controls.tsx index b8bf33365..32de45b0a 100644 --- a/frontend/app/components/Session_/Player/Controls/Controls.tsx +++ b/frontend/app/components/Session_/Player/Controls/Controls.tsx @@ -70,7 +70,7 @@ function getStorageName(type: any) { function Controls(props: any) { const { player, store } = React.useContext(PlayerContext); - const { uxtestingStore, uiPlayerStore } = useStore(); + const { uxtestingStore, uiPlayerStore, projectsStore } = useStore(); const fullscreen = uiPlayerStore.fullscreen; const bottomBlock = uiPlayerStore.bottomBlock; const toggleBottomBlock = uiPlayerStore.toggleBottomBlock; @@ -80,6 +80,7 @@ function Controls(props: any) { const skipInterval = uiPlayerStore.skipInterval; const showStorageRedux = !uiPlayerStore.hiddenHints.storage; const history = useHistory(); + const siteId = projectsStore.siteId; const { playing, completed, @@ -95,7 +96,6 @@ function Controls(props: any) { session, previousSessionId, nextSessionId, - siteId, setActiveTab, } = props; @@ -440,7 +440,6 @@ export default connect( totalAssistSessions: state.getIn(['liveSearch', 'total']), previousSessionId: state.getIn(['sessions', 'previousId']), nextSessionId: state.getIn(['sessions', 'nextId']), - siteId: state.getIn(['site', 'siteId']), }; }, { diff --git a/frontend/app/components/Session_/Player/Controls/components/CreateNote.tsx b/frontend/app/components/Session_/Player/Controls/components/CreateNote.tsx index 3da4a31d7..4d4eb49b6 100644 --- a/frontend/app/components/Session_/Player/Controls/components/CreateNote.tsx +++ b/frontend/app/components/Session_/Player/Controls/components/CreateNote.tsx @@ -1,10 +1,9 @@ import { Tag } from 'antd'; -import { List } from 'immutable'; import { Duration } from 'luxon'; import React from 'react'; import { connect } from 'react-redux'; import { toast } from 'react-toastify'; - +import { observer } from 'mobx-react-lite'; import { useStore } from 'App/mstore'; import { Note, @@ -13,8 +12,6 @@ import { iTag, tagProps, } from 'App/services/NotesService'; -import { fetchList as fetchSlack } from 'Duck/integrations/slack'; -import { fetchList as fetchTeams } from 'Duck/integrations/teams'; import { addNote, updateNote } from 'Duck/sessions'; import { Button, Checkbox, Icon } from 'UI'; @@ -27,10 +24,6 @@ interface Props { sessionId: string; isEdit?: boolean; editNote?: WriteNote; - slackChannels: List>; - teamsChannels: List>; - fetchSlack: () => void; - fetchTeams: () => void; hideModal: () => void; } @@ -40,12 +33,13 @@ function CreateNote({ isEdit, editNote, updateNote, - slackChannels, - fetchSlack, - teamsChannels, - fetchTeams, hideModal, }: Props) { + const { notesStore, integrationsStore } = useStore(); + const slackChannels = integrationsStore.slack.list; + const fetchSlack = integrationsStore.slack.fetchIntegrations; + const teamsChannels = integrationsStore.msteams.list; + const fetchTeams = integrationsStore.msteams.fetchIntegrations; const [text, setText] = React.useState(''); const [slackChannel, setSlackChannel] = React.useState(''); const [teamsChannel, setTeamsChannel] = React.useState(''); @@ -56,7 +50,6 @@ function CreateNote({ const [useTeams, setTeams] = React.useState(false); const inputRef = React.createRef(); - const { notesStore } = useStore(); React.useEffect(() => { if (isEdit && editNote) { @@ -151,14 +144,12 @@ function CreateNote({ .map(({ webhookId, name }) => ({ value: webhookId, label: name, - })) - .toJS() as unknown as { value: string; label: string }[]; + })) as unknown as { value: string; label: string }[]; const teamsChannelsOptions = teamsChannels .map(({ webhookId, name }) => ({ value: webhookId, label: name, - })) - .toJS() as unknown as { value: string; label: string }[]; + })) as unknown as { value: string; label: string }[]; slackChannelsOptions.unshift({ // @ts-ignore @@ -334,10 +325,8 @@ function CreateNote({ export default connect( (state: any) => { - const slackChannels = state.getIn(['slack', 'list']); - const teamsChannels = state.getIn(['teams', 'list']); const sessionId = state.getIn(['sessions', 'current']).sessionId; - return { sessionId, slackChannels, teamsChannels }; + return { sessionId }; }, - { addNote, updateNote, fetchSlack, fetchTeams } -)(CreateNote); + { addNote, updateNote,} +)(observer(CreateNote)); diff --git a/frontend/app/components/Session_/Player/Overlay/AutoplayTimer.tsx b/frontend/app/components/Session_/Player/Overlay/AutoplayTimer.tsx index 144d28208..40cd92231 100644 --- a/frontend/app/components/Session_/Player/Overlay/AutoplayTimer.tsx +++ b/frontend/app/components/Session_/Player/Overlay/AutoplayTimer.tsx @@ -2,21 +2,22 @@ import React, { useEffect, useState } from 'react'; import cn from 'classnames'; import { connect } from 'react-redux'; import { withRouter, RouteComponentProps } from 'react-router-dom'; -import { Button, Link, Icon } from 'UI'; +import { Button, Link } from 'UI'; import { session as sessionRoute, withSiteId } from 'App/routes'; import stl from './AutoplayTimer.module.css'; import clsOv from './overlay.module.css'; import AutoplayToggle from 'Shared/AutoplayToggle'; +import { useStore } from 'App/mstore'; interface IProps extends RouteComponentProps { nextId: number; - siteId: string; } -function AutoplayTimer({ nextId, siteId, history }: IProps) { +function AutoplayTimer({ nextId, history }: IProps) { let timer: NodeJS.Timer; const [cancelled, setCancelled] = useState(false); const [counter, setCounter] = useState(5); + const { projectsStore } = useStore(); useEffect(() => { if (counter > 0) { @@ -26,6 +27,7 @@ function AutoplayTimer({ nextId, siteId, history }: IProps) { } if (counter === 0) { + const siteId = projectsStore.getSiteId().siteId; history.push(withSiteId(sessionRoute(nextId), siteId)); } @@ -70,7 +72,6 @@ function AutoplayTimer({ nextId, siteId, history }: IProps) { export default withRouter( connect((state: any) => ({ - siteId: state.getIn(['site', 'siteId']), nextId: state.getIn(['sessions', 'nextId']), }))(AutoplayTimer) ); diff --git a/frontend/app/components/Session_/QueueControls/QueueControls.tsx b/frontend/app/components/Session_/QueueControls/QueueControls.tsx index 975b0c6c3..b31a90685 100644 --- a/frontend/app/components/Session_/QueueControls/QueueControls.tsx +++ b/frontend/app/components/Session_/QueueControls/QueueControls.tsx @@ -12,7 +12,6 @@ import { useStore } from 'App/mstore'; const PER_PAGE = 10; interface Props extends RouteComponentProps { - siteId: string; previousId: string; nextId: string; defaultList: any; @@ -24,8 +23,8 @@ interface Props extends RouteComponentProps { } function QueueControls(props: Props) { + const { projectsStore } = useStore(); const { - siteId, previousId, nextId, currentPage, @@ -56,10 +55,12 @@ function QueueControls(props: Props) { }, []); const nextHandler = () => { + const siteId = projectsStore.getSiteId().siteId!; props.history.push(withSiteId(sessionRoute(nextId), siteId)); }; const prevHandler = () => { + const siteId = projectsStore.getSiteId().siteId!; props.history.push(withSiteId(sessionRoute(previousId), siteId)); }; @@ -108,7 +109,6 @@ export default connect( (state: any) => ({ previousId: state.getIn(['sessions', 'previousId']), nextId: state.getIn(['sessions', 'nextId']), - siteId: state.getIn(['site', 'siteId']), currentPage: state.getIn(['search', 'currentPage']) || 1, total: state.getIn(['sessions', 'total']) || 0, sessionIds: state.getIn(['sessions', 'sessionIds']) || [], diff --git a/frontend/app/components/Session_/ScreenRecorder/ScreenRecorder.tsx b/frontend/app/components/Session_/ScreenRecorder/ScreenRecorder.tsx index 10f4595ee..1cecb3ffc 100644 --- a/frontend/app/components/Session_/ScreenRecorder/ScreenRecorder.tsx +++ b/frontend/app/components/Session_/ScreenRecorder/ScreenRecorder.tsx @@ -11,6 +11,7 @@ import { formatTimeOrDate } from 'App/date'; import { PlayerContext, ILivePlayerContext } from 'App/components/Session/playerContext'; import { observer } from 'mobx-react-lite'; import { ENTERPRISE_REQUEIRED } from 'App/constants'; +import { useStore } from 'App/mstore'; /** * "edge" || "edg/" chromium based edge (dev or canary) @@ -32,16 +33,16 @@ const supportedBrowsers = ['Chrome v91+', 'Edge v90+']; const supportedMessage = `Supported Browsers: ${supportedBrowsers.join(', ')}`; function ScreenRecorder({ - siteId, sessionId, agentId, isEnterprise, }: { - siteId: string; sessionId: string; isEnterprise: boolean; agentId: number, }) { + const { projectsStore } = useStore(); + const siteId = projectsStore.siteId; const { player, store } = React.useContext(PlayerContext) as ILivePlayerContext; const recordingState = store.get().recordingState; @@ -144,7 +145,6 @@ function ScreenRecorder({ export default connect((state: any) => ({ isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee' || state.getIn(['user', 'account', 'edition']) === 'msaas', - siteId: state.getIn(['site', 'siteId']), sessionId: state.getIn(['sessions', 'current']).sessionId, agentId: state.getIn(['user', 'account', 'id']), }))(observer(ScreenRecorder)); diff --git a/frontend/app/components/Session_/Subheader.js b/frontend/app/components/Session_/Subheader.js index 970963140..65164ee94 100644 --- a/frontend/app/components/Session_/Subheader.js +++ b/frontend/app/components/Session_/Subheader.js @@ -1,34 +1,40 @@ +import { ShareAltOutlined } from '@ant-design/icons'; +import { Button as AntButton, Popover, Switch, Tooltip } from 'antd'; +import cn from 'classnames'; +import { Link2 } from 'lucide-react'; +import { observer } from 'mobx-react-lite'; import React, { useMemo } from 'react'; +import { connect } from 'react-redux'; + +import { PlayerContext } from 'App/components/Session/playerContext'; +import { IFRAME } from 'App/constants/storageKeys'; import { useStore } from 'App/mstore'; +import { checkParam, truncateStringToFit } from 'App/utils'; +import SessionTabs from 'Components/Session/Player/SharedComponents/SessionTabs'; 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'; import Issues from './Issues/Issues'; +import QueueControls from './QueueControls'; import NotePopup from './components/NotePopup'; -import { PlayerContext } from 'App/components/Session/playerContext'; -import { observer } from 'mobx-react-lite'; -import { connect } from 'react-redux'; -import SessionTabs from 'Components/Session/Player/SharedComponents/SessionTabs'; -import { IFRAME } from 'App/constants/storageKeys'; -import cn from 'classnames'; -import { Switch, Button as AntButton, Popover, Tooltip } from 'antd'; -import { ShareAltOutlined } from '@ant-design/icons'; -import { checkParam, truncateStringToFit } from 'App/utils'; const localhostWarn = (project) => project + '_localhost_warn'; const disableDevtools = 'or_devtools_uxt_toggle'; function SubHeader(props) { - const localhostWarnKey = localhostWarn(props.siteId); - const defaultLocalhostWarn = localStorage.getItem(localhostWarnKey) !== '1'; + const { uxtestingStore, projectsStore } = useStore(); + const defaultLocalhostWarn = React.useMemo(() => { + const siteId = projectsStore.siteId; + const localhostWarnKey = localhostWarn(siteId); + return localStorage.getItem(localhostWarnKey) !== '1'; + }, [projectsStore.siteId]); const [showWarningModal, setWarning] = React.useState(defaultLocalhostWarn); const { store } = React.useContext(PlayerContext); const { location: currentLocation = 'loading...' } = store.get(); const hasIframe = localStorage.getItem(IFRAME) === 'true'; - const { uxtestingStore } = useStore(); const [hideTools, setHideTools] = React.useState(false); React.useEffect(() => { const hideDevtools = checkParam('hideTools'); @@ -46,10 +52,15 @@ function SubHeader(props) { return integrations.some((i) => i.token); }, [props.integrations]); - const locationTruncated = truncateStringToFit(currentLocation, window.innerWidth - 200); + const locationTruncated = truncateStringToFit( + currentLocation, + window.innerWidth - 200 + ); const showWarning = - currentLocation && /(localhost)|(127.0.0.1)|(0.0.0.0)/.test(currentLocation) && showWarningModal; + currentLocation && + /(localhost)|(127.0.0.1)|(0.0.0.0)/.test(currentLocation) && + showWarningModal; const closeWarning = () => { localStorage.setItem(localhostWarnKey, '1'); setWarning(false); @@ -65,7 +76,11 @@ function SubHeader(props) {

    {showWarning ? ( @@ -77,7 +92,7 @@ function SubHeader(props) { left: '50%', bottom: '-24px', transform: 'translate(-50%, 0)', - fontWeight: 500 + fontWeight: 500, }} > Some assets may load incorrectly on localhost. @@ -114,7 +129,10 @@ function SubHeader(props) { trigger={
    - + @@ -141,8 +159,8 @@ function SubHeader(props) { {locationTruncated && (
    - - + + {locationTruncated} @@ -155,7 +173,6 @@ function SubHeader(props) { } export default connect((state) => ({ - siteId: state.getIn(['site', 'siteId']), integrations: state.getIn(['issues', 'list']), - modules: state.getIn(['user', 'account', 'modules']) || [] + modules: state.getIn(['user', 'account', 'modules']) || [], }))(observer(SubHeader)); diff --git a/frontend/app/components/UsabilityTesting/TestOverview.tsx b/frontend/app/components/UsabilityTesting/TestOverview.tsx index fa7002aba..2dd8cc565 100644 --- a/frontend/app/components/UsabilityTesting/TestOverview.tsx +++ b/frontend/app/components/UsabilityTesting/TestOverview.tsx @@ -107,7 +107,7 @@ function TestOverview() { }, [testId, siteId]); if (!uxtestingStore.instance) { - return No Data; + return Loading Data...; } else { document.title = `Usability Tests | ${uxtestingStore.instance.title}`; } diff --git a/frontend/app/components/hocs/withReport.tsx b/frontend/app/components/hocs/withReport.tsx index ce33182a1..d7a2a7073 100644 --- a/frontend/app/components/hocs/withReport.tsx +++ b/frontend/app/components/hocs/withReport.tsx @@ -1,8 +1,7 @@ import React, { useEffect } from 'react'; import { convertElementToImage } from 'App/utils'; import { useStore } from 'App/mstore'; -import { useObserver } from 'mobx-react-lite'; -import { connect } from 'react-redux'; +import { observer } from 'mobx-react-lite'; import { fileNameFormat } from 'App/utils'; import { toast } from 'react-toastify'; import { forceVisible } from 'react-lazyload'; @@ -15,11 +14,11 @@ interface Props { export default function withReport

    (WrappedComponent: React.ComponentType

    ) { const ComponentWithReport = (props: P) => { const [rendering, setRendering] = React.useState(false); - const { site } = props; - const { dashboardStore } = useStore(); - const dashboard: any = useObserver(() => dashboardStore.selectedDashboard); - const period = useObserver(() => dashboardStore.period); - const pendingRequests = useObserver(() => dashboardStore.pendingRequests); + const { dashboardStore, projectsStore } = useStore(); + const site = projectsStore.instance; + const dashboard: any = dashboardStore.selectedDashboard; + const period = dashboardStore.period; + const pendingRequests = dashboardStore.pendingRequests; useEffect(() => { if (rendering && pendingRequests <= 0) { @@ -181,7 +180,5 @@ export default function withReport

    (WrappedComponent: React.Comp ); }; - return connect((state: any) => ({ - site: state.getIn(['site', 'instance']), - }))(ComponentWithReport); + return observer(ComponentWithReport); } 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/ErrorsBadge/ErrorsBadge.js b/frontend/app/components/shared/ErrorsBadge/ErrorsBadge.js index d8d4ed323..6706608c5 100644 --- a/frontend/app/components/shared/ErrorsBadge/ErrorsBadge.js +++ b/frontend/app/components/shared/ErrorsBadge/ErrorsBadge.js @@ -1,6 +1,5 @@ import { observer } from 'mobx-react-lite'; import React, { useEffect } from 'react'; -import { connect } from 'react-redux'; import { DATE_RANGE_VALUES, getDateRangeFromValue } from 'App/dateRange'; import { useStore } from 'App/mstore'; @@ -11,8 +10,9 @@ const AUTOREFRESH_INTERVAL = 5 * 60 * 1000; const weekRange = getDateRangeFromValue(DATE_RANGE_VALUES.LAST_7_DAYS); let intervalId = null; -function ErrorsBadge({ projects }) { - const { errorsStore } = useStore(); +function ErrorsBadge() { + const { errorsStore, projectsStore } = useStore(); + const projects = projectsStore.list; const errorsStats = errorsStore.stats; useEffect(() => { if (projects.size === 0 || !!intervalId) return; @@ -21,20 +21,16 @@ function ErrorsBadge({ projects }) { startTimestamp: weekRange.start.ts, endTimestamp: weekRange.end.ts, }; - errorsStore.fetchNewErrorsCount(params); + void errorsStore.fetchNewErrorsCount(params); intervalId = setInterval(() => { - errorsStore.fetchNewErrorsCount(params); + void errorsStore.fetchNewErrorsCount(params); }, AUTOREFRESH_INTERVAL); }, [projects]); return errorsStats.unresolvedAndUnviewed > 0 ? (

    {
    }
    - ) : ( - '' - ); + ) : null; } -export default connect((state) => ({ - projects: state.getIn(['site', 'list']), -}))(observer(ErrorsBadge)); +export default observer(ErrorsBadge); diff --git a/frontend/app/components/shared/Filters/FilterSelection/FilterSelection.tsx b/frontend/app/components/shared/Filters/FilterSelection/FilterSelection.tsx index b8de307bd..fdac13eae 100644 --- a/frontend/app/components/shared/Filters/FilterSelection/FilterSelection.tsx +++ b/frontend/app/components/shared/Filters/FilterSelection/FilterSelection.tsx @@ -14,7 +14,6 @@ interface Props { filterListLive: any; onFilterClick: (filter: any) => void; children?: any; - isLive?: boolean; excludeFilterKeys?: Array; allowedFilterKeys?: Array; disabled?: boolean; @@ -81,7 +80,6 @@ export default connect( (state: any) => ({ filterList: state.getIn(['search', 'filterList']), filterListLive: state.getIn(['search', 'filterListLive']), - isLive: state.getIn(['sessions', 'activeTab']).type === 'live' }), {} )(FilterSelection); diff --git a/frontend/app/components/shared/GettingStarted/StepList.tsx b/frontend/app/components/shared/GettingStarted/StepList.tsx index 485990434..178cb4aa1 100644 --- a/frontend/app/components/shared/GettingStarted/StepList.tsx +++ b/frontend/app/components/shared/GettingStarted/StepList.tsx @@ -5,7 +5,6 @@ import { Step } from 'App/mstore/types/gettingStarted'; import { useStore } from 'App/mstore'; import { onboarding as onboardingRoute, withSiteId } from 'App/routes'; import { RouteComponentProps, withRouter } from 'react-router'; -import { connect } from 'react-redux'; import { useModal } from 'App/components/Modal'; interface StepListProps extends RouteComponentProps { @@ -13,7 +12,6 @@ interface StepListProps extends RouteComponentProps { steps: Step[]; status: 'pending' | 'completed'; docsLink?: string; - siteId: string; } const StepItem = React.memo( @@ -63,11 +61,12 @@ const StepItem = React.memo( ); const StepList = React.memo((props: StepListProps) => { - const { title, steps, status } = props; + const { title, steps } = props; const { hideModal } = useModal(); const { settingsStore: { gettingStarted }, + projectsStore, } = useStore(); const onIgnore = (e: React.MouseEvent, step: any) => { @@ -80,7 +79,8 @@ const StepList = React.memo((props: StepListProps) => { } const onClick = (step: any) => { - const { siteId, history } = props; + const { history } = props; + const siteId = projectsStore.getSiteId().siteId!; hideModal(); history.push(withSiteId(onboardingRoute(step.url), siteId)); }; @@ -97,6 +97,4 @@ const StepList = React.memo((props: StepListProps) => { ); }); -export default connect((state: any) => ({ - siteId: state.getIn(['site', 'siteId']), -}))(withRouter(StepList)); +export default withRouter(StepList); diff --git a/frontend/app/components/shared/LiveSessionSearch/LiveSessionSearch.tsx b/frontend/app/components/shared/LiveSessionSearch/LiveSessionSearch.tsx index 289a05ac0..58ee93950 100644 --- a/frontend/app/components/shared/LiveSessionSearch/LiveSessionSearch.tsx +++ b/frontend/app/components/shared/LiveSessionSearch/LiveSessionSearch.tsx @@ -4,15 +4,18 @@ import FilterSelection from 'Shared/Filters/FilterSelection'; import { connect } from 'react-redux'; import { Button } from 'UI'; import { edit, addFilter } from 'Duck/liveSearch'; +import { observer } from 'mobx-react-lite'; +import { useStore } from 'App/mstore'; interface Props { appliedFilter: any; edit: typeof edit; addFilter: typeof addFilter; - saveRequestPayloads: boolean; } function LiveSessionSearch(props: Props) { - const { appliedFilter, saveRequestPayloads = false } = props; + const { appliedFilter } = props; + const { projectsStore } = useStore(); + const saveRequestPayloads = projectsStore.active?.saveRequestPayloads const hasEvents = appliedFilter.filters.filter(i => i.isEvent).size > 0; const hasFilters = appliedFilter.filters.filter(i => !i.isEvent).size > 0; @@ -89,6 +92,5 @@ function LiveSessionSearch(props: Props) { } export default connect(state => ({ - saveRequestPayloads: state.getIn(['site', 'active', 'saveRequestPayloads']), appliedFilter: state.getIn([ 'liveSearch', 'instance' ]), -}), { edit, addFilter })(LiveSessionSearch); \ No newline at end of file +}), { edit, addFilter })(observer(LiveSessionSearch)); \ No newline at end of file diff --git a/frontend/app/components/shared/MainSearchBar/MainSearchBar.tsx b/frontend/app/components/shared/MainSearchBar/MainSearchBar.tsx index feef4d38a..7e7750c6b 100644 --- a/frontend/app/components/shared/MainSearchBar/MainSearchBar.tsx +++ b/frontend/app/components/shared/MainSearchBar/MainSearchBar.tsx @@ -9,15 +9,15 @@ import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; interface Props { - site: any; + } const MainSearchBar = (props: Props) => { - const { site } = props; - const { searchStore } = useStore(); + const { searchStore, projectsStore } = useStore(); const appliedFilter = searchStore.instance; const savedSearch = searchStore.savedSearch; - const currSite = React.useRef(site); + const projectId = projectsStore.siteId; + const currSite = React.useRef(projectId); const hasFilters = appliedFilter && appliedFilter.filters && appliedFilter.filters.size > 0; const hasSavedSearch = savedSearch && savedSearch.exists(); const hasSearch = hasFilters || hasSavedSearch; @@ -27,12 +27,12 @@ const MainSearchBar = (props: Props) => { const isSaas = /app\.openreplay\.com/.test(originStr); React.useEffect(() => { - if (site !== currSite.current && currSite.current !== undefined) { + if (projectId !== currSite.current && currSite.current !== undefined) { console.debug('clearing filters due to project change'); searchStore.clearSearch(); - currSite.current = site; + currSite.current = projectId; } - }, [site]); + }, [projectId]); return (
    @@ -56,8 +56,4 @@ const MainSearchBar = (props: Props) => { ); }; -export default connect( - (state: any) => ({ - site: state.getIn(['site', 'siteId']) - }) -)(observer(MainSearchBar)); +export default observer(MainSearchBar); diff --git a/frontend/app/components/shared/NoSessionsMessage/NoSessionsMessage.js b/frontend/app/components/shared/NoSessionsMessage/NoSessionsMessage.js index 1a8e271be..cd8b36203 100644 --- a/frontend/app/components/shared/NoSessionsMessage/NoSessionsMessage.js +++ b/frontend/app/components/shared/NoSessionsMessage/NoSessionsMessage.js @@ -1,23 +1,19 @@ import React from 'react'; import { Alert, Space, Button } from 'antd'; -import { connect } from 'react-redux'; +import { observer } from 'mobx-react-lite' +import { useStore } from "App/mstore"; import { onboarding as onboardingRoute } from 'App/routes'; import { withRouter } from 'react-router-dom'; import * as routes from '../../../routes'; -import { indigo } from 'tailwindcss/colors'; import { SquareArrowOutUpRight } from 'lucide-react'; import { useHistory } from 'react-router'; - const withSiteId = routes.withSiteId; -const indigoWithOpacity = `rgba(${parseInt(indigo[500].slice(1, 3), 16)}, ${parseInt(indigo[500].slice(3, 5), 16)}, ${parseInt(indigo[500].slice(5, 7), 16)}, 0.1)`; // 0.5 is the opacity level - -const NoSessionsMessage = (props) => { - const { - sites, - siteId - } = props; +const NoSessionsMessage = () => { + const { projectsStore } = useStore(); + const sites = projectsStore.list; + const siteId = projectsStore.siteId; const history = useHistory(); const activeSite = sites.find((s) => s.id === siteId); const showNoSessions = !!activeSite && !activeSite.recorded; @@ -60,7 +56,4 @@ const NoSessionsMessage = (props) => { ); }; -export default connect((state) => ({ - site: state.getIn(['site', 'siteId']), - sites: state.getIn(['site', 'list']) -}))(withRouter(NoSessionsMessage)); +export default withRouter(observer(NoSessionsMessage)); diff --git a/frontend/app/components/shared/ProjectDropdown/ProjectDropdown.tsx b/frontend/app/components/shared/ProjectDropdown/ProjectDropdown.tsx index 989ce1d1f..e15671a83 100644 --- a/frontend/app/components/shared/ProjectDropdown/ProjectDropdown.tsx +++ b/frontend/app/components/shared/ProjectDropdown/ProjectDropdown.tsx @@ -9,12 +9,11 @@ import { connect } from 'react-redux'; import { RouteComponentProps, withRouter } from 'react-router-dom'; import { useStore, withStore } 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'; import { clearSearch as clearSearchLive } from 'Duck/liveSearch'; -import { setSiteId } from 'Duck/site'; -import { init as initProject } from 'Duck/site'; import { Icon } from 'UI'; const { Text } = Typography; @@ -26,17 +25,18 @@ interface Site { } interface Props extends RouteComponentProps { - sites: Site[]; - siteId: string; - setSiteId: (siteId: string) => 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 = @@ -45,22 +45,21 @@ function ProjectDropdown(props: Props) { const { customFieldStore, searchStore } = 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); // searchStore.clearSearch(location.pathname.includes('/sessions')); searchStore.clearSearch(); 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, clearSearchLive, - initProject - })(withStore(ProjectDropdown)) + })(observer(ProjectDropdown)) ); diff --git a/frontend/app/components/shared/SessionItem/PlayLink/PlayLink.tsx b/frontend/app/components/shared/SessionItem/PlayLink/PlayLink.tsx index 8c0543c5f..deab8d8a4 100644 --- a/frontend/app/components/shared/SessionItem/PlayLink/PlayLink.tsx +++ b/frontend/app/components/shared/SessionItem/PlayLink/PlayLink.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useState } from 'react'; -import { connect } from 'react-redux'; import { useHistory } from 'react-router'; import { @@ -8,6 +7,7 @@ import { withSiteId, } from 'App/routes'; import { Icon, Link } from 'UI'; +import { useStore } from 'App/mstore'; const PLAY_ICON_NAMES = { notPlayed: 'play-fill', @@ -30,6 +30,7 @@ interface Props { siteId?: string; } function PlayLink(props: Props) { + const { projectsStore } = useStore(); const { isAssist, viewed, sessionId, onClick = null, queryParams } = props; const history = useHistory(); const defaultIconName = getDefaultIconName(viewed); @@ -47,9 +48,10 @@ function PlayLink(props: Props) { : sessionRoute(sessionId); const handleBeforeOpen = (e: any) => { + const projectId = props.siteId ?? projectsStore.getSiteId().siteId!; const replayLink = withSiteId( link + (props.query ? props.query : ''), - props.siteId + projectId ); if (props.beforeOpen) { // check for ctrl or shift @@ -86,6 +88,4 @@ function PlayLink(props: Props) { ); } -export default connect((state: any, props: Props) => ({ - siteId: props.siteId || state.getIn(['site', 'siteId']), -}))(PlayLink); +export default PlayLink diff --git a/frontend/app/components/shared/SessionItem/SessionItem.tsx b/frontend/app/components/shared/SessionItem/SessionItem.tsx index 9449bc5b1..acceae4ed 100644 --- a/frontend/app/components/shared/SessionItem/SessionItem.tsx +++ b/frontend/app/components/shared/SessionItem/SessionItem.tsx @@ -2,11 +2,9 @@ import cn from 'classnames'; import { Duration } from 'luxon'; import { observer } from 'mobx-react-lite'; import React from 'react'; -import { connect } from 'react-redux'; import { RouteComponentProps, withRouter } from 'react-router-dom'; import { durationFormatted, formatTimeOrDate } from 'App/date'; -import { presetSession } from 'App/duck/sessions'; import { useStore } from 'App/mstore'; import { assist as assistRoute, @@ -79,7 +77,6 @@ interface Props { bookmarked?: boolean; toggleFavorite?: (sessionId: string) => void; query?: string; - presetSession?: typeof presetSession; } const PREFETCH_STATE = { @@ -105,7 +102,6 @@ function SessionItem(props: RouteComponentProps & Props) { ignoreAssist = false, bookmarked = false, query, - presetSession, } = props; const { @@ -176,8 +172,11 @@ function SessionItem(props: RouteComponentProps & Props) { || isAssist || prefetchState === PREFETCH_STATE.none || isMobile - ) return - presetSession?.(session); + ) { + return; + } + + sessionStore.prefetchSession(session); }; return ( i.isEvent).length > 0; const hasFilters = appliedFilter.filters.filter((i: any) => !i.isEvent).length > 0; - console.log('appliedFilter', appliedFilter) + const saveRequestPayloads = projectsStore.instance?.saveRequestPayloads ?? false useSessionSearchQueryHandler({ appliedFilter, @@ -148,8 +146,4 @@ function SessionSearch(props: Props) { ) : null; } -export default connect( - (state: any) => ({ - saveRequestPayloads: state.getIn(['site', 'instance', 'saveRequestPayloads']) - }) -)(observer(SessionSearch)); +export default observer(SessionSearch); diff --git a/frontend/app/components/shared/SessionSettings/SessionSettings.tsx b/frontend/app/components/shared/SessionSettings/SessionSettings.tsx index 787ced443..c0298e9ce 100644 --- a/frontend/app/components/shared/SessionSettings/SessionSettings.tsx +++ b/frontend/app/components/shared/SessionSettings/SessionSettings.tsx @@ -3,9 +3,12 @@ import ListingVisibility from './components/ListingVisibility'; import DefaultPlaying from './components/DefaultPlaying'; import DefaultTimezone from './components/DefaultTimezone'; import CaptureRate from './components/CaptureRate'; -import { connect } from 'react-redux'; +import { observer } from 'mobx-react-lite'; +import { useStore } from 'App/mstore'; -function SessionSettings({ projectId }: { projectId: number }) { +function SessionSettings() { + const { projectsStore } = useStore(); + const projectId = projectsStore.siteId; return (
    @@ -32,6 +35,4 @@ function SessionSettings({ projectId }: { projectId: number }) { ); } -export default connect((state: any) => ({ - projectId: state.getIn(['site', 'siteId']) -}))(SessionSettings); +export default observer(SessionSettings) diff --git a/frontend/app/components/shared/SessionSettings/components/CaptureRate.tsx b/frontend/app/components/shared/SessionSettings/components/CaptureRate.tsx index 215a2d26b..232733a91 100644 --- a/frontend/app/components/shared/SessionSettings/components/CaptureRate.tsx +++ b/frontend/app/components/shared/SessionSettings/components/CaptureRate.tsx @@ -11,7 +11,7 @@ import ConditionalRecordingSettings from 'Shared/SessionSettings/components/Cond type Props = { isAdmin: boolean; isEnterprise?: boolean; - projectId?: number; + projectId?: string; setShowCaptureRate: (show: boolean) => void; open: boolean; showCaptureRate: boolean; diff --git a/frontend/app/components/shared/SessionsTabOverview/components/SessionList/SessionList.tsx b/frontend/app/components/shared/SessionsTabOverview/components/SessionList/SessionList.tsx index 2bdb91347..66c54d244 100644 --- a/frontend/app/components/shared/SessionsTabOverview/components/SessionList/SessionList.tsx +++ b/frontend/app/components/shared/SessionsTabOverview/components/SessionList/SessionList.tsx @@ -6,11 +6,10 @@ import { NoContent, Loader, Pagination, Button } from 'UI'; import { withRouter, RouteComponentProps } from 'react-router-dom'; import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; import { numberWithCommas } from 'App/utils'; -import { toggleFavorite } from 'Duck/sessions'; import SessionDateRange from './SessionDateRange'; import RecordingStatus from 'Shared/SessionsTabOverview/components/RecordingStatus'; import { sessionService } from 'App/services'; -import { updateProjectRecordingStatus } from 'Duck/site'; +import { observer } from 'mobx-react-lite'; import { useStore } from 'App/mstore'; enum NoContentType { @@ -31,37 +30,25 @@ let sessionStatusTimeOut: any = null; const STATUS_FREQUENCY = 5000; interface Props extends RouteComponentProps { - loading: boolean; - list: any; - currentPage: number; - pageSize: number; - total: number; - filters: any; - lastPlayedSessionId: string; - metaList: any; - scrollY: number; - updateProjectRecordingStatus: (siteId: string, status: boolean) => void; - activeTab: any; isEnterprise?: boolean; - toggleFavorite: (sessionId: string) => Promise; - sites: object[]; isLoggedIn: boolean; - siteId: string; } function SessionList(props: Props) { + const { projectsStore, sessionStore, customFieldStore } = useStore(); + const list = sessionStore.list; + const lastPlayedSessionId = sessionStore.lastPlayedSessionId; + const loading = sessionStore.loadingSessions; + const total = sessionStore.total; + const onToggleFavorite = sessionStore.toggleFavorite; + const sites = projectsStore.list; + const siteId = projectsStore.siteId; + const updateProjectRecordingStatus = projectsStore.updateProjectRecordingStatus; const [noContentType, setNoContentType] = React.useState(NoContentType.ToDate); const { searchStore } = useStore(); const { - loading, - list, - total, - lastPlayedSessionId, - metaList, isEnterprise = false, - sites, isLoggedIn, - siteId } = props; const { currentPage, scrollY, activeTab, pageSize } = searchStore; const { filters } = searchStore.instance; @@ -72,6 +59,7 @@ function SessionList(props: Props) { const isVault = isBookmark && isEnterprise; const activeSite: any = sites.find((s: any) => s.id === siteId); const hasNoRecordings = !activeSite || !activeSite.recorded; + const metaList = customFieldStore.list; const NO_CONTENT = React.useMemo(() => { @@ -127,7 +115,7 @@ function SessionList(props: Props) { } if (statusData.status === 2 && activeSite) { // recording && processed - props.updateProjectRecordingStatus(activeSite.id, true); + updateProjectRecordingStatus(activeSite.id, true); searchStore.fetchSessions(true); clearInterval(sessionStatusTimeOut); } @@ -144,7 +132,7 @@ function SessionList(props: Props) { useEffect(() => { // handle scroll position - const { scrollY } = props; + const { scrollY } = searchStore; window.scrollTo(0, scrollY); if (total === 0 && !loading && !hasNoRecordings) { @@ -187,7 +175,7 @@ function SessionList(props: Props) { }; const toggleFavorite = (sessionId: string) => { - props.toggleFavorite(sessionId).then(() => { + onToggleFavorite(sessionId).then(() => { searchStore.fetchSessions(true); }); }; @@ -270,18 +258,6 @@ function SessionList(props: Props) { export default connect( (state: any) => ({ - list: state.getIn(['sessions', 'list']), - lastPlayedSessionId: state.getIn(['sessions', 'lastPlayedSessionId']), - metaList: state.getIn(['customFields', 'list']).map((i: any) => i.key), - loading: state.getIn(['sessions', 'loading']), - total: state.getIn(['sessions', 'total']) || 0, isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee', - siteId: state.getIn(['site', 'siteId']), - sites: state.getIn(['site', 'list']), - isLoggedIn: Boolean(state.getIn(['user', 'jwt'])) - }), - { - toggleFavorite, - updateProjectRecordingStatus - } -)(withRouter(SessionList)); + isLoggedIn: Boolean(state.getIn(['user', 'jwt'])), + }))(withRouter(observer(SessionList))); diff --git a/frontend/app/components/shared/SessionsTabOverview/components/SessionSort/SessionSort.tsx b/frontend/app/components/shared/SessionsTabOverview/components/SessionSort/SessionSort.tsx index acb5d5ce4..4984035a1 100644 --- a/frontend/app/components/shared/SessionsTabOverview/components/SessionSort/SessionSort.tsx +++ b/frontend/app/components/shared/SessionsTabOverview/components/SessionSort/SessionSort.tsx @@ -1,8 +1,7 @@ import { DownOutlined } from '@ant-design/icons'; import { Dropdown } from 'antd'; import React from 'react'; -import { connect } from 'react-redux'; -import { sort } from 'Duck/sessions'; +import { observer } from 'mobx-react-lite'; import { useStore } from 'App/mstore'; const sortOptionsMap = { @@ -30,7 +29,6 @@ export function SortDropdown({ defaultOption, onSort, sortOptions, current }: sortOptions: any, current: string }) { - return ( ({ defaultOption, onSort, sortOptions, current }: } function SessionSort(props: Props) { - const { searchStore } = useStore(); + const { searchStore, sessionStore } = useStore(); + const onSessionSort = sessionStore.sortSessions; const { sort, order } = searchStore.instance; const onSort = ({ key }: { key: string }) => { const [sort, order] = key.split('-'); const sign = order === 'desc' ? -1 : 1; searchStore.applyFilter({ order, sort }); - props.sort(sort, sign); + onSessionSort(sort, sign); }; const defaultOption = `${sort}-${order}`; @@ -74,9 +73,4 @@ function SessionSort(props: Props) { ); } -export default connect( - (state: any) => ({ - // filter: state.getIn(['search', 'instance']) - }), - { sort } -)(SessionSort); +export default observer(SessionSort); diff --git a/frontend/app/components/shared/SessionsTabOverview/components/SessionTags/SessionTags.tsx b/frontend/app/components/shared/SessionsTabOverview/components/SessionTags/SessionTags.tsx index 2d44f6b8c..8fcf8078c 100644 --- a/frontend/app/components/shared/SessionsTabOverview/components/SessionTags/SessionTags.tsx +++ b/frontend/app/components/shared/SessionsTabOverview/components/SessionTags/SessionTags.tsx @@ -2,10 +2,14 @@ import { issues_types, types } from 'Types/session/issue'; import { Segmented } from 'antd'; import cn from 'classnames'; import { Angry, CircleAlert, Skull, WifiOff } from 'lucide-react'; +import { observer } from 'mobx-react-lite'; import React, { memo } from 'react'; import { connect } from 'react-redux'; -import { Icon } from 'UI'; +import { bindActionCreators } from 'redux'; + import { useStore } from 'App/mstore'; +import { setActiveTab } from 'Duck/search'; +import { Icon } from 'UI'; interface Tag { name: string; @@ -14,7 +18,6 @@ interface Tag { } interface StateProps { - tags: Tag[]; total: number; } @@ -25,57 +28,63 @@ const tagIcons = { [types.JS_EXCEPTION]: , [types.BAD_REQUEST]: , [types.CLICK_RAGE]: , - [types.CRASH]: + [types.CRASH]: , } as Record; -const SessionTags: React.FC = memo( - ({ tags, total }) => { - const { searchStore } = useStore(); - const disable = searchStore.activeTab.type === 'all' && total === 0; - const activeTab = searchStore.activeTab; +const SessionTags: React.FC = ({ total }) => { + const { projectsStore, searchStore } = useStore(); + const platform = projectsStore.active?.platform || ''; + const disable = searchStore.activeTab.type === 'all' && total === 0; + const activeTab = searchStore.activeTab; + const tags = issues_types.filter( + (tag) => + tag.type !== 'mouse_thrashing' && + (platform === 'web' + ? tag.type !== types.TAP_RAGE + : tag.type !== types.CLICK_RAGE) + ); - const options = tags.map((tag, i) => ({ - label: ( -
    - {tag.icon ? ( - tagIcons[tag.type] ? ( - tagIcons[tag.type] - ) : ( - - ) - ) : null} -
    - {tag.name} -
    + const options = tags.map((tag, i) => ({ + label: ( +
    + {tag.icon ? ( + tagIcons[tag.type] ? ( + tagIcons[tag.type] + ) : ( + + ) + ) : null} +
    + {tag.name}
    - ), - value: tag.type, - disabled: disable && tag.type !== 'all' - })); - - const onPick = (tabValue: string) => { - const tab = tags.find((t) => t.type === tabValue); - if (tab) { - searchStore.setActiveTab(tab); - } - }; - return ( -
    -
    - ); - } -); + ), + value: tag.type, + disabled: disable && tag.type !== 'all', + })); + + const onPick = (tabValue: string) => { + const tab = tags.find((t) => t.type === tabValue); + if (tab) { + searchStore.setActiveTab(tab); + } + }; + return ( +
    + +
    + ); +}; // Separate the TagItem into its own memoized component. export const TagItem: React.FC<{ @@ -109,17 +118,10 @@ export const TagItem: React.FC<{ )); const mapStateToProps = (state: any): StateProps => { - const platform = state.getIn(['site', 'active'])?.platform || ''; - const filteredTags = issues_types.filter( - (tag) => - tag.type !== 'mouse_thrashing' && - (platform === 'web' - ? tag.type !== types.TAP_RAGE - : tag.type !== types.CLICK_RAGE) - ); const total = state.getIn(['sessions', 'total']) || 0; - - return { tags: filteredTags, total }; + return { total }; }; -export default connect(mapStateToProps)(SessionTags); +export default connect( + mapStateToProps +)(observer(SessionTags)); diff --git a/frontend/app/components/shared/SharePopup/SharePopup.tsx b/frontend/app/components/shared/SharePopup/SharePopup.tsx index ce7955924..66b61ef81 100644 --- a/frontend/app/components/shared/SharePopup/SharePopup.tsx +++ b/frontend/app/components/shared/SharePopup/SharePopup.tsx @@ -7,11 +7,10 @@ import styles from './sharePopup.module.css'; import IntegrateSlackButton from '../IntegrateSlackButton/IntegrateSlackButton'; import SessionCopyLink from './SessionCopyLink'; import Select from 'Shared/Select'; -import { fetchList as fetchSlack, sendSlackMsg } from 'Duck/integrations/slack'; -import { fetchList as fetchTeams, sendMsTeamsMsg } from 'Duck/integrations/teams'; import { Button, Segmented } from 'antd'; import { PlayerContext } from 'App/components/Session/playerContext'; import { observer } from 'mobx-react-lite'; +import { useStore } from 'App/mstore'; interface Msg { integrationId: string; @@ -51,15 +50,7 @@ const SharePopup = ({ interface Props { sessionId: string; - channels: { webhookId: string; name: string }[]; - slackLoaded: boolean; - msTeamsChannels: { webhookId: string; name: string }[]; - msTeamsLoaded: boolean; tenantId: string; - fetchSlack: () => void; - fetchTeams: () => void; - sendSlackMsg: (msg: Msg) => any; - sendMsTeamsMsg: (msg: Msg) => any; showCopyLink?: boolean; hideModal: () => void; time: number; @@ -67,18 +58,20 @@ interface Props { function ShareModalComp({ sessionId, - sendSlackMsg, - sendMsTeamsMsg, showCopyLink, - channels, - slackLoaded, - msTeamsChannels, - msTeamsLoaded, - fetchSlack, - fetchTeams, hideModal, time, }: Props) { + const { integrationsStore } = useStore(); + const channels = integrationsStore.slack.list; + const slackLoaded = integrationsStore.slack.loaded; + const msTeamsChannels = integrationsStore.msteams.list; + const msTeamsLoaded = integrationsStore.msteams.loaded; + const fetchSlack = integrationsStore.slack.fetchIntegrations; + const fetchTeams = integrationsStore.msteams.fetchIntegrations; + const sendSlackMsg = integrationsStore.slack.sendMessage; + const sendMsTeamsMsg = integrationsStore.msteams.sendMessage; + const [shareTo, setShareTo] = useState('slack'); const [comment, setComment] = useState(''); // @ts-ignore @@ -104,7 +97,7 @@ function ShareModalComp({ const editMessage = (e: React.ChangeEvent) => setComment(e.target.value); const shareToSlack = () => { setLoadingSlack(true); - sendSlackMsg({ + void sendSlackMsg({ integrationId: channelId, entity: 'sessions', entityId: sessionId, @@ -140,16 +133,12 @@ function ShareModalComp({ value: webhookId, label: name, })) - // @ts-ignore - .toJS(); const msTeamsOptions = msTeamsChannels .map(({ webhookId, name }) => ({ value: webhookId, label: name, })) - // @ts-ignore - .toJS(); const sendMsg = () => { if (shareTo === 'slack') { @@ -279,18 +268,9 @@ function ShareModalComp({ const mapStateToProps = (state: Record) => ({ sessionId: state.getIn(['sessions', 'current']).sessionId, - channels: state.getIn(['slack', 'list']), - slackLoaded: state.getIn(['slack', 'loaded']), - msTeamsChannels: state.getIn(['teams', 'list']), - msTeamsLoaded: state.getIn(['teams', 'loaded']), tenantId: state.getIn(['user', 'account', 'tenantId']), }); -const ShareModal = connect(mapStateToProps, { - fetchSlack, - fetchTeams, - sendSlackMsg, - sendMsTeamsMsg, -})(ShareModalComp); +const ShareModal = connect(mapStateToProps)(ShareModalComp); export default observer(SharePopup); diff --git a/frontend/app/components/shared/SiteDropdown/SiteDropdown.js b/frontend/app/components/shared/SiteDropdown/SiteDropdown.js index 44ebebd57..0b3916c32 100644 --- a/frontend/app/components/shared/SiteDropdown/SiteDropdown.js +++ b/frontend/app/components/shared/SiteDropdown/SiteDropdown.js @@ -1,9 +1,12 @@ import React from 'react'; -import { connect } from 'react-redux'; +import { observer } from 'mobx-react-lite' +import { useStore } from 'App/mstore' import Select from 'Shared/Select'; -const SiteDropdown = ({ contextName = '', sites, onChange, value }) => { - const options = sites.map(site => ({ value: site.id, label: site.host })).toJS(); +const SiteDropdown = ({ contextName = '', onChange, value }) => { + const { projectsStore } = useStore(); + const sites = projectsStore.list; + const options = sites.map(site => ({ value: site.id, label: site.host })); return (