getting rid of redux for good (#2556)

* start moving ui to redux tlk

* remove unused reducer

* changes for gdpr and site types

* ui: migrating duck/roles to mobx

* ui: drop unreferenced types

* ui: drop unreferenced types

* ui: move player slice reducer to mobx family

* ui: move assignments to issueReportingStore.ts

* remove issues store

* some fixes after issues store

* remove errors reducer, drop old components

* finish removing errors reducer

* start moving integrations state to mobx

* change(ui): funnel duck cleanup

* change(ui): custom fields

* change(ui): customMetrics cleanup

* change(ui): customMetrics cleanup

* change(ui): duck/filters minor cleanup

* change(ui): duck/filters cleanup

* change(ui): duck/customMetrics cleanup and upgrades

* fix integrations service, fix babel config to >.25 + not ie

* refactoring integrations reducers etc WIP

* finish removing integrations state

* some fixes for integrated check

* start of projects refactoring

* move api and "few" files to new project store

* new batch for site -> projects

* fix setid context

* move all critical components, drop site duck

* remove all duck/site refs, remove old components

* fixup for SessionTags.tsx, remove duck/sources (?)

* move session store

* init sessionstore outside of context

* fix userfilter

* replace simple actions for session store

* sessions sotre

* Rtm temp (#2597)

* change(ui): duck/search wip

* change(ui): duck/search wip

* change(ui): duck/search wip

* change(ui): duck/searchLive wip

* change(ui): duck/searchLive wip

* change(ui): duck/searchLive wip

* change(ui): duck/searchLive wip

* change(ui): search states

* change(ui): search states

* change(ui): search states

* change(ui): fix savedSearch store

* change(ui): fix savedSearch store

* some fixes for session connector

* change(ui): fix savedSearch store

* change(ui): fix searchLive

* change(ui): fix searchLive

* fixes for session replay

* change(ui): bookmark fetch

* last components for sessions

* add fetchautoplaylist

* finish session reducer, remove deleted reducers

* change(ui): fix the search fetch

* change(ui): fix the search fetch

* fix integrations call ctx

* ensure ctx for sessionstore

* fix(ui): checking for latest sessions path

* start removing user reducer

* removing user reducer pt2...

* finish user store

* remove rand log

* fix crashes

* tinkering workflow file for tracker test

* making sure prefetched sessions work properly

* fix conflict

* fix router redirects during loading

---------

Co-authored-by: Shekar Siri <sshekarsiri@gmail.com>
This commit is contained in:
Delirium 2024-10-03 11:38:36 +02:00 committed by GitHub
parent 70a337f766
commit a71381da40
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
431 changed files with 9681 additions and 17014 deletions

View file

@ -9,24 +9,16 @@ on:
pull_request:
branches: [ "dev", "main" ]
paths:
- frontend/**
- tracker/**
jobs:
build-and-test:
runs-on: macos-latest
name: Build and test Tracker
strategy:
matrix:
node-version: [ 18.x ]
steps:
- uses: oven-sh/setup-bun@v1
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Cache tracker modules
uses: actions/cache@v3
with:

1
frontend/.browserslistrc Normal file
View file

@ -0,0 +1 @@
> 0.25% and not dead

View file

@ -1,15 +1,15 @@
import React, { lazy, Suspense } from 'react';
import { Switch, Route } from 'react-router-dom';
import { connect } from 'react-redux';
import { Loader } from 'UI';
import withSiteIdUpdater from 'HOCs/withSiteIdUpdater';
import * as routes from './routes';
import { Map } from 'immutable';
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')),
@ -29,20 +29,16 @@ const LIVE_SESSION_PATH = routes.liveSession();
interface Props {
isEnterprise: boolean;
tenantId: string;
siteId: string;
jwt: string;
sites: Map<string, any>;
onboarding: boolean;
isJwt?: boolean;
isLoggedIn?: boolean;
loading: boolean;
}
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 { isJwt = false, isLoggedIn = false, loading } = props;
const siteIdList: any = sites.map(({ id }) => id);
if (isLoggedIn) {
return (
@ -72,14 +68,4 @@ 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);
export default observer(IFrameRoutes);

View file

@ -1,16 +1,13 @@
import withSiteIdUpdater from 'HOCs/withSiteIdUpdater';
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';
import APIClient from './api_client';
import { getScope } from './duck/user';
import * as routes from './routes';
const components: any = {
@ -108,21 +105,18 @@ const SPOTS_LIST_PATH = routes.spotsList();
const SPOT_PATH = routes.spot();
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;
function PrivateRoutes() {
const { projectsStore, userStore } = useStore();
const onboarding = userStore.onboarding;
const scope = userStore.scopeState;
const tenantId = userStore.account.tenantId;
const sites = projectsStore.list;
const siteId = projectsStore.siteId;
const hasRecordings = sites.some(s => s.recorded);
const redirectToSetup = props.scope === 0;
const redirectToSetup = 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)) && scope > 0;
const siteIdList: any = sites.map(({ id }) => id);
return (
<Suspense fallback={<Loader loading={true} className="flex-1" />}>
@ -151,7 +145,7 @@ function PrivateRoutes(props: Props) {
path={SPOT_PATH}
component={enhancedComponents.Spot}
/>
{props.scope === 1 ? <Redirect to={SPOTS_LIST_PATH} /> : null}
{scope === 1 ? <Redirect to={SPOTS_LIST_PATH} /> : null}
<Route
path="/integrations/"
render={({ location }) => {
@ -160,13 +154,13 @@ function PrivateRoutes(props: Props) {
case '/integrations/slack':
client.post('integrations/slack/add', {
code: location.search.split('=')[1],
state: props.tenantId,
state: tenantId,
});
break;
case '/integrations/msteams':
client.post('integrations/msteams/add', {
code: location.search.split('=')[1],
state: props.tenantId,
state: tenantId,
});
break;
}
@ -283,16 +277,12 @@ function PrivateRoutes(props: Props) {
{Object.entries(routes.redirects).map(([fr, to]) => (
<Redirect key={fr} exact strict from={fr} to={to} />
))}
<Redirect to={withSiteId(routes.sessions(), siteId)} />
<Route path={"*"}>
<Redirect to={withSiteId(routes.sessions(), siteId)} />
</Route>
</Switch>
</Suspense>
);
}
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);
export default observer(PrivateRoutes);

View file

@ -3,7 +3,8 @@ import { Loader } from 'UI';
import { Redirect, Route, Switch } from 'react-router-dom';
import Signup from 'Components/Signup/Signup';
import SupportCallout from 'Shared/SupportCallout';
import { connect } from 'react-redux';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import * as routes from 'App/routes';
@ -14,16 +15,12 @@ const SPOT_PATH = routes.spot();
const Login = lazy(() => import('Components/Login/Login'));
const ForgotPassword = lazy(() => import('Components/ForgotPassword/ForgotPassword'));
const UpdatePassword = lazy(() => import('Components/UpdatePassword/UpdatePassword'));
const Spot = lazy(() => import('Components/Spots/SpotPlayer/SpotPlayer'));
interface Props {
isEnterprise: boolean;
changePassword: boolean;
}
function PublicRoutes(props: Props) {
const hideSupport = props.isEnterprise || location.pathname.includes('spots') || location.pathname.includes('view-spot')
function PublicRoutes() {
const { userStore } = useStore();
const isEnterprise = userStore.isEnterprise;
const hideSupport = isEnterprise || location.pathname.includes('spots') || location.pathname.includes('view-spot')
return (
<Suspense fallback={<Loader loading={true} className='flex-1' />}>
<Switch>
@ -39,9 +36,4 @@ function PublicRoutes(props: Props) {
}
export default connect((state: any) => ({
changePassword: state.getIn(['user', 'account', 'changePassword']),
isEnterprise:
state.getIn(['user', 'account', 'edition']) === 'ee' ||
state.getIn(['user', 'authDetails', 'edition']) === 'ee'
}))(PublicRoutes);
export default observer(PublicRoutes)

View file

@ -1,6 +1,4 @@
import { Map } from 'immutable';
import React, { useEffect, useRef } from 'react';
import { ConnectedProps, connect } from 'react-redux';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import IFrameRoutes from 'App/IFrameRoutes';
@ -10,60 +8,49 @@ import {
GLOBAL_DESTINATION_PATH,
IFRAME,
JWT_PARAM,
SPOT_ONBOARDING,
SPOT_ONBOARDING
} from 'App/constants/storageKeys';
import Layout from 'App/layout/Layout';
import { 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 { fetchListActive as fetchMetadata } from 'Duck/customField';
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;
interface RouterProps extends RouteComponentProps {
match: {
params: {
siteId: string;
};
};
mstore: any;
setJwt: (params: { jwt: string; spotJwt: string | null }) => any;
fetchMetadata: (siteId: string) => void;
initSite: (site: any) => void;
scopeSetup: boolean;
localSpotJwt: string | null;
}
const Router: React.FC<RouterProps> = (props) => {
const {
isLoggedIn,
siteId,
sites,
loading,
location,
fetchUserInfo,
fetchSiteList,
history,
setSessionPath,
scopeSetup,
localSpotJwt,
logout,
} = props;
const mstore = useStore();
const { customFieldStore, projectsStore, sessionStore, searchStore, userStore } = mstore;
const jwt = userStore.jwt;
const changePassword = userStore.account.changePassword;
const userInfoLoading = userStore.fetchInfoRequest.loading;
const scopeSetup = userStore.scopeState === 0;
const localSpotJwt = userStore.spotJwt;
const isLoggedIn = Boolean(jwt && !changePassword);
const fetchUserInfo = userStore.fetchUserInfo;
const setJwt = userStore.updateJwt;
const logout = userStore.logout;
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');
@ -81,7 +68,7 @@ const Router: React.FC<RouterProps> = (props) => {
handleSpotLogin(spotJwt);
}
if (urlJWT) {
props.setJwt({ jwt: urlJWT, spotJwt: spotJwt ?? null });
setJwt({ jwt: urlJWT, spotJwt: spotJwt ?? null });
}
};
@ -109,9 +96,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);
@ -141,6 +128,7 @@ const Router: React.FC<RouterProps> = (props) => {
useEffect(() => {
checkParams();
handleJwtFromUrl();
mstore.initClient();
}, []);
useEffect(() => {
@ -169,18 +157,23 @@ const Router: React.FC<RouterProps> = (props) => {
if (localSpotJwt && !isTokenExpired(localSpotJwt)) {
handleSpotLogin(localSpotJwt);
} else {
logout();
void logout();
}
}
}, [isSpotCb, isLoggedIn, localSpotJwt, isSignup]);
useEffect(() => {
if (siteId && siteId !== lastFetchedSiteIdRef.current) {
const activeSite = sites.find((s) => s.id == siteId);
props.initSite(activeSite);
props.fetchMetadata(siteId);
lastFetchedSiteIdRef.current = siteId;
}
const fetchData = async () => {
if (siteId && siteId !== lastFetchedSiteIdRef.current) {
const activeSite = sites.find((s) => s.id == siteId);
initSite(activeSite ?? {});
lastFetchedSiteIdRef.current = activeSite?.id;
await customFieldStore.fetchListActive(siteId + '');
await searchStore.fetchSavedSearchList()
}
};
void fetchData();
}, [siteId]);
const lastFetchedSiteIdRef = useRef<any>(null);
@ -225,51 +218,4 @@ 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([
'user',
'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,
localSpotJwt: state.getIn(['user', 'spotJwt']),
isLoggedIn: jwt !== null && !changePassword,
scopeSetup,
loading,
email: state.getIn(['user', 'account', 'email']),
account: state.getIn(['user', 'account']),
organisation: state.getIn(['user', 'account', 'name']),
tenantId: state.getIn(['user', 'account', 'tenantId']),
tenants: state.getIn(['user', 'tenants']),
isEnterprise:
state.getIn(['user', 'account', 'edition']) === 'ee' ||
state.getIn(['user', 'authDetails', 'edition']) === 'ee',
};
};
const mapDispatchToProps = {
fetchUserInfo,
setSessionPath,
fetchSiteList,
setJwt,
fetchMetadata,
initSite,
logout,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
export default withStore(withRouter(connector(Router)));
export default withRouter(observer(Router));

View file

@ -1,6 +1,4 @@
import store from 'App/store';
import { queried } from './routes';
import { setJwt } from 'Duck/user';
const siteIdRequiredPaths: string[] = [
'/dashboard',
@ -54,27 +52,42 @@ 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 getJwt: () => string | null = () => null;
private onUpdateJwt: (data: { jwt?: string, spotJwt?: string }) => void;
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',
'Content-Type': 'application/json'
})
};
}
setJwt(jwt: string | null): void {
if (jwt !== null) {
(this.init.headers as Headers).set('Authorization', `Bearer ${jwt}`);
}
this.siteId = siteId;
}
setOnUpdateJwt(onUpdateJwt: (data: { jwt?: string, spotJwt?: string }) => void): void {
this.onUpdateJwt = onUpdateJwt;
}
setJwtChecker(checker: () => string | null): void {
this.getJwt = checker;
}
setSiteIdCheck(checker: () => { siteId: string | null }): void {
this.siteIdCheck = checker
}
private getInit(method: string = 'GET', params?: any, reqHeaders?: Record<string, any>): RequestInit {
// Always fetch the latest JWT from the store
const jwt = store.getState().getIn(['user', 'jwt']);
const jwt = this.getJwt()
const headers = new Headers({
'Accept': 'application/json',
'Content-Type': 'application/json',
@ -101,6 +114,9 @@ export default class APIClient {
delete init.body; // GET requests shouldn't have a body
}
// /:id/path
// const idFromPath = window.location.pathname.split('/')[1];
this.siteId = this.siteIdCheck?.().siteId ?? undefined;
return init;
}
@ -131,7 +147,7 @@ export default class APIClient {
clean?: boolean
} = { clean: true }, headers?: Record<string, any>): Promise<Response> {
let _path = path;
let jwt = store.getState().getIn(['user', 'jwt']);
let jwt = this.getJwt();
if (!path.includes('/refresh') && jwt && this.isTokenExpired(jwt)) {
jwt = await this.handleTokenRefresh();
(this.init.headers as Headers).set('Authorization', `Bearer ${jwt}`);
@ -158,9 +174,9 @@ export default class APIClient {
path !== '/targets_temp' &&
!path.includes('/metadata/session_search') &&
!path.includes('/assist/credentials') &&
!!this.siteId &&
siteIdRequiredPaths.some(sidPath => path.startsWith(sidPath))
) {
if (!this.siteId) console.trace('no id', path)
edp = `${edp}/${this.siteId}`;
}
@ -202,11 +218,11 @@ export default class APIClient {
const data = await response.json();
const refreshedJwt = data.jwt;
store.dispatch(setJwt({ jwt: refreshedJwt, }));
this.onUpdateJwt({ jwt: refreshedJwt });
return refreshedJwt;
} catch (error) {
console.error('Error refreshing token:', error);
store.dispatch(setJwt({ jwt: null }));
this.onUpdateJwt({ jwt: undefined });
throw error;
}
}

View file

@ -1,60 +0,0 @@
import logger from 'App/logger';
import APIClient from './api_client';
import { FETCH_ACCOUNT, UPDATE_JWT } from 'Duck/user';
import { handleSpotJWT } from "App/utils";
export default () => {
return (next: any) => async (action: any) => {
const { types, call, ...rest } = action;
if (!call) {
return next(action);
}
const [REQUEST, SUCCESS, FAILURE] = types;
next({ ...rest, type: REQUEST });
try {
const client = new APIClient();
const response = await call(client);
if (!response.ok) {
const text = await response.text();
throw new Error(text);
}
const json = await response.json() || {}; // TEMP TODO on server: no empty responses
const { jwt, spotJwt, errors, data } = json;
if (errors) {
next({ type: FAILURE, errors, data });
} else {
next({ type: SUCCESS, data, ...rest });
}
if (jwt) {
next({ type: UPDATE_JWT, data: { jwt } });
}
if (spotJwt) {
handleSpotJWT(spotJwt);
}
} catch (e) {
if (e.response?.status === 403) {
next({ type: FETCH_ACCOUNT.FAILURE });
}
const data = await e.response?.json();
logger.error('Error during API request. ', e);
return next({ type: FAILURE, errors: data ? parseError(data.errors) : [] });
}
};
};
export function parseError(e: any) {
try {
return [...JSON.parse(e).errors] || [];
} catch {
return Array.isArray(e) ? e : [e];
}
}

View file

@ -1,28 +1,15 @@
import React from 'react';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import withPageTitle from 'HOCs/withPageTitle';
import withPermissions from 'HOCs/withPermissions';
import AssistRouter from './AssistRouter';
import { connect } from 'react-redux';
interface Props extends RouteComponentProps {
siteId: string;
history: any;
isEnterprise: boolean;
}
function Assist(props: Props) {
function Assist() {
return (
<AssistRouter />
);
}
const Cont = connect((state: any) => ({
isEnterprise:
state.getIn(['user', 'account', 'edition']) === 'ee' ||
state.getIn(['user', 'authDetails', 'edition']) === 'ee'
}))(Assist);
export default withPageTitle('Assist - OpenReplay')(
withPermissions(['ASSIST_LIVE', 'SERVICE_ASSIST_LIVE'], '', false, false)(withRouter(Cont))
withPermissions(['ASSIST_LIVE', 'SERVICE_ASSIST_LIVE'], '', false, false)(Assist)
);

View file

@ -1,12 +1,7 @@
import React from 'react';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import AssistView from './AssistView'
interface Props extends RouteComponentProps {
match: any;
}
function AssistRouter(props: Props) {
function AssistRouter() {
return (
<div className="w-full">
<AssistView />
@ -14,4 +9,4 @@ function AssistRouter(props: Props) {
);
}
export default withRouter(AssistRouter);
export default AssistRouter;

View file

@ -1,55 +1,46 @@
import React from 'react';
import { connect } from 'react-redux';
import {
addFilterByKeyAndValue,
clearSearch,
edit as editFilter,
fetchFilterSearch,
} from 'Duck/liveSearch';
import { Button } from 'antd';
import { useModal } from 'App/components/Modal';
import SessionSearchField from 'Shared/SessionSearchField';
import { MODULES } from 'Components/Client/Modules';
import AssistStats from '../../AssistStats';
import Recordings from '../RecordingsList/Recordings'
import Recordings from '../RecordingsList/Recordings';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
interface Props {
appliedFilter: any;
fetchFilterSearch: any;
addFilterByKeyAndValue: any;
clearSearch: any;
isEnterprise: boolean;
modules: string[]
}
function AssistSearchField(props: Props) {
function AssistSearchField() {
const { searchStoreLive, userStore } = useStore();
const modules = userStore.account.settings?.modules ?? [];
const isEnterprise = userStore.isEnterprise
const hasEvents =
props.appliedFilter.filters.filter((i: any) => i.isEvent).size > 0;
searchStoreLive.instance.filters.filter((i: any) => i.isEvent).length > 0;
const hasFilters =
props.appliedFilter.filters.filter((i: any) => !i.isEvent).size > 0;
const { showModal, hideModal } = useModal();
searchStoreLive.instance.filters.filter((i: any) => !i.isEvent).length > 0;
const { showModal } = useModal();
const showStats = () => {
showModal(<AssistStats />, { right: true, width: 960 })
}
showModal(<AssistStats />, { right: true, width: 960 });
};
const showRecords = () => {
showModal(<Recordings />, { right: true, width: 960 })
}
showModal(<Recordings />, { right: true, width: 960 });
};
return (
<div className="flex items-center w-full gap-2">
<div style={{ width: '60%' }}>
<SessionSearchField />
</div>
{props.isEnterprise && props.modules.includes(MODULES.OFFLINE_RECORDINGS)
? <Button type="primary" ghost onClick={showRecords}>Training Videos</Button> : null
{isEnterprise && modules.includes(MODULES.OFFLINE_RECORDINGS)
? <Button type="primary" ghost onClick={showRecords}>Training Videos</Button> : null
}
<Button type="primary" ghost onClick={showStats} disabled={props.modules.includes(MODULES.ASSIST_STATS) || props.modules.includes(MODULES.ASSIST)}>Co-Browsing Reports</Button>
<Button type="primary" ghost onClick={showStats}
disabled={modules.includes(MODULES.ASSIST_STATS) || modules.includes(MODULES.ASSIST)}>Co-Browsing
Reports</Button>
<Button
type="link"
className="ml-auto font-medium"
disabled={!hasFilters && !hasEvents}
onClick={() => props.clearSearch()}
onClick={() => searchStoreLive.clearSearch()}
>
Clear Search
</Button>
@ -57,18 +48,4 @@ function AssistSearchField(props: Props) {
);
}
export default connect(
(state: any) => ({
appliedFilter: state.getIn(['liveSearch', 'instance']),
modules: state.getIn(['user', 'account', 'settings', 'modules']) || [],
isEnterprise:
state.getIn(['user', 'account', 'edition']) === 'ee' ||
state.getIn(['user', 'authDetails', 'edition']) === 'ee'
}),
{
fetchFilterSearch,
editFilter,
addFilterByKeyAndValue,
clearSearch,
}
)(AssistSearchField);
export default observer(AssistSearchField);

View file

@ -4,17 +4,12 @@ import Select from 'Shared/Select';
import RecordingsSearch from './RecordingsSearch';
import RecordingsList from './RecordingsList';
import { useStore } from 'App/mstore';
import { connect } from 'react-redux';
import SelectDateRange from 'Shared/SelectDateRange/SelectDateRange';
import { observer } from 'mobx-react-lite';
interface Props {
userId: string;
}
function Recordings(props: Props) {
const { userId } = props;
const { recordingsStore } = useStore();
function Recordings() {
const { recordingsStore, userStore } = useStore();
const userId = userStore.account.id;
const recordingsOwner = [
{ value: '0', label: 'All Videos' },
@ -51,6 +46,4 @@ function Recordings(props: Props) {
);
}
export default connect((state: any) => ({
userId: state.getIn(['user', 'account', 'id'])
}))(observer(Recordings));
export default observer(Recordings);

View file

@ -1,8 +1,9 @@
import React from 'react';
import { INDEXES } from 'App/constants/zindex';
import { connect } from 'react-redux';
import { Button, Loader, Icon } from 'UI';
import { PlayerContext } from 'App/components/Session/playerContext';
import { useStore } from "App/mstore";
import { observer } from 'mobx-react-lite';
interface Props {
userDisplayName: string;
@ -42,7 +43,9 @@ const WIN_VARIANTS = {
}
};
function RequestingWindow({ userDisplayName, getWindowType }: Props) {
function RequestingWindow({ getWindowType }: Props) {
const { sessionStore } = useStore();
const userDisplayName = sessionStore.current.userDisplayName;
const windowType = getWindowType()
if (!windowType) return;
const { player } = React.useContext(PlayerContext)
@ -81,6 +84,4 @@ function RequestingWindow({ userDisplayName, getWindowType }: Props) {
);
}
export default connect((state: any) => ({
userDisplayName: state.getIn(['sessions', 'current']).userDisplayName,
}))(RequestingWindow);
export default observer(RequestingWindow);

View file

@ -1,6 +1,5 @@
import React, { useState, useEffect } from 'react';
import { Button, Tooltip } from 'UI';
import { connect } from 'react-redux';
import cn from 'classnames';
import ChatWindow from '../../ChatWindow';
import { CallingState, ConnectionStatus, RemoteControlStatus, RequestLocalStream } from 'Player';
@ -12,6 +11,7 @@ import { confirm } from 'UI';
import stl from './AassistActions.module.css';
import ScreenRecorder from 'App/components/Session_/ScreenRecorder/ScreenRecorder';
import { audioContextManager } from 'App/utils/screenRecorder';
import { useStore } from "App/mstore";
function onReject() {
toast.info(`Call was rejected.`);
@ -31,12 +31,9 @@ function onError(e: any) {
interface Props {
userId: string;
hasPermission: boolean;
isEnterprise: boolean;
isCallActive: boolean;
agentIds: string[];
userDisplayName: string;
agentId: number,
}
const AssistActionsPing = {
@ -52,15 +49,17 @@ const AssistActionsPing = {
function AssistActions({
userId,
hasPermission,
isEnterprise,
isCallActive,
agentIds,
userDisplayName,
agentId,
}: Props) {
// @ts-ignore ???
const { player, store } = React.useContext<ILivePlayerContext>(PlayerContext);
const { sessionStore, userStore } = useStore();
const permissions = userStore.account.permissions || [];
const hasPermission = permissions.includes('ASSIST_CALL') || permissions.includes('SERVICE_ASSIST_CALL');
const isEnterprise = userStore.isEnterprise;
const agentId = userStore.account.id;
const userDisplayName = sessionStore.current.userDisplayName;
const {
assistManager: {
@ -289,14 +288,4 @@ function AssistActions({
);
}
const con = connect((state: any) => {
const permissions = state.getIn(['user', 'account', 'permissions']) || [];
return {
hasPermission: permissions.includes('ASSIST_CALL') || permissions.includes('SERVICE_ASSIST_CALL'),
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee',
userDisplayName: state.getIn(['sessions', 'current']).userDisplayName,
agentId: state.getIn(['user', 'account', 'id'])
};
});
export default con(observer(AssistActions));
export default observer(AssistActions);

View file

@ -1,6 +1,6 @@
import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import { fetchLiveList } from 'Duck/sessions';
import { observer } from 'mobx-react-lite'
import { useStore } from 'App/mstore';
import { Loader, NoContent, Label } from 'UI';
import SessionItem from 'Shared/SessionItem';
import { useModal } from 'App/components/Modal';
@ -11,16 +11,20 @@ interface Props {
list: any;
session: any;
userId: any;
fetchLiveList: (params: any) => void;
}
function SessionList(props: Props) {
const { hideModal } = useModal();
const { sessionStore } = useStore();
const fetchLiveList = sessionStore.fetchLiveSessions;
const session = sessionStore.current;
const list = sessionStore.liveSessions.filter((i: any) => i.userId === session.userId && i.sessionId !== session.sessionId);
const loading = sessionStore.loadingLiveSessions;
useEffect(() => {
const params: any = {};
if (props.session.userId) {
params.userId = props.session.userId;
}
props.fetchLiveList(params);
void fetchLiveList(params);
}, []);
return (
@ -33,9 +37,9 @@ function SessionList(props: Props) {
{props.userId}'s <span className="color-gray-medium">Live Sessions</span>{' '}
</div>
</div>
<Loader loading={props.loading}>
<Loader loading={loading}>
<NoContent
show={!props.loading && props.list.length === 0}
show={!loading && list.length === 0}
title={
<div className="flex items-center justify-center flex-col">
<AnimatedSVG name={ICONS.NO_LIVE_SESSIONS} size={60} />
@ -45,7 +49,7 @@ function SessionList(props: Props) {
}
>
<div className="p-4">
{props.list.map((session: any) => (
{list.map((session: any) => (
<div className="mb-6" key={session.sessionId}>
{session.pageTitle && session.pageTitle !== '' && (
<div className="flex items-center mb-2">
@ -65,14 +69,4 @@ function SessionList(props: Props) {
);
}
export default connect(
(state: any) => {
const session = state.getIn(['sessions', 'current']);
return {
session,
list: state.getIn(['sessions', 'liveSessions']).filter((i: any) => i.userId === session.userId && i.sessionId !== session.sessionId),
loading: state.getIn(['sessions', 'fetchLiveListRequest', 'loading']),
};
},
{ fetchLiveList }
)(SessionList);
export default observer(SessionList);

View file

@ -55,7 +55,7 @@ const CustomFieldForm = ({ field, saving, errors, edit, save, onSave, onClose, o
const mapStateToProps = (state) => ({
field: state.getIn(['customFields', 'instance']),
saving: state.getIn(['customFields', 'saveRequest', 'loading']),
errors: state.getIn(['customFields', 'saveRequest', 'errors']),
errors: state.getIn(['customFields', 'saveRequest', 'errors'])
});
export default connect(mapStateToProps, { edit, save })(CustomFieldForm);

View file

@ -0,0 +1,92 @@
import React, { useRef, useState } from 'react';
import { Form, Input, confirm } from 'UI';
import styles from './customFieldForm.module.css';
import { useStore } from 'App/mstore';
import { useModal } from 'Components/Modal';
import { toast } from 'react-toastify';
import { Button } from 'antd';
import { Trash } from 'UI/Icons';
import { observer } from 'mobx-react-lite';
interface CustomFieldFormProps {
siteId: string;
}
const CustomFieldForm: React.FC<CustomFieldFormProps> = ({ siteId }) => {
console.log('siteId', siteId);
const focusElementRef = useRef<HTMLInputElement>(null);
const { customFieldStore: store } = useStore();
const field = store.instance;
const { hideModal } = useModal();
const [loading, setLoading] = useState(false);
const write = ({ target: { value, name } }: any) => store.edit({ [name]: value });
const exists = field?.exists();
const onDelete = async () => {
if (
await confirm({
header: 'Metadata',
confirmation: `Are you sure you want to remove?`
})
) {
store.remove(siteId, field?.index!).then(() => {
hideModal();
});
}
};
const onSave = (field: any) => {
setLoading(true);
store.save(siteId, field).then((response) => {
if (!response || !response.errors || response.errors.size === 0) {
hideModal();
toast.success('Metadata added successfully!');
} else {
toast.error(response.errors[0]);
}
}).finally(() => {
setLoading(false);
});
};
return (
<div className="bg-white h-screen overflow-y-auto">
<h3 className="p-5 text-2xl">{exists ? 'Update' : 'Add'} Metadata Field</h3>
<Form className={styles.wrapper}>
<Form.Field>
<label>{'Field Name'}</label>
<Input
ref={focusElementRef}
name="key"
value={field?.key}
onChange={write}
placeholder="Field Name"
maxLength={50}
/>
</Form.Field>
<div className="flex justify-between">
<div className="flex items-center">
<Button
onClick={() => onSave(field)}
disabled={!field?.validate()}
loading={loading}
type="primary"
className="float-left mr-2"
>
{exists ? 'Update' : 'Add'}
</Button>
<Button data-hidden={!exists} onClick={hideModal}>
{'Cancel'}
</Button>
</div>
<Button type="text" icon={<Trash />} data-hidden={!exists} onClick={onDelete}></Button>
</div>
</Form>
</div>
);
};
export default observer(CustomFieldForm);

View file

@ -14,124 +14,124 @@ import { useModal } from 'App/components/Modal';
import { toast } from 'react-toastify';
function CustomFields(props) {
const [currentSite, setCurrentSite] = React.useState(props.sites.get(0));
const [deletingItem, setDeletingItem] = React.useState(null);
const { showModal, hideModal } = useModal();
const [currentSite, setCurrentSite] = React.useState(props.sites.get(0));
const [deletingItem, setDeletingItem] = React.useState(null);
const { showModal, hideModal } = useModal();
useEffect(() => {
const activeSite = props.sites.get(0);
if (!activeSite) return;
useEffect(() => {
const activeSite = props.sites.get(0);
if (!activeSite) return;
props.fetchList(activeSite.id);
}, []);
props.fetchList(activeSite.id);
}, []);
const save = (field) => {
props.save(currentSite.id, field).then((response) => {
if (!response || !response.errors || response.errors.size === 0) {
hideModal();
toast.success('Metadata added successfully!');
} else {
toast.error(response.errors[0]);
}
const save = (field) => {
props.save(currentSite.id, field).then((response) => {
if (!response || !response.errors || response.errors.size === 0) {
hideModal();
toast.success('Metadata added successfully!');
} else {
toast.error(response.errors[0]);
}
});
};
const init = (field) => {
props.init(field);
showModal(<CustomFieldForm onClose={hideModal} onSave={save} onDelete={() => removeMetadata(field)} />);
};
const onChangeSelect = ({ value }) => {
const site = props.sites.find((s) => s.id === value.value);
setCurrentSite(site);
props.fetchList(site.id);
};
const removeMetadata = async (field) => {
if (
await confirm({
header: 'Metadata',
confirmation: `Are you sure you want to remove?`
})
) {
setDeletingItem(field.index);
props
.remove(currentSite.id, field.index)
.then(() => {
hideModal();
})
.finally(() => {
setDeletingItem(null);
});
};
}
};
const init = (field) => {
props.init(field);
showModal(<CustomFieldForm onClose={hideModal} onSave={save} onDelete={() => removeMetadata(field)} />);
};
const onChangeSelect = ({ value }) => {
const site = props.sites.find((s) => s.id === value.value);
setCurrentSite(site);
props.fetchList(site.id);
};
const removeMetadata = async (field) => {
if (
await confirm({
header: 'Metadata',
confirmation: `Are you sure you want to remove?`,
})
) {
setDeletingItem(field.index);
props
.remove(currentSite.id, field.index)
.then(() => {
hideModal();
})
.finally(() => {
setDeletingItem(null);
});
}
};
const { fields, loading } = props;
return (
<div className="bg-white rounded-lg shadow-sm border p-5 ">
<div className={cn(styles.tabHeader)}>
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Metadata'}</h3>
<div style={{ marginRight: '15px' }}>
<SiteDropdown value={currentSite && currentSite.id} onChange={onChangeSelect} />
</div>
<div className="ml-auto">
<Tooltip title="You've reached the limit of 10 metadata." disabled={fields.size < 10}>
<Button disabled={fields.size >= 10} variant="primary" onClick={() => init()}>Add Metadata</Button>
</Tooltip>
</div>
</div>
<div className="text-base text-disabled-text flex px-5 items-center my-3">
<Icon name="info-circle-fill" className="mr-2" size={16} />
See additonal user information in sessions.
<a href="https://docs.openreplay.com/installation/metadata" className="link ml-1" target="_blank">Learn more</a>
</div>
<Loader loading={loading}>
<NoContent
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.NO_METADATA} size={60} />
{/* <div className="mt-4" /> */}
<div className="text-center my-4">None added yet</div>
</div>
}
size="small"
show={fields.size === 0}
>
<div className={styles.list}>
{fields
.filter((i) => i.index)
.map((field) => (
<>
<ListItem
disabled={deletingItem && deletingItem === field.index}
key={field._key}
field={field}
onEdit={init}
// onDelete={ () => removeMetadata(field) }
/>
<Divider className="m-0" />
</>
))}
</div>
</NoContent>
</Loader>
const { fields, loading } = props;
return (
<div className="bg-white rounded-lg shadow-sm border p-5 ">
<div className={cn(styles.tabHeader)}>
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Metadata'}</h3>
<div style={{ marginRight: '15px' }}>
<SiteDropdown value={currentSite && currentSite.id} onChange={onChangeSelect} />
</div>
);
<div className="ml-auto">
<Tooltip title="You've reached the limit of 10 metadata." disabled={fields.size < 10}>
<Button disabled={fields.size >= 10} variant="primary" onClick={() => init()}>Add Metadata</Button>
</Tooltip>
</div>
</div>
<div className="text-base text-disabled-text flex px-5 items-center my-3">
<Icon name="info-circle-fill" className="mr-2" size={16} />
See additonal user information in sessions.
<a href="https://docs.openreplay.com/installation/metadata" className="link ml-1" target="_blank">Learn more</a>
</div>
<Loader loading={loading}>
<NoContent
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.NO_METADATA} size={60} />
{/* <div className="mt-4" /> */}
<div className="text-center my-4">None added yet</div>
</div>
}
size="small"
show={fields.size === 0}
>
<div className={styles.list}>
{fields
.filter((i) => i.index)
.map((field) => (
<>
<ListItem
disabled={deletingItem && deletingItem === field.index}
key={field._key}
field={field}
onEdit={init}
// onDelete={ () => removeMetadata(field) }
/>
<Divider className="m-0" />
</>
))}
</div>
</NoContent>
</Loader>
</div>
);
}
export default connect(
(state) => ({
fields: state.getIn(['customFields', 'list']).sortBy((i) => i.index),
field: state.getIn(['customFields', 'instance']),
loading: state.getIn(['customFields', 'fetchRequest', 'loading']),
sites: state.getIn(['site', 'list']),
errors: state.getIn(['customFields', 'saveRequest', 'errors']),
}),
{
init,
fetchList,
save,
remove,
}
(state) => ({
fields: state.getIn(['customFields', 'list']).sortBy((i) => i.index),
field: state.getIn(['customFields', 'instance']),
loading: state.getIn(['customFields', 'fetchRequest', 'loading']),
sites: state.getIn(['site', 'list']),
errors: state.getIn(['customFields', 'saveRequest', 'errors'])
}),
{
init,
fetchList,
save,
remove
}
)(withPageTitle('Metadata - OpenReplay Preferences')(CustomFields));

