applied eslint
This commit is contained in:
parent
114bd4080b
commit
b822b1c067
2008 changed files with 40866 additions and 33681 deletions
|
|
@ -5,13 +5,11 @@ interface Props {
|
|||
redirect: string;
|
||||
}
|
||||
|
||||
const AdditionalRoutes = (props: Props) => {
|
||||
function AdditionalRoutes(props: Props) {
|
||||
const { redirect } = props;
|
||||
return (
|
||||
<>
|
||||
<Redirect to={redirect} />
|
||||
</>
|
||||
<Redirect to={redirect} />
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default AdditionalRoutes;
|
||||
|
|
|
|||
|
|
@ -3,31 +3,29 @@ import { Switch, Route } from 'react-router-dom';
|
|||
import { Loader } from 'UI';
|
||||
import withSiteIdUpdater from 'HOCs/withSiteIdUpdater';
|
||||
|
||||
import * as routes from './routes';
|
||||
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';
|
||||
import * as routes from './routes';
|
||||
|
||||
const components: any = {
|
||||
SessionPure: lazy(() => import('Components/Session/Session')),
|
||||
LiveSessionPure: lazy(() => import('Components/Session/LiveSession'))
|
||||
LiveSessionPure: lazy(() => import('Components/Session/LiveSession')),
|
||||
};
|
||||
|
||||
|
||||
const enhancedComponents: any = {
|
||||
Session: withSiteIdUpdater(components.SessionPure),
|
||||
LiveSession: withSiteIdUpdater(components.LiveSessionPure)
|
||||
LiveSession: withSiteIdUpdater(components.LiveSessionPure),
|
||||
};
|
||||
|
||||
const withSiteId = routes.withSiteId;
|
||||
const { withSiteId } = routes;
|
||||
|
||||
const SESSION_PATH = routes.session();
|
||||
const LIVE_SESSION_PATH = routes.liveSession();
|
||||
|
||||
|
||||
interface Props {
|
||||
isJwt?: boolean;
|
||||
isLoggedIn?: boolean;
|
||||
|
|
@ -43,15 +41,23 @@ function IFrameRoutes(props: Props) {
|
|||
if (isLoggedIn) {
|
||||
return (
|
||||
<ModalProvider>
|
||||
<Layout hideHeader={true}>
|
||||
<Loader loading={!!loading} className='flex-1'>
|
||||
<Suspense fallback={<Loader loading={true} className='flex-1' />}>
|
||||
<Switch key='content'>
|
||||
<Route exact strict path={withSiteId(SESSION_PATH, siteIdList)}
|
||||
component={enhancedComponents.Session} />
|
||||
<Route exact strict path={withSiteId(LIVE_SESSION_PATH, siteIdList)}
|
||||
component={enhancedComponents.LiveSession} />
|
||||
<Route path='*' render={NotFoundPage} />
|
||||
<Layout hideHeader>
|
||||
<Loader loading={!!loading} className="flex-1">
|
||||
<Suspense fallback={<Loader loading className="flex-1" />}>
|
||||
<Switch key="content">
|
||||
<Route
|
||||
exact
|
||||
strict
|
||||
path={withSiteId(SESSION_PATH, siteIdList)}
|
||||
component={enhancedComponents.Session}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
strict
|
||||
path={withSiteId(LIVE_SESSION_PATH, siteIdList)}
|
||||
component={enhancedComponents.LiveSession}
|
||||
/>
|
||||
<Route path="*" render={NotFoundPage} />
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</Loader>
|
||||
|
|
@ -67,5 +73,4 @@ function IFrameRoutes(props: Props) {
|
|||
return <PublicRoutes />;
|
||||
}
|
||||
|
||||
|
||||
export default observer(IFrameRoutes);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import withSiteIdUpdater from 'HOCs/withSiteIdUpdater';
|
||||
import React, { Suspense, lazy } from 'react';
|
||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useStore } from "./mstore";
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { GLOBAL_HAS_NO_RECORDINGS } from 'App/constants/storageKeys';
|
||||
import { OB_DEFAULT_TAB } from 'App/routes';
|
||||
import { Loader } from 'UI';
|
||||
import { useStore } from './mstore';
|
||||
|
||||
import APIClient from './api_client';
|
||||
import * as routes from './routes';
|
||||
|
|
@ -20,13 +20,13 @@ const components: any = {
|
|||
DashboardPure: lazy(() => import('Components/Dashboard/NewDashboard')),
|
||||
MultiviewPure: lazy(() => import('Components/Session_/Multiview/Multiview')),
|
||||
UsabilityTestingPure: lazy(
|
||||
() => import('Components/UsabilityTesting/UsabilityTesting')
|
||||
() => import('Components/UsabilityTesting/UsabilityTesting'),
|
||||
),
|
||||
UsabilityTestEditPure: lazy(
|
||||
() => import('Components/UsabilityTesting/TestEdit')
|
||||
() => import('Components/UsabilityTesting/TestEdit'),
|
||||
),
|
||||
UsabilityTestOverviewPure: lazy(
|
||||
() => import('Components/UsabilityTesting/TestOverview')
|
||||
() => import('Components/UsabilityTesting/TestOverview'),
|
||||
),
|
||||
SpotsListPure: lazy(() => import('Components/Spots/SpotsList')),
|
||||
SpotPure: lazy(() => import('Components/Spots/SpotPlayer')),
|
||||
|
|
@ -46,7 +46,7 @@ const enhancedComponents: any = {
|
|||
UsabilityTesting: withSiteIdUpdater(components.UsabilityTestingPure),
|
||||
UsabilityTestEdit: withSiteIdUpdater(components.UsabilityTestEditPure),
|
||||
UsabilityTestOverview: withSiteIdUpdater(
|
||||
components.UsabilityTestOverviewPure
|
||||
components.UsabilityTestOverviewPure,
|
||||
),
|
||||
SpotsList: withSiteIdUpdater(components.SpotsListPure),
|
||||
Spot: components.SpotPure,
|
||||
|
|
@ -54,7 +54,7 @@ const enhancedComponents: any = {
|
|||
Highlights: withSiteIdUpdater(components.HighlightsPure),
|
||||
};
|
||||
|
||||
const withSiteId = routes.withSiteId;
|
||||
const { withSiteId } = routes;
|
||||
|
||||
const METRICS_PATH = routes.metrics();
|
||||
const METRICS_DETAILS = routes.metricDetails();
|
||||
|
|
@ -100,25 +100,24 @@ const HIGHLIGHTS_PATH = routes.highlights();
|
|||
|
||||
function PrivateRoutes() {
|
||||
const { projectsStore, userStore, integrationsStore } = useStore();
|
||||
const onboarding = userStore.onboarding;
|
||||
const { onboarding } = userStore;
|
||||
const scope = userStore.scopeState;
|
||||
const tenantId = userStore.account.tenantId;
|
||||
const { tenantId } = userStore.account;
|
||||
const sites = projectsStore.list;
|
||||
const siteId = projectsStore.siteId;
|
||||
const hasRecordings = sites.some(s => s.recorded);
|
||||
const { siteId } = projectsStore;
|
||||
const hasRecordings = sites.some((s) => s.recorded);
|
||||
const redirectToSetup = scope === 0;
|
||||
const redirectToOnboarding =
|
||||
!onboarding && (localStorage.getItem(GLOBAL_HAS_NO_RECORDINGS) === 'true' || (sites.length > 0 && !hasRecordings)) && scope > 0;
|
||||
const redirectToOnboarding = !onboarding && (localStorage.getItem(GLOBAL_HAS_NO_RECORDINGS) === 'true' || (sites.length > 0 && !hasRecordings)) && scope > 0;
|
||||
const siteIdList: any = sites.map(({ id }) => id);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (siteId && integrationsStore.integrations.siteId !== siteId) {
|
||||
integrationsStore.integrations.setSiteId(siteId)
|
||||
integrationsStore.integrations.setSiteId(siteId);
|
||||
void integrationsStore.integrations.fetchIntegrations(siteId);
|
||||
}
|
||||
}, [siteId])
|
||||
}, [siteId]);
|
||||
return (
|
||||
<Suspense fallback={<Loader loading={true} className="flex-1" />}>
|
||||
<Suspense fallback={<Loader loading className="flex-1" />}>
|
||||
<Switch key="content">
|
||||
<Route
|
||||
exact
|
||||
|
|
@ -264,7 +263,7 @@ function PrivateRoutes() {
|
|||
{Object.entries(routes.redirects).map(([fr, to]) => (
|
||||
<Redirect key={fr} exact strict from={fr} to={to} />
|
||||
))}
|
||||
<Route path={"*"}>
|
||||
<Route path="*">
|
||||
<Redirect to={withSiteId(routes.sessions(), siteId)} />
|
||||
</Route>
|
||||
</Switch>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { useStore } from 'App/mstore';
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import * as routes from 'App/routes';
|
||||
|
||||
|
||||
const LOGIN_PATH = routes.login();
|
||||
const SIGNUP_PATH = routes.signup();
|
||||
const FORGOT_PASSWORD = routes.forgotPassword();
|
||||
|
|
@ -19,8 +18,8 @@ const Spot = lazy(() => import('Components/Spots/SpotPlayer/SpotPlayer'));
|
|||
|
||||
function PublicRoutes() {
|
||||
const { userStore } = useStore();
|
||||
const authDetails = userStore.authStore.authDetails;
|
||||
const isEnterprise = userStore.isEnterprise;
|
||||
const { authDetails } = userStore.authStore;
|
||||
const { isEnterprise } = userStore;
|
||||
const hideSupport = isEnterprise || location.pathname.includes('spots') || location.pathname.includes('view-spot');
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
|
||||
|
|
@ -34,7 +33,7 @@ function PublicRoutes() {
|
|||
|
||||
return (
|
||||
<Loader loading={loading} className="flex-1">
|
||||
<Suspense fallback={<Loader loading={true} className="flex-1" />}>
|
||||
<Suspense fallback={<Loader loading className="flex-1" />}>
|
||||
<Switch>
|
||||
<Route exact strict path={SPOT_PATH} component={Spot} />
|
||||
<Route exact strict path={FORGOT_PASSWORD} component={ForgotPassword} />
|
||||
|
|
@ -48,5 +47,4 @@ function PublicRoutes() {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
export default observer(PublicRoutes);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
GLOBAL_DESTINATION_PATH,
|
||||
IFRAME,
|
||||
JWT_PARAM,
|
||||
SPOT_ONBOARDING
|
||||
SPOT_ONBOARDING,
|
||||
} from 'App/constants/storageKeys';
|
||||
import Layout from 'App/layout/Layout';
|
||||
import { useStore } from 'App/mstore';
|
||||
|
|
@ -16,8 +16,8 @@ import { checkParam, handleSpotJWT, isTokenExpired } from 'App/utils';
|
|||
import { ModalProvider } from 'Components/Modal';
|
||||
import { ModalProvider as NewModalProvider } from 'Components/ModalContext';
|
||||
import { Loader } from 'UI';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import * as routes from './routes';
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
interface RouterProps extends RouteComponentProps {
|
||||
match: {
|
||||
|
|
@ -33,20 +33,22 @@ const Router: React.FC<RouterProps> = (props) => {
|
|||
history,
|
||||
} = props;
|
||||
const mstore = useStore();
|
||||
const { customFieldStore, projectsStore, sessionStore, searchStore, userStore } = mstore;
|
||||
const jwt = userStore.jwt;
|
||||
const changePassword = userStore.account.changePassword;
|
||||
const {
|
||||
customFieldStore, projectsStore, sessionStore, searchStore, userStore,
|
||||
} = mstore;
|
||||
const { jwt } = userStore;
|
||||
const { changePassword } = userStore.account;
|
||||
const userInfoLoading = userStore.fetchInfoRequest.loading;
|
||||
const scopeSetup = userStore.scopeState === 0;
|
||||
const localSpotJwt = userStore.spotJwt;
|
||||
const isLoggedIn = Boolean(jwt && !changePassword);
|
||||
const fetchUserInfo = userStore.fetchUserInfo;
|
||||
const { fetchUserInfo } = userStore;
|
||||
const setJwt = userStore.updateJwt;
|
||||
const logout = userStore.logout;
|
||||
const { logout } = userStore;
|
||||
|
||||
const setSessionPath = sessionStore.setSessionPath;
|
||||
const siteId = projectsStore.siteId;
|
||||
const sitesLoading = projectsStore.sitesLoading;
|
||||
const { setSessionPath } = sessionStore;
|
||||
const { siteId } = projectsStore;
|
||||
const { sitesLoading } = projectsStore;
|
||||
const sites = projectsStore.list;
|
||||
const loading = Boolean(userInfoLoading || (!scopeSetup && !siteId) || sitesLoading);
|
||||
const initSite = projectsStore.initProject;
|
||||
|
|
@ -75,10 +77,10 @@ const Router: React.FC<RouterProps> = (props) => {
|
|||
const handleSpotLogin = (jwt: string) => {
|
||||
if (spotReqSent.current) {
|
||||
return;
|
||||
} else {
|
||||
spotReqSent.current = true;
|
||||
setIsSpotCb(false);
|
||||
}
|
||||
spotReqSent.current = true;
|
||||
setIsSpotCb(false);
|
||||
|
||||
handleSpotJWT(jwt);
|
||||
};
|
||||
|
||||
|
|
@ -86,7 +88,7 @@ const Router: React.FC<RouterProps> = (props) => {
|
|||
if (!isLoggedIn && location.pathname !== routes.login()) {
|
||||
localStorage.setItem(
|
||||
GLOBAL_DESTINATION_PATH,
|
||||
location.pathname + location.search
|
||||
location.pathname + location.search,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -106,10 +108,10 @@ const Router: React.FC<RouterProps> = (props) => {
|
|||
|
||||
const destinationPath = localStorage.getItem(GLOBAL_DESTINATION_PATH);
|
||||
if (
|
||||
destinationPath &&
|
||||
!destinationPath.includes(routes.login()) &&
|
||||
!destinationPath.includes(routes.signup()) &&
|
||||
destinationPath !== '/'
|
||||
destinationPath
|
||||
&& !destinationPath.includes(routes.login())
|
||||
&& !destinationPath.includes(routes.signup())
|
||||
&& destinationPath !== '/'
|
||||
) {
|
||||
const url = new URL(destinationPath, window.location.origin);
|
||||
checkParams(url.search);
|
||||
|
|
@ -143,7 +145,7 @@ const Router: React.FC<RouterProps> = (props) => {
|
|||
useEffect(() => {
|
||||
handleDestinationPath();
|
||||
|
||||
setSessionPath(previousLocation ? previousLocation : location);
|
||||
setSessionPath(previousLocation || location);
|
||||
}, [location]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -163,14 +165,14 @@ const Router: React.FC<RouterProps> = (props) => {
|
|||
}, [isSpotCb, isLoggedIn, localSpotJwt, isSignup]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn) return
|
||||
if (!isLoggedIn) return;
|
||||
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()
|
||||
await customFieldStore.fetchListActive(`${siteId}`);
|
||||
await searchStore.fetchSavedSearchList();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -190,13 +192,12 @@ const Router: React.FC<RouterProps> = (props) => {
|
|||
const prevIsLoggedIn = usePrevious(isLoggedIn);
|
||||
const previousLocation = usePrevious(location);
|
||||
|
||||
const hideHeader =
|
||||
(location.pathname && location.pathname.includes('/session/')) ||
|
||||
location.pathname.includes('/assist/') ||
|
||||
location.pathname.includes('multiview') ||
|
||||
location.pathname.includes('/view-spot/') ||
|
||||
location.pathname.includes('/spots/') ||
|
||||
location.pathname.includes('/scope-setup');
|
||||
const hideHeader = (location.pathname && location.pathname.includes('/session/'))
|
||||
|| location.pathname.includes('/assist/')
|
||||
|| location.pathname.includes('multiview')
|
||||
|| location.pathname.includes('/view-spot/')
|
||||
|| location.pathname.includes('/spots/')
|
||||
|| location.pathname.includes('/scope-setup');
|
||||
|
||||
if (isIframe) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ const siteIdRequiredPaths: string[] = [
|
|||
'/check-recording-status',
|
||||
'/usability-tests',
|
||||
'/tags',
|
||||
'/intelligent'
|
||||
'/intelligent',
|
||||
];
|
||||
|
||||
export const clean = (obj: any, forbiddenValues: any[] = [undefined, '']): any => {
|
||||
|
|
@ -38,7 +38,7 @@ export const clean = (obj: any, forbiddenValues: any[] = [undefined, '']): any =
|
|||
? new Array(obj.length).fill().map((_, i) => i)
|
||||
: Object.keys(obj);
|
||||
const retObj = Array.isArray(obj) ? [] : {};
|
||||
keys.map(key => {
|
||||
keys.map((key) => {
|
||||
const value = obj[key];
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
retObj[key] = clean(value);
|
||||
|
|
@ -52,18 +52,23 @@ export const clean = (obj: any, forbiddenValues: any[] = [undefined, '']): any =
|
|||
|
||||
export default class APIClient {
|
||||
private init: RequestInit;
|
||||
|
||||
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() {
|
||||
this.init = {
|
||||
headers: new Headers({
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -82,14 +87,14 @@ export default class APIClient {
|
|||
}
|
||||
|
||||
setSiteIdCheck(checker: () => { siteId: string | null }): void {
|
||||
this.siteIdCheck = checker
|
||||
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 = this.getJwt()
|
||||
const jwt = this.getJwt();
|
||||
const headers = new Headers({
|
||||
'Accept': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
|
||||
|
|
@ -148,7 +153,7 @@ export default class APIClient {
|
|||
params?: any,
|
||||
method: string = 'GET',
|
||||
options: { clean?: boolean } = { clean: true },
|
||||
headers?: Record<string, any>
|
||||
headers?: Record<string, any>,
|
||||
): Promise<Response> {
|
||||
let _path = path;
|
||||
let jwt = this.getJwt();
|
||||
|
|
@ -167,21 +172,21 @@ export default class APIClient {
|
|||
delete init.body;
|
||||
}
|
||||
|
||||
const noChalice = path.includes('v1/integrations') || path.includes('/spot') && !path.includes('/login')
|
||||
let edp = window.env.API_EDP || window.location.origin + '/api';
|
||||
const noChalice = path.includes('v1/integrations') || path.includes('/spot') && !path.includes('/login');
|
||||
let edp = window.env.API_EDP || `${window.location.origin}/api`;
|
||||
if (noChalice && !edp.includes('api.openreplay.com')) {
|
||||
edp = edp.replace('/api', '')
|
||||
edp = edp.replace('/api', '');
|
||||
}
|
||||
if (
|
||||
path !== '/targets_temp' &&
|
||||
!path.includes('/metadata/session_search') &&
|
||||
!path.includes('/assist/credentials') &&
|
||||
siteIdRequiredPaths.some((sidPath) => path.startsWith(sidPath))
|
||||
path !== '/targets_temp'
|
||||
&& !path.includes('/metadata/session_search')
|
||||
&& !path.includes('/assist/credentials')
|
||||
&& siteIdRequiredPaths.some((sidPath) => path.startsWith(sidPath))
|
||||
) {
|
||||
edp = `${edp}/${this.siteId ?? ''}`;
|
||||
}
|
||||
if (path.includes('PROJECT_ID')) {
|
||||
_path = _path.replace('PROJECT_ID', this.siteId + '');
|
||||
_path = _path.replace('PROJECT_ID', `${this.siteId}`);
|
||||
}
|
||||
|
||||
const fullUrl = edp + _path;
|
||||
|
|
@ -193,7 +198,7 @@ export default class APIClient {
|
|||
if (response.ok) {
|
||||
return response;
|
||||
}
|
||||
let errorMsg = `Something went wrong.`;
|
||||
let errorMsg = 'Something went wrong.';
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorMsg = errorData.errors?.[0] || errorMsg;
|
||||
|
|
@ -204,7 +209,7 @@ export default class APIClient {
|
|||
async refreshToken(): Promise<string> {
|
||||
try {
|
||||
const response = await this.fetch('/refresh', {
|
||||
headers: this.init.headers
|
||||
headers: this.init.headers,
|
||||
}, 'GET', { clean: false });
|
||||
|
||||
if (!response.ok) {
|
||||
|
|
@ -249,5 +254,5 @@ export default class APIClient {
|
|||
|
||||
forceSiteId = (siteId: string) => {
|
||||
this.siteId = siteId;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
168
frontend/app/assets/prism/prism-bash.min.js
vendored
168
frontend/app/assets/prism/prism-bash.min.js
vendored
File diff suppressed because one or more lines are too long
127
frontend/app/assets/prism/prism-javascript.min.js
vendored
127
frontend/app/assets/prism/prism-javascript.min.js
vendored
|
|
@ -1 +1,126 @@
|
|||
Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp("(^|[^\\w$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w$])"),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp("((?:^|[^$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)/(?:(?:\\[(?:[^\\]\\\\\r\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:$|[\r\n,.;:})\\]]|//))"),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),Prism.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.markup.tag.addAttribute("on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)","javascript")),Prism.languages.js=Prism.languages.javascript;
|
||||
(Prism.languages.javascript = Prism.languages.extend('clike', {
|
||||
'class-name': [
|
||||
Prism.languages.clike['class-name'],
|
||||
{
|
||||
pattern:
|
||||
/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,
|
||||
lookbehind: !0,
|
||||
},
|
||||
],
|
||||
keyword: [
|
||||
{ pattern: /((?:^|\})\s*)catch\b/, lookbehind: !0 },
|
||||
{
|
||||
pattern:
|
||||
/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,
|
||||
lookbehind: !0,
|
||||
},
|
||||
],
|
||||
function:
|
||||
/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,
|
||||
number: {
|
||||
pattern: RegExp(
|
||||
'(^|[^\\w$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w$])',
|
||||
),
|
||||
lookbehind: !0,
|
||||
},
|
||||
operator:
|
||||
/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/,
|
||||
})),
|
||||
(Prism.languages.javascript['class-name'][0].pattern = /(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/),
|
||||
Prism.languages.insertBefore('javascript', 'keyword', {
|
||||
regex: {
|
||||
pattern: RegExp(
|
||||
'((?:^|[^$\\w\\xA0-\\uFFFF."\'\\])\\s]|\\b(?:return|yield))\\s*)/(?:(?:\\[(?:[^\\]\\\\\r\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:$|[\r\n,.;:})\\]]|//))',
|
||||
),
|
||||
lookbehind: !0,
|
||||
greedy: !0,
|
||||
inside: {
|
||||
'regex-source': {
|
||||
pattern: /^(\/)[\s\S]+(?=\/[a-z]*$)/,
|
||||
lookbehind: !0,
|
||||
alias: 'language-regex',
|
||||
inside: Prism.languages.regex,
|
||||
},
|
||||
'regex-delimiter': /^\/|\/$/,
|
||||
'regex-flags': /^[a-z]+$/,
|
||||
},
|
||||
},
|
||||
'function-variable': {
|
||||
pattern:
|
||||
/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,
|
||||
alias: 'function',
|
||||
},
|
||||
parameter: [
|
||||
{
|
||||
pattern:
|
||||
/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,
|
||||
lookbehind: !0,
|
||||
inside: Prism.languages.javascript,
|
||||
},
|
||||
{
|
||||
pattern:
|
||||
/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,
|
||||
lookbehind: !0,
|
||||
inside: Prism.languages.javascript,
|
||||
},
|
||||
{
|
||||
pattern:
|
||||
/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,
|
||||
lookbehind: !0,
|
||||
inside: Prism.languages.javascript,
|
||||
},
|
||||
{
|
||||
pattern:
|
||||
/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,
|
||||
lookbehind: !0,
|
||||
inside: Prism.languages.javascript,
|
||||
},
|
||||
],
|
||||
constant: /\b[A-Z](?:[A-Z_]|\dx?)*\b/,
|
||||
}),
|
||||
Prism.languages.insertBefore('javascript', 'string', {
|
||||
hashbang: { pattern: /^#!.*/, greedy: !0, alias: 'comment' },
|
||||
'template-string': {
|
||||
pattern:
|
||||
/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,
|
||||
greedy: !0,
|
||||
inside: {
|
||||
'template-punctuation': { pattern: /^`|`$/, alias: 'string' },
|
||||
interpolation: {
|
||||
pattern:
|
||||
/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,
|
||||
lookbehind: !0,
|
||||
inside: {
|
||||
'interpolation-punctuation': {
|
||||
pattern: /^\$\{|\}$/,
|
||||
alias: 'punctuation',
|
||||
},
|
||||
rest: Prism.languages.javascript,
|
||||
},
|
||||
},
|
||||
string: /[\s\S]+/,
|
||||
},
|
||||
},
|
||||
'string-property': {
|
||||
pattern:
|
||||
/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,
|
||||
lookbehind: !0,
|
||||
greedy: !0,
|
||||
alias: 'property',
|
||||
},
|
||||
}),
|
||||
Prism.languages.insertBefore('javascript', 'operator', {
|
||||
'literal-property': {
|
||||
pattern:
|
||||
/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,
|
||||
lookbehind: !0,
|
||||
alias: 'property',
|
||||
},
|
||||
}),
|
||||
Prism.languages.markup
|
||||
&& (Prism.languages.markup.tag.addInlined('script', 'javascript'),
|
||||
Prism.languages.markup.tag.addAttribute(
|
||||
'on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)',
|
||||
'javascript',
|
||||
)),
|
||||
(Prism.languages.js = Prism.languages.javascript);
|
||||
|
|
|
|||
96
frontend/app/assets/prism/prism-jsx.min.js
vendored
96
frontend/app/assets/prism/prism-jsx.min.js
vendored
|
|
@ -1 +1,95 @@
|
|||
!function(t){var n=t.util.clone(t.languages.javascript),e="(?:\\{<S>*\\.{3}(?:[^{}]|<BRACES>)*\\})";function a(t,n){return t=t.replace(/<S>/g,(function(){return"(?:\\s|//.*(?!.)|/\\*(?:[^*]|\\*(?!/))\\*/)"})).replace(/<BRACES>/g,(function(){return"(?:\\{(?:\\{(?:\\{[^{}]*\\}|[^{}])*\\}|[^{}])*\\})"})).replace(/<SPREAD>/g,(function(){return e})),RegExp(t,n)}e=a(e).source,t.languages.jsx=t.languages.extend("markup",n),t.languages.jsx.tag.pattern=a("</?(?:[\\w.:-]+(?:<S>+(?:[\\w.:$-]+(?:=(?:\"(?:\\\\[^]|[^\\\\\"])*\"|'(?:\\\\[^]|[^\\\\'])*'|[^\\s{'\"/>=]+|<BRACES>))?|<SPREAD>))*<S>*/?)?>"),t.languages.jsx.tag.inside.tag.pattern=/^<\/?[^\s>\/]*/,t.languages.jsx.tag.inside["attr-value"].pattern=/=(?!\{)(?:"(?:\\[\s\S]|[^\\"])*"|'(?:\\[\s\S]|[^\\'])*'|[^\s'">]+)/,t.languages.jsx.tag.inside.tag.inside["class-name"]=/^[A-Z]\w*(?:\.[A-Z]\w*)*$/,t.languages.jsx.tag.inside.comment=n.comment,t.languages.insertBefore("inside","attr-name",{spread:{pattern:a("<SPREAD>"),inside:t.languages.jsx}},t.languages.jsx.tag),t.languages.insertBefore("inside","special-attr",{script:{pattern:a("=<BRACES>"),alias:"language-javascript",inside:{"script-punctuation":{pattern:/^=(?=\{)/,alias:"punctuation"},rest:t.languages.jsx}}},t.languages.jsx.tag);var s=function(t){return t?"string"==typeof t?t:"string"==typeof t.content?t.content:t.content.map(s).join(""):""},g=function(n){for(var e=[],a=0;a<n.length;a++){var o=n[a],i=!1;if("string"!=typeof o&&("tag"===o.type&&o.content[0]&&"tag"===o.content[0].type?"</"===o.content[0].content[0].content?e.length>0&&e[e.length-1].tagName===s(o.content[0].content[1])&&e.pop():"/>"===o.content[o.content.length-1].content||e.push({tagName:s(o.content[0].content[1]),openedBraces:0}):e.length>0&&"punctuation"===o.type&&"{"===o.content?e[e.length-1].openedBraces++:e.length>0&&e[e.length-1].openedBraces>0&&"punctuation"===o.type&&"}"===o.content?e[e.length-1].openedBraces--:i=!0),(i||"string"==typeof o)&&e.length>0&&0===e[e.length-1].openedBraces){var r=s(o);a<n.length-1&&("string"==typeof n[a+1]||"plain-text"===n[a+1].type)&&(r+=s(n[a+1]),n.splice(a+1,1)),a>0&&("string"==typeof n[a-1]||"plain-text"===n[a-1].type)&&(r=s(n[a-1])+r,n.splice(a-1,1),a--),n[a]=new t.Token("plain-text",r,null,r)}o.content&&"string"!=typeof o.content&&g(o.content)}};t.hooks.add("after-tokenize",(function(t){"jsx"!==t.language&&"tsx"!==t.language||g(t.tokens)}))}(Prism);
|
||||
!(function (t) {
|
||||
const n = t.util.clone(t.languages.javascript);
|
||||
let e = '(?:\\{<S>*\\.{3}(?:[^{}]|<BRACES>)*\\})';
|
||||
function a(t, n) {
|
||||
return (
|
||||
(t = t
|
||||
.replace(/<S>/g, () => '(?:\\s|//.*(?!.)|/\\*(?:[^*]|\\*(?!/))\\*/)')
|
||||
.replace(/<BRACES>/g, () => '(?:\\{(?:\\{(?:\\{[^{}]*\\}|[^{}])*\\}|[^{}])*\\})')
|
||||
.replace(/<SPREAD>/g, () => e)),
|
||||
RegExp(t, n)
|
||||
);
|
||||
}
|
||||
(e = a(e).source),
|
||||
(t.languages.jsx = t.languages.extend('markup', n)),
|
||||
(t.languages.jsx.tag.pattern = a(
|
||||
'</?(?:[\\w.:-]+(?:<S>+(?:[\\w.:$-]+(?:=(?:"(?:\\\\[^]|[^\\\\"])*"|\'(?:\\\\[^]|[^\\\\\'])*\'|[^\\s{\'"/>=]+|<BRACES>))?|<SPREAD>))*<S>*/?)?>',
|
||||
)),
|
||||
(t.languages.jsx.tag.inside.tag.pattern = /^<\/?[^\s>\/]*/),
|
||||
(t.languages.jsx.tag.inside['attr-value'].pattern = /=(?!\{)(?:"(?:\\[\s\S]|[^\\"])*"|'(?:\\[\s\S]|[^\\'])*'|[^\s'">]+)/),
|
||||
(t.languages.jsx.tag.inside.tag.inside['class-name'] = /^[A-Z]\w*(?:\.[A-Z]\w*)*$/),
|
||||
(t.languages.jsx.tag.inside.comment = n.comment),
|
||||
t.languages.insertBefore(
|
||||
'inside',
|
||||
'attr-name',
|
||||
{ spread: { pattern: a('<SPREAD>'), inside: t.languages.jsx } },
|
||||
t.languages.jsx.tag,
|
||||
),
|
||||
t.languages.insertBefore(
|
||||
'inside',
|
||||
'special-attr',
|
||||
{
|
||||
script: {
|
||||
pattern: a('=<BRACES>'),
|
||||
alias: 'language-javascript',
|
||||
inside: {
|
||||
'script-punctuation': { pattern: /^=(?=\{)/, alias: 'punctuation' },
|
||||
rest: t.languages.jsx,
|
||||
},
|
||||
},
|
||||
},
|
||||
t.languages.jsx.tag,
|
||||
);
|
||||
const s = function (t) {
|
||||
return t
|
||||
? typeof t === 'string'
|
||||
? t
|
||||
: typeof t.content === 'string'
|
||||
? t.content
|
||||
: t.content.map(s).join('')
|
||||
: '';
|
||||
};
|
||||
const g = function (n) {
|
||||
for (let e = [], a = 0; a < n.length; a++) {
|
||||
const o = n[a];
|
||||
let i = !1;
|
||||
if (
|
||||
(typeof o !== 'string'
|
||||
&& (o.type === 'tag' && o.content[0] && o.content[0].type === 'tag'
|
||||
? o.content[0].content[0].content === '</'
|
||||
? e.length > 0
|
||||
&& e[e.length - 1].tagName === s(o.content[0].content[1])
|
||||
&& e.pop()
|
||||
: o.content[o.content.length - 1].content === '/>'
|
||||
|| e.push({
|
||||
tagName: s(o.content[0].content[1]),
|
||||
openedBraces: 0,
|
||||
})
|
||||
: e.length > 0 && o.type === 'punctuation' && o.content === '{'
|
||||
? e[e.length - 1].openedBraces++
|
||||
: e.length > 0
|
||||
&& e[e.length - 1].openedBraces > 0
|
||||
&& o.type === 'punctuation'
|
||||
&& o.content === '}'
|
||||
? e[e.length - 1].openedBraces--
|
||||
: (i = !0)),
|
||||
(i || typeof o === 'string')
|
||||
&& e.length > 0
|
||||
&& e[e.length - 1].openedBraces === 0)
|
||||
) {
|
||||
let r = s(o);
|
||||
a < n.length - 1
|
||||
&& (typeof n[a + 1] === 'string' || n[a + 1].type === 'plain-text')
|
||||
&& ((r += s(n[a + 1])), n.splice(a + 1, 1)),
|
||||
a > 0
|
||||
&& (typeof n[a - 1] === 'string' || n[a - 1].type === 'plain-text')
|
||||
&& ((r = s(n[a - 1]) + r), n.splice(a - 1, 1), a--),
|
||||
(n[a] = new t.Token('plain-text', r, null, r));
|
||||
}
|
||||
o.content && typeof o.content !== 'string' && g(o.content);
|
||||
}
|
||||
};
|
||||
t.hooks.add('after-tokenize', (t) => {
|
||||
(t.language !== 'jsx' && t.language !== 'tsx') || g(t.tokens);
|
||||
});
|
||||
}(Prism));
|
||||
|
|
|
|||
67
frontend/app/assets/prism/prism-kotlin.min.js
vendored
67
frontend/app/assets/prism/prism-kotlin.min.js
vendored
|
|
@ -1 +1,66 @@
|
|||
!function(n){n.languages.kotlin=n.languages.extend("clike",{keyword:{pattern:/(^|[^.])\b(?:abstract|actual|annotation|as|break|by|catch|class|companion|const|constructor|continue|crossinline|data|do|dynamic|else|enum|expect|external|final|finally|for|fun|get|if|import|in|infix|init|inline|inner|interface|internal|is|lateinit|noinline|null|object|open|operator|out|override|package|private|protected|public|reified|return|sealed|set|super|suspend|tailrec|this|throw|to|try|typealias|val|var|vararg|when|where|while)\b/,lookbehind:!0},function:[{pattern:/(?:`[^\r\n`]+`|\b\w+)(?=\s*\()/,greedy:!0},{pattern:/(\.)(?:`[^\r\n`]+`|\w+)(?=\s*\{)/,lookbehind:!0,greedy:!0}],number:/\b(?:0[xX][\da-fA-F]+(?:_[\da-fA-F]+)*|0[bB][01]+(?:_[01]+)*|\d+(?:_\d+)*(?:\.\d+(?:_\d+)*)?(?:[eE][+-]?\d+(?:_\d+)*)?[fFL]?)\b/,operator:/\+[+=]?|-[-=>]?|==?=?|!(?:!|==?)?|[\/*%<>]=?|[?:]:?|\.\.|&&|\|\||\b(?:and|inv|or|shl|shr|ushr|xor)\b/}),delete n.languages.kotlin["class-name"];var e={"interpolation-punctuation":{pattern:/^\$\{?|\}$/,alias:"punctuation"},expression:{pattern:/[\s\S]+/,inside:n.languages.kotlin}};n.languages.insertBefore("kotlin","string",{"string-literal":[{pattern:/"""(?:[^$]|\$(?:(?!\{)|\{[^{}]*\}))*?"""/,alias:"multiline",inside:{interpolation:{pattern:/\$(?:[a-z_]\w*|\{[^{}]*\})/i,inside:e},string:/[\s\S]+/}},{pattern:/"(?:[^"\\\r\n$]|\\.|\$(?:(?!\{)|\{[^{}]*\}))*"/,alias:"singleline",inside:{interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$(?:[a-z_]\w*|\{[^{}]*\})/i,lookbehind:!0,inside:e},string:/[\s\S]+/}}],char:{pattern:/'(?:[^'\\\r\n]|\\(?:.|u[a-fA-F0-9]{0,4}))'/,greedy:!0}}),delete n.languages.kotlin.string,n.languages.insertBefore("kotlin","keyword",{annotation:{pattern:/\B@(?:\w+:)?(?:[A-Z]\w*|\[[^\]]+\])/,alias:"builtin"}}),n.languages.insertBefore("kotlin","function",{label:{pattern:/\b\w+@|@\w+\b/,alias:"symbol"}}),n.languages.kt=n.languages.kotlin,n.languages.kts=n.languages.kotlin}(Prism);
|
||||
!(function (n) {
|
||||
(n.languages.kotlin = n.languages.extend('clike', {
|
||||
keyword: {
|
||||
pattern:
|
||||
/(^|[^.])\b(?:abstract|actual|annotation|as|break|by|catch|class|companion|const|constructor|continue|crossinline|data|do|dynamic|else|enum|expect|external|final|finally|for|fun|get|if|import|in|infix|init|inline|inner|interface|internal|is|lateinit|noinline|null|object|open|operator|out|override|package|private|protected|public|reified|return|sealed|set|super|suspend|tailrec|this|throw|to|try|typealias|val|var|vararg|when|where|while)\b/,
|
||||
lookbehind: !0,
|
||||
},
|
||||
function: [
|
||||
{ pattern: /(?:`[^\r\n`]+`|\b\w+)(?=\s*\()/, greedy: !0 },
|
||||
{
|
||||
pattern: /(\.)(?:`[^\r\n`]+`|\w+)(?=\s*\{)/,
|
||||
lookbehind: !0,
|
||||
greedy: !0,
|
||||
},
|
||||
],
|
||||
number:
|
||||
/\b(?:0[xX][\da-fA-F]+(?:_[\da-fA-F]+)*|0[bB][01]+(?:_[01]+)*|\d+(?:_\d+)*(?:\.\d+(?:_\d+)*)?(?:[eE][+-]?\d+(?:_\d+)*)?[fFL]?)\b/,
|
||||
operator:
|
||||
/\+[+=]?|-[-=>]?|==?=?|!(?:!|==?)?|[\/*%<>]=?|[?:]:?|\.\.|&&|\|\||\b(?:and|inv|or|shl|shr|ushr|xor)\b/,
|
||||
})),
|
||||
delete n.languages.kotlin['class-name'];
|
||||
const e = {
|
||||
'interpolation-punctuation': {
|
||||
pattern: /^\$\{?|\}$/,
|
||||
alias: 'punctuation',
|
||||
},
|
||||
expression: { pattern: /[\s\S]+/, inside: n.languages.kotlin },
|
||||
};
|
||||
n.languages.insertBefore('kotlin', 'string', {
|
||||
'string-literal': [
|
||||
{
|
||||
pattern: /"""(?:[^$]|\$(?:(?!\{)|\{[^{}]*\}))*?"""/,
|
||||
alias: 'multiline',
|
||||
inside: {
|
||||
interpolation: { pattern: /\$(?:[a-z_]\w*|\{[^{}]*\})/i, inside: e },
|
||||
string: /[\s\S]+/,
|
||||
},
|
||||
},
|
||||
{
|
||||
pattern: /"(?:[^"\\\r\n$]|\\.|\$(?:(?!\{)|\{[^{}]*\}))*"/,
|
||||
alias: 'singleline',
|
||||
inside: {
|
||||
interpolation: {
|
||||
pattern: /((?:^|[^\\])(?:\\{2})*)\$(?:[a-z_]\w*|\{[^{}]*\})/i,
|
||||
lookbehind: !0,
|
||||
inside: e,
|
||||
},
|
||||
string: /[\s\S]+/,
|
||||
},
|
||||
},
|
||||
],
|
||||
char: { pattern: /'(?:[^'\\\r\n]|\\(?:.|u[a-fA-F0-9]{0,4}))'/, greedy: !0 },
|
||||
}),
|
||||
delete n.languages.kotlin.string,
|
||||
n.languages.insertBefore('kotlin', 'keyword', {
|
||||
annotation: {
|
||||
pattern: /\B@(?:\w+:)?(?:[A-Z]\w*|\[[^\]]+\])/,
|
||||
alias: 'builtin',
|
||||
},
|
||||
}),
|
||||
n.languages.insertBefore('kotlin', 'function', {
|
||||
label: { pattern: /\b\w+@|@\w+\b/, alias: 'symbol' },
|
||||
}),
|
||||
(n.languages.kt = n.languages.kotlin),
|
||||
(n.languages.kts = n.languages.kotlin);
|
||||
}(Prism));
|
||||
|
|
|
|||
97
frontend/app/assets/prism/prism-swift.min.js
vendored
97
frontend/app/assets/prism/prism-swift.min.js
vendored
|
|
@ -1 +1,96 @@
|
|||
Prism.languages.swift={comment:{pattern:/(^|[^\\:])(?:\/\/.*|\/\*(?:[^/*]|\/(?!\*)|\*(?!\/)|\/\*(?:[^*]|\*(?!\/))*\*\/)*\*\/)/,lookbehind:!0,greedy:!0},"string-literal":[{pattern:RegExp('(^|[^"#])(?:"(?:\\\\(?:\\((?:[^()]|\\([^()]*\\))*\\)|\r\n|[^(])|[^\\\\\r\n"])*"|"""(?:\\\\(?:\\((?:[^()]|\\([^()]*\\))*\\)|[^(])|[^\\\\"]|"(?!""))*""")(?!["#])'),lookbehind:!0,greedy:!0,inside:{interpolation:{pattern:/(\\\()(?:[^()]|\([^()]*\))*(?=\))/,lookbehind:!0,inside:null},"interpolation-punctuation":{pattern:/^\)|\\\($/,alias:"punctuation"},punctuation:/\\(?=[\r\n])/,string:/[\s\S]+/}},{pattern:RegExp('(^|[^"#])(#+)(?:"(?:\\\\(?:#+\\((?:[^()]|\\([^()]*\\))*\\)|\r\n|[^#])|[^\\\\\r\n])*?"|"""(?:\\\\(?:#+\\((?:[^()]|\\([^()]*\\))*\\)|[^#])|[^\\\\])*?""")\\2'),lookbehind:!0,greedy:!0,inside:{interpolation:{pattern:/(\\#+\()(?:[^()]|\([^()]*\))*(?=\))/,lookbehind:!0,inside:null},"interpolation-punctuation":{pattern:/^\)|\\#+\($/,alias:"punctuation"},string:/[\s\S]+/}}],directive:{pattern:RegExp("#(?:(?:elseif|if)\\b(?:[ \t]*(?:![ \t]*)?(?:\\b\\w+\\b(?:[ \t]*\\((?:[^()]|\\([^()]*\\))*\\))?|\\((?:[^()]|\\([^()]*\\))*\\))(?:[ \t]*(?:&&|\\|\\|))?)+|(?:else|endif)\\b)"),alias:"property",inside:{"directive-name":/^#\w+/,boolean:/\b(?:false|true)\b/,number:/\b\d+(?:\.\d+)*\b/,operator:/!|&&|\|\||[<>]=?/,punctuation:/[(),]/}},literal:{pattern:/#(?:colorLiteral|column|dsohandle|file(?:ID|Literal|Path)?|function|imageLiteral|line)\b/,alias:"constant"},"other-directive":{pattern:/#\w+\b/,alias:"property"},attribute:{pattern:/@\w+/,alias:"atrule"},"function-definition":{pattern:/(\bfunc\s+)\w+/,lookbehind:!0,alias:"function"},label:{pattern:/\b(break|continue)\s+\w+|\b[a-zA-Z_]\w*(?=\s*:\s*(?:for|repeat|while)\b)/,lookbehind:!0,alias:"important"},keyword:/\b(?:Any|Protocol|Self|Type|actor|as|assignment|associatedtype|associativity|async|await|break|case|catch|class|continue|convenience|default|defer|deinit|didSet|do|dynamic|else|enum|extension|fallthrough|fileprivate|final|for|func|get|guard|higherThan|if|import|in|indirect|infix|init|inout|internal|is|isolated|lazy|left|let|lowerThan|mutating|none|nonisolated|nonmutating|open|operator|optional|override|postfix|precedencegroup|prefix|private|protocol|public|repeat|required|rethrows|return|right|safe|self|set|some|static|struct|subscript|super|switch|throw|throws|try|typealias|unowned|unsafe|var|weak|where|while|willSet)\b/,boolean:/\b(?:false|true)\b/,nil:{pattern:/\bnil\b/,alias:"constant"},"short-argument":/\$\d+\b/,omit:{pattern:/\b_\b/,alias:"keyword"},number:/\b(?:[\d_]+(?:\.[\de_]+)?|0x[a-f0-9_]+(?:\.[a-f0-9p_]+)?|0b[01_]+|0o[0-7_]+)\b/i,"class-name":/\b[A-Z](?:[A-Z_\d]*[a-z]\w*)?\b/,function:/\b[a-z_]\w*(?=\s*\()/i,constant:/\b(?:[A-Z_]{2,}|k[A-Z][A-Za-z_]+)\b/,operator:/[-+*/%=!<>&|^~?]+|\.[.\-+*/%=!<>&|^~?]+/,punctuation:/[{}[\]();,.:\\]/},Prism.languages.swift["string-literal"].forEach((function(e){e.inside.interpolation.inside=Prism.languages.swift}));
|
||||
(Prism.languages.swift = {
|
||||
comment: {
|
||||
pattern:
|
||||
/(^|[^\\:])(?:\/\/.*|\/\*(?:[^/*]|\/(?!\*)|\*(?!\/)|\/\*(?:[^*]|\*(?!\/))*\*\/)*\*\/)/,
|
||||
lookbehind: !0,
|
||||
greedy: !0,
|
||||
},
|
||||
'string-literal': [
|
||||
{
|
||||
pattern: RegExp(
|
||||
'(^|[^"#])(?:"(?:\\\\(?:\\((?:[^()]|\\([^()]*\\))*\\)|\r\n|[^(])|[^\\\\\r\n"])*"|"""(?:\\\\(?:\\((?:[^()]|\\([^()]*\\))*\\)|[^(])|[^\\\\"]|"(?!""))*""")(?!["#])',
|
||||
),
|
||||
lookbehind: !0,
|
||||
greedy: !0,
|
||||
inside: {
|
||||
interpolation: {
|
||||
pattern: /(\\\()(?:[^()]|\([^()]*\))*(?=\))/,
|
||||
lookbehind: !0,
|
||||
inside: null,
|
||||
},
|
||||
'interpolation-punctuation': {
|
||||
pattern: /^\)|\\\($/,
|
||||
alias: 'punctuation',
|
||||
},
|
||||
punctuation: /\\(?=[\r\n])/,
|
||||
string: /[\s\S]+/,
|
||||
},
|
||||
},
|
||||
{
|
||||
pattern: RegExp(
|
||||
'(^|[^"#])(#+)(?:"(?:\\\\(?:#+\\((?:[^()]|\\([^()]*\\))*\\)|\r\n|[^#])|[^\\\\\r\n])*?"|"""(?:\\\\(?:#+\\((?:[^()]|\\([^()]*\\))*\\)|[^#])|[^\\\\])*?""")\\2',
|
||||
),
|
||||
lookbehind: !0,
|
||||
greedy: !0,
|
||||
inside: {
|
||||
interpolation: {
|
||||
pattern: /(\\#+\()(?:[^()]|\([^()]*\))*(?=\))/,
|
||||
lookbehind: !0,
|
||||
inside: null,
|
||||
},
|
||||
'interpolation-punctuation': {
|
||||
pattern: /^\)|\\#+\($/,
|
||||
alias: 'punctuation',
|
||||
},
|
||||
string: /[\s\S]+/,
|
||||
},
|
||||
},
|
||||
],
|
||||
directive: {
|
||||
pattern: RegExp(
|
||||
'#(?:(?:elseif|if)\\b(?:[ \t]*(?:![ \t]*)?(?:\\b\\w+\\b(?:[ \t]*\\((?:[^()]|\\([^()]*\\))*\\))?|\\((?:[^()]|\\([^()]*\\))*\\))(?:[ \t]*(?:&&|\\|\\|))?)+|(?:else|endif)\\b)',
|
||||
),
|
||||
alias: 'property',
|
||||
inside: {
|
||||
'directive-name': /^#\w+/,
|
||||
boolean: /\b(?:false|true)\b/,
|
||||
number: /\b\d+(?:\.\d+)*\b/,
|
||||
operator: /!|&&|\|\||[<>]=?/,
|
||||
punctuation: /[(),]/,
|
||||
},
|
||||
},
|
||||
literal: {
|
||||
pattern:
|
||||
/#(?:colorLiteral|column|dsohandle|file(?:ID|Literal|Path)?|function|imageLiteral|line)\b/,
|
||||
alias: 'constant',
|
||||
},
|
||||
'other-directive': { pattern: /#\w+\b/, alias: 'property' },
|
||||
attribute: { pattern: /@\w+/, alias: 'atrule' },
|
||||
'function-definition': {
|
||||
pattern: /(\bfunc\s+)\w+/,
|
||||
lookbehind: !0,
|
||||
alias: 'function',
|
||||
},
|
||||
label: {
|
||||
pattern:
|
||||
/\b(break|continue)\s+\w+|\b[a-zA-Z_]\w*(?=\s*:\s*(?:for|repeat|while)\b)/,
|
||||
lookbehind: !0,
|
||||
alias: 'important',
|
||||
},
|
||||
keyword:
|
||||
/\b(?:Any|Protocol|Self|Type|actor|as|assignment|associatedtype|associativity|async|await|break|case|catch|class|continue|convenience|default|defer|deinit|didSet|do|dynamic|else|enum|extension|fallthrough|fileprivate|final|for|func|get|guard|higherThan|if|import|in|indirect|infix|init|inout|internal|is|isolated|lazy|left|let|lowerThan|mutating|none|nonisolated|nonmutating|open|operator|optional|override|postfix|precedencegroup|prefix|private|protocol|public|repeat|required|rethrows|return|right|safe|self|set|some|static|struct|subscript|super|switch|throw|throws|try|typealias|unowned|unsafe|var|weak|where|while|willSet)\b/,
|
||||
boolean: /\b(?:false|true)\b/,
|
||||
nil: { pattern: /\bnil\b/, alias: 'constant' },
|
||||
'short-argument': /\$\d+\b/,
|
||||
omit: { pattern: /\b_\b/, alias: 'keyword' },
|
||||
number:
|
||||
/\b(?:[\d_]+(?:\.[\de_]+)?|0x[a-f0-9_]+(?:\.[a-f0-9p_]+)?|0b[01_]+|0o[0-7_]+)\b/i,
|
||||
'class-name': /\b[A-Z](?:[A-Z_\d]*[a-z]\w*)?\b/,
|
||||
function: /\b[a-z_]\w*(?=\s*\()/i,
|
||||
constant: /\b(?:[A-Z_]{2,}|k[A-Z][A-Za-z_]+)\b/,
|
||||
operator: /[-+*/%=!<>&|^~?]+|\.[.\-+*/%=!<>&|^~?]+/,
|
||||
punctuation: /[{}[\]();,.:\\]/,
|
||||
}),
|
||||
Prism.languages.swift['string-literal'].forEach((e) => {
|
||||
e.inside.interpolation.inside = Prism.languages.swift;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1 +1,42 @@
|
|||
!function(e){e.languages.typescript=e.languages.extend("javascript",{"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|type)\s+)(?!keyof\b)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?:\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?/,lookbehind:!0,greedy:!0,inside:null},builtin:/\b(?:Array|Function|Promise|any|boolean|console|never|number|string|symbol|unknown)\b/}),e.languages.typescript.keyword.push(/\b(?:abstract|declare|is|keyof|readonly|require)\b/,/\b(?:asserts|infer|interface|module|namespace|type)\b(?=\s*(?:[{_$a-zA-Z\xA0-\uFFFF]|$))/,/\btype\b(?=\s*(?:[\{*]|$))/),delete e.languages.typescript.parameter,delete e.languages.typescript["literal-property"];var s=e.languages.extend("typescript",{});delete s["class-name"],e.languages.typescript["class-name"].inside=s,e.languages.insertBefore("typescript","function",{decorator:{pattern:/@[$\w\xA0-\uFFFF]+/,inside:{at:{pattern:/^@/,alias:"operator"},function:/^[\s\S]+/}},"generic-function":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>(?=\s*\()/,greedy:!0,inside:{function:/^#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*/,generic:{pattern:/<[\s\S]+/,alias:"class-name",inside:s}}}}),e.languages.ts=e.languages.typescript}(Prism);
|
||||
!(function (e) {
|
||||
(e.languages.typescript = e.languages.extend('javascript', {
|
||||
'class-name': {
|
||||
pattern:
|
||||
/(\b(?:class|extends|implements|instanceof|interface|new|type)\s+)(?!keyof\b)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?:\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?/,
|
||||
lookbehind: !0,
|
||||
greedy: !0,
|
||||
inside: null,
|
||||
},
|
||||
builtin:
|
||||
/\b(?:Array|Function|Promise|any|boolean|console|never|number|string|symbol|unknown)\b/,
|
||||
})),
|
||||
e.languages.typescript.keyword.push(
|
||||
/\b(?:abstract|declare|is|keyof|readonly|require)\b/,
|
||||
/\b(?:asserts|infer|interface|module|namespace|type)\b(?=\s*(?:[{_$a-zA-Z\xA0-\uFFFF]|$))/,
|
||||
/\btype\b(?=\s*(?:[\{*]|$))/,
|
||||
),
|
||||
delete e.languages.typescript.parameter,
|
||||
delete e.languages.typescript['literal-property'];
|
||||
const s = e.languages.extend('typescript', {});
|
||||
delete s['class-name'],
|
||||
(e.languages.typescript['class-name'].inside = s),
|
||||
e.languages.insertBefore('typescript', 'function', {
|
||||
decorator: {
|
||||
pattern: /@[$\w\xA0-\uFFFF]+/,
|
||||
inside: {
|
||||
at: { pattern: /^@/, alias: 'operator' },
|
||||
function: /^[\s\S]+/,
|
||||
},
|
||||
},
|
||||
'generic-function': {
|
||||
pattern:
|
||||
/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>(?=\s*\()/,
|
||||
greedy: !0,
|
||||
inside: {
|
||||
function: /^#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*/,
|
||||
generic: { pattern: /<[\s\S]+/, alias: 'class-name', inside: s },
|
||||
},
|
||||
},
|
||||
}),
|
||||
(e.languages.ts = e.languages.typescript);
|
||||
}(Prism));
|
||||
|
|
|
|||
913
frontend/app/assets/prism/prism.min.js
vendored
913
frontend/app/assets/prism/prism.min.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -1,395 +1,397 @@
|
|||
import React, {useEffect} from 'react';
|
||||
import {Form, Input, SegmentSelection, Checkbox, Icon} from 'UI';
|
||||
import {alertConditions as conditions} from 'App/constants';
|
||||
import stl from './alertForm.module.css';
|
||||
import DropdownChips from './DropdownChips';
|
||||
import {validateEmail} from 'App/validate';
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
Form, Input, SegmentSelection, Checkbox, Icon,
|
||||
} from 'UI';
|
||||
import { alertConditions as conditions } from 'App/constants';
|
||||
import { validateEmail } from 'App/validate';
|
||||
import cn from 'classnames';
|
||||
import {useStore} from 'App/mstore'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import Select from 'Shared/Select';
|
||||
import {Button} from "antd";
|
||||
import { Button } from 'antd';
|
||||
import DropdownChips from './DropdownChips';
|
||||
import stl from './alertForm.module.css';
|
||||
|
||||
const thresholdOptions = [
|
||||
{label: '15 minutes', value: 15},
|
||||
{label: '30 minutes', value: 30},
|
||||
{label: '1 hour', value: 60},
|
||||
{label: '2 hours', value: 120},
|
||||
{label: '4 hours', value: 240},
|
||||
{label: '1 day', value: 1440},
|
||||
{ label: '15 minutes', value: 15 },
|
||||
{ label: '30 minutes', value: 30 },
|
||||
{ label: '1 hour', value: 60 },
|
||||
{ label: '2 hours', value: 120 },
|
||||
{ label: '4 hours', value: 240 },
|
||||
{ label: '1 day', value: 1440 },
|
||||
];
|
||||
|
||||
const changeOptions = [
|
||||
{label: 'change', value: 'change'},
|
||||
{label: '% change', value: 'percent'},
|
||||
{ label: 'change', value: 'change' },
|
||||
{ label: '% change', value: 'percent' },
|
||||
];
|
||||
|
||||
const Circle = ({text}) => (
|
||||
function Circle({ text }) {
|
||||
return (
|
||||
<div className="circle mr-4 w-6 h-6 rounded-full bg-gray-light flex items-center justify-center">
|
||||
{text}
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
const Section = ({index, title, description, content}) => (
|
||||
function Section({
|
||||
index, title, description, content,
|
||||
}) {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="flex items-start">
|
||||
<Circle text={index}/>
|
||||
<div>
|
||||
<span className="font-medium">{title}</span>
|
||||
{description && <div className="text-sm color-gray-medium">{description}</div>}
|
||||
</div>
|
||||
<div className="flex items-start">
|
||||
<Circle text={index} />
|
||||
<div>
|
||||
<span className="font-medium">{title}</span>
|
||||
{description && <div className="text-sm color-gray-medium">{description}</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ml-10">{content}</div>
|
||||
<div className="ml-10">{content}</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
function AlertForm(props) {
|
||||
const {
|
||||
slackChannels,
|
||||
msTeamsChannels,
|
||||
webhooks,
|
||||
onDelete,
|
||||
style = {height: "calc('100vh - 40px')"},
|
||||
} = props;
|
||||
const {alertsStore, metricStore} = useStore()
|
||||
const {
|
||||
triggerOptions: allTriggerSeries,
|
||||
loading,
|
||||
} = alertsStore
|
||||
const {
|
||||
slackChannels,
|
||||
msTeamsChannels,
|
||||
webhooks,
|
||||
onDelete,
|
||||
style = { height: "calc('100vh - 40px')" },
|
||||
} = props;
|
||||
const { alertsStore, metricStore } = useStore();
|
||||
const {
|
||||
triggerOptions: allTriggerSeries,
|
||||
loading,
|
||||
} = alertsStore;
|
||||
|
||||
const triggerOptions = metricStore.instance.series.length > 0 ? allTriggerSeries.filter(s => {
|
||||
return metricStore.instance.series.findIndex(ms => ms.seriesId === s.value) !== -1
|
||||
}).map(v => {
|
||||
const labelArr = v.label.split('.')
|
||||
labelArr.shift()
|
||||
return {
|
||||
...v,
|
||||
label: labelArr.join('.')
|
||||
}
|
||||
}) : allTriggerSeries
|
||||
const instance = alertsStore.instance
|
||||
const deleting = loading
|
||||
|
||||
const write = ({target: {value, name}}) => alertsStore.edit({[name]: value});
|
||||
const writeOption = (e, {name, value}) => alertsStore.edit({[name]: value.value});
|
||||
const onChangeCheck = ({target: {checked, name}}) => alertsStore.edit({[name]: checked});
|
||||
|
||||
useEffect(() => {
|
||||
void alertsStore.fetchTriggerOptions();
|
||||
}, []);
|
||||
|
||||
const writeQueryOption = (e, {name, value}) => {
|
||||
const {query} = instance;
|
||||
alertsStore.edit({query: {...query, [name]: value}});
|
||||
const triggerOptions = metricStore.instance.series.length > 0 ? allTriggerSeries.filter((s) => metricStore.instance.series.findIndex((ms) => ms.seriesId === s.value) !== -1).map((v) => {
|
||||
const labelArr = v.label.split('.');
|
||||
labelArr.shift();
|
||||
return {
|
||||
...v,
|
||||
label: labelArr.join('.'),
|
||||
};
|
||||
}) : allTriggerSeries;
|
||||
const { instance } = alertsStore;
|
||||
const deleting = loading;
|
||||
|
||||
const writeQuery = ({target: {value, name}}) => {
|
||||
const {query} = instance;
|
||||
alertsStore.edit({query: {...query, [name]: value}});
|
||||
};
|
||||
const write = ({ target: { value, name } }) => alertsStore.edit({ [name]: value });
|
||||
const writeOption = (e, { name, value }) => alertsStore.edit({ [name]: value.value });
|
||||
const onChangeCheck = ({ target: { checked, name } }) => alertsStore.edit({ [name]: checked });
|
||||
|
||||
const metric =
|
||||
instance && instance.query.left
|
||||
? triggerOptions.find((i) => i.value === instance.query.left)
|
||||
: null;
|
||||
const unit = metric ? metric.unit : '';
|
||||
const isThreshold = instance.detectionMethod === 'threshold';
|
||||
useEffect(() => {
|
||||
void alertsStore.fetchTriggerOptions();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Form
|
||||
className={cn('pb-10', stl.wrapper)}
|
||||
style={style}
|
||||
onSubmit={() => props.onSubmit(instance)}
|
||||
id="alert-form"
|
||||
>
|
||||
<div className={cn('-mx-6 px-6 pb-12')}>
|
||||
<input
|
||||
autoFocus={true}
|
||||
className="text-lg border border-gray-light rounded w-full"
|
||||
name="name"
|
||||
style={{fontSize: '18px', padding: '10px', fontWeight: '600'}}
|
||||
value={instance && instance.name}
|
||||
onChange={write}
|
||||
placeholder="Untiltled Alert"
|
||||
id="name-field"
|
||||
/>
|
||||
<div className="mb-8"/>
|
||||
<Section
|
||||
index="1"
|
||||
title={'What kind of alert do you want to set?'}
|
||||
content={
|
||||
<div>
|
||||
<SegmentSelection
|
||||
primary
|
||||
name="detectionMethod"
|
||||
className="my-3"
|
||||
onSelect={(e, {name, value}) => alertsStore.edit({[name]: value})}
|
||||
value={{value: instance.detectionMethod}}
|
||||
list={[
|
||||
{name: 'Threshold', value: 'threshold'},
|
||||
{name: 'Change', value: 'change'},
|
||||
]}
|
||||
/>
|
||||
<div className="text-sm color-gray-medium">
|
||||
{isThreshold &&
|
||||
'Eg. Alert me if memory.avg is greater than 500mb over the past 4 hours.'}
|
||||
{!isThreshold &&
|
||||
'Eg. Alert me if % change of memory.avg is greater than 10% over the past 4 hours compared to the previous 4 hours.'}
|
||||
</div>
|
||||
<div className="my-4"/>
|
||||
</div>
|
||||
}
|
||||
const writeQueryOption = (e, { name, value }) => {
|
||||
const { query } = instance;
|
||||
alertsStore.edit({ query: { ...query, [name]: value } });
|
||||
};
|
||||
|
||||
const writeQuery = ({ target: { value, name } }) => {
|
||||
const { query } = instance;
|
||||
alertsStore.edit({ query: { ...query, [name]: value } });
|
||||
};
|
||||
|
||||
const metric = instance && instance.query.left
|
||||
? triggerOptions.find((i) => i.value === instance.query.left)
|
||||
: null;
|
||||
const unit = metric ? metric.unit : '';
|
||||
const isThreshold = instance.detectionMethod === 'threshold';
|
||||
|
||||
return (
|
||||
<Form
|
||||
className={cn('pb-10', stl.wrapper)}
|
||||
style={style}
|
||||
onSubmit={() => props.onSubmit(instance)}
|
||||
id="alert-form"
|
||||
>
|
||||
<div className={cn('-mx-6 px-6 pb-12')}>
|
||||
<input
|
||||
autoFocus
|
||||
className="text-lg border border-gray-light rounded w-full"
|
||||
name="name"
|
||||
style={{ fontSize: '18px', padding: '10px', fontWeight: '600' }}
|
||||
value={instance && instance.name}
|
||||
onChange={write}
|
||||
placeholder="Untiltled Alert"
|
||||
id="name-field"
|
||||
/>
|
||||
<div className="mb-8" />
|
||||
<Section
|
||||
index="1"
|
||||
title="What kind of alert do you want to set?"
|
||||
content={(
|
||||
<div>
|
||||
<SegmentSelection
|
||||
primary
|
||||
name="detectionMethod"
|
||||
className="my-3"
|
||||
onSelect={(e, { name, value }) => alertsStore.edit({ [name]: value })}
|
||||
value={{ value: instance.detectionMethod }}
|
||||
list={[
|
||||
{ name: 'Threshold', value: 'threshold' },
|
||||
{ name: 'Change', value: 'change' },
|
||||
]}
|
||||
/>
|
||||
<div className="text-sm color-gray-medium">
|
||||
{isThreshold
|
||||
&& 'Eg. Alert me if memory.avg is greater than 500mb over the past 4 hours.'}
|
||||
{!isThreshold
|
||||
&& 'Eg. Alert me if % change of memory.avg is greater than 10% over the past 4 hours compared to the previous 4 hours.'}
|
||||
</div>
|
||||
<div className="my-4" />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<hr className="my-8" />
|
||||
|
||||
<Section
|
||||
index="2"
|
||||
title="Condition"
|
||||
content={(
|
||||
<div>
|
||||
{!isThreshold && (
|
||||
<div className="flex items-center my-3">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal">Trigger when</label>
|
||||
<Select
|
||||
className="w-4/6"
|
||||
placeholder="change"
|
||||
options={changeOptions}
|
||||
name="change"
|
||||
defaultValue={instance.change}
|
||||
onChange={({ value }) => writeOption(null, { name: 'change', value })}
|
||||
id="change-dropdown"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<hr className="my-8"/>
|
||||
|
||||
<Section
|
||||
index="2"
|
||||
title="Condition"
|
||||
content={
|
||||
<div>
|
||||
{!isThreshold && (
|
||||
<div className="flex items-center my-3">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal">{'Trigger when'}</label>
|
||||
<Select
|
||||
className="w-4/6"
|
||||
placeholder="change"
|
||||
options={changeOptions}
|
||||
name="change"
|
||||
defaultValue={instance.change}
|
||||
onChange={({value}) => writeOption(null, {name: 'change', value})}
|
||||
id="change-dropdown"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center my-3">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal">
|
||||
{isThreshold ? 'Trigger when' : 'of'}
|
||||
</label>
|
||||
<Select
|
||||
className="w-4/6"
|
||||
placeholder="Select Metric"
|
||||
isSearchable={true}
|
||||
options={triggerOptions}
|
||||
name="left"
|
||||
value={triggerOptions.find((i) => i.value === instance.query.left)}
|
||||
<div className="flex items-center my-3">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal">
|
||||
{isThreshold ? 'Trigger when' : 'of'}
|
||||
</label>
|
||||
<Select
|
||||
className="w-4/6"
|
||||
placeholder="Select Metric"
|
||||
isSearchable
|
||||
options={triggerOptions}
|
||||
name="left"
|
||||
value={triggerOptions.find((i) => i.value === instance.query.left)}
|
||||
// onChange={ writeQueryOption }
|
||||
onChange={({value}) =>
|
||||
writeQueryOption(null, {name: 'left', value: value.value})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
onChange={({ value }) => writeQueryOption(null, { name: 'left', value: value.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center my-3">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal">{'is'}</label>
|
||||
<div className="w-4/6 flex items-center">
|
||||
<Select
|
||||
placeholder="Select Condition"
|
||||
options={conditions}
|
||||
name="operator"
|
||||
defaultValue={instance.query.operator}
|
||||
<div className="flex items-center my-3">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal">is</label>
|
||||
<div className="w-4/6 flex items-center">
|
||||
<Select
|
||||
placeholder="Select Condition"
|
||||
options={conditions}
|
||||
name="operator"
|
||||
defaultValue={instance.query.operator}
|
||||
// onChange={ writeQueryOption }
|
||||
onChange={({value}) =>
|
||||
writeQueryOption(null, {name: 'operator', value: value.value})
|
||||
}
|
||||
/>
|
||||
{unit && (
|
||||
<>
|
||||
<Input
|
||||
className="px-4"
|
||||
style={{marginRight: '31px'}}
|
||||
onChange={({ value }) => writeQueryOption(null, { name: 'operator', value: value.value })}
|
||||
/>
|
||||
{unit && (
|
||||
<>
|
||||
<Input
|
||||
className="px-4"
|
||||
style={{ marginRight: '31px' }}
|
||||
// label={{ basic: true, content: unit }}
|
||||
// labelPosition='right'
|
||||
name="right"
|
||||
value={instance.query.right}
|
||||
onChange={writeQuery}
|
||||
placeholder="E.g. 3"
|
||||
/>
|
||||
<span className="ml-2">{'test'}</span>
|
||||
</>
|
||||
)}
|
||||
{!unit && (
|
||||
<Input
|
||||
wrapperClassName="ml-2"
|
||||
name="right"
|
||||
value={instance.query.right}
|
||||
onChange={writeQuery}
|
||||
placeholder="E.g. 3"
|
||||
/>
|
||||
<span className="ml-2">test</span>
|
||||
</>
|
||||
)}
|
||||
{!unit && (
|
||||
<Input
|
||||
wrapperClassName="ml-2"
|
||||
// className="pl-4"
|
||||
name="right"
|
||||
value={instance.query.right}
|
||||
onChange={writeQuery}
|
||||
placeholder="Specify value"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
name="right"
|
||||
value={instance.query.right}
|
||||
onChange={writeQuery}
|
||||
placeholder="Specify value"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center my-3">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal">{'over the past'}</label>
|
||||
<Select
|
||||
className="w-2/6"
|
||||
placeholder="Select timeframe"
|
||||
options={thresholdOptions}
|
||||
name="currentPeriod"
|
||||
defaultValue={instance.currentPeriod}
|
||||
<div className="flex items-center my-3">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal">over the past</label>
|
||||
<Select
|
||||
className="w-2/6"
|
||||
placeholder="Select timeframe"
|
||||
options={thresholdOptions}
|
||||
name="currentPeriod"
|
||||
defaultValue={instance.currentPeriod}
|
||||
// onChange={ writeOption }
|
||||
onChange={({value}) => writeOption(null, {name: 'currentPeriod', value})}
|
||||
/>
|
||||
</div>
|
||||
{!isThreshold && (
|
||||
<div className="flex items-center my-3">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal">
|
||||
{'compared to previous'}
|
||||
</label>
|
||||
<Select
|
||||
className="w-2/6"
|
||||
placeholder="Select timeframe"
|
||||
options={thresholdOptions}
|
||||
name="previousPeriod"
|
||||
defaultValue={instance.previousPeriod}
|
||||
onChange={({ value }) => writeOption(null, { name: 'currentPeriod', value })}
|
||||
/>
|
||||
</div>
|
||||
{!isThreshold && (
|
||||
<div className="flex items-center my-3">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal">
|
||||
compared to previous
|
||||
</label>
|
||||
<Select
|
||||
className="w-2/6"
|
||||
placeholder="Select timeframe"
|
||||
options={thresholdOptions}
|
||||
name="previousPeriod"
|
||||
defaultValue={instance.previousPeriod}
|
||||
// onChange={ writeOption }
|
||||
onChange={({value}) => writeOption(null, {name: 'previousPeriod', value})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
onChange={({ value }) => writeOption(null, { name: 'previousPeriod', value })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<hr className="my-8"/>
|
||||
<hr className="my-8" />
|
||||
|
||||
<Section
|
||||
index="3"
|
||||
title="Notify Through"
|
||||
description="You'll be noticed in app notifications. Additionally opt in to receive alerts on:"
|
||||
content={
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center my-4">
|
||||
<Checkbox
|
||||
name="slack"
|
||||
className="mr-8"
|
||||
type="checkbox"
|
||||
checked={instance.slack}
|
||||
onClick={onChangeCheck}
|
||||
label="Slack"
|
||||
/>
|
||||
<Checkbox
|
||||
name="msteams"
|
||||
className="mr-8"
|
||||
type="checkbox"
|
||||
checked={instance.msteams}
|
||||
onClick={onChangeCheck}
|
||||
label="MS Teams"
|
||||
/>
|
||||
<Checkbox
|
||||
name="email"
|
||||
type="checkbox"
|
||||
checked={instance.email}
|
||||
onClick={onChangeCheck}
|
||||
className="mr-8"
|
||||
label="Email"
|
||||
/>
|
||||
<Checkbox
|
||||
name="webhook"
|
||||
type="checkbox"
|
||||
checked={instance.webhook}
|
||||
onClick={onChangeCheck}
|
||||
label="Webhook"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{instance.slack && (
|
||||
<div className="flex items-start my-4">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal pt-2">{'Slack'}</label>
|
||||
<div className="w-4/6">
|
||||
<DropdownChips
|
||||
fluid
|
||||
selected={instance.slackInput}
|
||||
options={slackChannels}
|
||||
placeholder="Select Channel"
|
||||
onChange={(selected) => alertsStore.edit({slackInput: selected})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{instance.msteams && (
|
||||
<div className="flex items-start my-4">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal pt-2">{'MS Teams'}</label>
|
||||
<div className="w-4/6">
|
||||
<DropdownChips
|
||||
fluid
|
||||
selected={instance.msteamsInput}
|
||||
options={msTeamsChannels}
|
||||
placeholder="Select Channel"
|
||||
onChange={(selected) => alertsStore.edit({msteamsInput: selected})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{instance.email && (
|
||||
<div className="flex items-start my-4">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal pt-2">{'Email'}</label>
|
||||
<div className="w-4/6">
|
||||
<DropdownChips
|
||||
textFiled
|
||||
validate={validateEmail}
|
||||
selected={instance.emailInput}
|
||||
placeholder="Type and press Enter key"
|
||||
onChange={(selected) => alertsStore.edit({emailInput: selected})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{instance.webhook && (
|
||||
<div className="flex items-start my-4">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal pt-2">{'Webhook'}</label>
|
||||
<DropdownChips
|
||||
fluid
|
||||
selected={instance.webhookInput}
|
||||
options={webhooks}
|
||||
placeholder="Select Webhook"
|
||||
onChange={(selected) => alertsStore.edit({webhookInput: selected})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
<Section
|
||||
index="3"
|
||||
title="Notify Through"
|
||||
description="You'll be noticed in app notifications. Additionally opt in to receive alerts on:"
|
||||
content={(
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center my-4">
|
||||
<Checkbox
|
||||
name="slack"
|
||||
className="mr-8"
|
||||
type="checkbox"
|
||||
checked={instance.slack}
|
||||
onClick={onChangeCheck}
|
||||
label="Slack"
|
||||
/>
|
||||
</div>
|
||||
<Checkbox
|
||||
name="msteams"
|
||||
className="mr-8"
|
||||
type="checkbox"
|
||||
checked={instance.msteams}
|
||||
onClick={onChangeCheck}
|
||||
label="MS Teams"
|
||||
/>
|
||||
<Checkbox
|
||||
name="email"
|
||||
type="checkbox"
|
||||
checked={instance.email}
|
||||
onClick={onChangeCheck}
|
||||
className="mr-8"
|
||||
label="Email"
|
||||
/>
|
||||
<Checkbox
|
||||
name="webhook"
|
||||
type="checkbox"
|
||||
checked={instance.webhook}
|
||||
onClick={onChangeCheck}
|
||||
label="Webhook"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex items-center justify-between absolute bottom-0 left-0 right-0 p-6 border-t z-10 bg-white">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
loading={loading}
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
disabled={loading || !instance.validate()}
|
||||
id="submit-button"
|
||||
>
|
||||
{instance.exists() ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
<div className="mx-1"/>
|
||||
<Button onClick={props.onClose}>Cancel</Button>
|
||||
{instance.slack && (
|
||||
<div className="flex items-start my-4">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal pt-2">Slack</label>
|
||||
<div className="w-4/6">
|
||||
<DropdownChips
|
||||
fluid
|
||||
selected={instance.slackInput}
|
||||
options={slackChannels}
|
||||
placeholder="Select Channel"
|
||||
onChange={(selected) => alertsStore.edit({ slackInput: selected })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{instance.exists() && (
|
||||
<Button
|
||||
hover
|
||||
primary="text"
|
||||
loading={deleting}
|
||||
type="button"
|
||||
onClick={() => onDelete(instance)}
|
||||
id="trash-button"
|
||||
>
|
||||
<Icon name="trash" color="gray-medium" size="18"/>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{instance.msteams && (
|
||||
<div className="flex items-start my-4">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal pt-2">MS Teams</label>
|
||||
<div className="w-4/6">
|
||||
<DropdownChips
|
||||
fluid
|
||||
selected={instance.msteamsInput}
|
||||
options={msTeamsChannels}
|
||||
placeholder="Select Channel"
|
||||
onChange={(selected) => alertsStore.edit({ msteamsInput: selected })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{instance.email && (
|
||||
<div className="flex items-start my-4">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal pt-2">Email</label>
|
||||
<div className="w-4/6">
|
||||
<DropdownChips
|
||||
textFiled
|
||||
validate={validateEmail}
|
||||
selected={instance.emailInput}
|
||||
placeholder="Type and press Enter key"
|
||||
onChange={(selected) => alertsStore.edit({ emailInput: selected })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{instance.webhook && (
|
||||
<div className="flex items-start my-4">
|
||||
<label className="w-2/6 flex-shrink-0 font-normal pt-2">Webhook</label>
|
||||
<DropdownChips
|
||||
fluid
|
||||
selected={instance.webhookInput}
|
||||
options={webhooks}
|
||||
placeholder="Select Webhook"
|
||||
onChange={(selected) => alertsStore.edit({ webhookInput: selected })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex items-center justify-between absolute bottom-0 left-0 right-0 p-6 border-t z-10 bg-white"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
loading={loading}
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
disabled={loading || !instance.validate()}
|
||||
id="submit-button"
|
||||
>
|
||||
{instance.exists() ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
<div className="mx-1" />
|
||||
<Button onClick={props.onClose}>Cancel</Button>
|
||||
</div>
|
||||
<div>
|
||||
{instance.exists() && (
|
||||
<Button
|
||||
hover
|
||||
primary="text"
|
||||
loading={deleting}
|
||||
type="button"
|
||||
onClick={() => onDelete(instance)}
|
||||
id="trash-button"
|
||||
>
|
||||
<Icon name="trash" color="gray-medium" size="18" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(AlertForm);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
import React, {useEffect, useState} from 'react';
|
||||
import {SlideModal} from 'UI';
|
||||
import {useStore} from 'App/mstore'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { SlideModal, confirm } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { SLACK, TEAMS, WEBHOOK } from 'App/constants/schedule';
|
||||
import AlertForm from '../AlertForm';
|
||||
import {SLACK, TEAMS, WEBHOOK} from 'App/constants/schedule';
|
||||
import {confirm} from 'UI';
|
||||
|
||||
interface Select {
|
||||
label: string;
|
||||
value: string | number
|
||||
}
|
||||
|
||||
|
||||
interface Props {
|
||||
showModal?: boolean;
|
||||
metricId?: number;
|
||||
|
|
@ -19,77 +17,76 @@ interface Props {
|
|||
}
|
||||
|
||||
function AlertFormModal(props: Props) {
|
||||
const {alertsStore, settingsStore} = useStore()
|
||||
const {metricId = null, showModal = false} = props;
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const webhooks = settingsStore.webhooks
|
||||
useEffect(() => {
|
||||
settingsStore.fetchWebhooks();
|
||||
}, []);
|
||||
const { alertsStore, settingsStore } = useStore();
|
||||
const { metricId = null, showModal = false } = props;
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const { webhooks } = settingsStore;
|
||||
useEffect(() => {
|
||||
settingsStore.fetchWebhooks();
|
||||
}, []);
|
||||
|
||||
const slackChannels: Select[] = [];
|
||||
const hooks: Select[] = [];
|
||||
const msTeamsChannels: Select[] = [];
|
||||
|
||||
const slackChannels: Select[] = []
|
||||
const hooks: Select[] = []
|
||||
const msTeamsChannels: Select[] = []
|
||||
webhooks.forEach((hook) => {
|
||||
const option = { value: hook.webhookId, label: hook.name };
|
||||
if (hook.type === SLACK) {
|
||||
slackChannels.push(option);
|
||||
}
|
||||
if (hook.type === WEBHOOK) {
|
||||
hooks.push(option);
|
||||
}
|
||||
if (hook.type === TEAMS) {
|
||||
msTeamsChannels.push(option);
|
||||
}
|
||||
});
|
||||
|
||||
webhooks.forEach((hook) => {
|
||||
const option = {value: hook.webhookId, label: hook.name}
|
||||
if (hook.type === SLACK) {
|
||||
slackChannels.push(option)
|
||||
}
|
||||
if (hook.type === WEBHOOK) {
|
||||
hooks.push(option)
|
||||
}
|
||||
if (hook.type === TEAMS) {
|
||||
msTeamsChannels.push(option)
|
||||
}
|
||||
})
|
||||
const saveAlert = (instance) => {
|
||||
const wasUpdating = instance.exists();
|
||||
alertsStore.save(instance).then(() => {
|
||||
if (!wasUpdating) {
|
||||
toggleForm(null, false);
|
||||
}
|
||||
if (props.onClose) {
|
||||
props.onClose();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const saveAlert = (instance) => {
|
||||
const wasUpdating = instance.exists();
|
||||
alertsStore.save(instance).then(() => {
|
||||
if (!wasUpdating) {
|
||||
toggleForm(null, false);
|
||||
}
|
||||
if (props.onClose) {
|
||||
props.onClose();
|
||||
}
|
||||
});
|
||||
};
|
||||
const onDelete = async (instance) => {
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: 'Are you sure you want to permanently delete this alert?',
|
||||
})
|
||||
) {
|
||||
alertsStore.remove(instance.alertId).then(() => {
|
||||
toggleForm(null, false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = async (instance) => {
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this alert?`,
|
||||
})
|
||||
) {
|
||||
alertsStore.remove(instance.alertId).then(() => {
|
||||
toggleForm(null, false);
|
||||
});
|
||||
}
|
||||
};
|
||||
const toggleForm = (instance, state) => {
|
||||
if (instance) {
|
||||
alertsStore.init(instance);
|
||||
}
|
||||
return setShowForm(state || !showForm);
|
||||
};
|
||||
|
||||
const toggleForm = (instance, state) => {
|
||||
if (instance) {
|
||||
alertsStore.init(instance);
|
||||
}
|
||||
return setShowForm(state ? state : !showForm);
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertForm
|
||||
metricId={metricId}
|
||||
edit={alertsStore.edit}
|
||||
slackChannels={slackChannels}
|
||||
msTeamsChannels={msTeamsChannels}
|
||||
webhooks={hooks}
|
||||
onSubmit={saveAlert}
|
||||
onClose={props.onClose}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<AlertForm
|
||||
metricId={metricId}
|
||||
edit={alertsStore.edit}
|
||||
slackChannels={slackChannels}
|
||||
msTeamsChannels={msTeamsChannels}
|
||||
webhooks={hooks}
|
||||
onSubmit={saveAlert}
|
||||
onClose={props.onClose}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(AlertFormModal);
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './AlertFormModal';
|
||||
export { default } from './AlertFormModal';
|
||||
|
|
|
|||
|
|
@ -2,65 +2,65 @@ import React from 'react';
|
|||
import { Input, TagBadge } from 'UI';
|
||||
import Select from 'Shared/Select';
|
||||
|
||||
const DropdownChips = ({
|
||||
textFiled = false,
|
||||
validate = null,
|
||||
placeholder = '',
|
||||
selected = [],
|
||||
options = [],
|
||||
badgeClassName = 'lowercase',
|
||||
onChange = () => null,
|
||||
...props
|
||||
}) => {
|
||||
const onRemove = (id) => {
|
||||
onChange(selected.filter((i) => i !== id));
|
||||
};
|
||||
function DropdownChips({
|
||||
textFiled = false,
|
||||
validate = null,
|
||||
placeholder = '',
|
||||
selected = [],
|
||||
options = [],
|
||||
badgeClassName = 'lowercase',
|
||||
onChange = () => null,
|
||||
...props
|
||||
}) {
|
||||
const onRemove = (id) => {
|
||||
onChange(selected.filter((i) => i !== id));
|
||||
};
|
||||
|
||||
const onSelect = ({ value }) => {
|
||||
const newSlected = selected.concat(value.value);
|
||||
onChange(newSlected);
|
||||
};
|
||||
const onSelect = ({ value }) => {
|
||||
const newSlected = selected.concat(value.value);
|
||||
onChange(newSlected);
|
||||
};
|
||||
|
||||
const onKeyPress = (e) => {
|
||||
const val = e.target.value;
|
||||
if (e.key !== 'Enter' || selected.includes(val)) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (validate && !validate(val)) return;
|
||||
const onKeyPress = (e) => {
|
||||
const val = e.target.value;
|
||||
if (e.key !== 'Enter' || selected.includes(val)) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (validate && !validate(val)) return;
|
||||
|
||||
const newSlected = selected.concat(val);
|
||||
e.target.value = '';
|
||||
onChange(newSlected);
|
||||
};
|
||||
const newSlected = selected.concat(val);
|
||||
e.target.value = '';
|
||||
onChange(newSlected);
|
||||
};
|
||||
|
||||
const _options = options.filter((item) => !selected.includes(item.value));
|
||||
const _options = options.filter((item) => !selected.includes(item.value));
|
||||
|
||||
const renderBadge = (item) => {
|
||||
const val = typeof item === 'string' ? item : item.value;
|
||||
const text = typeof item === 'string' ? item : item.label;
|
||||
return <TagBadge className={badgeClassName} key={text} text={text} hashed={false} onRemove={() => onRemove(val)} outline={true} />;
|
||||
};
|
||||
const renderBadge = (item) => {
|
||||
const val = typeof item === 'string' ? item : item.value;
|
||||
const text = typeof item === 'string' ? item : item.label;
|
||||
return <TagBadge className={badgeClassName} key={text} text={text} hashed={false} onRemove={() => onRemove(val)} outline />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{textFiled ? (
|
||||
<Input type="text" onKeyPress={onKeyPress} placeholder={placeholder} />
|
||||
) : (
|
||||
<Select
|
||||
placeholder={placeholder}
|
||||
isSearchable={true}
|
||||
options={_options}
|
||||
name="webhookInput"
|
||||
value={null}
|
||||
onChange={onSelect}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-wrap mt-3">
|
||||
{textFiled ? selected.map(renderBadge) : options.filter((i) => selected.includes(i.value)).map(renderBadge)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div className="w-full">
|
||||
{textFiled ? (
|
||||
<Input type="text" onKeyPress={onKeyPress} placeholder={placeholder} />
|
||||
) : (
|
||||
<Select
|
||||
placeholder={placeholder}
|
||||
isSearchable
|
||||
options={_options}
|
||||
name="webhookInput"
|
||||
value={null}
|
||||
onChange={onSelect}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-wrap mt-3">
|
||||
{textFiled ? selected.map(renderBadge) : options.filter((i) => selected.includes(i.value)).map(renderBadge)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DropdownChips;
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './DropdownChips'
|
||||
export { default } from './DropdownChips';
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { observer } from 'mobx-react-lite';
|
|||
import { Badge, Button, Tooltip } from 'antd';
|
||||
import { BellOutlined } from '@ant-design/icons';
|
||||
|
||||
|
||||
const AUTOREFRESH_INTERVAL = 5 * 60 * 1000;
|
||||
|
||||
function Notifications() {
|
||||
|
|
@ -28,16 +27,17 @@ function Notifications() {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<Badge dot={count > 0} size='small'>
|
||||
<Tooltip title='Alerts'>
|
||||
<Badge dot={count > 0} size="small">
|
||||
<Tooltip title="Alerts">
|
||||
<Button
|
||||
icon={<BellOutlined />}
|
||||
onClick={() => showModal(<AlertTriggersModal />, { right: true })}>
|
||||
{/*<Icon name='bell' size='18' color='gray-dark' />*/}
|
||||
onClick={() => showModal(<AlertTriggersModal />, { right: true })}
|
||||
>
|
||||
{/* <Icon name='bell' size='18' color='gray-dark' /> */}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(Notifications);
|
||||
export default observer(Notifications);
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './Notifications';
|
||||
export { default } from './Notifications';
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ function Assist() {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
export default withPageTitle('Assist - OpenReplay')(
|
||||
withPermissions(['ASSIST_LIVE', 'SERVICE_ASSIST_LIVE'], '', false, false)(Assist)
|
||||
withPermissions(['ASSIST_LIVE', 'SERVICE_ASSIST_LIVE'], '', false, false)(Assist),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import AssistView from './AssistView'
|
||||
import AssistView from './AssistView';
|
||||
|
||||
function AssistRouter() {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -3,19 +3,17 @@ import { Button, Tooltip } from 'antd';
|
|||
import { useModal } from 'App/components/Modal';
|
||||
import { MODULES } from 'Components/Client/Modules';
|
||||
|
||||
import AssistStats from '../../AssistStats';
|
||||
import Recordings from '../RecordingsList/Recordings';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import AssistStats from '../../AssistStats';
|
||||
import Recordings from '../RecordingsList/Recordings';
|
||||
|
||||
function AssistSearchActions() {
|
||||
const { searchStoreLive, userStore } = useStore();
|
||||
const modules = userStore.account.settings?.modules ?? [];
|
||||
const isEnterprise = userStore.isEnterprise
|
||||
const hasEvents =
|
||||
searchStoreLive.instance.filters.filter((i: any) => i.isEvent).length > 0;
|
||||
const hasFilters =
|
||||
searchStoreLive.instance.filters.filter((i: any) => !i.isEvent).length > 0;
|
||||
const { isEnterprise } = userStore;
|
||||
const hasEvents = searchStoreLive.instance.filters.filter((i: any) => i.isEvent).length > 0;
|
||||
const hasFilters = searchStoreLive.instance.filters.filter((i: any) => !i.isEvent).length > 0;
|
||||
const { showModal } = useModal();
|
||||
|
||||
const showStats = () => {
|
||||
|
|
@ -27,14 +25,17 @@ function AssistSearchActions() {
|
|||
return (
|
||||
<div className="flex items-center w-full gap-2">
|
||||
{isEnterprise && !modules.includes(MODULES.OFFLINE_RECORDINGS)
|
||||
? <Button type="text" onClick={showRecords}>Training Videos</Button> : null
|
||||
}
|
||||
? <Button type="text" onClick={showRecords}>Training Videos</Button> : null}
|
||||
{isEnterprise && userStore.account?.admin && (
|
||||
<Button type="text" onClick={showStats}
|
||||
disabled={modules.includes(MODULES.ASSIST_STATS) || modules.includes(MODULES.ASSIST)}>
|
||||
Co-Browsing Reports</Button>
|
||||
<Button
|
||||
type="text"
|
||||
onClick={showStats}
|
||||
disabled={modules.includes(MODULES.ASSIST_STATS) || modules.includes(MODULES.ASSIST)}
|
||||
>
|
||||
Co-Browsing Reports
|
||||
</Button>
|
||||
)}
|
||||
<Tooltip title='Clear Search Filters'>
|
||||
<Tooltip title="Clear Search Filters">
|
||||
<Button
|
||||
type="text"
|
||||
disabled={!hasFilters && !hasEvents}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './AssistSearchActions'
|
||||
export { default } from './AssistSearchActions';
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
import React from 'react';
|
||||
import LiveSessionList from 'Shared/LiveSessionList';
|
||||
import LiveSessionSearch from 'Shared/LiveSessionSearch';
|
||||
import AssistSearchActions from './AssistSearchActions';
|
||||
import usePageTitle from '@/hooks/usePageTitle';
|
||||
import AssistSearchActions from './AssistSearchActions';
|
||||
|
||||
function AssistView() {
|
||||
usePageTitle('Co-Browse - OpenReplay');
|
||||
return (
|
||||
<div className="w-full mx-auto" style={{ maxWidth: '1360px'}}>
|
||||
<div className="w-full mx-auto" style={{ maxWidth: '1360px' }}>
|
||||
<AssistSearchActions />
|
||||
<LiveSessionSearch />
|
||||
<div className="my-4" />
|
||||
<LiveSessionList />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default AssistView;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import React, { useState } from 'react'
|
||||
import stl from './ChatControls.module.css'
|
||||
import cn from 'classnames'
|
||||
import { Icon } from 'UI'
|
||||
import { Button } from 'antd'
|
||||
import React, { useState } from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Icon } from 'UI';
|
||||
import { Button } from 'antd';
|
||||
import type { LocalStream } from 'Player';
|
||||
import stl from './ChatControls.module.css';
|
||||
|
||||
interface Props {
|
||||
stream: LocalStream | null,
|
||||
|
|
@ -12,39 +12,41 @@ interface Props {
|
|||
isPrestart?: boolean,
|
||||
setVideoEnabled: (isEnabled: boolean) => void
|
||||
}
|
||||
function ChatControls({ stream, endCall, videoEnabled, setVideoEnabled, isPrestart } : Props) {
|
||||
const [audioEnabled, setAudioEnabled] = useState(true)
|
||||
function ChatControls({
|
||||
stream, endCall, videoEnabled, setVideoEnabled, isPrestart,
|
||||
} : Props) {
|
||||
const [audioEnabled, setAudioEnabled] = useState(true);
|
||||
|
||||
const toggleAudio = () => {
|
||||
if (!stream) { return; }
|
||||
setAudioEnabled(stream.toggleAudio());
|
||||
}
|
||||
};
|
||||
|
||||
const toggleVideo = () => {
|
||||
if (!stream) { return; }
|
||||
stream.toggleVideo()
|
||||
.then((v) => setVideoEnabled(v))
|
||||
}
|
||||
.then((v) => setVideoEnabled(v));
|
||||
};
|
||||
|
||||
/** muting user if he is auto connected to the call */
|
||||
React.useEffect(() => {
|
||||
if (isPrestart) {
|
||||
audioEnabled && toggleAudio();
|
||||
}
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={cn(stl.controls, "flex items-center w-full justify-start bottom-0 px-2")}>
|
||||
<div className={cn(stl.controls, 'flex items-center w-full justify-start bottom-0 px-2')}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={cn(stl.btnWrapper, { [stl.disabled]: audioEnabled})}>
|
||||
<Button size={'small'} variant="text" onClick={toggleAudio} icon={<Icon name={audioEnabled ? 'mic' : 'mic-mute'} size="16" />}>
|
||||
<span className={cn("ml-1 color-gray-medium text-sm", { 'color-red' : audioEnabled })}>{audioEnabled ? 'Mute' : 'Unmute'}</span>
|
||||
<div className={cn(stl.btnWrapper, { [stl.disabled]: audioEnabled })}>
|
||||
<Button size="small" variant="text" onClick={toggleAudio} icon={<Icon name={audioEnabled ? 'mic' : 'mic-mute'} size="16" />}>
|
||||
<span className={cn('ml-1 color-gray-medium text-sm', { 'color-red': audioEnabled })}>{audioEnabled ? 'Mute' : 'Unmute'}</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={cn(stl.btnWrapper, { [stl.disabled]: videoEnabled})}>
|
||||
<Button size={'small'} variant="text" onClick={toggleVideo} icon={<Icon name={ videoEnabled ? 'camera-video' : 'camera-video-off' } size="16" />}>
|
||||
<span className={cn("ml-1 color-gray-medium text-sm", { 'color-red' : videoEnabled })}>{videoEnabled ? 'Stop Video' : 'Start Video'}</span>
|
||||
<div className={cn(stl.btnWrapper, { [stl.disabled]: videoEnabled })}>
|
||||
<Button size="small" variant="text" onClick={toggleVideo} icon={<Icon name={videoEnabled ? 'camera-video' : 'camera-video-off'} size="16" />}>
|
||||
<span className={cn('ml-1 color-gray-medium text-sm', { 'color-red': videoEnabled })}>{videoEnabled ? 'Stop Video' : 'Start Video'}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -54,7 +56,7 @@ function ChatControls({ stream, endCall, videoEnabled, setVideoEnabled, isPresta
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default ChatControls
|
||||
export default ChatControls;
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './ChatControls'
|
||||
export { default } from './ChatControls';
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import VideoContainer from '../components/VideoContainer';
|
||||
import cn from 'classnames';
|
||||
import Counter from 'App/components/shared/SessionItem/Counter';
|
||||
import stl from './chatWindow.module.css';
|
||||
import ChatControls from '../ChatControls/ChatControls';
|
||||
import Draggable from 'react-draggable';
|
||||
import type { LocalStream } from 'Player';
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
import ChatControls from '../ChatControls/ChatControls';
|
||||
import stl from './chatWindow.module.css';
|
||||
import VideoContainer from '../components/VideoContainer';
|
||||
|
||||
export interface Props {
|
||||
incomeStream: MediaStream[] | null;
|
||||
|
|
@ -16,10 +16,12 @@ export interface Props {
|
|||
endCall: () => void;
|
||||
}
|
||||
|
||||
function ChatWindow({ userId, incomeStream, localStream, endCall, isPrestart }: Props) {
|
||||
const { player } = React.useContext(PlayerContext)
|
||||
function ChatWindow({
|
||||
userId, incomeStream, localStream, endCall, isPrestart,
|
||||
}: Props) {
|
||||
const { player } = React.useContext(PlayerContext);
|
||||
|
||||
const toggleVideoLocalStream = player.assistManager.toggleVideoLocalStream;
|
||||
const { toggleVideoLocalStream } = player.assistManager;
|
||||
|
||||
const [localVideoEnabled, setLocalVideoEnabled] = useState(false);
|
||||
const [anyRemoteEnabled, setRemoteEnabled] = useState(false);
|
||||
|
|
@ -27,8 +29,8 @@ function ChatWindow({ userId, incomeStream, localStream, endCall, isPrestart }:
|
|||
const onlyLocalEnabled = localVideoEnabled && !anyRemoteEnabled;
|
||||
|
||||
useEffect(() => {
|
||||
toggleVideoLocalStream(localVideoEnabled)
|
||||
}, [localVideoEnabled])
|
||||
toggleVideoLocalStream(localVideoEnabled);
|
||||
}, [localVideoEnabled]);
|
||||
|
||||
return (
|
||||
<Draggable handle=".handle" bounds="body" defaultPosition={{ x: 50, y: 200 }}>
|
||||
|
|
@ -38,7 +40,9 @@ function ChatWindow({ userId, incomeStream, localStream, endCall, isPrestart }:
|
|||
>
|
||||
<div className="handle flex items-center p-2 cursor-move select-none border-b">
|
||||
<div className={stl.headerTitle}>
|
||||
<b>Call with </b> {userId ? userId : 'Anonymous User'}
|
||||
<b>Call with </b>
|
||||
{' '}
|
||||
{userId || 'Anonymous User'}
|
||||
<br />
|
||||
{incomeStream && incomeStream.length > 2 ? ' (+ other agents in the call)' : ''}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './ChatWindow'
|
||||
export { default } from './ChatWindow';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { useObserver } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import { Modal, Form, Icon, Input } from 'UI';
|
||||
import { Button } from 'antd'
|
||||
import {
|
||||
Modal, Form, Icon, Input,
|
||||
} from 'UI';
|
||||
import { Button } from 'antd';
|
||||
|
||||
interface Props {
|
||||
show: boolean;
|
||||
|
|
@ -10,65 +12,67 @@ interface Props {
|
|||
onSave: (title: string) => void;
|
||||
}
|
||||
function EditRecordingModal(props: Props) {
|
||||
const { show, closeHandler, title, onSave } = props;
|
||||
const [text, setText] = React.useState(title)
|
||||
const {
|
||||
show, closeHandler, title, onSave,
|
||||
} = props;
|
||||
const [text, setText] = React.useState(title);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleEsc = (e: any) => e.key === 'Escape' && closeHandler?.()
|
||||
document.addEventListener("keydown", handleEsc, false);
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleEsc, false);
|
||||
}
|
||||
}, [])
|
||||
React.useEffect(() => {
|
||||
const handleEsc = (e: any) => e.key === 'Escape' && closeHandler?.();
|
||||
document.addEventListener('keydown', handleEsc, false);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleEsc, false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const write = ({ target: { value, name } }: any) => setText(value)
|
||||
const write = ({ target: { value, name } }: any) => setText(value);
|
||||
|
||||
const save = () => {
|
||||
onSave(text)
|
||||
}
|
||||
return useObserver(() => (
|
||||
<Modal open={ show } onClose={closeHandler}>
|
||||
<Modal.Header className="flex items-center justify-between">
|
||||
<div>{ 'Rename' }</div>
|
||||
<div onClick={ closeHandler }>
|
||||
<Icon
|
||||
color="gray-dark"
|
||||
size="14"
|
||||
name="close"
|
||||
/>
|
||||
</div>
|
||||
</Modal.Header>
|
||||
const save = () => {
|
||||
onSave(text);
|
||||
};
|
||||
return useObserver(() => (
|
||||
<Modal open={show} onClose={closeHandler}>
|
||||
<Modal.Header className="flex items-center justify-between">
|
||||
<div>Rename</div>
|
||||
<div onClick={closeHandler}>
|
||||
<Icon
|
||||
color="gray-dark"
|
||||
size="14"
|
||||
name="close"
|
||||
/>
|
||||
</div>
|
||||
</Modal.Header>
|
||||
|
||||
<Modal.Content>
|
||||
<Form onSubmit={save}>
|
||||
<Form.Field>
|
||||
<label>{'Title:'}</label>
|
||||
<Input
|
||||
className=""
|
||||
name="name"
|
||||
value={ text }
|
||||
onChange={write}
|
||||
placeholder="Title"
|
||||
maxLength={100}
|
||||
autoFocus
|
||||
/>
|
||||
</Form.Field>
|
||||
</Form>
|
||||
</Modal.Content>
|
||||
<Modal.Footer>
|
||||
<div className="-mx-2 px-2">
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={ save }
|
||||
className="float-left mr-2"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button className="mr-2" onClick={ closeHandler }>{ 'Cancel' }</Button>
|
||||
</div>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
));
|
||||
<Modal.Content>
|
||||
<Form onSubmit={save}>
|
||||
<Form.Field>
|
||||
<label>Title:</label>
|
||||
<Input
|
||||
className=""
|
||||
name="name"
|
||||
value={text}
|
||||
onChange={write}
|
||||
placeholder="Title"
|
||||
maxLength={100}
|
||||
autoFocus
|
||||
/>
|
||||
</Form.Field>
|
||||
</Form>
|
||||
</Modal.Content>
|
||||
<Modal.Footer>
|
||||
<div className="-mx-2 px-2">
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={save}
|
||||
className="float-left mr-2"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button className="mr-2" onClick={closeHandler}>Cancel</Button>
|
||||
</div>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
));
|
||||
}
|
||||
|
||||
export default EditRecordingModal;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import React from 'react';
|
||||
import { PageTitle } from 'UI';
|
||||
import Select from 'Shared/Select';
|
||||
import RecordingsSearch from './RecordingsSearch';
|
||||
import RecordingsList from './RecordingsList';
|
||||
import { useStore } from 'App/mstore';
|
||||
import SelectDateRange from 'Shared/SelectDateRange/SelectDateRange';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import RecordingsList from './RecordingsList';
|
||||
import RecordingsSearch from './RecordingsSearch';
|
||||
|
||||
function Recordings() {
|
||||
const { recordingsStore, userStore } = useStore();
|
||||
|
|
@ -13,7 +13,7 @@ function Recordings() {
|
|||
|
||||
const recordingsOwner = [
|
||||
{ value: '0', label: 'All Videos' },
|
||||
{ value: userId, label: 'My Videos' }
|
||||
{ value: userId, label: 'My Videos' },
|
||||
];
|
||||
|
||||
const onDateChange = (e: any) => {
|
||||
|
|
@ -21,22 +21,22 @@ function Recordings() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '1360px', margin: 'auto' }} className='bg-white rounded-lg py-4 border h-screen overflow-y-scroll'>
|
||||
<div className='flex items-center mb-4 justify-between px-6'>
|
||||
<div className='flex items-baseline mr-3'>
|
||||
<PageTitle title='Training Videos' />
|
||||
<div style={{ maxWidth: '1360px', margin: 'auto' }} className="bg-white rounded-lg py-4 border h-screen overflow-y-scroll">
|
||||
<div className="flex items-center mb-4 justify-between px-6">
|
||||
<div className="flex items-baseline mr-3">
|
||||
<PageTitle title="Training Videos" />
|
||||
</div>
|
||||
<div className='ml-auto flex items-center gap-4'>
|
||||
<SelectDateRange period={recordingsStore.period} onChange={onDateChange} right={true} />
|
||||
<div className="ml-auto flex items-center gap-4">
|
||||
<SelectDateRange period={recordingsStore.period} onChange={onDateChange} right />
|
||||
<Select
|
||||
name='recsOwner'
|
||||
name="recsOwner"
|
||||
plain
|
||||
right
|
||||
options={recordingsOwner}
|
||||
onChange={({ value }) => recordingsStore.setUserId(value.value)}
|
||||
defaultValue={recordingsOwner[0].value}
|
||||
/>
|
||||
<div className='w-1/4' style={{ minWidth: 300 }}>
|
||||
<div className="w-1/4" style={{ minWidth: 300 }}>
|
||||
<RecordingsSearch />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,28 +2,28 @@ import { observer } from 'mobx-react-lite';
|
|||
import React from 'react';
|
||||
import { NoContent, Pagination, Loader } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import RecordsListItem from './RecordsListItem';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
import RecordsListItem from './RecordsListItem';
|
||||
|
||||
function RecordingsList() {
|
||||
const { recordingsStore } = useStore();
|
||||
// const [shownRecordings, setRecordings] = React.useState<any[]>([]);
|
||||
const recordings = recordingsStore.recordings;
|
||||
const { recordings } = recordingsStore;
|
||||
const recordsSearch = recordingsStore.search;
|
||||
const page = recordingsStore.page;
|
||||
const pageSize = recordingsStore.pageSize;
|
||||
const total = recordingsStore.total;
|
||||
const { page } = recordingsStore;
|
||||
const { pageSize } = recordingsStore;
|
||||
const { total } = recordingsStore;
|
||||
|
||||
React.useEffect(() => {
|
||||
recordingsStore.fetchRecordings();
|
||||
}, [page, recordingsStore.period, recordsSearch, recordingsStore.userId]);
|
||||
|
||||
const length = recordings.length;
|
||||
const { length } = recordings;
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
show={length === 0}
|
||||
title={
|
||||
title={(
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.NO_RECORDINGS} size={60} />
|
||||
<div className="text-center mt-4">
|
||||
|
|
@ -32,14 +32,14 @@ function RecordingsList() {
|
|||
: 'No videos have been recorded in your co-browsing sessions.'}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
subtext={
|
||||
)}
|
||||
subtext={(
|
||||
<div className="text-center flex justify-center items-center flex-col">
|
||||
<span>
|
||||
Capture and share video recordings of co-browsing sessions with your team for product feedback and training.
|
||||
Capture and share video recordings of co-browsing sessions with your team for product feedback and training.
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="mt-3 border-b">
|
||||
<Loader loading={recordingsStore.loading}>
|
||||
|
|
@ -58,8 +58,15 @@ function RecordingsList() {
|
|||
|
||||
<div className="w-full flex items-center justify-between pt-4 px-6">
|
||||
<div className="text-disabled-text">
|
||||
Showing <span className="font-semibold">{Math.min(length, pageSize)}</span> out of{' '}
|
||||
<span className="font-semibold">{total}</span> Recording
|
||||
Showing
|
||||
{' '}
|
||||
<span className="font-semibold">{Math.min(length, pageSize)}</span>
|
||||
{' '}
|
||||
out of
|
||||
{' '}
|
||||
<span className="font-semibold">{total}</span>
|
||||
{' '}
|
||||
Recording
|
||||
</div>
|
||||
<Pagination
|
||||
page={page}
|
||||
|
|
|
|||
|
|
@ -4,33 +4,33 @@ import { useStore } from 'App/mstore';
|
|||
import { Icon } from 'UI';
|
||||
import { debounce } from 'App/utils';
|
||||
|
||||
let debounceUpdate: any = () => {}
|
||||
let debounceUpdate: any = () => {};
|
||||
|
||||
function RecordingsSearch() {
|
||||
const { recordingsStore } = useStore();
|
||||
const [query, setQuery] = useState(recordingsStore.search);
|
||||
useEffect(() => {
|
||||
debounceUpdate = debounce((value: any) => recordingsStore.updateSearch(value), 500);
|
||||
}, [])
|
||||
const { recordingsStore } = useStore();
|
||||
const [query, setQuery] = useState(recordingsStore.search);
|
||||
useEffect(() => {
|
||||
debounceUpdate = debounce((value: any) => recordingsStore.updateSearch(value), 500);
|
||||
}, []);
|
||||
|
||||
// @ts-ignore
|
||||
const write = ({ target: { value } }) => {
|
||||
setQuery(value);
|
||||
debounceUpdate(value);
|
||||
}
|
||||
// @ts-ignore
|
||||
const write = ({ target: { value } }) => {
|
||||
setQuery(value);
|
||||
debounceUpdate(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Icon name="search" className="absolute top-0 bottom-0 ml-2 m-auto" size="16" />
|
||||
<input
|
||||
value={query}
|
||||
name="recordsSearch"
|
||||
className="bg-white p-2 border border-borderColor-gray-light-shade rounded w-full pl-10"
|
||||
placeholder="Filter by title or description"
|
||||
onChange={write}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="relative">
|
||||
<Icon name="search" className="absolute top-0 bottom-0 ml-2 m-auto" size="16" />
|
||||
<input
|
||||
value={query}
|
||||
name="recordsSearch"
|
||||
className="bg-white p-2 border border-borderColor-gray-light-shade rounded w-full pl-10"
|
||||
placeholder="Filter by title or description"
|
||||
onChange={write}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(RecordingsSearch);
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ function RecordsListItem(props: Props) {
|
|||
const onDelete = () => {
|
||||
recordingsStore.deleteRecording(record.recordId).then(() => {
|
||||
recordingsStore.setRecordings(
|
||||
recordingsStore.recordings.filter((rec) => rec.recordId !== record.recordId)
|
||||
recordingsStore.recordings.filter((rec) => rec.recordId !== record.recordId),
|
||||
);
|
||||
toast.success('Recording deleted');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import React from 'react';
|
||||
import { INDEXES } from 'App/constants/zindex';
|
||||
import { Loader, Icon } from 'UI';
|
||||
import { Button } from 'antd'
|
||||
import { Button } from 'antd';
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
import { useStore } from "App/mstore";
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -41,30 +41,29 @@ const WIN_VARIANTS = {
|
|||
icon: 'record-circle' as const,
|
||||
iconColor: 'red',
|
||||
action: Actions.RecordingEnd,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function RequestingWindow({ getWindowType }: Props) {
|
||||
const { sessionStore } = useStore();
|
||||
const userDisplayName = sessionStore.current.userDisplayName;
|
||||
const windowType = getWindowType()
|
||||
const { userDisplayName } = sessionStore.current;
|
||||
const windowType = getWindowType();
|
||||
if (!windowType) return;
|
||||
const { player } = React.useContext(PlayerContext)
|
||||
|
||||
const { player } = React.useContext(PlayerContext);
|
||||
|
||||
const {
|
||||
assistManager: {
|
||||
initiateCallEnd,
|
||||
releaseRemoteControl,
|
||||
stopRecording,
|
||||
}
|
||||
} = player
|
||||
},
|
||||
} = player;
|
||||
|
||||
const actions = {
|
||||
[Actions.CallEnd]: initiateCallEnd,
|
||||
[Actions.ControlEnd]: releaseRemoteControl,
|
||||
[Actions.RecordingEnd]: stopRecording,
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full absolute top-0 left-0 flex items-center justify-center"
|
||||
|
|
@ -73,7 +72,9 @@ function RequestingWindow({ getWindowType }: Props) {
|
|||
<div className="rounded bg-white pt-4 pb-2 px-8 flex flex-col text-lg items-center max-w-lg text-center">
|
||||
<Icon size={40} color={WIN_VARIANTS[windowType].iconColor} name={WIN_VARIANTS[windowType].icon} className="mb-4" />
|
||||
<div>
|
||||
Waiting for <span className="font-semibold">{userDisplayName}</span>
|
||||
Waiting for
|
||||
{' '}
|
||||
<span className="font-semibold">{userDisplayName}</span>
|
||||
</div>
|
||||
<span>{WIN_VARIANTS[windowType].text}</span>
|
||||
<Loader size={30} style={{ minHeight: 60 }} />
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default, WindowType } from './RequestingWindow'
|
||||
export { default, WindowType } from './RequestingWindow';
|
||||
|
|
|
|||
|
|
@ -1,20 +1,22 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Button } from 'antd';
|
||||
import cn from 'classnames';
|
||||
import ChatWindow from '../../ChatWindow';
|
||||
import { CallingState, ConnectionStatus, RemoteControlStatus, RequestLocalStream } from 'Player';
|
||||
import {
|
||||
CallingState, ConnectionStatus, RemoteControlStatus, RequestLocalStream,
|
||||
} from 'Player';
|
||||
import type { LocalStream } from 'Player';
|
||||
import { PlayerContext, ILivePlayerContext } from 'App/components/Session/playerContext';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { toast } from 'react-toastify';
|
||||
import { confirm, Icon, Tooltip } 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";
|
||||
import { useStore } from 'App/mstore';
|
||||
import stl from './AassistActions.module.css';
|
||||
import ChatWindow from '../../ChatWindow';
|
||||
|
||||
function onReject() {
|
||||
toast.info(`Call was rejected.`);
|
||||
toast.info('Call was rejected.');
|
||||
}
|
||||
|
||||
function onControlReject() {
|
||||
|
|
@ -39,13 +41,13 @@ interface Props {
|
|||
const AssistActionsPing = {
|
||||
control: {
|
||||
start: 's_control_started',
|
||||
end: 's_control_ended'
|
||||
end: 's_control_ended',
|
||||
},
|
||||
call: {
|
||||
start: 's_call_started',
|
||||
end: 's_call_ended'
|
||||
end: 's_call_ended',
|
||||
},
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
function AssistActions({
|
||||
userId,
|
||||
|
|
@ -57,9 +59,9 @@ function AssistActions({
|
|||
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 { isEnterprise } = userStore;
|
||||
const agentId = userStore.account.id;
|
||||
const userDisplayName = sessionStore.current.userDisplayName;
|
||||
const { userDisplayName } = sessionStore.current;
|
||||
|
||||
const {
|
||||
assistManager: {
|
||||
|
|
@ -82,12 +84,11 @@ function AssistActions({
|
|||
const [isPrestart, setPrestart] = useState(false);
|
||||
const [incomeStream, setIncomeStream] = useState<MediaStream[] | null>([]);
|
||||
const [localStream, setLocalStream] = useState<LocalStream | null>(null);
|
||||
const [callObject, setCallObject] = useState<{ end: () => void } | null>(null);
|
||||
const [callObject, setCallObject] = useState<{ end:() => void } | null>(null);
|
||||
|
||||
const onCall = calling === CallingState.OnCall || calling === CallingState.Reconnecting;
|
||||
const callRequesting = calling === CallingState.Connecting;
|
||||
const cannotCall =
|
||||
peerConnectionStatus !== ConnectionStatus.Connected || (isEnterprise && !hasPermission);
|
||||
const cannotCall = peerConnectionStatus !== ConnectionStatus.Connected || (isEnterprise && !hasPermission);
|
||||
|
||||
const remoteRequesting = remoteControlStatus === RemoteControlStatus.Requesting;
|
||||
const remoteActive = remoteControlStatus === RemoteControlStatus.Enabled;
|
||||
|
|
@ -121,13 +122,11 @@ function AssistActions({
|
|||
}
|
||||
}, [remoteActive]);
|
||||
|
||||
useEffect(() => {
|
||||
return callObject?.end();
|
||||
}, []);
|
||||
useEffect(() => callObject?.end(), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (peerConnectionStatus == ConnectionStatus.Disconnected) {
|
||||
toast.info(`Live session was closed.`);
|
||||
toast.info('Live session was closed.');
|
||||
}
|
||||
}, [peerConnectionStatus]);
|
||||
|
||||
|
|
@ -151,11 +150,11 @@ function AssistActions({
|
|||
lStream,
|
||||
addIncomeStream,
|
||||
() => {
|
||||
player.assistManager.ping(AssistActionsPing.call.end, agentId)
|
||||
player.assistManager.ping(AssistActionsPing.call.end, agentId);
|
||||
lStream.stop.bind(lStream);
|
||||
},
|
||||
onReject,
|
||||
onError
|
||||
onError,
|
||||
);
|
||||
setCallObject(callPeer());
|
||||
if (additionalAgentIds) {
|
||||
|
|
@ -172,7 +171,7 @@ function AssistActions({
|
|||
await confirm({
|
||||
header: 'Start Call',
|
||||
confirmButton: 'Call',
|
||||
confirmation: `Are you sure you want to call ${userId ? userId : 'User'}?`,
|
||||
confirmation: `Are you sure you want to call ${userId || 'User'}?`,
|
||||
})
|
||||
) {
|
||||
call(agentIds);
|
||||
|
|
@ -181,15 +180,15 @@ function AssistActions({
|
|||
|
||||
const requestControl = () => {
|
||||
const onStart = () => {
|
||||
player.assistManager.ping(AssistActionsPing.control.start, agentId)
|
||||
}
|
||||
player.assistManager.ping(AssistActionsPing.control.start, agentId);
|
||||
};
|
||||
const onEnd = () => {
|
||||
player.assistManager.ping(AssistActionsPing.control.end, agentId)
|
||||
}
|
||||
player.assistManager.ping(AssistActionsPing.control.end, agentId);
|
||||
};
|
||||
setRemoteControlCallbacks({
|
||||
onReject: onControlReject,
|
||||
onStart: onStart,
|
||||
onEnd: onEnd,
|
||||
onStart,
|
||||
onEnd,
|
||||
onBusy: onControlBusy,
|
||||
});
|
||||
requestReleaseRemoteControl();
|
||||
|
|
@ -197,9 +196,9 @@ function AssistActions({
|
|||
|
||||
React.useEffect(() => {
|
||||
if (onCall) {
|
||||
player.assistManager.ping(AssistActionsPing.call.start, agentId)
|
||||
player.assistManager.ping(AssistActionsPing.call.start, agentId);
|
||||
}
|
||||
}, [onCall])
|
||||
}, [onCall]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
|
|
@ -214,7 +213,7 @@ function AssistActions({
|
|||
>
|
||||
<Button
|
||||
icon={<Icon name={annotating ? 'pencil-stop' : 'pencil'} size={16} />}
|
||||
type={'text'}
|
||||
type="text"
|
||||
style={{ height: '28px' }}
|
||||
className={annotating ? 'text-red' : 'text-main'}
|
||||
>
|
||||
|
|
@ -240,7 +239,7 @@ function AssistActions({
|
|||
>
|
||||
<Button
|
||||
icon={<Icon name={remoteActive ? 'window-x' : 'remote-control'} size={16} />}
|
||||
type={'text'}
|
||||
type="text"
|
||||
className={remoteActive ? 'text-red' : 'text-main'}
|
||||
style={{ height: '28px' }}
|
||||
>
|
||||
|
|
@ -253,8 +252,8 @@ function AssistActions({
|
|||
<Tooltip
|
||||
title={
|
||||
cannotCall
|
||||
? `You don't have the permissions to perform this action.`
|
||||
: `Call ${userId ? userId : 'User'}`
|
||||
? 'You don\'t have the permissions to perform this action.'
|
||||
: `Call ${userId || 'User'}`
|
||||
}
|
||||
disabled={onCall}
|
||||
>
|
||||
|
|
@ -266,8 +265,8 @@ function AssistActions({
|
|||
role="button"
|
||||
>
|
||||
<Button
|
||||
icon={<Icon name={'headset'} size={16} />}
|
||||
type={'text'}
|
||||
icon={<Icon name="headset" size={16} />}
|
||||
type="text"
|
||||
className={onCall ? 'text-red' : isPrestart ? 'text-green' : 'text-main'}
|
||||
style={{ height: '28px' }}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './AssistActions'
|
||||
export { default } from './AssistActions';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { Loader, NoContent, Label } from 'UI';
|
||||
import SessionItem from 'Shared/SessionItem';
|
||||
|
|
@ -13,60 +13,65 @@ interface Props {
|
|||
userId: any;
|
||||
}
|
||||
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;
|
||||
}
|
||||
void fetchLiveList(params);
|
||||
}, []);
|
||||
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;
|
||||
}
|
||||
void fetchLiveList(params);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="border-r shadow h-screen overflow-y-auto"
|
||||
style={{ backgroundColor: '#FAFAFA', zIndex: 999, width: '100%', minWidth: '700px' }}
|
||||
>
|
||||
<div className="p-4">
|
||||
<div className="text-2xl">
|
||||
{props.userId}'s <span className="color-gray-medium">Live Sessions</span>{' '}
|
||||
</div>
|
||||
</div>
|
||||
<Loader loading={loading}>
|
||||
<NoContent
|
||||
show={!loading && list.length === 0}
|
||||
title={
|
||||
<div className="flex items-center justify-center flex-col">
|
||||
<AnimatedSVG name={ICONS.NO_LIVE_SESSIONS} size={60} />
|
||||
<div className="mt-4" />
|
||||
<div className="text-center text-lg font-medium">No live sessions found.</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="p-4">
|
||||
{list.map((session: any) => (
|
||||
<div className="mb-6" key={session.sessionId}>
|
||||
{session.pageTitle && session.pageTitle !== '' && (
|
||||
<div className="flex items-center mb-2">
|
||||
<Label size="small" className="p-1">
|
||||
<span className="color-gray-medium">TAB</span>
|
||||
</Label>
|
||||
<span className="ml-2 font-medium">{session.pageTitle}</span>
|
||||
</div>
|
||||
)}
|
||||
<SessionItem compact={true} onClick={hideModal} session={session} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
return (
|
||||
<div
|
||||
className="border-r shadow h-screen overflow-y-auto"
|
||||
style={{
|
||||
backgroundColor: '#FAFAFA', zIndex: 999, width: '100%', minWidth: '700px',
|
||||
}}
|
||||
>
|
||||
<div className="p-4">
|
||||
<div className="text-2xl">
|
||||
{props.userId}
|
||||
's
|
||||
<span className="color-gray-medium">Live Sessions</span>
|
||||
{' '}
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
<Loader loading={loading}>
|
||||
<NoContent
|
||||
show={!loading && list.length === 0}
|
||||
title={(
|
||||
<div className="flex items-center justify-center flex-col">
|
||||
<AnimatedSVG name={ICONS.NO_LIVE_SESSIONS} size={60} />
|
||||
<div className="mt-4" />
|
||||
<div className="text-center text-lg font-medium">No live sessions found.</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div className="p-4">
|
||||
{list.map((session: any) => (
|
||||
<div className="mb-6" key={session.sessionId}>
|
||||
{session.pageTitle && session.pageTitle !== '' && (
|
||||
<div className="flex items-center mb-2">
|
||||
<Label size="small" className="p-1">
|
||||
<span className="color-gray-medium">TAB</span>
|
||||
</Label>
|
||||
<span className="ml-2 font-medium">{session.pageTitle}</span>
|
||||
</div>
|
||||
)}
|
||||
<SessionItem compact onClick={hideModal} session={session} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(SessionList);
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './SessionList';
|
||||
export { default } from './SessionList';
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ interface Props {
|
|||
setRemoteEnabled?: (isEnabled: boolean) => void;
|
||||
}
|
||||
|
||||
function VideoContainer({ stream, muted = false, height = 280, setRemoteEnabled }: Props) {
|
||||
function VideoContainer({
|
||||
stream, muted = false, height = 280, setRemoteEnabled,
|
||||
}: Props) {
|
||||
const ref = useRef<HTMLVideoElement>(null);
|
||||
const [isEnabled, setEnabled] = React.useState(false);
|
||||
|
||||
|
|
@ -22,12 +24,12 @@ function VideoContainer({ stream, muted = false, height = 280, setRemoteEnabled
|
|||
return;
|
||||
}
|
||||
const iid = setInterval(() => {
|
||||
const track = stream.getVideoTracks()[0]
|
||||
const track = stream.getVideoTracks()[0];
|
||||
const settings = track?.getSettings();
|
||||
const isDummyVideoTrack = settings
|
||||
? settings.width === 2 ||
|
||||
settings.frameRate === 0 ||
|
||||
(!settings.frameRate && !settings.width)
|
||||
? settings.width === 2
|
||||
|| settings.frameRate === 0
|
||||
|| (!settings.frameRate && !settings.width)
|
||||
: true;
|
||||
const shouldBeEnabled = track.enabled && !isDummyVideoTrack;
|
||||
|
||||
|
|
@ -41,7 +43,7 @@ function VideoContainer({ stream, muted = false, height = 280, setRemoteEnabled
|
|||
|
||||
return (
|
||||
<div
|
||||
className={'flex-1'}
|
||||
className="flex-1"
|
||||
style={{
|
||||
display: isEnabled ? undefined : 'none',
|
||||
width: isEnabled ? undefined : '0px!important',
|
||||
|
|
@ -49,7 +51,7 @@ function VideoContainer({ stream, muted = false, height = 280, setRemoteEnabled
|
|||
border: '1px solid grey',
|
||||
}}
|
||||
>
|
||||
<video autoPlay ref={ref} muted={muted} style={{ height: height }} />
|
||||
<video autoPlay ref={ref} muted={muted} style={{ height }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './VideoContainer'
|
||||
export { default } from './VideoContainer';
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './Assist'
|
||||
export { default } from './Assist';
|
||||
|
|
|
|||
|
|
@ -13,14 +13,14 @@ import { FilePdfOutlined, ArrowUpOutlined } from '@ant-design/icons';
|
|||
import Period, { LAST_24_HOURS } from 'Types/app/period';
|
||||
import SelectDateRange from 'Shared/SelectDateRange/SelectDateRange';
|
||||
import TeamMembers from 'Components/AssistStats/components/TeamMembers';
|
||||
import { durationFromMsFormatted, formatTimeOrDate } from 'App/date'
|
||||
import { durationFromMsFormatted, formatTimeOrDate } from 'App/date';
|
||||
import { exportCSVFile } from 'App/utils';
|
||||
import { assistStatsService } from 'App/services';
|
||||
|
||||
import { getPdf2 } from 'Components/AssistStats/pdfGenerator';
|
||||
import UserSearch from './components/UserSearch';
|
||||
import Chart from './components/Charts';
|
||||
import StatsTable from './components/Table';
|
||||
import { getPdf2 } from "Components/AssistStats/pdfGenerator";
|
||||
|
||||
const chartNames = {
|
||||
assistTotal: 'Total Live Duration',
|
||||
|
|
@ -68,7 +68,7 @@ function AssistStats() {
|
|||
const topMembersPr = assistStatsService.getTopMembers({
|
||||
startTimestamp: usedP.start,
|
||||
endTimestamp: usedP.end,
|
||||
userId: selectedUser ? selectedUser : undefined,
|
||||
userId: selectedUser || undefined,
|
||||
sort: membersSort,
|
||||
order: 'desc',
|
||||
});
|
||||
|
|
@ -79,7 +79,7 @@ function AssistStats() {
|
|||
endTimestamp: usedP.end,
|
||||
sort: tableSort,
|
||||
order: 'desc',
|
||||
userId: selectedUser ? selectedUser : undefined,
|
||||
userId: selectedUser || undefined,
|
||||
page: 1,
|
||||
limit: 10,
|
||||
});
|
||||
|
|
@ -88,7 +88,7 @@ function AssistStats() {
|
|||
topMembers.status === 'fulfilled' && setTopMembers(topMembers.value);
|
||||
graphs.status === 'fulfilled' && setGraphs(graphs.value);
|
||||
sessions.status === 'fulfilled' && setSessions(sessions.value);
|
||||
}
|
||||
},
|
||||
);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
|
@ -149,26 +149,25 @@ function AssistStats() {
|
|||
page: 1,
|
||||
limit: 10000,
|
||||
}).then((sessions) => {
|
||||
const data = sessions.list.map((s) => ({
|
||||
...s,
|
||||
members: `"${s.teamMembers.map((m) => m.name).join(', ')}"`,
|
||||
dateStr: `"${formatTimeOrDate(s.timestamp, undefined, true)}"`,
|
||||
assistDuration: `"${durationFromMsFormatted(s.assistDuration)}"`,
|
||||
callDuration: `"${durationFromMsFormatted(s.callDuration)}"`,
|
||||
controlDuration: `"${durationFromMsFormatted(s.controlDuration)}"`,
|
||||
}));
|
||||
const headers = [
|
||||
{ label: 'Date', key: 'dateStr' },
|
||||
{ label: 'Team Members', key: 'members' },
|
||||
{ label: 'Live Duration', key: 'assistDuration' },
|
||||
{ label: 'Call Duration', key: 'callDuration' },
|
||||
{ label: 'Remote Duration', key: 'controlDuration' },
|
||||
{ label: 'Session ID', key: 'sessionId' }
|
||||
];
|
||||
const data = sessions.list.map((s) => ({
|
||||
...s,
|
||||
members: `"${s.teamMembers.map((m) => m.name).join(', ')}"`,
|
||||
dateStr: `"${formatTimeOrDate(s.timestamp, undefined, true)}"`,
|
||||
assistDuration: `"${durationFromMsFormatted(s.assistDuration)}"`,
|
||||
callDuration: `"${durationFromMsFormatted(s.callDuration)}"`,
|
||||
controlDuration: `"${durationFromMsFormatted(s.controlDuration)}"`,
|
||||
}));
|
||||
const headers = [
|
||||
{ label: 'Date', key: 'dateStr' },
|
||||
{ label: 'Team Members', key: 'members' },
|
||||
{ label: 'Live Duration', key: 'assistDuration' },
|
||||
{ label: 'Call Duration', key: 'callDuration' },
|
||||
{ label: 'Remote Duration', key: 'controlDuration' },
|
||||
{ label: 'Session ID', key: 'sessionId' },
|
||||
];
|
||||
|
||||
exportCSVFile(headers, data, `Assist_Stats_${new Date().toLocaleDateString()}`)
|
||||
|
||||
})
|
||||
exportCSVFile(headers, data, `Assist_Stats_${new Date().toLocaleDateString()}`);
|
||||
});
|
||||
};
|
||||
|
||||
const onUserSelect = (id: any) => {
|
||||
|
|
@ -191,83 +190,82 @@ function AssistStats() {
|
|||
order: 'desc',
|
||||
page: 1,
|
||||
limit: 10,
|
||||
})
|
||||
});
|
||||
|
||||
Promise.allSettled([topMembersPr, graphsPr, sessionsPr]).then(
|
||||
([topMembers, graphs, sessions]) => {
|
||||
topMembers.status === 'fulfilled' && setTopMembers(topMembers.value);
|
||||
graphs.status === 'fulfilled' && setGraphs(graphs.value);
|
||||
sessions.status === 'fulfilled' && setSessions(sessions.value);
|
||||
}
|
||||
},
|
||||
);
|
||||
setIsLoading(false);
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={'w-full h-screen overflow-y-auto'}>
|
||||
<div className={'mx-auto p-4 bg-white rounded border'} style={{ maxWidth: 1360 }} id={'pdf-anchor'}>
|
||||
<div id={'pdf-ignore'} className={'w-full flex items-center mb-2'}>
|
||||
<div className="w-full h-screen overflow-y-auto">
|
||||
<div className="mx-auto p-4 bg-white rounded border" style={{ maxWidth: 1360 }} id="pdf-anchor">
|
||||
<div id="pdf-ignore" className="w-full flex items-center mb-2">
|
||||
<Typography.Title style={{ marginBottom: 0 }} level={4}>
|
||||
Co-browsing Reports
|
||||
</Typography.Title>
|
||||
<div className={'ml-auto flex items-center gap-2'}>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<UserSearch onUserSelect={onUserSelect} />
|
||||
|
||||
<SelectDateRange period={period} onChange={onChangePeriod} right={true} isAnt small />
|
||||
<SelectDateRange period={period} onChange={onChangePeriod} right isAnt small />
|
||||
<Tooltip title={!sessions || sessions.total === 0 ? 'No data at the moment to export.' : 'Export PDF'}>
|
||||
<Button
|
||||
onClick={getPdf2}
|
||||
shape={'default'}
|
||||
size={'small'}
|
||||
shape="default"
|
||||
size="small"
|
||||
disabled={!sessions || sessions.total === 0}
|
||||
icon={<FilePdfOutlined rev={undefined} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'w-full grid grid-cols-3 gap-2 flex-2 col-span-2'}>
|
||||
{Object.keys(graphs.currentPeriod).map((i: PeriodKeys) => (
|
||||
<div className={'bg-white rounded border'}>
|
||||
<div className={'pt-2 px-2'}>
|
||||
<Typography.Text strong style={{ marginBottom: 0 }}>
|
||||
{chartNames[i]}
|
||||
</Typography.Text>
|
||||
<div className={'flex gap-1 items-center'}>
|
||||
<Typography.Title style={{ marginBottom: 0 }} level={5}>
|
||||
{graphs.currentPeriod[i]
|
||||
? durationFromMsFormatted(graphs.currentPeriod[i])
|
||||
: null}
|
||||
</Typography.Title>
|
||||
{graphs.previousPeriod[i] ? (
|
||||
<div
|
||||
className={
|
||||
<div className="w-full grid grid-cols-3 gap-2 flex-2 col-span-2">
|
||||
{Object.keys(graphs.currentPeriod).map((i: PeriodKeys) => (
|
||||
<div className="bg-white rounded border">
|
||||
<div className="pt-2 px-2">
|
||||
<Typography.Text strong style={{ marginBottom: 0 }}>
|
||||
{chartNames[i]}
|
||||
</Typography.Text>
|
||||
<div className="flex gap-1 items-center">
|
||||
<Typography.Title style={{ marginBottom: 0 }} level={5}>
|
||||
{graphs.currentPeriod[i]
|
||||
? durationFromMsFormatted(graphs.currentPeriod[i])
|
||||
: null}
|
||||
</Typography.Title>
|
||||
{graphs.previousPeriod[i] ? (
|
||||
<div
|
||||
className={
|
||||
graphs.currentPeriod[i] > graphs.previousPeriod[i]
|
||||
? 'flex items-center gap-1 text-green'
|
||||
: 'flex items-center gap-2 text-red'
|
||||
? 'flex items-center gap-1 text-green'
|
||||
: 'flex items-center gap-2 text-red'
|
||||
}
|
||||
>
|
||||
<ArrowUpOutlined
|
||||
rev={undefined}
|
||||
rotate={graphs.currentPeriod[i] > graphs.previousPeriod[i] ? 0 : 180}
|
||||
/>
|
||||
{`${Math.round(
|
||||
calculatePercentageDelta(
|
||||
graphs.currentPeriod[i],
|
||||
graphs.previousPeriod[i]
|
||||
)
|
||||
)}%`}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
>
|
||||
<ArrowUpOutlined
|
||||
rev={undefined}
|
||||
rotate={graphs.currentPeriod[i] > graphs.previousPeriod[i] ? 0 : 180}
|
||||
/>
|
||||
{`${Math.round(
|
||||
calculatePercentageDelta(
|
||||
graphs.currentPeriod[i],
|
||||
graphs.previousPeriod[i],
|
||||
),
|
||||
)}%`}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<Loader loading={isLoading} style={{ minHeight: 90, height: 90 }} size={36}>
|
||||
<Chart data={generateListData(graphs.list, i)} label={chartNames[i]} />
|
||||
</Loader>
|
||||
</div>
|
||||
))}
|
||||
<Loader loading={isLoading} style={{ minHeight: 90, height: 90 }} size={36}>
|
||||
<Chart data={generateListData(graphs.list, i)} label={chartNames[i]} />
|
||||
</Loader>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={'w-full mt-2'}>
|
||||
<div className="w-full mt-2">
|
||||
<TeamMembers
|
||||
isLoading={isLoading}
|
||||
topMembers={topMembers}
|
||||
|
|
@ -275,7 +273,7 @@ function AssistStats() {
|
|||
membersSort={membersSort}
|
||||
/>
|
||||
</div>
|
||||
<div className={'w-full mt-2'}>
|
||||
<div className="w-full mt-2">
|
||||
<StatsTable
|
||||
exportCSV={exportCSV}
|
||||
sessions={sessions}
|
||||
|
|
@ -286,7 +284,7 @@ function AssistStats() {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div id={'stats-layer'} />
|
||||
<div id="stats-layer" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ function Chart(props: Props) {
|
|||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
title={<div className={'text-base font-normal'}>No data available</div>}
|
||||
title={<div className="text-base font-normal">No data available</div>}
|
||||
show={data && data.length === 0}
|
||||
style={{ height: '100px' }}
|
||||
>
|
||||
|
|
@ -51,7 +51,7 @@ function Chart(props: Props) {
|
|||
fillOpacity={1}
|
||||
strokeWidth={2}
|
||||
strokeOpacity={0.8}
|
||||
fill={'url(#colorCount)'}
|
||||
fill="url(#colorCount)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { DownOutlined } from '@ant-design/icons';
|
||||
import { DownOutlined, CloudDownloadOutlined, TableOutlined } from '@ant-design/icons';
|
||||
import { AssistStatsSession, SessionsResponse } from 'App/services/AssistStatsService';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
import React from 'react';
|
||||
import { Button, Dropdown, Space, Typography, Tooltip } from 'antd';
|
||||
import { CloudDownloadOutlined, TableOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button, Dropdown, Space, Typography, Tooltip,
|
||||
} from 'antd';
|
||||
import { Loader, Pagination, NoContent } from 'UI';
|
||||
import PlayLink from 'Shared/SessionItem/PlayLink';
|
||||
import { recordingsService } from 'App/services';
|
||||
|
|
@ -43,7 +44,9 @@ const sortItems = [
|
|||
// },
|
||||
];
|
||||
|
||||
function StatsTable({ onSort, isLoading, onPageChange, page, sessions, exportCSV }: Props) {
|
||||
function StatsTable({
|
||||
onSort, isLoading, onPageChange, page, sessions, exportCSV,
|
||||
}: Props) {
|
||||
const [sortValue, setSort] = React.useState(sortItems[0].label);
|
||||
const updateRange = ({ key }: { key: string }) => {
|
||||
const item = sortItems.find((item) => item.key === key);
|
||||
|
|
@ -52,14 +55,14 @@ function StatsTable({ onSort, isLoading, onPageChange, page, sessions, exportCSV
|
|||
};
|
||||
|
||||
return (
|
||||
<div className={'rounded bg-white border'}>
|
||||
<div className={'flex items-center p-4 gap-2'}>
|
||||
<div className="rounded bg-white border">
|
||||
<div className="flex items-center p-4 gap-2">
|
||||
<Typography.Title level={5} style={{ marginBottom: 0 }}>
|
||||
Assisted Sessions
|
||||
</Typography.Title>
|
||||
<div className={'ml-auto'} />
|
||||
<div className="ml-auto" />
|
||||
<Dropdown menu={{ items: sortItems, onClick: updateRange }}>
|
||||
<Button size={'small'}>
|
||||
<Button size="small">
|
||||
<Space>
|
||||
<Typography.Text>{sortValue}</Typography.Text>
|
||||
<DownOutlined rev={undefined} />
|
||||
|
|
@ -67,7 +70,7 @@ function StatsTable({ onSort, isLoading, onPageChange, page, sessions, exportCSV
|
|||
</Button>
|
||||
</Dropdown>
|
||||
<Button
|
||||
size={'small'}
|
||||
size="small"
|
||||
icon={<TableOutlined rev={undefined} />}
|
||||
onClick={exportCSV}
|
||||
disabled={sessions?.list.length === 0}
|
||||
|
|
@ -75,7 +78,7 @@ function StatsTable({ onSort, isLoading, onPageChange, page, sessions, exportCSV
|
|||
Export CSV
|
||||
</Button>
|
||||
</div>
|
||||
<div className={'bg-gray-lightest grid grid-cols-9 items-center font-semibold p-4'}>
|
||||
<div className="bg-gray-lightest grid grid-cols-9 items-center font-semibold p-4">
|
||||
<Cell size={2}>Date</Cell>
|
||||
<Cell size={2}>Team Members</Cell>
|
||||
<Cell size={1}>Live Duration</Cell>
|
||||
|
|
@ -83,31 +86,52 @@ function StatsTable({ onSort, isLoading, onPageChange, page, sessions, exportCSV
|
|||
<Cell size={2}>Remote Duration</Cell>
|
||||
<Cell size={1}>{/* BUTTONS */}</Cell>
|
||||
</div>
|
||||
<div className={'bg-white'}>
|
||||
<Loader loading={isLoading} style={{ height: 300 }}>
|
||||
<div className="bg-white">
|
||||
<Loader loading={isLoading} style={{ height: 300 }}>
|
||||
<NoContent
|
||||
size={'small'}
|
||||
title={<div className={'text-base font-normal'}>No data available</div>}
|
||||
size="small"
|
||||
title={<div className="text-base font-normal">No data available</div>}
|
||||
show={sessions.list && sessions.list.length === 0}
|
||||
style={{ height: '100px' }}
|
||||
>
|
||||
{sessions.list.map((session) => (
|
||||
<Row session={session} />
|
||||
))}
|
||||
{sessions.list.map((session) => (
|
||||
<Row session={session} />
|
||||
))}
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</Loader>
|
||||
</div>
|
||||
<div className={'flex items-center justify-between p-4'}>
|
||||
<div className="flex items-center justify-between p-4">
|
||||
{sessions.total > 0 ? (
|
||||
<div>
|
||||
Showing <span className="font-medium">{(page - 1) * PER_PAGE + 1}</span> to{' '}
|
||||
<span className="font-medium">{(page - 1) * PER_PAGE + sessions.list.length}</span> of{' '}
|
||||
<span className="font-medium">{numberWithCommas(sessions.total)}</span> sessions.
|
||||
Showing
|
||||
{' '}
|
||||
<span className="font-medium">{(page - 1) * PER_PAGE + 1}</span>
|
||||
{' '}
|
||||
to
|
||||
{' '}
|
||||
<span className="font-medium">{(page - 1) * PER_PAGE + sessions.list.length}</span>
|
||||
{' '}
|
||||
of
|
||||
{' '}
|
||||
<span className="font-medium">{numberWithCommas(sessions.total)}</span>
|
||||
{' '}
|
||||
sessions.
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
Showing <span className="font-medium">0</span> to <span className="font-medium">0</span>{' '}
|
||||
of <span className="font-medium">0</span> sessions.
|
||||
Showing
|
||||
{' '}
|
||||
<span className="font-medium">0</span>
|
||||
{' '}
|
||||
to
|
||||
{' '}
|
||||
<span className="font-medium">0</span>
|
||||
{' '}
|
||||
of
|
||||
{' '}
|
||||
<span className="font-medium">0</span>
|
||||
{' '}
|
||||
sessions.
|
||||
</div>
|
||||
)}
|
||||
<Pagination
|
||||
|
|
@ -124,14 +148,14 @@ function StatsTable({ onSort, isLoading, onPageChange, page, sessions, exportCSV
|
|||
|
||||
function Row({ session }: { session: AssistStatsSession }) {
|
||||
const { hideModal } = useModal();
|
||||
|
||||
|
||||
return (
|
||||
<div className={'grid grid-cols-9 p-4 border-b hover:bg-active-blue'}>
|
||||
<div className="grid grid-cols-9 p-4 border-b hover:bg-active-blue">
|
||||
<Cell size={2}>{checkForRecent(getDateFromMill(session.timestamp)!, 'LLL dd, hh:mm a')}</Cell>
|
||||
<Cell size={2}>
|
||||
<div className={'flex gap-2 flex-wrap'}>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{session.teamMembers.map((member) => (
|
||||
<div className={'p-1 rounded border bg-gray-lightest w-fit'}>{member.name}</div>
|
||||
<div className="p-1 rounded border bg-gray-lightest w-fit">{member.name}</div>
|
||||
))}
|
||||
</div>
|
||||
</Cell>
|
||||
|
|
@ -139,7 +163,7 @@ function Row({ session }: { session: AssistStatsSession }) {
|
|||
<Cell size={1}>{durationFromMsFormatted(session.callDuration)}</Cell>
|
||||
<Cell size={2}>{durationFromMsFormatted(session.controlDuration)}</Cell>
|
||||
<Cell size={1}>
|
||||
<div className={'w-full flex justify-end gap-4'}>
|
||||
<div className="w-full flex justify-end gap-4">
|
||||
{session.recordings?.length > 0 ? (
|
||||
session.recordings?.length > 1 ? (
|
||||
<Dropdown
|
||||
|
|
@ -148,15 +172,14 @@ function Row({ session }: { session: AssistStatsSession }) {
|
|||
key: recording.recordId,
|
||||
label: recording.name.slice(0, 20),
|
||||
})),
|
||||
onClick: (item) =>
|
||||
recordingsService.fetchRecording(item.key as unknown as number),
|
||||
onClick: (item) => recordingsService.fetchRecording(item.key as unknown as number),
|
||||
}}
|
||||
>
|
||||
<CloudDownloadOutlined rev={undefined} style={{ fontSize: 22, color: '#8C8C8C' }} />
|
||||
</Dropdown>
|
||||
) : (
|
||||
<div
|
||||
className={'cursor-pointer'}
|
||||
className="cursor-pointer"
|
||||
onClick={() => recordingsService.fetchRecording(session.recordings[0].recordId)}
|
||||
>
|
||||
<CloudDownloadOutlined rev={undefined} style={{ fontSize: 22, color: '#8C8C8C' }} />
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { DownOutlined, TableOutlined } from '@ant-design/icons';
|
||||
import { Button, Dropdown, Space, Typography, Tooltip } from 'antd';
|
||||
import {
|
||||
Button, Dropdown, Space, Typography, Tooltip,
|
||||
} from 'antd';
|
||||
import { durationFromMsFormatted } from 'App/date';
|
||||
import { Member } from 'App/services/AssistStatsService';
|
||||
import { getInitials } from 'App/utils';
|
||||
import { getInitials, exportCSVFile } from 'App/utils';
|
||||
import React from 'react';
|
||||
import { Loader, NoContent } from 'UI';
|
||||
import { exportCSVFile } from 'App/utils';
|
||||
|
||||
const items = [
|
||||
{
|
||||
|
|
@ -65,14 +66,14 @@ function TeamMembers({
|
|||
};
|
||||
|
||||
return (
|
||||
<div className={'rounded bg-white border p-2 h-full w-full flex flex-col'}>
|
||||
<div className={'flex items-center'}>
|
||||
<div className="rounded bg-white border p-2 h-full w-full flex flex-col">
|
||||
<div className="flex items-center">
|
||||
<Typography.Title style={{ marginBottom: 0 }} level={5}>
|
||||
Team Members
|
||||
</Typography.Title>
|
||||
<div className={'ml-auto flex items-center gap-2'}>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<Dropdown menu={{ items, onClick: updateRange }}>
|
||||
<Button size={'small'}>
|
||||
<Button size="small">
|
||||
<Space>
|
||||
<Typography.Text>{dateRange}</Typography.Text>
|
||||
<DownOutlined rev={undefined} />
|
||||
|
|
@ -82,8 +83,8 @@ function TeamMembers({
|
|||
<Tooltip title={topMembers.list.length === 0 ? 'No data at the moment to export.' : 'Export CSV'}>
|
||||
<Button
|
||||
onClick={onExport}
|
||||
shape={'default'}
|
||||
size={'small'}
|
||||
shape="default"
|
||||
size="small"
|
||||
disabled={topMembers.list.length === 0}
|
||||
icon={<TableOutlined rev={undefined} />}
|
||||
/>
|
||||
|
|
@ -92,19 +93,19 @@ function TeamMembers({
|
|||
</div>
|
||||
<Loader loading={isLoading} style={{ minHeight: 150, height: 300 }} size={48}>
|
||||
<NoContent
|
||||
size={'small'}
|
||||
title={<div className={'text-base font-normal'}>No data available</div>}
|
||||
size="small"
|
||||
title={<div className="text-base font-normal">No data available</div>}
|
||||
show={topMembers.list && topMembers.list.length === 0}
|
||||
style={{ height: '100px' }}
|
||||
>
|
||||
{topMembers.list.map((member) => (
|
||||
<div key={member.name} className={'w-full flex items-center gap-2 border-b pt-2 pb-1'}>
|
||||
<div key={member.name} className="w-full flex items-center gap-2 border-b pt-2 pb-1">
|
||||
<div className="relative flex items-center justify-center w-10 h-10">
|
||||
<div className="absolute left-0 right-0 top-0 bottom-0 mx-auto w-10 h-10 rounded-full opacity-30 bg-tealx" />
|
||||
<div className="text-lg uppercase color-tealx">{getInitials(member.name)}</div>
|
||||
</div>
|
||||
<div>{member.name}</div>
|
||||
<div className={'ml-auto'}>
|
||||
<div className="ml-auto">
|
||||
{membersSort === 'sessionsAssisted'
|
||||
? member.count
|
||||
: durationFromMsFormatted(member.count)}
|
||||
|
|
@ -113,7 +114,7 @@ function TeamMembers({
|
|||
))}
|
||||
</NoContent>
|
||||
</Loader>
|
||||
<div className={'flex items-center justify-center text-disabled-text p-2 mt-auto'}>
|
||||
<div className="flex items-center justify-center text-disabled-text p-2 mt-auto">
|
||||
{isLoading || topMembers.list.length === 0
|
||||
? ''
|
||||
: `Showing 1 to ${topMembers.total} of the total`}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import type { SelectProps } from 'antd/es/select';
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
|
||||
const UserSearch = ({ onUserSelect }: { onUserSelect: (id: any) => void }) => {
|
||||
function UserSearch({ onUserSelect }: { onUserSelect: (id: any) => void }) {
|
||||
const [selectedValue, setSelectedValue] = useState<string | undefined>(undefined);
|
||||
const { userStore } = useStore();
|
||||
const allUsers = userStore.list.map((user) => ({
|
||||
|
|
@ -20,7 +20,7 @@ const UserSearch = ({ onUserSelect }: { onUserSelect: (id: any) => void }) => {
|
|||
r.map((user: any) => ({
|
||||
value: user.userId,
|
||||
label: user.name,
|
||||
}))
|
||||
})),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -28,12 +28,12 @@ const UserSearch = ({ onUserSelect }: { onUserSelect: (id: any) => void }) => {
|
|||
|
||||
const handleSearch = (value: string) => {
|
||||
setOptions(
|
||||
value ? allUsers.filter((u) => u.label.toLowerCase().includes(value.toLocaleLowerCase())) : []
|
||||
value ? allUsers.filter((u) => u.label.toLowerCase().includes(value.toLocaleLowerCase())) : [],
|
||||
);
|
||||
};
|
||||
|
||||
const onSelect = (value?: string) => {
|
||||
onUserSelect(value)
|
||||
onUserSelect(value);
|
||||
setSelectedValue(allUsers.find((u) => u.value === value)?.label || '');
|
||||
};
|
||||
|
||||
|
|
@ -46,8 +46,8 @@ const UserSearch = ({ onUserSelect }: { onUserSelect: (id: any) => void }) => {
|
|||
onSearch={handleSearch}
|
||||
value={selectedValue}
|
||||
onChange={(e) => {
|
||||
setSelectedValue(e)
|
||||
if (!e) onUserSelect(undefined)
|
||||
setSelectedValue(e);
|
||||
if (!e) onUserSelect(undefined);
|
||||
}}
|
||||
onClear={() => onSelect(undefined)}
|
||||
onDeselect={() => onSelect(undefined)}
|
||||
|
|
@ -56,12 +56,12 @@ const UserSearch = ({ onUserSelect }: { onUserSelect: (id: any) => void }) => {
|
|||
<Input.Search
|
||||
allowClear
|
||||
placeholder="Filter by team member name"
|
||||
size={'small'}
|
||||
size="small"
|
||||
classNames={{ input: '!border-0 focus:!border-0' }}
|
||||
style={{ width: 200 }}
|
||||
/>
|
||||
</AutoComplete>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default observer(UserSearch);
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './AssistStats'
|
||||
export { default } from './AssistStats';
|
||||
|
|
|
|||
|
|
@ -28,9 +28,9 @@ export const getPdf2 = async () => {
|
|||
}).then((canvas) => {
|
||||
const imgData = canvas.toDataURL('img/png');
|
||||
|
||||
let imgWidth = 290;
|
||||
let pageHeight = 200;
|
||||
let imgHeight = (canvas.height * imgWidth) / canvas.width;
|
||||
const imgWidth = 290;
|
||||
const pageHeight = 200;
|
||||
const imgHeight = (canvas.height * imgWidth) / canvas.width;
|
||||
let heightLeft = imgHeight - pageHeight;
|
||||
let position = 0;
|
||||
const A4Height = 295;
|
||||
|
|
@ -39,15 +39,16 @@ export const getPdf2 = async () => {
|
|||
doc.addImage(imgData, 'PNG', 3, 10, imgWidth, imgHeight);
|
||||
|
||||
doc.addImage('/assets/img/cobrowising-report-head.png', 'png', A4Height / 2 - headerW / 2, 2, 45, 5);
|
||||
if (position === 0 && heightLeft === 0)
|
||||
if (position === 0 && heightLeft === 0) {
|
||||
doc.addImage(
|
||||
'/assets/img/report-head.png',
|
||||
'png',
|
||||
imgWidth / 2 - headerW / 2,
|
||||
pageHeight - 5,
|
||||
logoWidth,
|
||||
5
|
||||
5,
|
||||
);
|
||||
}
|
||||
|
||||
while (heightLeft >= 0) {
|
||||
position = heightLeft - imgHeight;
|
||||
|
|
@ -59,12 +60,12 @@ export const getPdf2 = async () => {
|
|||
A4Height / 2 - headerW / 2,
|
||||
pageHeight - 5,
|
||||
logoWidth,
|
||||
5
|
||||
5,
|
||||
);
|
||||
heightLeft -= pageHeight;
|
||||
}
|
||||
|
||||
doc.save(fileNameFormat('Assist_Stats_' + Date.now(), '.pdf'));
|
||||
doc.save(fileNameFormat(`Assist_Stats_${Date.now()}`, '.pdf'));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import React from 'react';
|
||||
import { BarChart } from 'echarts/charts';
|
||||
import {
|
||||
DataProps,
|
||||
buildCategories,
|
||||
customTooltipFormatter
|
||||
customTooltipFormatter,
|
||||
} from './utils';
|
||||
import { buildBarDatasetsAndSeries } from './barUtils';
|
||||
import { defaultOptions, echarts, initWindowStorages } from "./init";
|
||||
import { BarChart } from 'echarts/charts';
|
||||
import { defaultOptions, echarts, initWindowStorages } from './init';
|
||||
|
||||
echarts.use([BarChart]);
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ function ORBarChart(props: BarChartProps) {
|
|||
React.useEffect(() => {
|
||||
if (!chartRef.current) return;
|
||||
const chart = echarts.init(chartRef.current);
|
||||
const obs = new ResizeObserver(() => chart.resize())
|
||||
const obs = new ResizeObserver(() => chart.resize());
|
||||
obs.observe(chartRef.current);
|
||||
|
||||
const categories = buildCategories(props.data);
|
||||
|
|
@ -45,7 +45,6 @@ function ORBarChart(props: BarChartProps) {
|
|||
});
|
||||
});
|
||||
|
||||
|
||||
const xAxis: any = {
|
||||
type: 'category',
|
||||
data: categories,
|
||||
|
|
@ -81,11 +80,11 @@ function ORBarChart(props: BarChartProps) {
|
|||
chart.on('click', (event) => {
|
||||
const index = event.dataIndex;
|
||||
const timestamp = (window as any).__timestampMap?.[chartUuid.current]?.[index];
|
||||
props.onClick?.({ activePayload: [{ payload: { timestamp }}]})
|
||||
props.onClick?.({ activePayload: [{ payload: { timestamp } }] });
|
||||
setTimeout(() => {
|
||||
props.onSeriesFocus?.(event.seriesName)
|
||||
}, 0)
|
||||
})
|
||||
props.onSeriesFocus?.(event.seriesName);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
return () => {
|
||||
chart.dispose();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
import { defaultOptions, echarts } from './init';
|
||||
import { BarChart } from 'echarts/charts';
|
||||
import { defaultOptions, echarts } from './init';
|
||||
import { customTooltipFormatter } from './utils';
|
||||
import { buildColumnChart } from './barUtils'
|
||||
import { buildColumnChart } from './barUtils';
|
||||
|
||||
echarts.use([BarChart]);
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ function ColumnChart(props: ColumnChartProps) {
|
|||
const { data, compData, label } = props;
|
||||
const chartRef = React.useRef<HTMLDivElement>(null);
|
||||
const chartUuid = React.useRef<string>(
|
||||
Math.random().toString(36).substring(7)
|
||||
Math.random().toString(36).substring(7),
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
|
|
@ -42,10 +42,10 @@ function ColumnChart(props: ColumnChartProps) {
|
|||
(window as any).__seriesValueMap[chartUuid.current] = {};
|
||||
(window as any).__seriesColorMap = (window as any).__seriesColorMap ?? {};
|
||||
(window as any).__seriesColorMap[chartUuid.current] = {};
|
||||
(window as any).__yAxisData = (window as any).__yAxisData ?? {}
|
||||
(window as any).__yAxisData = (window as any).__yAxisData ?? {};
|
||||
|
||||
const { yAxisData, series } = buildColumnChart(chartUuid.current, data, compData);
|
||||
(window as any).__yAxisData[chartUuid.current] = yAxisData
|
||||
(window as any).__yAxisData[chartUuid.current] = yAxisData;
|
||||
|
||||
chart.setOption({
|
||||
...defaultOptions,
|
||||
|
|
@ -89,7 +89,7 @@ function ColumnChart(props: ColumnChartProps) {
|
|||
chart.on('click', (event) => {
|
||||
const focusedSeriesName = event.name;
|
||||
props.onSeriesFocus?.(focusedSeriesName);
|
||||
})
|
||||
});
|
||||
|
||||
return () => {
|
||||
chart.dispose();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
import { echarts, defaultOptions, initWindowStorages } from './init';
|
||||
import { customTooltipFormatter, buildCategories, buildDatasetsAndSeries } from './utils'
|
||||
import type { DataProps } from './utils'
|
||||
import { LineChart } from 'echarts/charts';
|
||||
import { echarts, defaultOptions, initWindowStorages } from './init';
|
||||
import { customTooltipFormatter, buildCategories, buildDatasetsAndSeries } from './utils';
|
||||
import type { DataProps } from './utils';
|
||||
|
||||
echarts.use([LineChart]);
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ function ORLineChart(props: Props) {
|
|||
React.useEffect(() => {
|
||||
if (!chartRef.current) return;
|
||||
const chart = echarts.init(chartRef.current);
|
||||
const obs = new ResizeObserver(() => chart.resize())
|
||||
const obs = new ResizeObserver(() => chart.resize());
|
||||
obs.observe(chartRef.current);
|
||||
|
||||
const categories = buildCategories(props.data);
|
||||
|
|
@ -55,7 +55,7 @@ function ORLineChart(props: Props) {
|
|||
chart.setOption({
|
||||
...defaultOptions,
|
||||
title: {
|
||||
text: props.chartName ?? "Line Chart",
|
||||
text: props.chartName ?? 'Line Chart',
|
||||
show: false,
|
||||
},
|
||||
legend: {
|
||||
|
|
@ -75,7 +75,7 @@ function ORLineChart(props: Props) {
|
|||
nameTextStyle: {
|
||||
padding: [0, 0, 0, 15],
|
||||
},
|
||||
minInterval: 1
|
||||
minInterval: 1,
|
||||
},
|
||||
tooltip: {
|
||||
...defaultOptions.tooltip,
|
||||
|
|
@ -92,11 +92,11 @@ function ORLineChart(props: Props) {
|
|||
chart.on('click', (event) => {
|
||||
const index = event.dataIndex;
|
||||
const timestamp = (window as any).__timestampMap?.[chartUuid.current]?.[index];
|
||||
props.onClick?.({ activePayload: [{ payload: { timestamp }}]})
|
||||
props.onClick?.({ activePayload: [{ payload: { timestamp } }] });
|
||||
setTimeout(() => {
|
||||
props.onSeriesFocus?.(event.seriesName)
|
||||
}, 0)
|
||||
})
|
||||
props.onSeriesFocus?.(event.seriesName);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
return () => {
|
||||
chart.dispose();
|
||||
|
|
|
|||
|
|
@ -22,13 +22,15 @@ interface PieChartProps {
|
|||
}
|
||||
|
||||
function PieChart(props: PieChartProps) {
|
||||
const { data, label, onClick = () => {}, inGrid = false } = props;
|
||||
const {
|
||||
data, label, onClick = () => {}, inGrid = false,
|
||||
} = props;
|
||||
const chartRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chartRef.current) return;
|
||||
if (!data.chart || data.chart.length === 0) {
|
||||
chartRef.current.innerHTML = `<div style="text-align:center;padding:20px;">No data available</div>`;
|
||||
chartRef.current.innerHTML = '<div style="text-align:center;padding:20px;">No data available</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -36,7 +38,7 @@ function PieChart(props: PieChartProps) {
|
|||
|
||||
const pieData = buildPieData(data.chart, data.namesMap);
|
||||
if (!pieData.length) {
|
||||
chartRef.current.innerHTML = `<div style="text-align:center;padding:20px;">No data available</div>`;
|
||||
chartRef.current.innerHTML = '<div style="text-align:center;padding:20px;">No data available</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -75,28 +77,24 @@ function PieChart(props: PieChartProps) {
|
|||
name: label ?? 'Data',
|
||||
radius: [50, 100],
|
||||
center: ['50%', '55%'],
|
||||
data: pieData.map((d, idx) => {
|
||||
return {
|
||||
name: d.name,
|
||||
value: d.value,
|
||||
label: {
|
||||
show: false, //d.value / largestVal >= 0.03,
|
||||
position: 'outside',
|
||||
formatter: (params: any) => {
|
||||
return params.value;
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false, // d.value / largestVal >= 0.03,
|
||||
length: 10,
|
||||
length2: 20,
|
||||
lineStyle: { color: '#3EAAAF' },
|
||||
},
|
||||
itemStyle: {
|
||||
color: pickColorByIndex(idx),
|
||||
},
|
||||
};
|
||||
}),
|
||||
data: pieData.map((d, idx) => ({
|
||||
name: d.name,
|
||||
value: d.value,
|
||||
label: {
|
||||
show: false, // d.value / largestVal >= 0.03,
|
||||
position: 'outside',
|
||||
formatter: (params: any) => params.value,
|
||||
},
|
||||
labelLine: {
|
||||
show: false, // d.value / largestVal >= 0.03,
|
||||
length: 10,
|
||||
length2: 20,
|
||||
lineStyle: { color: '#3EAAAF' },
|
||||
},
|
||||
itemStyle: {
|
||||
color: pickColorByIndex(idx),
|
||||
},
|
||||
})),
|
||||
emphasis: {
|
||||
scale: true,
|
||||
scaleSize: 4,
|
||||
|
|
@ -106,11 +104,11 @@ function PieChart(props: PieChartProps) {
|
|||
};
|
||||
|
||||
chartInstance.setOption(option);
|
||||
const obs = new ResizeObserver(() => chartInstance.resize())
|
||||
const obs = new ResizeObserver(() => chartInstance.resize());
|
||||
obs.observe(chartRef.current);
|
||||
|
||||
chartInstance.on('click', function (params) {
|
||||
const focusedSeriesName = params.name
|
||||
chartInstance.on('click', (params) => {
|
||||
const focusedSeriesName = params.name;
|
||||
props.onSeriesFocus?.(focusedSeriesName);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import React from 'react';
|
||||
import { echarts, defaultOptions } from './init';
|
||||
import { SankeyChart } from 'echarts/charts';
|
||||
import { sankeyTooltip, getEventPriority, getNodeName } from './sankeyUtils';
|
||||
import { NoContent } from 'App/components/ui';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { sankeyTooltip, getEventPriority, getNodeName } from './sankeyUtils';
|
||||
import { echarts, defaultOptions } from './init';
|
||||
|
||||
echarts.use([SankeyChart]);
|
||||
|
||||
|
|
@ -36,21 +36,23 @@ interface Props {
|
|||
}
|
||||
|
||||
const EChartsSankey: React.FC<Props> = (props) => {
|
||||
const { data, height = 240, onChartClick, isUngrouped } = props;
|
||||
const {
|
||||
data, height = 240, onChartClick, isUngrouped,
|
||||
} = props;
|
||||
const chartRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
if (data.nodes.length === 0 || data.links.length === 0) {
|
||||
return (
|
||||
<NoContent
|
||||
style={{ minHeight: height }}
|
||||
title={
|
||||
title={(
|
||||
<div className="flex items-center relative">
|
||||
<InfoCircleOutlined className="hidden md:inline-block mr-1" />
|
||||
Set a start or end point to visualize the journey. If set, try
|
||||
adjusting filters.
|
||||
</div>
|
||||
}
|
||||
show={true}
|
||||
)}
|
||||
show
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -60,8 +62,8 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
React.useEffect(() => {
|
||||
if (!chartRef.current) return;
|
||||
|
||||
let finalNodes = data.nodes;
|
||||
let finalLinks = data.links;
|
||||
const finalNodes = data.nodes;
|
||||
const finalLinks = data.links;
|
||||
|
||||
const chart = echarts.init(chartRef.current);
|
||||
|
||||
|
|
@ -71,8 +73,8 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
const sourceNode = finalNodes.find((n) => n.id === l.source);
|
||||
const targetNode = finalNodes.find((n) => n.id === l.target);
|
||||
return (
|
||||
(sourceNode?.depth ?? 0) <= maxDepth &&
|
||||
(targetNode?.depth ?? 0) <= maxDepth
|
||||
(sourceNode?.depth ?? 0) <= maxDepth
|
||||
&& (targetNode?.depth ?? 0) <= maxDepth
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -89,10 +91,9 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
} else {
|
||||
nodeValues[i] = 0;
|
||||
}
|
||||
const itemColor =
|
||||
computedName === 'Others'
|
||||
? 'rgba(34,44,154,.9)'
|
||||
: n.eventType === 'DROP'
|
||||
const itemColor = computedName === 'Others'
|
||||
? 'rgba(34,44,154,.9)'
|
||||
: n.eventType === 'DROP'
|
||||
? '#B5B7C8'
|
||||
: '#394eff';
|
||||
|
||||
|
|
@ -110,9 +111,8 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
return (
|
||||
getEventPriority(a.type || '') - getEventPriority(b.type || '')
|
||||
);
|
||||
} else {
|
||||
return (a.depth as number) - (b.depth as number);
|
||||
}
|
||||
return (a.depth as number) - (b.depth as number);
|
||||
});
|
||||
|
||||
const echartLinks = filteredLinks.map((l) => ({
|
||||
|
|
@ -174,25 +174,24 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
maxWidth: 30,
|
||||
distance: 3,
|
||||
offset: [-20, 0],
|
||||
formatter: function (params: any) {
|
||||
formatter(params: any) {
|
||||
const nodeVal = params.value;
|
||||
const percentage = startNodeValue
|
||||
? ((nodeVal / startNodeValue) * 100).toFixed(1) + '%'
|
||||
? `${((nodeVal / startNodeValue) * 100).toFixed(1)}%`
|
||||
: '0%';
|
||||
|
||||
const maxLen = 20;
|
||||
const safeName =
|
||||
params.name.length > maxLen
|
||||
? params.name.slice(0, maxLen / 2 - 2) +
|
||||
'...' +
|
||||
params.name.slice(-(maxLen / 2 - 2))
|
||||
: params.name;
|
||||
const safeName = params.name.length > maxLen
|
||||
? `${params.name.slice(0, maxLen / 2 - 2)
|
||||
}...${
|
||||
params.name.slice(-(maxLen / 2 - 2))}`
|
||||
: params.name;
|
||||
const nodeType = params.data.type;
|
||||
|
||||
const icon = getIcon(nodeType)
|
||||
const icon = getIcon(nodeType);
|
||||
return (
|
||||
`${icon}{header| ${safeName}}\n` +
|
||||
`{body|}{percentage|${percentage}} {sessions|${nodeVal}}`
|
||||
`${icon}{header| ${safeName}}\n`
|
||||
+ `{body|}{percentage|${percentage}} {sessions|${nodeVal}}`
|
||||
);
|
||||
},
|
||||
rich: {
|
||||
|
|
@ -245,7 +244,7 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
},
|
||||
customEventIcon: {
|
||||
backgroundColor: {
|
||||
image: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWNvZGUiPjxwb2x5bGluZSBwb2ludHM9IjE2IDE4IDIyIDEyIDE2IDYiLz48cG9seWxpbmUgcG9pbnRzPSI4IDYgMiAxMiA4IDE4Ii8+PC9zdmc+'
|
||||
image: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWNvZGUiPjxwb2x5bGluZSBwb2ludHM9IjE2IDE4IDIyIDEyIDE2IDYiLz48cG9seWxpbmUgcG9pbnRzPSI4IDYgMiAxMiA4IDE4Ii8+PC9zdmc+',
|
||||
},
|
||||
height: 20,
|
||||
width: 14,
|
||||
|
|
@ -263,7 +262,7 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
},
|
||||
height: 20,
|
||||
width: 14,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
|
|
@ -318,17 +317,16 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
const originalNodes = [...echartNodes];
|
||||
const originalLinks = [...echartLinks];
|
||||
|
||||
chart.on('mouseover', function (params: any) {
|
||||
chart.on('mouseover', (params: any) => {
|
||||
if (params.dataType === 'node') {
|
||||
const hoveredIndex = params.dataIndex;
|
||||
const connectedChain = getConnectedChain(hoveredIndex);
|
||||
|
||||
const updatedNodes = echartNodes.map((node, idx) => {
|
||||
const baseOpacity = connectedChain.has(idx) ? 1 : 0.35;
|
||||
const extraStyle =
|
||||
idx === hoveredIndex
|
||||
? { borderColor: '#000', borderWidth: 1, borderType: 'dotted' }
|
||||
: {};
|
||||
const extraStyle = idx === hoveredIndex
|
||||
? { borderColor: '#000', borderWidth: 1, borderType: 'dotted' }
|
||||
: {};
|
||||
return {
|
||||
...node,
|
||||
itemStyle: {
|
||||
|
|
@ -361,7 +359,7 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
}
|
||||
});
|
||||
|
||||
chart.on('mouseout', function (params: any) {
|
||||
chart.on('mouseout', (params: any) => {
|
||||
if (params.dataType === 'node') {
|
||||
chart.setOption({
|
||||
series: [
|
||||
|
|
@ -374,7 +372,7 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
}
|
||||
});
|
||||
|
||||
chart.on('click', function (params: any) {
|
||||
chart.on('click', (params: any) => {
|
||||
if (!onChartClick) return;
|
||||
const unsupported = ['other', 'drop'];
|
||||
|
||||
|
|
@ -388,7 +386,7 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
}
|
||||
filters.push({
|
||||
operator: 'is',
|
||||
type: type,
|
||||
type,
|
||||
value: [node.name],
|
||||
isEvent: true,
|
||||
});
|
||||
|
|
@ -403,8 +401,8 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
const firstNodeType = firstNode?.eventType?.toLowerCase() ?? 'location';
|
||||
const lastNodeType = lastNode?.eventType?.toLowerCase() ?? 'location';
|
||||
if (
|
||||
unsupported.includes(firstNodeType) ||
|
||||
unsupported.includes(lastNodeType)
|
||||
unsupported.includes(firstNodeType)
|
||||
|| unsupported.includes(lastNodeType)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -457,7 +455,10 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxHeight: 620, overflow: 'auto', maxWidth: 1240, minHeight: 240 }}>
|
||||
<div style={{
|
||||
maxHeight: 620, overflow: 'auto', maxWidth: 1240, minHeight: 240,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={chartRef}
|
||||
style={containerStyle}
|
||||
|
|
@ -469,13 +470,13 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
|
||||
function getIcon(type: string) {
|
||||
if (type === 'LOCATION') {
|
||||
return '{locationIcon|}'
|
||||
return '{locationIcon|}';
|
||||
}
|
||||
if (type === 'INPUT') {
|
||||
return '{inputIcon|}'
|
||||
return '{inputIcon|}';
|
||||
}
|
||||
if (type === 'CUSTOM_EVENT') {
|
||||
return '{customEventIcon|}'
|
||||
return '{customEventIcon|}';
|
||||
}
|
||||
if (type === 'CLICK') {
|
||||
return '{clickIcon|}';
|
||||
|
|
@ -486,7 +487,7 @@ function getIcon(type: string) {
|
|||
if (type === 'OTHER') {
|
||||
return '{groupIcon|}';
|
||||
}
|
||||
return ''
|
||||
return '';
|
||||
}
|
||||
|
||||
export default EChartsSankey;
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ export function buildBarDatasetsAndSeries(props: DataProps) {
|
|||
return { datasets, series };
|
||||
}
|
||||
|
||||
|
||||
// START GEN
|
||||
function sumSeries(chart: DataItem[], seriesName: string): number {
|
||||
return chart.reduce((acc, row) => acc + (Number(row[seriesName]) || 0), 0);
|
||||
|
|
@ -62,7 +61,7 @@ function sumSeries(chart: DataItem[], seriesName: string): number {
|
|||
export function buildColumnChart(
|
||||
chartUuid: string,
|
||||
data: DataProps['data'],
|
||||
compData: DataProps['compData']
|
||||
compData: DataProps['compData'],
|
||||
) {
|
||||
const categories = data.namesMap.filter(Boolean);
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ echarts.use([
|
|||
LegendComponent,
|
||||
// TransformComponent,
|
||||
SVGRenderer,
|
||||
ToolboxComponent
|
||||
ToolboxComponent,
|
||||
]);
|
||||
|
||||
const defaultOptions = {
|
||||
|
|
@ -38,9 +38,9 @@ const defaultOptions = {
|
|||
type: 'cross',
|
||||
snap: true,
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
backgroundColor: '#6a7985',
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
bottom: 20,
|
||||
|
|
@ -56,16 +56,16 @@ const defaultOptions = {
|
|||
feature: {
|
||||
saveAsImage: {
|
||||
pixelRatio: 1.5,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
type: 'plain',
|
||||
show: true,
|
||||
top: 10,
|
||||
icon: 'pin'
|
||||
icon: 'pin',
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export function initWindowStorages(chartUuid: string, categories: string[] = [], chartArr: any[] = [], compChartArr: any[] = []) {
|
||||
(window as any).__seriesValueMap = (window as any).__seriesValueMap ?? {};
|
||||
|
|
@ -91,4 +91,4 @@ export function initWindowStorages(chartUuid: string, categories: string[] = [],
|
|||
}
|
||||
}
|
||||
|
||||
export { echarts, defaultOptions };
|
||||
export { echarts, defaultOptions };
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import { colors } from './utils';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
|
||||
import { colors } from './utils';
|
||||
|
||||
export function buildPieData(
|
||||
chart: Array<Record<string, any>>,
|
||||
namesMap: string[]
|
||||
namesMap: string[],
|
||||
) {
|
||||
const result: { name: string; value: number }[] = namesMap.map((name) => {
|
||||
let sum = 0;
|
||||
|
|
@ -17,7 +16,9 @@ export function buildPieData(
|
|||
}
|
||||
|
||||
export function pieTooltipFormatter(params: any) {
|
||||
const { name, value, marker, percent } = params;
|
||||
const {
|
||||
name, value, marker, percent,
|
||||
} = params;
|
||||
return `
|
||||
<div class="flex flex-col gap-1 bg-white shadow border rounded p-2 z-50">
|
||||
<div style="margin-bottom: 2px;">${marker} <b>${name}</b></div>
|
||||
|
|
@ -28,4 +29,4 @@ export function pieTooltipFormatter(params: any) {
|
|||
|
||||
export function pickColorByIndex(idx: number) {
|
||||
return colors[idx % colors.length];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export function sankeyTooltip(
|
||||
echartNodes: any[],
|
||||
nodeValues: Record<string, number>
|
||||
nodeValues: Record<string, number>,
|
||||
) {
|
||||
return (params: any) => {
|
||||
if ('source' in params.data && 'target' in params.data) {
|
||||
|
|
@ -25,8 +25,8 @@ export function sankeyTooltip(
|
|||
</div>
|
||||
<div class="flex items-baseline gap-2 text-black">
|
||||
<span>${params.data.value} ( ${params.data.percentage.toFixed(
|
||||
2
|
||||
)}% )</span>
|
||||
2,
|
||||
)}% )</span>
|
||||
<span class="text-disabled-text">Sessions</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -49,13 +49,12 @@ const shortenString = (str: string) => {
|
|||
const limit = 60;
|
||||
const leftPart = 25;
|
||||
const rightPart = 20;
|
||||
const safeStr =
|
||||
str.length > limit
|
||||
? `${str.slice(0, leftPart)}...${str.slice(
|
||||
str.length - rightPart,
|
||||
str.length
|
||||
)}`
|
||||
: str;
|
||||
const safeStr = str.length > limit
|
||||
? `${str.slice(0, leftPart)}...${str.slice(
|
||||
str.length - rightPart,
|
||||
str.length,
|
||||
)}`
|
||||
: str;
|
||||
|
||||
return safeStr;
|
||||
};
|
||||
|
|
@ -73,7 +72,7 @@ export const getEventPriority = (type: string): number => {
|
|||
|
||||
export const getNodeName = (
|
||||
eventType: string,
|
||||
nodeName: string | null
|
||||
nodeName: string | null,
|
||||
): string => {
|
||||
if (!nodeName) {
|
||||
return eventType.charAt(0) + eventType.slice(1).toLowerCase();
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ function buildCategoryColorMap(categories: string[]): Record<number, string> {
|
|||
*/
|
||||
export function assignColorsByCategory(
|
||||
series: any[],
|
||||
categories: string[]
|
||||
categories: string[],
|
||||
) {
|
||||
const categoryColorMap = buildCategoryColorMap(categories);
|
||||
|
||||
|
|
@ -112,8 +112,8 @@ export function customTooltipFormatter(uuid: string) {
|
|||
</div>
|
||||
|
||||
<div style="border-left: 2px solid ${
|
||||
params.color
|
||||
};" class="flex flex-col px-2 ml-2">
|
||||
params.color
|
||||
};" class="flex flex-col px-2 ml-2">
|
||||
<div class="text-neutral-600 text-sm">
|
||||
Total:
|
||||
</div>
|
||||
|
|
@ -124,8 +124,7 @@ export function customTooltipFormatter(uuid: string) {
|
|||
</div>
|
||||
`;
|
||||
if (partnerValue !== undefined) {
|
||||
const partnerColor =
|
||||
(window as any).__seriesColorMap?.[uuid]?.[partnerName] || '#999';
|
||||
const partnerColor = (window as any).__seriesColorMap?.[uuid]?.[partnerName] || '#999';
|
||||
str += `
|
||||
<div style="border-left: 2px dashed ${partnerColor};" class="flex flex-col px-2 ml-2">
|
||||
<div class="text-neutral-600 text-sm">
|
||||
|
|
@ -178,8 +177,8 @@ export function customTooltipFormatter(uuid: string) {
|
|||
</div>
|
||||
|
||||
<div style="border-left: 2px solid ${
|
||||
params.color
|
||||
};" class="flex flex-col px-2 ml-2">
|
||||
params.color
|
||||
};" class="flex flex-col px-2 ml-2">
|
||||
<div class="text-neutral-600 text-sm">
|
||||
${firstTs ? formatTimeOrDate(firstTs) : categoryLabel}
|
||||
</div>
|
||||
|
|
@ -191,8 +190,7 @@ export function customTooltipFormatter(uuid: string) {
|
|||
`;
|
||||
|
||||
if (partnerVal !== undefined) {
|
||||
const partnerColor =
|
||||
(window as any).__seriesColorMap?.[uuid]?.[partnerName] || '#999';
|
||||
const partnerColor = (window as any).__seriesColorMap?.[uuid]?.[partnerName] || '#999';
|
||||
tooltipContent += `
|
||||
<div style="border-left: 2px dashed ${partnerColor};" class="flex flex-col px-2 ml-2">
|
||||
<div class="text-neutral-600 text-sm">
|
||||
|
|
@ -262,8 +260,7 @@ export function createDataset(id: string, data: DataProps['data']) {
|
|||
const source = data.chart.map((item, idx) => {
|
||||
const row: (number | undefined)[] = [idx];
|
||||
data.namesMap.forEach((name) => {
|
||||
const val =
|
||||
typeof item[name] === 'number' ? (item[name] as number) : undefined;
|
||||
const val = typeof item[name] === 'number' ? (item[name] as number) : undefined;
|
||||
row.push(val);
|
||||
});
|
||||
return row;
|
||||
|
|
@ -279,7 +276,7 @@ export function createSeries(
|
|||
data: DataProps['data'],
|
||||
datasetId: string,
|
||||
dashed: boolean,
|
||||
hideFromLegend: boolean
|
||||
hideFromLegend: boolean,
|
||||
) {
|
||||
return data.namesMap.filter(Boolean).map((fullName) => {
|
||||
const baseName = fullName.replace(/^Previous\s+/, '');
|
||||
|
|
|
|||
|
|
@ -6,48 +6,48 @@ interface Props {
|
|||
audit: any;
|
||||
}
|
||||
function AuditDetailModal(props: Props) {
|
||||
const { audit } = props;
|
||||
// const jsonResponse = typeof audit.payload === 'string' ? JSON.parse(audit.payload) : audit.payload;
|
||||
// console.log('jsonResponse', jsonResponse)
|
||||
const { audit } = props;
|
||||
// const jsonResponse = typeof audit.payload === 'string' ? JSON.parse(audit.payload) : audit.payload;
|
||||
// console.log('jsonResponse', jsonResponse)
|
||||
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto">
|
||||
<h1 className="text-2xl p-4">Audit Details</h1>
|
||||
<div className="p-4">
|
||||
<h5 className="mb-2">{ 'URL'}</h5>
|
||||
<div className="color-gray-darkest p-2 bg-gray-lightest rounded">{ audit.endPoint }</div>
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto">
|
||||
<h1 className="text-2xl p-4">Audit Details</h1>
|
||||
<div className="p-4">
|
||||
<h5 className="mb-2">URL</h5>
|
||||
<div className="color-gray-darkest p-2 bg-gray-lightest rounded">{ audit.endPoint }</div>
|
||||
|
||||
<div className="grid grid-cols-2 my-6">
|
||||
<div className="">
|
||||
<div className="font-medium mb-2">Username</div>
|
||||
<div>{audit.username}</div>
|
||||
</div>
|
||||
<div className="">
|
||||
<div className="font-medium mb-2">Created At</div>
|
||||
<div>{audit.createdAt && checkForRecent(audit.createdAt, 'LLL dd, yyyy, hh:mm a')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 my-6">
|
||||
<div className="">
|
||||
<div className="font-medium mb-2">Action</div>
|
||||
<div>{audit.action}</div>
|
||||
</div>
|
||||
<div className="">
|
||||
<div className="font-medium mb-2">Method</div>
|
||||
<div>{audit.method}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ audit.payload && (
|
||||
<div className="my-6">
|
||||
<div className="font-medium mb-3">Payload</div>
|
||||
<JSONTree src={ audit.payload } collapsed={ false } enableClipboard />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 my-6">
|
||||
<div className="">
|
||||
<div className="font-medium mb-2">Username</div>
|
||||
<div>{audit.username}</div>
|
||||
</div>
|
||||
<div className="">
|
||||
<div className="font-medium mb-2">Created At</div>
|
||||
<div>{audit.createdAt && checkForRecent(audit.createdAt, 'LLL dd, yyyy, hh:mm a')}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
<div className="grid grid-cols-2 my-6">
|
||||
<div className="">
|
||||
<div className="font-medium mb-2">Action</div>
|
||||
<div>{audit.action}</div>
|
||||
</div>
|
||||
<div className="">
|
||||
<div className="font-medium mb-2">Method</div>
|
||||
<div>{audit.method}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ audit.payload && (
|
||||
<div className="my-6">
|
||||
<div className="font-medium mb-3">Payload</div>
|
||||
<JSONTree src={audit.payload} collapsed={false} enableClipboard />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AuditDetailModal;
|
||||
export default AuditDetailModal;
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './AuditDetailModal';
|
||||
export { default } from './AuditDetailModal';
|
||||
|
|
|
|||
|
|
@ -3,73 +3,73 @@ import { useStore } from 'App/mstore';
|
|||
import { useObserver } from 'mobx-react-lite';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Loader, Pagination, NoContent } from 'UI';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
import AuditDetailModal from '../AuditDetailModal';
|
||||
import AuditListItem from '../AuditListItem';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
|
||||
interface Props {
|
||||
|
||||
}
|
||||
function AuditList(props: Props) {
|
||||
const { auditStore } = useStore();
|
||||
const loading = useObserver(() => auditStore.isLoading);
|
||||
const list = useObserver(() => auditStore.list);
|
||||
const searchQuery = useObserver(() => auditStore.searchQuery);
|
||||
const page = useObserver(() => auditStore.page);
|
||||
const order = useObserver(() => auditStore.order);
|
||||
const period = useObserver(() => auditStore.period);
|
||||
const { showModal } = useModal();
|
||||
|
||||
useEffect(() => {
|
||||
const { startTimestamp, endTimestamp } = period.toTimestamps();
|
||||
auditStore.fetchAudits({
|
||||
page: auditStore.page,
|
||||
limit: auditStore.pageSize,
|
||||
query: auditStore.searchQuery,
|
||||
order: auditStore.order,
|
||||
startDate: startTimestamp,
|
||||
endDate: endTimestamp,
|
||||
});
|
||||
}, [page, searchQuery, order, period]);
|
||||
const { auditStore } = useStore();
|
||||
const loading = useObserver(() => auditStore.isLoading);
|
||||
const list = useObserver(() => auditStore.list);
|
||||
const searchQuery = useObserver(() => auditStore.searchQuery);
|
||||
const page = useObserver(() => auditStore.page);
|
||||
const order = useObserver(() => auditStore.order);
|
||||
const period = useObserver(() => auditStore.period);
|
||||
const { showModal } = useModal();
|
||||
|
||||
return useObserver(() => (
|
||||
<Loader loading={loading}>
|
||||
<NoContent
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.NO_AUDIT_TRAIL} size={60} />
|
||||
<div className="text-center my-4">No data available</div>
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={list.length === 0}
|
||||
>
|
||||
<div className="grid grid-cols-12 py-3 px-5 font-medium">
|
||||
<div className="col-span-5">Name</div>
|
||||
<div className="col-span-4">Action</div>
|
||||
<div className="col-span-3">Time</div>
|
||||
</div>
|
||||
useEffect(() => {
|
||||
const { startTimestamp, endTimestamp } = period.toTimestamps();
|
||||
auditStore.fetchAudits({
|
||||
page: auditStore.page,
|
||||
limit: auditStore.pageSize,
|
||||
query: auditStore.searchQuery,
|
||||
order: auditStore.order,
|
||||
startDate: startTimestamp,
|
||||
endDate: endTimestamp,
|
||||
});
|
||||
}, [page, searchQuery, order, period]);
|
||||
|
||||
{list.map((item, index) => (
|
||||
<AuditListItem
|
||||
key={index}
|
||||
audit={item}
|
||||
onShowDetails={() => showModal(<AuditDetailModal audit={item} />, { right: true, width: 500 })}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div className="w-full flex items-center justify-center py-10">
|
||||
<Pagination
|
||||
page={auditStore.page}
|
||||
total={auditStore.total}
|
||||
onPageChange={(page) => auditStore.updateKey('page', page)}
|
||||
limit={auditStore.pageSize}
|
||||
debounceRequest={200}
|
||||
/>
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
));
|
||||
return useObserver(() => (
|
||||
<Loader loading={loading}>
|
||||
<NoContent
|
||||
title={(
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.NO_AUDIT_TRAIL} size={60} />
|
||||
<div className="text-center my-4">No data available</div>
|
||||
</div>
|
||||
)}
|
||||
size="small"
|
||||
show={list.length === 0}
|
||||
>
|
||||
<div className="grid grid-cols-12 py-3 px-5 font-medium">
|
||||
<div className="col-span-5">Name</div>
|
||||
<div className="col-span-4">Action</div>
|
||||
<div className="col-span-3">Time</div>
|
||||
</div>
|
||||
|
||||
{list.map((item, index) => (
|
||||
<AuditListItem
|
||||
key={index}
|
||||
audit={item}
|
||||
onShowDetails={() => showModal(<AuditDetailModal audit={item} />, { right: true, width: 500 })}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div className="w-full flex items-center justify-center py-10">
|
||||
<Pagination
|
||||
page={auditStore.page}
|
||||
total={auditStore.total}
|
||||
onPageChange={(page) => auditStore.updateKey('page', page)}
|
||||
limit={auditStore.pageSize}
|
||||
debounceRequest={200}
|
||||
/>
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
));
|
||||
}
|
||||
|
||||
export default AuditList;
|
||||
export default AuditList;
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './AuditList'
|
||||
export { default } from './AuditList';
|
||||
|
|
|
|||
|
|
@ -6,14 +6,14 @@ interface Props {
|
|||
onShowDetails: () => void;
|
||||
}
|
||||
function AuditListItem(props: Props) {
|
||||
const { audit, onShowDetails } = props;
|
||||
return (
|
||||
<div className="grid grid-cols-12 py-4 px-5 border-t items-center select-none hover:bg-active-blue group">
|
||||
<div className="col-span-5">{audit.username}</div>
|
||||
<div className="col-span-4 link cursor-pointer select-none" onClick={onShowDetails}>{audit.action}</div>
|
||||
<div className="col-span-3">{audit.createdAt && checkForRecent(audit.createdAt, 'LLL dd, yyyy, hh:mm a')}</div>
|
||||
</div>
|
||||
);
|
||||
const { audit, onShowDetails } = props;
|
||||
return (
|
||||
<div className="grid grid-cols-12 py-4 px-5 border-t items-center select-none hover:bg-active-blue group">
|
||||
<div className="col-span-5">{audit.username}</div>
|
||||
<div className="col-span-4 link cursor-pointer select-none" onClick={onShowDetails}>{audit.action}</div>
|
||||
<div className="col-span-3">{audit.createdAt && checkForRecent(audit.createdAt, 'LLL dd, yyyy, hh:mm a')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AuditListItem;
|
||||
export default AuditListItem;
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './AuditListItem';
|
||||
export { default } from './AuditListItem';
|
||||
|
|
|
|||
|
|
@ -2,33 +2,33 @@ import React, { useEffect } from 'react';
|
|||
import { Icon, Input } from 'UI';
|
||||
import { debounce } from 'App/utils';
|
||||
|
||||
let debounceUpdate: any = () => {}
|
||||
let debounceUpdate: any = () => {};
|
||||
interface Props {
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
function AuditSearchField(props: Props) {
|
||||
const { onChange } = props;
|
||||
|
||||
useEffect(() => {
|
||||
debounceUpdate = debounce((value) => onChange(value), 500);
|
||||
}, [])
|
||||
const { onChange } = props;
|
||||
|
||||
const write = ({ target: { name, value } }) => {
|
||||
debounceUpdate(value);
|
||||
}
|
||||
useEffect(() => {
|
||||
debounceUpdate = debounce((value) => onChange(value), 500);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative" style={{ width: '220px'}}>
|
||||
<Icon name="search" className="absolute top-0 bottom-0 ml-3 m-auto" size="16" />
|
||||
<Input
|
||||
name="searchQuery"
|
||||
const write = ({ target: { name, value } }) => {
|
||||
debounceUpdate(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative" style={{ width: '220px' }}>
|
||||
<Icon name="search" className="absolute top-0 bottom-0 ml-3 m-auto" size="16" />
|
||||
<Input
|
||||
name="searchQuery"
|
||||
// className="bg-white p-2 border border-gray-light rounded w-full pl-10"
|
||||
placeholder="Filter by name"
|
||||
onChange={write}
|
||||
icon="search"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
placeholder="Filter by name"
|
||||
onChange={write}
|
||||
icon="search"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AuditSearchField;
|
||||
export default AuditSearchField;
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './AuditSearchField';
|
||||
export { default } from './AuditSearchField';
|
||||
|
|
|
|||
|
|
@ -1,77 +1,77 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { PageTitle, Icon } from 'UI';
|
||||
import { Button } from 'antd'
|
||||
import AuditList from '../AuditList';
|
||||
import AuditSearchField from '../AuditSearchField';
|
||||
import { Button } from 'antd';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import Select from 'Shared/Select';
|
||||
import SelectDateRange from 'Shared/SelectDateRange';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import AuditSearchField from '../AuditSearchField';
|
||||
import AuditList from '../AuditList';
|
||||
|
||||
function AuditView() {
|
||||
const { auditStore } = useStore();
|
||||
const order = useObserver(() => auditStore.order);
|
||||
const total = useObserver(() => numberWithCommas(auditStore.total));
|
||||
const { auditStore } = useStore();
|
||||
const order = useObserver(() => auditStore.order);
|
||||
const total = useObserver(() => numberWithCommas(auditStore.total));
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
auditStore.updateKey('searchQuery', '');
|
||||
}
|
||||
}, [])
|
||||
useEffect(() => () => {
|
||||
auditStore.updateKey('searchQuery', '');
|
||||
}, []);
|
||||
|
||||
const exportToCsv = () => {
|
||||
auditStore.exportToCsv();
|
||||
}
|
||||
const exportToCsv = () => {
|
||||
auditStore.exportToCsv();
|
||||
};
|
||||
|
||||
const onChange = (data) => {
|
||||
auditStore.setDateRange(data);
|
||||
}
|
||||
const onChange = (data) => {
|
||||
auditStore.setDateRange(data);
|
||||
};
|
||||
|
||||
return useObserver(() => (
|
||||
<div className="bg-white rounded-lg shadow-sm border">
|
||||
<div className="flex items-center mb-4 px-5 pt-5">
|
||||
<PageTitle title={
|
||||
<div className="flex items-center">
|
||||
<span>Audit Trail</span>
|
||||
<span className="color-gray-medium ml-2">{total}</span>
|
||||
</div>
|
||||
} />
|
||||
<div className="flex items-center ml-auto">
|
||||
<div className="mx-2">
|
||||
<SelectDateRange
|
||||
period={auditStore.period}
|
||||
onChange={onChange}
|
||||
right={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-2">
|
||||
<Select
|
||||
options={[
|
||||
{ label: 'Newest First', value: 'desc' },
|
||||
{ label: 'Oldest First', value: 'asc' },
|
||||
]}
|
||||
defaultValue={order}
|
||||
plain
|
||||
onChange={({ value }) => auditStore.updateKey('order', value.value)}
|
||||
/>
|
||||
</div>
|
||||
<AuditSearchField onChange={(value) => {
|
||||
auditStore.updateKey('searchQuery', value);
|
||||
auditStore.updateKey('page', 1)
|
||||
} }/>
|
||||
<div>
|
||||
<Button type="text" icon={<Icon name="grid-3x3" color="teal" />} className="ml-3" onClick={exportToCsv}>
|
||||
<span className="ml-2">Export to CSV</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AuditList />
|
||||
return useObserver(() => (
|
||||
<div className="bg-white rounded-lg shadow-sm border">
|
||||
<div className="flex items-center mb-4 px-5 pt-5">
|
||||
<PageTitle title={(
|
||||
<div className="flex items-center">
|
||||
<span>Audit Trail</span>
|
||||
<span className="color-gray-medium ml-2">{total}</span>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center ml-auto">
|
||||
<div className="mx-2">
|
||||
<SelectDateRange
|
||||
period={auditStore.period}
|
||||
onChange={onChange}
|
||||
right
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-2">
|
||||
<Select
|
||||
options={[
|
||||
{ label: 'Newest First', value: 'desc' },
|
||||
{ label: 'Oldest First', value: 'asc' },
|
||||
]}
|
||||
defaultValue={order}
|
||||
plain
|
||||
onChange={({ value }) => auditStore.updateKey('order', value.value)}
|
||||
/>
|
||||
</div>
|
||||
<AuditSearchField onChange={(value) => {
|
||||
auditStore.updateKey('searchQuery', value);
|
||||
auditStore.updateKey('page', 1);
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<Button type="text" icon={<Icon name="grid-3x3" color="teal" />} className="ml-3" onClick={exportToCsv}>
|
||||
<span className="ml-2">Export to CSV</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
</div>
|
||||
|
||||
<AuditList />
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
export default withPageTitle('Audit Trail - OpenReplay Preferences')(AuditView);
|
||||
export default withPageTitle('Audit Trail - OpenReplay Preferences')(AuditView);
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './AuditView'
|
||||
export { default } from './AuditView';
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { withRouter } from 'react-router-dom';
|
|||
import { Switch, Route, Redirect } from 'react-router';
|
||||
import { CLIENT_TABS, client as clientRoute } from 'App/routes';
|
||||
|
||||
import SessionsListingSettings from 'Components/Client/SessionsListingSettings';
|
||||
import Modules from 'Components/Client/Modules';
|
||||
import ProfileSettings from './ProfileSettings';
|
||||
import Integrations from './Integrations';
|
||||
import UserView from './Users/UsersView';
|
||||
|
|
@ -13,8 +15,6 @@ import CustomFields from './CustomFields';
|
|||
import Webhooks from './Webhooks';
|
||||
import Notifications from './Notifications';
|
||||
import Roles from './Roles';
|
||||
import SessionsListingSettings from 'Components/Client/SessionsListingSettings';
|
||||
import Modules from 'Components/Client/Modules';
|
||||
|
||||
@withRouter
|
||||
export default class Client extends React.PureComponent {
|
||||
|
|
@ -46,11 +46,11 @@ export default class Client extends React.PureComponent {
|
|||
render() {
|
||||
const {
|
||||
match: {
|
||||
params: { activeTab }
|
||||
}
|
||||
params: { activeTab },
|
||||
},
|
||||
} = this.props;
|
||||
return (
|
||||
<div className='w-full mx-auto mb-8' style={{ maxWidth: '1360px' }}>
|
||||
<div className="w-full mx-auto mb-8" style={{ maxWidth: '1360px' }}>
|
||||
{activeTab && this.renderActiveTab()}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import { edit, save } from 'Duck/customField';
|
|||
import { Form, Input, Button } from 'UI';
|
||||
import styles from './customFieldForm.module.css';
|
||||
|
||||
const CustomFieldForm = ({ field, saving, errors, edit, save, onSave, onClose, onDelete }) => {
|
||||
function CustomFieldForm({
|
||||
field, saving, errors, edit, save, onSave, onClose, onDelete,
|
||||
}) {
|
||||
const focusElementRef = useRef(null);
|
||||
|
||||
const setFocus = () => focusElementRef.current.focus();
|
||||
|
|
@ -15,10 +17,14 @@ const CustomFieldForm = ({ field, saving, errors, edit, save, onSave, onClose, o
|
|||
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto">
|
||||
<h3 className="p-5 text-xl">{exists ? 'Update' : 'Add'} Metadata Field</h3>
|
||||
<h3 className="p-5 text-xl">
|
||||
{exists ? 'Update' : 'Add'}
|
||||
{' '}
|
||||
Metadata Field
|
||||
</h3>
|
||||
<Form className={styles.wrapper}>
|
||||
<Form.Field>
|
||||
<label>{'Field Name'}</label>
|
||||
<label>Field Name</label>
|
||||
<Input
|
||||
ref={focusElementRef}
|
||||
name="key"
|
||||
|
|
@ -41,21 +47,21 @@ const CustomFieldForm = ({ field, saving, errors, edit, save, onSave, onClose, o
|
|||
{exists ? 'Update' : 'Add'}
|
||||
</Button>
|
||||
<Button data-hidden={!exists} onClick={onClose}>
|
||||
{'Cancel'}
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button variant="text" icon="trash" data-hidden={!exists} onClick={onDelete}></Button>
|
||||
<Button variant="text" icon="trash" data-hidden={!exists} onClick={onDelete} />
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import React, { useRef, useState } from 'react';
|
||||
import { Form, Input } 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, Modal } from 'antd';
|
||||
import { Trash } from 'UI/Icons';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import styles from './customFieldForm.module.css';
|
||||
|
||||
interface CustomFieldFormProps {
|
||||
siteId: string;
|
||||
|
|
@ -25,11 +25,11 @@ const CustomFieldForm: React.FC<CustomFieldFormProps> = ({ siteId }) => {
|
|||
const onDelete = async () => {
|
||||
Modal.confirm({
|
||||
title: 'Metadata',
|
||||
content: `Are you sure you want to remove?`,
|
||||
content: 'Are you sure you want to remove?',
|
||||
onOk: async () => {
|
||||
await store.remove(siteId, field?.index!);
|
||||
hideModal();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -51,10 +51,14 @@ const CustomFieldForm: React.FC<CustomFieldFormProps> = ({ siteId }) => {
|
|||
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto">
|
||||
<h3 className="p-5 text-xl">{exists ? 'Update' : 'Add'} Metadata Field</h3>
|
||||
<h3 className="p-5 text-xl">
|
||||
{exists ? 'Update' : 'Add'}
|
||||
{' '}
|
||||
Metadata Field
|
||||
</h3>
|
||||
<Form className={styles.wrapper}>
|
||||
<Form.Field>
|
||||
<label>{'Field Name'}</label>
|
||||
<label>Field Name</label>
|
||||
<Input
|
||||
ref={focusElementRef}
|
||||
name="key"
|
||||
|
|
@ -76,12 +80,12 @@ const CustomFieldForm: React.FC<CustomFieldFormProps> = ({ siteId }) => {
|
|||
>
|
||||
{exists ? 'Update' : 'Add'}
|
||||
</Button>
|
||||
<Button type='text' data-hidden={!exists} onClick={hideModal}>
|
||||
{'Cancel'}
|
||||
<Button type="text" data-hidden={!exists} onClick={hideModal}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button type="text" icon={<Trash />} data-hidden={!exists} onClick={onDelete}></Button>
|
||||
<Button type="text" icon={<Trash />} data-hidden={!exists} onClick={onDelete} />
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import CustomFieldForm from './CustomFieldForm';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { List, Space, Typography, Button, Tooltip, Empty } from 'antd';
|
||||
import {
|
||||
List, Space, Typography, Button, Tooltip, Empty,
|
||||
} from 'antd';
|
||||
import { PlusIcon, Tags } from 'lucide-react';
|
||||
import {EditOutlined } from '@ant-design/icons';
|
||||
import { EditOutlined } from '@ant-design/icons';
|
||||
import usePageTitle from '@/hooks/usePageTitle';
|
||||
import CustomFieldForm from './CustomFieldForm';
|
||||
|
||||
|
||||
const CustomFields = () => {
|
||||
function CustomFields() {
|
||||
usePageTitle('Metadata - OpenReplay Preferences');
|
||||
const { customFieldStore: store, projectsStore } = useStore();
|
||||
const currentSite = projectsStore.config.project;
|
||||
|
|
@ -27,8 +28,8 @@ const CustomFields = () => {
|
|||
|
||||
const handleInit = (field?: any) => {
|
||||
store.init(field);
|
||||
showModal(<CustomFieldForm siteId={currentSite?.projectId + ''} />, {
|
||||
title: field ? 'Edit Metadata' : 'Add Metadata', right: true
|
||||
showModal(<CustomFieldForm siteId={`${currentSite?.projectId}`} />, {
|
||||
title: field ? 'Edit Metadata' : 'Add Metadata', right: true,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -39,7 +40,7 @@ const CustomFields = () => {
|
|||
<Typography.Text>
|
||||
Attach key-value pairs to session replays for enhanced filtering, searching, and identifying relevant user
|
||||
sessions.
|
||||
<a href="https://docs.openreplay.com/installation/metadata" className="link ml-1" target="_blank">
|
||||
<a href="https://docs.openreplay.com/installation/metadata" className="link ml-1" target="_blank" rel="noreferrer">
|
||||
Learn more
|
||||
</a>
|
||||
</Typography.Text>
|
||||
|
|
@ -48,13 +49,17 @@ const CustomFields = () => {
|
|||
<Tooltip
|
||||
title={remaining > 0 ? '' : 'You\'ve reached the limit of 10 metadata.'}
|
||||
>
|
||||
<Button icon={<PlusIcon size={18} />} type="primary" size='small'
|
||||
disabled={remaining === 0}
|
||||
onClick={() => handleInit()}>
|
||||
<Button
|
||||
icon={<PlusIcon size={18} />}
|
||||
type="primary"
|
||||
size="small"
|
||||
disabled={remaining === 0}
|
||||
onClick={() => handleInit()}
|
||||
>
|
||||
Add Metadata
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{/*{remaining === 0 && <Icon name="info-circle" size={16} color="black" />}*/}
|
||||
{/* {remaining === 0 && <Icon name="info-circle" size={16} color="black" />} */}
|
||||
<Typography.Text type="secondary">
|
||||
{remaining === 0 ? 'You have reached the limit of 10 metadata.' : `${remaining}/10 Remaining for this project`}
|
||||
</Typography.Text>
|
||||
|
|
@ -62,7 +67,7 @@ const CustomFields = () => {
|
|||
|
||||
<List
|
||||
locale={{
|
||||
emptyText: <Empty description="None added yet" image={<AnimatedSVG name={ICONS.NO_METADATA} size={60} />} />
|
||||
emptyText: <Empty description="None added yet" image={<AnimatedSVG name={ICONS.NO_METADATA} size={60} />} />,
|
||||
}}
|
||||
loading={loading}
|
||||
dataSource={fields}
|
||||
|
|
@ -71,7 +76,7 @@ const CustomFields = () => {
|
|||
onClick={() => handleInit(field)}
|
||||
className="cursor-pointer group hover:bg-active-blue !px-4"
|
||||
actions={[
|
||||
<Button type='link' className="opacity-0 group-hover:!opacity-100" icon={<EditOutlined size={14} />} />
|
||||
<Button type="link" className="opacity-0 group-hover:!opacity-100" icon={<EditOutlined size={14} />} />,
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
|
|
@ -79,9 +84,10 @@ const CustomFields = () => {
|
|||
avatar={<Tags size={20} />}
|
||||
/>
|
||||
</List.Item>
|
||||
)} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default observer(CustomFields);
|
||||
|
|
|
|||
|
|
@ -4,24 +4,24 @@ import { Icon } from 'UI';
|
|||
import { Button } from 'antd';
|
||||
import styles from './listItem.module.css';
|
||||
|
||||
const ListItem = ({ field, onEdit, disabled }) => {
|
||||
function 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
|
||||
}
|
||||
[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 type="text" icon={<Icon name={"pencil"} size={16} />} />
|
||||
<Button type="text" icon={<Icon name="pencil" size={16} />} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default ListItem;
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './CustomFields';
|
||||
export { default } from './CustomFields';
|
||||
|
|
|
|||
|
|
@ -1,33 +1,32 @@
|
|||
import React from 'react'
|
||||
import { KEY, options } from 'App/dev/console'
|
||||
import React from 'react';
|
||||
import { KEY, options } from 'App/dev/console';
|
||||
import { Switch } from 'UI';
|
||||
|
||||
function getDefaults() {
|
||||
const storedString = localStorage.getItem(KEY)
|
||||
const storedString = localStorage.getItem(KEY);
|
||||
if (storedString) {
|
||||
const storedOptions = JSON.parse(storedString)
|
||||
return storedOptions.verbose
|
||||
} else {
|
||||
return false
|
||||
const storedOptions = JSON.parse(storedString);
|
||||
return storedOptions.verbose;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function DebugLog() {
|
||||
const [showLogs, setShowLogs] = React.useState(getDefaults)
|
||||
const [showLogs, setShowLogs] = React.useState(getDefaults);
|
||||
|
||||
const onChange = (checked: boolean) => {
|
||||
setShowLogs(checked)
|
||||
options.logStuff(checked)
|
||||
}
|
||||
setShowLogs(checked);
|
||||
options.logStuff(checked);
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<h3 className={'text-lg'}>Player Debug Logs</h3>
|
||||
<div className={'my-1'}>Show debug information in browser console.</div>
|
||||
<div className={'mt-2'}>
|
||||
<h3 className="text-lg">Player Debug Logs</h3>
|
||||
<div className="my-1">Show debug information in browser console.</div>
|
||||
<div className="mt-2">
|
||||
<Switch checked={showLogs} onChange={onChange} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default DebugLog
|
||||
export default DebugLog;
|
||||
|
|
|
|||
|
|
@ -23,15 +23,15 @@ const initialValues = {
|
|||
app_key: '',
|
||||
};
|
||||
|
||||
const DatadogFormModal = ({
|
||||
function DatadogFormModal({
|
||||
onClose,
|
||||
integrated,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
integrated: boolean;
|
||||
}) => {
|
||||
}) {
|
||||
const { integrationsStore } = useStore();
|
||||
const siteId = integrationsStore.integrations.siteId;
|
||||
const { siteId } = integrationsStore.integrations;
|
||||
|
||||
const {
|
||||
data = initialValues,
|
||||
|
|
@ -39,7 +39,9 @@ const DatadogFormModal = ({
|
|||
saveMutation,
|
||||
removeMutation,
|
||||
} = useIntegration<DatadogConfig>('datadog', siteId, initialValues);
|
||||
const { values, errors, handleChange, hasErrors, checkErrors } = useForm(data, {
|
||||
const {
|
||||
values, errors, handleChange, hasErrors, checkErrors,
|
||||
} = useForm(data, {
|
||||
site: {
|
||||
required: true,
|
||||
},
|
||||
|
|
@ -59,7 +61,7 @@ const DatadogFormModal = ({
|
|||
try {
|
||||
await saveMutation.mutateAsync({ values, siteId, exists });
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
console.error(e);
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
|
@ -68,7 +70,7 @@ const DatadogFormModal = ({
|
|||
try {
|
||||
await removeMutation.mutateAsync({ siteId });
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
console.error(e);
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
|
@ -117,7 +119,7 @@ const DatadogFormModal = ({
|
|||
onChange={handleChange}
|
||||
errors={errors.app_key}
|
||||
/>
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={save}
|
||||
disabled={hasErrors}
|
||||
|
|
@ -129,7 +131,7 @@ const DatadogFormModal = ({
|
|||
|
||||
{integrated && (
|
||||
<Button loading={removeMutation.isPending} onClick={remove}>
|
||||
{'Delete'}
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -137,7 +139,7 @@ const DatadogFormModal = ({
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
DatadogFormModal.displayName = 'DatadogForm';
|
||||
|
||||
|
|
|
|||
|
|
@ -24,22 +24,24 @@ const initialValues = {
|
|||
client_secret: '',
|
||||
resource: '',
|
||||
};
|
||||
const DynatraceFormModal = ({
|
||||
function DynatraceFormModal({
|
||||
onClose,
|
||||
integrated,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
integrated: boolean;
|
||||
}) => {
|
||||
}) {
|
||||
const { integrationsStore } = useStore();
|
||||
const siteId = integrationsStore.integrations.siteId;
|
||||
const { siteId } = integrationsStore.integrations;
|
||||
const {
|
||||
data = initialValues,
|
||||
isPending,
|
||||
saveMutation,
|
||||
removeMutation,
|
||||
} = useIntegration<DynatraceConfig>('dynatrace', siteId, initialValues);
|
||||
const { values, errors, handleChange, hasErrors, checkErrors } = useForm(data, {
|
||||
const {
|
||||
values, errors, handleChange, hasErrors, checkErrors,
|
||||
} = useForm(data, {
|
||||
environment: {
|
||||
required: true,
|
||||
},
|
||||
|
|
@ -62,7 +64,7 @@ const DynatraceFormModal = ({
|
|||
try {
|
||||
await saveMutation.mutateAsync({ values, siteId, exists });
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
console.error(e);
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
|
@ -71,7 +73,7 @@ const DynatraceFormModal = ({
|
|||
try {
|
||||
await removeMutation.mutateAsync({ siteId });
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
console.error(e);
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
|
@ -136,7 +138,7 @@ const DynatraceFormModal = ({
|
|||
errors={errors.resource}
|
||||
/>
|
||||
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={save}
|
||||
disabled={hasErrors}
|
||||
|
|
@ -148,7 +150,7 @@ const DynatraceFormModal = ({
|
|||
|
||||
{integrated && (
|
||||
<Button loading={removeMutation.isPending} onClick={remove}>
|
||||
{'Delete'}
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -156,7 +158,7 @@ const DynatraceFormModal = ({
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
DynatraceFormModal.displayName = 'DynatraceFormModal';
|
||||
|
||||
|
|
|
|||
|
|
@ -33,14 +33,16 @@ function ElasticsearchForm({
|
|||
integrated: boolean;
|
||||
}) {
|
||||
const { integrationsStore } = useStore();
|
||||
const siteId = integrationsStore.integrations.siteId;
|
||||
const { siteId } = integrationsStore.integrations;
|
||||
const {
|
||||
data = initialValues,
|
||||
isPending,
|
||||
saveMutation,
|
||||
removeMutation,
|
||||
} = useIntegration<ElasticConfig>('elasticsearch', siteId, initialValues);
|
||||
const { values, errors, handleChange, hasErrors, checkErrors } = useForm(data, {
|
||||
const {
|
||||
values, errors, handleChange, hasErrors, checkErrors,
|
||||
} = useForm(data, {
|
||||
url: {
|
||||
required: true,
|
||||
},
|
||||
|
|
@ -60,7 +62,7 @@ function ElasticsearchForm({
|
|||
try {
|
||||
await saveMutation.mutateAsync({ values, siteId, exists });
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
console.error(e);
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
|
@ -69,7 +71,7 @@ function ElasticsearchForm({
|
|||
try {
|
||||
await removeMutation.mutateAsync({ siteId });
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
console.error(e);
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
|
@ -126,7 +128,7 @@ function ElasticsearchForm({
|
|||
onChange={handleChange}
|
||||
errors={errors.indexes}
|
||||
/>
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={save}
|
||||
disabled={hasErrors}
|
||||
|
|
@ -138,7 +140,7 @@ function ElasticsearchForm({
|
|||
|
||||
{integrated && (
|
||||
<Button loading={removeMutation.isPending} onClick={remove}>
|
||||
{'Delete'}
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -33,14 +33,16 @@ function SentryForm({
|
|||
integrated: boolean;
|
||||
}) {
|
||||
const { integrationsStore } = useStore();
|
||||
const siteId = integrationsStore.integrations.siteId;
|
||||
const { siteId } = integrationsStore.integrations;
|
||||
const {
|
||||
data = initialValues,
|
||||
isPending,
|
||||
saveMutation,
|
||||
removeMutation,
|
||||
} = useIntegration<SentryConfig>('sentry', siteId, initialValues);
|
||||
const { values, errors, handleChange, hasErrors, checkErrors, } = useForm(data, {
|
||||
const {
|
||||
values, errors, handleChange, hasErrors, checkErrors,
|
||||
} = useForm(data, {
|
||||
url: {
|
||||
required: false,
|
||||
},
|
||||
|
|
@ -63,7 +65,7 @@ function SentryForm({
|
|||
try {
|
||||
await saveMutation.mutateAsync({ values, siteId, exists });
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
console.error(e);
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
|
@ -72,7 +74,7 @@ function SentryForm({
|
|||
try {
|
||||
await removeMutation.mutateAsync({ siteId });
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
console.error(e);
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
|
@ -130,7 +132,7 @@ function SentryForm({
|
|||
errors={errors.token}
|
||||
/>
|
||||
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={save}
|
||||
disabled={hasErrors}
|
||||
|
|
@ -142,7 +144,7 @@ function SentryForm({
|
|||
|
||||
{integrated && (
|
||||
<Button loading={removeMutation.isPending} onClick={remove}>
|
||||
{'Delete'}
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { Input } from 'antd'
|
||||
import React from 'react';
|
||||
import { Input } from 'antd';
|
||||
|
||||
export function FormField({
|
||||
label,
|
||||
|
|
@ -30,4 +30,4 @@ export function FormField({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
export default FormField;
|
||||
export default FormField;
|
||||
|
|
|
|||
|
|
@ -1,32 +1,37 @@
|
|||
import React from 'react';
|
||||
import IntegrationForm from './IntegrationForm';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModalCard';
|
||||
import IntegrationForm from './IntegrationForm';
|
||||
|
||||
const GithubForm = (props) => (
|
||||
<div className='bg-white h-screen overflow-y-auto' style={{ width: '350px' }}>
|
||||
<IntegrationModalCard title='Github' icon='integrations/github'
|
||||
description='Integrate GitHub with OpenReplay to enable the direct creation of a new issue from a session.' />
|
||||
<div className='p-5 border-b mb-4'>
|
||||
<div>Integrate GitHub with OpenReplay and create issues directly from the recording page.</div>
|
||||
<div className='mt-8'>
|
||||
<DocLink className='mt-4' label='Integrate Github' url='https://docs.openreplay.com/integrations/github' />
|
||||
function GithubForm(props) {
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<IntegrationModalCard
|
||||
title="Github"
|
||||
icon="integrations/github"
|
||||
description="Integrate GitHub with OpenReplay to enable the direct creation of a new issue from a session."
|
||||
/>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>Integrate GitHub with OpenReplay and create issues directly from the recording page.</div>
|
||||
<div className="mt-8">
|
||||
<DocLink className="mt-4" label="Integrate Github" url="https://docs.openreplay.com/integrations/github" />
|
||||
</div>
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{...props}
|
||||
ignoreProject
|
||||
name="github"
|
||||
customPath="github"
|
||||
formFields={[
|
||||
{
|
||||
key: 'token',
|
||||
label: 'Token',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{...props}
|
||||
ignoreProject
|
||||
name='github'
|
||||
customPath='github'
|
||||
formFields={[
|
||||
{
|
||||
key: 'token',
|
||||
label: 'Token'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
GithubForm.displayName = 'GithubForm';
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import React from 'react';
|
|||
|
||||
import { Icon } from 'UI';
|
||||
|
||||
|
||||
interface Props {
|
||||
onChange: any;
|
||||
activeItem: string;
|
||||
|
|
@ -14,23 +13,22 @@ interface Props {
|
|||
const allItem = { key: 'all', title: 'All' };
|
||||
|
||||
function IntegrationFilters(props: Props) {
|
||||
|
||||
const segmentItems = [allItem, ...props.filters].map((item: any) => ({
|
||||
key: item.key,
|
||||
value: item.key,
|
||||
label: (
|
||||
<div className={'flex items-center gap-2'}>
|
||||
{item.icon ? <Icon name={item.icon} color={'inherit'} /> : null}
|
||||
<div className="flex items-center gap-2">
|
||||
{item.icon ? <Icon name={item.icon} color="inherit" /> : null}
|
||||
<div>{item.title}</div>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
}));
|
||||
|
||||
const onChange = (val) => {
|
||||
props.onChange(val)
|
||||
}
|
||||
props.onChange(val);
|
||||
};
|
||||
return (
|
||||
<div className='flex items-center gap-4'>
|
||||
<div className="flex items-center gap-4">
|
||||
<Segmented
|
||||
value={props.activeItem}
|
||||
onChange={onChange}
|
||||
|
|
@ -40,4 +38,4 @@ function IntegrationFilters(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default IntegrationFilters;
|
||||
export default IntegrationFilters;
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ import React from 'react';
|
|||
|
||||
import { useStore } from 'App/mstore';
|
||||
import { namedStore } from 'App/mstore/integrationsStore';
|
||||
import { Checkbox, Form, Input, Loader } from 'UI';
|
||||
import { Button } from 'antd'
|
||||
import {
|
||||
Checkbox, Form, Input, Loader,
|
||||
} from 'UI';
|
||||
import { Button } from 'antd';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
function IntegrationForm(props: any) {
|
||||
|
|
@ -13,17 +15,21 @@ function IntegrationForm(props: any) {
|
|||
const initialSiteId = integrationsStore.integrations.siteId;
|
||||
const integrationStore = integrationsStore[name as unknown as namedStore];
|
||||
const config = integrationStore.instance;
|
||||
const loading = integrationStore.loading;
|
||||
const { loading } = integrationStore;
|
||||
const onSave = integrationStore.saveIntegration;
|
||||
const onRemove = integrationStore.deleteIntegration;
|
||||
const edit = integrationStore.edit;
|
||||
const { edit } = integrationStore;
|
||||
const fetchIntegrationList = integrationsStore.integrations.fetchIntegrations;
|
||||
|
||||
const fetchList = () => {
|
||||
void fetchIntegrationList(initialSiteId);
|
||||
};
|
||||
|
||||
const write = ({ target: { value, name: key, type, checked } }) => {
|
||||
const write = ({
|
||||
target: {
|
||||
value, name: key, type, checked,
|
||||
},
|
||||
}) => {
|
||||
if (type === 'checkbox') edit({ [key]: checked });
|
||||
else edit({ [key]: value });
|
||||
};
|
||||
|
|
@ -65,10 +71,9 @@ function IntegrationForm(props: any) {
|
|||
type = 'text',
|
||||
checkIfDisplayed,
|
||||
autoFocus = false,
|
||||
}) =>
|
||||
(typeof checkIfDisplayed !== 'function' ||
|
||||
checkIfDisplayed(config)) &&
|
||||
(type === 'checkbox' ? (
|
||||
}) => (typeof checkIfDisplayed !== 'function'
|
||||
|| checkIfDisplayed(config))
|
||||
&& (type === 'checkbox' ? (
|
||||
<Form.Field key={key}>
|
||||
<Checkbox
|
||||
label={label}
|
||||
|
|
@ -91,7 +96,7 @@ function IntegrationForm(props: any) {
|
|||
autoFocus={autoFocus}
|
||||
/>
|
||||
</Form.Field>
|
||||
))
|
||||
)),
|
||||
)}
|
||||
|
||||
<Button
|
||||
|
|
@ -106,7 +111,7 @@ function IntegrationForm(props: any) {
|
|||
|
||||
{integrated && (
|
||||
<Button loading={loading} onClick={remove}>
|
||||
{'Delete'}
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Icon } from 'UI';
|
||||
import stl from './integrationItem.module.css';
|
||||
import { Tooltip } from 'antd';
|
||||
import stl from './integrationItem.module.css';
|
||||
|
||||
interface Props {
|
||||
integration: any;
|
||||
|
|
@ -12,32 +12,34 @@ interface Props {
|
|||
useIcon?: boolean;
|
||||
}
|
||||
|
||||
const IntegrationItem = (props: Props) => {
|
||||
const { integration, integrated, hide = false, useIcon } = props;
|
||||
function IntegrationItem(props: Props) {
|
||||
const {
|
||||
integration, integrated, hide = false, useIcon,
|
||||
} = props;
|
||||
return hide ? null : (
|
||||
<div
|
||||
className={cn('flex flex-col border rounded-lg p-3 bg-white relative justify-between cursor-pointer hover:bg-active-blue')}
|
||||
onClick={(e) => props.onClick(e)}
|
||||
style={{ height: '136px' }}
|
||||
>
|
||||
<div className='flex gap-3'>
|
||||
<div className="flex gap-3">
|
||||
<div className="shrink-0">
|
||||
{useIcon ? <Icon name={integration.icon} size={40} /> : <img className="h-10 w-10" src={"/assets/" + integration.icon + ".svg"} alt="integration" />}
|
||||
{useIcon ? <Icon name={integration.icon} size={40} /> : <img className="h-10 w-10" src={`/assets/${integration.icon}.svg`} alt="integration" />}
|
||||
</div>
|
||||
<div className='flex flex-col'>
|
||||
<h4 className='text-lg'>{integration.title}</h4>
|
||||
<p className='text-sm color-gray-medium m-0 p-0 h-3'>{integration.subtitle && integration.subtitle}</p>
|
||||
<div className="flex flex-col">
|
||||
<h4 className="text-lg">{integration.title}</h4>
|
||||
<p className="text-sm color-gray-medium m-0 p-0 h-3">{integration.subtitle && integration.subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{integrated && (
|
||||
<div className='ml-12 p-1 flex items-center justify-center color-tealx border rounded w-fit'>
|
||||
<Icon name='check-circle-fill' size='14' color='tealx' className="mr-2" />
|
||||
<span>Integrated</span>
|
||||
</div>
|
||||
<div className="ml-12 p-1 flex items-center justify-center color-tealx border rounded w-fit">
|
||||
<Icon name="check-circle-fill" size="14" color="tealx" className="mr-2" />
|
||||
<span>Integrated</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default IntegrationItem;
|
||||
|
|
|
|||
|
|
@ -9,18 +9,20 @@ interface Props {
|
|||
}
|
||||
|
||||
function IntegrationModalCard(props: Props) {
|
||||
const { title, icon, description, useIcon } = props;
|
||||
const {
|
||||
title, icon, description, useIcon,
|
||||
} = props;
|
||||
return (
|
||||
<div className='flex items-start p-5 gap-4'>
|
||||
<div className='border rounded-lg p-2 shrink-0'>
|
||||
{useIcon ? <Icon name={icon} size={80} /> : <img className="h-20 w-20" src={"/assets/" + icon + ".svg"} alt="integration" />}
|
||||
<div className="flex items-start p-5 gap-4">
|
||||
<div className="border rounded-lg p-2 shrink-0">
|
||||
{useIcon ? <Icon name={icon} size={80} /> : <img className="h-20 w-20" src={`/assets/${icon}.svg`} alt="integration" />}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className='text-2xl'>{title}</h3>
|
||||
<h3 className="text-2xl">{title}</h3>
|
||||
<div>{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default IntegrationModalCard;
|
||||
export default IntegrationModalCard;
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ interface Props {
|
|||
function Integrations(props: Props) {
|
||||
const { integrationsStore, projectsStore } = useStore();
|
||||
const initialSiteId = projectsStore.siteId;
|
||||
const siteId = integrationsStore.integrations.siteId;
|
||||
const { siteId } = integrationsStore.integrations;
|
||||
const fetchIntegrationList = integrationsStore.integrations.fetchIntegrations;
|
||||
const storeIntegratedList = integrationsStore.integrations.list;
|
||||
const { hideHeader = false } = props;
|
||||
|
|
@ -61,9 +61,9 @@ function Integrations(props: Props) {
|
|||
|
||||
const onClick = (integration: any, width: number) => {
|
||||
if (
|
||||
integration.slug &&
|
||||
integration.slug !== 'slack' &&
|
||||
integration.slug !== 'msteams'
|
||||
integration.slug
|
||||
&& integration.slug !== 'slack'
|
||||
&& integration.slug !== 'msteams'
|
||||
) {
|
||||
const intName = integration.slug as
|
||||
| 'sentry'
|
||||
|
|
@ -86,7 +86,7 @@ function Integrations(props: Props) {
|
|||
siteId,
|
||||
onClose: hideModal,
|
||||
}),
|
||||
{ right: true, width }
|
||||
{ right: true, width },
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -110,7 +110,7 @@ function Integrations(props: Props) {
|
|||
}));
|
||||
|
||||
const allIntegrations = filteredIntegrations.flatMap(
|
||||
(cat) => cat.integrations
|
||||
(cat) => cat.integrations,
|
||||
);
|
||||
|
||||
const onChangeSelect = ({ value }: any) => {
|
||||
|
|
@ -120,7 +120,7 @@ function Integrations(props: Props) {
|
|||
return (
|
||||
<>
|
||||
<div className="bg-white rounded-lg border shadow-sm p-5 mb-4">
|
||||
<div className={'flex items-center gap-4 mb-2'}>
|
||||
<div className="flex items-center gap-4 mb-2">
|
||||
{!hideHeader && <PageTitle title={<div>Integrations</div>} />}
|
||||
<SiteDropdown value={siteId} onChange={onChangeSelect} />
|
||||
</div>
|
||||
|
|
@ -134,7 +134,7 @@ function Integrations(props: Props) {
|
|||
<div className="mb-4" />
|
||||
|
||||
<div
|
||||
className={'mt-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3'}
|
||||
className="mt-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"
|
||||
>
|
||||
{allIntegrations.map((integration, i) => (
|
||||
<React.Fragment key={`${integration.slug}+${i}`}>
|
||||
|
|
@ -142,21 +142,17 @@ function Integrations(props: Props) {
|
|||
integrated={integratedList.includes(integration.slug)}
|
||||
integration={integration}
|
||||
useIcon={integration.useIcon}
|
||||
onClick={() =>
|
||||
onClick(
|
||||
integration,
|
||||
filteredIntegrations.find((cat) =>
|
||||
cat.integrations.includes(integration)
|
||||
)?.title === 'Plugins'
|
||||
? 500
|
||||
: 350
|
||||
)
|
||||
}
|
||||
onClick={() => 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 === 'github'
|
||||
&& integratedList.includes('jira'))
|
||||
|| (integration.slug === 'jira'
|
||||
&& integratedList.includes('github'))
|
||||
}
|
||||
/>
|
||||
</React.Fragment>
|
||||
|
|
@ -167,7 +163,7 @@ function Integrations(props: Props) {
|
|||
}
|
||||
|
||||
export default withPageTitle('Integrations - OpenReplay Preferences')(
|
||||
observer(Integrations)
|
||||
observer(Integrations),
|
||||
);
|
||||
|
||||
const integrations = [
|
||||
|
|
|
|||
|
|
@ -1,56 +1,60 @@
|
|||
import React from 'react';
|
||||
import IntegrationForm from '../IntegrationForm';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModalCard';
|
||||
import IntegrationForm from '../IntegrationForm';
|
||||
|
||||
const JiraForm = (props) => {
|
||||
function JiraForm(props) {
|
||||
const { hideModal } = useModal();
|
||||
return (
|
||||
<div className='bg-white h-screen overflow-y-auto' style={{ width: '350px' }}>
|
||||
<IntegrationModalCard title='Jira' icon='integrations/jira'
|
||||
description='Integrate Jira with OpenReplay to enable the creation of a new ticket directly from a session.' />
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<IntegrationModalCard
|
||||
title="Jira"
|
||||
icon="integrations/jira"
|
||||
description="Integrate Jira with OpenReplay to enable the creation of a new ticket directly from a session."
|
||||
/>
|
||||
|
||||
|
||||
<div className='border-b my-4 p-5'>
|
||||
<div className='font-medium mb-1'>How it works?</div>
|
||||
<ol className='list-decimal list-inside'>
|
||||
<div className="border-b my-4 p-5">
|
||||
<div className="font-medium mb-1">How it works?</div>
|
||||
<ol className="list-decimal list-inside">
|
||||
<li>Create a new API token</li>
|
||||
<li>Enter the token below</li>
|
||||
</ol>
|
||||
<div className='mt-8'>
|
||||
<DocLink className='mt-4' label='Integrate Jira Cloud'
|
||||
url='https://docs.openreplay.com/integrations/jira' />
|
||||
<div className="mt-8">
|
||||
<DocLink
|
||||
className="mt-4"
|
||||
label="Integrate Jira Cloud"
|
||||
url="https://docs.openreplay.com/integrations/jira"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<IntegrationForm
|
||||
{...props}
|
||||
ignoreProject={true}
|
||||
name='jira'
|
||||
customPath='jira'
|
||||
ignoreProject
|
||||
name="jira"
|
||||
customPath="jira"
|
||||
onClose={hideModal}
|
||||
formFields={[
|
||||
{
|
||||
key: 'username',
|
||||
label: 'Username',
|
||||
autoFocus: true
|
||||
autoFocus: true,
|
||||
},
|
||||
{
|
||||
key: 'token',
|
||||
label: 'API Token'
|
||||
label: 'API Token',
|
||||
},
|
||||
{
|
||||
key: 'url',
|
||||
label: 'JIRA URL',
|
||||
placeholder: 'E.x. https://myjira.atlassian.net'
|
||||
}
|
||||
placeholder: 'E.x. https://myjira.atlassian.net',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
JiraForm.displayName = 'JiraForm';
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './JiraForm';
|
||||
export { default } from './JiraForm';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useStore } from "App/mstore";
|
||||
import { useStore } from 'App/mstore';
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { CodeBlock } from 'UI';
|
||||
|
|
@ -6,11 +6,11 @@ import { CodeBlock } from 'UI';
|
|||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
import ToggleContent from 'Shared/ToggleContent';
|
||||
|
||||
const ProfilerDoc = () => {
|
||||
function 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 { siteId } = integrationsStore.integrations;
|
||||
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';
|
||||
|
|
@ -58,8 +58,8 @@ const fn = profiler('call_name')(() => {
|
|||
|
||||
<div className="font-bold my-2">Installation</div>
|
||||
<CodeBlock
|
||||
code={`npm i @openreplay/tracker-profiler --save`}
|
||||
language={'bash'}
|
||||
code="npm i @openreplay/tracker-profiler --save"
|
||||
language="bash"
|
||||
/>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
|
|
@ -72,8 +72,8 @@ const fn = profiler('call_name')(() => {
|
|||
<div className="font-bold my-2">Usage</div>
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={<CodeBlock language={'js'} code={usage} />}
|
||||
second={<CodeBlock language={'jsx'} code={usageCjs} />}
|
||||
first={<CodeBlock language="js" code={usage} />}
|
||||
second={<CodeBlock language="jsx" code={usageCjs} />}
|
||||
/>
|
||||
|
||||
<DocLink
|
||||
|
|
@ -84,7 +84,7 @@ const fn = profiler('call_name')(() => {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
ProfilerDoc.displayName = 'ProfilerDoc';
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './ProfilerDoc'
|
||||
export { default } from './ProfilerDoc';
|
||||
|
|
|
|||
|
|
@ -1,25 +1,24 @@
|
|||
import React from 'react';
|
||||
import { Form, Input, Message, confirm } from 'UI';
|
||||
import { Button } from 'antd'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useStore } from 'App/mstore'
|
||||
import {
|
||||
Form, Input, Message, confirm,
|
||||
} from 'UI';
|
||||
import { Button } from 'antd';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
|
||||
function SlackAddForm(props) {
|
||||
const { onClose } = props;
|
||||
const { integrationsStore } = useStore();
|
||||
const instance = integrationsStore.slack.instance;
|
||||
const { instance } = integrationsStore.slack;
|
||||
const saving = integrationsStore.slack.loading;
|
||||
const errors = integrationsStore.slack.errors;
|
||||
const edit = integrationsStore.slack.edit;
|
||||
const { errors } = integrationsStore.slack;
|
||||
const { edit } = integrationsStore.slack;
|
||||
const onSave = integrationsStore.slack.saveIntegration;
|
||||
const update = integrationsStore.slack.update;
|
||||
const init = integrationsStore.slack.init;
|
||||
const { update } = integrationsStore.slack;
|
||||
const { init } = integrationsStore.slack;
|
||||
const onRemove = integrationsStore.slack.removeInt;
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => init({})
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => () => init({}), []);
|
||||
|
||||
const save = () => {
|
||||
if (instance.exists()) {
|
||||
|
|
@ -34,7 +33,7 @@ function SlackAddForm(props) {
|
|||
await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this channel?`,
|
||||
confirmation: 'Are you sure you want to permanently delete this channel?',
|
||||
})
|
||||
) {
|
||||
await onRemove(id);
|
||||
|
|
@ -43,7 +42,7 @@ function SlackAddForm(props) {
|
|||
};
|
||||
|
||||
const write = ({ target: { name, value } }) => edit({ [name]: value });
|
||||
|
||||
|
||||
return (
|
||||
<div className="p-5" style={{ minWidth: '300px' }}>
|
||||
<Form>
|
||||
|
|
@ -79,11 +78,11 @@ function SlackAddForm(props) {
|
|||
{instance.exists() ? 'Update' : 'Add'}
|
||||
</Button>
|
||||
|
||||
<Button onClick={onClose}>{'Cancel'}</Button>
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
</div>
|
||||
|
||||
<Button onClick={() => remove(instance.webhookId)} disabled={!instance.exists()}>
|
||||
{'Delete'}
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './SlackAddForm'
|
||||
export { default } from './SlackAddForm';
|
||||
|
|
|
|||
|
|
@ -1,48 +1,48 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useStore } from 'App/mstore'
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
|
||||
function SlackChannelList(props) {
|
||||
const { integrationsStore } = useStore();
|
||||
const list = integrationsStore.slack.list;
|
||||
const edit = integrationsStore.slack.edit;
|
||||
const { integrationsStore } = useStore();
|
||||
const { list } = integrationsStore.slack;
|
||||
const { edit } = integrationsStore.slack;
|
||||
|
||||
const onEdit = (instance) => {
|
||||
edit(instance.toData());
|
||||
props.onEdit();
|
||||
};
|
||||
const onEdit = (instance) => {
|
||||
edit(instance.toData());
|
||||
props.onEdit();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<NoContent
|
||||
title={
|
||||
<div className="p-5 mb-4">
|
||||
<div className="text-base text-left">
|
||||
Integrate Slack 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 Slack" url="https://docs.openreplay.com/integrations/slack" />
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={list.length === 0}
|
||||
>
|
||||
{list.map((c) => (
|
||||
<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 Slack 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 Slack" url="https://docs.openreplay.com/integrations/slack" />
|
||||
</div>
|
||||
)}
|
||||
size="small"
|
||||
show={list.length === 0}
|
||||
>
|
||||
{list.map((c) => (
|
||||
<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 observer(SlackChannelList);
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './SlackChannelList'
|
||||
export { default } from './SlackChannelList';
|
||||
|
|
|
|||
|
|
@ -1,48 +1,48 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import SlackChannelList from './SlackChannelList/SlackChannelList';
|
||||
import SlackAddForm from './SlackAddForm';
|
||||
import { Icon } from 'UI';
|
||||
import { Button } from 'antd';
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useStore } from 'App/mstore'
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
import SlackAddForm from './SlackAddForm';
|
||||
import SlackChannelList from './SlackChannelList/SlackChannelList';
|
||||
|
||||
const SlackForm = () => {
|
||||
const { integrationsStore } = useStore();
|
||||
const init = integrationsStore.slack.init;
|
||||
const fetchList = integrationsStore.slack.fetchIntegrations;
|
||||
const [active, setActive] = React.useState(false);
|
||||
function SlackForm() {
|
||||
const { integrationsStore } = useStore();
|
||||
const { init } = integrationsStore.slack;
|
||||
const fetchList = integrationsStore.slack.fetchIntegrations;
|
||||
const [active, setActive] = React.useState(false);
|
||||
|
||||
const onEdit = () => {
|
||||
setActive(true);
|
||||
};
|
||||
const onEdit = () => {
|
||||
setActive(true);
|
||||
};
|
||||
|
||||
const onNew = () => {
|
||||
setActive(true);
|
||||
init({});
|
||||
}
|
||||
const onNew = () => {
|
||||
setActive(true);
|
||||
init({});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void fetchList();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
void fetchList();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto flex items-start" style={{ width: active ? '700px' : '350px' }}>
|
||||
{active && (
|
||||
<div className="border-r h-full" style={{ width: '350px' }}>
|
||||
<SlackAddForm onClose={() => setActive(false)} />
|
||||
</div>
|
||||
)}
|
||||
<div className="shrink-0" style={{ width: '350px' }}>
|
||||
<div className="flex items-center p-5">
|
||||
<h3 className="text-2xl mr-3">Slack</h3>
|
||||
<Button shape={'circle'} type={'text'} icon={<Icon name={"plus"} size={24} />} onClick={onNew}/>
|
||||
</div>
|
||||
<SlackChannelList onEdit={onEdit} />
|
||||
</div>
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto flex items-start" style={{ width: active ? '700px' : '350px' }}>
|
||||
{active && (
|
||||
<div className="border-r h-full" style={{ width: '350px' }}>
|
||||
<SlackAddForm onClose={() => setActive(false)} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)}
|
||||
<div className="shrink-0" style={{ width: '350px' }}>
|
||||
<div className="flex items-center p-5">
|
||||
<h3 className="text-2xl mr-3">Slack</h3>
|
||||
<Button shape="circle" type="text" icon={<Icon name="plus" size={24} />} onClick={onNew} />
|
||||
</div>
|
||||
<SlackChannelList onEdit={onEdit} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SlackForm.displayName = 'SlackForm';
|
||||
|
||||
export default observer(SlackForm);
|
||||
export default observer(SlackForm);
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ import { observer } from 'mobx-react-lite';
|
|||
import React from 'react';
|
||||
|
||||
import { useStore } from 'App/mstore';
|
||||
import { confirm, Form, Input, Message } from 'UI';
|
||||
import { Button } from 'antd'
|
||||
import {
|
||||
confirm, Form, Input, Message,
|
||||
} from 'UI';
|
||||
import { Button } from 'antd';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
|
|
@ -11,18 +13,16 @@ interface Props {
|
|||
|
||||
function TeamsAddForm({ onClose }: Props) {
|
||||
const { integrationsStore } = useStore();
|
||||
const instance = integrationsStore.msteams.instance;
|
||||
const { instance } = integrationsStore.msteams;
|
||||
const saving = integrationsStore.msteams.loading;
|
||||
const errors = integrationsStore.msteams.errors;
|
||||
const edit = integrationsStore.msteams.edit;
|
||||
const { errors } = integrationsStore.msteams;
|
||||
const { edit } = integrationsStore.msteams;
|
||||
const onSave = integrationsStore.msteams.saveIntegration;
|
||||
const init = integrationsStore.msteams.init;
|
||||
const { init } = integrationsStore.msteams;
|
||||
const onRemove = integrationsStore.msteams.removeInt;
|
||||
const update = integrationsStore.msteams.update;
|
||||
const { update } = integrationsStore.msteams;
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => init({});
|
||||
}, []);
|
||||
React.useEffect(() => () => init({}), []);
|
||||
|
||||
const save = () => {
|
||||
if (instance?.exists()) {
|
||||
|
|
@ -41,7 +41,7 @@ function TeamsAddForm({ onClose }: Props) {
|
|||
await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this channel?`
|
||||
confirmation: 'Are you sure you want to permanently delete this channel?',
|
||||
})
|
||||
) {
|
||||
void onRemove(id).then(onClose);
|
||||
|
|
@ -49,8 +49,8 @@ function TeamsAddForm({ onClose }: Props) {
|
|||
};
|
||||
|
||||
const write = ({
|
||||
target: { name, value }
|
||||
}: {
|
||||
target: { name, value },
|
||||
}: {
|
||||
target: { name: string; value: string };
|
||||
}) => edit({ [name]: value });
|
||||
|
||||
|
|
@ -89,14 +89,14 @@ function TeamsAddForm({ onClose }: Props) {
|
|||
{instance?.exists() ? 'Update' : 'Add'}
|
||||
</Button>
|
||||
|
||||
<Button onClick={onClose}>{'Cancel'}</Button>
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => remove(instance?.webhookId)}
|
||||
disabled={!instance.exists()}
|
||||
>
|
||||
{'Delete'}
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import DocLink from 'Shared/DocLink/DocLink';
|
|||
|
||||
function TeamsChannelList(props: { onEdit: () => void }) {
|
||||
const { integrationsStore } = useStore();
|
||||
const list = integrationsStore.msteams.list;
|
||||
const edit = integrationsStore.msteams.edit;
|
||||
const { list } = integrationsStore.msteams;
|
||||
const { edit } = integrationsStore.msteams;
|
||||
|
||||
const onEdit = (instance: Record<string, any>) => {
|
||||
edit(instance);
|
||||
|
|
@ -19,7 +19,7 @@ function TeamsChannelList(props: { onEdit: () => void }) {
|
|||
return (
|
||||
<div className="mt-6">
|
||||
<NoContent
|
||||
title={
|
||||
title={(
|
||||
<div className="p-5 mb-4">
|
||||
<div className="text-base text-left">
|
||||
Integrate MS Teams with OpenReplay and share insights with the
|
||||
|
|
@ -31,7 +31,7 @@ function TeamsChannelList(props: { onEdit: () => void }) {
|
|||
url="https://docs.openreplay.com/integrations/msteams"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
size="small"
|
||||
show={list.length === 0}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -2,47 +2,47 @@ import React, { useEffect } from 'react';
|
|||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Icon } from 'UI';
|
||||
import { Button } from 'antd'
|
||||
import { Button } from 'antd';
|
||||
|
||||
import TeamsChannelList from './TeamsChannelList';
|
||||
import TeamsAddForm from './TeamsAddForm';
|
||||
|
||||
const MSTeams = () => {
|
||||
const { integrationsStore } = useStore();
|
||||
const fetchList = integrationsStore.msteams.fetchIntegrations;
|
||||
const init = integrationsStore.msteams.init;
|
||||
const [active, setActive] = React.useState(false);
|
||||
function MSTeams() {
|
||||
const { integrationsStore } = useStore();
|
||||
const fetchList = integrationsStore.msteams.fetchIntegrations;
|
||||
const { init } = integrationsStore.msteams;
|
||||
const [active, setActive] = React.useState(false);
|
||||
|
||||
const onEdit = () => {
|
||||
setActive(true);
|
||||
};
|
||||
const onEdit = () => {
|
||||
setActive(true);
|
||||
};
|
||||
|
||||
const onNew = () => {
|
||||
setActive(true);
|
||||
init({});
|
||||
}
|
||||
const onNew = () => {
|
||||
setActive(true);
|
||||
init({});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void fetchList();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
void fetchList();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto flex items-start" style={{ width: active ? '700px' : '350px' }}>
|
||||
{active && (
|
||||
<div className="border-r h-full" style={{ width: '350px' }}>
|
||||
<TeamsAddForm onClose={() => setActive(false)} />
|
||||
</div>
|
||||
)}
|
||||
<div className="shrink-0" style={{ width: '350px' }}>
|
||||
<div className="flex items-center p-5">
|
||||
<h3 className="text-2xl mr-3">Microsoft Teams</h3>
|
||||
<Button shape={'circle'} icon={<Icon name={'plus'} size={24} />} type="text" onClick={onNew}/>
|
||||
</div>
|
||||
<TeamsChannelList onEdit={onEdit} />
|
||||
</div>
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto flex items-start" style={{ width: active ? '700px' : '350px' }}>
|
||||
{active && (
|
||||
<div className="border-r h-full" style={{ width: '350px' }}>
|
||||
<TeamsAddForm onClose={() => setActive(false)} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)}
|
||||
<div className="shrink-0" style={{ width: '350px' }}>
|
||||
<div className="flex items-center p-5">
|
||||
<h3 className="text-2xl mr-3">Microsoft Teams</h3>
|
||||
<Button shape="circle" icon={<Icon name="plus" size={24} />} type="text" onClick={onNew} />
|
||||
</div>
|
||||
<TeamsChannelList onEdit={onEdit} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
MSTeams.displayName = 'MSTeams';
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue