Merge remote-tracking branch 'origin/redux-toolkit-move' into rtm-temp

This commit is contained in:
Shekar Siri 2024-09-19 19:06:53 +05:30
commit 13f24ea6f4
116 changed files with 2473 additions and 2783 deletions

View file

@ -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);
}))(observer(IFrameRoutes));

View file

@ -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<string, any>;
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 (
<Suspense fallback={<Loader loading={true} className="flex-1" />}>
<Switch key="content">
@ -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));

View file

@ -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<typeof connector> {
isLoggedIn: boolean;
sites: Map<string, any>;
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<RouterProps> = (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<RouterProps> = (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<RouterProps> = (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<RouterProps> = (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<any>(null);
@ -229,7 +226,6 @@ const Router: React.FC<RouterProps> = (props) => {
};
const mapStateToProps = (state: Map<string, any>) => {
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<string, any>) => {
'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<string, any>) => {
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)));

View file

@ -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<string> | 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<string, any>): 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;
}

View file

@ -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 <span className="color-gray-medium">Live Sessions</span>{' '}
</div>
</div>
<Loader loading={props.loading}>
<Loader loading={loading}>
<NoContent
show={!props.loading && props.list.length === 0}
show={!loading && list.length === 0}
title={
<div className="flex items-center justify-center flex-col">
<AnimatedSVG name={ICONS.NO_LIVE_SESSIONS} size={60} />
@ -45,7 +49,7 @@ function SessionList(props: Props) {
}
>
<div className="p-4">
{props.list.map((session: any) => (
{list.map((session: any) => (
<div className="mb-6" key={session.sessionId}>
{session.pageTitle && session.pageTitle !== '' && (
<div className="flex items-center mb-2">
@ -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);

View file

@ -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<CustomFieldsProps> = (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<number | null>(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<CustomFieldsProps> = (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<CustomFieldsProps> = (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<CustomFieldsProps> = (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));

View file

@ -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);

View file

@ -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) {

View file

@ -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) => (
<div className='bg-white h-screen overflow-y-auto' style={{ width: '350px' }}>
<IntegrationModalCard title='Cloud Watch' icon='integrations/aws'
description='Integrate CloudWatch to see backend logs and errors alongside session replay.' />
<div className='p-5 border-b mb-4'>
<div className='font-medium mb-1'>How it works?</div>
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
<IntegrationModalCard
title="Cloud Watch"
icon="integrations/aws"
description="Integrate CloudWatch to see backend logs and errors alongside session replay."
/>
<div className="p-5 border-b mb-4">
<div className="font-medium mb-1">How it works?</div>
<ol className="list-decimal list-inside">
<li>Create a Service Account</li>
<li>Enter the details below</li>
<li>Propagate openReplaySessionToken</li>
</ol>
<DocLink className='mt-4' label='Integrate CloudWatch'
url='https://docs.openreplay.com/integrations/cloudwatch' />
<DocLink
className="mt-4"
label="Integrate CloudWatch"
url="https://docs.openreplay.com/integrations/cloudwatch"
/>
</div>
<IntegrationForm
{...props}
name='cloudwatch'
name="cloudwatch"
formFields={[
{
key: 'awsAccessKeyId',
label: 'AWS Access Key ID'
label: 'AWS Access Key ID',
},
{
key: 'awsSecretAccessKey',
label: 'AWS Secret Access Key'
label: 'AWS Secret Access Key',
},
{
key: 'region',
label: 'Region',
component: RegionDropdown
component: RegionDropdown,
},
{
key: 'logGroupName',
@ -44,8 +56,8 @@ const CloudwatchForm = (props) => (
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,
},
]}
/>
</div>

View file

@ -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 (
<Select
// selection
options={ options }
name={ name }
value={ options.find(o => o.value === value) }
placeholder={ placeholder }
onChange={ this.onChange }
loading={ loading }
/>
);
}
}
};
const options = values.map((g) => ({ text: g, value: g }));
return (
<Select
options={options}
name={name}
value={options.find((o) => o.value === value)}
placeholder={placeholder}
onChange={handleChange}
loading={loading}
/>
);
};
export default observer(LogGroupDropdown);

View file

@ -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 (
<div className='bg-white h-screen overflow-y-auto' style={{ width: '350px' }}>
<IntegrationModalCard title='Elasticsearch' icon='integrations/elasticsearch'
description='Integrate Elasticsearch with session replays to seamlessly observe backend errors.' />
const ElasticsearchForm = (props) => {
return (
<div
className="bg-white h-screen overflow-y-auto"
style={{ width: '350px' }}
>
<IntegrationModalCard
title="Elasticsearch"
icon="integrations/elasticsearch"
description="Integrate Elasticsearch with session replays to seamlessly observe backend errors."
/>
<div className='p-5 border-b mb-4'>
<div className='font-medium mb-1'>How it works?</div>
<ol className="list-decimal list-inside">
<li>Create a new Elastic API key</li>
<li>Enter the API key below</li>
<li>Propagate openReplaySessionToken</li>
</ol>
<DocLink className='mt-4' label='Integrate Elasticsearch'
url='https://docs.openreplay.com/integrations/elastic' />
</div>
<IntegrationForm
{...props}
name='elasticsearch'
formFields={[
{
key: 'host',
label: 'Host'
},
{
key: 'apiKeyId',
label: 'API Key ID'
},
{
key: 'apiKey',
label: 'API Key'
},
{
key: 'indexes',
label: 'Indexes'
},
{
key: 'port',
label: 'Port',
type: 'number'
}
]}
<div className="p-5 border-b mb-4">
<div className="font-medium mb-1">How it works?</div>
<ol className="list-decimal list-inside">
<li>Create a new Elastic API key</li>
<li>Enter the API key below</li>
<li>Propagate openReplaySessionToken</li>
</ol>
<DocLink
className="mt-4"
label="Integrate Elasticsearch"
url="https://docs.openreplay.com/integrations/elastic"
/>
</div>
);
}
}
<IntegrationForm
{...props}
name="elasticsearch"
formFields={[
{
key: 'host',
label: 'Host',
},
{
key: 'apiKeyId',
label: 'API Key ID',
},
{
key: 'apiKey',
label: 'API Key',
},
{
key: 'indexes',
label: 'Indexes',
},
{
key: 'port',
label: 'Port',
type: 'number',
},
]}
/>
</div>
);
};
export default ElasticsearchForm;

View file

@ -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);

View file

@ -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 (
<Loader loading={loading}>
<div className="ph-20">
<Form>
{formFields.map(
({
key,
label,
placeholder = label,
component: Component = 'input',
type = 'text',
checkIfDisplayed,
autoFocus = false,
}) =>
(typeof checkIfDisplayed !== 'function' || checkIfDisplayed(config)) &&
(type === 'checkbox' ? (
<Form.Field key={key}>
<Checkbox
label={label}
name={key}
value={config[key]}
onChange={this.write}
placeholder={placeholder}
type={Component === 'input' ? type : null}
/>
</Form.Field>
) : (
<Form.Field key={key}>
<label>{label}</label>
<Input
name={key}
value={config[key]}
onChange={this.write}
placeholder={placeholder}
type={Component === 'input' ? type : null}
autoFocus={autoFocus}
/>
</Form.Field>
))
)}
<Button
onClick={this.save}
disabled={!config.validate()}
loading={saving || loading}
variant="primary"
className="float-left mr-2"
>
{config.exists() ? 'Update' : 'Add'}
</Button>
{integrated && (
<Button loading={removing} onClick={this.remove}>
{'Delete'}
</Button>
)}
</Form>
</div>
</Loader>
);
}
}

View file

@ -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 (
<Loader loading={loading}>
<div className="ph-20">
<Form>
{formFields.map(
({
key,
label,
placeholder = label,
component: Component = 'input',
type = 'text',
checkIfDisplayed,
autoFocus = false,
}) =>
(typeof checkIfDisplayed !== 'function' ||
checkIfDisplayed(config)) &&
(type === 'checkbox' ? (
<Form.Field key={key}>
<Checkbox
label={label}
name={key}
value={config[key]}
onChange={write}
placeholder={placeholder}
type={Component === 'input' ? type : null}
/>
</Form.Field>
) : (
<Form.Field key={key}>
<label>{label}</label>
<Input
name={key}
value={config[key]}
onChange={write}
placeholder={placeholder}
type={Component === 'input' ? type : null}
autoFocus={autoFocus}
/>
</Form.Field>
))
)}
<Button
onClick={save}
disabled={!config?.validate()}
loading={loading}
variant="primary"
className="float-left mr-2"
>
{config?.exists() ? 'Update' : 'Add'}
</Button>
{integrated && (
<Button loading={loading} onClick={remove}>
{'Delete'}
</Button>
)}
</Form>
</div>
</Loader>
);
}
export default observer(IntegrationForm);

View file

@ -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<string[]>([]);
const [activeFilter, setActiveFilter] = useState<string>('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 (
<>
<div className='bg-white rounded-lg border shadow-sm p-5 mb-4'>
<div className="bg-white rounded-lg border shadow-sm p-5 mb-4">
{!hideHeader && <PageTitle title={<div>Integrations</div>} />}
<IntegrationFilters onChange={onChange} activeItem={activeFilter} filters={filters} />
<IntegrationFilters
onChange={onChange}
activeItem={activeFilter}
filters={filters}
/>
</div>
<div className='mb-4' />
<div className="mb-4" />
<div className={cn(`
<div
className={cn(`
mt-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3
`)}>
`)}
>
{allIntegrations.map((integration: any) => (
<IntegrationItem
integrated={integratedList.includes(integration.slug)}
integration={integration}
onClick={() =>
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'))
}
/>
))}
</div>
</>
);
}
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: <JiraForm />
component: <JiraForm />,
},
{
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: <GithubForm />
}
]
component: <GithubForm />,
},
],
},
{
title: 'Backend Logging',
@ -186,106 +202,119 @@ const integrations = [
'Sync your backend errors with sessions replays and see what happened front-to-back.',
docs: () => (
<DocCard
title='Why use integrations?'
icon='question-lg'
iconBgColor='bg-red-lightest'
iconColor='red'
title="Why use integrations?"
icon="question-lg"
iconBgColor="bg-red-lightest"
iconColor="red"
>
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.
</DocCard>
),
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: <SentryForm />
component: <SentryForm />,
},
{
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: <BugsnagForm />
component: <BugsnagForm />,
},
{
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: <RollbarForm />
component: <RollbarForm />,
},
{
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: <ElasticsearchForm />
component: <ElasticsearchForm />,
},
{
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: <DatadogForm />
component: <DatadogForm />,
},
{
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: <SumoLogicForm />
component: <SumoLogicForm />,
},
{
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: <StackdriverForm />
component: <StackdriverForm />,
},
{
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: <CloudwatchForm />
component: <CloudwatchForm />,
},
{
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: <NewrelicForm />
}
]
component: <NewrelicForm />,
},
],
},
{
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: <SlackForm />,
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: <MSTeams />,
shared: true
}
]
shared: true,
},
],
},
// {
// title: 'State Management',
@ -302,72 +331,82 @@ const integrations = [
icon: 'chat-left-text',
docs: () => (
<DocCard
title='What are plugins?'
icon='question-lg'
iconBgColor='bg-red-lightest'
iconColor='red'
title="What are plugins?"
icon="question-lg"
iconBgColor="bg-red-lightest"
iconColor="red"
>
Plugins capture your applications store, monitor queries, track performance issues and even
assist your end user through live sessions.
Plugins capture your applications store, monitor queries, track
performance issues and even assist your end user through live sessions.
</DocCard>
),
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: <ReduxDoc />
subtitle:
'Capture Redux actions/state and inspect them later on while replaying session recordings.',
icon: 'integrations/redux',
component: <ReduxDoc />,
},
{
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: <VueDoc />
component: <VueDoc />,
},
{
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: <PiniaDoc />
component: <PiniaDoc />,
},
{
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: <GraphQLDoc />
component: <GraphQLDoc />,
},
{
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: <NgRxDoc />
component: <NgRxDoc />,
},
{
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: <MobxDoc />
component: <MobxDoc />,
},
{
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: <ProfilerDoc />
component: <ProfilerDoc />,
},
{
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: <AssistDoc />
component: <AssistDoc />,
},
{
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: <ZustandDoc />
}
]
}
component: <ZustandDoc />,
},
],
},
];