View file

@ -0,0 +1,108 @@
import React, { useEffect, useState } from 'react';
import cn from 'classnames';
import withPageTitle from 'HOCs/withPageTitle';
import { Button, Loader, NoContent, Icon, Tooltip, Divider } from 'UI';
import SiteDropdown from 'Shared/SiteDropdown';
import styles from './customFields.module.css';
import CustomFieldForm from './CustomFieldForm';
import ListItem from './ListItem';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import { useModal } from 'App/components/Modal';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
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 fields = store.list;
const [loading, setLoading] = useState(false);
useEffect(() => {
const activeSite = sites[0];
if (!activeSite) return;
setCurrentSite(activeSite);
setLoading(true);
store.fetchList(activeSite.id).finally(() => {
setLoading(false);
});
}, [sites]);
const handleInit = (field?: any) => {
console.log('field', field);
store.init(field);
showModal(<CustomFieldForm siteId={currentSite.id} />, {
title: field ? 'Edit Metadata' : 'Add Metadata', right: true
});
};
const onChangeSelect = ({ value }: { value: { value: number } }) => {
const site = sites.find((s: any) => s.id === value.value);
setCurrentSite(site);
setLoading(true);
store.fetchList(site.id).finally(() => {
setLoading(false);
});
};
return (
<div className="bg-white rounded-lg shadow-sm border p-5">
<div className={cn(styles.tabHeader)}>
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Metadata'}</h3>
<div style={{ marginRight: '15px' }}>
<SiteDropdown value={currentSite && currentSite.id} onChange={onChangeSelect} />
</div>
<div className="ml-auto">
<Tooltip title="You've reached the limit of 10 metadata." disabled={fields.length < 10}>
<Button disabled={fields.length >= 10} variant="primary" onClick={() => handleInit()}>
Add Metadata
</Button>
</Tooltip>
</div>
</div>
<div className="text-base text-disabled-text flex px-5 items-center my-3">
<Icon name="info-circle-fill" className="mr-2" size={16} />
See additional user information in sessions.
<a href="https://docs.openreplay.com/installation/metadata" className="link ml-1" target="_blank">
Learn more
</a>
</div>
<Loader loading={loading}>
<NoContent
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.NO_METADATA} size={60} />
<div className="text-center my-4">None added yet</div>
</div>
}
size="small"
show={fields.length === 0}
>
<div className={styles.list}>
{fields
.filter((i: any) => i.index)
.map((field: any) => (
<>
<ListItem
disabled={deletingItem !== null && deletingItem === field.index}
key={field._key}
field={field}
onEdit={handleInit}
/>
<Divider className="m-0" />
</>
))}
</div>
</NoContent>
</Loader>
</div>
);
};
export default withPageTitle('Metadata - OpenReplay Preferences')(observer(CustomFields));

View file

@ -4,23 +4,23 @@ import { Button } from 'UI';
import styles from './listItem.module.css';
const ListItem = ({ field, onEdit, disabled }) => {
return (
<div
className={cn(
'group hover:bg-active-blue flex items-center justify-between py-3 px-5 cursor-pointer',
field.index === 0 ? styles.preDefined : '',
{
[styles.disabled]: disabled,
}
)}
onClick={() => field.index != 0 && onEdit(field)}
>
<span>{field.key}</span>
<div className="invisible group-hover:visible" data-hidden={field.index === 0}>
<Button variant="text-primary" icon="pencil" />
</div>
</div>
);
return (
<div
className={cn(
'group hover:bg-active-blue flex items-center justify-between py-3 px-5 cursor-pointer',
field.index === 0 ? styles.preDefined : '',
{
[styles.disabled]: disabled
}
)}
onClick={() => field.index !== 0 && onEdit(field)}
>
<span>{field.key}</span>
<div className="invisible group-hover:visible" data-hidden={field.index === 0}>
<Button variant="text-primary" icon="pencil" />
</div>
</div>
);
};
export default ListItem;

View file

