Merge remote-tracking branch 'origin/redux-toolkit-move' into rtm-temp
This commit is contained in:
commit
13f24ea6f4
116 changed files with 2473 additions and 2783 deletions
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
109
frontend/app/components/Client/Integrations/IntegrationForm.tsx
Normal file
109
frontend/app/components/Client/Integrations/IntegrationForm.tsx
Normal 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);
|
||||
|
|
@ -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 application’s store, monitor queries, track performance issues and even
|
||||
assist your end user through live sessions.
|
||||
Plugins capture your application’s store, monitor queries, track
|
||||
performance issues and even assist your end user through live sessions.
|
||||
</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 />,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import React from 'react';
|
||||
import { Tooltip, Button } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import { init, remove, fetchGDPR } from 'Duck/site';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { connect } from 'react-redux';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import NewSiteForm from '../NewSiteForm';
|
||||
|
|
@ -10,16 +9,15 @@ import NewSiteForm from '../NewSiteForm';
|
|||
const PERMISSION_WARNING = 'You don’t have the permissions to perform this action.';
|
||||
const LIMIT_WARNING = 'You have reached site limit.';
|
||||
|
||||
function AddProjectButton({ isAdmin = false, init = () => {} }: any) {
|
||||
const { userStore } = useStore();
|
||||
function AddProjectButton({ isAdmin = false }: any) {
|
||||
const { userStore, projectsStore } = useStore();
|
||||
const init = projectsStore.initProject;
|
||||
const { showModal, hideModal } = useModal();
|
||||
const limtis = useObserver(() => userStore.limits);
|
||||
const canAddProject = useObserver(
|
||||
() => isAdmin && (limtis.projects === -1 || limtis.projects > 0)
|
||||
);
|
||||
const limits = userStore.limits;
|
||||
const canAddProject = isAdmin && (limits.projects === -1 || limits.projects > 0)
|
||||
|
||||
const onClick = () => {
|
||||
init();
|
||||
init({});
|
||||
showModal(<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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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)));
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
|
|
|
|||
|
|
@ -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, }
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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']),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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']),
|
||||
};
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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']),
|
||||
};
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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']) || [],
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }} />
|
||||
}))
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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))
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './TrackerUpdateMessage'
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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 don’t have the necessary
|
||||
permissions to access this feature. Please check with your
|
||||
admin.
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
You don’t 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 don’t have the necessary
|
||||
permissions to access this feature. Please check with your admin.
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
You don’t 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))
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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
|
||||
);
|
||||
};
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
);
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Reference in a new issue