View file

@ -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)

View file

@ -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(<options>))]; // 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);

View file

@ -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 (
<div className="bg-white h-screen overflow-y-auto" style={{ width: '500px' }}>
<div
className="bg-white h-screen overflow-y-auto"
style={{ width: '500px' }}
>
<h3 className="p-5 text-2xl">VueX</h3>
<div className="p-5">
<div>
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.
</div>
<div className="font-bold my-2 text-lg">Installation</div>
<CodeBlock code={`npm i @openreplay/tracker-vuex --save`} language="bash" />
<CodeBlock
code={`npm i @openreplay/tracker-vuex --save`}
language="bash"
/>
<div className="font-bold my-2 text-lg">Usage</div>
<p>
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.
</p>
<div className="py-3" />
<ToggleContent
label="Server-Side-Rendered (SSR)?"
first={
<CodeBlock code={usage} language="js" />
}
second={
<CodeBlock code={usageCjs} language="js" />
}
first={<CodeBlock code={usage} language="js" />}
second={<CodeBlock code={usageCjs} language="js" />}
/>
<DocLink
@ -97,10 +109,4 @@ piniaStorePlugin(examplePiniaStore)
PiniaDoc.displayName = 'PiniaDoc';
export default connect((state: any) => {
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);

View file

@ -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);

View file

@ -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);

View file

@ -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 (
<div className="p-5" style={{ minWidth: '300px' }}>
<Form>
<Form.Field>
<label>Name</label>
<Input
name="name"
value={instance.name}
onChange={this.write}
placeholder="Enter any name"
type="text"
/>
</Form.Field>
<Form.Field>
<label>URL</label>
<Input
name="endpoint"
value={instance.endpoint}
onChange={this.write}
placeholder="Slack webhook URL"
type="text"
/>
</Form.Field>
<div className="flex justify-between">
<div className="flex">
<Button
onClick={this.save}
disabled={!instance.validate()}
loading={saving}
variant="primary"
className="float-left mr-2"
>
{instance.exists() ? 'Update' : 'Add'}
</Button>
<Button onClick={onClose}>{'Cancel'}</Button>
</div>
<Button onClick={() => this.remove(instance.webhookId)} disabled={!instance.exists()}>
{'Delete'}
const write = ({ target: { name, value } }) => edit({ [name]: value });
return (
<div className="p-5" style={{ minWidth: '300px' }}>
<Form>
<Form.Field>
<label>Name</label>
<Input
name="name"
value={instance.name}
onChange={write}
placeholder="Enter any name"
type="text"
/>
</Form.Field>
<Form.Field>
<label>URL</label>
<Input
name="endpoint"
value={instance.endpoint}
onChange={write}
placeholder="Slack webhook URL"
type="text"
/>
</Form.Field>
<div className="flex justify-between">
<div className="flex">
<Button
onClick={save}
disabled={!instance.validate()}
loading={saving}
variant="primary"
className="float-left mr-2"
>
{instance.exists() ? 'Update' : 'Add'}
</Button>
</div>
</Form>
{errors && (
<div className="my-3">
{errors.map((error) => (
<Message visible={errors} size="mini" error key={error}>
{error}
</Message>
))}
<Button onClick={onClose}>{'Cancel'}</Button>
</div>
)}
</div>
);
}
<Button onClick={() => remove(instance.webhookId)} disabled={!instance.exists()}>
{'Delete'}
</Button>
</div>
</Form>
{errors && (
<div className="my-3">
{errors.map((error) => (
<Message visible={errors} size="mini" error key={error}>
{error}
</Message>
))}
</div>
)}
</div>
);
}
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);

View file

@ -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) {
</div>
}
size="small"
show={list.size === 0}
show={list.length === 0}
>
{list.map((c) => (
<div
@ -43,9 +45,4 @@ function SlackChannelList(props) {
);
}
export default connect(
(state) => ({
list: state.getIn(['slack', 'list']),
}),
{ remove, edit, init }
)(SlackChannelList);
export default observer(SlackChannelList);

View file

@ -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);

View file

@ -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<Props> {
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<Props> {
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 (
<div className="p-5" style={{ minWidth: '300px' }}>
<Form>
<Form.Field>
<label>Name</label>
<Input
name="name"
value={instance.name}
onChange={this.write}
placeholder="Enter any name"
type="text"
/>
</Form.Field>
<Form.Field>
<label>URL</label>
<Input
name="endpoint"
value={instance.endpoint}
onChange={this.write}
placeholder="Teams webhook URL"
type="text"
/>
</Form.Field>
<div className="flex justify-between">
<div className="flex">
<Button
onClick={this.save}
disabled={!instance.validate()}
loading={saving}
variant="primary"
className="float-left mr-2"
>
{instance.exists() ? 'Update' : 'Add'}
</Button>
<Button onClick={onClose}>{'Cancel'}</Button>
</div>
<Button onClick={() => this.remove(instance.webhookId)} disabled={!instance.exists()}>
{'Delete'}
return (
<div className="p-5" style={{ minWidth: '300px' }}>
<Form>
<Form.Field>
<label>Name</label>
<Input
name="name"
value={instance?.name}
onChange={write}
placeholder="Enter any name"
type="text"
/>
</Form.Field>
<Form.Field>
<label>URL</label>
<Input
name="endpoint"
value={instance?.endpoint}
onChange={write}
placeholder="Teams webhook URL"
type="text"
/>
</Form.Field>
<div className="flex justify-between">
<div className="flex">
<Button
onClick={save}
disabled={!instance?.validate()}
loading={saving}
variant="primary"
className="float-left mr-2"
>
{instance?.exists() ? 'Update' : 'Add'}
</Button>
</div>
</Form>
{errors && (
<div className="my-3">
{errors.map((error: any) => (
<Message visible={errors} key={error}>
{error}
</Message>
))}
<Button onClick={onClose}>{'Cancel'}</Button>
</div>
)}
</div>
);
}
<Button
onClick={() => remove(instance?.webhookId)}
disabled={!instance.exists()}
>
{'Delete'}
</Button>
</div>
</Form>
{errors && (
<div className="my-3">
{errors.map((error: any) => (
<Message visible={errors} key={error}>
{error}
</Message>
))}
</div>
)}
</div>
);
}
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);