@ -1,10 +1,11 @@
import { useStore } from "App/mstore";
import React from 'react';
import DocLink from 'Shared/DocLink/DocLink';
import AssistScript from './AssistScript';
import AssistNpm from './AssistNpm';
import { Tabs, CodeBlock } from 'UI';
import { useState } from 'react';
import { connect } from 'react-redux';
import { observer } from 'mobx-react-lite'
const NPM = 'NPM';
const SCRIPT = 'SCRIPT';
@ -13,8 +14,11 @@ const TABS = [
{ key: NPM, text: NPM },
];
const AssistDoc = (props) => {
const { projectKey } = props;
const AssistDoc = () => {
const { integrationsStore, projectsStore } = useStore();
const sites = projectsStore.list;
const siteId = integrationsStore.integrations.siteId
const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey
const [activeTab, setActiveTab] = useState(SCRIPT);
const renderActiveTab = () => {
@ -53,10 +57,4 @@ const AssistDoc = (props) => {
AssistDoc.displayName = 'AssistDoc';
export default connect((state) => {
const siteId = state.getIn(['integrations', 'siteId']);
const sites = state.getIn(['site', 'list']);
return {
projectKey: sites.find((site) => site.get('id') === siteId).get('projectKey'),
};
})(AssistDoc);
export default observer(AssistDoc);

View file

@ -1,7 +1,7 @@
import React from 'react';
import { tokenRE } from 'Types/integrations/bugsnagConfig';
import IntegrationForm from '../IntegrationForm';
import ProjectListDropdown from './ProjectListDropdown';
// import ProjectListDropdown from './ProjectListDropdown';
import DocLink from 'Shared/DocLink/DocLink';
import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModalCard';
@ -31,7 +31,7 @@ const BugsnagForm = (props) => (
key: 'bugsnagProjectId',
label: 'Project',
checkIfDisplayed: (config) => tokenRE.test(config.authorizationToken),
component: ProjectListDropdown
// component: ProjectListDropdown
}
]}
/>

View file

@ -1,17 +1,20 @@
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';
function ProjectListDropdown(props) {
}
@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 +38,7 @@ export default class ProjectListDropdown extends React.PureComponent {
if (!tokenRE.test(token)) return;
this.props.fetchProjectList({
authorizationToken: token,
}).then(() => {
const { value, projects } = this.props;
const values = projects.map(p => p.id);
if (!values.includes(value) && values.length > 0) {
this.props.edit("bugsnag", {
projectId: values[0],
});
}
});
})
}
componentDidUpdate(prevProps) {
if (prevProps.token !== this.props.token) {

View file

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

View file

@ -1,77 +1,93 @@
import React from 'react';
import { connect } from 'react-redux';
import React, { useState, useEffect, useCallback } from 'react';
import { ACCESS_KEY_ID_LENGTH, SECRET_ACCESS_KEY_LENGTH } from 'Types/integrations/cloudwatchConfig';
import { edit } from 'Duck/integrations/actions';
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
import Select from 'Shared/Select';
import { withRequest } from 'HOCs';
import { integrationsService } from "App/services";
@connect(state => ({
config: state.getIn([ 'cloudwatch', 'instance' ])
}), { edit })
@withRequest({
dataName: "values",
initialData: [],
resetBeforeRequest: true,
requestName: "fetchLogGroups",
endpoint: '/integrations/cloudwatch/list_groups',
method: 'POST',
})
export default class LogGroupDropdown extends React.PureComponent {
constructor(props) {
super(props);
this.fetchLogGroups()
}
fetchLogGroups() {
const { config } = this.props;
if (config.region === "" ||
config.awsSecretAccessKey.length !== SECRET_ACCESS_KEY_LENGTH ||
config.awsAccessKeyId.length !== ACCESS_KEY_ID_LENGTH
) return;
this.props.fetchLogGroups({
region: config.region,
awsSecretAccessKey: config.awsSecretAccessKey,
awsAccessKeyId: config.awsAccessKeyId,
}).then(() => {
const { value, values, name } = this.props;
if (!values.includes(value) && values.length > 0) {
this.props.edit("cloudwatch", {
[ name ]: values[0],
});
}
});
}
componentDidUpdate(prevProps) {
const { config } = this.props;
if (prevProps.config.region !== config.region ||
prevProps.config.awsSecretAccessKey !== config.awsSecretAccessKey ||
prevProps.config.awsAccessKeyId !== config.awsAccessKeyId) {
this.fetchLogGroups();
const LogGroupDropdown = (props) => {
const { integrationsStore } = useStore();
const config = integrationsStore.cloudwatch.instance;
const edit = integrationsStore.cloudwatch.edit;
const {
value,
name,
placeholder,
onChange,
} = props;
const [values, setValues] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const { region, awsSecretAccessKey, awsAccessKeyId } = config;
const fetchLogGroups = useCallback(() => {
if (
region === '' ||
awsSecretAccessKey.length !== SECRET_ACCESS_KEY_LENGTH ||
awsAccessKeyId.length !== ACCESS_KEY_ID_LENGTH
) {
return;
}
}
onChange = (target) => {
if (typeof this.props.onChange === 'function') {
this.props.onChange({ target });
setLoading(true);
setError(false);
setValues([]); // Reset values before request
const params = {
region: region,
awsSecretAccessKey: awsSecretAccessKey,
awsAccessKeyId: awsAccessKeyId,
};
integrationsService.client
.post('/integrations/cloudwatch/list_groups', params)
.then((response) => response.json())
.then(({ errors, data }) => {
if (errors) {
setError(true);
setLoading(false);
return;
}
setValues(data);
setLoading(false);
// If current value is not in the new values list, update it
if (!data.includes(value) && data.length > 0) {
edit({
[name]: data[0],
});
}
})
.catch(() => {
setError(true);
setLoading(false);
});
}, [region, awsSecretAccessKey, awsAccessKeyId, value, name, edit]);
// Fetch log groups on mount and when config changes
useEffect(() => {
fetchLogGroups();
}, [fetchLogGroups]);
const handleChange = (target) => {
if (typeof onChange === 'function') {
onChange({ target });
}
}
render() {
const {
values,
name,
value,
placeholder,
loading,
} = this.props;
const options = values.map(g => ({ text: g, value: g }));
return (
<Select
// selection
options={ options }
name={ name }
value={ options.find(o => o.value === value) }
placeholder={ placeholder }
onChange={ this.onChange }
loading={ loading }
/>
);
}
}
};
const options = values.map((g) => ({ text: g, value: g }));
return (
<Select
options={options}
name={name}
value={options.find((o) => o.value === value)}
placeholder={placeholder}
onChange={handleChange}
loading={loading}
/>
);
};
export default observer(LogGroupDropdown);

View file

@ -1,97 +1,64 @@
import React from 'react';
import { connect } from 'react-redux';
import IntegrationForm from './IntegrationForm';
import { withRequest } from 'HOCs';
import { edit } from 'Duck/integrations/actions';
import DocLink from 'Shared/DocLink/DocLink';
import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModalCard';
@connect(
(state) => ({
config: state.getIn(['elasticsearch', 'instance'])
}),
{ edit }
)
@withRequest({
dataName: 'isValid',
initialData: false,
dataWrapper: (data) => data.state,
requestName: 'validateConfig',
endpoint: '/integrations/elasticsearch/test',
method: 'POST'
})
export default class ElasticsearchForm extends React.PureComponent {
componentWillReceiveProps(newProps) {
const {
config: { host, port, apiKeyId, apiKey }
} = this.props;
const { loading, config } = newProps;
const valuesChanged = host !== config.host || port !== config.port || apiKeyId !== config.apiKeyId || apiKey !== config.apiKey;
if (!loading && valuesChanged && newProps.config.validateKeys() && newProps) {
this.validateConfig(newProps);
}
}
import DocLink from 'Shared/DocLink/DocLink';
validateConfig = (newProps) => {
const { config } = newProps;
this.props
.validateConfig({
host: config.host,
port: config.port,
apiKeyId: config.apiKeyId,
apiKey: config.apiKey
})
.then((res) => {
const { isValid } = this.props;
this.props.edit('elasticsearch', { isValid: isValid });
});
};
import IntegrationForm from './IntegrationForm';
render() {
const props = this.props;
return (
<div className='bg-white h-screen overflow-y-auto' style={{ width: '350px' }}>
<IntegrationModalCard title='Elasticsearch' icon='integrations/elasticsearch'
description='Integrate Elasticsearch with session replays to seamlessly observe backend errors.' />
const ElasticsearchForm = (props) => {
return (
<div
className="bg-white h-screen overflow-y-auto"
style={{ width: '350px' }}
>
<IntegrationModalCard
title="Elasticsearch"
icon="integrations/elasticsearch"
description="Integrate Elasticsearch with session replays to seamlessly observe backend errors."
/>
<div className='p-5 border-b mb-4'>
<div className='font-medium mb-1'>How it works?</div>
<ol className="list-decimal list-inside">
<li>Create a new Elastic API key</li>
<li>Enter the API key below</li>
<li>Propagate openReplaySessionToken</li>
</ol>
<DocLink className='mt-4' label='Integrate Elasticsearch'
url='https://docs.openreplay.com/integrations/elastic' />
</div>
<IntegrationForm
{...props}
name='elasticsearch'
formFields={[
{
key: 'host',
label: 'Host'
},
{
key: 'apiKeyId',
label: 'API Key ID'
},
{
key: 'apiKey',
label: 'API Key'
},
{
key: 'indexes',
label: 'Indexes'
},
{
key: 'port',
label: 'Port',
type: 'number'
}
]}
<div className="p-5 border-b mb-4">
<div className="font-medium mb-1">How it works?</div>
<ol className="list-decimal list-inside">
<li>Create a new Elastic API key</li>
<li>Enter the API key below</li>
<li>Propagate openReplaySessionToken</li>
</ol>
<DocLink
className="mt-4"
label="Integrate Elasticsearch"
url="https://docs.openreplay.com/integrations/elastic"
/>
</div>
);
}
}
<IntegrationForm
{...props}
name="elasticsearch"
formFields={[
{
key: 'host',
label: 'Host',
},
{
key: 'apiKeyId',
label: 'API Key ID',
},
{
key: 'apiKey',
label: 'API Key',
},
{
key: 'indexes',
label: 'Indexes',
},
{
key: 'port',
label: 'Port',
type: 'number',
},
]}
/>
</div>
);
};
export default ElasticsearchForm;

View file

@ -1,11 +1,15 @@
import { useStore } from "App/mstore";
import React from 'react';
import { CodeBlock } from "UI";
import DocLink from 'Shared/DocLink/DocLink';
import ToggleContent from 'Shared/ToggleContent';
import { connect } from 'react-redux';
import { observer } from 'mobx-react-lite'
const GraphQLDoc = (props) => {
const { projectKey } = props;
const GraphQLDoc = () => {
const { integrationsStore, projectsStore } = useStore();
const sites = projectsStore.list;
const siteId = integrationsStore.integrations.siteId
const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey
const usage = `import OpenReplay from '@openreplay/tracker';
import trackerGraphQL from '@openreplay/tracker-graphql';
//...
@ -70,10 +74,4 @@ export const recordGraphQL = tracker.use(trackerGraphQL());`
GraphQLDoc.displayName = 'GraphQLDoc';
export default connect((state) => {
const siteId = state.getIn(['integrations', 'siteId']);
const sites = state.getIn(['site', 'list']);
return {
projectKey: sites.find((site) => site.get('id') === siteId).get('projectKey'),
};
})(GraphQLDoc);
export default observer(GraphQLDoc);

View file

@ -1,142 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import { Input, Form, Button, Checkbox, Loader } from 'UI';
import { save, init, edit, remove } from 'Duck/integrations/actions';
import { fetchIntegrationList } from 'Duck/integrations/integrations';
@connect(
(state, { name, customPath }) => ({
sites: state.getIn(['site', 'list']),
initialSiteId: state.getIn(['site', 'siteId']),
list: state.getIn([name, 'list']),
config: state.getIn([name, 'instance']),
loading: state.getIn([name, 'fetchRequest', 'loading']),
saving: state.getIn([customPath || name, 'saveRequest', 'loading']),
removing: state.getIn([name, 'removeRequest', 'loading']),
siteId: state.getIn(['integrations', 'siteId']),
}),
{
save,
init,
edit,
remove,
// fetchList,
fetchIntegrationList,
}
)
export default class IntegrationForm extends React.PureComponent {
constructor(props) {
super(props);
}
fetchList = () => {
const { siteId, initialSiteId } = this.props;
if (!siteId) {
this.props.fetchIntegrationList(initialSiteId);
} else {
this.props.fetchIntegrationList(siteId);
}
}
write = ({ target: { value, name: key, type, checked } }) => {
if (type === 'checkbox') this.props.edit(this.props.name, { [key]: checked });
else this.props.edit(this.props.name, { [key]: value });
};
// onChangeSelect = ({ value }) => {
// const { sites, list, name } = this.props;
// const site = sites.find((s) => s.id === value.value);
// this.setState({ currentSiteId: site.id });
// this.init(value.value);
// };
// init = (siteId) => {
// const { list, name } = this.props;
// const config = parseInt(siteId) > 0 ? list.find((s) => s.projectId === siteId) : undefined;
// this.props.init(name, config ? config : list.first());
// };
save = () => {
const { config, name, customPath, ignoreProject } = this.props;
const isExists = config.exists();
// const { currentSiteId } = this.state;
this.props.save(customPath || name, !ignoreProject ? this.props.siteId : null, config).then(() => {
// this.props.fetchList(name);
this.fetchList();
this.props.onClose();
if (isExists) return;
});
};
remove = () => {
const { name, config, ignoreProject } = this.props;
this.props.remove(name, !ignoreProject ? config.projectId : null).then(() => {
this.props.onClose();
this.fetchList();
});
};
render() {
const { config, saving, removing, formFields, name, loading, integrated } = this.props;
return (
<Loader loading={loading}>
<div className="ph-20">
<Form>
{formFields.map(
({
key,
label,
placeholder = label,
component: Component = 'input',
type = 'text',
checkIfDisplayed,
autoFocus = false,
}) =>
(typeof checkIfDisplayed !== 'function' || checkIfDisplayed(config)) &&
(type === 'checkbox' ? (
<Form.Field key={key}>
<Checkbox
label={label}
name={key}
value={config[key]}
onChange={this.write}
placeholder={placeholder}
type={Component === 'input' ? type : null}
/>
</Form.Field>
) : (
<Form.Field key={key}>
<label>{label}</label>
<Input
name={key}
value={config[key]}
onChange={this.write}
placeholder={placeholder}
type={Component === 'input' ? type : null}
autoFocus={autoFocus}
/>
</Form.Field>
))
)}
<Button
onClick={this.save}
disabled={!config.validate()}
loading={saving || loading}
variant="primary"
className="float-left mr-2"
>
{config.exists() ? 'Update' : 'Add'}
</Button>
{integrated && (
<Button loading={removing} onClick={this.remove}>
{'Delete'}
</Button>
)}
</Form>
</div>
</Loader>
);
}
}

View file

@ -0,0 +1,107 @@
import { observer } from 'mobx-react-lite';
import React from 'react';
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 initialSiteId = projectsStore.siteId;
const integrationStore = integrationsStore[name as unknown as namedStore];
const config = integrationStore.instance;
const loading = integrationStore.loading;
const onSave = integrationStore.saveIntegration;
const onRemove = integrationStore.deleteIntegration;
const edit = integrationStore.edit;
const fetchIntegrationList = integrationsStore.integrations.fetchIntegrations;
const fetchList = () => {
void fetchIntegrationList(initialSiteId);
};
const write = ({ target: { value, name: key, type, checked } }) => {
if (type === 'checkbox') edit({ [key]: checked });
else edit({ [key]: value });
};
const save = () => {
const { name, customPath } = props;
onSave(customPath || name).then(() => {
fetchList();
props.onClose();
});
};
const remove = () => {
onRemove().then(() => {
props.onClose();
fetchList();
});
};
return (
<Loader loading={loading}>
<div className="ph-20">
<Form>
{formFields.map(
({
key,
label,
placeholder = label,
component: Component = 'input',
type = 'text',
checkIfDisplayed,
autoFocus = false,
}) =>
(typeof checkIfDisplayed !== 'function' ||
checkIfDisplayed(config)) &&
(type === 'checkbox' ? (
<Form.Field key={key}>
<Checkbox
label={label}
name={key}
value={config[key]}
onChange={write}
placeholder={placeholder}
type={Component === 'input' ? type : null}
/>
</Form.Field>
) : (
<Form.Field key={key}>
<label>{label}</label>
<Input
name={key}
value={config[key]}
onChange={write}
placeholder={placeholder}
type={Component === 'input' ? type : null}
autoFocus={autoFocus}
/>
</Form.Field>
))
)}
<Button
onClick={save}
disabled={!config?.validate()}
loading={loading}
variant="primary"
className="float-left mr-2"
>
{config?.exists() ? 'Update' : 'Add'}
</Button>
{integrated && (
<Button loading={loading} onClick={remove}>
{'Delete'}
</Button>
)}
</Form>
</div>
</Loader>
);
}
export default observer(IntegrationForm);

View file

@ -1,88 +1,94 @@
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { useModal } from 'App/components/Modal';
import cn from 'classnames';
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 cn from 'classnames';
import { observer } from 'mobx-react-lite';
import React, { useEffect, useState } from 'react';
import { useModal } from 'App/components/Modal';
import { useStore } from 'App/mstore';
import IntegrationFilters from 'Components/Client/Integrations/IntegrationFilters';
import { PageTitle } from 'UI';
import DocCard from 'Shared/DocCard/DocCard';
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 +105,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 +201,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 +330,82 @@ const integrations = [
icon: 'chat-left-text',
docs: () => (
<DocCard
title='What are plugins?'
icon='question-lg'
iconBgColor='bg-red-lightest'
iconColor='red'
title="What are plugins?"
icon="question-lg"
iconBgColor="bg-red-lightest"
iconColor="red"
>
Plugins capture your applications store, monitor queries, track performance issues and even
assist your end user through live sessions.
Plugins capture your applications store, monitor queries, track
performance issues and even assist your end user through live sessions.
</DocCard>
),
description:
'Reproduce issues as if they happened in your own browser. Plugins help capture your application\'s store, HTTP requeets, GraphQL queries, and more.',
"Reproduce issues as if they happened in your own browser. Plugins help capture your application's store, HTTP requeets, GraphQL queries, and more.",
integrations: [
{
title: 'Redux',
subtitle: 'Capture Redux actions/state and inspect them later on while replaying session recordings.',
icon: 'integrations/redux', component: <ReduxDoc />
subtitle:
'Capture Redux actions/state and inspect them later on while replaying session recordings.',
icon: 'integrations/redux',
component: <ReduxDoc />,
},
{
title: 'VueX',
subtitle: 'Capture VueX mutations/state and inspect them later on while replaying session recordings.',
subtitle:
'Capture VueX mutations/state and inspect them later on while replaying session recordings.',
icon: 'integrations/vuejs',
component: <VueDoc />
component: <VueDoc />,
},
{
title: 'Pinia',
subtitle: 'Capture Pinia mutations/state and inspect them later on while replaying session recordings.',
subtitle:
'Capture Pinia mutations/state and inspect them later on while replaying session recordings.',
icon: 'integrations/pinia',
component: <PiniaDoc />
component: <PiniaDoc />,
},
{
title: 'GraphQL',
subtitle: 'Capture GraphQL requests and inspect them later on while replaying session recordings. This plugin is compatible with Apollo and Relay implementations.',
subtitle:
'Capture GraphQL requests and inspect them later on while replaying session recordings. This plugin is compatible with Apollo and Relay implementations.',
icon: 'integrations/graphql',
component: <GraphQLDoc />
component: <GraphQLDoc />,
},
{
title: 'NgRx',
subtitle: 'Capture NgRx actions/state and inspect them later on while replaying session recordings.\n',
subtitle:
'Capture NgRx actions/state and inspect them later on while replaying session recordings.\n',
icon: 'integrations/ngrx',
component: <NgRxDoc />
component: <NgRxDoc />,
},
{
title: 'MobX',
subtitle: 'Capture MobX mutations and inspect them later on while replaying session recordings.',
subtitle:
'Capture MobX mutations and inspect them later on while replaying session recordings.',
icon: 'integrations/mobx',
component: <MobxDoc />
component: <MobxDoc />,
},
{
title: 'Profiler',
subtitle: 'Plugin allows you to measure your JS functions performance and capture both arguments and result for each call.',
subtitle:
'Plugin allows you to measure your JS functions performance and capture both arguments and result for each call.',
icon: 'integrations/openreplay',
component: <ProfilerDoc />
component: <ProfilerDoc />,
},
{
title: 'Assist',
subtitle: 'OpenReplay Assist allows you to support your users by seeing their live screen and instantly hopping on call (WebRTC) with them without requiring any 3rd-party screen sharing software.\n',
subtitle:
'OpenReplay Assist allows you to support your users by seeing their live screen and instantly hopping on call (WebRTC) with them without requiring any 3rd-party screen sharing software.\n',
icon: 'integrations/openreplay',
component: <AssistDoc />
component: <AssistDoc />,
},
{
title: 'Zustand',
subtitle: 'Capture Zustand mutations/state and inspect them later on while replaying session recordings.',
subtitle:
'Capture Zustand mutations/state and inspect them later on while replaying session recordings.',
icon: 'integrations/zustand',
// header: '🐻',
component: <ZustandDoc />
}
]
}
component: <ZustandDoc />,
},
],
},
];

View file

@ -1,11 +1,15 @@
import React from 'react';
import ToggleContent from 'Shared/ToggleContent';
import DocLink from 'Shared/DocLink/DocLink';
import { connect } from 'react-redux';
import { CodeBlock } from "UI";
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
const MobxDoc = (props) => {
const { projectKey } = props;
const MobxDoc = () => {
const { integrationsStore, projectsStore } = useStore();
const sites = projectsStore.list;
const siteId = integrationsStore.integrations.siteId
const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey
const mobxUsage = `import OpenReplay from '@openreplay/tracker';
import trackerMobX from '@openreplay/tracker-mobx';
@ -67,10 +71,4 @@ function SomeFunctionalComponent() {
MobxDoc.displayName = 'MobxDoc';
export default connect((state) => {
const siteId = state.getIn(['integrations', 'siteId']);
const sites = state.getIn(['site', 'list']);
return {
projectKey: sites.find((site) => site.get('id') === siteId).get('projectKey'),
};
})(MobxDoc);
export default observer(MobxDoc)

View file

@ -1,11 +1,15 @@
import { useStore } from "App/mstore";
import React from 'react';
import { CodeBlock } from "UI";
import ToggleContent from 'Shared/ToggleContent';
import DocLink from 'Shared/DocLink/DocLink';
import { connect } from 'react-redux';
import { observer } from 'mobx-react-lite'
const NgRxDoc = (props) => {
const { projectKey } = props;
const NgRxDoc = () => {
const { integrationsStore, projectsStore } = useStore();
const sites = projectsStore.list;
const siteId = integrationsStore.integrations.siteId
const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey
const usage = `import { StoreModule } from '@ngrx/store';
import { reducers } from './reducers';
import OpenReplay from '@openreplay/tracker';
@ -80,10 +84,4 @@ const metaReducers = [tracker.use(trackerNgRx(<options>))]; // check list of ava
NgRxDoc.displayName = 'NgRxDoc';
export default connect((state) => {
const siteId = state.getIn(['integrations', 'siteId']);
const sites = state.getIn(['site', 'list']);
return {
projectKey: sites.find((site) => site.get('id') === siteId).get('projectKey'),
};
})(NgRxDoc);
export default observer(NgRxDoc);

View file

@ -1,11 +1,19 @@
import { observer } from 'mobx-react-lite';
import React from 'react';
import { CodeBlock } from "UI";
import ToggleContent from '../../../shared/ToggleContent';
import DocLink from 'Shared/DocLink/DocLink';
import { connect } from 'react-redux';
const PiniaDoc = (props) => {
const { projectKey } = props;
import { useStore } from 'App/mstore';
import ToggleContent from 'Components/shared/ToggleContent';
import { CodeBlock } from 'UI';
import DocLink from 'Shared/DocLink/DocLink';
const PiniaDoc = () => {
const { integrationsStore, projectsStore } = useStore();
const sites = projectsStore.list;
const siteId = integrationsStore.integrations.siteId;
const projectKey = siteId
? sites.find((site) => site.id === siteId)?.projectKey
: sites[0]?.projectKey;
const usage = `import Vuex from 'vuex'
import OpenReplay from '@openreplay/tracker';
import trackerVuex from '@openreplay/tracker-vuex';
@ -28,7 +36,7 @@ piniaStorePlugin(examplePiniaStore)
// now you can use examplePiniaStore as
// usual pinia store
// (destructure values or return it as a whole etc)
`
`;
const usageCjs = `import Vuex from 'vuex'
import OpenReplay from '@openreplay/tracker/cjs';
import trackerVuex from '@openreplay/tracker-vuex/cjs';
@ -55,34 +63,38 @@ piniaStorePlugin(examplePiniaStore)
// now you can use examplePiniaStore as
// usual pinia store
// (destructure values or return it as a whole etc)
}`
}`;
return (
<div className="bg-white h-screen overflow-y-auto" style={{ width: '500px' }}>
<div
className="bg-white h-screen overflow-y-auto"
style={{ width: '500px' }}
>
<h3 className="p-5 text-2xl">VueX</h3>
<div className="p-5">
<div>
This plugin allows you to capture Pinia mutations + state and inspect them later on while
replaying session recordings. This is very useful for understanding and fixing issues.
This plugin allows you to capture Pinia mutations + state and inspect
them later on while replaying session recordings. This is very useful
for understanding and fixing issues.
</div>
<div className="font-bold my-2 text-lg">Installation</div>
<CodeBlock code={`npm i @openreplay/tracker-vuex --save`} language="bash" />
<CodeBlock
code={`npm i @openreplay/tracker-vuex --save`}
language="bash"
/>
<div className="font-bold my-2 text-lg">Usage</div>
<p>
Initialize the @openreplay/tracker package as usual and load the plugin into it. Then put
the generated plugin into your plugins field of your store.
Initialize the @openreplay/tracker package as usual and load the
plugin into it. Then put the generated plugin into your plugins field
of your store.
</p>
<div className="py-3" />
<ToggleContent
label="Server-Side-Rendered (SSR)?"
first={
<CodeBlock code={usage} language="js" />
}
second={
<CodeBlock code={usageCjs} language="js" />
}
first={<CodeBlock code={usage} language="js" />}
second={<CodeBlock code={usageCjs} language="js" />}
/>
<DocLink
@ -97,10 +109,4 @@ piniaStorePlugin(examplePiniaStore)
PiniaDoc.displayName = 'PiniaDoc';
export default connect((state: any) => {
const siteId = state.getIn(['integrations', 'siteId']);
const sites = state.getIn(['site', 'list']);
return {
projectKey: sites.find((site: any) => site.get('id') === siteId).get('projectKey'),
};
})(PiniaDoc);
export default observer(PiniaDoc);

View file

@ -1,13 +1,16 @@
import { useStore } from "App/mstore";
import React from 'react';
import { connect } from 'react-redux';
import { observer } from 'mobx-react-lite';
import { CodeBlock } from 'UI';
import DocLink from 'Shared/DocLink/DocLink';
import ToggleContent from 'Shared/ToggleContent';
const ProfilerDoc = (props) => {
const { projectKey } = props;
const ProfilerDoc = () => {
const { integrationsStore, projectsStore } = useStore();
const sites = projectsStore.list;
const siteId = integrationsStore.integrations.siteId
const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey
const usage = `import OpenReplay from '@openreplay/tracker';
import trackerProfiler from '@openreplay/tracker-profiler';
@ -87,12 +90,4 @@ const fn = profiler('call_name')(() => {
ProfilerDoc.displayName = 'ProfilerDoc';
export default connect((state) => {
const siteId = state.getIn(['integrations', 'siteId']);
const sites = state.getIn(['site', 'list']);
return {
projectKey: sites
.find((site) => site.get('id') === siteId)
.get('projectKey'),
};
})(ProfilerDoc);
export default observer(ProfilerDoc);

View file

@ -1,11 +1,15 @@
import { useStore } from "App/mstore";
import React from 'react';
import { CodeBlock } from 'UI'
import ToggleContent from '../../../shared/ToggleContent';
import ToggleContent from 'Components/shared/ToggleContent';
import DocLink from 'Shared/DocLink/DocLink';
import { connect } from 'react-redux';
import { observer } from 'mobx-react-lite'
const ReduxDoc = (props) => {
const { projectKey } = props;
const ReduxDoc = () => {
const { integrationsStore, projectsStore } = useStore();
const sites = projectsStore.list;
const siteId = integrationsStore.integrations.siteId
const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey
const usage = `import { applyMiddleware, createStore } from 'redux';
import OpenReplay from '@openreplay/tracker';
@ -74,10 +78,4 @@ const store = createStore(
ReduxDoc.displayName = 'ReduxDoc';
export default connect((state) => {
const siteId = state.getIn(['integrations', 'siteId']);
const sites = state.getIn(['site', 'list']);
return {
projectKey: sites.find((site) => site.get('id') === siteId).get('projectKey'),
};
})(ReduxDoc);
export default observer(ReduxDoc);

View file

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

View file

@ -1,14 +1,16 @@
import React from 'react';
import { connect } from 'react-redux';
import { NoContent } from 'UI';
import { remove, edit, init } from 'Duck/integrations/slack';
import DocLink from 'Shared/DocLink/DocLink';
import { observer } from 'mobx-react-lite'
import { useStore } from 'App/mstore'
function SlackChannelList(props) {
const { list } = props;
const { integrationsStore } = useStore();
const list = integrationsStore.slack.list;
const edit = integrationsStore.slack.edit;
const onEdit = (instance) => {
props.edit(instance);
edit(instance.toData());
props.onEdit();
};
@ -24,7 +26,7 @@ function SlackChannelList(props) {
</div>
}
size="small"
show={list.size === 0}
show={list.length === 0}
>
{list.map((c) => (
<div
@ -43,9 +45,4 @@ function SlackChannelList(props) {
);
}
export default connect(
(state) => ({
list: state.getIn(['slack', 'list']),
}),
{ remove, edit, init }
)(SlackChannelList);
export default observer(SlackChannelList);

View file

@ -1,17 +1,14 @@
import React, { useEffect } from 'react';
import SlackChannelList from './SlackChannelList/SlackChannelList';
import { fetchList, init } from 'Duck/integrations/slack';
import { connect } from 'react-redux';
import SlackAddForm from './SlackAddForm';
import { Button } from 'UI';
import { observer } from 'mobx-react-lite'
import { useStore } from 'App/mstore'
interface Props {
onEdit?: (integration: any) => void;
istance: any;
fetchList: any;
init: any;
}
const SlackForm = (props: Props) => {
const SlackForm = () => {
const { integrationsStore } = useStore();
const init = integrationsStore.slack.init;
const fetchList = integrationsStore.slack.fetchIntegrations;
const [active, setActive] = React.useState(false);
const onEdit = () => {
@ -20,11 +17,11 @@ const SlackForm = (props: Props) => {
const onNew = () => {
setActive(true);
props.init({});
init({});
}
useEffect(() => {
props.fetchList();
void fetchList();
}, []);
return (
@ -47,9 +44,4 @@ const SlackForm = (props: Props) => {
SlackForm.displayName = 'SlackForm';
export default connect(
(state: any) => ({
istance: state.getIn(['slack', 'instance']),
}),
{ fetchList, init }
)(SlackForm);
export default observer(SlackForm);

View file

@ -1,36 +1,38 @@
import { observer } from 'mobx-react-lite';
import React from 'react';
import { connect } from 'react-redux';
import { edit, save, init, update, remove } from 'Duck/integrations/teams';
import { Form, Input, Button, Message } from 'UI';
import { useStore } from 'App/mstore';
import { Button, Form, Input, Message } from 'UI';
import { confirm } from 'UI';
interface Props {
edit: (inst: any) => void;
save: (inst: any) => void;
init: (inst: any) => void;
update: (inst: any) => void;
remove: (id: string) => void;
onClose: () => void;
instance: any;
saving: boolean;
errors: any;
}
class TeamsAddForm extends React.PureComponent<Props> {
componentWillUnmount() {
this.props.init({});
}
function TeamsAddForm({ onClose }: Props) {
const { integrationsStore } = useStore();
const instance = integrationsStore.msteams.instance;
const saving = integrationsStore.msteams.loading;
const errors = integrationsStore.msteams.errors;
const edit = integrationsStore.msteams.edit;
const onSave = integrationsStore.msteams.saveIntegration;
const init = integrationsStore.msteams.init;
const onRemove = integrationsStore.msteams.removeInt;
const update = integrationsStore.msteams.update;
save = () => {
const instance = this.props.instance;
if (instance.exists()) {
this.props.update(this.props.instance);
React.useEffect(() => {
return () => init({});
}, []);
const save = () => {
if (instance?.exists()) {
void update();
} else {
this.props.save(this.props.instance);
void onSave();
}
};
remove = async (id: string) => {
const remove = async (id: string) => {
if (
await confirm({
header: 'Confirm',
@ -38,80 +40,74 @@ class TeamsAddForm extends React.PureComponent<Props> {
confirmation: `Are you sure you want to permanently delete this channel?`,
})
) {
this.props.remove(id);
void onRemove(id);
}
};
write = ({ target: { name, value } }: { target: { name: string; value: string } }) =>
this.props.edit({ [name]: value });
const write = ({
target: { name, value },
}: {
target: { name: string; value: string };
}) => edit({ [name]: value });
render() {
const { instance, saving, errors, onClose } = this.props;
return (
<div className="p-5" style={{ minWidth: '300px' }}>
<Form>
<Form.Field>
<label>Name</label>
<Input
name="name"
value={instance.name}
onChange={this.write}
placeholder="Enter any name"
type="text"
/>
</Form.Field>
<Form.Field>
<label>URL</label>
<Input
name="endpoint"
value={instance.endpoint}
onChange={this.write}
placeholder="Teams webhook URL"
type="text"
/>
</Form.Field>
<div className="flex justify-between">
<div className="flex">
<Button
onClick={this.save}
disabled={!instance.validate()}
loading={saving}
variant="primary"
className="float-left mr-2"
>
{instance.exists() ? 'Update' : 'Add'}
</Button>
<Button onClick={onClose}>{'Cancel'}</Button>
</div>
<Button onClick={() => this.remove(instance.webhookId)} disabled={!instance.exists()}>
{'Delete'}
return (
<div className="p-5" style={{ minWidth: '300px' }}>
<Form>
<Form.Field>
<label>Name</label>
<Input
name="name"
value={instance?.name}
onChange={write}
placeholder="Enter any name"
type="text"
/>
</Form.Field>
<Form.Field>
<label>URL</label>
<Input
name="endpoint"
value={instance?.endpoint}
onChange={write}
placeholder="Teams webhook URL"
type="text"
/>
</Form.Field>
<div className="flex justify-between">
<div className="flex">
<Button
onClick={save}
disabled={!instance?.validate()}
loading={saving}
variant="primary"
className="float-left mr-2"
>
{instance?.exists() ? 'Update' : 'Add'}
</Button>
</div>
</Form>
{errors && (
<div className="my-3">
{errors.map((error: any) => (
<Message visible={errors} key={error}>
{error}
</Message>
))}
<Button onClick={onClose}>{'Cancel'}</Button>
</div>
)}
</div>
);
}
<Button
onClick={() => remove(instance?.webhookId)}
disabled={!instance.exists()}
>
{'Delete'}
</Button>
</div>
</Form>
{errors && (
<div className="my-3">
{errors.map((error: any) => (
<Message visible={errors} key={error}>
{error}
</Message>
))}
</div>
)}
</div>
);
}
export default connect(
(state: any) => ({
instance: state.getIn(['teams', 'instance']),
saving:
state.getIn(['teams', 'saveRequest', 'loading']) ||
state.getIn(['teams', 'updateRequest', 'loading']),
errors: state.getIn(['teams', 'saveRequest', 'errors']),
}),
{ edit, save, init, remove, update }
)(TeamsAddForm);
export default observer(TeamsAddForm);

View file

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

View file

@ -1,17 +1,15 @@
import React, { useEffect } from 'react';
import TeamsChannelList from './TeamsChannelList';
import { fetchList, init } from 'Duck/integrations/teams';
import { connect } from 'react-redux';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import TeamsAddForm from './TeamsAddForm';
import { Button } from 'UI';
interface Props {
onEdit?: (integration: any) => void;
istance: any;
fetchList: any;
init: any;
}
const MSTeams = (props: Props) => {
const MSTeams = () => {
const { integrationsStore } = useStore();
const fetchList = integrationsStore.msteams.fetchIntegrations;
const init = integrationsStore.msteams.init;
const [active, setActive] = React.useState(false);
const onEdit = () => {
@ -20,11 +18,11 @@ const MSTeams = (props: Props) => {
const onNew = () => {
setActive(true);
props.init({});
init({});
}
useEffect(() => {
props.fetchList();
void fetchList();
}, []);
return (
@ -47,9 +45,4 @@ const MSTeams = (props: Props) => {
MSTeams.displayName = 'MSTeams';
export default connect(
(state: any) => ({
istance: state.getIn(['teams', 'instance']),
}),
{ fetchList, init }
)(MSTeams);
export default observer(MSTeams);

View file

@ -1,11 +1,15 @@
import { useStore } from "App/mstore";
import React from 'react';
import { CodeBlock } from "UI";
import ToggleContent from '../../../shared/ToggleContent';
import ToggleContent from 'Components/shared/ToggleContent';
import DocLink from 'Shared/DocLink/DocLink';
import { connect } from 'react-redux';
import { observer } from 'mobx-react-lite';
const VueDoc = (props) => {
const { projectKey, siteId } = props;
const VueDoc = () => {
const { integrationsStore, projectsStore } = useStore();
const sites = projectsStore.list;
const siteId = integrationsStore.integrations.siteId
const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey
const usage = `import Vuex from 'vuex'
import OpenReplay from '@openreplay/tracker';
@ -81,10 +85,4 @@ const store = new Vuex.Store({
VueDoc.displayName = 'VueDoc';
export default connect((state) => {
const siteId = state.getIn(['integrations', 'siteId']);
const sites = state.getIn(['site', 'list']);
return {
projectKey: sites.find((site) => site.get('id') === siteId).get('projectKey'),
};
})(VueDoc);
export default observer(VueDoc);

View file

@ -1,11 +1,15 @@
import { useStore } from "App/mstore";
import React from 'react';
import { CodeBlock } from "UI";
import ToggleContent from '../../../shared/ToggleContent';
import ToggleContent from 'Components//shared/ToggleContent';
import DocLink from 'Shared/DocLink/DocLink';
import { connect } from 'react-redux';
import { observer } from 'mobx-react-lite'
const ZustandDoc = (props) => {
const { projectKey } = props;
const { integrationsStore, projectsStore } = useStore();
const sites = projectsStore.list;
const siteId = integrationsStore.integrations.siteId
const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey
const usage = `import create from "zustand";
import Tracker from '@openreplay/tracker';
@ -97,10 +101,4 @@ const useBearStore = create(
ZustandDoc.displayName = 'ZustandDoc';
export default connect((state) => {
const siteId = state.getIn(['integrations', 'siteId']);
const sites = state.getIn(['site', 'list']);
return {
projectKey: sites.find((site) => site.get('id') === siteId).get('projectKey'),
};
})(ZustandDoc);
export default observer(ZustandDoc);

View file

@ -2,20 +2,17 @@ import React, { useEffect } from 'react';
import ModuleCard from 'Components/Client/Modules/ModuleCard';
import { modules as list } from './';
import withPageTitle from 'HOCs/withPageTitle';
import { connect } from 'react-redux';
import { userService } from 'App/services';
import { toast } from 'react-toastify';
import { updateModule } from 'Duck/user';
import { useStore } from "App/mstore";
import { observer } from 'mobx-react-lite';
interface Props {
modules: string[];
updateModule: (moduleKey: string) => void;
isEnterprise: boolean;
}
function Modules(props: Props) {
const { modules } = props;
const [modulesState, setModulesState, isEnterprise = false] = React.useState<any[]>([]);
function Modules() {
const { userStore } = useStore();
const updateModule = userStore.updateModule;
const modules = userStore.account.settings?.modules ?? [];
const isEnterprise = userStore.account.edition === 'ee';
const [modulesState, setModulesState] = React.useState<any[]>([]);
const onToggle = async (module: any) => {
try {
@ -26,7 +23,7 @@ function Modules(props: Props) {
module: module.key,
status: isEnabled,
});
props.updateModule(module.key);
updateModule(module.key);
toast.success(`Module ${module.label} ${!isEnabled ? 'enabled' : 'disabled'}`);
} catch (err) {
console.error(err);
@ -66,7 +63,4 @@ function Modules(props: Props) {
}
export default withPageTitle('Modules - OpenReplay Preferences')(connect((state: any) => ({
modules: state.getIn(['user', 'account', 'settings', 'modules']) || [],
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee'
}), { updateModule })(Modules));
export default withPageTitle('Modules - OpenReplay Preferences')(observer(Modules));

View file

@ -1,46 +1,25 @@
import React from 'react';
import copy from 'copy-to-clipboard';
import { connect } from 'react-redux';
import styles from './profileSettings.module.css';
import { Form, Input, Button, CopyButton } from 'UI';
import { observer } from 'mobx-react-lite'
import { useStore } from 'App/mstore';
import { CopyButton, Form, Input } from 'UI';
@connect(state => ({
apiKey: state.getIn([ 'user', 'account', 'apiKey' ]),
loading: state.getIn([ 'user', 'updateAccountRequest', 'loading' ]) ||
state.getIn([ 'user', 'putClientRequest', 'loading' ]),
}))
export default class Api extends React.PureComponent {
state = { copied: false }
function ApiKeySettings() {
const { userStore } = useStore();
copyHandler = () => {
const { apiKey } = this.props;
this.setState({ copied: true });
copy(apiKey);
setTimeout(() => {
this.setState({ copied: false });
}, 1000);
};
render() {
const { apiKey } = this.props;
const { copied } = this.state;
return (
<Form onSubmit={ this.handleSubmit } className={ styles.form }>
<Form.Field>
<label htmlFor="apiKey">{ 'Organization API Key' }</label>
<Input
name="apiKey"
id="apiKey"
type="text"
readOnly={ true }
value={ apiKey }
leadingButton={
<CopyButton content={ apiKey } />
}
/>
</Form.Field>
</Form>
);
}
const apiKey = userStore.account.apiKey;
return (
<Form.Field>
<label htmlFor="apiKey">{'Organization API Key'}</label>
<Input
name="apiKey"
id="apiKey"
type="text"
readOnly={true}
value={apiKey}
leadingButton={<CopyButton content={apiKey} />}
/>
</Form.Field>
);
}
export default observer(ApiKeySettings);

View file

@ -1,18 +1,20 @@
import React, { useState, useCallback } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { Button, Message, Form, Input } from 'UI';
import styles from './profileSettings.module.css';
import { updatePassword } from 'Duck/user';
import { toast } from 'react-toastify';
import { validatePassword } from 'App/validate';
import { PASSWORD_POLICY } from 'App/constants';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
const ERROR_DOESNT_MATCH = "Passwords don't match";
const MIN_LENGTH = 8;
type PropsFromRedux = ConnectedProps<typeof connector>;
const ChangePassword: React.FC<PropsFromRedux> = ({ passwordErrors, loading, updatePassword }) => {
const ChangePassword = () => {
const { userStore } = useStore();
const updatePassword = userStore.updatePassword;
const passwordErrors = userStore.updatePasswordRequest.errors;
const loading = userStore.updatePasswordRequest.loading;
const [oldPassword, setOldPassword] = useState<string>('');
const [newPassword, setNewPassword] = useState<{ value: string; error: boolean }>({
value: '',
@ -22,7 +24,6 @@ const ChangePassword: React.FC<PropsFromRedux> = ({ passwordErrors, loading, upd
value: '',
error: false,
});
const [success, setSuccess] = useState<boolean>(false);
const [show, setShow] = useState<boolean>(false);
const checkDoesntMatch = useCallback((newPassword: string, newPasswordRepeat: string) => {
@ -55,7 +56,6 @@ const ChangePassword: React.FC<PropsFromRedux> = ({ passwordErrors, loading, upd
newPassword: newPassword.value,
}).then((e: any) => {
const success = !e || !e.errors || e.errors.length === 0;
setSuccess(success);
setShow(!success);
if (success) {
toast.success(`Successfully changed password`);
@ -133,7 +133,6 @@ const ChangePassword: React.FC<PropsFromRedux> = ({ passwordErrors, loading, upd
setOldPassword('');
setNewPassword({ value: '', error: false });
setNewPasswordRepeat({ value: '', error: false });
setSuccess(false);
setShow(false);
}}
>
@ -148,15 +147,4 @@ const ChangePassword: React.FC<PropsFromRedux> = ({ passwordErrors, loading, upd
);
};
const mapStateToProps = (state: any) => ({
passwordErrors: state.getIn(['user', 'passwordErrors']),
loading: state.getIn(['user', 'updatePasswordRequest', 'loading']),
});
const mapDispatchToProps = {
updatePassword,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
export default connector(ChangePassword);
export default observer(ChangePassword);

View file

@ -1,7 +1,10 @@
import React from 'react'
import { connect } from 'react-redux'
import { observer } from 'mobx-react-lite'
import { useStore } from 'App/mstore'
function Licenses({ account }) {
function Licenses() {
const { userStore } = useStore()
const account = userStore.account
return (
<div>
<div>{account.license}</div>
@ -14,6 +17,4 @@ function Licenses({ account }) {
)
}
export default connect(state => ({
account: state.getIn([ 'user', 'account' ]),
}))(Licenses)
export default observer(Licenses)

View file

@ -1,28 +1,29 @@
import React from 'react'
import { connect } from 'react-redux';
import { Checkbox } from 'UI'
import { updateClient } from 'Duck/user'
import { observer } from 'mobx-react-lite'
import { useStore } from "App/mstore";
function OptOut() {
const { userStore } = useStore();
const optOut = userStore.account.optOut;
const updateClient = userStore.updateClient;
function OptOut(props) {
const { optOut } = props;
const onChange = () => {
props.updateClient({ optOut: !optOut })
void updateClient({ optOut: !optOut });
}
return (
<div>
<Checkbox
name="isPublic"
className="font-medium"
type="checkbox"
checked={ optOut }
onClick={ onChange }
className="mr-8"
className="font-medium mr-8"
label="Anonymize"
/>
</div>
)
}
export default connect(state => ({
optOut: state.getIn([ 'user', 'account', 'optOut' ]),
}), { updateClient })(OptOut);
export default observer(OptOut);

View file

@ -7,106 +7,104 @@ import Api from './Api';
import TenantKey from './TenantKey';
import OptOut from './OptOut';
import Licenses from './Licenses';
import { connect } from 'react-redux';
import { PageTitle } from 'UI';
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
@withPageTitle('Account - OpenReplay Preferences')
@connect((state) => ({
account: state.getIn(['user', 'account']),
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee',
}))
export default class ProfileSettings extends React.PureComponent {
render() {
const { account, isEnterprise } = this.props;
return (
<div className="bg-white rounded-lg border shadow-sm p-5">
<PageTitle title={<div>Account</div>} />
<div className="flex items-center">
<div className={styles.left}>
<h4 className="text-lg mb-4">{'Profile'}</h4>
<div className={styles.info}>{'Your email address is your identity on OpenReplay and is used to login.'}</div>
</div>
<div>
<Settings />
</div>
</div>
function ProfileSettings() {
const { userStore } = useStore();
const account = userStore.account;
const isEnterprise = userStore.isEnterprise;
return (
<div className="bg-white rounded-lg border shadow-sm p-5">
<PageTitle title={<div>Account</div>} />
<div className="flex items-center">
<div className={styles.left}>
<h4 className="text-lg mb-4">{'Profile'}</h4>
<div className={styles.info}>{'Your email address is your identity on OpenReplay and is used to login.'}</div>
</div>
<div>
<Settings />
</div>
</div>
<div className="border-b my-10" />
<div className="border-b my-10" />
{account.hasPassword && (
<>
<div className="flex items-center">
<div className={styles.left}>
<h4 className="text-lg mb-4">{'Change Password'}</h4>
<div className={styles.info}>{'Updating your password from time to time enhances your accounts security.'}</div>
</div>
<div>
<ChangePassword />
</div>
</div>
<div className="border-b my-10" />
</>
)}
<div className="flex items-center">
<div className={styles.left}>
<h4 className="text-lg mb-4">{'Organization API Key'}</h4>
<div className={styles.info}>{'Your API key gives you access to an extra set of services.'}</div>
</div>
<div>
<Api />
</div>
</div>
{isEnterprise && (account.admin || account.superAdmin) && (
<>
<div className="border-b my-10" />
<div className="flex items-center">
<div className={styles.left}>
<h4 className="text-lg mb-4">{'Tenant Key'}</h4>
<div className={styles.info}>{'For SSO (SAML) authentication.'}</div>
</div>
<div>
<TenantKey />
</div>
</div>
</>
)}
{!isEnterprise && (
<>
<div className="border-b my-10" />
<div className="flex items-center">
<div className={styles.left}>
<h4 className="text-lg mb-4">{'Data Collection'}</h4>
<div className={styles.info}>
{'Enables you to control how OpenReplay captures data on your organizations usage to improve our product.'}
</div>
</div>
<div>
<OptOut />
</div>
</div>
</>
)}
{account.license && (
<>
<div className="border-b my-10" />
<div className="flex items-center">
<div className={styles.left}>
<h4 className="text-lg mb-4">{'License'}</h4>
<div className={styles.info}>{'License key and expiration date.'}</div>
</div>
<div>
<Licenses />
</div>
</div>
</>
)}
{account.hasPassword && (
<>
<div className="flex items-center">
<div className={styles.left}>
<h4 className="text-lg mb-4">{'Change Password'}</h4>
<div className={styles.info}>{'Updating your password from time to time enhances your accounts security.'}</div>
</div>
);
}
<div>
<ChangePassword />
</div>
</div>
<div className="border-b my-10" />
</>
)}
<div className="flex items-center">
<div className={styles.left}>
<h4 className="text-lg mb-4">{'Organization API Key'}</h4>
<div className={styles.info}>{'Your API key gives you access to an extra set of services.'}</div>
</div>
<div>
<Api />
</div>
</div>
{isEnterprise && (account.admin || account.superAdmin) && (
<>
<div className="border-b my-10" />
<div className="flex items-center">
<div className={styles.left}>
<h4 className="text-lg mb-4">{'Tenant Key'}</h4>
<div className={styles.info}>{'For SSO (SAML) authentication.'}</div>
</div>
<div>
<TenantKey />
</div>
</div>
</>
)}
{!isEnterprise && (
<>
<div className="border-b my-10" />
<div className="flex items-center">
<div className={styles.left}>
<h4 className="text-lg mb-4">{'Data Collection'}</h4>
<div className={styles.info}>
{'Enables you to control how OpenReplay captures data on your organizations usage to improve our product.'}
</div>
</div>
<div>
<OptOut />
</div>
</div>
</>
)}
{account.license && (
<>
<div className="border-b my-10" />
<div className="flex items-center">
<div className={styles.left}>
<h4 className="text-lg mb-4">{'License'}</h4>
<div className={styles.info}>{'License key and expiration date.'}</div>
</div>
<div>
<Licenses />
</div>
</div>
</>
)}
</div>
);
}
export default withPageTitle('Account - OpenReplay Preferences')(observer(ProfileSettings));

View file

@ -1,74 +1,66 @@
import React from 'react';
import { connect } from 'react-redux';
import { Button, Input, Form } from 'UI';
import { updateAccount, updateClient } from 'Duck/user';
import styles from './profileSettings.module.css';
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
@connect(
(state) => ({
accountName: state.getIn(['user', 'account', 'name']),
organizationName: state.getIn(['user', 'account', 'tenantName']),
loading:
state.getIn(['user', 'updateAccountRequest', 'loading']) ||
state.getIn(['user', 'putClientRequest', 'loading']),
}),
{
updateAccount,
updateClient,
function Settings() {
const { userStore } = useStore();
const updateClient = userStore.updateClient;
const storeAccountName = userStore.account.name;
const storeOrganizationName = userStore.account.tenantName;
const loading = userStore.loading;
const [accountName, setAccountName] = React.useState(storeAccountName);
const [organizationName, setOrganizationName] = React.useState(storeOrganizationName);
const [changed, setChanged] = React.useState(false);
const onAccNameChange = (e) => {
setAccountName(e.target.value);
setChanged(true);
}
)
export default class Settings extends React.PureComponent {
state = {
accountName: this.props.accountName,
organizationName: this.props.organizationName,
};
onChange = ({ target: { value, name } }) => {
this.setState({ changed: true, [name]: value });
};
const onOrgNameChange = (e) => {
setOrganizationName(e.target.value);
setChanged(true);
}
handleSubmit = (e) => {
const handleSubmit = async (e) => {
e.preventDefault();
const { accountName, organizationName } = this.state;
this.props
.updateClient({ name: accountName, tenantName: organizationName })
.then(() => this.setState({ changed: false }));
};
render() {
const { loading } = this.props;
const { accountName, organizationName, changed, copied } = this.state;
return (
<Form onSubmit={this.handleSubmit} className={styles.form}>
<Form.Field>
<label htmlFor="accountName">{'Name'}</label>
<Input
name="accountName"
id="accountName"
type="text"
onChange={this.onChange}
value={accountName}
maxLength={50}
/>
</Form.Field>
<Form.Field>
<label htmlFor="organizationName">{'Organization'}</label>
<Input
name="organizationName"
id="organizationName"
type="text"
onChange={this.onChange}
value={organizationName}
maxLength={50}
/>
</Form.Field>
<Button variant="outline" loading={loading} disabled={!changed} type="submit">
{'Update'}
</Button>
</Form>
);
await updateClient({ name: accountName, tenantName: organizationName });
setChanged(false);
}
return (
<Form onSubmit={handleSubmit} className={styles.form}>
<Form.Field>
<label htmlFor="accountName">{'Name'}</label>
<Input
name="accountName"
id="accountName"
type="text"
onChange={onAccNameChange}
value={accountName}
maxLength={50}
/>
</Form.Field>
<Form.Field>
<label htmlFor="organizationName">{'Organization'}</label>
<Input
name="organizationName"
id="organizationName"
type="text"
onChange={onOrgNameChange}
value={organizationName}
maxLength={50}
/>
</Form.Field>
<Button variant="outline" loading={loading} disabled={!changed} type="submit">
{'Update'}
</Button>
</Form>
);
}
export default observer(Settings);

View file

@ -1,51 +1,43 @@
// TODO this can be deleted
import React from 'react';
import copy from 'copy-to-clipboard';
import { connect } from 'react-redux';
import styles from './profileSettings.module.css';
import { Form, Input, Button } from "UI";
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
@connect(state => ({
tenantKey: state.getIn([ 'user', 'account', 'tenantKey' ]),
}))
export default class TenantKey extends React.PureComponent {
state = { copied: false }
copyHandler = () => {
const { tenantKey } = this.props;
this.setState({ copied: true });
function TenantKey() {
const [ copied, setCopied ] = React.useState(false);
const { userStore } = useStore();
const tenantKey = userStore.account.tenantKey;
const copyHandler = () => {
setCopied(true);
copy(tenantKey);
setTimeout(() => {
this.setState({ copied: false });
setCopied(false);
}, 1000);
};
render() {
const { tenantKey } = this.props;
const { copied } = this.state;
return (
<Form onSubmit={ this.handleSubmit } className={ styles.form }>
<Form.Field>
<label htmlFor="tenantKey">{ 'Tenant Key' }</label>
<Input
name="tenantKey"
id="tenantKey"
type="text"
readOnly={ true }
value={ tenantKey }
leadingButton={
<Button
variant="text-primary"
role="button"
onClick={ this.copyHandler }
>
{ copied ? 'Copied' : 'Copy' }
</Button>
}
/>
</Form.Field>
</Form>
);
}
return (
<Form.Field>
<label htmlFor="tenantKey">{ 'Tenant Key' }</label>
<Input
name="tenantKey"
id="tenantKey"
type="text"
readOnly={ true }
value={ tenantKey }
leadingButton={
<Button
variant="text-primary"
role="button"
onClick={ copyHandler }
>
{ copied ? 'Copied' : 'Copy' }
</Button>
}
/>
</Form.Field>
);
}
export default observer(TenantKey);

View file

@ -1,133 +1,117 @@
import React, { useEffect } from 'react';
import cn from 'classnames';
import { Loader, NoContent, Button, Tooltip } from 'UI';
import { connect } from 'react-redux';
import stl from './roles.module.css';
import RoleForm from './components/RoleForm';
import { init, edit, fetchList, remove as deleteRole, resetErrors } from 'Duck/roles';
import RoleItem from './components/RoleItem';
import { confirm } from 'UI';
import { toast } from 'react-toastify';
import withPageTitle from 'HOCs/withPageTitle';
import cn from 'classnames';
import { observer } from 'mobx-react-lite';
import React, { useEffect } from 'react';
import { useModal } from 'App/components/Modal';
import { useStore } from 'App/mstore';
import { Button, Loader, NoContent, Tooltip } from 'UI';
import { confirm } from 'UI';
interface Props {
loading: boolean;
init: (role?: any) => void;
edit: (role: any) => void;
instance: any;
roles: any[];
deleteRole: (id: any) => Promise<void>;
fetchList: () => Promise<void>;
account: any;
permissionsMap: any;
removeErrors: any;
resetErrors: () => void;
projectsMap: any;
}
import RoleForm from './components/RoleForm';
import RoleItem from './components/RoleItem';
import stl from './roles.module.css';
function Roles(props: Props) {
const { loading, roles, init, edit, deleteRole, account, permissionsMap, projectsMap, removeErrors } = props;
const { showModal, hideModal } = useModal();
const isAdmin = account.admin || account.superAdmin;
function Roles() {
const { roleStore, projectsStore, userStore } = useStore();
const account = userStore.account;
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;
const deleteRole = roleStore.deleteRole;
const permissionsMap: any = {};
roleStore.permissions.forEach((p: any) => {
permissionsMap[p.value] = p.text;
});
const { showModal, hideModal } = useModal();
const isAdmin = account.admin || account.superAdmin;
useEffect(() => {
props.fetchList();
}, []);
useEffect(() => {
void roleStore.fetchRoles();
}, []);
useEffect(() => {
if (removeErrors && removeErrors.size > 0) {
removeErrors.forEach((e: any) => {
toast.error(e);
});
}
return () => {
props.resetErrors();
};
}, [removeErrors]);
const editHandler = (role: any) => {
init(role);
showModal(<RoleForm closeModal={hideModal} permissionsMap={permissionsMap} deleteHandler={deleteHandler} />, { right: true });
};
const deleteHandler = async (role: any) => {
if (
await confirm({
header: 'Roles',
confirmation: `Are you sure you want to remove this role?`,
})
) {
deleteRole(role.roleId).then(hideModal);
}
};
return (
<React.Fragment>
<Loader loading={loading}>
<div className="bg-white rounded-lg shadow-sm border">
<div className={cn(stl.tabHeader, 'flex items-center')}>
<div className="flex items-center mr-auto px-5 pt-5">
<h3 className={cn(stl.tabTitle, 'text-2xl')}>Roles and Access</h3>
<Tooltip title="You dont have the permissions to perform this action." disabled={isAdmin}>
<Button variant="primary" onClick={() => editHandler({})}>Add</Button>
</Tooltip>
</div>
</div>
<NoContent title="No roles are available" size="small" show={false}>
<div className={''}>
<div className={cn('flex items-start py-3 border-b px-5 pr-20 font-medium')}>
<div className="" style={{ width: '20%' }}>
Title
</div>
<div className="" style={{ width: '30%' }}>
Project Access
</div>
<div className="" style={{ width: '50%' }}>
Feature Access
</div>
<div></div>
</div>
{roles.map((role) => (
<RoleItem
key={role.roleId}
role={role}
isAdmin={isAdmin}
permissions={permissionsMap}
projects={projectsMap}
editHandler={editHandler}
deleteHandler={deleteHandler}
/>
))}
</div>
</NoContent>
</div>
</Loader>
</React.Fragment>
const editHandler = (role: any) => {
init(role);
showModal(
<RoleForm
closeModal={hideModal}
permissionsMap={permissionsMap}
deleteHandler={deleteHandler}
/>,
{ right: true }
);
};
const deleteHandler = async (role: any) => {
if (
await confirm({
header: 'Roles',
confirmation: `Are you sure you want to remove this role?`,
})
) {
deleteRole(role.roleId).then(hideModal);
}
};
return (
<React.Fragment>
<Loader loading={loading}>
<div className="bg-white rounded-lg shadow-sm border">
<div className={cn(stl.tabHeader, 'flex items-center')}>
<div className="flex items-center mr-auto px-5 pt-5">
<h3 className={cn(stl.tabTitle, 'text-2xl')}>Roles and Access</h3>
<Tooltip
title="You dont have the permissions to perform this action."
disabled={isAdmin}
>
<Button variant="primary" onClick={() => editHandler({})}>
Add
</Button>
</Tooltip>
</div>
</div>
<NoContent title="No roles are available" size="small" show={false}>
<div className={''}>
<div
className={cn(
'flex items-start py-3 border-b px-5 pr-20 font-medium'
)}
>
<div className="" style={{ width: '20%' }}>
Title
</div>
<div className="" style={{ width: '30%' }}>
Project Access
</div>
<div className="" style={{ width: '50%' }}>
Feature Access
</div>
<div></div>
</div>
{roles.map((role) => (
<RoleItem
key={role.roleId}
role={role}
isAdmin={isAdmin}
permissions={permissionsMap}
projects={projectsMap}
editHandler={editHandler}
deleteHandler={deleteHandler}
/>
))}
</div>
</NoContent>
</div>
</Loader>
</React.Fragment>
);
}
export default connect(
(state: any) => {
const permissions = state.getIn(['roles', 'permissions']);
const permissionsMap: any = {};
permissions.forEach((p: any) => {
permissionsMap[p.value] = p.text;
});
const projects = state.getIn(['site', 'list']);
return {
instance: state.getIn(['roles', 'instance']) || null,
permissionsMap: permissionsMap,
roles: state.getIn(['roles', 'list']),
removeErrors: state.getIn(['roles', 'removeRequest', 'errors']),
loading: state.getIn(['roles', 'fetchRequest', 'loading']),
account: state.getIn(['user', 'account']),
projectsMap: projects.reduce((acc: any, p: any) => {
acc[p.get('id')] = p.get('name');
return acc;
}, {}),
};
},
{ init, edit, fetchList, deleteRole, resetErrors }
)(withPageTitle('Roles & Access - OpenReplay Preferences')(Roles));
export default withPageTitle('Roles & Access - OpenReplay Preferences')(
observer(Roles)
);

View file

@ -1,15 +0,0 @@
import React from 'react';
import Role from 'Types/role'
interface Props {
role: Role
}
function Permissions(props: Props) {
return (
<div>
</div>
);
}
export default Permissions;

View file

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

View file

@ -1,196 +1,222 @@
import React, { useRef, useEffect } from 'react';
import { connect } from 'react-redux';
import stl from './roleForm.module.css';
import { save, edit } from 'Duck/roles';
import { Form, Input, Button, Checkbox, Icon } from 'UI';
import { observer } from 'mobx-react-lite';
import React, { useEffect, useRef } from 'react';
import { useStore } from 'App/mstore';
import { Button, Checkbox, Form, Icon, Input } from 'UI';
import Select from 'Shared/Select';
interface Permission {
name: string;
value: string;
}
import stl from './roleForm.module.css';
interface Props {
role: any;
edit: (role: any) => void;
save: (role: any) => Promise<void>;
closeModal: (toastMessage?: string) => void;
saving: boolean;
permissions: Array<Permission>[];
projectOptions: Array<any>[];
permissionsMap: any;
projectsMap: any;
deleteHandler: (id: any) => Promise<void>;
closeModal: (toastMessage?: string) => void;
permissionsMap: any;
deleteHandler: (id: any) => Promise<void>;
}
const RoleForm = (props: Props) => {
const { role, edit, save, closeModal, saving, permissions, projectOptions, permissionsMap, projectsMap } = props;
let focusElement = useRef<any>(null);
const _save = () => {
save(role).then(() => {
closeModal(role.exists() ? 'Role updated' : 'Role created');
});
};
const { roleStore, projectsStore } = useStore();
const projects = projectsStore.list;
const role = roleStore.instance;
const saving = roleStore.loading;
const { closeModal, permissionsMap } = props;
const projectOptions = projects
.filter(({ value }) => !role.projects.includes(value))
.map((p: any) => ({
key: p.id,
value: p.id,
label: p.name,
}))
.filter(({ value }: any) => !role.projects.includes(value));
const projectsMap = projects.reduce((acc: any, p: any) => {
acc[p.id] = p.name;
return acc;
}, {});
const write = ({ target: { value, name } }: any) => edit({ [name]: value });
let focusElement = useRef<any>(null);
const permissions: {}[] = roleStore.permissions
.filter(({ value }) => !role.permissions.includes(value))
.map((p) => ({
label: p.text,
value: p.value,
}));
const _save = () => {
roleStore.saveRole(role).then(() => {
closeModal(role.exists() ? 'Role updated' : 'Role created');
});
};
const onChangePermissions = (e: any) => {
const { permissions } = role;
const index = permissions.indexOf(e);
const _perms = permissions.contains(e) ? permissions.remove(index) : permissions.push(e);
edit({ permissions: _perms });
};
const write = ({ target: { value, name } }: any) =>
roleStore.editRole({ [name]: value });
const onChangeProjects = (e: any) => {
const { projects } = role;
const index = projects.indexOf(e);
const _projects = index === -1 ? projects.push(e) : projects.remove(index);
edit({ projects: _projects });
};
const onChangePermissions = (e: any) => {
const { permissions } = role;
const index = permissions.indexOf(e);
let _perms;
if (permissions.includes(e)) {
permissions.splice(index, 1);
_perms = permissions;
} else {
_perms = permissions.concat(e);
}
roleStore.editRole({ permissions: _perms });
};
const writeOption = ({ name, value }: any) => {
if (name === 'permissions') {
onChangePermissions(value);
} else if (name === 'projects') {
onChangeProjects(value);
}
};
const onChangeProjects = (e: any) => {
const { projects } = role;
const index = projects.indexOf(e);
let _projects;
if (index === -1) {
_projects = projects.concat(e);
} else {
projects.splice(index, 1);
_projects = projects;
}
roleStore.editRole({ projects: _projects });
};
const toggleAllProjects = () => {
const { allProjects } = role;
edit({ allProjects: !allProjects });
};
const writeOption = ({ name, value }: any) => {
if (name === 'permissions') {
onChangePermissions(value);
} else if (name === 'projects') {
onChangeProjects(value);
}
};
useEffect(() => {
focusElement && focusElement.current && focusElement.current.focus();
}, []);
const toggleAllProjects = () => {
const { allProjects } = role;
roleStore.editRole({ allProjects: !allProjects });
};
return (
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
<h3 className="p-5 text-2xl">{role.exists() ? 'Edit Role' : 'Create Role'}</h3>
<div className="px-5">
<Form onSubmit={_save}>
<Form.Field>
<label>{'Title'}</label>
<Input
ref={focusElement}
name="name"
value={role.name}
onChange={write}
maxLength={40}
className={stl.input}
id="name-field"
placeholder="Ex. Admin"
/>
</Form.Field>
useEffect(() => {
focusElement && focusElement.current && focusElement.current.focus();
}, []);
<Form.Field>
<label>{'Project Access'}</label>
return (
<div
className="bg-white h-screen overflow-y-auto"
style={{ width: '350px' }}
>
<h3 className="p-5 text-2xl">
{role.exists() ? 'Edit Role' : 'Create Role'}
</h3>
<div className="px-5">
<Form onSubmit={_save}>
<Form.Field>
<label>{'Title'}</label>
<Input
ref={focusElement}
name="name"
value={role.name}
onChange={write}
maxLength={40}
className={stl.input}
id="name-field"
placeholder="Ex. Admin"
/>
</Form.Field>
<div className="flex my-3">
<Checkbox
name="allProjects"
className="font-medium mr-3"
type="checkbox"
checked={role.allProjects}
onClick={toggleAllProjects}
label={''}
/>
<div className="cursor-pointer leading-none select-none" onClick={toggleAllProjects}>
<div>All Projects</div>
<span className="text-xs text-gray-600">(Uncheck to select specific projects)</span>
</div>
</div>
{!role.allProjects && (
<>
<Select
isSearchable
name="projects"
options={projectOptions}
onChange={({ value }: any) => writeOption({ name: 'projects', value: value.value })}
value={null}
/>
{role.projects.size > 0 && (
<div className="flex flex-row items-start flex-wrap mt-4">
{role.projects.map((p: any) => OptionLabel(projectsMap, p, onChangeProjects))}
</div>
)}
</>
)}
</Form.Field>
<Form.Field>
<label>{'Project Access'}</label>
<Form.Field>
<label>{'Capability Access'}</label>
<Select
isSearchable
name="permissions"
options={permissions}
onChange={({ value }: any) => writeOption({ name: 'permissions', value: value.value })}
value={null}
/>
{role.permissions.size > 0 && (
<div className="flex flex-row items-start flex-wrap mt-4">
{role.permissions.map((p: any) => OptionLabel(permissionsMap, p, onChangePermissions))}
</div>
)}
</Form.Field>
</Form>
<div className="flex items-center">
<div className="flex items-center mr-auto">
<Button onClick={_save} disabled={!role.validate()} loading={saving} variant="primary" className="float-left mr-2">
{role.exists() ? 'Update' : 'Add'}
</Button>
{role.exists() && <Button onClick={closeModal}>{'Cancel'}</Button>}
</div>
{role.exists() && (
<Button variant="text" onClick={() => props.deleteHandler(role)}>
<Icon name="trash" size="18" />
</Button>
)}
</div>
<div className="flex my-3">
<Checkbox
name="allProjects"
className="font-medium mr-3"
type="checkbox"
checked={role.allProjects}
onClick={toggleAllProjects}
label={''}
/>
<div
className="cursor-pointer leading-none select-none"
onClick={toggleAllProjects}
>
<div>All Projects</div>
<span className="text-xs text-gray-600">
(Uncheck to select specific projects)
</span>
</div>
</div>
{!role.allProjects && (
<>
<Select
isSearchable
name="projects"
options={projectOptions}
onChange={({ value }: any) =>
writeOption({ name: 'projects', value: value.value })
}
value={null}
/>
{role.projects.size > 0 && (
<div className="flex flex-row items-start flex-wrap mt-4">
{role.projects.map((p: any) =>
OptionLabel(projectsMap, p, onChangeProjects)
)}
</div>
)}
</>
)}
</Form.Field>
<Form.Field>
<label>{'Capability Access'}</label>
<Select
isSearchable
name="permissions"
options={permissions}
onChange={({ value }: any) =>
writeOption({ name: 'permissions', value: value.value })
}
value={null}
/>
{role.permissions.length > 0 && (
<div className="flex flex-row items-start flex-wrap mt-4">
{role.permissions.map((p: any) =>
OptionLabel(permissionsMap, p, onChangePermissions)
)}
</div>
)}
</Form.Field>
</Form>
<div className="flex items-center">
<div className="flex items-center mr-auto">
<Button
onClick={_save}
disabled={!role.validate}
loading={saving}
variant="primary"
className="float-left mr-2"
>
{role.exists() ? 'Update' : 'Add'}
</Button>
{role.exists() && <Button onClick={closeModal}>{'Cancel'}</Button>}
</div>
{role.exists() && (
<Button variant="text" onClick={() => props.deleteHandler(role)}>
<Icon name="trash" size="18" />
</Button>
)}
</div>
);
</div>
</div>
);
};
export default connect(
(state: any) => {
const role = state.getIn(['roles', 'instance']);
const projects = state.getIn(['site', 'list']);
return {
role,
projectOptions: projects
.map((p: any) => ({
key: p.get('id'),
value: p.get('id'),
label: p.get('name'),
// isDisabled: role.projects.includes(p.get('id')),
}))
.filter(({ value }: any) => !role.projects.includes(value))
.toJS(),
permissions: state
.getIn(['roles', 'permissions'])
.filter(({ value }: any) => !role.permissions.includes(value))
.map(({ text, value }: any) => ({ label: text, value }))
.toJS(),
saving: state.getIn(['roles', 'saveRequest', 'loading']),
projectsMap: projects.reduce((acc: any, p: any) => {
acc[p.get('id')] = p.get('name');
return acc;
}, {}),
};
},
{ edit, save }
)(RoleForm);
export default observer(RoleForm);
function OptionLabel(nameMap: any, p: any, onChangeOption: (e: any) => void) {
return (
<div className="px-2 py-1 rounded bg-gray-lightest mr-2 mb-2 border flex items-center justify-between" key={p.roleId}>
<div>{nameMap[p]}</div>
<div className="cursor-pointer ml-2" onClick={() => onChangeOption(p)}>
<Icon name="close" size="12" />
</div>
</div>
);
return (
<div
className="px-2 py-1 rounded bg-gray-lightest mr-2 mb-2 border flex items-center justify-between"
key={p.roleId}
>
<div>{nameMap[p]}</div>
<div className="cursor-pointer ml-2" onClick={() => onChangeOption(p)}>
<Icon name="close" size="12" />
</div>
</div>
);
}

View file

@ -1,41 +1,29 @@
import withPageTitle from 'HOCs/withPageTitle';
import React from 'react';
import { connect } from 'react-redux';
import { PageTitle, Divider } from 'UI';
import ListingVisibility from 'Shared/SessionSettings/components/ListingVisibility';
import { Divider, PageTitle } from 'UI';
import DefaultPlaying from 'Shared/SessionSettings/components/DefaultPlaying';
import DefaultTimezone from 'Shared/SessionSettings/components/DefaultTimezone';
import withPageTitle from 'HOCs/withPageTitle';
import ListingVisibility from 'Shared/SessionSettings/components/ListingVisibility';
import MouseTrailSettings from 'Shared/SessionSettings/components/MouseTrailSettings';
type Props = {}
const mapStateToProps = (state: any) => ({
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee',
account: state.getIn(['user', 'account'])
});
const connector = connect(mapStateToProps);
function SessionsListingSettings(props: Props) {
function SessionsListingSettings() {
return (
<div className='bg-white rounded-lg border shadow-sm p-5'>
<div className="bg-white rounded-lg border shadow-sm p-5">
<PageTitle title={<div>Sessions Listing</div>} />
<div className='flex flex-col mt-4'>
<div className='max-w-lg'>
<div className="flex flex-col mt-4">
<div className="max-w-lg">
<ListingVisibility />
</div>
<Divider />
<div>
<DefaultPlaying />
</div>
<Divider />
<div>
<DefaultTimezone />
</div>
@ -44,12 +32,11 @@ function SessionsListingSettings(props: Props) {
<div>
<MouseTrailSettings />
</div>
</div>
</div>
);
}
export default connector(
withPageTitle('Sessions Listings - OpenReplay Preferences')(SessionsListingSettings)
export default withPageTitle('Sessions Listings - OpenReplay Preferences')(
SessionsListingSettings
);

View file

@ -1,25 +1,22 @@
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 { connect } from 'react-redux';
import { observer } from 'mobx-react-lite';
import { useModal } from 'App/components/Modal';
import NewSiteForm from '../NewSiteForm';
const PERMISSION_WARNING = 'You dont have the permissions to perform this action.';
const LIMIT_WARNING = 'You have reached site limit.';
function AddProjectButton({ isAdmin = false, init = () => {} }: any) {
const { userStore } = useStore();
function AddProjectButton({ isAdmin = false }: any) {
const { userStore, projectsStore } = useStore();
const init = projectsStore.initProject;
const { showModal, hideModal } = useModal();
const limtis = useObserver(() => userStore.limits);
const canAddProject = useObserver(
() => isAdmin && (limtis.projects === -1 || limtis.projects > 0)
);
const limits = userStore.limits;
const canAddProject = isAdmin && (limits.projects === -1 || limits.projects > 0)
const onClick = () => {
init();
init({});
showModal(<NewSiteForm onClose={hideModal} />, { right: true });
};
return (
@ -34,4 +31,4 @@ function AddProjectButton({ isAdmin = false, init = () => {} }: any) {
);
}
export default connect(null, { init, remove, fetchGDPR })(AddProjectButton);
export default observer(AddProjectButton);

View file

@ -1,7 +1,7 @@
import React from 'react';
import { connect } from 'react-redux';
import { observer } from 'mobx-react-lite';
import { useStore } from "App/mstore";
import { Form, Button, Input, Icon } from 'UI';
import { editGDPR, saveGDPR } from 'Duck/site';
import { validateNumber } from 'App/validate';
import styles from './siteForm.module.css';
import Select from 'Shared/Select';
@ -12,124 +12,118 @@ const inputModeOptions = [
{ label: 'Obscure all inputs', value: 'hidden' },
];
@connect(state => ({
site: state.getIn([ 'site', 'instance' ]),
gdpr: state.getIn([ 'site', 'instance', 'gdpr' ]),
saving: state.getIn([ 'site', 'saveGDPR', 'loading' ]),
}), {
editGDPR,
saveGDPR,
})
export default class GDPRForm extends React.PureComponent {
onChange = ({ target: { name, value } }) => {
function GDPRForm(props) {
const { projectsStore } = useStore();
const site = projectsStore.instance;
const gdpr = site.gdpr;
const saving = false //projectsStore.;
const editGDPR = projectsStore.editGDPR;
const saveGDPR = projectsStore.saveGDPR;
const onChange = ({ target: { name, value } }) => {
if (name === "sampleRate") {
if (!validateNumber(value, { min: 0, max: 100 })) return;
if (value.length > 1 && value[0] === "0") {
value = value.slice(1);
}
}
this.props.editGDPR({ [ name ]: value });
editGDPR({ [ name ]: value });
}
onSampleRateBlur = ({ target: { name, value } }) => { //TODO: editState hoc
const onSampleRateBlur = ({ target: { name, value } }) => { //TODO: editState hoc
if (value === ''){
this.props.editGDPR({ sampleRate: 100 });
editGDPR({ sampleRate: 100 });
}
}
onChangeSelect = ({ name, value }) => {
this.props.editGDPR({ [ name ]: value });
const onChangeSelect = ({ name, value }) => {
props.editGDPR({ [ name ]: value });
};
onChangeOption = ({ target: { checked, name } }) => {
this.props.editGDPR({ [ name ]: checked });
const onChangeOption = ({ target: { checked, name } }) => {
editGDPR({ [ name ]: checked });
}
onSubmit = (e) => {
const onSubmit = (e) => {
e.preventDefault();
const { site, gdpr } = this.props;
this.props.saveGDPR(site.id, gdpr);
void saveGDPR(site.id);
}
render() {
const {
site, onClose, saving, gdpr,
} = this.props;
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>
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 htmlFor="defaultInputMode">{ 'Data Recording Options' }</label>
<Select
name="defaultInputMode"
options={ inputModeOptions }
onChange={ onChangeSelect }
placeholder="Default Input Mode"
value={ gdpr.defaultInputMode }
/>
</Form.Field>
<Form.Field>
<label>
<input
name="maskNumbers"
type="checkbox"
checked={ gdpr.maskNumbers }
onChange={ onChangeOption }
/>
</Form.Field>
{ 'Do not record any numeric text' }
<div className={ styles.controlSubtext }>{ 'If enabled, OpenReplay will not record or store any numeric text for all sessions.' }</div>
</label>
</Form.Field>
<Form.Field>
<label htmlFor="defaultInputMode">{ 'Data Recording Options' }</label>
<Select
name="defaultInputMode"
options={ inputModeOptions }
onChange={ this.onChangeSelect }
placeholder="Default Input Mode"
value={ gdpr.defaultInputMode }
// className={ styles.dropdown }
<Form.Field>
<label>
<input
name="maskEmails"
type="checkbox"
checked={ gdpr.maskEmails }
onChange={ onChangeOption }
/>
</Form.Field>
{ 'Do not record email addresses ' }
<div className={ styles.controlSubtext }>{ 'If enabled, OpenReplay will not record or store any email address for all sessions.' }</div>
</label>
</Form.Field>
<Form.Field>
<label>
<input
name="maskNumbers"
type="checkbox"
checked={ gdpr.maskNumbers }
onChange={ this.onChangeOption }
/>
{ 'Do not record any numeric text' }
<div className={ styles.controlSubtext }>{ 'If enabled, OpenReplay will not record or store any numeric text for all sessions.' }</div>
</label>
</Form.Field>
<Form.Field>
<label>
<input
name="maskEmails"
type="checkbox"
checked={ gdpr.maskEmails }
onChange={ this.onChangeOption }
/>
{ 'Do not record email addresses ' }
<div className={ styles.controlSubtext }>{ 'If enabled, OpenReplay will not record or store any email address for all sessions.' }</div>
</label>
</Form.Field>
<div className={ styles.blockIpWarapper }>
<div className={ styles.button } onClick={ this.props.toggleBlockedIp }>
{ 'Block IP' } <Icon name="next1" size="18" />
</div>
<div className={ styles.blockIpWarapper }>
<div className={ styles.button } onClick={ props.toggleBlockedIp }>
{ 'Block IP' } <Icon name="next1" size="18" />
</div>
</div>
</div>
<div className={ styles.footer }>
<Button
variant="outline"
className="float-left mr-2"
loading={ saving }
content="Update"
/>
<Button onClick={ onClose } content="Cancel" />
</div>
</Form>
);
}
<div className={ styles.footer }>
<Button
variant="outline"
className="float-left mr-2"
loading={ saving }
content="Update"
/>
<Button onClick={ onClose } content="Cancel" />
</div>
</Form>
)
}
export default observer(GDPRForm);

View file

@ -1,61 +1,48 @@
import { Segmented } from 'antd';
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 { withStore } from 'App/mstore';
import { clearSearch as clearSearchLive } from 'Duck/liveSearch';
import { clearSearch } from 'Duck/search';
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 { useStore } from 'App/mstore';
import { Button, Form, Icon, Input } 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>;
type Props = PropsFromRedux & RouteComponentProps & OwnProps;
type Props = RouteComponentProps & OwnProps;
const NewSiteForm = ({
site,
loading,
save,
remove,
edit,
update,
pushNewSite,
fetchList,
setSiteId,
clearSearch,
clearSearchLive,
location: { pathname },
onClose,
mstore,
activeSiteId,
canDelete,
}: Props) => {
location: { pathname },
onClose
}: 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')) {
@ -67,11 +54,11 @@ const NewSiteForm = ({
}
});
} else {
save(site).then((response: any) => {
saveProject(site!).then((response: any) => {
if (!response || !response.errors || response.errors.size === 0) {
onClose(null);
clearSearch();
clearSearchLive();
searchStore.clearSearch();
mstore.searchStoreLive.clearSearch();
mstore.initClient();
toast.success('Project added successfully');
} else {
@ -87,10 +74,11 @@ const NewSiteForm = ({
header: 'Project Deletion Alert',
confirmation: `Are you sure you want to delete this project? Deleting it will permanently remove the project, along with all associated sessions and data.`,
confirmButton: 'Yes, delete',
cancelButton: 'Cancel',
cancelButton: 'Cancel'
})
&& site?.id
) {
remove(site.id).then(() => {
projectsStore.removeProject(site.id).then(() => {
onClose(null);
if (site.id === activeSiteId) {
setSiteId(null);
@ -100,12 +88,15 @@ const NewSiteForm = ({
};
const handleEdit = ({
target: { name, value },
}: ChangeEvent<HTMLInputElement>) => {
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"
@ -116,7 +107,7 @@ const NewSiteForm = ({
</h3>
<Form
className={styles.formWrapper}
onSubmit={site.validate() && onSubmit}
onSubmit={site.validate && onSubmit}
>
<div className={styles.content}>
<Form.Field>
@ -137,16 +128,16 @@ const NewSiteForm = ({
options={[
{
value: 'web',
label: 'Web',
label: 'Web'
},
{
value: 'ios',
label: 'Mobile',
},
label: 'Mobile'
}
]}
value={site.platform}
onChange={(value) => {
edit({ platform: value });
projectsStore.editInstance({ platform: value });
}}
/>
</div>
@ -157,9 +148,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
@ -183,26 +174,4 @@ 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 connector = connect(mapStateToProps, {
save,
remove,
edit,
update,
pushNewSite,
fetchList,
setSiteId,
clearSearch,
clearSearchLive,
});
export default connector(withRouter(withStore(NewSiteForm)));
export default withRouter(observer(NewSiteForm));

View file

@ -1,9 +1,7 @@
import React, { useState } from 'react';
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 +14,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';
@ -27,9 +27,12 @@ type Project = {
sampleRate: number;
};
type PropsFromRedux = ConnectedProps<typeof connector>;
const Sites = ({ loading, sites, user, init }: PropsFromRedux) => {
const Sites = () => {
const { projectsStore, userStore } = useStore();
const user = userStore.account;
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 +143,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 +163,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}
/>
@ -180,19 +183,4 @@ 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,
});
export default connector(withPageTitle('Projects - OpenReplay Preferences')(Sites));
export default withPageTitle('Projects - OpenReplay Preferences')(observer(Sites));

View file

@ -6,18 +6,17 @@ import { useObserver } from 'mobx-react-lite';
import UserSearch from './components/UserSearch';
import { useModal } from 'App/components/Modal';
import UserForm from './components/UserForm';
import { connect } from 'react-redux';
import { observer } from 'mobx-react-lite';
import AddUserButton from './components/AddUserButton';
import withPageTitle from 'HOCs/withPageTitle';
interface Props {
isOnboarding?: boolean;
account: any;
isEnterprise: boolean;
}
function UsersView(props: Props) {
const { account, isEnterprise, isOnboarding = false } = props;
function UsersView({ isOnboarding = false }: Props) {
const { userStore, roleStore } = useStore();
const account = userStore.account;
const isEnterprise = userStore.isEnterprise;
const userCount = useObserver(() => userStore.list.length);
const roles = useObserver(() => roleStore.list);
const { showModal } = useModal();
@ -31,7 +30,7 @@ function UsersView(props: Props) {
useEffect(() => {
if (roles.length === 0 && isEnterprise) {
roleStore.fetchRoles();
void roleStore.fetchRoles();
}
}, []);
@ -60,7 +59,4 @@ function UsersView(props: Props) {
);
}
export default connect((state: any) => ({
account: state.getIn(['user', 'account']),
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee',
}))(withPageTitle('Team - OpenReplay Preferences')(UsersView));
export default withPageTitle('Team - OpenReplay Preferences')(observer(UsersView));

View file

@ -1,162 +1,171 @@
import React from 'react';
import { Form, Input, CopyButton, Button, Icon } from 'UI'
import cn from 'classnames';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
import { observer } from 'mobx-react-lite';
import React from 'react';
import { useModal } from 'App/components/Modal';
import Select from 'Shared/Select';
import { useStore } from 'App/mstore';
import { Button, CopyButton, Form, Icon, Input } from 'UI';
import { confirm } from 'UI';
import { connect } from 'react-redux';
interface Props {
isSmtp?: boolean;
isEnterprise?: boolean;
}
function UserForm(props: Props) {
const { isSmtp = false, isEnterprise = false } = props;
const { hideModal } = useModal();
const { userStore, roleStore } = useStore();
const isSaving = useObserver(() => userStore.saving);
const user: any = useObserver(() => userStore.instance || userStore.initUser());
const roles = useObserver(() => roleStore.list.filter(r => r.isProtected ? user.isSuperAdmin : true).map(r => ({ label: r.name, value: r.roleId })));
import Select from 'Shared/Select';
const onChangeCheckbox = (e: any) => {
user.updateKey('isAdmin', !user.isAdmin);
function UserForm() {
const { hideModal } = useModal();
const { userStore, roleStore } = useStore();
const isEnterprise = userStore.isEnterprise;
const isSmtp = userStore.account.smtp;
const isSaving = userStore.saving;
const user: any = userStore.instance || userStore.initUser();
const roles = roleStore.list
.filter((r) => (r.isProtected ? user.isSuperAdmin : true))
.map((r) => ({ label: r.name, value: r.roleId }));
const onChangeCheckbox = (e: any) => {
user.updateKey('isAdmin', !user.isAdmin);
};
const onSave = () => {
userStore.saveUser(user).then(() => {
hideModal();
userStore.fetchLimits();
});
};
const write = ({ target: { name, value } }) => {
user.updateKey(name, value);
};
const deleteHandler = async () => {
if (
await confirm({
header: 'Confirm',
confirmButton: 'Yes, delete',
confirmation: `Are you sure you want to permanently delete this user?`,
})
) {
userStore.deleteUser(user.userId).then(() => {
hideModal();
userStore.fetchLimits();
});
}
};
const onSave = () => {
userStore.saveUser(user).then(() => {
hideModal();
userStore.fetchLimits();
});
}
return useObserver(() => (
<div className="bg-white h-screen p-6">
<div className="">
<h1 className="text-2xl mb-4">{`${
user.exists() ? 'Update' : 'Invite'
} User`}</h1>
</div>
<Form onSubmit={onSave}>
<Form.Field>
<label>{'Full Name'}</label>
<Input
name="name"
autoFocus
maxLength="50"
value={user.name}
onChange={write}
className="w-full"
id="name-field"
/>
</Form.Field>
const write = ({ target: { name, value } }) => {
user.updateKey(name, value);
}
const deleteHandler = async () => {
if (await confirm({
header: 'Confirm',
confirmButton: 'Yes, delete',
confirmation: `Are you sure you want to permanently delete this user?`
})) {
userStore.deleteUser(user.userId).then(() => {
hideModal();
userStore.fetchLimits();
});
}
}
return useObserver(() => (
<div className="bg-white h-screen p-6">
<div className="">
<h1 className="text-2xl mb-4">{`${user.exists() ? 'Update' : 'Invite'} User`}</h1>
</div>
<Form onSubmit={ onSave } >
<Form.Field>
<label>{ 'Full Name' }</label>
<Input
name="name"
autoFocus
maxLength="50"
value={ user.name }
onChange={ write }
className="w-full"
id="name-field"
/>
</Form.Field>
<div className="form-group">
<label>{ 'Email Address' }</label>
<Input
disabled={user.exists()}
name="email"
maxLength="320"
value={ user.email }
onChange={ write }
className="w-full"
/>
</div>
{ !isSmtp &&
<div className={cn("mb-4 p-2 bg-yellow rounded")}>
SMTP is not configured (see <a className="link" href="https://docs.openreplay.com/configuration/configure-smtp" target="_blank">here</a> how to set it up). You can still add new users, but youd have to manually copy then send them the invitation link.
</div>
}
<Form.Field>
<label className="flex items-start cursor-pointer">
<input
name="admin"
type="checkbox"
checked={ !!user.isAdmin || !!user.isSuperAdmin }
onChange={ onChangeCheckbox }
disabled={user.isSuperAdmin}
className="mt-1"
/>
<div className="ml-2 select-none">
<span>Admin Privileges</span>
<div className="text-sm color-gray-medium -mt-1">{ 'Can manage Projects and team members.' }</div>
</div>
</label>
</Form.Field>
{ isEnterprise && (
<Form.Field>
<label htmlFor="role">{ 'Role' }</label>
<Select
placeholder="Select Role"
selection
options={ roles }
name="roleId"
defaultValue={ user.roleId }
onChange={({ value }) => user.updateKey('roleId', value.value)}
className="block"
isDisabled={user.isSuperAdmin}
/>
</Form.Field>
)}
</Form>
<div className="flex items-center">
<div className="flex items-center mr-auto">
<Button
onClick={ onSave }
disabled={ !user.valid(isEnterprise) || isSaving }
loading={ isSaving }
variant="primary"
className="float-left mr-2"
>
{ user.exists() ? 'Update' : 'Invite' }
</Button>
{user.exists() && (
<Button onClick={ hideModal }>
{ 'Cancel' }
</Button>
)}
</div>
<div>
<Button
disabled={user.isSuperAdmin}
data-hidden={ !user.exists() }
onClick={ deleteHandler }
>
<Icon name="trash" size="16" />
</Button>
</div>
</div>
{ !user.isJoined && user.invitationLink &&
<CopyButton
content={user.invitationLink}
className="link mt-4"
btnText="Copy invite link"
/>
}
<div className="form-group">
<label>{'Email Address'}</label>
<Input
disabled={user.exists()}
name="email"
maxLength="320"
value={user.email}
onChange={write}
className="w-full"
/>
</div>
));
{!isSmtp && (
<div className={cn('mb-4 p-2 bg-yellow rounded')}>
SMTP is not configured (see{' '}
<a
className="link"
href="https://docs.openreplay.com/configuration/configure-smtp"
target="_blank"
>
here
</a>{' '}
how to set it up). You can still add new users, but youd have to
manually copy then send them the invitation link.
</div>
)}
<Form.Field>
<label className="flex items-start cursor-pointer">
<input
name="admin"
type="checkbox"
checked={!!user.isAdmin || !!user.isSuperAdmin}
onChange={onChangeCheckbox}
disabled={user.isSuperAdmin}
className="mt-1"
/>
<div className="ml-2 select-none">
<span>Admin Privileges</span>
<div className="text-sm color-gray-medium -mt-1">
{'Can manage Projects and team members.'}
</div>
</div>
</label>
</Form.Field>
{isEnterprise && (
<Form.Field>
<label htmlFor="role">{'Role'}</label>
<Select
placeholder="Select Role"
selection
options={roles}
name="roleId"
defaultValue={user.roleId}
onChange={({ value }) => user.updateKey('roleId', value.value)}
className="block"
isDisabled={user.isSuperAdmin}
/>
</Form.Field>
)}
</Form>
<div className="flex items-center">
<div className="flex items-center mr-auto">
<Button
onClick={onSave}
disabled={!user.valid(isEnterprise) || isSaving}
loading={isSaving}
variant="primary"
className="float-left mr-2"
>
{user.exists() ? 'Update' : 'Invite'}
</Button>
{user.exists() && <Button onClick={hideModal}>{'Cancel'}</Button>}
</div>
<div>
<Button
disabled={user.isSuperAdmin}
data-hidden={!user.exists()}
onClick={deleteHandler}
>
<Icon name="trash" size="16" />
</Button>
</div>
</div>
{!user.isJoined && user.invitationLink && (
<CopyButton
content={user.invitationLink}
className="link mt-4"
btnText="Copy invite link"
/>
)}
</div>
));
}
export default connect((state: any) => ({
isEnterprise: state.getIn([ 'user', 'account', 'edition' ]) === 'ee',
isSmtp: state.getIn([ 'user', 'account', 'smtp' ]),
}))(UserForm);
export default observer(UserForm);

View file

@ -1,33 +0,0 @@
import React from 'react'
import { connect } from 'react-redux'
import cn from 'classnames'
import stl from './widgetHolder.module.css'
import LazyLoad from 'react-lazyload';
const WidgetHolder = props => {
const { comparing, Component, period, periodCompare, fullWidth = false } = props;
const showSync = comparing && period.rangeName === periodCompare.rangeName;
return (
<div className={ cn(stl.wrapper, { 'grid grid-cols-2 gap-4 mb-2' : comparing && !fullWidth })}>
<LazyLoad height={300} offset={320} >
{<Component showSync={showSync} />}
</LazyLoad>
{comparing && (
<LazyLoad height={300} offset={320}>
<React.Fragment>
<div className={fullWidth ? 'mt-4' : ''}>
<Component compare showSync={showSync} />
</div>
</React.Fragment>
</LazyLoad>
)}
</div>
)
}
export default connect(state => ({
comparing: state.getIn([ 'dashboard', 'comparing' ]),
period: state.getIn([ 'dashboard', 'period' ]),
periodCompare: state.getIn([ 'dashboard', 'periodCompare' ])
}))(WidgetHolder)

View file

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

View file

@ -1,12 +0,0 @@
.wrapper {
border: dotted 2px transparent;
border-radius: 3px;
margin: -5px;
padding: 5px;
transition: all 0.3s;
&:hover {
transition: all 0.2s;
border: dotted 2px $gray-medium;
}
}

View file

@ -2,17 +2,13 @@ import React from 'react'
import { useStore } from 'App/mstore'
import { observer } from 'mobx-react-lite'
import ClickMapRenderer from 'App/components/Session/Player/ClickMapRenderer'
import { connect } from 'react-redux'
import { fetchInsights } from 'App/duck/sessions'
import { NoContent, Icon } from 'App/components/ui'
function ClickMapCard({
insights,
fetchInsights,
insightsFilters,
}: any) {
function ClickMapCard() {
const [customSession, setCustomSession] = React.useState<any>(null)
const { metricStore, dashboardStore } = useStore();
const { metricStore, dashboardStore, sessionStore } = useStore();
const fetchInsights = sessionStore.fetchInsights
const insights = sessionStore.insights
const onMarkerClick = (s: string, innerText: string) => {
metricStore.changeClickMapSearch(s, innerText)
}
@ -36,7 +32,7 @@ function ClickMapCard({
const rangeValue = dashboardStore.drillDownPeriod.rangeValue
const startDate = dashboardStore.drillDownPeriod.start
const endDate = dashboardStore.drillDownPeriod.end
fetchInsights({ ...insightsFilters, url: mapUrl || '/', startDate, endDate, rangeValue, clickRage: metricStore.clickMapFilter })
void fetchInsights({ url: mapUrl || '/', startDate, endDate, rangeValue, clickRage: metricStore.clickMapFilter })
}, [dashboardStore.drillDownPeriod.start, dashboardStore.drillDownPeriod.end, dashboardStore.drillDownPeriod.rangeValue, metricStore.clickMapFilter])
if (!metricStore.instance.data.domURL || insights.size === 0) {
@ -76,13 +72,4 @@ function ClickMapCard({
)
}
export default connect(
(state: any) => ({
insightsFilters: state.getIn(['sessions', 'insightFilters']),
visitedEvents: state.getIn(['sessions', 'visitedEvents']),
insights: state.getIn(['sessions', 'insights']),
host: state.getIn(['sessions', 'host']),
}),
{ fetchInsights, }
)
(observer(ClickMapCard))
export default observer(ClickMapCard)

View file

@ -1,6 +1,5 @@
import React, { useEffect } from 'react';
import { Form, SegmentSelection } from 'UI';
import { connect } from 'react-redux';
import { validateEmail } from 'App/validate';
import { confirm } from 'UI';
import { toast } from 'react-toastify';

View file

@ -10,7 +10,6 @@ import {
} from 'antd';
import { observer } from 'mobx-react-lite';
import React from 'react';
import { connect } from 'react-redux';
import { useHistory } from 'react-router';
import { checkForRecent } from 'App/date';
@ -24,11 +23,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 +219,4 @@ function DashboardList({ siteId }: { siteId: string }) {
);
}
export default connect((state: any) => ({
siteId: state.getIn(['site', 'siteId']),
}))(observer(DashboardList));
export default observer(DashboardList);

View file

@ -1,8 +1,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,16 +12,16 @@ interface NewDashboardModalProps {
open: boolean;
isAddingFromLibrary?: boolean;
isEnterprise?: boolean;
isMobile?: boolean;
}
const NewDashboardModal: React.FC<NewDashboardModalProps> = ({
onClose,
open,
isAddingFromLibrary = false,
isEnterprise = false,
isMobile = false,
}) => {
const { projectsStore, userStore } = useStore();
const isEnterprise = userStore.isEnterprise;
const isMobile = projectsStore.isMobile;
const [step, setStep] = React.useState<number>(0);
const [selectedCategory, setSelectedCategory] =
React.useState<string>('product-analytics');
@ -74,11 +74,4 @@ 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 observer(NewDashboardModal);

View file

@ -1,17 +1,18 @@
import React from 'react';
import { ItemMenu } from 'UI';
import { connect } from 'react-redux';
import { observer } from 'mobx-react-lite';
import { useStore } from "App/mstore";
import { ENTERPRISE_REQUEIRED } from 'App/constants';
interface Props {
editHandler: (isTitle: boolean) => void;
deleteHandler: any;
renderReport: any;
isEnterprise: boolean;
isTitlePresent?: boolean;
}
function DashboardOptions(props: Props) {
const { editHandler, deleteHandler, renderReport, isEnterprise, isTitlePresent } = props;
const { userStore } = useStore();
const isEnterprise = userStore.isEnterprise;
const { editHandler, deleteHandler, renderReport } = props;
const menuItems = [
{ icon: 'pencil', text: 'Rename', onClick: () => editHandler(true) },
{ icon: 'users', text: 'Visibility & Access', onClick: editHandler },
@ -27,6 +28,4 @@ function DashboardOptions(props: Props) {
);
}
export default connect(state => ({
isEnterprise: state.getIn([ 'user', 'account', 'edition' ]) === 'ee' || state.getIn([ 'user', 'account', 'edition' ]) === 'msaas',
}))(DashboardOptions);
export default observer(DashboardOptions);

View file

@ -2,17 +2,13 @@ import React from 'react';
import { SideMenuitem } from 'UI';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { withSiteId, metrics, dashboard, alerts } from 'App/routes';
import { connect } from 'react-redux';
import { compose } from 'redux';
import { setShowAlerts } from 'Duck/dashboard';
interface Props extends RouteComponentProps {
siteId: string;
history: any;
setShowAlerts: (show: boolean) => void;
}
function DashboardSideMenu(props: Props) {
const { history, siteId, setShowAlerts } = props;
const { history, siteId } = props;
const isMetric = history.location.pathname.includes('metrics');
const isDashboards = history.location.pathname.includes('dashboard');
const isAlerts = history.location.pathname.includes('alerts');
@ -57,4 +53,4 @@ function DashboardSideMenu(props: Props) {
);
}
export default compose(withRouter, connect(null, { setShowAlerts }))(DashboardSideMenu);
export default withRouter(DashboardSideMenu);

View file

@ -1,67 +0,0 @@
import React, { useState } from 'react'
import ErrorFrame from './ErrorFrame'
import { IconButton, Icon } from 'UI';
const docLink = 'https://docs.openreplay.com/installation/upload-sourcemaps';
interface Props {
error: any,
errorStack: any,
}
function ErrorDetails({ className, name = "Error", message, errorStack, sourcemapUploaded }: any) {
const [showRaw, setShowRaw] = useState(false)
const firstFunc = errorStack.first() && errorStack.first().function
const openDocs = () => {
window.open(docLink, '_blank');
}
return (
<div className={className} >
{ !sourcemapUploaded && (
<div
style={{ backgroundColor: 'rgba(204, 0, 0, 0.1)' }}
className="font-normal flex items-center text-sm font-regular color-red border p-2 rounded"
>
<Icon name="info" size="16" color="red" />
<div className="ml-2">Source maps must be uploaded to OpenReplay to be able to see stack traces. <a href="#" className="color-red font-medium underline" style={{ textDecoration: 'underline' }} onClick={openDocs}>Learn more.</a></div>
</div>
) }
<div className="flex items-center my-3">
<h3 className="text-xl mr-auto">
Stacktrace
</h3>
<div className="flex justify-end mr-2">
<IconButton
onClick={() => setShowRaw(false) }
label="FULL"
plain={!showRaw}
primaryText={!showRaw}
/>
<IconButton
primaryText={showRaw}
onClick={() => setShowRaw(true) }
plain={showRaw}
label="RAW"
/>
</div>
</div>
<div className="mb-6 code-font" data-hidden={showRaw}>
<div className="leading-relaxed font-weight-bold">{ name }</div>
<div style={{ wordBreak: 'break-all'}}>{message}</div>
</div>
{ showRaw &&
<div className="mb-3 code-font">{name} : {firstFunc ? firstFunc : '?' }</div>
}
{ errorStack.map((frame: any, i: any) => (
<div className="mb-3" key={frame.key}>
<ErrorFrame frame={frame} showRaw={showRaw} isFirst={i == 0} />
</div>
))
}
</div>
)
}
ErrorDetails.displayName = "ErrorDetails";
export default ErrorDetails;

View file

@ -1,53 +0,0 @@
import React, { useState } from 'react'
import { Icon } from 'UI';
import cn from 'classnames';
import stl from './errorFrame.module.css';
function ErrorFrame({ frame = {}, showRaw, isFirst }) {
const [open, setOpen] = useState(isFirst)
const hasContext = frame.context && frame.context.length > 0;
return (
<div>
{ showRaw ?
<div className={stl.rawLine}>at { frame.function ? frame.function : '?' } <span className="color-gray-medium">({`${frame.filename}:${frame.lineNo}:${frame.colNo}`})</span></div>
:
<div className={stl.formatted}>
<div className={cn(stl.header, 'flex items-center cursor-pointer')} onClick={() => setOpen(!open)}>
<div className="truncate">
<span className="font-medium">{ frame.absPath }</span>
{ frame.function &&
<>
<span>{' in '}</span>
<span className="font-medium"> {frame.function} </span>
</>
}
<span>{' at line '}</span>
<span className="font-medium">
{frame.lineNo}:{frame.colNo}
</span>
</div>
{ hasContext &&
<div className="ml-auto mr-3">
<Icon name={ open ? 'minus' : 'plus'} size="14" color="gray-medium" />
</div>
}
</div>
{ open && hasContext &&
<ol start={ frame.context[0][0]} className={stl.content}>
{ frame.context.map(i => (
<li
key={i[0]}
className={ cn("leading-7 text-sm break-all h-auto pl-2", { [stl.errorLine] :i[0] == frame.lineNo }) }
>
<span>{ i[1].replace(/ /g, "\u00a0") }</span>
</li>
))}
</ol>
}
</div>
}
</div>
)
}
export default ErrorFrame;

View file

@ -1,25 +0,0 @@
.rawLine {
margin-left: 30px;
font-family: 'Menlo', 'monaco', 'consolas', monospace;
font-size: 13px;
}
.formatted {
border: solid thin #EEE;
border-radius: 3px;
}
.header {
background-color: $gray-lightest;
padding: 8px;
border-bottom: solid thin #EEE;
}
.content {
font-family: 'Menlo', 'monaco', 'consolas', monospace;
list-style-position: inside;
list-style-type: decimal-leading-zero;
}
.errorLine {
background-color: $teal;
color: white !important;
font-weight: bold;
}

View file

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

View file

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

View file

@ -17,10 +17,7 @@ function ErrorListItem(props: Props) {
const { error, className = '' } = props;
// const { showModal } = useModal();
// const onClick = () => {
// alert('test')
// showModal(<ErrorDetailsModal />, { right: true });
// }
return (
<div
className={ cn("p-3 grid grid-cols-12 gap-4 cursor-pointer py-4 hover:bg-active-blue", className) }

View file

@ -1,21 +0,0 @@
import React, { useEffect } from 'react';
import ErrorListItem from '../ErrorListItem';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
function ErrorsList(props) {
const { errorStore, metricStore } = useStore();
const metric = useObserver(() => metricStore.instance);
useEffect(() => {
errorStore.fetchErrors();
}, []);
return (
<div>
Errors List
<ErrorListItem error={{}} />
</div>
);
}
export default ErrorsList;

View file

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

View file

@ -1,12 +0,0 @@
import React from 'react';
import ErrorsList from '../ErrorsList';
function ErrorsWidget(props) {
return (
<div>
<ErrorsList />
</div>
);
}
export default ErrorsWidget;

View file

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

View file

@ -1,4 +1,3 @@
import filters from 'App/duck/filters';
import Filter from 'App/mstore/types/filter';
import { FilterKey } from 'App/types/filter/filterType';
import { observer } from 'mobx-react-lite';

View file

@ -6,20 +6,20 @@ import { TYPES, LIBRARY, INSIGHTS } from 'App/constants/card';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { dashboardMetricCreate, metricCreate, withSiteId } from 'App/routes';
import { useStore } from 'App/mstore';
import { connect } from 'react-redux';
import { observer } from 'mobx-react-lite';
import { ENTERPRISE_REQUEIRED } from 'App/constants';
interface Props extends RouteComponentProps {
dashboardId?: number;
siteId: string;
isEnterprise: boolean;
isList?: boolean;
}
function MetricTypeList(props: Props) {
const { dashboardId, siteId, history, isEnterprise, isList = false } = props;
const { metricStore } = useStore();
const { dashboardId, siteId, history, isList = false } = props;
const { metricStore, userStore } = useStore();
const { showModal, hideModal } = useModal();
const isEnterprise = userStore.isEnterprise;
const list = React.useMemo(() => {
return TYPES.map((metric: MetricType) => {
@ -67,6 +67,4 @@ function MetricTypeList(props: Props) {
);
}
export default connect((state: any) => ({
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee' || state.getIn(['user', 'account', 'edition']) === 'msaas'
}))(withRouter(MetricTypeList));
export default withRouter(observer(MetricTypeList));

View file

@ -7,16 +7,14 @@ import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
import withLocationHandlers from 'HOCs/withLocationHandlers';
import { Icon } from 'UI';
import { connect } from 'react-redux';
interface Props {
query: Record<string, (key: string) => any>;
onSelect: (arg: any) => void;
isEnterprise?: boolean;
}
function MetricTypeDropdown(props: Props) {
const { isEnterprise } = props;
const { metricStore } = useStore();
const { metricStore, userStore } = useStore();
const isEnterprise = userStore.isEnterprise;
const metric: any = metricStore.instance;
const options = React.useMemo(() => {
@ -84,6 +82,4 @@ function MetricTypeDropdown(props: Props) {
);
}
export default connect((state: any) => ({
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee',
}))(withLocationHandlers()(observer(MetricTypeDropdown)));
export default withLocationHandlers()(observer(MetricTypeDropdown));

View file

@ -11,22 +11,20 @@ import useIsMounted from 'App/hooks/useIsMounted';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import { numberWithCommas } from 'App/utils';
import { HEATMAP } from 'App/constants/card';
import { connect } from 'react-redux';
import { Tag } from "antd";
import { Tag } from 'antd';
interface Props {
className?: string;
metaList: any;
}
function WidgetSessions(props: Props) {
const { className = '', metaList } = props;
const { className = '' } = props;
const [activeSeries, setActiveSeries] = useState('all');
const [data, setData] = useState<any>([]);
const isMounted = useIsMounted();
const [loading, setLoading] = useState(false);
const filteredSessions = getListSessionsBySeries(data, activeSeries);
const { dashboardStore, metricStore, sessionStore } = useStore();
const { dashboardStore, metricStore, sessionStore, customFieldStore } = useStore();
const filter = dashboardStore.drillDownFilter;
const widget = metricStore.instance;
const startTime = DateTime.fromMillis(filter.startTimestamp).toFormat('LLL dd, yyyy HH:mm');
@ -34,6 +32,7 @@ function WidgetSessions(props: Props) {
const [seriesOptions, setSeriesOptions] = useState([{ label: 'All', value: 'all' }]);
const hasFilters = filter.filters.length > 0 || (filter.startTimestamp !== dashboardStore.drillDownPeriod.start || filter.endTimestamp !== dashboardStore.drillDownPeriod.end);
const filterText = filter.filters.length > 0 ? filter.filters[0].value : '';
const metaList = customFieldStore.list.map((i: any) => i.key);
const writeOption = ({ value }: any) => setActiveSeries(value.value);
useEffect(() => {
@ -118,38 +117,39 @@ function WidgetSessions(props: Props) {
return (
<div className={cn(className, 'bg-white p-3 pb-0 rounded-lg shadow-sm border mt-3')}>
<div className='flex items-center justify-between'>
<div className="flex items-center justify-between">
<div>
<div className='flex items-baseline'>
<h2 className='text-xl'>{metricStore.clickMapSearch ? 'Clicks' : 'Sessions'}</h2>
<div className='ml-2 color-gray-medium'>
<div className="flex items-baseline">
<h2 className="text-xl">{metricStore.clickMapSearch ? 'Clicks' : 'Sessions'}</h2>
<div className="ml-2 color-gray-medium">
{metricStore.clickMapLabel ? `on "${metricStore.clickMapLabel}" ` : null}
between <span className='font-medium color-gray-darkest'>{startTime}</span> and{' '}
<span className='font-medium color-gray-darkest'>{endTime}</span>{' '}
between <span className="font-medium color-gray-darkest">{startTime}</span> and{' '}
<span className="font-medium color-gray-darkest">{endTime}</span>{' '}
</div>
</div>
{hasFilters && widget.metricType === 'table' && <div className="py-2"><Tag closable onClose={clearFilters}>{filterText}</Tag></div>}
{hasFilters && widget.metricType === 'table' &&
<div className="py-2"><Tag closable onClose={clearFilters}>{filterText}</Tag></div>}
</div>
<div className='flex items-center gap-4'>
{hasFilters && <Button variant='text-primary' onClick={clearFilters}>Clear Filters</Button>}
<div className="flex items-center gap-4">
{hasFilters && <Button variant="text-primary" onClick={clearFilters}>Clear Filters</Button>}
{widget.metricType !== 'table' && widget.metricType !== HEATMAP && (
<div className='flex items-center ml-6'>
<span className='mr-2 color-gray-medium'>Filter by Series</span>
<div className="flex items-center ml-6">
<span className="mr-2 color-gray-medium">Filter by Series</span>
<Select options={seriesOptions} defaultValue={'all'} onChange={writeOption} plain />
</div>
)}
</div>
</div>
<div className='mt-3'>
<div className="mt-3">
<Loader loading={loading}>
<NoContent
title={
<div className='flex items-center justify-center flex-col'>
<div className="flex items-center justify-center flex-col">
<AnimatedSVG name={ICONS.NO_SESSIONS} size={60} />
<div className='mt-4' />
<div className='text-center'>
<div className="mt-4" />
<div className="text-center">
No relevant sessions found for the selected time period
</div>
</div>
@ -159,22 +159,22 @@ function WidgetSessions(props: Props) {
{filteredSessions.sessions.map((session: any) => (
<React.Fragment key={session.sessionId}>
<SessionItem session={session} metaList={metaList} />
<div className='border-b' />
<div className="border-b" />
</React.Fragment>
))}
<div className='flex items-center justify-between p-5'>
<div className="flex items-center justify-between p-5">
<div>
Showing{' '}
<span className='font-medium'>
<span className="font-medium">
{(metricStore.sessionsPage - 1) * metricStore.sessionsPageSize + 1}
</span>{' '}
to{' '}
<span className='font-medium'>
<span className="font-medium">
{(metricStore.sessionsPage - 1) * metricStore.sessionsPageSize +
filteredSessions.sessions.length}
</span>{' '}
of <span className='font-medium'>{numberWithCommas(filteredSessions.total)}</span>{' '}
of <span className="font-medium">{numberWithCommas(filteredSessions.total)}</span>{' '}
sessions.
</div>
<Pagination
@ -217,8 +217,4 @@ const getListSessionsBySeries = (data: any, seriesId: any) => {
return arr;
};
const mapStateToProps = (state: any) => ({
metaList: state.getIn(['customFields', 'list']).map((i: any) => i.key),
});
export default connect(mapStateToProps)(observer(WidgetSessions));
export default observer(WidgetSessions);

View file

@ -1,89 +1,48 @@
import { observer } from 'mobx-react-lite';
import React from 'react';
import { connect } from 'react-redux';
import withSiteIdRouter from 'HOCs/withSiteIdRouter';
import { error as errorRoute } from 'App/routes';
import { NoContent, Loader } from 'UI';
import { fetch, fetchTrace } from 'Duck/errors';
import MainSection from './MainSection';
import SideSection from './SideSection';
import { useStore } from 'App/mstore';
import { Loader, NoContent } from 'UI';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
@connect(
(state) => ({
errorIdInStore: state.getIn(['errors', 'instance']).errorId,
list: state.getIn(['errors', 'instanceTrace']),
loading:
state.getIn(['errors', 'fetch', 'loading']) ||
state.getIn(['errors', 'fetchTrace', 'loading']),
errorOnFetch:
state.getIn(['errors', 'fetch', 'errors']) || state.getIn(['errors', 'fetchTrace', 'errors']),
}),
{
fetch,
fetchTrace,
}
)
@withSiteIdRouter
export default class ErrorInfo extends React.PureComponent {
ensureInstance() {
const { errorId, loading, errorOnFetch } = this.props;
if (!loading && this.props.errorIdInStore !== errorId && errorId != null) {
this.props.fetch(errorId);
this.props.fetchTrace(errorId);
}
}
componentDidMount() {
this.ensureInstance();
}
componentDidUpdate(prevProps) {
if (prevProps.errorId !== this.props.errorId || prevProps.errorIdInStore !== this.props.errorIdInStore) {
this.ensureInstance();
}
}
next = () => {
const { list, errorId } = this.props;
const curIndex = list.findIndex((e) => e.errorId === errorId);
const next = list.get(curIndex + 1);
if (next != null) {
this.props.history.push(errorRoute(next.errorId));
}
};
prev = () => {
const { list, errorId } = this.props;
const curIndex = list.findIndex((e) => e.errorId === errorId);
const prev = list.get(curIndex - 1);
if (prev != null) {
this.props.history.push(errorRoute(prev.errorId));
}
};
render() {
const { loading, errorIdInStore, list, errorId } = this.props;
import MainSection from './MainSection';
import SideSection from './SideSection';
let nextDisabled = true,
prevDisabled = true;
if (list.size > 0) {
nextDisabled = loading || list.last().errorId === errorId;
prevDisabled = loading || list.first().errorId === errorId;
}
function ErrorInfo(props) {
const { errorStore } = useStore();
const instance = errorStore.instance;
const ensureInstance = () => {
if (errorStore.isLoading) return;
errorStore.fetchError(props.errorId);
errorStore.fetchErrorTrace(props.errorId);
};
return (
<NoContent
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.EMPTY_STATE} size="170" />
<div className="mt-4">No Error Found!</div>
</div>
}
subtext="Please try to find existing one."
show={!loading && errorIdInStore == null}
>
<div className="flex w-full">
<Loader loading={loading} className="w-full">
<MainSection className="w-9/12" />
<SideSection className="w-3/12" />
</Loader>
React.useEffect(() => {
ensureInstance();
}, [props.errorId]);
const errorIdInStore = errorStore.instance?.errorId;
const loading = errorStore.isLoading;
return (
<NoContent
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.EMPTY_STATE} size="170" />
<div className="mt-4">No Error Found!</div>
</div>
</NoContent>
);
}
}
subtext="Please try to find existing one."
show={!loading && errorIdInStore == null}
>
<div className="flex w-full">
<Loader loading={loading || !instance} className="w-full">
<MainSection className="w-9/12" />
<SideSection className="w-3/12" />
</Loader>
</div>
</NoContent>
);
}
export default observer(ErrorInfo);

View file

@ -1,154 +1,132 @@
import React from 'react';
import { connect } from 'react-redux';
import cn from 'classnames';
import withSiteIdRouter from 'HOCs/withSiteIdRouter';
import { ErrorDetails, Icon, Loader, Button } from 'UI';
import { sessions as sessionsRoute } from 'App/routes';
import { RESOLVED } from 'Types/errorInfo';
import { addFilterByKeyAndValue } from 'Duck/search';
import { resolve, unresolve, ignore, toggleFavorite } from 'Duck/errors';
import { FilterKey } from 'Types/filter/filterType';
import cn from 'classnames';
import { observer } from 'mobx-react-lite';
import React from 'react';
import { withRouter } from 'react-router-dom';
import { resentOrDate } from 'App/date';
import { useStore } from 'App/mstore';
import { sessions as sessionsRoute } from 'App/routes';
import Divider from 'Components/Errors/ui/Divider';
import ErrorName from 'Components/Errors/ui/ErrorName';
import Label from 'Components/Errors/ui/Label';
import { FilterKey } from 'Types/filter/filterType';
import { Button, ErrorDetails, Icon, Loader } from 'UI';
import SessionBar from './SessionBar';
@withSiteIdRouter
@connect(
(state) => ({
error: state.getIn(['errors', 'instance']),
trace: state.getIn(['errors', 'instanceTrace']),
sourcemapUploaded: state.getIn(['errors', 'sourcemapUploaded']),
resolveToggleLoading:
state.getIn(['errors', 'resolve', 'loading']) ||
state.getIn(['errors', 'unresolve', 'loading']),
ignoreLoading: state.getIn(['errors', 'ignore', 'loading']),
toggleFavoriteLoading: state.getIn(['errors', 'toggleFavorite', 'loading']),
traceLoading: state.getIn(['errors', 'fetchTrace', 'loading']),
}),
{
resolve,
unresolve,
ignore,
toggleFavorite,
addFilterByKeyAndValue,
}
)
export default class MainSection extends React.PureComponent {
resolve = () => {
const { error } = this.props;
this.props.resolve(error.errorId);
};
function MainSection(props) {
const { errorStore, searchStore } = useStore();
const error = errorStore.instance;
const trace = errorStore.instanceTrace;
const sourcemapUploaded = errorStore.sourcemapUploaded;
const loading = errorStore.isLoading;
const className = props.className;
unresolve = () => {
const { error } = this.props;
this.props.unresolve(error.errorId);
const findSessions = () => {
searchStore.addFilterByKeyAndValue(FilterKey.ERROR, error.message);
props.history.push(sessionsRoute());
};
ignore = () => {
const { error } = this.props;
this.props.ignore(error.errorId);
};
bookmark = () => {
const { error } = this.props;
this.props.toggleFavorite(error.errorId);
};
findSessions = () => {
this.props.addFilterByKeyAndValue(FilterKey.ERROR, this.props.error.message);
this.props.history.push(sessionsRoute());
};
render() {
const {
error,
trace,
sourcemapUploaded,
ignoreLoading,
resolveToggleLoading,
toggleFavoriteLoading,
className,
traceLoading,
} = this.props;
const isPlayer = window.location.pathname.includes('/session/');
return (
<div className={cn(className, 'bg-white border-radius-3 thin-gray-border mb-6')}>
<div className="m-4">
<ErrorName
className="text-lg leading-relaxed"
name={error.name}
message={error.stack0InfoString}
lineThrough={error.status === RESOLVED}
/>
<div className="flex flex-col">
<div
className="flex items-center color-gray-dark font-semibold"
style={{ wordBreak: 'break-all' }}
>
{error.message}
return (
<div
className={cn(
className,
'bg-white border-radius-3 thin-gray-border mb-6'
)}
>
<div className="m-4">
<ErrorName
className="text-lg leading-relaxed"
name={error.name}
message={error.stack0InfoString}
lineThrough={error.status === RESOLVED}
/>
<div className="flex flex-col">
<div
className="flex items-center color-gray-dark font-semibold"
style={{ wordBreak: 'break-all' }}
>
{error.message}
</div>
<div className="flex items-center mt-2">
<div className="flex">
<Label
topValue={error.sessions}
horizontal
topValueSize="text-lg"
bottomValue="Sessions"
/>
<Label
topValue={error.users}
horizontal
topValueSize="text-lg"
bottomValue="Users"
/>
</div>
<div className="flex items-center mt-2">
<div className="flex">
<Label
topValue={error.sessions}
horizontal
topValueSize="text-lg"
bottomValue="Sessions"
/>
<Label
topValue={error.users}
horizontal
topValueSize="text-lg"
bottomValue="Users"
/>
</div>
<div className="text-xs color-gray-medium">Over the past 30 days</div>
<div className="text-xs color-gray-medium">
Over the past 30 days
</div>
</div>
</div>
<Divider />
<div className="m-4">
<div className="flex items-center">
<h3 className="text-xl inline-block mr-2">Last session with this error</h3>
<span className="font-thin text-sm">{resentOrDate(error.lastOccurrence)}</span>
<Button className="ml-auto" variant="text-primary" onClick={this.findSessions}>
Find all sessions with this error
<Icon className="ml-1" name="next1" color="teal" />
</Button>
</div>
<SessionBar className="my-4" session={error.lastHydratedSession} />
{error.customTags.length > 0 ? (
<div className="flex items-start flex-col">
<div>
<span className="font-semibold">More Info</span> <span className="text-disabled-text">(most recent call)</span>
</div>
<div className="mt-4 flex items-center gap-3 w-full flex-wrap">
{error.customTags.map((tag) => (
<div className="flex items-center rounded overflow-hidden bg-gray-lightest">
<div className="bg-gray-light-shade py-1 px-2 text-disabled-text">{Object.entries(tag)[0][0]}</div> <div className="py-1 px-2 text-gray-dark">{Object.entries(tag)[0][1]}</div>
</div>
))}
</div>
</div>
) : null}
</div>
<Divider />
<div className="m-4">
<Loader loading={traceLoading}>
<ErrorDetails
name={error.name}
message={error.message}
errorStack={trace}
error={error}
sourcemapUploaded={sourcemapUploaded}
/>
</Loader>
</div>
</div>
);
}
<Divider />
<div className="m-4">
<div className="flex items-center">
<h3 className="text-xl inline-block mr-2">
Last session with this error
</h3>
<span className="font-thin text-sm">
{resentOrDate(error.lastOccurrence)}
</span>
<Button
className="ml-auto"
variant="text-primary"
onClick={findSessions}
>
Find all sessions with this error
<Icon className="ml-1" name="next1" color="teal" />
</Button>
</div>
<SessionBar className="my-4" session={error.lastHydratedSession} />
{error.customTags.length > 0 ? (
<div className="flex items-start flex-col">
<div>
<span className="font-semibold">More Info</span>{' '}
<span className="text-disabled-text">(most recent call)</span>
</div>
<div className="mt-4 flex items-center gap-3 w-full flex-wrap">
{error.customTags.map((tag) => (
<div className="flex items-center rounded overflow-hidden bg-gray-lightest">
<div className="bg-gray-light-shade py-1 px-2 text-disabled-text">
{Object.entries(tag)[0][0]}
</div>
{' '}
<div className="py-1 px-2 text-gray-dark">
{Object.entries(tag)[0][1]}
</div>
</div>
))}
</div>
</div>
) : null}
</div>
<Divider />
<div className="m-4">
<Loader loading={loading}>
<ErrorDetails
name={error.name}
message={error.message}
errorStack={trace}
error={error}
sourcemapUploaded={sourcemapUploaded}
/>
</Loader>
</div>
</div>
);
}
export default withRouter(
(observer(MainSection))
);

View file

@ -1,123 +1,120 @@
import React from 'react';
import { connect } from 'react-redux';
import cn from 'classnames';
import withRequest from 'HOCs/withRequest';
import { Loader } from 'UI';
import { observer } from 'mobx-react-lite';
import React from 'react';
import { countries } from 'App/constants';
import Trend from './Trend';
import { useStore } from 'App/mstore';
import { Loader } from 'UI';
import DateAgo from './DateAgo';
import DistributionBar from './DistributionBar';
import Trend from './Trend';
import { errorService } from 'App/services';
const MAX_PERCENTAGE = 3;
const MIN_COUNT = 4;
const MAX_COUNT = 10;
function hidePredicate(percentage, index) {
if (index < MIN_COUNT) return false;
if (index < MAX_COUNT && percentage < MAX_PERCENTAGE) return false;
return true;
if (index < MIN_COUNT) return false;
if (index < MAX_COUNT && percentage < MAX_PERCENTAGE) return false;
return true;
}
function partitionsWrapper(partitions = [], mapCountry = false) {
const counts = partitions.map(({ count }) => count);
const sum = counts.reduce((a,b)=>parseInt(a)+parseInt(b),0);
const sum = counts.reduce((a, b) => parseInt(a) + parseInt(b), 0);
if (sum === 0) {
return [];
return [];
}
const otherPrcs = counts
.map(c => c/sum * 100)
.filter(hidePredicate);
const otherPrcsSum = otherPrcs.reduce((a,b)=>a+b,0);
const showLength = partitions.length - otherPrcs.length;
const show = partitions
.sort((a, b) => b.count - a.count)
.slice(0, showLength)
.map(p => ({
label: mapCountry
? (countries[p.name] || "Unknown")
: p.name,
prc: p.count/sum * 100,
}))
const otherPrcs = counts.map((c) => (c / sum) * 100).filter(hidePredicate);
const otherPrcsSum = otherPrcs.reduce((a, b) => a + b, 0);
const showLength = partitions.length - otherPrcs.length;
const show = partitions
.sort((a, b) => b.count - a.count)
.slice(0, showLength)
.map((p) => ({
label: mapCountry ? countries[p.name] || 'Unknown' : p.name,
prc: (p.count / sum) * 100,
}));
if (otherPrcsSum > 0) {
show.push({
label: "Other",
label: 'Other',
prc: otherPrcsSum,
other: true,
})
});
}
return show;
}
function tagsWrapper(tags = []) {
return tags.map(({ name, partitions }) => ({
name,
partitions: partitionsWrapper(partitions, name === "country")
}))
return tags.map(({ name, partitions }) => ({
name,
partitions: partitionsWrapper(partitions, name === 'country'),
}));
}
function dataWrapper(data = {}) {
return {
chart24: data.chart24 || [],
chart30: data.chart30 || [],
tags: tagsWrapper(data.tags),
};
return {
chart24: data.chart24 || [],
chart30: data.chart30 || [],
tags: tagsWrapper(data.tags),
};
}
@connect(state => ({
error: state.getIn([ "errors", "instance" ])
}))
@withRequest({
initialData: props => dataWrapper(props.error),
endpoint: props => `/errors/${ props.error.errorId }/stats`,
dataWrapper,
})
export default class SideSection extends React.PureComponent {
onDateChange = ({ startDate, endDate }) => {
this.props.request({ startDate, endDate });
}
function SideSection(props) {
const [data, setData] = React.useState({
chart24: [],
chart30: [],
tags: [],
});
const [loading, setLoading] = React.useState(false);
const { className } = props;
const { errorStore } = useStore();
const error = errorStore.instance;
render() {
const {
className,
error,
data,
loading,
} = this.props;
return (
<div className={ cn(className, "pl-5") }>
<h3 className="text-xl mb-2">Overview</h3>
<Trend
chart={ data.chart24 }
title="Past 24 hours"
/>
<div className="mb-6" />
<Trend
chart={ data.chart30 }
title="Last 30 days"
timeFormat={'l'}
/>
<div className="mb-6" />
<DateAgo
className="my-4"
title="First Seen"
timestamp={ error.firstOccurrence }
/>
<DateAgo
className="my-4"
title="Last Seen"
timestamp={ error.lastOccurrence }
/>
{ data.tags.length > 0 && <h4 className="text-xl mt-6 mb-3">Summary</h4> }
<Loader loading={loading}>
{ data.tags.map(({ name, partitions }) =>
<DistributionBar
key={ name }
title={name}
partitions={partitions}
className="mb-6"
/>
)}
</Loader>
</div>
);
}
const grabData = async () => {
setLoading(true);
errorService.fetchErrorStats(error.errorId)
.then(data => {
setData(dataWrapper(data))
})
.finally(() => setLoading(false));
}
React.useEffect(() => {
setData(dataWrapper(error))
}, [error.errorId])
return (
<div className={cn(className, 'pl-5')}>
<h3 className="text-xl mb-2">Overview</h3>
<Trend chart={data.chart24} title="Past 24 hours" />
<div className="mb-6" />
<Trend chart={data.chart30} title="Last 30 days" timeFormat={'l'} />
<div className="mb-6" />
<DateAgo
className="my-4"
title="First Seen"
timestamp={error.firstOccurrence}
/>
<DateAgo
className="my-4"
title="Last Seen"
timestamp={error.lastOccurrence}
/>
{data.tags.length > 0 && <h4 className="text-xl mt-6 mb-3">Summary</h4>}
<Loader loading={loading}>
{data.tags.map(({ name, partitions }) => (
<DistributionBar
key={name}
title={name}
partitions={partitions}
className="mb-6"
/>
))}
</Loader>
</div>
);
}
export default observer(SideSection);

View file

@ -1,154 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import withSiteIdRouter from 'HOCs/withSiteIdRouter';
import withPermissions from 'HOCs/withPermissions'
import { UNRESOLVED, RESOLVED, IGNORED, BOOKMARK } from "Types/errorInfo";
import { fetchBookmarks, editOptions } from "Duck/errors";
import { applyFilter } from 'Duck/search';
import { errors as errorsRoute, isRoute } from "App/routes";
import withPageTitle from 'HOCs/withPageTitle';
import cn from 'classnames';
import SelectDateRange from 'Shared/SelectDateRange';
import Period from 'Types/app/period';
import List from './List/List';
import ErrorInfo from './Error/ErrorInfo';
import Header from './Header';
import SideMenuSection from './SideMenu/SideMenuSection';
import SideMenuDividedItem from './SideMenu/SideMenuDividedItem';
const ERRORS_ROUTE = errorsRoute();
function getStatusLabel(status) {
switch(status) {
case UNRESOLVED:
return "Unresolved";
case RESOLVED:
return "Resolved";
case IGNORED:
return "Ignored";
default:
return "";
}
}
@withPermissions(['ERRORS'], 'page-margin container-90')
@withSiteIdRouter
@connect(state => ({
list: state.getIn([ "errors", "list" ]),
status: state.getIn([ "errors", "options", "status" ]),
filter: state.getIn([ 'search', 'instance' ]),
}), {
fetchBookmarks,
applyFilter,
editOptions,
})
@withPageTitle("Errors - OpenReplay")
export default class Errors extends React.PureComponent {
constructor(props) {
super(props)
this.state = {
filter: '',
}
}
ensureErrorsPage() {
const { history } = this.props;
if (!isRoute(ERRORS_ROUTE, history.location.pathname)) {
history.push(ERRORS_ROUTE);
}
}
onStatusItemClick = ({ key }) => {
this.props.editOptions({ status: key });
}
onBookmarksClick = () => {
this.props.editOptions({ status: BOOKMARK });
}
onDateChange = (e) => {
const dateValues = e.toJSON();
this.props.applyFilter(dateValues);
};
render() {
const {
count,
match: {
params: { errorId }
},
status,
list,
history,
filter,
} = this.props;
const { startDate, endDate, rangeValue } = filter;
const period = new Period({ start: startDate, end: endDate, rangeName: rangeValue });
return (
<div className="page-margin container-90" >
<div className={cn("side-menu", {'disabled' : !isRoute(ERRORS_ROUTE, history.location.pathname)})}>
<SideMenuSection
title="Errors"
onItemClick={this.onStatusItemClick}
items={[
{
key: UNRESOLVED,
icon: "exclamation-circle",
label: getStatusLabel(UNRESOLVED),
active: status === UNRESOLVED,
},
{
key: RESOLVED,
icon: "check",
label: getStatusLabel(RESOLVED),
active: status === RESOLVED,
},
{
key: IGNORED,
icon: "ban",
label: getStatusLabel(IGNORED),
active: status === IGNORED,
}
]}
/>
<SideMenuDividedItem
className="mt-3 mb-4"
iconName="star"
title="Bookmarks"
active={ status === BOOKMARK }
onClick={ this.onBookmarksClick }
/>
</div>
<div className="side-menu-margined">
{ errorId == null ?
<>
<div className="mb-5 flex items-baseline">
<Header
text={ status === BOOKMARK ? "Bookmarks" : getStatusLabel(status) }
count={ list.size }
/>
<div className="ml-3 flex items-center">
<span className="mr-2 color-gray-medium">Seen in</span>
<SelectDateRange
period={period}
onChange={this.onDateChange}
/>
</div>
</div>
<List
status={ status }
list={ list }
/>
</>
:
<ErrorInfo errorId={ errorId } list={ list } />
}
</div>
</div>
);
}
}

View file

@ -1,14 +0,0 @@
import React from 'react';
function Header({ text, count }) {
return (
<h3 className="text-2xl capitalize">
<span>{ text }</span>
{ count != null && <span className="ml-2 font-normal color-gray-medium">{ count }</span> }
</h3>
);
}
Header.displayName = "Header";
export default Header;

View file

@ -1,259 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import { Set } from "immutable";
import { NoContent, Loader, Checkbox, IconButton, Input, Pagination } from 'UI';
import { merge, resolve, unresolve, ignore, updateCurrentPage, editOptions } from "Duck/errors";
import { applyFilter } from 'Duck/filters';
import { IGNORED, UNRESOLVED } from 'Types/errorInfo';
import Divider from 'Components/Errors/ui/Divider';
import ListItem from './ListItem/ListItem';
import { debounce } from 'App/utils';
import Select from 'Shared/Select';
import EmptyStateSvg from '../../../svg/no-results.svg';
const sortOptionsMap = {
'occurrence-desc': 'Last Occurrence',
'occurrence-desc': 'First Occurrence',
'sessions-asc': 'Sessions Ascending',
'sessions-desc': 'Sessions Descending',
'users-asc': 'Users Ascending',
'users-desc': 'Users Descending',
};
const sortOptions = Object.entries(sortOptionsMap)
.map(([ value, label ]) => ({ value, label }));
@connect(state => ({
loading: state.getIn([ "errors", "loading" ]),
resolveToggleLoading: state.getIn(["errors", "resolve", "loading"]) ||
state.getIn(["errors", "unresolve", "loading"]),
ignoreLoading: state.getIn([ "errors", "ignore", "loading" ]),
mergeLoading: state.getIn([ "errors", "merge", "loading" ]),
currentPage: state.getIn(["errors", "currentPage"]),
limit: state.getIn(["errors", "limit"]),
total: state.getIn([ 'errors', 'totalCount' ]),
sort: state.getIn([ 'errors', 'options', 'sort' ]),
order: state.getIn([ 'errors', 'options', 'order' ]),
query: state.getIn([ "errors", "options", "query" ]),
}), {
merge,
resolve,
unresolve,
ignore,
applyFilter,
updateCurrentPage,
editOptions,
})
export default class List extends React.PureComponent {
constructor(props) {
super(props)
this.state = {
checkedAll: false,
checkedIds: Set(),
query: props.query,
}
this.debounceFetch = debounce(this.props.editOptions, 1000);
}
componentDidMount() {
this.props.applyFilter({ });
}
check = ({ errorId }) => {
const { checkedIds } = this.state;
const newCheckedIds = checkedIds.contains(errorId)
? checkedIds.remove(errorId)
: checkedIds.add(errorId);
this.setState({
checkedAll: newCheckedIds.size === this.props.list.size,
checkedIds: newCheckedIds
});
}
checkAll = () => {
if (this.state.checkedAll) {
this.setState({
checkedAll: false,
checkedIds: Set(),
});
} else {
this.setState({
checkedAll: true,
checkedIds: this.props.list.map(({ errorId }) => errorId).toSet(),
});
}
}
resetChecked = () => {
this.setState({
checkedAll: false,
checkedIds: Set(),
});
}
currentCheckedIds() {
return this.state.checkedIds
.intersect(this.props.list.map(({ errorId }) => errorId).toSet());
}
merge = () => {
this.props.merge(currentCheckedIds().toJS()).then(this.resetChecked);
}
applyToAllChecked(f) {
return Promise.all(this.currentCheckedIds().map(f).toJS()).then(this.resetChecked);
}
resolve = () => {
this.applyToAllChecked(this.props.resolve);
}
unresolve = () => {
this.applyToAllChecked(this.props.unresolve);
}
ignore = () => {
this.applyToAllChecked(this.props.ignore);
}
addPage = () => this.props.updateCurrentPage(this.props.currentPage + 1)
writeOption = ({ name, value }) => {
const [ sort, order ] = value.split('-');
if (name === 'sort') {
this.props.editOptions({ sort, order });
}
}
// onQueryChange = ({ target: { value, name } }) => props.edit({ [ name ]: value })
onQueryChange = ({ target: { value, name } }) => {
this.setState({ query: value });
this.debounceFetch({ query: value });
}
render() {
const {
list,
status,
loading,
ignoreLoading,
resolveToggleLoading,
mergeLoading,
currentPage,
total,
sort,
order,
limit,
} = this.props;
const {
checkedAll,
checkedIds,
query,
} = this.state;
const someLoading = loading || ignoreLoading || resolveToggleLoading || mergeLoading;
const currentCheckedIds = this.currentCheckedIds();
return (
<div className="bg-white p-5 border-radius-3 thin-gray-border">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center" style={{ height: "36px" }}>
<Checkbox
className="mr-3"
checked={ checkedAll }
onChange={ this.checkAll }
/>
{ status === UNRESOLVED
? <IconButton
outline
className="mr-3"
label="Resolve"
icon="check"
size="small"
loading={ resolveToggleLoading }
onClick={ this.resolve }
disabled={ someLoading || currentCheckedIds.size === 0}
/>
: <IconButton
outline
className="mr-3"
label="Unresolve"
icon="exclamation-circle"
size="small"
loading={ resolveToggleLoading }
onClick={ this.unresolve }
disabled={ someLoading || currentCheckedIds.size === 0}
/>
}
{ status !== IGNORED &&
<IconButton
outline
className="mr-3"
label="Ignore"
icon="ban"
size="small"
loading={ ignoreLoading }
onClick={ this.ignore }
disabled={ someLoading || currentCheckedIds.size === 0}
/>
}
</div>
<div className="flex items-center ml-6">
<span className="mr-2 color-gray-medium">Sort By</span>
<Select
defaultValue={ `${sort}-${order}` }
name="sort"
plain
options={ sortOptions }
onChange={ this.writeOption }
/>
<Input
style={{ width: '350px'}}
wrapperClassName="ml-3"
placeholder="Filter by name or message"
icon="search"
name="filter"
onChange={ this.onQueryChange }
value={query}
/>
</div>
</div>
<Divider />
<NoContent
title={
<div className="flex flex-col items-center justify-center">
<object style={{ width: "180px"}} type="image/svg+xml" data={EmptyStateSvg} />
<span className="mr-2">No Errors Found!</span>
</div>
}
subtext="Please try to change your search parameters."
// animatedIcon="empty-state"
show={ !loading && list.size === 0}
>
<Loader loading={ loading }>
{ list.map(e =>
<div key={e.errorId} style={{ opacity: e.disabled ? 0.5 : 1}}>
<ListItem
disabled={someLoading || e.disabled}
key={e.errorId}
error={e}
checked={ checkedIds.contains(e.errorId) }
onCheck={ this.check }
/>
<Divider/>
</div>
)}
<div className="w-full flex items-center justify-center mt-4">
<Pagination
page={currentPage}
total={total}
onPageChange={(page) => this.props.updateCurrentPage(page)}
limit={limit}
debounceRequest={500}
/>
</div>
</Loader>
</NoContent>
</div>
);
}
}

View file

@ -1,87 +0,0 @@
import React from 'react';
import { BarChart, Bar, YAxis, Tooltip, XAxis } from 'recharts';
import cn from 'classnames';
import { DateTime } from 'luxon'
import { diffFromNowString } from 'App/date';
import { error as errorRoute } from 'App/routes';
import { IGNORED, RESOLVED } from 'Types/errorInfo';
import { Checkbox, Link } from 'UI';
import ErrorName from 'Components/Errors/ui/ErrorName';
import Label from 'Components/Errors/ui/Label';
import stl from './listItem.module.css';
import { Styles } from '../../../Dashboard/Widgets/common';
const CustomTooltip = ({ active, payload, label }) => {
if (active) {
const p = payload[0].payload;
const dateStr = p.timestamp ? DateTime.fromMillis(p.timestamp).toFormat('l') : ''
return (
<div className="rounded border bg-white p-2">
<p className="label text-sm color-gray-medium">{dateStr}</p>
<p className="text-sm">Sessions: {p.count}</p>
</div>
);
}
return null;
};
function ListItem({ className, onCheck, checked, error, disabled }) {
const getDateFormat = val => {
const d = new Date(val);
return (d.getMonth()+ 1) + '/' + d.getDate()
}
return (
<div className={ cn("flex justify-between cursor-pointer py-4", className) } id="error-item">
<Checkbox
disabled={disabled}
checked={ checked }
onChange={ () => onCheck(error) }
/>
<div className={ cn("ml-3 flex-1 leading-tight", stl.name) } >
<Link to={errorRoute(error.errorId)} >
<ErrorName
icon={error.status === IGNORED ? 'ban' : null }
lineThrough={error.status === RESOLVED}
name={ error.name }
message={ error.stack0InfoString }
bold={ !error.viewed }
/>
<div
className={ cn("truncate color-gray-medium", { "line-through" : error.status === RESOLVED}) }
>
{ error.message }
</div>
</Link>
</div>
<BarChart width={ 150 } height={ 40 } data={ error.chart }>
<XAxis hide dataKey="timestamp" />
<YAxis hide domain={[0, 'dataMax + 8']} />
<Tooltip {...Styles.tooltip} label="Sessions" content={<CustomTooltip />} />
<Bar name="Sessions" minPointSize={1} dataKey="count" fill="#A8E0DA" />
</BarChart>
<Label
className={stl.sessions}
topValue={ error.sessions }
bottomValue="Sessions"
/>
<Label
className={stl.users}
topValue={ error.users }
bottomValue="Users"
/>
<Label
className={stl.occurrence}
topValue={ `${diffFromNowString(error.lastOccurrence)} ago` }
bottomValue="Last Seen"
/>
</div>
);
}
ListItem.displayName = "ListItem";
export default ListItem;

View file

@ -1,16 +0,0 @@
.name {
min-width: 55%;
}
.sessions {
width: 6%;
}
.users {
width: 5%;
}
.occurrence {
width: 15%;
min-width: 152px;
}

View file

@ -1,20 +0,0 @@
import React from 'react';
import { SideMenuitem } from "UI";
import Divider from 'Components/Errors/ui/Divider';
function SideMenuDividedItem({ className, noTopDivider = false, noBottomDivider = false, ...props }) {
return (
<div className={className}>
{ !noTopDivider && <Divider /> }
<SideMenuitem
className="my-3"
{ ...props }
/>
{ !noBottomDivider && <Divider /> }
</div>
);
}
SideMenuDividedItem.displayName = "SideMenuDividedItem";
export default SideMenuDividedItem;

View file

@ -1,14 +0,0 @@
import React from 'react';
import cn from 'classnames';
import stl from './sideMenuHeader.module.css';
function SideMenuHeader({ text, className }) {
return (
<div className={ cn(className, stl.label, "uppercase color-gray") }>
{ text }
</div>
)
}
SideMenuHeader.displayName = "SideMenuHeader";
export default SideMenuHeader;

View file

@ -1,24 +0,0 @@
import React from 'react';
import { SideMenuitem } from 'UI';
import SideMenuHeader from './SideMenuHeader';
function SideMenuSection({ title, items, onItemClick }) {
return (
<>
<SideMenuHeader className="mb-4" text={ title }/>
{ items.map(item =>
<SideMenuitem
key={ item.key }
active={ item.active }
title={ item.label }
iconName={ item.icon }
onClick={() => onItemClick(item)}
/>
)}
</>
);
}
SideMenuSection.displayName = "SideMenuSection";
export default SideMenuSection;

View file

@ -1,4 +0,0 @@
.label {
letter-spacing: 0.2em;
color: gray;
}

View file

@ -2,7 +2,7 @@ import React from 'react';
import { observer } from 'mobx-react-lite';
import { Button } from 'UI';
import cn from 'classnames';
import FeatureFlag from 'MOBX/types/FeatureFlag';
import FeatureFlag from 'App/mstore/types/FeatureFlag';
function Description({
isDescrEditing,

View file

@ -1,8 +1,8 @@
import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
import ReCAPTCHA from 'react-google-recaptcha';
import { Form, Input, Loader, Button, Icon, Message } from 'UI';
import { requestResetPassword, resetPassword, resetErrors } from 'Duck/user';
import stl from './forgotPassword.module.css';
import { validatePassword } from 'App/validate';
import { PASSWORD_POLICY } from 'App/constants';
@ -13,29 +13,26 @@ const CAPTCHA_ENABLED = window.env.CAPTCHA_ENABLED === 'true';
const CAPTCHA_SITE_KEY = window.env.CAPTCHA_SITE_KEY;
interface Props {
errors: any;
resetErrors: any;
loading: boolean;
params: any;
resetPassword: Function;
}
function CreatePassword(props: Props) {
const { loading, params } = props;
const { params } = props;
const { userStore } = useStore();
const loading = userStore.loading;
const resetPassword = userStore.resetPassword;
const [error, setError] = React.useState<String | null>(null);
const [validationError, setValidationError] = React.useState<String | null>(null);
const [updated, setUpdated] = React.useState(false);
const [requested, setRequested] = React.useState(false);
const [passwordRepeat, setPasswordRepeat] = React.useState('');
const [password, setPassword] = React.useState('');
const [doesntMatch, setDoesntMatch] = React.useState(false);
const pass = params.get('pass');
const invitation = params.get('invitation');
const handleSubmit = (token?: any) => {
const handleSubmit = () => {
if (!validatePassword(password)) {
return;
}
props.resetPassword({ invitation, pass, password }).then((response: any) => {
resetPassword({ invitation, pass, password }).then((response: any) => {
if (response && response.errors && response.errors.length > 0) {
setError(response.errors[0]);
} else {
@ -84,7 +81,6 @@ function CreatePassword(props: Props) {
<ReCAPTCHA
ref={recaptchaRef}
size="invisible"
data-hidden={requested}
sitekey={CAPTCHA_SITE_KEY}
onChange={(token: any) => handleSubmit(token)}
/>
@ -150,17 +146,4 @@ function CreatePassword(props: Props) {
);
}
export default connect(
(state: any) => ({
errors: state.getIn(['user', 'requestResetPassowrd', 'errors']),
resetErrors: state.getIn(['user', 'resetPassword', 'errors']),
loading:
state.getIn(['user', 'requestResetPassowrd', 'loading']) ||
state.getIn(['user', 'resetPassword', 'loading']),
}),
{
requestResetPassword,
resetPassword,
resetErrors,
}
)(CreatePassword);
export default observer(CreatePassword);

View file

@ -1,19 +1,15 @@
import Copyright from 'Shared/Copyright';
import React from 'react';
import { Form, Input, Loader, Link, Icon, Message } from 'UI';
import { Link } from 'UI';
import {Button} from 'antd';
import { login as loginRoute } from 'App/routes';
import { connect } from 'react-redux';
import ResetPassword from './ResetPasswordRequest';
import CreatePassword from './CreatePassword';
const LOGIN = loginRoute();
interface Props {
params: any;
}
function ForgotPassword(props: Props) {
const { params } = props;
function ForgotPassword(props) {
const params = new URLSearchParams(props.location.search);
const pass = params.get('pass');
const invitation = params.get('invitation');
const creatingNewPassword = pass && invitation;
@ -54,6 +50,4 @@ function ForgotPassword(props: Props) {
);
}
export default connect((state: any, props: any) => ({
params: new URLSearchParams(props.location.search),
}))(ForgotPassword);
export default ForgotPassword;

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