View file

@ -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<string, any>) => {
props.edit(instance);
props.onEdit();
};
const onEdit = (instance: Record<string, any>) => {
edit(instance);
props.onEdit();
};
return (
<div className="mt-6">
<NoContent
title={
<div className="p-5 mb-4">
<div className="text-base text-left">
Integrate MS Teams with OpenReplay and share insights with the rest of the team, directly from the recording page.
</div>
<DocLink className="mt-4 text-base" label="Integrate MS Teams" url="https://docs.openreplay.com/integrations/msteams" />
</div>
}
size="small"
show={list.size === 0}
>
{list.map((c: any) => (
<div
key={c.webhookId}
className="border-t px-5 py-2 flex items-center justify-between cursor-pointer hover:bg-active-blue"
onClick={() => onEdit(c)}
>
<div className="flex-grow-0" style={{ maxWidth: '90%' }}>
<div>{c.name}</div>
<div className="truncate test-xs color-gray-medium">{c.endpoint}</div>
</div>
</div>
))}
</NoContent>
</div>
);
return (
<div className="mt-6">
<NoContent
title={
<div className="p-5 mb-4">
<div className="text-base text-left">
Integrate MS Teams with OpenReplay and share insights with the
rest of the team, directly from the recording page.
</div>
<DocLink
className="mt-4 text-base"
label="Integrate MS Teams"
url="https://docs.openreplay.com/integrations/msteams"
/>
</div>
}
size="small"
show={list.length === 0}
>
{list.map((c: any) => (
<div
key={c.webhookId}
className="border-t px-5 py-2 flex items-center justify-between cursor-pointer hover:bg-active-blue"
onClick={() => onEdit(c)}
>
<div className="flex-grow-0" style={{ maxWidth: '90%' }}>
<div>{c.name}</div>
<div className="truncate test-xs color-gray-medium">
{c.endpoint}
</div>
</div>
</div>
))}
</NoContent>
</div>
);
}
export default connect(
(state: any) => ({
list: state.getIn(['teams', 'list']),
}),
{ remove, edit, init }
)(TeamsChannelList);
export default observer(TeamsChannelList);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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)));

View file

@ -23,16 +23,16 @@ interface Permission {
interface Props {
closeModal: (toastMessage?: string) => void;
projects: any[];
permissionsMap: any;
deleteHandler: (id: any) => Promise<void>;
}
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 (

View file

@ -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 dont 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(<NewSiteForm onClose={hideModal} />, { 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);

View file

@ -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 (
<Form className={ styles.formWrapper } onSubmit={ onSubmit }>
<div className={ styles.content }>
<Form.Field>
<label>{ 'Name' }</label>
<div>{ site.host }</div>
</Form.Field>
<Form.Field>
<label>{ 'Session Capture Rate' }</label>
<Input
icon="percent"
name="sampleRate"
value={ gdpr.sampleRate }
onChange={ onChange }
onBlur={ onSampleRateBlur }
className={ styles.sampleRate }
/>
</Form.Field>
render() {
const {
site, onClose, saving, gdpr,
} = this.props;
<Form.Field>
<label htmlFor="defaultInputMode">{ 'Data Recording Options' }</label>
<Select
name="defaultInputMode"
options={ inputModeOptions }
onChange={ onChangeSelect }
placeholder="Default Input Mode"
value={ gdpr.defaultInputMode }
/>
</Form.Field>
return (
<Form className={ styles.formWrapper } onSubmit={ this.onSubmit }>
<div className={ styles.content }>
<Form.Field>
<label>{ 'Name' }</label>
<div>{ site.host }</div>
</Form.Field>
<Form.Field>
<label>{ 'Session Capture Rate' }</label>
<Input
icon="percent"
name="sampleRate"
value={ gdpr.sampleRate }
onChange={ this.onChange }
onBlur={ this.onSampleRateBlur }
className={ styles.sampleRate }
<Form.Field>
<label>
<input
name="maskNumbers"
type="checkbox"
checked={ gdpr.maskNumbers }
onChange={ onChangeOption }
/>
</Form.Field>
{ 'Do not record any numeric text' }
<div className={ styles.controlSubtext }>{ 'If enabled, OpenReplay will not record or store any numeric text for all sessions.' }</div>
</label>
</Form.Field>
<Form.Field>
<label htmlFor="defaultInputMode">{ 'Data Recording Options' }</label>
<Select
name="defaultInputMode"
options={ inputModeOptions }
onChange={ this.onChangeSelect }
placeholder="Default Input Mode"
value={ gdpr.defaultInputMode }
// className={ styles.dropdown }
<Form.Field>
<label>
<input
name="maskEmails"
type="checkbox"
checked={ gdpr.maskEmails }
onChange={ onChangeOption }
/>
</Form.Field>
{ 'Do not record email addresses ' }
<div className={ styles.controlSubtext }>{ 'If enabled, OpenReplay will not record or store any email address for all sessions.' }</div>
</label>
</Form.Field>
<Form.Field>
<label>
<input
name="maskNumbers"
type="checkbox"
checked={ gdpr.maskNumbers }
onChange={ this.onChangeOption }
/>
{ 'Do not record any numeric text' }
<div className={ styles.controlSubtext }>{ 'If enabled, OpenReplay will not record or store any numeric text for all sessions.' }</div>
</label>
</Form.Field>
<Form.Field>
<label>
<input
name="maskEmails"
type="checkbox"
checked={ gdpr.maskEmails }
onChange={ this.onChangeOption }
/>
{ 'Do not record email addresses ' }
<div className={ styles.controlSubtext }>{ 'If enabled, OpenReplay will not record or store any email address for all sessions.' }</div>
</label>
</Form.Field>
<div className={ styles.blockIpWarapper }>
<div className={ styles.button } onClick={ this.props.toggleBlockedIp }>
{ 'Block IP' } <Icon name="next1" size="18" />
</div>
<div className={ styles.blockIpWarapper }>
<div className={ styles.button } onClick={ props.toggleBlockedIp }>
{ 'Block IP' } <Icon name="next1" size="18" />
</div>
</div>
</div>
<div className={ styles.footer }>
<Button
variant="outline"
className="float-left mr-2"
loading={ saving }
content="Update"
/>
<Button onClick={ onClose } content="Cancel" />
</div>
</Form>
);
}
<div className={ styles.footer }>
<Button
variant="outline"
className="float-left mr-2"
loading={ saving }
content="Update"
/>
<Button onClick={ onClose } content="Cancel" />
</div>
</Form>
)
}
export default observer(GDPRForm);

View file

@ -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<typeof connector>;
@ -25,36 +21,34 @@ type PropsFromRedux = ConnectedProps<typeof connector>;
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<HTMLInputElement>) => {
setExistsError(false);
edit({ [name]: value });
projectsStore.editInstance({ [name]: value });
};
if (!site) {
return null
}
return (
<div
className="bg-white h-screen overflow-y-auto"
@ -115,7 +113,7 @@ const NewSiteForm = ({
</h3>
<Form
className={styles.formWrapper}
onSubmit={site.validate() && onSubmit}
onSubmit={site.validate && onSubmit}
>
<div className={styles.content}>
<Form.Field>
@ -145,7 +143,7 @@ const NewSiteForm = ({
]}
value={site.platform}
onChange={(value) => {
edit({ platform: value });
projectsStore.editInstance({ platform: value });
}}
/>
</div>
@ -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'}
</Button>
{site.exists() && (
<Button
@ -182,25 +180,9 @@ const NewSiteForm = ({
);
};
const mapStateToProps = (state: any) => ({
activeSiteId: state.getIn(['site', 'active', 'id']),
site: state.getIn(['site', 'instance']),
siteList: state.getIn(['site', 'list']),
loading:
state.getIn(['site', 'save', 'loading']) ||
state.getIn(['site', 'remove', 'loading']),
canDelete: state.getIn(['site', 'list']).size > 1,
});
const mapStateToProps = null;
const connector = connect(mapStateToProps, {
save,
remove,
edit,
update,
pushNewSite,
fetchList,
setSiteId,
clearSearchLive,
});
export default connector(withRouter(withStore(NewSiteForm)));
export default connector(withRouter(observer(NewSiteForm)));

View file

@ -3,7 +3,6 @@ import { connect, ConnectedProps } from 'react-redux';
import { Tag } from 'antd';
import cn from 'classnames';
import { Loader, Button, TextLink, NoContent, Pagination, PageTitle, Divider, Icon } from 'UI';
import { init, remove, fetchGDPR, setSiteId } from 'Duck/site';
import withPageTitle from 'HOCs/withPageTitle';
import stl from './sites.module.css';
import NewSiteForm from './NewSiteForm';
@ -16,9 +15,11 @@ import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import { useModal } from 'App/components/Modal';
import CaptureRate from 'Shared/SessionSettings/components/CaptureRate';
import { BranchesOutlined } from '@ant-design/icons';
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore'
type Project = {
id: number;
id: string;
name: string;
conditionsCount: number;
platform: 'web' | 'mobile';
@ -29,7 +30,11 @@ type Project = {
type PropsFromRedux = ConnectedProps<typeof connector>;
const Sites = ({ loading, sites, user, init }: PropsFromRedux) => {
const Sites = ({ user }: PropsFromRedux) => {
const { projectsStore } = useStore();
const sites = projectsStore.list;
const loading = projectsStore.sitesLoading;
const init = projectsStore.initProject
const [searchQuery, setSearchQuery] = useState('');
const [showCaptureRate, setShowCaptureRate] = useState(true);
const [activeProject, setActiveProject] = useState<Project | null>(null);
@ -140,7 +145,7 @@ const Sites = ({ loading, sites, user, init }: PropsFromRedux) => {
</div>
}
size="small"
show={!loading && filteredSites.size === 0}
show={!loading && filteredSites.length === 0}
>
<div className="grid grid-cols-12 gap-2 w-full items-center px-5 py-3 font-medium">
<div className="col-span-4">Project Name</div>
@ -160,7 +165,7 @@ const Sites = ({ loading, sites, user, init }: PropsFromRedux) => {
<div className="w-full flex items-center justify-center py-10">
<Pagination
page={page}
total={filteredSites.size}
total={filteredSites.length}
onPageChange={(page) => 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)));

View file

@ -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, }
)

View file

@ -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);

View file

@ -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<NewDashboardModalProps> = ({
@ -20,8 +20,9 @@ const NewDashboardModal: React.FC<NewDashboardModalProps> = ({
open,
isAddingFromLibrary = false,
isEnterprise = false,
isMobile = false,
}) => {
const { projectsStore } = useStore();
const isMobile = projectsStore.isMobile;
const [step, setStep] = React.useState<number>(0);
const [selectedCategory, setSelectedCategory] =
React.useState<string>('product-analytics');
@ -75,10 +76,9 @@ const NewDashboardModal: React.FC<NewDashboardModalProps> = ({
};
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));

View file

@ -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(<NewSiteForm onClose={hideModal} />, { right: true });
};
function NewProjectButton() {
const { projectsStore } = useStore();
const { showModal, hideModal } = useModal();
return (
<li onClick={onClick}>
<Icon name="folder-plus" size="16" color="teal" />
<span className="ml-3 color-teal">Add Project</span>
</li>
// <div
// className={cn('flex items-center justify-center py-3 cursor-pointer hover:bg-active-blue ', { disabled: !canAddProject })}
// onClick={onClick}
// >
// <Icon name="plus" size={12} className="mr-2" color="teal" />
// <span className="color-teal">Add New Project</span>
// </div>
);
const onClick = () => {
projectsStore.initProject({});
showModal(<NewSiteForm onClose={hideModal} />, { right: true });
};
return (
<li onClick={onClick}>
<Icon name="folder-plus" size="16" color="teal" />
<span className="ml-3 color-teal">Add Project</span>
</li>
);
}
export default connect(null, { init })(NewProjectButton);
export default observer(NewProjectButton);

View file

@ -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<MetadataListProps> = (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<MetadataListProps> = (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);

View file

@ -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);

View file

@ -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);

View file

@ -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 = <P extends RouteComponentProps>(
Component: React.ComponentType<P & WithOnboardingProps & PropsFromRedux>
) => {
const WithOnboarding: React.FC<P & WithOnboardingProps & PropsFromRedux> = (props) => {
const { projectsStore } = useStore();
const sites = projectsStore.list;
const {
sites,
match: {
params: { siteId },
},
@ -43,7 +43,7 @@ const withOnboarding = <P extends RouteComponentProps>(
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 = <P extends RouteComponentProps>(
return <Component skip={skip} navTo={navTo} {...props} site={site} />;
};
return withRouter(connector(WithOnboarding as React.ComponentType<any>));
return withRouter(connector(observer(WithOnboarding as React.ComponentType<any>)));
};
export default withOnboarding;

View file

@ -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<string, (key: string) => 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<ILivePlayerContext>(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']),

View file

@ -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)

View file

@ -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<Note | undefined>(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,

View file

@ -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(() => (
</Tab>
));
function AssistTabs({ session, siteId }: { session: Record<string, any>; siteId: string }) {
function AssistTabs({ session }: { session: Record<string, any> }) {
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<string, any>; siteId:
);
}
export default connect((state: any) => ({ siteId: state.getIn(['site', 'siteId']) }))(
observer(AssistTabs)
);
export default observer(AssistTabs);

View file

@ -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),
};

View file

@ -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']),
};
},
{

View file

@ -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),
};
},

View file

@ -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);

View file

@ -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),
};
},

View file

@ -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<any>;
@ -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));
});
};

View file

@ -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,
}

View file

@ -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<string, any>
| 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)));

View file

@ -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<string, any>[];
}) {
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,

View file

@ -42,9 +42,10 @@ const CurrentTab = React.memo(() => (
</Tab>
));
function AssistTabs({ session, siteId }: { session: Record<string, any>; siteId: string }) {
function AssistTabs({ session }: { session: Record<string, any> }) {
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<string, any>; siteId:
);
}
export default connect((state: any) => ({ siteId: state.getIn(['site', 'siteId']) }))(
observer(AssistTabs)
);
export default observer(AssistTabs)

View file

@ -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']),
};
},
{

View file

@ -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<Record<string, any>>;
teamsChannels: List<Record<string, any>>;
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<HTMLTextAreaElement>();
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));

View file

@ -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)
);

View file

@ -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']) || [],

View file

@ -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));

View file

@ -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) {
<div
className="w-full px-4 flex items-center border-b relative"
style={{
background: uxtestingStore.isUxt() ? (props.live ? '#F6FFED' : '#EBF4F5') : undefined
background: uxtestingStore.isUxt()
? props.live
? '#F6FFED'
: '#EBF4F5'
: undefined,
}}
>
{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={
<div className="relative">
<Tooltip title="Share Session" placement="bottom">
<AntButton size={'small'} className="flex items-center justify-center">
<AntButton
size={'small'}
className="flex items-center justify-center"
>
<ShareAltOutlined />
</AntButton>
</Tooltip>
@ -141,8 +159,8 @@ function SubHeader(props) {
{locationTruncated && (
<div className={'w-full bg-white border-b border-gray-lighter'}>
<div className="flex w-fit items-center cursor-pointer color-gray-medium text-sm p-1">
<Link2 className="mx-2" size={16} />
<Tooltip title="Open in new tab" delay={0} placement='bottom'>
<Link2 className="mx-2" size={16} />
<Tooltip title="Open in new tab" delay={0} placement="bottom">
<a href={currentLocation} target="_blank" className="truncate">
{locationTruncated}
</a>
@ -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));

View file

@ -107,7 +107,7 @@ function TestOverview() {
}, [testId, siteId]);
if (!uxtestingStore.instance) {
return <Loader loading={uxtestingStore.isLoading}>No Data</Loader>;
return <Loader loading={uxtestingStore.isLoading}>Loading Data...</Loader>;
} else {
document.title = `Usability Tests | ${uxtestingStore.instance.title}`;
}

View file

@ -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<P extends Props>(WrappedComponent: React.ComponentType<P>) {
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<P extends Props>(WrappedComponent: React.Comp
);
};
return connect((state: any) => ({
site: state.getIn(['site', 'instance']),
}))(ComponentWithReport);
return observer(ComponentWithReport);
}

View file

@ -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 <BaseComponent {...other} history={{ ...history, push: this.push }} />
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
}
return <BaseComponent {...other} history={{ ...history, push: push }} />
}))

View file

@ -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 : <BaseComponent {...this.props} />;
}
}
const key = props.siteId;
return WrapperClass
}
const passedProps = { ...props, siteId, setSiteId, urlSiteId };
return <BaseComponent key={key} {...passedProps} />;
};
return observer(WrapperComponent);
};
export default withSiteIdUpdater;

View file

@ -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 ? (
<div>{<div className={stl.badge} />}</div>
) : (
''
);
) : null;
}
export default connect((state) => ({
projects: state.getIn(['site', 'list']),
}))(observer(ErrorsBadge));
export default observer(ErrorsBadge);

View file

@ -14,7 +14,6 @@ interface Props {
filterListLive: any;
onFilterClick: (filter: any) => void;
children?: any;
isLive?: boolean;
excludeFilterKeys?: Array<string>;
allowedFilterKeys?: Array<string>;
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);

View file

@ -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<HTMLAnchorElement>, 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);

View file

@ -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);
}), { edit, addFilter })(observer(LiveSessionSearch));

View file

@ -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 (
<div className="flex items-center flex-wrap">
<div style={{ flex: 3, marginRight: '10px' }}>
@ -56,8 +56,4 @@ const MainSearchBar = (props: Props) => {
);
};
export default connect(
(state: any) => ({
site: state.getIn(['site', 'siteId'])
})
)(observer(MainSearchBar));
export default observer(MainSearchBar);

View file

@ -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));

View file

@ -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(<NewSiteForm onClose={hideModal} />, { right: true });
};
// @ts-ignore immutable
const menuItems = sites.toJS().map((site) => ({
const menuItems = sites.map((site) => ({
key: site.id,
label: (
<div
@ -141,15 +140,11 @@ function ProjectDropdown(props: Props) {
}
const mapStateToProps = (state: any) => ({
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))
);

View file

@ -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

View file

@ -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 (
<Tooltip
@ -417,4 +416,4 @@ function SessionItem(props: RouteComponentProps & Props) {
);
}
export default withRouter(connect(null, { presetSession })(observer(SessionItem)));
export default withRouter(observer(SessionItem));

View file

@ -3,7 +3,6 @@ import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import FilterList from 'Shared/Filters/FilterList';
import FilterSelection from 'Shared/Filters/FilterSelection';
import SaveFilterButton from 'Shared/SaveFilterButton';
import { connect } from 'react-redux';
import { FilterKey } from 'Types/filter/filterType';
import { addOptionsToFilter } from 'Types/filter/newFilter';
import { Button, Loader } from 'UI';
@ -21,13 +20,12 @@ interface Props {
}
function SessionSearch(props: Props) {
const { tagWatchStore, aiFiltersStore, searchStore, customFieldStore } = useStore();
const { tagWatchStore, aiFiltersStore, searchStore, customFieldStore, projectsStore } = useStore();
const appliedFilter = searchStore.instance;
const metaLoading = customFieldStore.isLoading;
const { saveRequestPayloads = false } = props;
const hasEvents = appliedFilter.filters.filter((i: any) => 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);

View file

@ -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 (
<div className='bg-white box-shadow h-screen overflow-y-auto'>
<div className='px-6 pt-6'>
@ -32,6 +35,4 @@ function SessionSettings({ projectId }: { projectId: number }) {
);
}
export default connect((state: any) => ({
projectId: state.getIn(['site', 'siteId'])
}))(SessionSettings);
export default observer(SessionSettings)

View file

@ -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;

View file

@ -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<void>;
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>(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)));

View file

@ -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<T>({ defaultOption, onSort, sortOptions, current }:
sortOptions: any,
current: string
}) {
return (
<Dropdown
menu={{
@ -53,13 +51,14 @@ export function SortDropdown<T>({ 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);

View file

@ -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]: <CircleAlert size={14} />,
[types.BAD_REQUEST]: <WifiOff size={14} />,
[types.CLICK_RAGE]: <Angry size={14} />,
[types.CRASH]: <Skull size={14} />
[types.CRASH]: <Skull size={14} />,
} as Record<string, any>;
const SessionTags: React.FC<Props> = memo(
({ tags, total }) => {
const { searchStore } = useStore();
const disable = searchStore.activeTab.type === 'all' && total === 0;
const activeTab = searchStore.activeTab;
const SessionTags: React.FC<Props> = ({ 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: (
<div className={'flex items-center gap-2'}>
{tag.icon ? (
tagIcons[tag.type] ? (
tagIcons[tag.type]
) : (
<Icon
name={tag.icon}
color={activeTab.type === tag.type ? 'main' : undefined}
size="14"
className={cn('group-hover:fill-teal')}
/>
)
) : null}
<div className={activeTab.type === tag.type ? 'text-main' : ''}>
{tag.name}
</div>
const options = tags.map((tag, i) => ({
label: (
<div className={'flex items-center gap-2'}>
{tag.icon ? (
tagIcons[tag.type] ? (
tagIcons[tag.type]
) : (
<Icon
name={tag.icon}
color={activeTab.type === tag.type ? 'main' : undefined}
size="14"
className={cn('group-hover:fill-teal')}
/>
)
) : null}
<div className={activeTab.type === tag.type ? 'text-main' : ''}>
{tag.name}
</div>
),
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 (
<div className="flex items-center">
<Segmented
options={options}
value={activeTab.type}
onChange={onPick}
size={'small'}
/>
</div>
);
}
);
),
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 (
<div className="flex items-center">
<Segmented
options={options}
value={activeTab.type}
onChange={onPick}
size={'small'}
/>
</div>
);
};
// 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));

View file

@ -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<HTMLTextAreaElement>) => 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<string, any>) => ({
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);

View file

@ -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 (
<Select
name={`${contextName}_site`}
@ -17,6 +20,4 @@ const SiteDropdown = ({ contextName = '', sites, onChange, value }) => {
SiteDropdown.displayName = 'SiteDropdown';
export default connect(state => ({
sites: state.getIn(['site', 'list'])
}))(SiteDropdown);
export default observer(SiteDropdown);

View file

@ -1,49 +0,0 @@
import React, { useEffect } from 'react'
import { Icon } from 'UI'
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom';
import { onboarding as onboardingRoute } from 'App/routes'
import { withSiteId } from 'App/routes';
import { isGreaterOrEqualVersion } from 'App/utils'
const TrackerUpdateMessage= (props) => {
const [needUpdate, setNeedUpdate] = React.useState(false)
const { sites, match: { params: { siteId } } } = props;
const activeSite = sites.find(s => s.id == siteId);
useEffect(() => {
if (!activeSite || !activeSite.trackerVersion) return;
const isLatest = isGreaterOrEqualVersion(activeSite.trackerVersion, window.env.TRACKER_VERSION);
if (!isLatest && activeSite.recorded) {
setNeedUpdate(true)
}
}, [activeSite])
return needUpdate ? (
<>
{(
<div>
<div
className="rounded text-sm flex items-center justify-between mb-4"
style={{ height: '42px', backgroundColor: 'rgba(255, 239, 239, 1)', border: 'solid thin rgba(221, 181, 181, 1)'}}
>
<div className="flex items-center w-full">
<div className="flex-shrink-0 w-8 flex justify-center">
<Icon name="info-circle" size="14" color="gray-darkest" />
</div>
<div className="ml-2color-gray-darkest mr-auto">
There might be a mismatch between the tracker and the backend versions. Please make sure to <a href="#" className="link" onClick={() => props.history.push(withSiteId(onboardingRoute('installing'), siteId))}>update</a> the tracker to latest version (<a href="https://www.npmjs.com/package/@openreplay/tracker" target="_blank">{window.env.TRACKER_VERSION}</a>).
</div>
</div>
</div>
</div>
)}
</>
) : ''
}
export default connect(state => ({
site: state.getIn([ 'site', 'instance' ]),
sites: state.getIn([ 'site', 'list' ])
}))(withRouter(TrackerUpdateMessage))

View file

@ -1 +0,0 @@
export { default } from './TrackerUpdateMessage'

View file

@ -1,6 +1,6 @@
import React, { useState } from 'react'
import { connect } from 'react-redux';
import { editGDPR, saveGDPR } from 'Duck/site';
import { observer } from 'mobx-react-lite'
import { useStore } from 'App/mstore'
import { Checkbox } from 'UI';
import cn from 'classnames'
import styles from './projectCodeSnippet.module.css'
@ -18,21 +18,25 @@ inputModeOptions.forEach((o, i) => inputModeOptionsMap[o.value] = i)
const ProjectCodeSnippet = props => {
const { gdpr, site } = props;
const { projectsStore } = useStore();
const site = props.site;
const gdpr = projectsStore.instance.gdpr;
const saveGdpr = projectsStore.saveGDPR;
const editGdpr = projectsStore.editGDPR;
const [changed, setChanged] = useState(false)
const saveGDPR = () => {
setChanged(true)
props.saveGDPR(site.id);
saveGdpr(site.id);
}
const onChangeSelect = ({ name, value }) => {
props.editGDPR({ [ name ]: value });
editGdpr({ [ name ]: value });
saveGDPR();
};
const onChangeOption = ({ target: { name, checked }}) => {
props.editGDPR({ [ name ]: checked });
editGdpr({ [ name ]: checked });
saveGDPR()
}
@ -94,7 +98,4 @@ const ProjectCodeSnippet = props => {
)
}
export default connect(state => ({
gdpr: state.getIn([ 'site', 'instance', 'gdpr' ]),
saving: state.getIn([ 'site', 'saveGDPR', 'loading' ])
}), { editGDPR, saveGDPR })(ProjectCodeSnippet)
export default observer(ProjectCodeSnippet)

View file

@ -1,20 +1,23 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { observer } from 'mobx-react-lite'
import { useStore } from "App/mstore";
import cn from 'classnames';
import { withSiteId } from 'App/routes';
import styles from './link.module.css';
const OpenReplayLink = ({ siteId, to, className="", dispatch, ...other }) => (
<Link
{ ...other }
className={ cn(className, styles.link , 'px-2', 'hover:text-inherit') }
to={ withSiteId(to, siteId) }
/>
);
const OpenReplayLink = ({ siteId, to, className="", dispatch, ...other }) => {
const { projectsStore } = useStore();
const projectId = projectsStore.siteId;
return (
<Link
{ ...other }
className={ cn(className, styles.link , 'px-2', 'hover:text-inherit') }
to={ withSiteId(to, siteId ?? projectId) }
/>
)
};
OpenReplayLink.displayName = 'OpenReplayLink';
export default connect((state, props) => ({
siteId: props.siteId || state.getIn([ 'site', 'siteId' ])
}))(OpenReplayLink);
export default OpenReplayLink;

View file

@ -1,77 +1,81 @@
import React from "react";
import stl from "./NoSessionPermission.module.css";
import { Icon, Button } from "UI";
import { connect } from "react-redux";
import { observer } from 'mobx-react-lite';
import React from 'react';
import { connect } from 'react-redux';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { useStore } from 'App/mstore';
import {
sessions as sessionsRoute,
assist as assistRoute,
withSiteId,
} from "App/routes";
import { withRouter, RouteComponentProps } from "react-router-dom";
assist as assistRoute,
sessions as sessionsRoute,
withSiteId,
} from 'App/routes';
import { Button, Icon } from 'UI';
import stl from './NoSessionPermission.module.css';
const SESSIONS_ROUTE = sessionsRoute();
const ASSIST_ROUTE = assistRoute();
interface Props extends RouteComponentProps {
session: any;
siteId: string;
history: any;
sessionPath: any;
isAssist: boolean;
session: any;
history: any;
sessionPath: any;
isAssist: boolean;
}
function NoSessionPermission(props: Props) {
const { session, history, siteId, sessionPath, isAssist } = props;
const { projectsStore } = useStore();
const siteId = projectsStore.siteId!;
const { session, history, sessionPath, isAssist } = props;
const backHandler = () => {
if (
sessionPath.pathname === history.location.pathname ||
sessionPath.pathname.includes("/session/") ||
isAssist
) {
history.push(
withSiteId(isAssist ? ASSIST_ROUTE : SESSIONS_ROUTE, siteId)
);
} else {
history.push(
sessionPath
? sessionPath.pathname + sessionPath.search
: withSiteId(SESSIONS_ROUTE, siteId)
);
}
};
const backHandler = () => {
if (
sessionPath.pathname === history.location.pathname ||
sessionPath.pathname.includes('/session/') ||
isAssist
) {
history.push(
withSiteId(isAssist ? ASSIST_ROUTE : SESSIONS_ROUTE, siteId)
);
} else {
history.push(
sessionPath
? sessionPath.pathname + sessionPath.search
: withSiteId(SESSIONS_ROUTE, siteId)
);
}
};
return (
<div className={stl.wrapper}>
<Icon name="shield-lock" size="50" className="py-16" />
<div className={stl.title}>Not allowed</div>
{session.isLive ? (
<span>
This session is still live, and you dont have the necessary
permissions to access this feature. Please check with your
admin.
</span>
) : (
<span>
You dont have the necessary permissions to access this
feature. Please check with your admin.
</span>
)}
{/* <Link to="/"> */}
<Button variant="primary" onClick={backHandler} className="mt-6">
GO BACK
</Button>
{/* </Link> */}
</div>
);
return (
<div className={stl.wrapper}>
<Icon name="shield-lock" size="50" className="py-16" />
<div className={stl.title}>Not allowed</div>
{session.isLive ? (
<span>
This session is still live, and you dont have the necessary
permissions to access this feature. Please check with your admin.
</span>
) : (
<span>
You dont have the necessary permissions to access this feature.
Please check with your admin.
</span>
)}
{/* <Link to="/"> */}
<Button variant="primary" onClick={backHandler} className="mt-6">
GO BACK
</Button>
{/* </Link> */}
</div>
);
}
export default withRouter(
connect((state: any) => {
const isAssist = window.location.pathname.includes("/assist/");
return {
isAssist,
session: state.getIn(["sessions", "current"]),
siteId: state.getIn(["site", "siteId"]),
sessionPath: state.getIn(["sessions", "sessionPath"]),
};
})(NoSessionPermission)
connect((state: any) => {
const isAssist = window.location.pathname.includes('/assist/');
return {
isAssist,
session: state.getIn(['sessions', 'current']),
sessionPath: state.getIn(['sessions', 'sessionPath']),
};
})(observer(NoSessionPermission))
);

View file

@ -1,7 +1,7 @@
import React from 'react'
import Select from 'Shared/Select';
import { connect } from 'react-redux';
import { setTimezone } from 'Duck/sessions';
import { observer } from 'mobx-react-lite';
import { useStore } from "App/mstore";
const localMachineFormat = new Date().toString().match(/([A-Z]+[\+-][0-9]+)/)[1]
const middlePoint = localMachineFormat.length - 2
@ -13,7 +13,10 @@ const timezoneOptions = {
'UTC': 'UTC'
};
function TimezoneDropdown({ local, setTimezone }) {
function TimezoneDropdown() {
const { sessionStore } = useStore();
const local = sessionStore.timezone;
const setTimezone = sessionStore.setTimezone;
const sortOptions = Object.entries(timezoneOptions)
.map(([ value, label ]) => ({ value, label }));
@ -33,6 +36,4 @@ function TimezoneDropdown({ local, setTimezone }) {
)
}
export default connect(state => ({
local: state.getIn(['sessions', 'timezone']),
}), { setTimezone })(TimezoneDropdown)
export default observer(TimezoneDropdown)

View file

@ -3,22 +3,16 @@ import { combineReducers } from 'redux-immutable';
import user from './user';
import sessions from './sessions';
import sources from './sources';
import site from './site';
import customFields from './customField';
import integrations from './integrations';
import search from './search';
import liveSearch from './liveSearch';
const rootReducer = combineReducers({
user,
sessions,
site,
customFields,
search,
liveSearch,
...integrations,
...sources
});
export type RootStore = ReturnType<typeof rootReducer>

View file

@ -1,46 +0,0 @@
import { fetchListType, fetchType, saveType, editType, initType, removeType } from '../funcTools/types';
export function fetchList(name) {
return {
types: fetchListType(name).array,
call: (client) => client.get(`/integrations/${name}`),
name,
};
}
export function fetch(name, siteId) {
return {
types: fetchType(name).array,
call: (client) => client.get(siteId && name !== 'github' && name !== 'jira' ? `/${siteId}/integrations/${name}` : `/integrations/${name}`),
name,
};
}
export function save(name, siteId, instance) {
return {
types: saveType(name).array,
call: (client) => client.post((siteId ? `/${siteId}` : '') + `/integrations/${name}`, instance.toData()),
};
}
export function edit(name, instance) {
return {
type: editType(name),
instance,
};
}
export function init(name, instance) {
return {
type: initType(name),
instance,
};
}
export function remove(name, siteId) {
return {
types: removeType(name).array,
call: (client) => client.delete((siteId ? `/${siteId}` : '') + `/integrations/${name}`),
siteId,
};
}

View file

@ -1,37 +0,0 @@
import SentryConfig from 'Types/integrations/sentryConfig';
import DatadogConfig from 'Types/integrations/datadogConfig';
import StackdriverConfig from 'Types/integrations/stackdriverConfig';
import RollbarConfig from 'Types/integrations/rollbarConfig';
import NewrelicConfig from 'Types/integrations/newrelicConfig';
import BugsnagConfig from 'Types/integrations/bugsnagConfig';
import CloudWatch from 'Types/integrations/cloudwatchConfig';
import ElasticsearchConfig from 'Types/integrations/elasticsearchConfig';
import SumoLogicConfig from 'Types/integrations/sumoLogicConfig';
import JiraConfig from 'Types/integrations/jiraConfig';
import GithubConfig from 'Types/integrations/githubConfig';
import IssueTracker from 'Types/integrations/issueTracker';
import slack from './slack';
import integrations from './integrations';
import teams from './teams'
import { createIntegrationReducer } from './reducer';
export default {
sentry: createIntegrationReducer('sentry', SentryConfig),
datadog: createIntegrationReducer('datadog', DatadogConfig),
stackdriver: createIntegrationReducer('stackdriver', StackdriverConfig),
rollbar: createIntegrationReducer('rollbar', RollbarConfig),
newrelic: createIntegrationReducer('newrelic', NewrelicConfig),
bugsnag: createIntegrationReducer('bugsnag', BugsnagConfig),
cloudwatch: createIntegrationReducer('cloudwatch', CloudWatch),
elasticsearch: createIntegrationReducer('elasticsearch', ElasticsearchConfig),
sumologic: createIntegrationReducer('sumologic', SumoLogicConfig),
jira: createIntegrationReducer('jira', JiraConfig),
github: createIntegrationReducer('github', GithubConfig),
issues: createIntegrationReducer('issues', IssueTracker),
slack,
teams,
integrations,
};
export * from './actions';

View file

@ -1,40 +0,0 @@
import { Map } from 'immutable';
import { fetchListType } from '../funcTools/types';
import { createRequestReducer } from '../funcTools/request';
const FETCH_LIST = fetchListType('integrations/FETCH_LIST');
const SET_SITE_ID = 'integrations/SET_SITE_ID';
const initialState = Map({
list: [],
siteId: null,
});
const reducer = (state = initialState, action = {}) => {
switch (action.type) {
case FETCH_LIST.success:
return state.set('list', action.data);
case SET_SITE_ID:
return state.set('siteId', action.siteId);
}
return state;
};
export default createRequestReducer(
{
fetchRequest: FETCH_LIST,
},
reducer
);
export function fetchIntegrationList(siteID) {
return {
types: FETCH_LIST.array,
call: (client) => client.get(`/${siteID}/integrations`),
};
}
export function setSiteId(siteId) {
return {
type: SET_SITE_ID,
siteId,
};
}

View file

@ -1,52 +0,0 @@
import { List, Map } from 'immutable';
import { createRequestReducer } from '../funcTools/request';
import { fetchListType, saveType, removeType, editType, initType, fetchType } from '../funcTools/types';
import { createItemInListUpdater } from '../funcTools/tools';
const idKey = 'siteId';
const itemInListUpdater = createItemInListUpdater(idKey);
export const createIntegrationReducer = (name, Config) => {
const FETCH_LIST = fetchListType(name);
const SAVE = saveType(name);
const REMOVE = removeType(name);
const EDIT = editType(name);
const INIT = initType(name);
const FETCH = fetchType(name);
const initialState = Map({
instance: Config(),
list: List(),
fetched: false,
issuesFetched: false,
});
const reducer = (state = initialState, action = {}) => {
switch (action.type) {
case FETCH_LIST.success:
return state
.set('list', Array.isArray(action.data) ? List(action.data).map(Config) : List([new Config(action.data)]))
.set(action.name + 'Fetched', true);
case FETCH.success:
return state.set('instance', Config(action.data || {}));
case SAVE.success:
const config = Config(action.data);
return state.update('list', itemInListUpdater(config)).set('instance', config);
case REMOVE.success:
return state.update('list', (list) => list.filter((site) => site.siteId !== action.siteId)).set('instance', Config());
case EDIT:
return state.mergeIn(['instance'], action.instance);
case INIT:
return state.set('instance', Config(action.instance));
}
return state;
};
return createRequestReducer(
{
// fetchRequest: FETCH_LIST,
fetchRequest: FETCH,
saveRequest: SAVE,
removeRequest: REMOVE,
},
reducer
);
};

View file

@ -1,102 +0,0 @@
import { Map, List } from 'immutable';
import withRequestState, { RequestTypes } from 'Duck/requestStateCreator';
import Config from 'Types/integrations/slackConfig';
import { createItemInListUpdater } from '../funcTools/tools';
const SAVE = new RequestTypes('slack/SAVE');
const UPDATE = new RequestTypes('slack/UPDATE');
const REMOVE = new RequestTypes('slack/REMOVE');
const FETCH_LIST = new RequestTypes('slack/FETCH_LIST');
const SEND_MSG = new RequestTypes('slack/SEND_MSG');
const EDIT = 'slack/EDIT';
const INIT = 'slack/INIT';
const idKey = 'webhookId';
const itemInListUpdater = createItemInListUpdater(idKey);
const initialState = Map({
instance: Config(),
loaded: false,
list: List(),
});
const reducer = (state = initialState, action = {}) => {
switch (action.type) {
case FETCH_LIST.REQUEST:
return state.set('loaded', true);
case FETCH_LIST.SUCCESS:
return state.set('list', List(action.data).map(Config)).set('loaded', true)
case UPDATE.SUCCESS:
case SAVE.SUCCESS:
const config = Config(action.data);
return state.update('list', itemInListUpdater(config)).set('instance', config);
case REMOVE.SUCCESS:
return state.update('list', (list) => list.filter((item) => item.webhookId !== action.id)).set('instance', Config());
case EDIT:
return state.mergeIn(['instance'], action.instance);
case INIT:
return state.set('instance', Config(action.instance));
}
return state;
};
export default withRequestState(
{
fetchRequest: FETCH_LIST,
saveRequest: SAVE,
updateRequest: UPDATE,
removeRequest: REMOVE,
},
reducer
);
export function fetchList() {
return {
types: FETCH_LIST.toArray(),
call: (client) => client.get('/integrations/slack/channels'),
};
}
export function save(instance) {
return {
types: SAVE.toArray(),
call: (client) => client.post(`/integrations/slack`, instance.toData()),
};
}
export function update(instance) {
return {
types: UPDATE.toArray(),
call: (client) => client.post(`/integrations/slack/${instance.webhookId}`, instance.toData()),
};
}
export function edit(instance) {
return {
type: EDIT,
instance,
};
}
export function init(instance) {
return {
type: INIT,
instance,
};
}
export function remove(id) {
return {
types: REMOVE.toArray(),
call: (client) => client.delete(`/integrations/slack/${id}`),
id,
};
}
// https://api.openreplay.com/5587/integrations/slack/notify/315/sessions/7856803626558104
//
export function sendSlackMsg({ integrationId, entity, entityId, data }) {
return {
types: SEND_MSG.toArray(),
call: (client) => client.post(`/integrations/slack/notify/${integrationId}/${entity}/${entityId}`, data)
}
}

View file

@ -1,103 +0,0 @@
import { Map, List } from 'immutable';
import withRequestState, { RequestTypes } from 'Duck/requestStateCreator';
import Config from 'Types/integrations/slackConfig';
import { createItemInListUpdater } from '../funcTools/tools';
const SAVE = new RequestTypes('msteams/SAVE');
const UPDATE = new RequestTypes('msteams/UPDATE');
const REMOVE = new RequestTypes('msteams/REMOVE');
const FETCH_LIST = new RequestTypes('msteams/FETCH_LIST');
const SEND_MSG = new RequestTypes('msteams/SEND_MSG');
const EDIT = 'msteams/EDIT';
const INIT = 'msteams/INIT';
const idKey = 'webhookId';
const itemInListUpdater = createItemInListUpdater(idKey);
const initialState = Map({
instance: Config(),
list: List(),
loaded: false,
});
const reducer = (state = initialState, action = {}) => {
switch (action.type) {
case FETCH_LIST.REQUEST:
return state.set('loaded', true);
case FETCH_LIST.SUCCESS:
return state.set('list', List(action.data).map(Config)).set('loaded', true);
case UPDATE.SUCCESS:
case SAVE.SUCCESS:
const config = Config(action.data);
return state.update('list', itemInListUpdater(config)).set('instance', config);
case REMOVE.SUCCESS:
return state.update('list', (list) => list.filter((item) => item.webhookId !== action.id)).set('instance', Config());
case EDIT:
return state.mergeIn(['instance'], action.instance);
case INIT:
return state.set('instance', Config(action.instance));
}
return state;
};
export default withRequestState(
{
fetchRequest: FETCH_LIST,
saveRequest: SAVE,
updateRequest: UPDATE,
removeRequest: REMOVE,
},
reducer
);
export function fetchList() {
return {
types: FETCH_LIST.toArray(),
call: (client) => client.get('/integrations/msteams/channels'),
};
}
export function save(instance) {
return {
types: SAVE.toArray(),
call: (client) => client.post(`/integrations/msteams`, instance.toData()),
};
}
export function update(instance) {
return {
types: UPDATE.toArray(),
call: (client) => client.post(`/integrations/msteams/${instance.webhookId}`, instance.toData()),
};
}
export function edit(instance) {
return {
type: EDIT,
instance,
};
}
export function init(instance) {
return {
type: INIT,
instance,
};
}
export function remove(id) {
return {
types: REMOVE.toArray(),
call: (client) => client.delete(`/integrations/msteams/${id}`),
id,
};
}
// https://api.openreplay.com/5587/integrations/msteams/notify/315/sessions/7856803626558104
//
export function sendMsTeamsMsg({ integrationId, entity, entityId, data }) {
return {
types: SEND_MSG.toArray(),
call: (client) => client.post(`/integrations/msteams/notify/${integrationId}/${entity}/${entityId}`, data)
}
}

View file

@ -36,12 +36,9 @@ const FETCH_ERROR_STACK = new RequestTypes('sessions/FETCH_ERROR_STACK');
const FETCH_INSIGHTS = new RequestTypes('sessions/FETCH_INSIGHTS');
const FETCH_SESSION_CLICKMAP = new RequestTypes('sessions/FETCH_SESSION_CLICKMAP');
const SORT = 'sessions/SORT';
const REDEFINE_TARGET = 'sessions/REDEFINE_TARGET';
const SET_TIMEZONE = 'sessions/SET_TIMEZONE';
const SET_EVENT_QUERY = 'sessions/SET_EVENT_QUERY';
const SET_AUTOPLAY_VALUES = 'sessions/SET_AUTOPLAY_VALUES';
const TOGGLE_CHAT_WINDOW = 'sessions/TOGGLE_CHAT_WINDOW';
const SET_FUNNEL_PAGE_FLAG = 'sessions/SET_FUNNEL_PAGE_FLAG';
const SET_TIMELINE_POINTER = 'sessions/SET_TIMELINE_POINTER';
const SET_TIMELINE_HOVER_POINTER = 'sessions/SET_TIMELINE_HOVER_POINTER';
@ -75,8 +72,6 @@ const initObj = {
prefetched: false,
eventsAsked: false,
total: 0,
keyMap: Map(),
wdTypeCount: Map(),
favoriteList: List(),
activeTab: Watchdog({ name: 'All', type: 'all' }),
timezone: 'local',
@ -85,13 +80,11 @@ const initObj = {
sourcemapUploaded: true,
filteredEvents: null,
eventsQuery: '',
showChatWindow: false,
liveSessions: [],
visitedEvents: List(),
insights: List(),
insightFilters: defaultDateFilters,
host: '',
funnelPage: Map(),
timelinePointer: null,
sessionPath: {},
lastPlayedSessionId: null,
@ -374,19 +367,12 @@ const reducer = (state = initialState, action: IAction) => {
);
case SET_TIMEZONE:
return state.set('timezone', action.timezone);
case TOGGLE_CHAT_WINDOW:
return state.set('showChatWindow', action.state);
case FETCH_SESSION_CLICKMAP.SUCCESS:
case FETCH_INSIGHTS.SUCCESS:
return state.set(
'insights',
List(action.data).sort((a, b) => b.count - a.count)
);
case SET_FUNNEL_PAGE_FLAG:
return state.set(
'funnelPage',
action.funnelPage ? Map(action.funnelPage) : false
);
case SET_TIMELINE_POINTER:
return state.set('timelinePointer', action.pointer);
case SET_TIMELINE_HOVER_POINTER:
@ -605,13 +591,6 @@ export function fetchLiveList(params = {}) {
};
}
export function toggleChatWindow(state) {
return {
type: TOGGLE_CHAT_WINDOW,
state,
};
}
export function sort(sortKey, sign = 1, listName = 'list') {
return {
type: SORT,
@ -647,13 +626,6 @@ export function setEventFilter(filter) {
};
}
export function setFunnelPage(funnelPage) {
return {
type: SET_FUNNEL_PAGE_FLAG,
funnelPage,
};
}
export function setTimelinePointer(pointer) {
return {
type: SET_TIMELINE_POINTER,

View file

@ -1,164 +0,0 @@
import Site from "Types/site";
import GDPR from 'Types/site/gdpr';
import {
mergeReducers,
success,
array,
createListUpdater
} from './funcTools/tools';
import {
createCRUDReducer,
getCRUDRequestTypes,
createInit,
createEdit,
createRemove,
createUpdate,
saveType
} from './funcTools/crud';
import { createRequestReducer } from './funcTools/request';
import { Map, List, fromJS } from 'immutable';
import { GLOBAL_HAS_NO_RECORDINGS, SITE_ID_STORAGE_KEY } from 'App/constants/storageKeys';
const storedSiteId = localStorage.getItem(SITE_ID_STORAGE_KEY);
const name = 'project';
const idKey = 'id';
const updateItemInList = createListUpdater(idKey);
const EDIT_GDPR = 'sites/EDIT_GDPR';
const SAVE_GDPR = 'sites/SAVE_GDPR';
const FETCH_GDPR = 'sites/FETCH_GDPR';
const FETCH_LIST = 'sites/FETCH_LIST';
const SET_SITE_ID = 'sites/SET_SITE_ID';
const FETCH_GDPR_SUCCESS = success(FETCH_GDPR);
const SAVE_GDPR_SUCCESS = success(SAVE_GDPR);
const FETCH_LIST_SUCCESS = success(FETCH_LIST);
const SAVE = saveType('sites/SAVE');
const UPDATE_PROJECT_RECORDING_STATUS = 'sites/UPDATE_PROJECT_RECORDING_STATUS';
const initialState = Map({
list: List(),
instance: fromJS(),
remainingSites: undefined,
siteId: null,
active: null
});
const reducer = (state = initialState, action = {}) => {
switch (action.type) {
case EDIT_GDPR:
return state.mergeIn(['instance', 'gdpr'], action.gdpr);
case FETCH_GDPR_SUCCESS:
return state.mergeIn(['instance', 'gdpr'], action.data);
case success(SAVE):
const newSite = Site(action.data);
return updateItemInList(state, newSite)
.set('siteId', newSite.get('id'))
.set('active', newSite);
case SAVE_GDPR_SUCCESS:
const gdpr = GDPR(action.data);
return state.setIn(['instance', 'gdpr'], gdpr);
case FETCH_LIST_SUCCESS:
let siteId = state.get('siteId');
const siteIds = action.data.map(s => parseInt(s.projectId));
const siteExists = siteIds.includes(siteId);
if (action.siteIdFromPath && siteIds.includes(parseInt(action.siteIdFromPath))) {
siteId = action.siteIdFromPath;
} else if (!siteId || !siteExists) {
siteId = siteIds.includes(parseInt(storedSiteId))
? storedSiteId
: action.data[0].projectId;
}
const list = List(action.data.map(Site));
const hasRecordings = list.some(s => s.recorded);
if (!hasRecordings) {
localStorage.setItem(GLOBAL_HAS_NO_RECORDINGS, true);
} else {
localStorage.removeItem(GLOBAL_HAS_NO_RECORDINGS);
}
return state.set('list', list)
.set('siteId', siteId)
.set('active', list.find(s => parseInt(s.id) === parseInt(siteId)));
case SET_SITE_ID:
const _siteId = action.siteId ? action.siteId : state.get('list').get(0).id;
localStorage.setItem(SITE_ID_STORAGE_KEY, _siteId);
const site = state.get('list').find(s => parseInt(s.id) == _siteId);
return state.set('siteId', _siteId).set('active', site);
case UPDATE_PROJECT_RECORDING_STATUS:
const { siteId: _siteIdToUpdate, status } = action;
const siteToUpdate = state.get('list').find(s => parseInt(s.id) === parseInt(_siteIdToUpdate));
const updatedSite = siteToUpdate.set('recorded', status);
return updateItemInList(state, updatedSite);
}
return state;
};
export function editGDPR(gdpr) {
return {
type: EDIT_GDPR,
gdpr
};
}
export function fetchGDPR(siteId) {
return {
types: array(FETCH_GDPR),
call: client => client.get(`/${siteId}/gdpr`)
};
}
export const saveGDPR = (siteId, gdpr) => (dispatch, getState) => {
const g = getState().getIn(['site', 'instance', 'gdpr']);
return dispatch({
types: array(SAVE_GDPR),
call: client => client.post(`/${siteId}/gdpr`, g.toData())
});
};
export function fetchList(siteId) {
return {
types: array(FETCH_LIST),
call: client => client.get('/projects'),
siteIdFromPath: siteId
};
}
export function save(site) {
return {
types: array(SAVE),
call: client => client.post(`/projects`, site.toData())
};
}
// export const fetchList = createFetchList(name);
export const init = createInit(name);
export const edit = createEdit(name);
// export const save = createSave(name);
export const update = createUpdate(name);
export const remove = createRemove(name);
export function setSiteId(siteId) {
return {
type: SET_SITE_ID,
siteId
};
}
export const updateProjectRecordingStatus = (siteId, status) => {
return {
type: UPDATE_PROJECT_RECORDING_STATUS,
siteId,
status
};
};
export default mergeReducers(
reducer,
createCRUDReducer(name, Site, idKey),
createRequestReducer({
saveGDPR: SAVE_GDPR,
...getCRUDRequestTypes(name)
})
);

View file

@ -1,24 +0,0 @@
import { fromJS, Map, List } from 'immutable';
import listSourceCreator, { getAction } from './listSourceCreator';
const filtersFromJS = data => fromJS(data)
.update('USERDEVICE', list => list.filter(value => value !== ""))
.update('FID0', list => list.filter(value => value !== ""))
export default {
values: listSourceCreator('values', '/events/values', ({ value }) => value),
selectors: listSourceCreator('selectors', '/events/selectors', ({ targetSelector }) => targetSelector),
filterValues: listSourceCreator('filterValues', '/sessions/filters', filtersFromJS, true, Map({
USEROS: List(),
USERBROWSER: List(),
USERDEVICE: List(),
FID0: List(),
REFERRER: List(),
USERCOUNTRY: List(),
})),
};
export function fetch(name, params) {
return getAction(name, params);
}

View file

@ -1,39 +0,0 @@
import { List, Map } from 'immutable';
import withRequestState, { RequestTypes } from 'Duck/requestStateCreator';
const actionMap = {};
export default (
name,
endpoint,
fromJS = a => a,
convertFromRoot = false,
customInitialState = Map({ list: List() }),
) => {
const initialState = customInitialState || Map({
list: List(),
});
const FETCH_LIST = new RequestTypes(`${ name }/FETCH_LIST`);
actionMap[ name ] = params => ({
types: FETCH_LIST.toArray(),
call: client => client.get(endpoint, params),
});
const reducer = (state = initialState, action = {}) => {
switch (action.type) {
case FETCH_LIST.SUCCESS:
return convertFromRoot
? state.merge(fromJS(action.data))
: state.set('list', List(action.data).map(fromJS).toSet().toList()); // ??
}
return state;
};
return withRequestState(FETCH_LIST, reducer);
};
export function getAction(name, params) {
return actionMap[ name ](params);
}

Some files were not shown because too many files have changed in this diff Show more