diff --git a/frontend/.prettierrc b/frontend/.prettierrc index 7c2b61247..1fe40a59f 100644 --- a/frontend/.prettierrc +++ b/frontend/.prettierrc @@ -5,5 +5,9 @@ "singleQuote": true, "importOrderSeparation": true, "importOrderSortSpecifiers": true, - "importOrder": ["^Components|^App|^UI|^Duck", "^Shared", "^[./]"] + "importOrder": ["^Components|^App|^UI|^Duck", "^Shared", "^[./]"], + "bracketSpacing": true, + "arrowParens": "always", + "semi": true, + "trailingComma": "all" } diff --git a/frontend/app/AdditionalRoutes.tsx b/frontend/app/AdditionalRoutes.tsx index db93ff9c5..a380834c6 100644 --- a/frontend/app/AdditionalRoutes.tsx +++ b/frontend/app/AdditionalRoutes.tsx @@ -5,13 +5,9 @@ interface Props { redirect: string; } -const AdditionalRoutes = (props: Props) => { +function AdditionalRoutes(props: Props) { const { redirect } = props; - return ( - <> - - - ); -}; + return ; +} export default AdditionalRoutes; diff --git a/frontend/app/IFrameRoutes.tsx b/frontend/app/IFrameRoutes.tsx index 8bfe3522b..434a6af56 100644 --- a/frontend/app/IFrameRoutes.tsx +++ b/frontend/app/IFrameRoutes.tsx @@ -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 ( - - - }> - - - - + + + }> + + + + @@ -67,5 +73,4 @@ function IFrameRoutes(props: Props) { return ; } - export default observer(IFrameRoutes); diff --git a/frontend/app/PrivateRoutes.tsx b/frontend/app/PrivateRoutes.tsx index d256df7f8..af356772d 100644 --- a/frontend/app/PrivateRoutes.tsx +++ b/frontend/app/PrivateRoutes.tsx @@ -21,13 +21,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')), @@ -47,7 +47,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, @@ -55,7 +55,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(); @@ -104,13 +104,16 @@ function PrivateRoutes() { const { projectsStore, userStore, integrationsStore, searchStore } = useStore(); const onboarding = userStore.onboarding; 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; + !onboarding && + (localStorage.getItem(GLOBAL_HAS_NO_RECORDINGS) === 'true' || + (sites.length > 0 && !hasRecordings)) && + scope > 0; const siteIdList: any = sites.map(({ id }) => id); React.useEffect(() => { @@ -130,7 +133,7 @@ function PrivateRoutes() { }, [searchStore.instance.filters, searchStore.instance.eventsOrder]); return ( - }> + }> import('Components/Login/Login')); -const ForgotPassword = lazy(() => import('Components/ForgotPassword/ForgotPassword')); +const ForgotPassword = lazy( + () => import('Components/ForgotPassword/ForgotPassword'), +); const Spot = lazy(() => import('Components/Spots/SpotPlayer/SpotPlayer')); function PublicRoutes() { const { userStore } = useStore(); - const authDetails = userStore.authStore.authDetails; - const isEnterprise = userStore.isEnterprise; - const hideSupport = isEnterprise || location.pathname.includes('spots') || location.pathname.includes('view-spot'); + 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); useEffect(() => { @@ -34,10 +38,15 @@ function PublicRoutes() { return ( - }> + }> - + @@ -48,5 +57,4 @@ function PublicRoutes() { ); } - export default observer(PublicRoutes); diff --git a/frontend/app/Router.tsx b/frontend/app/Router.tsx index 000a6d6d5..f89d409d1 100644 --- a/frontend/app/Router.tsx +++ b/frontend/app/Router.tsx @@ -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: { @@ -28,27 +28,32 @@ interface RouterProps extends RouteComponentProps { } const Router: React.FC = (props) => { - const { - location, - history, - } = props; + const { location, 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 loading = Boolean( + userInfoLoading || (!scopeSetup && !siteId) || sitesLoading, + ); const initSite = projectsStore.initProject; const fetchSiteList = projectsStore.fetchList; @@ -75,10 +80,10 @@ const Router: React.FC = (props) => { const handleSpotLogin = (jwt: string) => { if (spotReqSent.current) { return; - } else { - spotReqSent.current = true; - setIsSpotCb(false); } + spotReqSent.current = true; + setIsSpotCb(false); + handleSpotJWT(jwt); }; @@ -86,7 +91,7 @@ const Router: React.FC = (props) => { if (!isLoggedIn && location.pathname !== routes.login()) { localStorage.setItem( GLOBAL_DESTINATION_PATH, - location.pathname + location.search + location.pathname + location.search, ); } }; @@ -143,7 +148,7 @@ const Router: React.FC = (props) => { useEffect(() => { handleDestinationPath(); - setSessionPath(previousLocation ? previousLocation : location); + setSessionPath(previousLocation || location); }, [location]); useEffect(() => { @@ -163,14 +168,14 @@ const Router: React.FC = (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(); } }; diff --git a/frontend/app/api_client.ts b/frontend/app/api_client.ts index 4d4309a22..a2dc3cb4c 100644 --- a/frontend/app/api_client.ts +++ b/frontend/app/api_client.ts @@ -30,15 +30,18 @@ const siteIdRequiredPaths: string[] = [ '/check-recording-status', '/usability-tests', '/tags', - '/intelligent' + '/intelligent', ]; -export const clean = (obj: any, forbiddenValues: any[] = [undefined, '']): any => { +export const clean = ( + obj: any, + forbiddenValues: any[] = [undefined, ''], +): any => { const keys = Array.isArray(obj) ? 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 +55,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 onUpdateJwt: (data: { jwt?: string; spotJwt?: string }) => void; + private refreshingTokenPromise: Promise | null = null; constructor() { this.init = { headers: new Headers({ Accept: 'application/json', - 'Content-Type': 'application/json' - }) + 'Content-Type': 'application/json', + }), }; } @@ -73,7 +81,9 @@ export default class APIClient { } } - setOnUpdateJwt(onUpdateJwt: (data: { jwt?: string, spotJwt?: string }) => void): void { + setOnUpdateJwt( + onUpdateJwt: (data: { jwt?: string; spotJwt?: string }) => void, + ): void { this.onUpdateJwt = onUpdateJwt; } @@ -85,7 +95,11 @@ export default class APIClient { this.siteIdCheck = checker; } - private getInit(method: string = 'GET', params?: any, reqHeaders?: Record): RequestInit { + private getInit( + method: string = 'GET', + params?: any, + reqHeaders?: Record, + ): RequestInit { // Always fetch the latest JWT from the store const jwt = this.getJwt(); const headers = new Headers({ @@ -148,7 +162,7 @@ export default class APIClient { params?: any, method: string = 'GET', options: { clean?: boolean } = { clean: true }, - headers?: Record + headers?: Record, ): Promise { let _path = path; let jwt = this.getJwt(); @@ -157,7 +171,11 @@ export default class APIClient { (this.init.headers as Headers).set('Authorization', `Bearer ${jwt}`); } - const init = this.getInit(method, options.clean && params ? clean(params) : params, headers); + const init = this.getInit( + method, + options.clean && params ? clean(params) : params, + headers, + ); if (params !== undefined) { const cleanedParams = options.clean ? clean(params) : params; @@ -193,7 +211,7 @@ export default class APIClient { 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; @@ -205,7 +223,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; @@ -216,9 +234,14 @@ export default class APIClient { async refreshToken(): Promise { try { - const response = await this.fetch('/refresh', { - headers: this.init.headers - }, 'GET', { clean: false }); + const response = await this.fetch( + '/refresh', + { + headers: this.init.headers, + }, + 'GET', + { clean: false }, + ); if (!response.ok) { throw new Error('Failed to refresh token'); @@ -235,12 +258,28 @@ export default class APIClient { } } - get(path: string, params?: any, options?: any, headers?: Record): Promise { + get( + path: string, + params?: any, + options?: any, + headers?: Record, + ): Promise { this.init.method = 'GET'; - return this.fetch(queried(path, params), options, 'GET', undefined, headers); + return this.fetch( + queried(path, params), + options, + 'GET', + undefined, + headers, + ); } - post(path: string, params?: any, options?: any, headers?: Record): Promise { + post( + path: string, + params?: any, + options?: any, + headers?: Record, + ): Promise { this.init.method = 'POST'; return this.fetch(path, params, 'POST', options, headers); } diff --git a/frontend/app/components/Alerts/AlertForm.js b/frontend/app/components/Alerts/AlertForm.js deleted file mode 100644 index da3b74148..000000000 --- a/frontend/app/components/Alerts/AlertForm.js +++ /dev/null @@ -1,395 +0,0 @@ -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 cn from 'classnames'; -import {useStore} from 'App/mstore' -import {observer} from 'mobx-react-lite' -import Select from 'Shared/Select'; -import {Button} from "antd"; - -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}, -]; - -const changeOptions = [ - {label: 'change', value: 'change'}, - {label: '% change', value: 'percent'}, -]; - -const Circle = ({text}) => ( -
- {text} -
-); - -const Section = ({index, title, description, content}) => ( -
-
- -
- {title} - {description &&
{description}
} -
-
- -
{content}
-
-); - -function AlertForm(props) { - 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 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 ( -
props.onSubmit(instance)} - id="alert-form" - > -
- -
-
- alertsStore.edit({[name]: value})} - value={{value: instance.detectionMethod}} - list={[ - {name: 'Threshold', value: 'threshold'}, - {name: 'Change', value: 'change'}, - ]} - /> -
- {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.'} -
-
-
- } - /> - -
- -
- {!isThreshold && ( -
- - i.value === instance.query.left)} - // onChange={ writeQueryOption } - onChange={({value}) => - writeQueryOption(null, {name: 'left', value: value.value}) - } - /> -
- -
- -
- - {'test'} - - )} - {!unit && ( - - )} -
-
- -
- - writeOption(null, {name: 'previousPeriod', value})} - /> -
- )} -
- } - /> - -
- -
-
- - - - -
- - {instance.slack && ( -
- -
- alertsStore.edit({slackInput: selected})} - /> -
-
- )} - {instance.msteams && ( -
- -
- alertsStore.edit({msteamsInput: selected})} - /> -
-
- )} - - {instance.email && ( -
- -
- alertsStore.edit({emailInput: selected})} - /> -
-
- )} - - {instance.webhook && ( -
- - alertsStore.edit({webhookInput: selected})} - /> -
- )} -
- } - /> - - -
-
- -
- -
-
- {instance.exists() && ( - - )} -
-
- - ); -}; - -export default observer(AlertForm); diff --git a/frontend/app/components/Alerts/AlertForm.tsx b/frontend/app/components/Alerts/AlertForm.tsx new file mode 100644 index 000000000..fefa89ff0 --- /dev/null +++ b/frontend/app/components/Alerts/AlertForm.tsx @@ -0,0 +1,462 @@ +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 Select from 'Shared/Select'; +import { Button } from 'antd'; +import DropdownChips from './DropdownChips'; +import stl from './alertForm.module.css'; +import { useTranslation } from 'react-i18next'; +import { TFunction } from 'i18next'; + +const thresholdOptions = (t: TFunction) => [ + { label: t('15 minutes'), value: 15 }, + { label: t('30 minutes'), value: 30 }, + { label: t('1 hour'), value: 60 }, + { label: t('2 hours'), value: 120 }, + { label: t('4 hours'), value: 240 }, + { label: t('1 day'), value: 1440 }, +]; + +const changeOptions = (t: TFunction) => [ + { label: t('change'), value: 'change' }, + { label: t('% change'), value: 'percent' }, +]; + +function Circle({ text }: { text: string }) { + return ( +
+ {text} +
+ ); +} + +function Section({ + index, + title, + description, + content, +}: { + index: string; + title: string; + description?: string; + content: any; +}) { + return ( +
+
+ +
+ {title} + {description && ( +
{description}
+ )} +
+
+ +
{content}
+
+ ); +} + +function AlertForm(props) { + const { t } = useTranslation(); + 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) => + 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 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 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 ( +
props.onSubmit(instance)} + id="alert-form" + > +
+ +
+
+ + alertsStore.edit({ [name]: value }) + } + value={{ value: instance.detectionMethod }} + list={[ + { name: t('Threshold'), value: 'threshold' }, + { name: t('Change'), value: 'change' }, + ]} + /> +
+ {isThreshold && + t( + 'Eg. Alert me if memory.avg is greater than 500mb over the past 4 hours.', + )} + {!isThreshold && + t( + 'Eg. Alert me if % change of memory.avg is greater than 10% over the past 4 hours compared to the previous 4 hours.', + )} +
+
+
+ } + /> + +
+ +
+ {!isThreshold && ( +
+ + i.value === instance.query.left, + )} + // onChange={ writeQueryOption } + onChange={({ value }) => + writeQueryOption(null, { name: 'left', value: value.value }) + } + /> +
+ +
+ +
+ + {t('test')} + + )} + {!unit && ( + + )} +
+
+ +
+ + + writeOption(null, { name: 'previousPeriod', value }) + } + /> +
+ )} +
+ } + /> + +
+ +
+
+ + + + +
+ + {instance.slack && ( +
+ +
+ + alertsStore.edit({ slackInput: selected }) + } + /> +
+
+ )} + {instance.msteams && ( +
+ +
+ + alertsStore.edit({ msteamsInput: selected }) + } + /> +
+
+ )} + + {instance.email && ( +
+ +
+ + alertsStore.edit({ emailInput: selected }) + } + /> +
+
+ )} + + {instance.webhook && ( +
+ + + alertsStore.edit({ webhookInput: selected }) + } + /> +
+ )} +
+ } + /> +
+ +
+
+ +
+ +
+
+ {instance.exists() && ( + + )} +
+
+ + ); +} + +export default observer(AlertForm); diff --git a/frontend/app/components/Alerts/AlertFormModal/AlertFormModal.tsx b/frontend/app/components/Alerts/AlertFormModal/AlertFormModal.tsx index 50e0c3cf6..096fa9e42 100644 --- a/frontend/app/components/Alerts/AlertFormModal/AlertFormModal.tsx +++ b/frontend/app/components/Alerts/AlertFormModal/AlertFormModal.tsx @@ -1,95 +1,92 @@ -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 + label: string; + value: string | number; } - interface Props { - showModal?: boolean; - metricId?: number; - onClose?: () => void; + showModal?: boolean; + metricId?: number; + onClose?: () => void; } 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 ( - - ); + return ( + + ); } export default observer(AlertFormModal); diff --git a/frontend/app/components/Alerts/AlertFormModal/index.ts b/frontend/app/components/Alerts/AlertFormModal/index.ts index 6eb4de1f2..1c1d42263 100644 --- a/frontend/app/components/Alerts/AlertFormModal/index.ts +++ b/frontend/app/components/Alerts/AlertFormModal/index.ts @@ -1 +1 @@ -export { default } from './AlertFormModal'; \ No newline at end of file +export { default } from './AlertFormModal'; diff --git a/frontend/app/components/Alerts/DropdownChips/DropdownChips.js b/frontend/app/components/Alerts/DropdownChips/DropdownChips.js index 1f805057d..de2fde6a8 100644 --- a/frontend/app/components/Alerts/DropdownChips/DropdownChips.js +++ b/frontend/app/components/Alerts/DropdownChips/DropdownChips.js @@ -2,65 +2,76 @@ 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 renderBadge = (item) => { - const val = typeof item === 'string' ? item : item.value; - const text = typeof item === 'string' ? item : item.label; - return onRemove(val)} outline={true} />; - }; + 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 ( -
- {textFiled ? ( - - ) : ( - + ) : ( + - - - - -
- - -
-
- - )); + +
+ + + + +
+
+ +
+ + +
+
+ + )); } export default EditRecordingModal; diff --git a/frontend/app/components/Assist/RecordingsList/Recordings.tsx b/frontend/app/components/Assist/RecordingsList/Recordings.tsx index 394db4aac..074386797 100644 --- a/frontend/app/components/Assist/RecordingsList/Recordings.tsx +++ b/frontend/app/components/Assist/RecordingsList/Recordings.tsx @@ -1,19 +1,21 @@ 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'; +import { useTranslation } from 'react-i18next'; function Recordings() { + const { t } = useTranslation(); const { recordingsStore, userStore } = useStore(); const userId = userStore.account.id; const recordingsOwner = [ - { value: '0', label: 'All Videos' }, - { value: userId, label: 'My Videos' } + { value: '0', label: t('All Videos') }, + { value: userId, label: t('My Videos') }, ]; const onDateChange = (e: any) => { @@ -21,22 +23,29 @@ function Recordings() { }; return ( -
-
-
- +
+
+
+
-
- +
+ -
+ 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); + }; + + return ( +
+ + +
+ ); } export default observer(RecordingsSearch); diff --git a/frontend/app/components/Assist/RecordingsList/RecordsListItem.tsx b/frontend/app/components/Assist/RecordingsList/RecordsListItem.tsx index 3d5f70cc6..5d0764e34 100644 --- a/frontend/app/components/Assist/RecordingsList/RecordsListItem.tsx +++ b/frontend/app/components/Assist/RecordingsList/RecordsListItem.tsx @@ -6,12 +6,14 @@ import { useStore } from 'App/mstore'; import { toast } from 'react-toastify'; import cn from 'classnames'; import EditRecordingModal from './EditRecordingModal'; +import { useTranslation } from 'react-i18next'; interface Props { record: IRecord; } function RecordsListItem(props: Props) { + const { t } = useTranslation(); const { record } = props; const { recordingsStore, settingsStore } = useStore(); const { timezone } = settingsStore.sessionSettings; @@ -34,17 +36,19 @@ 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'); + toast.success(t('Recording deleted')); }); }; const menuItems = [ - { icon: 'pencil', text: 'Rename', onClick: () => setEdit(true) }, + { icon: 'pencil', text: t('Rename'), onClick: () => setEdit(true) }, { icon: 'trash', - text: 'Delete', + text: t('Delete'), onClick: onDelete, }, ]; @@ -54,9 +58,9 @@ function RecordsListItem(props: Props) { .updateRecordingName(record.recordId, title) .then(() => { setRecordingTitle(title); - toast.success('Recording name updated'); + toast.success(t('Recording name updated')); }) - .catch(() => toast.error("Couldn't update recording name")); + .catch(() => toast.error(t("Couldn't update recording name"))); setEdit(false); }; @@ -78,7 +82,9 @@ function RecordsListItem(props: Props) {
{recordingTitle}
-
{durationFromMs(record.duration)}
+
+ {durationFromMs(record.duration)} +
@@ -95,14 +101,19 @@ function RecordsListItem(props: Props) { className="group flex items-center gap-1 cursor-pointer link" onClick={onRecordClick} > - + -
Play Video
+
{t('Play Video')}
diff --git a/frontend/app/components/Assist/RequestingWindow/RequestingWindow.tsx b/frontend/app/components/Assist/RequestingWindow/RequestingWindow.tsx index 2aff2db05..bae8359ec 100644 --- a/frontend/app/components/Assist/RequestingWindow/RequestingWindow.tsx +++ b/frontend/app/components/Assist/RequestingWindow/RequestingWindow.tsx @@ -1,10 +1,12 @@ 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'; +import { useTranslation } from 'react-i18next'; +import { TFunction } from 'i18next'; interface Props { userDisplayName: string; @@ -23,62 +25,70 @@ enum Actions { RecordingEnd, } -const WIN_VARIANTS = { +const WIN_VARIANTS = (t: TFunction) => ({ [WindowType.Call]: { - text: 'to accept the call', + text: t('to accept the call'), icon: 'call' as const, action: Actions.CallEnd, iconColor: 'teal', }, [WindowType.Control]: { - text: 'to accept remote control request', + text: t('to accept remote control request'), icon: 'remote-control' as const, action: Actions.ControlEnd, iconColor: 'teal', }, [WindowType.Record]: { - text: 'to accept recording request', + text: t('to accept recording request'), icon: 'record-circle' as const, iconColor: 'red', action: Actions.RecordingEnd, - } -}; + }, +}); function RequestingWindow({ getWindowType }: Props) { + const { t } = useTranslation(); 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 + assistManager: { initiateCallEnd, releaseRemoteControl, stopRecording }, + } = player; const actions = { [Actions.CallEnd]: initiateCallEnd, [Actions.ControlEnd]: releaseRemoteControl, [Actions.RecordingEnd]: stopRecording, - } + }; return (
- +
- Waiting for {userDisplayName} + {t('Waiting for')}{' '} + {userDisplayName}
- {WIN_VARIANTS[windowType].text} + {WIN_VARIANTS(t)[windowType].text} -
diff --git a/frontend/app/components/Assist/RequestingWindow/index.ts b/frontend/app/components/Assist/RequestingWindow/index.ts index 1a50403c4..5a610cd8e 100644 --- a/frontend/app/components/Assist/RequestingWindow/index.ts +++ b/frontend/app/components/Assist/RequestingWindow/index.ts @@ -1 +1 @@ -export { default, WindowType } from './RequestingWindow' +export { default, WindowType } from './RequestingWindow'; diff --git a/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx b/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx index 28043f5b8..105bb3f46 100644 --- a/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx +++ b/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx @@ -2,28 +2,26 @@ import React, { useState, useEffect } from 'react'; import { Button } from 'antd'; import {Headset} from 'lucide-react'; 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 { + 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"; - -function onReject() { - toast.info(`Call was rejected.`); -} - -function onControlReject() { - toast.info('Remote control request was rejected by user'); -} -function onControlBusy() { - toast.info('Remote control busy'); -} +import { useStore } from 'App/mstore'; +import stl from './AassistActions.module.css'; +import ChatWindow from '../../ChatWindow'; +import { useTranslation } from 'react-i18next'; function onError(e: any) { console.log(e); @@ -40,27 +38,26 @@ 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, - isCallActive, - agentIds, -}: Props) { +function AssistActions({ userId, isCallActive, agentIds }: Props) { // @ts-ignore ??? + const { t } = useTranslation(); const { player, store } = React.useContext(PlayerContext); const { sessionStore, userStore } = useStore(); const permissions = userStore.account.permissions || []; - const hasPermission = permissions.includes('ASSIST_CALL') || permissions.includes('SERVICE_ASSIST_CALL'); - const isEnterprise = userStore.isEnterprise; + const hasPermission = + permissions.includes('ASSIST_CALL') || + permissions.includes('SERVICE_ASSIST_CALL'); + const { isEnterprise } = userStore; const agentId = userStore.account.id; - const userDisplayName = sessionStore.current.userDisplayName; + const { userDisplayName } = sessionStore.current; const { assistManager: { @@ -81,16 +78,23 @@ function AssistActions({ } = store.get(); const [isPrestart, setPrestart] = useState(false); - const [incomeStream, setIncomeStream] = useState<{ stream: MediaStream; isAgent: boolean }[] | null>([]); + const [incomeStream, setIncomeStream] = useState< + { stream: MediaStream; isAgent: boolean }[] | null + >([]); const [localStream, setLocalStream] = useState(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 onCall = + calling === CallingState.OnCall || calling === CallingState.Reconnecting; const callRequesting = calling === CallingState.Connecting; const cannotCall = - peerConnectionStatus !== ConnectionStatus.Connected || (isEnterprise && !hasPermission); + peerConnectionStatus !== ConnectionStatus.Connected || + (isEnterprise && !hasPermission); - const remoteRequesting = remoteControlStatus === RemoteControlStatus.Requesting; + const remoteRequesting = + remoteControlStatus === RemoteControlStatus.Requesting; const remoteActive = remoteControlStatus === RemoteControlStatus.Enabled; useEffect(() => { @@ -122,20 +126,22 @@ function AssistActions({ } }, [remoteActive]); - useEffect(() => { - return callObject?.end(); - }, []); + useEffect(() => callObject?.end(), []); useEffect(() => { if (peerConnectionStatus == ConnectionStatus.Disconnected) { - toast.info(`Live session was closed.`); + toast.info(t('Live session was closed.')); } }, [peerConnectionStatus]); const addIncomeStream = (stream: MediaStream, isAgent: boolean) => { setIncomeStream((oldState) => { if (oldState === null) return [{ stream, isAgent }]; - if (!oldState.find((existingStream) => existingStream.stream.id === stream.id)) { + if ( + !oldState.find( + (existingStream) => existingStream.stream.id === stream.id, + ) + ) { audioContextManager.mergeAudioStreams(stream); return [...oldState, { stream, isAgent }]; } @@ -146,10 +152,24 @@ function AssistActions({ const removeIncomeStream = (stream: MediaStream) => { setIncomeStream((prevState) => { if (!prevState) return []; - return prevState.filter((existingStream) => existingStream.stream.id !== stream.id); + return prevState.filter( + (existingStream) => existingStream.stream.id !== stream.id, + ); }); }; + function onReject() { + toast.info(t('Call was rejected.')); + } + + function onControlReject() { + toast.info(t('Remote control request was rejected by user')); + } + + function onControlBusy() { + toast.info(t('Remote control busy')); + } + function call() { RequestLocalStream() .then((lStream) => { @@ -159,12 +179,12 @@ function AssistActions({ lStream, addIncomeStream, () => { - player.assistManager.ping(AssistActionsPing.call.end, agentId) + player.assistManager.ping(AssistActionsPing.call.end, agentId); lStream.stop.apply(lStream); removeIncomeStream(lStream.stream); }, onReject, - onError + onError, ); setCallObject(callPeer()); // if (additionalAgentIds) { @@ -179,9 +199,9 @@ function AssistActions({ if ( await confirm({ - header: 'Start Call', - confirmButton: 'Call', - confirmation: `Are you sure you want to call ${userId ? userId : 'User'}?`, + header: t('Start Call'), + confirmButton: t('Call'), + confirmation: `${t('Are you sure you want to call')} ${userId || t('User')}?`, }) ) { call(agentIds); @@ -190,15 +210,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(); @@ -206,9 +226,9 @@ function AssistActions({ React.useEffect(() => { if (onCall) { - player.assistManager.ping(AssistActionsPing.call.start, agentId) + player.assistManager.ping(AssistActionsPing.call.start, agentId); } - }, [onCall]) + }, [onCall]); return (
@@ -227,7 +247,7 @@ function AssistActions({ size='small' className={annotating ? 'text-red' : 'text-main'} > - Annotate + {t('Annotate')}
@@ -241,7 +261,8 @@ function AssistActions({
} size='small' > - Remote Control + {t('Remote Control')}
@@ -260,8 +281,8 @@ function AssistActions({ @@ -278,7 +299,7 @@ function AssistActions({ className={onCall ? 'text-red' : isPrestart ? 'text-green' : 'text-main'} size='small' > - {onCall ? 'End' : isPrestart ? 'Join Call' : 'Call'} + {onCall ? t('End') : isPrestart ? t('Join Call') : t('Call')}
diff --git a/frontend/app/components/Assist/components/AssistActions/index.ts b/frontend/app/components/Assist/components/AssistActions/index.ts index 3e5108198..12caa65f5 100644 --- a/frontend/app/components/Assist/components/AssistActions/index.ts +++ b/frontend/app/components/Assist/components/AssistActions/index.ts @@ -1 +1 @@ -export { default } from './AssistActions' \ No newline at end of file +export { default } from './AssistActions'; diff --git a/frontend/app/components/Assist/components/SessionList/SessionList.tsx b/frontend/app/components/Assist/components/SessionList/SessionList.tsx index 2bf5d2ecc..3a95ee5d1 100644 --- a/frontend/app/components/Assist/components/SessionList/SessionList.tsx +++ b/frontend/app/components/Assist/components/SessionList/SessionList.tsx @@ -1,72 +1,90 @@ +/* eslint-disable i18next/no-literal-string */ 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'; import { useModal } from 'App/components/Modal'; import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; +import { useTranslation } from 'react-i18next'; interface Props { - loading: boolean; - list: any; - session: any; - userId: any; + loading: boolean; + list: any; + session: any; + 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); - }, []); - return ( -
-
-
- {props.userId}'s Live Sessions{' '} -
-
- - - -
-
No live sessions found.
-
- } - > -
- {list.map((session: any) => ( -
- {session.pageTitle && session.pageTitle !== '' && ( -
- - {session.pageTitle} -
- )} - -
- ))} -
-
-
+function SessionList(props: Props) { + const { t } = useTranslation(); + 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 ( +
+
+
+ {props.userId} + 's + {t('Live Sessions')}{' '}
- ); +
+ + + +
+
+ {t('No live sessions found.')} +
+
+ } + > +
+ {list.map((session: any) => ( +
+ {session.pageTitle && session.pageTitle !== '' && ( +
+ + + {session.pageTitle} + +
+ )} + +
+ ))} +
+
+
+
+ ); } export default observer(SessionList); diff --git a/frontend/app/components/Assist/components/SessionList/index.ts b/frontend/app/components/Assist/components/SessionList/index.ts index 779c9df2a..7ad26942d 100644 --- a/frontend/app/components/Assist/components/SessionList/index.ts +++ b/frontend/app/components/Assist/components/SessionList/index.ts @@ -1 +1 @@ -export { default } from './SessionList'; \ No newline at end of file +export { default } from './SessionList'; diff --git a/frontend/app/components/Assist/components/VideoContainer/VideoContainer.tsx b/frontend/app/components/Assist/components/VideoContainer/VideoContainer.tsx index 4d8b9ece4..3899d1ea0 100644 --- a/frontend/app/components/Assist/components/VideoContainer/VideoContainer.tsx +++ b/frontend/app/components/Assist/components/VideoContainer/VideoContainer.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; interface Props { stream: MediaStream | null; @@ -17,6 +18,7 @@ function VideoContainer({ local, isAgent, }: Props) { + const { t } = useTranslation(); const ref = useRef(null); const [isEnabled, setEnabled] = React.useState(false); @@ -50,7 +52,7 @@ function VideoContainer({ return (
-
diff --git a/frontend/app/components/Assist/components/VideoContainer/index.ts b/frontend/app/components/Assist/components/VideoContainer/index.ts index 546964d7a..00b9cfe75 100644 --- a/frontend/app/components/Assist/components/VideoContainer/index.ts +++ b/frontend/app/components/Assist/components/VideoContainer/index.ts @@ -1 +1 @@ -export { default } from './VideoContainer' \ No newline at end of file +export { default } from './VideoContainer'; diff --git a/frontend/app/components/Assist/index.ts b/frontend/app/components/Assist/index.ts index 1c61fa2d8..5e0445422 100644 --- a/frontend/app/components/Assist/index.ts +++ b/frontend/app/components/Assist/index.ts @@ -1 +1 @@ -export { default } from './Assist' \ No newline at end of file +export { default } from './Assist'; diff --git a/frontend/app/components/AssistStats/AssistStats.tsx b/frontend/app/components/AssistStats/AssistStats.tsx index b73f38034..9a4ac190a 100644 --- a/frontend/app/components/AssistStats/AssistStats.tsx +++ b/frontend/app/components/AssistStats/AssistStats.tsx @@ -13,34 +13,42 @@ 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"; +import { useTranslation } from 'react-i18next'; +import { TFunction } from 'i18next'; -const chartNames = { - assistTotal: 'Total Live Duration', - assistAvg: 'Avg Live Duration', - callTotal: 'Total Call Duration', - callAvg: 'Avg Call Duration', - controlTotal: 'Total Remote Duration', - controlAvg: 'Avg Remote Duration', -}; +const chartNames = (t: TFunction) => ({ + assistTotal: t('Total Live Duration'), + assistAvg: t('Avg Live Duration'), + callTotal: t('Total Call Duration'), + callAvg: t('Avg Call Duration'), + controlTotal: t('Total Remote Duration'), + controlAvg: t('Avg Remote Duration'), +}); function calculatePercentageDelta(currP: number, prevP: number) { return ((currP - prevP) / prevP) * 100; } function AssistStats() { + const { t } = useTranslation(); const [selectedUser, setSelectedUser] = React.useState(null); - const [period, setPeriod] = React.useState(Period({ rangeName: LAST_24_HOURS })); + const [period, setPeriod] = React.useState( + Period({ rangeName: LAST_24_HOURS }), + ); const [membersSort, setMembersSort] = React.useState('sessionsAssisted'); const [tableSort, setTableSort] = React.useState('timestamp'); - const [topMembers, setTopMembers] = React.useState<{ list: Member[]; total: number }>({ + const [topMembers, setTopMembers] = React.useState<{ + list: Member[]; + total: number; + }>({ list: [], total: 0, }); @@ -68,7 +76,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 +87,7 @@ function AssistStats() { endTimestamp: usedP.end, sort: tableSort, order: 'desc', - userId: selectedUser ? selectedUser : undefined, + userId: selectedUser || undefined, page: 1, limit: 10, }); @@ -88,7 +96,7 @@ function AssistStats() { topMembers.status === 'fulfilled' && setTopMembers(topMembers.value); graphs.status === 'fulfilled' && setGraphs(graphs.value); sessions.status === 'fulfilled' && setSessions(sessions.value); - } + }, ); setIsLoading(false); }; @@ -148,27 +156,31 @@ function AssistStats() { order: 'desc', 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' } - ]; + }) + .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: t('Date'), key: 'dateStr' }, + { label: t('Team Members'), key: 'members' }, + { label: t('Live Duration'), key: 'assistDuration' }, + { label: t('Call Duration'), key: 'callDuration' }, + { label: t('Remote Duration'), key: 'controlDuration' }, + { label: t('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 +203,109 @@ 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 ( -
-
-
+
+
+
- Co-browsing Reports + {t('Co-browsing Reports')} -
+
- - + +
-
- {Object.keys(graphs.currentPeriod).map((i: PeriodKeys) => ( -
-
- - {chartNames[i]} - -
- - {graphs.currentPeriod[i] - ? durationFromMsFormatted(graphs.currentPeriod[i]) - : null} - - {graphs.previousPeriod[i] ? ( -
graphs.previousPeriod[i] +
+ {Object.keys(graphs.currentPeriod).map((i: PeriodKeys) => ( +
+
+ + {chartNames(t)[i]} + +
+ + {graphs.currentPeriod[i] + ? durationFromMsFormatted(graphs.currentPeriod[i]) + : null} + + {graphs.previousPeriod[i] ? ( +
graphs.previousPeriod[i] ? 'flex items-center gap-1 text-green' : 'flex items-center gap-2 text-red' + } + > + graphs.previousPeriod[i] + ? 0 + : 180 } - > - graphs.previousPeriod[i] ? 0 : 180} - /> - {`${Math.round( - calculatePercentageDelta( - graphs.currentPeriod[i], - graphs.previousPeriod[i] - ) - )}%`} -
- ) : null} -
+ /> + {`${Math.round( + calculatePercentageDelta( + graphs.currentPeriod[i], + graphs.previousPeriod[i], + ), + )}%`} +
+ ) : null}
- - -
- ))} + + + +
+ ))}
-
+
-
+
-
+
); } diff --git a/frontend/app/components/AssistStats/components/Charts.tsx b/frontend/app/components/AssistStats/components/Charts.tsx index 665040277..676ea6d2b 100644 --- a/frontend/app/components/AssistStats/components/Charts.tsx +++ b/frontend/app/components/AssistStats/components/Charts.tsx @@ -1,13 +1,8 @@ import React from 'react'; import { NoContent } from 'UI'; import { Styles } from 'Components/Dashboard/Widgets/common'; -import { - AreaChart, - Area, - ResponsiveContainer, - XAxis, - YAxis, -} from 'recharts'; +import { AreaChart, Area, ResponsiveContainer, XAxis, YAxis } from 'recharts'; +import { useTranslation } from 'react-i18next'; interface Props { data: any; @@ -15,13 +10,16 @@ interface Props { } function Chart(props: Props) { + const { t } = useTranslation(); const { data, label } = props; const gradientDef = Styles.gradientDef(); return ( No data available
} + title={ +
{t('No data available')}
+ } show={data && data.length === 0} style={{ height: '100px' }} > @@ -51,7 +49,7 @@ function Chart(props: Props) { fillOpacity={1} strokeWidth={2} strokeOpacity={0.8} - fill={'url(#colorCount)'} + fill="url(#colorCount)" /> diff --git a/frontend/app/components/AssistStats/components/Table.tsx b/frontend/app/components/AssistStats/components/Table.tsx index 01abade37..8875425c9 100644 --- a/frontend/app/components/AssistStats/components/Table.tsx +++ b/frontend/app/components/AssistStats/components/Table.tsx @@ -1,14 +1,26 @@ -import { DownOutlined } from '@ant-design/icons'; -import { AssistStatsSession, SessionsResponse } from 'App/services/AssistStatsService'; +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 { Loader, Pagination, NoContent } from 'UI'; import PlayLink from 'Shared/SessionItem/PlayLink'; import { recordingsService } from 'App/services'; -import { checkForRecent, durationFromMsFormatted, getDateFromMill } from 'App/date'; +import { + checkForRecent, + durationFromMsFormatted, + getDateFromMill, +} from 'App/date'; import { useModal } from 'Components/Modal'; +import { useTranslation } from 'react-i18next'; +import { TFunction } from 'i18next'; interface Props { onSort: (v: string) => void; @@ -20,22 +32,22 @@ interface Props { } const PER_PAGE = 10; -const sortItems = [ +const sortItems = (t: TFunction) => [ { key: 'timestamp', - label: 'Newest First', + label: t('Newest First'), }, { key: 'assist_duration', - label: 'Live Duration', + label: t('Live Duration'), }, { key: 'call_duration', - label: 'Call Duration', + label: t('Call Duration'), }, { key: 'control_duration', - label: 'Remote Duration', + label: t('Remote Duration'), }, // { // key: '5', @@ -43,23 +55,31 @@ const sortItems = [ // }, ]; -function StatsTable({ onSort, isLoading, onPageChange, page, sessions, exportCSV }: Props) { - const [sortValue, setSort] = React.useState(sortItems[0].label); +function StatsTable({ + onSort, + isLoading, + onPageChange, + page, + sessions, + exportCSV, +}: Props) { + const { t } = useTranslation(); + const [sortValue, setSort] = React.useState(sortItems(t)[0].label); const updateRange = ({ key }: { key: string }) => { - const item = sortItems.find((item) => item.key === key); - setSort(item?.label || sortItems[0].label); + const item = sortItems(t).find((item) => item.key === key); + setSort(item?.label || sortItems(t)[0].label); item?.key && onSort(item.key); }; return ( -
-
+
+
- Assisted Sessions + {t('Assisted Sessions')} -
- -
-
- Date - Team Members - Live Duration - Call Duration - Remote Duration +
+ {t('Date')} + {t('Team Members')} + {t('Live Duration')} + {t('Call Duration')} + {t('Remote Duration')} {/* BUTTONS */}
-
- +
+ No data available
} + size="small" + title={ +
+ {t('No data available')} +
+ } show={sessions.list && sessions.list.length === 0} style={{ height: '100px' }} > - {sessions.list.map((session) => ( - - ))} + {sessions.list.map((session) => ( + + ))} -
+
-
+
{sessions.total > 0 ? (
- Showing {(page - 1) * PER_PAGE + 1} to{' '} - {(page - 1) * PER_PAGE + sessions.list.length} of{' '} - {numberWithCommas(sessions.total)} sessions. + {t('Showing')}{' '} + {(page - 1) * PER_PAGE + 1} +  {t('to')}  + + {(page - 1) * PER_PAGE + sessions.list.length} + {' '} + {t('of')}{' '} + + {numberWithCommas(sessions.total)} + {' '} + {t('sessions.')}
) : (
- Showing 0 to 0{' '} - of 0 sessions. + {t('Showing')} 0  + {t('to')}  + 0 {t('of')}  + 0 {t('sessions.')}
)} - {checkForRecent(getDateFromMill(session.timestamp)!, 'LLL dd, hh:mm a')} +
-
+ {checkForRecent(getDateFromMill(session.timestamp)!, 'LLL dd, hh:mm a')} + + +
{session.teamMembers.map((member) => ( -
{member.name}
+
+ {member.name} +
))}
@@ -139,7 +177,7 @@ function Row({ session }: { session: AssistStatsSession }) { {durationFromMsFormatted(session.callDuration)} {durationFromMsFormatted(session.controlDuration)} -
+
{session.recordings?.length > 0 ? ( session.recordings?.length > 1 ? ( - recordingsService.fetchRecording(item.key as unknown as number), + recordingsService.fetchRecording( + item.key as unknown as number, + ), }} > - + ) : (
recordingsService.fetchRecording(session.recordings[0].recordId)} + className="cursor-pointer" + onClick={() => + recordingsService.fetchRecording( + session.recordings[0].recordId, + ) + } > - +
) ) : null} - +
); } -function Cell({ size, children }: { size: number; children?: React.ReactNode }) { +function Cell({ + size, + children, +}: { + size: number; + children?: React.ReactNode; +}) { return
{children}
; } diff --git a/frontend/app/components/AssistStats/components/TeamMembers.tsx b/frontend/app/components/AssistStats/components/TeamMembers.tsx index d1440095d..6d61fb69b 100644 --- a/frontend/app/components/AssistStats/components/TeamMembers.tsx +++ b/frontend/app/components/AssistStats/components/TeamMembers.tsx @@ -2,26 +2,27 @@ import { DownOutlined, TableOutlined } from '@ant-design/icons'; 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 { TFunction } from 'i18next'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import { Loader, NoContent } from 'UI'; -import { exportCSVFile } from 'App/utils'; -const items = [ +const items = (t: TFunction) => [ { - label: 'Sessions Assisted', + label: t('Sessions Assisted'), key: 'sessionsAssisted', }, { - label: 'Live Duration', + label: t('Live Duration'), key: 'assistDuration', }, { - label: 'Call Duration', + label: t('Call Duration'), key: 'callDuration', }, { - label: 'Remote Duration', + label: t('Remote Duration'), key: 'controlDuration', }, ]; @@ -37,20 +38,21 @@ function TeamMembers({ onMembersSort: (v: string) => void; membersSort: string; }) { - const [dateRange, setDateRange] = React.useState(items[0].label); + const { t } = useTranslation(); + const [dateRange, setDateRange] = React.useState(items(t)[0].label); const updateRange = ({ key }: { key: string }) => { - const item = items.find((item) => item.key === key); - setDateRange(item?.label || items[0].label); - onMembersSort(item?.key || items[0].key); + const item = items(t).find((item) => item.key === key); + setDateRange(item?.label || items(t)[0].label); + onMembersSort(item?.key || items(t)[0].key); }; const onExport = () => { const headers = [ - { label: 'Team Member', key: 'name' }, - { label: 'Sessions Assisted', key: 'sessionsAssisted' }, - { label: 'Live Duration', key: 'assistDuration' }, - { label: 'Call Duration', key: 'callDuration' }, - { label: 'Remote Duration', key: 'controlDuration' }, + { label: t('Team Member'), key: 'name' }, + { label: t('Sessions Assisted'), key: 'sessionsAssisted' }, + { label: t('Live Duration'), key: 'assistDuration' }, + { label: t('Call Duration'), key: 'callDuration' }, + { label: t('Remote Duration'), key: 'controlDuration' }, ]; const data = topMembers.list.map((member) => ({ @@ -61,50 +63,73 @@ function TeamMembers({ controlDuration: `"${durationFromMsFormatted(member.controlDuration)}"`, })); - exportCSVFile(headers, data, `Team_Members_${new Date().toLocaleDateString()}`); + exportCSVFile( + headers, + data, + `Team_Members_${new Date().toLocaleDateString()}`, + ); }; return ( -
-
+
+
- Team Members + {t('Team Members')} -
+
- - +
- + No data available
} + size="small" + title={ +
+ {t('No data available')} +
+ } show={topMembers.list && topMembers.list.length === 0} style={{ height: '100px' }} > {topMembers.list.map((member) => ( -
+
-
{getInitials(member.name)}
+
+ {getInitials(member.name)} +
{member.name}
-
+
{membersSort === 'sessionsAssisted' ? member.count : durationFromMsFormatted(member.count)} @@ -113,10 +138,10 @@ function TeamMembers({ ))} -
+
{isLoading || topMembers.list.length === 0 ? '' - : `Showing 1 to ${topMembers.total} of the total`} + : `${t('Showing 1 to')} ${topMembers.total} ${t('of the total')}`}
); diff --git a/frontend/app/components/AssistStats/components/UserSearch.tsx b/frontend/app/components/AssistStats/components/UserSearch.tsx index 3e9baac1a..f2ad84a96 100644 --- a/frontend/app/components/AssistStats/components/UserSearch.tsx +++ b/frontend/app/components/AssistStats/components/UserSearch.tsx @@ -4,8 +4,10 @@ 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 }) => { - const [selectedValue, setSelectedValue] = useState(undefined); +function UserSearch({ onUserSelect }: { onUserSelect: (id: any) => void }) { + const [selectedValue, setSelectedValue] = useState( + undefined, + ); const { userStore } = useStore(); const allUsers = userStore.list.map((user) => ({ value: user.userId, @@ -20,7 +22,7 @@ const UserSearch = ({ onUserSelect }: { onUserSelect: (id: any) => void }) => { r.map((user: any) => ({ value: user.userId, label: user.name, - })) + })), ); }); } @@ -28,12 +30,16 @@ 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 +52,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 +62,12 @@ const UserSearch = ({ onUserSelect }: { onUserSelect: (id: any) => void }) => { ); -}; +} export default observer(UserSearch); diff --git a/frontend/app/components/AssistStats/index.ts b/frontend/app/components/AssistStats/index.ts index 96108d5f2..dbc6886e4 100644 --- a/frontend/app/components/AssistStats/index.ts +++ b/frontend/app/components/AssistStats/index.ts @@ -1 +1 @@ -export { default } from './AssistStats' +export { default } from './AssistStats'; diff --git a/frontend/app/components/AssistStats/pdfGenerator.ts b/frontend/app/components/AssistStats/pdfGenerator.ts index 2d1f0f3f1..628b5dd90 100644 --- a/frontend/app/components/AssistStats/pdfGenerator.ts +++ b/frontend/app/components/AssistStats/pdfGenerator.ts @@ -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; @@ -38,16 +38,24 @@ export const getPdf2 = async () => { const logoWidth = 55; 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) + doc.addImage( + '/assets/img/cobrowising-report-head.png', + 'png', + A4Height / 2 - headerW / 2, + 2, + 45, + 5, + ); + 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 +67,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')); }); } diff --git a/frontend/app/components/Charts/BarChart.tsx b/frontend/app/components/Charts/BarChart.tsx index 2d23dd834..9e81eb43d 100644 --- a/frontend/app/components/Charts/BarChart.tsx +++ b/frontend/app/components/Charts/BarChart.tsx @@ -1,12 +1,8 @@ import React from 'react'; -import { - DataProps, - buildCategories, - customTooltipFormatter -} from './utils'; -import { buildBarDatasetsAndSeries } from './barUtils'; -import { defaultOptions, echarts, initWindowStorages } from "./init"; import { BarChart } from 'echarts/charts'; +import { DataProps, buildCategories, customTooltipFormatter } from './utils'; +import { buildBarDatasetsAndSeries } from './barUtils'; +import { defaultOptions, echarts, initWindowStorages } from './init'; echarts.use([BarChart]); @@ -17,21 +13,29 @@ interface BarChartProps extends DataProps { } function ORBarChart(props: BarChartProps) { - const chartUuid = React.useRef(Math.random().toString(36).substring(7)); + const chartUuid = React.useRef( + Math.random().toString(36).substring(7), + ); const chartRef = React.useRef(null); 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); const { datasets, series } = buildBarDatasetsAndSeries(props); - initWindowStorages(chartUuid.current, categories, props.data.chart, props.compData?.chart ?? []); + initWindowStorages( + chartUuid.current, + categories, + props.data.chart, + props.compData?.chart ?? [], + ); series.forEach((s: any) => { - (window as any).__seriesColorMap[chartUuid.current][s.name] = s.itemStyle?.color ?? '#999'; + (window as any).__seriesColorMap[chartUuid.current][s.name] = + s.itemStyle?.color ?? '#999'; const ds = datasets.find((d) => d.id === s.datasetId); if (!ds) return; const yDim = s.encode.y; @@ -41,11 +45,11 @@ function ORBarChart(props: BarChartProps) { (window as any).__seriesValueMap[chartUuid.current][s.name] = {}; ds.source.forEach((row: any[]) => { const rowIdx = row[0]; // 'idx' - (window as any).__seriesValueMap[chartUuid.current][s.name][rowIdx] = row[yDimIndex]; + (window as any).__seriesValueMap[chartUuid.current][s.name][rowIdx] = + row[yDimIndex]; }); }); - const xAxis: any = { type: 'category', data: categories, @@ -62,7 +66,9 @@ function ORBarChart(props: BarChartProps) { ...defaultOptions, legend: { ...defaultOptions.legend, - data: series.filter((s: any) => !s._hideInLegend).map((s: any) => s.name), + data: series + .filter((s: any) => !s._hideInLegend) + .map((s: any) => s.name), }, tooltip: { ...defaultOptions.tooltip, @@ -80,12 +86,14 @@ 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 }}]}) + const timestamp = (window as any).__timestampMap?.[chartUuid.current]?.[ + index + ]; + props.onClick?.({ activePayload: [{ payload: { timestamp } }] }); setTimeout(() => { - props.onSeriesFocus?.(event.seriesName) - }, 0) - }) + props.onSeriesFocus?.(event.seriesName); + }, 0); + }); return () => { chart.dispose(); diff --git a/frontend/app/components/Charts/ColumnChart.tsx b/frontend/app/components/Charts/ColumnChart.tsx index f16992761..5a8f678c5 100644 --- a/frontend/app/components/Charts/ColumnChart.tsx +++ b/frontend/app/components/Charts/ColumnChart.tsx @@ -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(null); const chartUuid = React.useRef( - Math.random().toString(36).substring(7) + Math.random().toString(36).substring(7), ); React.useEffect(() => { @@ -42,10 +42,14 @@ 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 + const { yAxisData, series } = buildColumnChart( + chartUuid.current, + data, + compData, + ); + (window as any).__yAxisData[chartUuid.current] = yAxisData; chart.setOption({ ...defaultOptions, @@ -89,7 +93,7 @@ function ColumnChart(props: ColumnChartProps) { chart.on('click', (event) => { const focusedSeriesName = event.name; props.onSeriesFocus?.(focusedSeriesName); - }) + }); return () => { chart.dispose(); diff --git a/frontend/app/components/Charts/LineChart.tsx b/frontend/app/components/Charts/LineChart.tsx index 9155ed92c..93e505b23 100644 --- a/frontend/app/components/Charts/LineChart.tsx +++ b/frontend/app/components/Charts/LineChart.tsx @@ -1,8 +1,12 @@ 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]); @@ -16,19 +20,26 @@ interface Props extends DataProps { } function ORLineChart(props: Props) { - const chartUuid = React.useRef(Math.random().toString(36).substring(7)); + const chartUuid = React.useRef( + Math.random().toString(36).substring(7), + ); const chartRef = React.useRef(null); 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); const { datasets, series } = buildDatasetsAndSeries(props); - initWindowStorages(chartUuid.current, categories, props.data.chart, props.compData?.chart ?? []); + initWindowStorages( + chartUuid.current, + categories, + props.data.chart, + props.compData?.chart ?? [], + ); series.forEach((s: any) => { if (props.isArea) { @@ -37,7 +48,8 @@ function ORLineChart(props: Props) { } else { s.areaStyle = null; } - (window as any).__seriesColorMap[chartUuid.current][s.name] = s.itemStyle?.color ?? '#999'; + (window as any).__seriesColorMap[chartUuid.current][s.name] = + s.itemStyle?.color ?? '#999'; const datasetId = s.datasetId || 'current'; const ds = datasets.find((d) => d.id === datasetId); if (!ds) return; @@ -48,20 +60,23 @@ function ORLineChart(props: Props) { (window as any).__seriesValueMap[chartUuid.current][s.name] = {}; ds.source.forEach((row: any[]) => { const rowIdx = row[0]; - (window as any).__seriesValueMap[chartUuid.current][s.name][rowIdx] = row[yDimIndex]; + (window as any).__seriesValueMap[chartUuid.current][s.name][rowIdx] = + row[yDimIndex]; }); }); chart.setOption({ ...defaultOptions, title: { - text: props.chartName ?? "Line Chart", + text: props.chartName ?? 'Line Chart', show: false, }, legend: { ...defaultOptions.legend, // Only show legend for “current” series - data: series.filter((s: any) => !s._hideInLegend).map((s: any) => s.name), + data: series + .filter((s: any) => !s._hideInLegend) + .map((s: any) => s.name), }, xAxis: { type: 'category', @@ -75,7 +90,7 @@ function ORLineChart(props: Props) { nameTextStyle: { padding: [0, 0, 0, 15], }, - minInterval: 1 + minInterval: 1, }, tooltip: { ...defaultOptions.tooltip, @@ -91,12 +106,14 @@ 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 }}]}) + const timestamp = (window as any).__timestampMap?.[chartUuid.current]?.[ + index + ]; + props.onClick?.({ activePayload: [{ payload: { timestamp } }] }); setTimeout(() => { - props.onSeriesFocus?.(event.seriesName) - }, 0) - }) + props.onSeriesFocus?.(event.seriesName); + }, 0); + }); return () => { chart.dispose(); diff --git a/frontend/app/components/Charts/PieChart.tsx b/frontend/app/components/Charts/PieChart.tsx index 86a5c9ade..eb107253b 100644 --- a/frontend/app/components/Charts/PieChart.tsx +++ b/frontend/app/components/Charts/PieChart.tsx @@ -1,7 +1,11 @@ import React, { useEffect, useRef } from 'react'; import { PieChart as EchartsPieChart } from 'echarts/charts'; import { echarts, defaultOptions } from './init'; -import { buildPieData, pieTooltipFormatter, pickColorByIndex } from './pieUtils'; +import { + buildPieData, + pieTooltipFormatter, + pickColorByIndex, +} from './pieUtils'; echarts.use([EchartsPieChart]); @@ -28,7 +32,8 @@ function PieChart(props: PieChartProps) { useEffect(() => { if (!chartRef.current) return; if (!data.chart || data.chart.length === 0) { - chartRef.current.innerHTML = `
No data available
`; + chartRef.current.innerHTML = + '
No data available
'; return; } @@ -36,7 +41,8 @@ function PieChart(props: PieChartProps) { const pieData = buildPieData(data.chart, data.namesMap); if (!pieData.length) { - chartRef.current.innerHTML = `
No data available
`; + chartRef.current.innerHTML = + '
No data available
'; return; } @@ -75,28 +81,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 +108,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); }); @@ -121,7 +123,10 @@ function PieChart(props: PieChartProps) { }, [data, label, onClick, inGrid]); return ( -
+
); } diff --git a/frontend/app/components/Charts/SankeyChart.tsx b/frontend/app/components/Charts/SankeyChart.tsx index 4618d709d..9f2ca31c4 100644 --- a/frontend/app/components/Charts/SankeyChart.tsx +++ b/frontend/app/components/Charts/SankeyChart.tsx @@ -1,9 +1,10 @@ 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'; +import { useTranslation } from 'react-i18next'; echarts.use([SankeyChart]); @@ -36,6 +37,7 @@ interface Props { } const EChartsSankey: React.FC = (props) => { + const { t } = useTranslation(); const { data, height = 240, onChartClick, isUngrouped } = props; const chartRef = React.useRef(null); @@ -44,8 +46,8 @@ const EChartsSankey: React.FC = (props) => { React.useEffect(() => { if (!chartRef.current || data.nodes.length === 0 || data.links.length === 0) return; - let finalNodes = data.nodes; - let finalLinks = data.links; + const finalNodes = data.nodes; + const finalLinks = data.links; const chart = echarts.init(chartRef.current); @@ -77,8 +79,8 @@ const EChartsSankey: React.FC = (props) => { computedName === 'Others' ? 'rgba(34,44,154,.9)' : n.eventType === 'DROP' - ? '#B5B7C8' - : '#394eff'; + ? '#B5B7C8' + : '#394eff'; return { name: computedName, @@ -94,9 +96,8 @@ const EChartsSankey: React.FC = (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) => ({ @@ -158,22 +159,23 @@ const EChartsSankey: React.FC = (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.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}}` @@ -208,46 +210,52 @@ const EChartsSankey: React.FC = (props) => { }, clickIcon: { backgroundColor: { - image: 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22currentColor%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20class%3D%22lucide%20lucide-pointer%22%3E%3Cpath%20d%3D%22M22%2014a8%208%200%200%201-8%208%22%2F%3E%3Cpath%20d%3D%22M18%2011v-1a2%202%200%200%200-2-2a2%202%200%200%200-2%202%22%2F%3E%3Cpath%20d%3D%22M14%2010V9a2%202%200%200%200-2-2a2%202%200%200%200-2%202v1%22%2F%3E%3Cpath%20d%3D%22M10%209.5V4a2%202%200%200%200-2-2a2%202%200%200%200-2%202v10%22%2F%3E%3Cpath%20d%3D%22M18%2011a2%202%200%201%201%204%200v3a8%208%200%200%201-8%208h-2c-2.8%200-4.5-.86-5.99-2.34l-3.6-3.6a2%202%200%200%201%202.83-2.82L7%2015%22%2F%3E%3C%2Fsvg%3E', + image: + 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22currentColor%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20class%3D%22lucide%20lucide-pointer%22%3E%3Cpath%20d%3D%22M22%2014a8%208%200%200%201-8%208%22%2F%3E%3Cpath%20d%3D%22M18%2011v-1a2%202%200%200%200-2-2a2%202%200%200%200-2%202%22%2F%3E%3Cpath%20d%3D%22M14%2010V9a2%202%200%200%200-2-2a2%202%200%200%200-2%202v1%22%2F%3E%3Cpath%20d%3D%22M10%209.5V4a2%202%200%200%200-2-2a2%202%200%200%200-2%202v10%22%2F%3E%3Cpath%20d%3D%22M18%2011a2%202%200%201%201%204%200v3a8%208%200%200%201-8%208h-2c-2.8%200-4.5-.86-5.99-2.34l-3.6-3.6a2%202%200%200%201%202.83-2.82L7%2015%22%2F%3E%3C%2Fsvg%3E', }, height: 20, width: 14, }, locationIcon: { backgroundColor: { - image: 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22currentColor%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20class%3D%22lucide%20lucide-navigation%22%3E%3Cpolygon%20points%3D%223%2011%2022%202%2013%2021%2011%2013%203%2011%22%2F%3E%3C%2Fsvg%3E', + image: + 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22currentColor%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20class%3D%22lucide%20lucide-navigation%22%3E%3Cpolygon%20points%3D%223%2011%2022%202%2013%2021%2011%2013%203%2011%22%2F%3E%3C%2Fsvg%3E', }, height: 20, width: 14, }, inputIcon: { backgroundColor: { - image: 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22currentColor%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20class%3D%22lucide%20lucide-rectangle-ellipsis%22%3E%3Crect%20width%3D%2220%22%20height%3D%2212%22%20x%3D%222%22%20y%3D%226%22%20rx%3D%222%22%2F%3E%3Cpath%20d%3D%22M12%2012h.01%22%2F%3E%3Cpath%20d%3D%22M17%2012h.01%22%2F%3E%3Cpath%20d%3D%22M7%2012h.01%22%2F%3E%3C%2Fsvg%3E', + image: + 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22currentColor%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20class%3D%22lucide%20lucide-rectangle-ellipsis%22%3E%3Crect%20width%3D%2220%22%20height%3D%2212%22%20x%3D%222%22%20y%3D%226%22%20rx%3D%222%22%2F%3E%3Cpath%20d%3D%22M12%2012h.01%22%2F%3E%3Cpath%20d%3D%22M17%2012h.01%22%2F%3E%3Cpath%20d%3D%22M7%2012h.01%22%2F%3E%3C%2Fsvg%3E', }, height: 20, width: 14, }, customEventIcon: { backgroundColor: { - image: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWNvZGUiPjxwb2x5bGluZSBwb2ludHM9IjE2IDE4IDIyIDEyIDE2IDYiLz48cG9seWxpbmUgcG9pbnRzPSI4IDYgMiAxMiA4IDE4Ii8+PC9zdmc+' + image: + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWNvZGUiPjxwb2x5bGluZSBwb2ludHM9IjE2IDE4IDIyIDEyIDE2IDYiLz48cG9seWxpbmUgcG9pbnRzPSI4IDYgMiAxMiA4IDE4Ii8+PC9zdmc+', }, height: 20, width: 14, }, dropEventIcon: { backgroundColor: { - image: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWNpcmNsZS1hcnJvdy1kb3duIj48Y2lyY2xlIGN4PSIxMiIgY3k9IjEyIiByPSIxMCIvPjxwYXRoIGQ9Ik0xMiA4djgiLz48cGF0aCBkPSJtOCAxMiA0IDQgNC00Ii8+PC9zdmc+', + image: + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWNpcmNsZS1hcnJvdy1kb3duIj48Y2lyY2xlIGN4PSIxMiIgY3k9IjEyIiByPSIxMCIvPjxwYXRoIGQ9Ik0xMiA4djgiLz48cGF0aCBkPSJtOCAxMiA0IDQgNC00Ii8+PC9zdmc+', }, height: 20, width: 14, }, groupIcon: { backgroundColor: { - image: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWNvbXBvbmVudCI+PHBhdGggZD0iTTE1LjUzNiAxMS4yOTNhMSAxIDAgMCAwIDAgMS40MTRsMi4zNzYgMi4zNzdhMSAxIDAgMCAwIDEuNDE0IDBsMi4zNzctMi4zNzdhMSAxIDAgMCAwIDAtMS40MTRsLTIuMzc3LTIuMzc3YTEgMSAwIDAgMC0xLjQxNCAweiIvPjxwYXRoIGQ9Ik0yLjI5NyAxMS4yOTNhMSAxIDAgMCAwIDAgMS40MTRsMi4zNzcgMi4zNzdhMSAxIDAgMCAwIDEuNDE0IDBsMi4zNzctMi4zNzdhMSAxIDAgMCAwIDAtMS40MTRMNi4wODggOC45MTZhMSAxIDAgMCAwLTEuNDE0IDB6Ii8+PHBhdGggZD0iTTguOTE2IDE3LjkxMmExIDEgMCAwIDAgMCAxLjQxNWwyLjM3NyAyLjM3NmExIDEgMCAwIDAgMS40MTQgMGwyLjM3Ny0yLjM3NmExIDEgMCAwIDAgMC0xLjQxNWwtMi4zNzctMi4zNzZhMSAxIDAgMCAwLTEuNDE0IDB6Ii8+PHBhdGggZD0iTTguOTE2IDQuNjc0YTEgMSAwIDAgMCAwIDEuNDE0bDIuMzc3IDIuMzc2YTEgMSAwIDAgMCAxLjQxNCAwbDIuMzc3LTIuMzc2YTEgMSAwIDAgMCAwLTEuNDE0bC0yLjM3Ny0yLjM3N2ExIDEgMCAwIDAtMS40MTQgMHoiLz48L3N2Zz4=', + image: + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWNvbXBvbmVudCI+PHBhdGggZD0iTTE1LjUzNiAxMS4yOTNhMSAxIDAgMCAwIDAgMS40MTRsMi4zNzYgMi4zNzdhMSAxIDAgMCAwIDEuNDE0IDBsMi4zNzctMi4zNzdhMSAxIDAgMCAwIDAtMS40MTRsLTIuMzc3LTIuMzc3YTEgMSAwIDAgMC0xLjQxNCAweiIvPjxwYXRoIGQ9Ik0yLjI5NyAxMS4yOTNhMSAxIDAgMCAwIDAgMS40MTRsMi4zNzcgMi4zNzdhMSAxIDAgMCAwIDEuNDE0IDBsMi4zNzctMi4zNzdhMSAxIDAgMCAwIDAtMS40MTRMNi4wODggOC45MTZhMSAxIDAgMCAwLTEuNDE0IDB6Ii8+PHBhdGggZD0iTTguOTE2IDE3LjkxMmExIDEgMCAwIDAgMCAxLjQxNWwyLjM3NyAyLjM3NmExIDEgMCAwIDAgMS40MTQgMGwyLjM3Ny0yLjM3NmExIDEgMCAwIDAgMC0xLjQxNWwtMi4zNzctMi4zNzZhMSAxIDAgMCAwLTEuNDE0IDB6Ii8+PHBhdGggZD0iTTguOTE2IDQuNjc0YTEgMSAwIDAgMCAwIDEuNDE0bDIuMzc3IDIuMzc2YTEgMSAwIDAgMCAxLjQxNCAwbDIuMzc3LTIuMzc2YTEgMSAwIDAgMCAwLTEuNDE0bC0yLjM3Ny0yLjM3N2ExIDEgMCAwIDAtMS40MTQgMHoiLz48L3N2Zz4=', }, height: 20, width: 14, - } + }, }, }, tooltip: { @@ -302,7 +310,7 @@ const EChartsSankey: React.FC = (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); @@ -345,7 +353,7 @@ const EChartsSankey: React.FC = (props) => { } }); - chart.on('mouseout', function (params: any) { + chart.on('mouseout', (params: any) => { if (params.dataType === 'node') { chart.setOption({ series: [ @@ -358,7 +366,7 @@ const EChartsSankey: React.FC = (props) => { } }); - chart.on('click', function (params: any) { + chart.on('click', (params: any) => { if (!onChartClick) return; const unsupported = ['other', 'drop']; @@ -372,7 +380,7 @@ const EChartsSankey: React.FC = (props) => { } filters.push({ operator: 'is', - type: type, + type, value: [node.name], isEvent: true, }); @@ -456,25 +464,28 @@ const EChartsSankey: React.FC = (props) => { } return ( -
-
+
+
); }; 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|}'; @@ -485,7 +496,7 @@ function getIcon(type: string) { if (type === 'OTHER') { return '{groupIcon|}'; } - return '' + return ''; } export default EChartsSankey; diff --git a/frontend/app/components/Charts/barUtils.ts b/frontend/app/components/Charts/barUtils.ts index 86c55a95d..1912cd4fd 100644 --- a/frontend/app/components/Charts/barUtils.ts +++ b/frontend/app/components/Charts/barUtils.ts @@ -1,5 +1,9 @@ import type { DataProps, DataItem } from './utils'; -import { createDataset, assignColorsByBaseName, assignColorsByCategory } from './utils'; +import { + createDataset, + assignColorsByBaseName, + assignColorsByCategory, +} from './utils'; export function createBarSeries( data: DataProps['data'], @@ -13,7 +17,9 @@ export function createBarSeries( const encode = { x: 'idx', y: fullName }; const borderRadius = [6, 6, 0, 0]; - const decal = dashed ? { symbol: 'line', symbolSize: 10, rotation: 1 } : { symbol: 'none' }; + const decal = dashed + ? { symbol: 'line', symbolSize: 10, rotation: 1 } + : { symbol: 'none' }; return { name: fullName, _baseName: baseName, @@ -47,7 +53,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 +67,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); @@ -114,7 +119,9 @@ export function buildColumnChart( }; } - const series = previousSeries ? [currentSeries, previousSeries] : [currentSeries]; + const series = previousSeries + ? [currentSeries, previousSeries] + : [currentSeries]; assignColorsByCategory(series, categories); diff --git a/frontend/app/components/Charts/init.ts b/frontend/app/components/Charts/init.ts index 6544dece3..3869ab4ca 100644 --- a/frontend/app/components/Charts/init.ts +++ b/frontend/app/components/Charts/init.ts @@ -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,18 +56,23 @@ 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[] = []) { +export function initWindowStorages( + chartUuid: string, + categories: string[] = [], + chartArr: any[] = [], + compChartArr: any[] = [], +) { (window as any).__seriesValueMap = (window as any).__seriesValueMap ?? {}; (window as any).__seriesColorMap = (window as any).__seriesColorMap ?? {}; (window as any).__timestampMap = (window as any).__timestampMap ?? {}; @@ -84,11 +89,15 @@ export function initWindowStorages(chartUuid: string, categories: string[] = [], (window as any).__categoryMap[chartUuid] = categories; } if (!(window as any).__timestampMap[chartUuid]) { - (window as any).__timestampMap[chartUuid] = chartArr.map((item) => item.timestamp); + (window as any).__timestampMap[chartUuid] = chartArr.map( + (item) => item.timestamp, + ); } if (!(window as any).__timestampCompMap[chartUuid]) { - (window as any).__timestampCompMap[chartUuid] = compChartArr.map((item) => item.timestamp); + (window as any).__timestampCompMap[chartUuid] = compChartArr.map( + (item) => item.timestamp, + ); } } -export { echarts, defaultOptions }; \ No newline at end of file +export { echarts, defaultOptions }; diff --git a/frontend/app/components/Charts/pieUtils.ts b/frontend/app/components/Charts/pieUtils.ts index 2c5426cf4..96567b0ec 100644 --- a/frontend/app/components/Charts/pieUtils.ts +++ b/frontend/app/components/Charts/pieUtils.ts @@ -1,10 +1,9 @@ -import { colors } from './utils'; import { numberWithCommas } from 'App/utils'; - +import { colors } from './utils'; export function buildPieData( chart: Array>, - namesMap: string[] + namesMap: string[], ) { const result: { name: string; value: number }[] = namesMap.map((name) => { let sum = 0; @@ -28,4 +27,4 @@ export function pieTooltipFormatter(params: any) { export function pickColorByIndex(idx: number) { return colors[idx % colors.length]; -} \ No newline at end of file +} diff --git a/frontend/app/components/Charts/sankeyUtils.ts b/frontend/app/components/Charts/sankeyUtils.ts index a4e0826af..56b9947c0 100644 --- a/frontend/app/components/Charts/sankeyUtils.ts +++ b/frontend/app/components/Charts/sankeyUtils.ts @@ -1,6 +1,6 @@ export function sankeyTooltip( echartNodes: any[], - nodeValues: Record + nodeValues: Record, ) { return (params: any) => { if ('source' in params.data && 'target' in params.data) { @@ -25,8 +25,8 @@ export function sankeyTooltip(
${params.data.value} ( ${params.data.percentage.toFixed( - 2 - )}% ) + 2, + )}% ) Sessions
@@ -53,7 +53,7 @@ const shortenString = (str: string) => { str.length > limit ? `${str.slice(0, leftPart)}...${str.slice( str.length - rightPart, - str.length + str.length, )}` : str; @@ -73,7 +73,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(); diff --git a/frontend/app/components/Charts/utils.ts b/frontend/app/components/Charts/utils.ts index 8a5454991..c20733dcc 100644 --- a/frontend/app/components/Charts/utils.ts +++ b/frontend/app/components/Charts/utils.ts @@ -52,10 +52,7 @@ function buildCategoryColorMap(categories: string[]): Record { * For each series, transform its data array to an array of objects * with `value` and `itemStyle.color` based on the category index. */ -export function assignColorsByCategory( - series: any[], - categories: string[] -) { +export function assignColorsByCategory(series: any[], categories: string[]) { const categoryColorMap = buildCategoryColorMap(categories); series.forEach((s, si) => { @@ -94,7 +91,9 @@ export function customTooltipFormatter(uuid: string) { const isPrevious = /Previous/.test(seriesName); const categoryName = (window as any).__yAxisData?.[uuid]?.[dataIndex]; const fullname = isPrevious ? `Previous ${categoryName}` : categoryName; - const partnerName = isPrevious ? categoryName : `Previous ${categoryName}`; + const partnerName = isPrevious + ? categoryName + : `Previous ${categoryName}`; const partnerValue = (window as any).__seriesValueMap?.[uuid]?.[ partnerName ]; @@ -279,7 +278,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+/, ''); diff --git a/frontend/app/components/Client/Audit/AuditDetailModal/AuditDetailModal.tsx b/frontend/app/components/Client/Audit/AuditDetailModal/AuditDetailModal.tsx index 634eace5d..756f76009 100644 --- a/frontend/app/components/Client/Audit/AuditDetailModal/AuditDetailModal.tsx +++ b/frontend/app/components/Client/Audit/AuditDetailModal/AuditDetailModal.tsx @@ -1,53 +1,60 @@ import React from 'react'; import { JSONTree } from 'UI'; import { checkForRecent } from 'App/date'; +import { useTranslation } from 'react-i18next'; interface Props { - audit: any; + 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 { t } = useTranslation(); + const { audit } = props; + // const jsonResponse = typeof audit.payload === 'string' ? JSON.parse(audit.payload) : audit.payload; + // console.log('jsonResponse', jsonResponse) - return ( -
-

Audit Details

-
-
{ 'URL'}
-
{ audit.endPoint }
- -
-
-
Username
-
{audit.username}
-
-
-
Created At
-
{audit.createdAt && checkForRecent(audit.createdAt, 'LLL dd, yyyy, hh:mm a')}
-
-
- -
-
-
Action
-
{audit.action}
-
-
-
Method
-
{audit.method}
-
-
- - { audit.payload && ( -
-
Payload
- -
- )} -
+ return ( +
+

{t('Audit Details')}

+
+
{t('URL')}
+
+ {audit.endPoint}
- ); + +
+
+
{t('Username')}
+
{audit.username}
+
+
+
{t('Created At')}
+
+ {audit.createdAt && + checkForRecent(audit.createdAt, 'LLL dd, yyyy, hh:mm a')} +
+
+
+ +
+
+
{t('Action')}
+
{audit.action}
+
+
+
{t('Method')}
+
{audit.method}
+
+
+ + {audit.payload && ( +
+
{t('Payload')}
+ +
+ )} +
+
+ ); } -export default AuditDetailModal; \ No newline at end of file +export default AuditDetailModal; diff --git a/frontend/app/components/Client/Audit/AuditDetailModal/index.ts b/frontend/app/components/Client/Audit/AuditDetailModal/index.ts index fcf6bb2b4..9fe899989 100644 --- a/frontend/app/components/Client/Audit/AuditDetailModal/index.ts +++ b/frontend/app/components/Client/Audit/AuditDetailModal/index.ts @@ -1 +1 @@ -export { default } from './AuditDetailModal'; \ No newline at end of file +export { default } from './AuditDetailModal'; diff --git a/frontend/app/components/Client/Audit/AuditList/AuditList.tsx b/frontend/app/components/Client/Audit/AuditList/AuditList.tsx index 43ba629e3..c9a38c8ef 100644 --- a/frontend/app/components/Client/Audit/AuditList/AuditList.tsx +++ b/frontend/app/components/Client/Audit/AuditList/AuditList.tsx @@ -3,73 +3,78 @@ 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'; +import { useTranslation } from 'react-i18next'; -interface Props { - -} +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 { t } = useTranslation(); + 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(() => ( - - - -
No data available
-
- } - size="small" - show={list.length === 0} - > -
-
Name
-
Action
-
Time
-
+ 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) => ( - showModal(, { right: true, width: 500 })} - /> - ))} - -
- auditStore.updateKey('page', page)} - limit={auditStore.pageSize} - debounceRequest={200} - /> -
- - - )); + return useObserver(() => ( + + + +
{t('No data available')}
+
+ } + size="small" + show={list.length === 0} + > +
+
{t('Name')}
+
{t('Action')}
+
{t('Time')}
+
+ + {list.map((item, index) => ( + + showModal(, { + right: true, + width: 500, + }) + } + /> + ))} + +
+ auditStore.updateKey('page', page)} + limit={auditStore.pageSize} + debounceRequest={200} + /> +
+ + + )); } -export default AuditList; \ No newline at end of file +export default AuditList; diff --git a/frontend/app/components/Client/Audit/AuditList/index.ts b/frontend/app/components/Client/Audit/AuditList/index.ts index 2e6bc3739..6f70bca2f 100644 --- a/frontend/app/components/Client/Audit/AuditList/index.ts +++ b/frontend/app/components/Client/Audit/AuditList/index.ts @@ -1 +1 @@ -export { default } from './AuditList' \ No newline at end of file +export { default } from './AuditList'; diff --git a/frontend/app/components/Client/Audit/AuditListItem/AuditListItem.tsx b/frontend/app/components/Client/Audit/AuditListItem/AuditListItem.tsx index 2c9792d33..064532a89 100644 --- a/frontend/app/components/Client/Audit/AuditListItem/AuditListItem.tsx +++ b/frontend/app/components/Client/Audit/AuditListItem/AuditListItem.tsx @@ -2,18 +2,26 @@ import React from 'react'; import { checkForRecent } from 'App/date'; interface Props { - audit: any; - onShowDetails: () => void; + audit: any; + onShowDetails: () => void; } function AuditListItem(props: Props) { - const { audit, onShowDetails } = props; - return ( -
-
{audit.username}
-
{audit.action}
-
{audit.createdAt && checkForRecent(audit.createdAt, 'LLL dd, yyyy, hh:mm a')}
-
- ); + const { audit, onShowDetails } = props; + return ( +
+
{audit.username}
+
+ {audit.action} +
+
+ {audit.createdAt && + checkForRecent(audit.createdAt, 'LLL dd, yyyy, hh:mm a')} +
+
+ ); } -export default AuditListItem; \ No newline at end of file +export default AuditListItem; diff --git a/frontend/app/components/Client/Audit/AuditListItem/index.ts b/frontend/app/components/Client/Audit/AuditListItem/index.ts index 821ee9639..dbd6b2a5f 100644 --- a/frontend/app/components/Client/Audit/AuditListItem/index.ts +++ b/frontend/app/components/Client/Audit/AuditListItem/index.ts @@ -1 +1 @@ -export { default } from './AuditListItem'; \ No newline at end of file +export { default } from './AuditListItem'; diff --git a/frontend/app/components/Client/Audit/AuditSearchField/AuditSearchField.tsx b/frontend/app/components/Client/Audit/AuditSearchField/AuditSearchField.tsx index 93adf3d3e..7272370c6 100644 --- a/frontend/app/components/Client/Audit/AuditSearchField/AuditSearchField.tsx +++ b/frontend/app/components/Client/Audit/AuditSearchField/AuditSearchField.tsx @@ -2,33 +2,37 @@ 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; + 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 ( -
- - -
- ); + const write = ({ target: { name, value } }) => { + debounceUpdate(value); + }; + + return ( +
+ + +
+ ); } -export default AuditSearchField; \ No newline at end of file +export default AuditSearchField; diff --git a/frontend/app/components/Client/Audit/AuditSearchField/index.ts b/frontend/app/components/Client/Audit/AuditSearchField/index.ts index 646947095..ac0f6c188 100644 --- a/frontend/app/components/Client/Audit/AuditSearchField/index.ts +++ b/frontend/app/components/Client/Audit/AuditSearchField/index.ts @@ -1 +1 @@ -export { default } from './AuditSearchField'; \ No newline at end of file +export { default } from './AuditSearchField'; diff --git a/frontend/app/components/Client/Audit/AuditView/AuditView.tsx b/frontend/app/components/Client/Audit/AuditView/AuditView.tsx index 1fde1ff31..41852e31e 100644 --- a/frontend/app/components/Client/Audit/AuditView/AuditView.tsx +++ b/frontend/app/components/Client/Audit/AuditView/AuditView.tsx @@ -1,77 +1,91 @@ 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'; +import { useTranslation } from 'react-i18next'; function AuditView() { - const { auditStore } = useStore(); - const order = useObserver(() => auditStore.order); - const total = useObserver(() => numberWithCommas(auditStore.total)); + const { t } = useTranslation(); + 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(() => ( -
-
- - Audit Trail - {total} -
- } /> -
-
- -
-
- + auditStore.updateKey('order', value.value) + } + /> +
+ { + auditStore.updateKey('searchQuery', value); + auditStore.updateKey('page', 1); + }} + /> +
+ +
- )); +
+ + +
+ )); } -export default withPageTitle('Audit Trail - OpenReplay Preferences')(AuditView); \ No newline at end of file +export default withPageTitle('Audit Trail - OpenReplay Preferences')(AuditView); diff --git a/frontend/app/components/Client/Audit/AuditView/index.ts b/frontend/app/components/Client/Audit/AuditView/index.ts index ba32b1be0..fcaf5cf0c 100644 --- a/frontend/app/components/Client/Audit/AuditView/index.ts +++ b/frontend/app/components/Client/Audit/AuditView/index.ts @@ -1 +1 @@ -export { default } from './AuditView' \ No newline at end of file +export { default } from './AuditView'; diff --git a/frontend/app/components/Client/Client.tsx b/frontend/app/components/Client/Client.tsx index 06991988d..cf41335a9 100644 --- a/frontend/app/components/Client/Client.tsx +++ b/frontend/app/components/Client/Client.tsx @@ -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 { @@ -28,17 +28,72 @@ export default class Client extends React.PureComponent { renderActiveTab = () => ( - - - - - - - - - - - + + + + + + + + + + + ); @@ -46,11 +101,11 @@ export default class Client extends React.PureComponent { render() { const { match: { - params: { activeTab } - } + params: { activeTab }, + }, } = this.props; return ( -
+
{activeTab && this.renderActiveTab()}
); diff --git a/frontend/app/components/Client/CustomFields/CustomFieldForm.js b/frontend/app/components/Client/CustomFields/CustomFieldForm.js index 19cf244d2..2c42e3d3e 100644 --- a/frontend/app/components/Client/CustomFields/CustomFieldForm.js +++ b/frontend/app/components/Client/CustomFields/CustomFieldForm.js @@ -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 (
-

{exists ? 'Update' : 'Add'} Metadata Field

+

+ {exists ? 'Update' : 'Add'} + {' '} + Metadata Field +

- +
- +
); -}; +} 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); diff --git a/frontend/app/components/Client/CustomFields/CustomFieldForm.tsx b/frontend/app/components/Client/CustomFields/CustomFieldForm.tsx index 82c0372d0..1999e27ad 100644 --- a/frontend/app/components/Client/CustomFields/CustomFieldForm.tsx +++ b/frontend/app/components/Client/CustomFields/CustomFieldForm.tsx @@ -1,66 +1,75 @@ 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'; +import { useTranslation } from 'react-i18next'; interface CustomFieldFormProps { siteId: string; } const CustomFieldForm: React.FC = ({ siteId }) => { + const { t } = useTranslation(); const focusElementRef = useRef(null); const { customFieldStore: store } = useStore(); const field = store.instance; const { hideModal } = useModal(); const [loading, setLoading] = useState(false); - const write = ({ target: { value, name } }: any) => store.edit({ [name]: value }); + const write = ({ target: { value, name } }: any) => + store.edit({ [name]: value }); const exists = field?.exists(); const onDelete = async () => { Modal.confirm({ - title: 'Metadata', - content: `Are you sure you want to remove?`, + title: t('Metadata'), + content: t('Are you sure you want to remove?'), onOk: async () => { await store.remove(siteId, field?.index!); hideModal(); - } + }, }); }; const onSave = (field: any) => { setLoading(true); - store.save(siteId, field).then((response) => { - if (!response || !response.errors || response.errors.size === 0) { - hideModal(); - toast.success('Metadata added successfully!'); - } else { - toast.error(response.errors[0]); - } - }).catch(() => { - toast.error('An error occurred while saving metadata.'); - }).finally(() => { - setLoading(false); - }); + store + .save(siteId, field) + .then((response) => { + if (!response || !response.errors || response.errors.size === 0) { + hideModal(); + toast.success(t('Metadata added successfully!')); + } else { + toast.error(response.errors[0]); + } + }) + .catch(() => { + toast.error(t('An error occurred while saving metadata.')); + }) + .finally(() => { + setLoading(false); + }); }; return (
-

{exists ? 'Update' : 'Add'} Metadata Field

+

+ {exists ? t('Update') : 'Add'} {t('Metadata Field')} +

- + @@ -74,14 +83,19 @@ const CustomFieldForm: React.FC = ({ siteId }) => { type="primary" className="float-left mr-2" > - {exists ? 'Update' : 'Add'} + {exists ? t('Update') : t('Add')} -
- +
diff --git a/frontend/app/components/Client/CustomFields/CustomFields.tsx b/frontend/app/components/Client/CustomFields/CustomFields.tsx index dd816a2a1..5ce43c0f0 100644 --- a/frontend/app/components/Client/CustomFields/CustomFields.tsx +++ b/frontend/app/components/Client/CustomFields/CustomFields.tsx @@ -1,20 +1,21 @@ 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 { 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'; +import { useTranslation } from 'react-i18next'; - -const CustomFields = () => { +function CustomFields() { usePageTitle('Metadata - OpenReplay Preferences'); + const { t } = useTranslation(); const { customFieldStore: store, projectsStore } = useStore(); const currentSite = projectsStore.config.project; - const { showModal, hideModal } = useModal(); + const { showModal } = useModal(); const fields = store.list; const [loading, setLoading] = useState(false); @@ -27,8 +28,9 @@ const CustomFields = () => { const handleInit = (field?: any) => { store.init(field); - showModal(, { - title: field ? 'Edit Metadata' : 'Add Metadata', right: true + showModal(, { + title: field ? t('Edit Metadata') : t('Add Metadata'), + right: true, }); }; @@ -37,32 +39,44 @@ const CustomFields = () => { return (
- Attach key-value pairs to session replays for enhanced filtering, searching, and identifying relevant user - sessions. + {t('Attach key-value pairs to session replays for enhanced filtering, searching, and identifying relevant user sessions.')} - Learn more + {t('Learn more')} 0 ? '' : 'You\'ve reached the limit of 10 metadata.'} + title={ + remaining > 0 ? '' : t("You've reached the limit of 10 metadata.") + } > - - {/*{remaining === 0 && }*/} + {/* {remaining === 0 && } */} - {remaining === 0 ? 'You have reached the limit of 10 metadata.' : `${remaining}/10 Remaining for this project`} + {remaining === 0 + ? t('You have reached the limit of 10 metadata.') + : `${remaining}${t('/10 Remaining for this project')}`} } /> + emptyText: ( + } + /> + ), }} loading={loading} dataSource={fields} @@ -71,17 +85,19 @@ const CustomFields = () => { onClick={() => handleInit(field)} className="cursor-pointer group hover:bg-active-blue !px-4" actions={[ -
); -}; +} export default observer(CustomFields); diff --git a/frontend/app/components/Client/CustomFields/ListItem.js b/frontend/app/components/Client/CustomFields/ListItem.js index 00c484c6e..5bee97333 100644 --- a/frontend/app/components/Client/CustomFields/ListItem.js +++ b/frontend/app/components/Client/CustomFields/ListItem.js @@ -4,24 +4,27 @@ 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 (
field.index !== 0 && onEdit(field)} > {field.key} -
-
); -}; +} export default ListItem; diff --git a/frontend/app/components/Client/CustomFields/index.js b/frontend/app/components/Client/CustomFields/index.js index 2b1c8b48b..8519422a1 100644 --- a/frontend/app/components/Client/CustomFields/index.js +++ b/frontend/app/components/Client/CustomFields/index.js @@ -1 +1 @@ -export { default } from './CustomFields'; \ No newline at end of file +export { default } from './CustomFields'; diff --git a/frontend/app/components/Client/DebugLog.tsx b/frontend/app/components/Client/DebugLog.tsx index 0a5f8f292..84c8ce18f 100644 --- a/frontend/app/components/Client/DebugLog.tsx +++ b/frontend/app/components/Client/DebugLog.tsx @@ -1,33 +1,36 @@ -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'; +import { useTranslation } from 'react-i18next'; 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 { t } = useTranslation(); + const [showLogs, setShowLogs] = React.useState(getDefaults); const onChange = (checked: boolean) => { - setShowLogs(checked) - options.logStuff(checked) - } + setShowLogs(checked); + options.logStuff(checked); + }; return (
-

Player Debug Logs

-
Show debug information in browser console.
-
+

{t('Player Debug Logs')}

+
+ {t('Show debug information in browser console.')} +
+
- ) + ); } -export default DebugLog \ No newline at end of file +export default DebugLog; diff --git a/frontend/app/components/Client/Integrations/Backend/DatadogForm/DatadogFormModal.tsx b/frontend/app/components/Client/Integrations/Backend/DatadogForm/DatadogFormModal.tsx index 2910621d0..c5962d7bd 100644 --- a/frontend/app/components/Client/Integrations/Backend/DatadogForm/DatadogFormModal.tsx +++ b/frontend/app/components/Client/Integrations/Backend/DatadogForm/DatadogFormModal.tsx @@ -10,6 +10,7 @@ import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModa import { Loader } from 'UI'; import DocLink from 'Shared/DocLink/DocLink'; +import { useTranslation } from 'react-i18next'; interface DatadogConfig { site: string; @@ -23,15 +24,16 @@ const initialValues = { app_key: '', }; -const DatadogFormModal = ({ +function DatadogFormModal({ onClose, integrated, }: { onClose: () => void; integrated: boolean; -}) => { +}) { + const { t } = useTranslation(); const { integrationsStore } = useStore(); - const siteId = integrationsStore.integrations.siteId; + const { siteId } = integrationsStore.integrations; const { data = initialValues, @@ -39,17 +41,20 @@ const DatadogFormModal = ({ saveMutation, removeMutation, } = useIntegration('datadog', siteId, initialValues); - const { values, errors, handleChange, hasErrors, checkErrors } = useForm(data, { - site: { - required: true, + const { values, errors, handleChange, hasErrors, checkErrors } = useForm( + data, + { + site: { + required: true, + }, + api_key: { + required: true, + }, + app_key: { + required: true, + }, }, - api_key: { - required: true, - }, - app_key: { - required: true, - }, - }); + ); const exists = Boolean(data.api_key); const save = async () => { @@ -59,7 +64,7 @@ const DatadogFormModal = ({ try { await saveMutation.mutateAsync({ values, siteId, exists }); } catch (e) { - console.error(e) + console.error(e); } onClose(); }; @@ -68,7 +73,7 @@ const DatadogFormModal = ({ try { await removeMutation.mutateAsync({ siteId }); } catch (e) { - console.error(e) + console.error(e); } onClose(); }; @@ -83,20 +88,20 @@ const DatadogFormModal = ({ description="Incorporate DataDog to visualize backend errors alongside session replay, for easy troubleshooting." />
-
How it works?
+
{t('How it works?')}
    -
  1. Generate Datadog API Key & Application Key
  2. -
  3. Enter the API key below
  4. -
  5. Propagate openReplaySessionToken
  6. +
  7. {t('Generate Datadog API Key & Application Key')}
  8. +
  9. {t('Enter the API key below')}
  10. +
  11. {t('Propagate openReplaySessionToken')}
-
+
{integrated && ( )}
@@ -137,7 +142,7 @@ const DatadogFormModal = ({
); -}; +} DatadogFormModal.displayName = 'DatadogForm'; diff --git a/frontend/app/components/Client/Integrations/Backend/DynatraceForm/DynatraceFormModal.tsx b/frontend/app/components/Client/Integrations/Backend/DynatraceForm/DynatraceFormModal.tsx index 325fd6079..6b01bc274 100644 --- a/frontend/app/components/Client/Integrations/Backend/DynatraceForm/DynatraceFormModal.tsx +++ b/frontend/app/components/Client/Integrations/Backend/DynatraceForm/DynatraceFormModal.tsx @@ -10,6 +10,7 @@ import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModa import { Loader } from 'UI'; import DocLink from 'Shared/DocLink/DocLink'; +import { useTranslation } from 'react-i18next'; interface DynatraceConfig { environment: string; @@ -24,35 +25,39 @@ const initialValues = { client_secret: '', resource: '', }; -const DynatraceFormModal = ({ +function DynatraceFormModal({ onClose, integrated, }: { onClose: () => void; integrated: boolean; -}) => { +}) { + const { t } = useTranslation(); const { integrationsStore } = useStore(); - const siteId = integrationsStore.integrations.siteId; + const { siteId } = integrationsStore.integrations; const { data = initialValues, isPending, saveMutation, removeMutation, } = useIntegration('dynatrace', siteId, initialValues); - const { values, errors, handleChange, hasErrors, checkErrors } = useForm(data, { - environment: { - required: true, + const { values, errors, handleChange, hasErrors, checkErrors } = useForm( + data, + { + environment: { + required: true, + }, + client_id: { + required: true, + }, + client_secret: { + required: true, + }, + resource: { + required: true, + }, }, - client_id: { - required: true, - }, - client_secret: { - required: true, - }, - resource: { - required: true, - }, - }); + ); const exists = Boolean(data.client_id); const save = async () => { @@ -62,7 +67,7 @@ const DynatraceFormModal = ({ try { await saveMutation.mutateAsync({ values, siteId, exists }); } catch (e) { - console.error(e) + console.error(e); } onClose(); }; @@ -71,7 +76,7 @@ const DynatraceFormModal = ({ try { await removeMutation.mutateAsync({ siteId }); } catch (e) { - console.error(e) + console.error(e); } onClose(); }; @@ -81,33 +86,40 @@ const DynatraceFormModal = ({ style={{ width: '350px' }} >
-
How it works?
+
{t('How it works?')}
  1. - Enter your Environment ID, Client ID, Client Secret, and Account URN - in the form below. + {t( + 'Enter your Environment ID, Client ID, Client Secret, and Account URN in the form below.', + )}
  2. - Create a custom Log attribute openReplaySessionToken in Dynatrace. + {t( + 'Create a custom Log attribute openReplaySessionToken in Dynatrace.', + )}
  3. - Propagate openReplaySessionToken in your application's backend logs. + {t( + "Propagate openReplaySessionToken in your application's backend logs.", + )}
-
+
{integrated && ( )}
@@ -156,7 +168,7 @@ const DynatraceFormModal = ({
); -}; +} DynatraceFormModal.displayName = 'DynatraceFormModal'; diff --git a/frontend/app/components/Client/Integrations/Backend/ElasticForm/ElasticFormModal.tsx b/frontend/app/components/Client/Integrations/Backend/ElasticForm/ElasticFormModal.tsx index f86dfbd79..4c5009ba9 100644 --- a/frontend/app/components/Client/Integrations/Backend/ElasticForm/ElasticFormModal.tsx +++ b/frontend/app/components/Client/Integrations/Backend/ElasticForm/ElasticFormModal.tsx @@ -10,6 +10,7 @@ import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModa import { Loader } from 'UI'; import DocLink from 'Shared/DocLink/DocLink'; +import { useTranslation } from 'react-i18next'; interface ElasticConfig { url: string; @@ -32,25 +33,29 @@ function ElasticsearchForm({ onClose: () => void; integrated: boolean; }) { + const { t } = useTranslation(); const { integrationsStore } = useStore(); - const siteId = integrationsStore.integrations.siteId; + const { siteId } = integrationsStore.integrations; const { data = initialValues, isPending, saveMutation, removeMutation, } = useIntegration('elasticsearch', siteId, initialValues); - const { values, errors, handleChange, hasErrors, checkErrors } = useForm(data, { - url: { - required: true, + const { values, errors, handleChange, hasErrors, checkErrors } = useForm( + data, + { + url: { + required: true, + }, + api_key_id: { + required: true, + }, + api_key: { + required: true, + }, }, - api_key_id: { - required: true, - }, - api_key: { - required: true, - }, - }); + ); const exists = Boolean(data.api_key_id); const save = async () => { @@ -60,7 +65,7 @@ function ElasticsearchForm({ try { await saveMutation.mutateAsync({ values, siteId, exists }); } catch (e) { - console.error(e) + console.error(e); } onClose(); }; @@ -69,7 +74,7 @@ function ElasticsearchForm({ try { await removeMutation.mutateAsync({ siteId }); } catch (e) { - console.error(e) + console.error(e); } onClose(); }; @@ -81,24 +86,26 @@ function ElasticsearchForm({
-
How it works?
+
{t('How it works?')}
    -
  1. Create a new Elastic API key
  2. -
  3. Enter the API key below
  4. -
  5. Propagate openReplaySessionToken
  6. +
  7. {t('Create a new Elastic API key')}
  8. +
  9. {t('Enter the API key below')}
  10. +
  11. {t('Propagate openReplaySessionToken')}
-
+
{integrated && ( )}
diff --git a/frontend/app/components/Client/Integrations/Backend/SentryForm/SentryFormModal.tsx b/frontend/app/components/Client/Integrations/Backend/SentryForm/SentryFormModal.tsx index 76b427bfd..01d416fdf 100644 --- a/frontend/app/components/Client/Integrations/Backend/SentryForm/SentryFormModal.tsx +++ b/frontend/app/components/Client/Integrations/Backend/SentryForm/SentryFormModal.tsx @@ -10,6 +10,7 @@ import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModa import { Loader } from 'UI'; import { toast } from 'react-toastify'; import DocLink from 'Shared/DocLink/DocLink'; +import { useTranslation } from 'react-i18next'; interface SentryConfig { url: string; @@ -32,28 +33,32 @@ function SentryForm({ onClose: () => void; integrated: boolean; }) { + const { t } = useTranslation(); const { integrationsStore } = useStore(); - const siteId = integrationsStore.integrations.siteId; + const { siteId } = integrationsStore.integrations; const { data = initialValues, isPending, saveMutation, removeMutation, } = useIntegration('sentry', siteId, initialValues); - const { values, errors, handleChange, hasErrors, checkErrors, } = useForm(data, { - url: { - required: false, + const { values, errors, handleChange, hasErrors, checkErrors } = useForm( + data, + { + url: { + required: false, + }, + organization_slug: { + required: true, + }, + project_slug: { + required: true, + }, + token: { + required: true, + }, }, - organization_slug: { - required: true, - }, - project_slug: { - required: true, - }, - token: { - required: true, - }, - }); + ); const exists = Boolean(data.token); const save = async () => { @@ -63,7 +68,7 @@ function SentryForm({ try { await saveMutation.mutateAsync({ values, siteId, exists }); } catch (e) { - console.error(e) + console.error(e); } onClose(); }; @@ -72,7 +77,7 @@ function SentryForm({ try { await removeMutation.mutateAsync({ siteId }); } catch (e) { - console.error(e) + console.error(e); } onClose(); }; @@ -87,28 +92,28 @@ function SentryForm({ description="Integrate Sentry with session replays to seamlessly observe backend errors." />
-
How it works?
+
{t('How it works?')}
    -
  1. Generate Sentry Auth Token
  2. -
  3. Enter the token below
  4. -
  5. Propagate openReplaySessionToken
  6. +
  7. {t('Generate Sentry Auth Token')}
  8. +
  9. {t('Enter the token below')}
  10. +
  11. {t('Propagate openReplaySessionToken')}
-
+
{integrated && ( )}
diff --git a/frontend/app/components/Client/Integrations/FormField.tsx b/frontend/app/components/Client/Integrations/FormField.tsx index e6e17ea2f..96b93051a 100644 --- a/frontend/app/components/Client/Integrations/FormField.tsx +++ b/frontend/app/components/Client/Integrations/FormField.tsx @@ -1,5 +1,5 @@ -import React from "react"; -import { Input } from 'antd' +import React from 'react'; +import { Input } from 'antd'; export function FormField({ label, @@ -10,7 +10,7 @@ export function FormField({ errors, }: { label: string; - name: string + name: string; value: string; onChange: (e: React.ChangeEvent) => void; autoFocus?: boolean; @@ -30,4 +30,4 @@ export function FormField({
); } -export default FormField; \ No newline at end of file +export default FormField; diff --git a/frontend/app/components/Client/Integrations/GithubForm.js b/frontend/app/components/Client/Integrations/GithubForm.js deleted file mode 100644 index b54343c5f..000000000 --- a/frontend/app/components/Client/Integrations/GithubForm.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import IntegrationForm from './IntegrationForm'; -import DocLink from 'Shared/DocLink/DocLink'; -import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModalCard'; - -const GithubForm = (props) => ( -
- -
-
Integrate GitHub with OpenReplay and create issues directly from the recording page.
-
- -
-
- -
-); - -GithubForm.displayName = 'GithubForm'; - -export default GithubForm; diff --git a/frontend/app/components/Client/Integrations/GithubForm.tsx b/frontend/app/components/Client/Integrations/GithubForm.tsx new file mode 100644 index 000000000..d8d1f4d4c --- /dev/null +++ b/frontend/app/components/Client/Integrations/GithubForm.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import DocLink from 'Shared/DocLink/DocLink'; +import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModalCard'; +import IntegrationForm from './IntegrationForm'; +import { useTranslation } from 'react-i18next'; + +function GithubForm(props) { + const { t } = useTranslation(); + return ( +
+ +
+
+ {t( + 'Integrate GitHub with OpenReplay and create issues directly from the recording page.', + )} +
+
+ +
+
+ +
+ ); +} + +GithubForm.displayName = 'GithubForm'; + +export default GithubForm; diff --git a/frontend/app/components/Client/Integrations/IntegrationFilters.tsx b/frontend/app/components/Client/Integrations/IntegrationFilters.tsx index 898d38587..b1cf3ffcf 100644 --- a/frontend/app/components/Client/Integrations/IntegrationFilters.tsx +++ b/frontend/app/components/Client/Integrations/IntegrationFilters.tsx @@ -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: ( -
- {item.icon ? : null} +
+ {item.icon ? : null}
{item.title}
), - })) + })); const onChange = (val) => { - props.onChange(val) - } + props.onChange(val); + }; return ( -
+
{ @@ -30,19 +32,21 @@ function IntegrationForm(props: any) { const save = () => { const { name, customPath } = props; - onSave(customPath || name).then(() => { - fetchList(); - props.onClose(); - }).catch(async (error) => { - if (error.response) { - const errorResponse = await error.response.json(); - if (errorResponse.errors && Array.isArray(errorResponse.errors)) { - toast.error(errorResponse.errors.map((e: any) => e).join(', ')); - } else { - toast.error('Failed to save integration'); + onSave(customPath || name) + .then(() => { + fetchList(); + props.onClose(); + }) + .catch(async (error) => { + if (error.response) { + const errorResponse = await error.response.json(); + if (errorResponse.errors && Array.isArray(errorResponse.errors)) { + toast.error(errorResponse.errors.map((e: any) => e).join(', ')); + } else { + toast.error(t('Failed to save integration')); + } } - } - }); + }); }; const remove = () => { @@ -91,7 +95,7 @@ function IntegrationForm(props: any) { autoFocus={autoFocus} /> - )) + )), )} {integrated && ( )} diff --git a/frontend/app/components/Client/Integrations/IntegrationItem.tsx b/frontend/app/components/Client/Integrations/IntegrationItem.tsx index 4c4e4db5e..2c011c92b 100644 --- a/frontend/app/components/Client/Integrations/IntegrationItem.tsx +++ b/frontend/app/components/Client/Integrations/IntegrationItem.tsx @@ -1,8 +1,7 @@ import React from 'react'; import cn from 'classnames'; import { Icon } from 'UI'; -import stl from './integrationItem.module.css'; -import { Tooltip } from 'antd'; +import { useTranslation } from 'react-i18next'; interface Props { integration: any; @@ -12,32 +11,50 @@ interface Props { useIcon?: boolean; } -const IntegrationItem = (props: Props) => { +function IntegrationItem(props: Props) { + const { t } = useTranslation(); const { integration, integrated, hide = false, useIcon } = props; return hide ? null : (
props.onClick(e)} style={{ height: '136px' }} > -
+
- {useIcon ? : integration} + {useIcon ? ( + + ) : ( + integration + )}
-
-

{integration.title}

-

{integration.subtitle && integration.subtitle}

+
+

{integration.title}

+

+ {integration.subtitle && integration.subtitle} +

{integrated && ( -
- - Integrated -
+
+ + {t('Integrated')} +
)}
); -}; +} export default IntegrationItem; diff --git a/frontend/app/components/Client/Integrations/IntegrationModalCard.tsx b/frontend/app/components/Client/Integrations/IntegrationModalCard.tsx index 37ff588f0..1ac2fe6e8 100644 --- a/frontend/app/components/Client/Integrations/IntegrationModalCard.tsx +++ b/frontend/app/components/Client/Integrations/IntegrationModalCard.tsx @@ -11,16 +11,24 @@ interface Props { function IntegrationModalCard(props: Props) { const { title, icon, description, useIcon } = props; return ( -
-
- {useIcon ? : integration} +
+
+ {useIcon ? ( + + ) : ( + integration + )}
-

{title}

+

{title}

{description}
); } -export default IntegrationModalCard; \ No newline at end of file +export default IntegrationModalCard; diff --git a/frontend/app/components/Client/Integrations/Integrations.tsx b/frontend/app/components/Client/Integrations/Integrations.tsx index b00e5a0b7..5e9026a52 100644 --- a/frontend/app/components/Client/Integrations/Integrations.tsx +++ b/frontend/app/components/Client/Integrations/Integrations.tsx @@ -28,6 +28,8 @@ import PiniaDoc from './Tracker/PiniaDoc'; import ReduxDoc from './Tracker/ReduxDoc'; import VueDoc from './Tracker/VueDoc'; import ZustandDoc from './Tracker/ZustandDoc'; +import { TFunction } from 'i18next'; +import { useTranslation } from 'react-i18next'; interface Props { siteId: string; @@ -35,9 +37,10 @@ interface Props { } function Integrations(props: Props) { + const { t } = useTranslation(); 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; @@ -46,8 +49,9 @@ function Integrations(props: Props) { const [activeFilter, setActiveFilter] = useState('all'); useEffect(() => { - const list = integrationsStore.integrations.integratedServices - .map((item: any) => item.name); + const list = integrationsStore.integrations.integratedServices.map( + (item: any) => item.name, + ); setIntegratedList(list); }, [storeIntegratedList]); @@ -86,7 +90,7 @@ function Integrations(props: Props) { siteId, onClose: hideModal, }), - { right: true, width } + { right: true, width }, ); }; @@ -94,7 +98,7 @@ function Integrations(props: Props) { setActiveFilter(key); }; - const filteredIntegrations = integrations.filter((cat: any) => { + const filteredIntegrations = integrations(t).filter((cat: any) => { if (activeFilter === 'all') { return true; } @@ -102,7 +106,7 @@ function Integrations(props: Props) { return cat.key === activeFilter; }); - const filters = integrations.map((cat: any) => ({ + const filters = integrations(t).map((cat: any) => ({ key: cat.key, title: cat.title, label: cat.title, @@ -110,7 +114,7 @@ function Integrations(props: Props) { })); const allIntegrations = filteredIntegrations.flatMap( - (cat) => cat.integrations + (cat) => cat.integrations, ); const onChangeSelect = ({ value }: any) => { @@ -120,8 +124,8 @@ function Integrations(props: Props) { return ( <>
-
- {!hideHeader && Integrations
} />} +
+ {!hideHeader && {t('Integrations')}
} />}
-
+
{allIntegrations.map((integration, i) => ( - cat.integrations.includes(integration) + cat.integrations.includes(integration), )?.title === 'Plugins' ? 500 - : 350 + : 350, ) } hide={ @@ -167,31 +169,34 @@ function Integrations(props: Props) { } export default withPageTitle('Integrations - OpenReplay Preferences')( - observer(Integrations) + observer(Integrations), ); -const integrations = [ +const integrations = (t: TFunction) => [ { - title: 'Issue Reporting', + title: t('Issue Reporting'), key: 'issue-reporting', - description: + description: t( 'Seamlessly report issues or share issues with your team right from OpenReplay.', + ), isProject: false, icon: 'exclamation-triangle', integrations: [ { - title: 'Jira', - subtitle: + title: t('Jira'), + subtitle: t( 'Integrate Jira with OpenReplay to enable the creation of a new ticket directly from a session.', + ), slug: 'jira', category: 'Errors', icon: 'integrations/jira', component: , }, { - title: 'Github', - subtitle: + title: t('Github'), + subtitle: t( 'Integrate GitHub with OpenReplay to enable the direct creation of a new issue from a session.', + ), slug: 'github', category: 'Errors', icon: 'integrations/github', @@ -200,52 +205,58 @@ const integrations = [ ], }, { - title: 'Backend Logging', + title: t('Backend Logging'), key: 'backend-logging', isProject: true, icon: 'terminal', - description: + description: t( 'Sync your backend errors with sessions replays and see what happened front-to-back.', + ), docs: () => ( - Sync your backend errors with sessions replays and see what happened - front-to-back. + {t( + 'Sync your backend errors with sessions replays and see what happened front-to-back.', + )} ), integrations: [ { - title: 'Sentry', - subtitle: + title: t('Sentry'), + subtitle: t( 'Integrate Sentry with session replays to seamlessly observe backend errors.', + ), slug: 'sentry', icon: 'integrations/sentry', component: , }, { - title: 'Elasticsearch', - subtitle: + title: t('Elasticsearch'), + subtitle: t( 'Integrate Elasticsearch with session replays to seamlessly observe backend errors.', + ), slug: 'elasticsearch', icon: 'integrations/elasticsearch', component: , }, { - title: 'Datadog', - subtitle: + title: t('Datadog'), + subtitle: t( 'Incorporate DataDog to visualize backend errors alongside session replay, for easy troubleshooting.', + ), slug: 'datadog', icon: 'integrations/datadog', component: , }, { - title: 'Dynatrace', - subtitle: + title: t('Dynatrace'), + subtitle: t( 'Integrate Dynatrace with session replays to link backend logs with user sessions for faster issue resolution.', + ), slug: 'dynatrace', icon: 'integrations/dynatrace', useIcon: true, @@ -254,17 +265,19 @@ const integrations = [ ], }, { - title: 'Collaboration', + title: t('Collaboration'), key: 'collaboration', isProject: false, icon: 'file-code', - description: + description: t( 'Share your sessions with your team and collaborate on issues.', + ), integrations: [ { - title: 'Slack', - subtitle: + title: t('Slack'), + subtitle: t( 'Integrate Slack to empower every user in your org with the ability to send sessions to any Slack channel.', + ), slug: 'slack', category: 'Errors', icon: 'integrations/slack', @@ -272,9 +285,10 @@ const integrations = [ shared: true, }, { - title: 'MS Teams', - subtitle: + title: t('MS Teams'), + subtitle: t( 'Integrate MS Teams to empower every user in your org with the ability to send sessions to any MS Teams channel.', + ), slug: 'msteams', category: 'Errors', icon: 'integrations/teams', @@ -292,84 +306,95 @@ const integrations = [ // integrations: [] // }, { - title: 'Plugins', + title: t('Plugins'), key: 'plugins', isProject: true, icon: 'chat-left-text', docs: () => ( - Plugins capture your application’s store, monitor queries, track - performance issues and even assist your end user through live sessions. + {t( + 'Plugins capture your application’s store, monitor queries, track performance issues and even assist your end user through live sessions.', + )} ), - description: + description: t( "Reproduce issues as if they happened in your own browser. Plugins help capture your application's store, HTTP requeets, GraphQL queries, and more.", + ), integrations: [ { - title: 'Redux', - subtitle: + title: t('Redux'), + subtitle: t( 'Capture Redux actions/state and inspect them later on while replaying session recordings.', + ), icon: 'integrations/redux', component: , }, { - title: 'VueX', - subtitle: + title: t('VueX'), + subtitle: t( 'Capture VueX mutations/state and inspect them later on while replaying session recordings.', + ), icon: 'integrations/vuejs', component: , }, { - title: 'Pinia', - subtitle: + title: t('Pinia'), + subtitle: t( 'Capture Pinia mutations/state and inspect them later on while replaying session recordings.', + ), icon: 'integrations/pinia', component: , }, { - title: 'GraphQL', - subtitle: + title: t('GraphQL'), + subtitle: t( 'Capture GraphQL requests and inspect them later on while replaying session recordings. This plugin is compatible with Apollo and Relay implementations.', + ), icon: 'integrations/graphql', component: , }, { - title: 'NgRx', - subtitle: + title: t('NgRx'), + subtitle: t( 'Capture NgRx actions/state and inspect them later on while replaying session recordings.\n', + ), icon: 'integrations/ngrx', component: , }, { - title: 'MobX', - subtitle: + title: t('MobX'), + subtitle: t( 'Capture MobX mutations and inspect them later on while replaying session recordings.', + ), icon: 'integrations/mobx', component: , }, { - title: 'Profiler', - subtitle: + title: t('Profiler'), + subtitle: t( 'Plugin allows you to measure your JS functions performance and capture both arguments and result for each call.', + ), icon: 'integrations/openreplay', component: , }, { - title: 'Assist', - subtitle: + title: t('Assist'), + subtitle: t( 'OpenReplay Assist allows you to support your users by seeing their live screen and instantly hopping on call (WebRTC) with them without requiring any 3rd-party screen sharing software.\n', + ), icon: 'integrations/openreplay', component: , }, { - title: 'Zustand', - subtitle: + title: t('Zustand'), + subtitle: t( 'Capture Zustand mutations/state and inspect them later on while replaying session recordings.', + ), icon: 'integrations/zustand', // header: '🐻', component: , diff --git a/frontend/app/components/Client/Integrations/JiraForm/JiraForm.js b/frontend/app/components/Client/Integrations/JiraForm/JiraForm.js deleted file mode 100644 index dc8f3e49f..000000000 --- a/frontend/app/components/Client/Integrations/JiraForm/JiraForm.js +++ /dev/null @@ -1,57 +0,0 @@ -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'; - -const JiraForm = (props) => { - const { hideModal } = useModal(); - return ( -
- - - -
-
How it works?
-
    -
  1. Create a new API token
  2. -
  3. Enter the token below
  4. -
-
- -
-
- - - -
- ); -}; - -JiraForm.displayName = 'JiraForm'; - -export default JiraForm; diff --git a/frontend/app/components/Client/Integrations/JiraForm/JiraForm.tsx b/frontend/app/components/Client/Integrations/JiraForm/JiraForm.tsx new file mode 100644 index 000000000..5e10ff540 --- /dev/null +++ b/frontend/app/components/Client/Integrations/JiraForm/JiraForm.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import DocLink from 'Shared/DocLink/DocLink'; +import { useModal } from 'App/components/Modal'; +import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModalCard'; +import IntegrationForm from '../IntegrationForm'; +import { useTranslation } from 'react-i18next'; + +function JiraForm(props) { + const { t } = useTranslation(); + const { hideModal } = useModal(); + return ( +
+ + +
+
{t('How it works?')}
+
    +
  1. {t('Create a new API token')}
  2. +
  3. {t('Enter the token below')}
  4. +
+
+ +
+
+ + +
+ ); +} + +JiraForm.displayName = 'JiraForm'; + +export default JiraForm; diff --git a/frontend/app/components/Client/Integrations/JiraForm/index.js b/frontend/app/components/Client/Integrations/JiraForm/index.js index d914b3234..8f007bd52 100644 --- a/frontend/app/components/Client/Integrations/JiraForm/index.js +++ b/frontend/app/components/Client/Integrations/JiraForm/index.js @@ -1 +1 @@ -export { default } from './JiraForm'; \ No newline at end of file +export { default } from './JiraForm'; diff --git a/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js b/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.tsx similarity index 58% rename from frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js rename to frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.tsx index 171224871..eb6013a7c 100644 --- a/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js +++ b/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.tsx @@ -1,16 +1,20 @@ -import { useStore } from "App/mstore"; +import { useStore } from 'App/mstore'; import React from 'react'; import { observer } from 'mobx-react-lite'; import { CodeBlock } from 'UI'; import DocLink from 'Shared/DocLink/DocLink'; import ToggleContent from 'Shared/ToggleContent'; +import { useTranslation } from 'react-i18next'; -const ProfilerDoc = () => { +function ProfilerDoc() { + const { t } = useTranslation(); 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'; @@ -48,43 +52,45 @@ const fn = profiler('call_name')(() => { className="bg-white h-screen overflow-y-auto" style={{ width: '500px' }} > -

Profiler

+

{t('Profiler')}

- The profiler plugin allows you to measure your JS functions' - performance and capture both arguments and result for each function - call. + {t( + 'The profiler plugin allows you to measure your JS functions performance and capture both arguments and result for each function call', + )} + .
-
Installation
+
{t('Installation')}
-
Usage
+
{t('Usage')}

- Initialize the tracker and load the plugin into it. Then decorate any - function inside your code with the generated function. + {t( + 'Initialize the tracker and load the plugin into it. Then decorate any function inside your code with the generated function.', + )}

-
Usage
+
{t('Usage')}
} - second={} + label={t('Server-Side-Rendered (SSR)?')} + first={} + second={} />
); -}; +} ProfilerDoc.displayName = 'ProfilerDoc'; diff --git a/frontend/app/components/Client/Integrations/ProfilerDoc/index.js b/frontend/app/components/Client/Integrations/ProfilerDoc/index.js index 5a3da5d09..3a5655ce7 100644 --- a/frontend/app/components/Client/Integrations/ProfilerDoc/index.js +++ b/frontend/app/components/Client/Integrations/ProfilerDoc/index.js @@ -1 +1 @@ -export { default } from './ProfilerDoc' \ No newline at end of file +export { default } from './ProfilerDoc'; diff --git a/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js b/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.tsx similarity index 62% rename from frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js rename to frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.tsx index 79feddbc3..74203a900 100644 --- a/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js +++ b/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.tsx @@ -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 { Button } from 'antd'; +import { observer } from 'mobx-react-lite'; +import { useStore } from 'App/mstore'; +import { useTranslation } from 'react-i18next'; function SlackAddForm(props) { + const { t } = useTranslation(); 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()) { @@ -32,9 +31,11 @@ function SlackAddForm(props) { const remove = async (id) => { if ( await confirm({ - header: 'Confirm', - confirmButton: 'Yes, delete', - confirmation: `Are you sure you want to permanently delete this channel?`, + header: t('Confirm'), + confirmButton: t('Yes, delete'), + confirmation: t( + 'Are you sure you want to permanently delete this channel?', + ), }) ) { await onRemove(id); @@ -43,27 +44,27 @@ function SlackAddForm(props) { }; const write = ({ target: { name, value } }) => edit({ [name]: value }); - + return (
- + - + @@ -76,14 +77,17 @@ function SlackAddForm(props) { type="primary" className="float-left mr-2" > - {instance.exists() ? 'Update' : 'Add'} + {instance.exists() ? t('Update') : t('Add')} - +
-
diff --git a/frontend/app/components/Client/Integrations/SlackAddForm/index.js b/frontend/app/components/Client/Integrations/SlackAddForm/index.js index beb10bb6d..33b9a4b2f 100644 --- a/frontend/app/components/Client/Integrations/SlackAddForm/index.js +++ b/frontend/app/components/Client/Integrations/SlackAddForm/index.js @@ -1 +1 @@ -export { default } from './SlackAddForm' \ No newline at end of file +export { default } from './SlackAddForm'; diff --git a/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js b/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js deleted file mode 100644 index db53d3100..000000000 --- a/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js +++ /dev/null @@ -1,48 +0,0 @@ -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' - -function SlackChannelList(props) { - const { integrationsStore } = useStore(); - const list = integrationsStore.slack.list; - const edit = integrationsStore.slack.edit; - - const onEdit = (instance) => { - edit(instance.toData()); - props.onEdit(); - }; - - return ( -
- -
- Integrate Slack with OpenReplay and share insights with the rest of the team, directly from the recording page. -
- -
- } - size="small" - show={list.length === 0} - > - {list.map((c) => ( -
onEdit(c)} - > -
-
{c.name}
-
{c.endpoint}
-
-
- ))} - -
- ); -} - -export default observer(SlackChannelList); diff --git a/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.tsx b/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.tsx new file mode 100644 index 000000000..cb138096d --- /dev/null +++ b/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.tsx @@ -0,0 +1,56 @@ +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 { useTranslation } from 'react-i18next'; + +function SlackChannelList(props) { + const { t } = useTranslation(); + const { integrationsStore } = useStore(); + const { list } = integrationsStore.slack; + const { edit } = integrationsStore.slack; + + const onEdit = (instance) => { + edit(instance.toData()); + props.onEdit(); + }; + + return ( +
+ +
+ {t('Integrate Slack with OpenReplay and share insights with the rest of the team, directly from the recording page.')} +
+ +
+ } + size="small" + show={list.length === 0} + > + {list.map((c) => ( +
onEdit(c)} + > +
+
{c.name}
+
+ {c.endpoint} +
+
+
+ ))} + +
+ ); +} + +export default observer(SlackChannelList); diff --git a/frontend/app/components/Client/Integrations/SlackChannelList/index.js b/frontend/app/components/Client/Integrations/SlackChannelList/index.js index d9709c104..9056062a5 100644 --- a/frontend/app/components/Client/Integrations/SlackChannelList/index.js +++ b/frontend/app/components/Client/Integrations/SlackChannelList/index.js @@ -1 +1 @@ -export { default } from './SlackChannelList' \ No newline at end of file +export { default } from './SlackChannelList'; diff --git a/frontend/app/components/Client/Integrations/SlackForm.tsx b/frontend/app/components/Client/Integrations/SlackForm.tsx index 4c7e97209..ecc3487e6 100644 --- a/frontend/app/components/Client/Integrations/SlackForm.tsx +++ b/frontend/app/components/Client/Integrations/SlackForm.tsx @@ -1,48 +1,58 @@ 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'; +import { useTranslation } from 'react-i18next'; -const SlackForm = () => { - const { integrationsStore } = useStore(); - const init = integrationsStore.slack.init; - const fetchList = integrationsStore.slack.fetchIntegrations; - const [active, setActive] = React.useState(false); +function SlackForm() { + const { t } = useTranslation(); + 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 ( -
- {active && ( -
- setActive(false)} /> -
- )} -
-
-

Slack

-
- -
+ return ( +
+ {active && ( +
+ setActive(false)} />
- ); -}; + )} +
+
+

{t('Slack')}

+
+ +
+
+ ); +} SlackForm.displayName = 'SlackForm'; -export default observer(SlackForm); \ No newline at end of file +export default observer(SlackForm); diff --git a/frontend/app/components/Client/Integrations/Teams/TeamsAddForm.tsx b/frontend/app/components/Client/Integrations/Teams/TeamsAddForm.tsx index 9341b08ef..8061740a8 100644 --- a/frontend/app/components/Client/Integrations/Teams/TeamsAddForm.tsx +++ b/frontend/app/components/Client/Integrations/Teams/TeamsAddForm.tsx @@ -3,26 +3,26 @@ import React from 'react'; import { useStore } from 'App/mstore'; import { confirm, Form, Input, Message } from 'UI'; -import { Button } from 'antd' +import { Button } from 'antd'; +import { useTranslation } from 'react-i18next'; interface Props { onClose: () => void; } function TeamsAddForm({ onClose }: Props) { + const { t } = useTranslation(); 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()) { @@ -39,9 +39,11 @@ function TeamsAddForm({ onClose }: Props) { const remove = async (id: string) => { if ( await confirm({ - header: 'Confirm', - confirmButton: 'Yes, delete', - confirmation: `Are you sure you want to permanently delete this channel?` + header: t('Confirm'), + confirmButton: t('Yes, delete'), + confirmation: t( + 'Are you sure you want to permanently delete this channel?', + ), }) ) { void onRemove(id).then(onClose); @@ -49,8 +51,8 @@ function TeamsAddForm({ onClose }: Props) { }; const write = ({ - target: { name, value } - }: { + target: { name, value }, + }: { target: { name: string; value: string }; }) => edit({ [name]: value }); @@ -58,22 +60,22 @@ function TeamsAddForm({ onClose }: Props) {
- + - + @@ -86,17 +88,17 @@ function TeamsAddForm({ onClose }: Props) { type="primary" className="float-left mr-2" > - {instance?.exists() ? 'Update' : 'Add'} + {instance?.exists() ? t('Update') : t('Add')} - +
diff --git a/frontend/app/components/Client/Integrations/Teams/TeamsChannelList.tsx b/frontend/app/components/Client/Integrations/Teams/TeamsChannelList.tsx index 131a404c8..ce7dee91d 100644 --- a/frontend/app/components/Client/Integrations/Teams/TeamsChannelList.tsx +++ b/frontend/app/components/Client/Integrations/Teams/TeamsChannelList.tsx @@ -5,11 +5,13 @@ import { useStore } from 'App/mstore'; import { NoContent } from 'UI'; import DocLink from 'Shared/DocLink/DocLink'; +import { useTranslation } from 'react-i18next'; function TeamsChannelList(props: { onEdit: () => void }) { + const { t } = useTranslation(); 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) => { edit(instance); @@ -22,12 +24,11 @@ function TeamsChannelList(props: { onEdit: () => void }) { title={
- Integrate MS Teams with OpenReplay and share insights with the - rest of the team, directly from the recording page. + {t('Integrate MS Teams with OpenReplay and share insights with the rest of the team, directly from the recording page.')}
diff --git a/frontend/app/components/Client/Integrations/Teams/index.tsx b/frontend/app/components/Client/Integrations/Teams/index.tsx index 21ffec387..3d665c8e7 100644 --- a/frontend/app/components/Client/Integrations/Teams/index.tsx +++ b/frontend/app/components/Client/Integrations/Teams/index.tsx @@ -2,47 +2,57 @@ 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'; +import { useTranslation } from 'react-i18next'; -const MSTeams = () => { - const { integrationsStore } = useStore(); - const fetchList = integrationsStore.msteams.fetchIntegrations; - const init = integrationsStore.msteams.init; - const [active, setActive] = React.useState(false); +function MSTeams() { + const { t } = useTranslation(); + 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 ( -
- {active && ( -
- setActive(false)} /> -
- )} -
-
-

Microsoft Teams

-
- -
+ return ( +
+ {active && ( +
+ setActive(false)} />
- ); -}; + )} +
+
+

{t('Microsoft Teams')}

+
+ +
+
+ ); +} MSTeams.displayName = 'MSTeams'; diff --git a/frontend/app/components/Client/Integrations/Tracker/AssistDoc/AssistDoc.js b/frontend/app/components/Client/Integrations/Tracker/AssistDoc/AssistDoc.js deleted file mode 100644 index 1165af6ec..000000000 --- a/frontend/app/components/Client/Integrations/Tracker/AssistDoc/AssistDoc.js +++ /dev/null @@ -1,60 +0,0 @@ -import { useStore } from "App/mstore"; -import React from 'react'; -import DocLink from 'Shared/DocLink/DocLink'; -import AssistScript from './AssistScript'; -import AssistNpm from './AssistNpm'; -import { Tabs, CodeBlock } from 'UI'; -import { useState } from 'react'; -import { observer } from 'mobx-react-lite' - -const NPM = 'NPM'; -const SCRIPT = 'SCRIPT'; -const TABS = [ - { key: SCRIPT, text: SCRIPT }, - { key: NPM, text: NPM }, -]; - -const AssistDoc = () => { - const { integrationsStore, projectsStore } = useStore(); - const sites = projectsStore.list; - const siteId = integrationsStore.integrations.siteId - const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey - const [activeTab, setActiveTab] = useState(SCRIPT); - - const renderActiveTab = () => { - switch (activeTab) { - case SCRIPT: - return ; - case NPM: - return ; - } - return null; - }; - - return ( -
-

Assist

-
-
- OpenReplay Assist allows you to support your users by seeing their live screen and instantly hopping on call (WebRTC) with them - without requiring any 3rd-party screen sharing software. -
- -
Installation
- -
- -
Usage
- setActiveTab(tab)} /> - -
{renderActiveTab()}
- - -
-
- ); -}; - -AssistDoc.displayName = 'AssistDoc'; - -export default observer(AssistDoc); diff --git a/frontend/app/components/Client/Integrations/Tracker/AssistDoc/AssistDoc.tsx b/frontend/app/components/Client/Integrations/Tracker/AssistDoc/AssistDoc.tsx new file mode 100644 index 000000000..c2b1f6d2b --- /dev/null +++ b/frontend/app/components/Client/Integrations/Tracker/AssistDoc/AssistDoc.tsx @@ -0,0 +1,75 @@ +import { useStore } from 'App/mstore'; +import React, { useState } from 'react'; +import DocLink from 'Shared/DocLink/DocLink'; +import { Tabs, CodeBlock } from 'UI'; +import { observer } from 'mobx-react-lite'; +import AssistScript from './AssistScript'; +import AssistNpm from './AssistNpm'; +import { useTranslation } from 'react-i18next'; + +const NPM = 'NPM'; +const SCRIPT = 'SCRIPT'; +const TABS = [ + { key: SCRIPT, text: SCRIPT }, + { key: NPM, text: NPM }, +]; + +function AssistDoc() { + const { t } = useTranslation(); + const { integrationsStore, projectsStore } = useStore(); + const sites = projectsStore.list; + const { siteId } = integrationsStore.integrations; + const projectKey = siteId + ? sites.find((site) => site.id === siteId)?.projectKey + : sites[0]?.projectKey; + const [activeTab, setActiveTab] = useState(SCRIPT); + + const renderActiveTab = () => { + switch (activeTab) { + case SCRIPT: + return ; + case NPM: + return ; + } + return null; + }; + + return ( +
+

{t('Assist')}

+
+
+ {t( + 'OpenReplay Assist allows you to support your users by seeing their live screen and instantly hopping on call (WebRTC) with them without requiring any 3rd-party screen sharing software.', + )} +
+ +
{t('Installation')}
+ +
+ +
{t('Usage')}
+ setActiveTab(tab)} + /> + +
{renderActiveTab()}
+ + +
+
+ ); +} + +AssistDoc.displayName = 'AssistDoc'; + +export default observer(AssistDoc); diff --git a/frontend/app/components/Client/Integrations/Tracker/AssistDoc/AssistNpm.tsx b/frontend/app/components/Client/Integrations/Tracker/AssistDoc/AssistNpm.tsx index 00fe1cb26..30893ba95 100644 --- a/frontend/app/components/Client/Integrations/Tracker/AssistDoc/AssistNpm.tsx +++ b/frontend/app/components/Client/Integrations/Tracker/AssistDoc/AssistNpm.tsx @@ -3,8 +3,10 @@ import React from 'react'; import { CodeBlock } from 'UI'; import ToggleContent from 'Shared/ToggleContent'; +import { useTranslation } from 'react-i18next'; function AssistNpm(props) { + const { t } = useTranslation(); const usage = `import OpenReplay from '@openreplay/tracker'; import trackerAssist from '@openreplay/tracker-assist'; const tracker = new OpenReplay({ @@ -56,21 +58,23 @@ type ButtonOptions = HTMLButtonElement | string | { innerHTML?: string, // to pass an svg string or text style?: StyleObject, // style object (i.e {color: 'red', borderRadius: '10px'}) } -` +`; return (

- Initialize the tracker then load the @openreplay/tracker-assist plugin. + {t( + 'Initialize the tracker then load the @openreplay/tracker-assist plugin.', + )}

-
Usage
+
{t('Usage')}
} second={} /> -
Options
+
{t('Options')}
); diff --git a/frontend/app/components/Client/Integrations/Tracker/AssistDoc/AssistScript.tsx b/frontend/app/components/Client/Integrations/Tracker/AssistDoc/AssistScript.tsx index 23ce8ff02..4db279dff 100644 --- a/frontend/app/components/Client/Integrations/Tracker/AssistDoc/AssistScript.tsx +++ b/frontend/app/components/Client/Integrations/Tracker/AssistDoc/AssistScript.tsx @@ -1,7 +1,9 @@ import React from 'react'; -import { CodeBlock } from "UI"; +import { useTranslation } from 'react-i18next'; +import { CodeBlock } from 'UI'; function AssistScript(props) { + const { t } = useTranslation(); const scriptCode = ` ` +`; return (
-

If your OpenReplay tracker is set up using the JS snippet, then simply replace the .../openreplay.js occurrence with .../openreplay-assist.js. Below is an example of how the script should like after the change:

+

+ {t( + 'If your OpenReplay tracker is set up using the JS snippet, then simply replace the .../openreplay.js occurrence with .../openreplay-assist.js. Below is an example of how the script should like after the change:', + )} +

- +
); } -export default AssistScript; \ No newline at end of file +export default AssistScript; diff --git a/frontend/app/components/Client/Integrations/Tracker/AssistDoc/index.js b/frontend/app/components/Client/Integrations/Tracker/AssistDoc/index.js index 6086d5389..8d1c447e9 100644 --- a/frontend/app/components/Client/Integrations/Tracker/AssistDoc/index.js +++ b/frontend/app/components/Client/Integrations/Tracker/AssistDoc/index.js @@ -1 +1 @@ -export { default } from './AssistDoc' \ No newline at end of file +export { default } from './AssistDoc'; diff --git a/frontend/app/components/Client/Integrations/Tracker/GraphQLDoc/GraphQLDoc.js b/frontend/app/components/Client/Integrations/Tracker/GraphQLDoc/GraphQLDoc.js deleted file mode 100644 index 889ecfd1f..000000000 --- a/frontend/app/components/Client/Integrations/Tracker/GraphQLDoc/GraphQLDoc.js +++ /dev/null @@ -1,75 +0,0 @@ -import { useStore } from "App/mstore"; -import React from 'react'; -import { CodeBlock } from "UI"; -import DocLink from 'Shared/DocLink/DocLink'; -import ToggleContent from 'Shared/ToggleContent'; -import { observer } from 'mobx-react-lite' - -const GraphQLDoc = () => { - const { integrationsStore, projectsStore } = useStore(); - const sites = projectsStore.list; - const siteId = integrationsStore.integrations.siteId - const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey - const usage = `import OpenReplay from '@openreplay/tracker'; -import trackerGraphQL from '@openreplay/tracker-graphql'; -//... -const tracker = new OpenReplay({ - projectKey: '${projectKey}' -}); -tracker.start() -//... -export const recordGraphQL = tracker.use(trackerGraphQL());` - const usageCjs = `import OpenReplay from '@openreplay/tracker/cjs'; -import trackerGraphQL from '@openreplay/tracker-graphql/cjs'; -//... -const tracker = new OpenReplay({ - projectKey: '${projectKey}' -}); -//... -function SomeFunctionalComponent() { - useEffect(() => { // or componentDidMount in case of Class approach - tracker.start() - }, []) -} -//... -export const recordGraphQL = tracker.use(trackerGraphQL());` - return ( -
-

GraphQL

-
-

- This plugin allows you to capture GraphQL requests and inspect them later on while replaying session recordings. This is very - useful for understanding and fixing issues. -

-

GraphQL plugin is compatible with Apollo and Relay implementations.

- -
Installation
- - -
Usage
-

- The plugin call will return the function, which receives four variables operationKind, operationName, variables and result. It - returns result without changes. -

- -
- - - } - second={ - - } - /> - - -
-
- ); -}; - -GraphQLDoc.displayName = 'GraphQLDoc'; - -export default observer(GraphQLDoc); diff --git a/frontend/app/components/Client/Integrations/Tracker/GraphQLDoc/GraphQLDoc.tsx b/frontend/app/components/Client/Integrations/Tracker/GraphQLDoc/GraphQLDoc.tsx new file mode 100644 index 000000000..e47ab0c4a --- /dev/null +++ b/frontend/app/components/Client/Integrations/Tracker/GraphQLDoc/GraphQLDoc.tsx @@ -0,0 +1,91 @@ +import { useStore } from 'App/mstore'; +import React from 'react'; +import { CodeBlock } from 'UI'; +import DocLink from 'Shared/DocLink/DocLink'; +import ToggleContent from 'Shared/ToggleContent'; +import { observer } from 'mobx-react-lite'; +import { useTranslation } from 'react-i18next'; + +function GraphQLDoc() { + const { t } = useTranslation(); + const { integrationsStore, projectsStore } = useStore(); + const sites = projectsStore.list; + 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 trackerGraphQL from '@openreplay/tracker-graphql'; +//... +const tracker = new OpenReplay({ + projectKey: '${projectKey}' +}); +tracker.start() +//... +export const recordGraphQL = tracker.use(trackerGraphQL());`; + const usageCjs = `import OpenReplay from '@openreplay/tracker/cjs'; +import trackerGraphQL from '@openreplay/tracker-graphql/cjs'; +//... +const tracker = new OpenReplay({ + projectKey: '${projectKey}' +}); +//... +function SomeFunctionalComponent() { + useEffect(() => { // or componentDidMount in case of Class approach + tracker.start() + }, []) +} +//... +export const recordGraphQL = tracker.use(trackerGraphQL());`; + return ( +
+

{t('GraphQL')}

+
+

+ {t( + 'This plugin allows you to capture GraphQL requests and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.', + )} +

+

+ {t( + 'GraphQL plugin is compatible with Apollo and Relay implementations.', + )} +

+ +
{t('Installation')}
+ + +
{t('Usage')}
+

+ {t( + 'The plugin call will return the function, which receives four variables operationKind, operationName, variables and result. It returns result without changes.', + )} +

+ +
+ + } + second={} + /> + + +
+
+ ); +} + +GraphQLDoc.displayName = 'GraphQLDoc'; + +export default observer(GraphQLDoc); diff --git a/frontend/app/components/Client/Integrations/Tracker/GraphQLDoc/index.js b/frontend/app/components/Client/Integrations/Tracker/GraphQLDoc/index.js index 6fe38bba7..f7317d7d4 100644 --- a/frontend/app/components/Client/Integrations/Tracker/GraphQLDoc/index.js +++ b/frontend/app/components/Client/Integrations/Tracker/GraphQLDoc/index.js @@ -1 +1 @@ -export { default } from './GraphQLDoc' \ No newline at end of file +export { default } from './GraphQLDoc'; diff --git a/frontend/app/components/Client/Integrations/Tracker/MobxDoc/MobxDoc.js b/frontend/app/components/Client/Integrations/Tracker/MobxDoc/MobxDoc.js deleted file mode 100644 index 2541aae2b..000000000 --- a/frontend/app/components/Client/Integrations/Tracker/MobxDoc/MobxDoc.js +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import ToggleContent from 'Shared/ToggleContent'; -import DocLink from 'Shared/DocLink/DocLink'; -import { CodeBlock } from "UI"; -import { useStore } from 'App/mstore'; -import { observer } from 'mobx-react-lite'; - -const MobxDoc = () => { - const { integrationsStore, projectsStore } = useStore(); - const sites = projectsStore.list; - const siteId = integrationsStore.integrations.siteId - const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey - - const mobxUsage = `import OpenReplay from '@openreplay/tracker'; -import trackerMobX from '@openreplay/tracker-mobx'; -//... -const tracker = new OpenReplay({ - projectKey: '${projectKey}' -}); -tracker.use(trackerMobX()); // check list of available options below -tracker.start(); -` - - const mobxUsageCjs = `import OpenReplay from '@openreplay/tracker/cjs'; -import trackerMobX from '@openreplay/tracker-mobx/cjs'; -//... -const tracker = new OpenReplay({ - projectKey: '${projectKey}' -}); -tracker.use(trackerMobX()); // check list of available options below -//... -function SomeFunctionalComponent() { - useEffect(() => { // or componentDidMount in case of Class approach - tracker.start() - }, []) -}` - - return ( -
-

MobX

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

- Initialize the @openreplay/tracker package as usual and load the plugin into it. Then put the generated middleware into your Redux - chain. -

-
- -
Usage
- } - second={} - /> - - -
-
- ); -}; - -MobxDoc.displayName = 'MobxDoc'; - -export default observer(MobxDoc) diff --git a/frontend/app/components/Client/Integrations/Tracker/MobxDoc/MobxDoc.tsx b/frontend/app/components/Client/Integrations/Tracker/MobxDoc/MobxDoc.tsx new file mode 100644 index 000000000..e31a25b86 --- /dev/null +++ b/frontend/app/components/Client/Integrations/Tracker/MobxDoc/MobxDoc.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import ToggleContent from 'Shared/ToggleContent'; +import DocLink from 'Shared/DocLink/DocLink'; +import { CodeBlock } from 'UI'; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; +import { useTranslation } from 'react-i18next'; + +function MobxDoc() { + const { t } = useTranslation(); + const { integrationsStore, projectsStore } = useStore(); + const sites = projectsStore.list; + const { siteId } = integrationsStore.integrations; + const projectKey = siteId + ? sites.find((site) => site.id === siteId)?.projectKey + : sites[0]?.projectKey; + + const mobxUsage = `import OpenReplay from '@openreplay/tracker'; +import trackerMobX from '@openreplay/tracker-mobx'; +//... +const tracker = new OpenReplay({ + projectKey: '${projectKey}' +}); +tracker.use(trackerMobX()); // check list of available options below +tracker.start(); +`; + + const mobxUsageCjs = `import OpenReplay from '@openreplay/tracker/cjs'; +import trackerMobX from '@openreplay/tracker-mobx/cjs'; +//... +const tracker = new OpenReplay({ + projectKey: '${projectKey}' +}); +tracker.use(trackerMobX()); // check list of available options below +//... +function SomeFunctionalComponent() { + useEffect(() => { // or componentDidMount in case of Class approach + tracker.start() + }, []) +}`; + + return ( +
+

{t('MobX')}

+
+
+ {t( + 'This plugin allows you to capture MobX events and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.', + )} +
+ +
{t('Installation')}
+ + +
{t('Usage')}
+

+ {t( + 'Initialize the @openreplay/tracker package as usual and load the plugin into it. Then put the generated middleware into your Redux chain.', + )} +

+
+ +
{t('Usage')}
+ } + second={} + /> + + +
+
+ ); +} + +MobxDoc.displayName = 'MobxDoc'; + +export default observer(MobxDoc); diff --git a/frontend/app/components/Client/Integrations/Tracker/MobxDoc/index.js b/frontend/app/components/Client/Integrations/Tracker/MobxDoc/index.js index 51b6dedae..ed631b7b7 100644 --- a/frontend/app/components/Client/Integrations/Tracker/MobxDoc/index.js +++ b/frontend/app/components/Client/Integrations/Tracker/MobxDoc/index.js @@ -1 +1 @@ -export { default } from './MobxDoc' \ No newline at end of file +export { default } from './MobxDoc'; diff --git a/frontend/app/components/Client/Integrations/Tracker/NgRxDoc/NgRxDoc.js b/frontend/app/components/Client/Integrations/Tracker/NgRxDoc/NgRxDoc.js deleted file mode 100644 index f960918b6..000000000 --- a/frontend/app/components/Client/Integrations/Tracker/NgRxDoc/NgRxDoc.js +++ /dev/null @@ -1,85 +0,0 @@ -import { useStore } from "App/mstore"; -import React from 'react'; -import { CodeBlock } from "UI"; -import ToggleContent from 'Shared/ToggleContent'; -import DocLink from 'Shared/DocLink/DocLink'; -import { observer } from 'mobx-react-lite' - -const NgRxDoc = () => { - const { integrationsStore, projectsStore } = useStore(); - const sites = projectsStore.list; - const siteId = integrationsStore.integrations.siteId - const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey - const usage = `import { StoreModule } from '@ngrx/store'; -import { reducers } from './reducers'; -import OpenReplay from '@openreplay/tracker'; -import trackerNgRx from '@openreplay/tracker-ngrx'; -//... -const tracker = new OpenReplay({ - projectKey: '${projectKey}' -}); -tracker.start() -//... -const metaReducers = [tracker.use(trackerNgRx())]; // check list of available options below -//... -@NgModule({ - imports: [StoreModule.forRoot(reducers, { metaReducers })] -}) -export class AppModule {}` - const usageCjs = `import { StoreModule } from '@ngrx/store'; -import { reducers } from './reducers'; -import OpenReplay from '@openreplay/tracker/cjs'; -import trackerNgRx from '@openreplay/tracker-ngrx/cjs'; -//... -const tracker = new OpenReplay({ - projectKey: '${projectKey}' -}); -//... -function SomeFunctionalComponent() { - useEffect(() => { // or componentDidMount in case of Class approach - tracker.start() - }, []) -//... -const metaReducers = [tracker.use(trackerNgRx())]; // check list of available options below -//... - @NgModule({ - imports: [StoreModule.forRoot(reducers, { metaReducers })] - }) - export class AppModule {} -}` - return ( -
-

NgRx

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

Add the generated meta-reducer into your imports. See NgRx documentation for more details.

-
- -
Usage
- - } - second={ - - } - /> - - -
-
- ); -}; - -NgRxDoc.displayName = 'NgRxDoc'; - -export default observer(NgRxDoc); diff --git a/frontend/app/components/Client/Integrations/Tracker/NgRxDoc/NgRxDoc.tsx b/frontend/app/components/Client/Integrations/Tracker/NgRxDoc/NgRxDoc.tsx new file mode 100644 index 000000000..d1401899f --- /dev/null +++ b/frontend/app/components/Client/Integrations/Tracker/NgRxDoc/NgRxDoc.tsx @@ -0,0 +1,100 @@ +import { useStore } from 'App/mstore'; +import React from 'react'; +import { CodeBlock } from 'UI'; +import ToggleContent from 'Shared/ToggleContent'; +import DocLink from 'Shared/DocLink/DocLink'; +import { observer } from 'mobx-react-lite'; +import { useTranslation } from 'react-i18next'; + +function NgRxDoc() { + const { t } = useTranslation(); + const { integrationsStore, projectsStore } = useStore(); + const sites = projectsStore.list; + const { siteId } = integrationsStore.integrations; + const projectKey = siteId + ? sites.find((site) => site.id === siteId)?.projectKey + : sites[0]?.projectKey; + const usage = `import { StoreModule } from '@ngrx/store'; +import { reducers } from './reducers'; +import OpenReplay from '@openreplay/tracker'; +import trackerNgRx from '@openreplay/tracker-ngrx'; +//... +const tracker = new OpenReplay({ + projectKey: '${projectKey}' +}); +tracker.start() +//... +const metaReducers = [tracker.use(trackerNgRx())]; // check list of available options below +//... +@NgModule({ + imports: [StoreModule.forRoot(reducers, { metaReducers })] +}) +export class AppModule {}`; + const usageCjs = `import { StoreModule } from '@ngrx/store'; +import { reducers } from './reducers'; +import OpenReplay from '@openreplay/tracker/cjs'; +import trackerNgRx from '@openreplay/tracker-ngrx/cjs'; +//... +const tracker = new OpenReplay({ + projectKey: '${projectKey}' +}); +//... +function SomeFunctionalComponent() { + useEffect(() => { // or componentDidMount in case of Class approach + tracker.start() + }, []) +//... +const metaReducers = [tracker.use(trackerNgRx())]; // check list of available options below +//... + @NgModule({ + imports: [StoreModule.forRoot(reducers, { metaReducers })] + }) + export class AppModule {} +}`; + return ( +
+

{t('NgRx')}

+
+
+ {t( + 'This plugin allows you to capture NgRx actions/state and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.', + )} +
+ +
{t('Installation')}
+ + +
{t('Usage')}
+

+ {t( + 'Add the generated meta-reducer into your imports. See NgRx documentation for more details.', + )} +

+
+ +
{t('Usage')}
+ } + second={} + /> + + +
+
+ ); +} + +NgRxDoc.displayName = 'NgRxDoc'; + +export default observer(NgRxDoc); diff --git a/frontend/app/components/Client/Integrations/Tracker/NgRxDoc/index.js b/frontend/app/components/Client/Integrations/Tracker/NgRxDoc/index.js index 65f97eed5..12044446b 100644 --- a/frontend/app/components/Client/Integrations/Tracker/NgRxDoc/index.js +++ b/frontend/app/components/Client/Integrations/Tracker/NgRxDoc/index.js @@ -1 +1 @@ -export { default } from './NgRxDoc' \ No newline at end of file +export { default } from './NgRxDoc'; diff --git a/frontend/app/components/Client/Integrations/Tracker/PiniaDoc/PiniaDoc.tsx b/frontend/app/components/Client/Integrations/Tracker/PiniaDoc/PiniaDoc.tsx index b63b50fba..8c18077f2 100644 --- a/frontend/app/components/Client/Integrations/Tracker/PiniaDoc/PiniaDoc.tsx +++ b/frontend/app/components/Client/Integrations/Tracker/PiniaDoc/PiniaDoc.tsx @@ -6,11 +6,13 @@ import ToggleContent from 'Components/shared/ToggleContent'; import { CodeBlock } from 'UI'; import DocLink from 'Shared/DocLink/DocLink'; +import { useTranslation } from 'react-i18next'; -const PiniaDoc = () => { +function PiniaDoc() { + const { t } = useTranslation(); const { integrationsStore, projectsStore } = useStore(); const sites = projectsStore.list; - const siteId = integrationsStore.integrations.siteId; + const { siteId } = integrationsStore.integrations; const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey; @@ -67,43 +69,43 @@ piniaStorePlugin(examplePiniaStore) className="bg-white h-screen overflow-y-auto" style={{ width: '500px' }} > -

VueX

+

{t('Pinia')}

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

- Initialize the @openreplay/tracker package as usual and load the - plugin into it. Then put the generated plugin into your plugins field - of your store. + {t( + 'Initialize the @openreplay/tracker package as usual and load the plugin into it. Then put the generated plugin into your plugins field of your store.', + )}

} second={} />
); -}; +} PiniaDoc.displayName = 'PiniaDoc'; diff --git a/frontend/app/components/Client/Integrations/Tracker/PiniaDoc/index.js b/frontend/app/components/Client/Integrations/Tracker/PiniaDoc/index.js index 730c76beb..95010dbc2 100644 --- a/frontend/app/components/Client/Integrations/Tracker/PiniaDoc/index.js +++ b/frontend/app/components/Client/Integrations/Tracker/PiniaDoc/index.js @@ -1 +1 @@ -export { default } from './PiniaDoc' +export { default } from './PiniaDoc'; diff --git a/frontend/app/components/Client/Integrations/Tracker/ReduxDoc/ReduxDoc.js b/frontend/app/components/Client/Integrations/Tracker/ReduxDoc/ReduxDoc.js deleted file mode 100644 index 5c69bec47..000000000 --- a/frontend/app/components/Client/Integrations/Tracker/ReduxDoc/ReduxDoc.js +++ /dev/null @@ -1,79 +0,0 @@ -import { useStore } from "App/mstore"; -import React from 'react'; -import { CodeBlock } from 'UI' -import ToggleContent from 'Components/shared/ToggleContent'; -import DocLink from 'Shared/DocLink/DocLink'; -import { observer } from 'mobx-react-lite' - -const ReduxDoc = () => { - const { integrationsStore, projectsStore } = useStore(); - const sites = projectsStore.list; - const siteId = integrationsStore.integrations.siteId - const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey - - const usage = `import { applyMiddleware, createStore } from 'redux'; -import OpenReplay from '@openreplay/tracker'; -import trackerRedux from '@openreplay/tracker-redux'; -//... -const tracker = new OpenReplay({ - projectKey: '${projectKey}' -}); -tracker.start() -//... -const store = createStore( - reducer, - applyMiddleware(tracker.use(trackerRedux())) // check list of available options below -);` - const usageCjs = `import { applyMiddleware, createStore } from 'redux'; -import OpenReplay from '@openreplay/tracker/cjs'; -import trackerRedux from '@openreplay/tracker-redux/cjs'; -//... -const tracker = new OpenReplay({ - projectKey: '${projectKey}' -}); -//... -function SomeFunctionalComponent() { - useEffect(() => { // or componentDidMount in case of Class approach - tracker.start() - }, []) -//... -const store = createStore( - reducer, - applyMiddleware(tracker.use(trackerRedux())) // check list of available options below - ); -}` - return ( -
-

Redux

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

Initialize the tracker then put the generated middleware into your Redux chain.

-
- - } - second={ - - } - /> - - -
-
- ); -}; - -ReduxDoc.displayName = 'ReduxDoc'; - -export default observer(ReduxDoc); diff --git a/frontend/app/components/Client/Integrations/Tracker/ReduxDoc/ReduxDoc.tsx b/frontend/app/components/Client/Integrations/Tracker/ReduxDoc/ReduxDoc.tsx new file mode 100644 index 000000000..35c2ad37a --- /dev/null +++ b/frontend/app/components/Client/Integrations/Tracker/ReduxDoc/ReduxDoc.tsx @@ -0,0 +1,94 @@ +import { useStore } from 'App/mstore'; +import React from 'react'; +import { CodeBlock } from 'UI'; +import ToggleContent from 'Components/shared/ToggleContent'; +import DocLink from 'Shared/DocLink/DocLink'; +import { observer } from 'mobx-react-lite'; +import { useTranslation } from 'react-i18next'; + +function ReduxDoc() { + const { t } = useTranslation(); + const { integrationsStore, projectsStore } = useStore(); + const sites = projectsStore.list; + const { siteId } = integrationsStore.integrations; + const projectKey = siteId + ? sites.find((site) => site.id === siteId)?.projectKey + : sites[0]?.projectKey; + + const usage = `import { applyMiddleware, createStore } from 'redux'; +import OpenReplay from '@openreplay/tracker'; +import trackerRedux from '@openreplay/tracker-redux'; +//... +const tracker = new OpenReplay({ + projectKey: '${projectKey}' +}); +tracker.start() +//... +const store = createStore( + reducer, + applyMiddleware(tracker.use(trackerRedux())) // check list of available options below +);`; + const usageCjs = `import { applyMiddleware, createStore } from 'redux'; +import OpenReplay from '@openreplay/tracker/cjs'; +import trackerRedux from '@openreplay/tracker-redux/cjs'; +//... +const tracker = new OpenReplay({ + projectKey: '${projectKey}' +}); +//... +function SomeFunctionalComponent() { + useEffect(() => { // or componentDidMount in case of Class approach + tracker.start() + }, []) +//... +const store = createStore( + reducer, + applyMiddleware(tracker.use(trackerRedux())) // check list of available options below + ); +}`; + return ( +
+

{t('Redux')}

+ +
+
+ {t( + 'This plugin allows you to capture Redux actions/state and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.', + )} +
+ +
{t('Installation')}
+ + +
{t('Usage')}
+

+ {t( + 'Initialize the tracker then put the generated middleware into your Redux chain.', + )} +

+
+ } + second={} + /> + + +
+
+ ); +} + +ReduxDoc.displayName = 'ReduxDoc'; + +export default observer(ReduxDoc); diff --git a/frontend/app/components/Client/Integrations/Tracker/ReduxDoc/index.js b/frontend/app/components/Client/Integrations/Tracker/ReduxDoc/index.js index 3c6245bd8..76c8271d4 100644 --- a/frontend/app/components/Client/Integrations/Tracker/ReduxDoc/index.js +++ b/frontend/app/components/Client/Integrations/Tracker/ReduxDoc/index.js @@ -1 +1 @@ -export { default } from './ReduxDoc' \ No newline at end of file +export { default } from './ReduxDoc.tsx'; diff --git a/frontend/app/components/Client/Integrations/Tracker/VueDoc/VueDoc.js b/frontend/app/components/Client/Integrations/Tracker/VueDoc/VueDoc.tsx similarity index 52% rename from frontend/app/components/Client/Integrations/Tracker/VueDoc/VueDoc.js rename to frontend/app/components/Client/Integrations/Tracker/VueDoc/VueDoc.tsx index f9e23c67a..b32e9f1f4 100644 --- a/frontend/app/components/Client/Integrations/Tracker/VueDoc/VueDoc.js +++ b/frontend/app/components/Client/Integrations/Tracker/VueDoc/VueDoc.tsx @@ -1,15 +1,19 @@ -import { useStore } from "App/mstore"; +import { useStore } from 'App/mstore'; import React from 'react'; -import { CodeBlock } from "UI"; +import { CodeBlock } from 'UI'; import ToggleContent from 'Components/shared/ToggleContent'; import DocLink from 'Shared/DocLink/DocLink'; import { observer } from 'mobx-react-lite'; +import { useTranslation } from 'react-i18next'; -const VueDoc = () => { +function VueDoc() { + const { t } = useTranslation(); 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 Vuex from 'vuex' import OpenReplay from '@openreplay/tracker'; @@ -23,7 +27,7 @@ tracker.start() const store = new Vuex.Store({ //... plugins: [tracker.use(trackerVuex())] // check list of available options below -});` +});`; const usageCjs = `import Vuex from 'vuex' import OpenReplay from '@openreplay/tracker/cjs'; import trackerVuex from '@openreplay/tracker-vuex/cjs'; @@ -41,45 +45,49 @@ const store = new Vuex.Store({ //... plugins: [tracker.use(trackerVuex())] // check list of available options below }); -}` +}`; return ( -
-

VueX

+
+

{t('VueX')}

- This plugin allows you to capture VueX mutations/state and inspect them later on while - replaying session recordings. This is very useful for understanding and fixing issues. + {t( + 'This plugin allows you to capture VueX mutations/state and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.', + )}
-
Installation
- +
{t('Installation')}
+ -
Usage
+
{t('Usage')}

- Initialize the @openreplay/tracker package as usual and load the plugin into it. Then put - the generated plugin into your plugins field of your store. + {t( + 'Initialize the @openreplay/tracker package as usual and load the plugin into it. Then put the generated plugin into your plugins field of your store.', + )}

- } - second={ - - } + label={t('Server-Side-Rendered (SSR)?')} + first={} + second={} />
); -}; +} VueDoc.displayName = 'VueDoc'; diff --git a/frontend/app/components/Client/Integrations/Tracker/VueDoc/index.js b/frontend/app/components/Client/Integrations/Tracker/VueDoc/index.js index 555e8acfb..b50d6d1dd 100644 --- a/frontend/app/components/Client/Integrations/Tracker/VueDoc/index.js +++ b/frontend/app/components/Client/Integrations/Tracker/VueDoc/index.js @@ -1 +1 @@ -export { default } from './VueDoc' \ No newline at end of file +export { default } from './VueDoc'; diff --git a/frontend/app/components/Client/Integrations/Tracker/ZustandDoc/ZustandDoc.js b/frontend/app/components/Client/Integrations/Tracker/ZustandDoc/ZustandDoc.js deleted file mode 100644 index eb7dcd091..000000000 --- a/frontend/app/components/Client/Integrations/Tracker/ZustandDoc/ZustandDoc.js +++ /dev/null @@ -1,104 +0,0 @@ -import { useStore } from "App/mstore"; -import React from 'react'; -import { CodeBlock } from "UI"; -import ToggleContent from 'Components//shared/ToggleContent'; -import DocLink from 'Shared/DocLink/DocLink'; -import { observer } from 'mobx-react-lite' - -const ZustandDoc = (props) => { - const { integrationsStore, projectsStore } = useStore(); - const sites = projectsStore.list; - const siteId = integrationsStore.integrations.siteId - const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey - - const usage = `import create from "zustand"; -import Tracker from '@openreplay/tracker'; -import trackerZustand, { StateLogger } from '@openreplay/tracker-zustand'; - - -const tracker = new Tracker({ - projectKey: ${projectKey}, -}); - -// as per https://docs.pmnd.rs/zustand/guides/typescript#middleware-that-doesn't-change-the-store-type -// cast type to new one -// but this seems to not be required and everything is working as is -const zustandPlugin = tracker.use(trackerZustand()) as unknown as StateLogger - - -const useBearStore = create( - zustandPlugin((set: any) => ({ - bears: 0, - increasePopulation: () => set((state: any) => ({ bears: state.bears + 1 })), - removeAllBears: () => set({ bears: 0 }), - }), - // store name is optional - // and is randomly generated if undefined - 'bear_store' - ) -) -` - const usageCjs =`import create from "zustand"; -import Tracker from '@openreplay/tracker/cjs'; -import trackerZustand, { StateLogger } from '@openreplay/tracker-zustand/cjs'; - - -const tracker = new Tracker({ - projectKey: ${projectKey}, -}); - -// as per https://docs.pmnd.rs/zustand/guides/typescript#middleware-that-doesn't-change-the-store-type -// cast type to new one -// but this seems to not be required and everything is working as is -const zustandPlugin = tracker.use(trackerZustand()) as unknown as StateLogger - - -const useBearStore = create( - zustandPlugin((set: any) => ({ - bears: 0, - increasePopulation: () => set((state: any) => ({ bears: state.bears + 1 })), - removeAllBears: () => set({ bears: 0 }), - }), - // store name is optional - // and is randomly generated if undefined - 'bear_store' - ) -)` - return ( -
-

Zustand

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

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

-
- - - } - second={ - - } - /> - - -
-
- ); -}; - -ZustandDoc.displayName = 'ZustandDoc'; - -export default observer(ZustandDoc); diff --git a/frontend/app/components/Client/Integrations/Tracker/ZustandDoc/ZustandDoc.tsx b/frontend/app/components/Client/Integrations/Tracker/ZustandDoc/ZustandDoc.tsx new file mode 100644 index 000000000..b25f40dd6 --- /dev/null +++ b/frontend/app/components/Client/Integrations/Tracker/ZustandDoc/ZustandDoc.tsx @@ -0,0 +1,116 @@ +import { useStore } from 'App/mstore'; +import React from 'react'; +import { CodeBlock } from 'UI'; +import ToggleContent from 'Components//shared/ToggleContent'; +import DocLink from 'Shared/DocLink/DocLink'; +import { observer } from 'mobx-react-lite'; +import { useTranslation } from 'react-i18next'; + +function ZustandDoc(props) { + const { t } = useTranslation(); + const { integrationsStore, projectsStore } = useStore(); + const sites = projectsStore.list; + const { siteId } = integrationsStore.integrations; + const projectKey = siteId + ? sites.find((site) => site.id === siteId)?.projectKey + : sites[0]?.projectKey; + + const usage = `import create from "zustand"; +import Tracker from '@openreplay/tracker'; +import trackerZustand, { StateLogger } from '@openreplay/tracker-zustand'; + + +const tracker = new Tracker({ + projectKey: ${projectKey}, +}); + +// as per https://docs.pmnd.rs/zustand/guides/typescript#middleware-that-doesn't-change-the-store-type +// cast type to new one +// but this seems to not be required and everything is working as is +const zustandPlugin = tracker.use(trackerZustand()) as unknown as StateLogger + + +const useBearStore = create( + zustandPlugin((set: any) => ({ + bears: 0, + increasePopulation: () => set((state: any) => ({ bears: state.bears + 1 })), + removeAllBears: () => set({ bears: 0 }), + }), + // store name is optional + // and is randomly generated if undefined + 'bear_store' + ) +) +`; + const usageCjs = `import create from "zustand"; +import Tracker from '@openreplay/tracker/cjs'; +import trackerZustand, { StateLogger } from '@openreplay/tracker-zustand/cjs'; + + +const tracker = new Tracker({ + projectKey: ${projectKey}, +}); + +// as per https://docs.pmnd.rs/zustand/guides/typescript#middleware-that-doesn't-change-the-store-type +// cast type to new one +// but this seems to not be required and everything is working as is +const zustandPlugin = tracker.use(trackerZustand()) as unknown as StateLogger + + +const useBearStore = create( + zustandPlugin((set: any) => ({ + bears: 0, + increasePopulation: () => set((state: any) => ({ bears: state.bears + 1 })), + removeAllBears: () => set({ bears: 0 }), + }), + // store name is optional + // and is randomly generated if undefined + 'bear_store' + ) +)`; + return ( +
+

{t('Zustand')}

+
+
+ {t( + 'This plugin allows you to capture Zustand mutations/state and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.', + )} +
+ +
{t('Installation')}
+ + +
{t('Usage')}
+

+ {t( + 'Initialize the @openreplay/tracker package as usual and load the plugin into it. Then put the generated plugin into your plugins field of your store.', + )} +

+
+ + } + second={} + /> + + +
+
+ ); +} + +ZustandDoc.displayName = 'ZustandDoc'; + +export default observer(ZustandDoc); diff --git a/frontend/app/components/Client/Integrations/Tracker/ZustandDoc/index.js b/frontend/app/components/Client/Integrations/Tracker/ZustandDoc/index.js index f7061eb7a..d0e1674b3 100644 --- a/frontend/app/components/Client/Integrations/Tracker/ZustandDoc/index.js +++ b/frontend/app/components/Client/Integrations/Tracker/ZustandDoc/index.js @@ -1 +1 @@ -export { default } from './ZustandDoc' +export { default } from './ZustandDoc'; diff --git a/frontend/app/components/Client/Integrations/apiMethods.ts b/frontend/app/components/Client/Integrations/apiMethods.ts index 3924a068b..6d70fdd94 100644 --- a/frontend/app/components/Client/Integrations/apiMethods.ts +++ b/frontend/app/components/Client/Integrations/apiMethods.ts @@ -12,10 +12,10 @@ export const serviceNames: Record = { export async function getIntegrationData( name: ServiceName, - projectId: string + projectId: string, ): Promise { const r = await client.get( - `/integrations/v1/integrations/${name}/${projectId}` + `/integrations/v1/integrations/${name}/${projectId}`, ); return r.json(); } @@ -23,7 +23,7 @@ export async function getIntegrationData( export function useIntegration( name: ServiceName, projectId: string, - initialValues: T + initialValues: T, ) { const { data, isPending } = useQuery({ queryKey: ['integrationData', name], @@ -36,12 +36,12 @@ export function useIntegration( }, initialData: initialValues, retry: (failureCount, error) => { - const status = error.status || error.response.status + const status = error.status || error.response.status; if (status === 404) { return false; } return failureCount < 4; - } + }, }); const saveMutation = useMutation({ @@ -72,13 +72,13 @@ export async function saveIntegration( name: string, data: T, projectId: string, - exists?: boolean + exists?: boolean, ) { const method = exists ? 'patch' : 'post'; try { const r = await client[method]( `/integrations/v1/integrations/${name}/${projectId}`, - { data } + { data }, ); if (r.ok) { toast.success(`${name} integration saved`); @@ -99,7 +99,7 @@ export async function saveIntegration( export async function removeIntegration(name: string, projectId: string) { try { const r = await client.delete( - `/integrations/v1/integrations/${name}/${projectId}` + `/integrations/v1/integrations/${name}/${projectId}`, ); if (r.ok) { toast.success(`${name} integration removed`); diff --git a/frontend/app/components/Client/Integrations/index.js b/frontend/app/components/Client/Integrations/index.js index 1a6b7f6cb..033519dfa 100644 --- a/frontend/app/components/Client/Integrations/index.js +++ b/frontend/app/components/Client/Integrations/index.js @@ -1 +1 @@ -export { default } from './Integrations'; \ No newline at end of file +export { default } from './Integrations'; diff --git a/frontend/app/components/Client/Modules/ModuleCard.tsx b/frontend/app/components/Client/Modules/ModuleCard.tsx index f6d6cadc6..72437062a 100644 --- a/frontend/app/components/Client/Modules/ModuleCard.tsx +++ b/frontend/app/components/Client/Modules/ModuleCard.tsx @@ -3,7 +3,6 @@ import { Icon } from 'UI'; import { Switch } from 'antd'; import { Module } from 'Components/Client/Modules/index'; - interface Props { module: Module; onToggle: (module: Module) => void; @@ -12,22 +11,26 @@ interface Props { function ModuleCard(props: Props) { const { module } = props; return ( -
-
- +
+
+
-
-
-

{module.label}

-

{module.description}

+
+
+

{module.label}

+

{module.description}

-
- props.onToggle(module)} /> +
+ props.onToggle(module)} + />
); } -export default ModuleCard; \ No newline at end of file +export default ModuleCard; diff --git a/frontend/app/components/Client/Modules/Modules.tsx b/frontend/app/components/Client/Modules/Modules.tsx index 2767ff76f..cca204eef 100644 --- a/frontend/app/components/Client/Modules/Modules.tsx +++ b/frontend/app/components/Client/Modules/Modules.tsx @@ -1,15 +1,17 @@ import React, { useEffect } from 'react'; import ModuleCard from 'Components/Client/Modules/ModuleCard'; -import { modules as list } from './'; import withPageTitle from 'HOCs/withPageTitle'; import { userService } from 'App/services'; import { toast } from 'react-toastify'; -import { useStore } from "App/mstore"; +import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; +import { modules as list } from '.'; +import { useTranslation } from 'react-i18next'; function Modules() { + const { t, i18n } = useTranslation(); const { userStore } = useStore(); - const updateModule = userStore.updateModule; + const { updateModule } = userStore; const modules = userStore.account.settings?.modules ?? []; const isEnterprise = userStore.account.edition === 'ee'; const [modulesState, setModulesState] = React.useState([]); @@ -24,36 +26,51 @@ function Modules() { status: isEnabled, }); updateModule(module.key); - toast.success(`Module ${module.label} ${!isEnabled ? 'enabled' : 'disabled'}`); + toast.success( + `${t('Module')} ${module.label} ${!isEnabled ? t('enabled') : t('disabled')}`, + ); } catch (err) { console.error(err); - toast.error(`Failed to ${module.isEnabled ? 'disable' : 'enable'} module ${module.label}`); + toast.error( + `${t('Failed to')} ${module.isEnabled ? t('disable') : t('enable')} module ${module.label}`, + ); module.isEnabled = !module.isEnabled; setModulesState((prevState) => [...prevState]); } }; useEffect(() => { - list.forEach((module) => { + list(t).forEach((module) => { module.isEnabled = modules.includes(module.key); }); - setModulesState(list.filter((module) => !module.hidden && (!module.enterprise || isEnterprise))); - }, [modules]); - + setModulesState( + list(t).filter( + (module) => !module.hidden && (!module.enterprise || isEnterprise), + ), + ); + }, [modules, i18n.language]); return (
-
-

Modules

-
    -
  • OpenReplay's modules are a collection of advanced features that provide enhanced functionality.
  • -
  • Easily enable any desired module within the user interface to access its capabilities
  • +
    +

    {t('Modules')}

    +
      +
    • + {t( + "OpenReplay's modules are a collection of advanced features that provide enhanced functionality.", + )} +
    • +
    • + {t( + 'Easily enable any desired module within the user interface to access its capabilities', + )} +
    -
    +
    {modulesState.map((module) => ( -
    +
    ))} @@ -62,5 +79,6 @@ function Modules() { ); } - -export default withPageTitle('Modules - OpenReplay Preferences')(observer(Modules)); +export default withPageTitle('Modules - OpenReplay Preferences')( + observer(Modules), +); diff --git a/frontend/app/components/Client/Modules/index.ts b/frontend/app/components/Client/Modules/index.ts index a95b10cec..85425d5f2 100644 --- a/frontend/app/components/Client/Modules/index.ts +++ b/frontend/app/components/Client/Modules/index.ts @@ -1,10 +1,12 @@ +import { TFunction } from 'i18next'; + export { default } from './Modules'; export const enum MODULES { ASSIST = 'assist', HIGHLIGHTS = 'notes', BUG_REPORTS = 'bug-reports', -OFFLINE_RECORDINGS = 'offline-recordings', + OFFLINE_RECORDINGS = 'offline-recordings', ALERTS = 'alerts', ASSIST_STATS = 'assist-stats', FEATURE_FLAGS = 'feature-flags', @@ -22,55 +24,69 @@ export interface Module { enterprise?: boolean; } -export const modules = [ +export const modules = (t: TFunction) => [ { - label: 'Co-Browse', - description: 'Enable live session replay, remote control, annotations and webRTC call/video.', + label: t('Co-Browse'), + description: t( + 'Enable live session replay, remote control, annotations and webRTC call/video.', + ), key: MODULES.ASSIST, - icon: 'broadcast' + icon: 'broadcast', }, { - label: 'Recordings', - description: 'Record live sessions while co-browsing with users and share it with your team for training purposes.', + label: t('Recordings'), + description: t( + 'Record live sessions while co-browsing with users and share it with your team for training purposes.', + ), key: MODULES.OFFLINE_RECORDINGS, - icon: 'record2' + icon: 'record2', }, { - label: 'Cobrowsing Reports', - description: 'Keep an eye on cobrowsing metrics across your team and generate reports.', + label: t('Cobrowsing Reports'), + description: t( + 'Keep an eye on cobrowsing metrics across your team and generate reports.', + ), key: MODULES.ASSIST_STATS, icon: 'file-bar-graph', - enterprise: true + enterprise: true, }, { - label: 'Highlights', - description: 'Add highlights to sessions and share with your team.', + label: t('Highlights'), + description: t('Add highlights to sessions and share with your team.'), key: MODULES.HIGHLIGHTS, icon: 'chat-square-quote', - isEnabled: true + isEnabled: true, }, { - label: 'Alerts', - description: 'Create alerts on cards and get notified when a metric hits a certain threshold.', + label: t('Alerts'), + description: t( + 'Create alerts on cards and get notified when a metric hits a certain threshold.', + ), key: MODULES.ALERTS, - icon: 'bell' + icon: 'bell', }, { - label: 'Feature Flags', - description: 'Make gradual releases and A/B test all of your new features without redeploying your app.', + label: t('Feature Flags'), + description: t( + 'Make gradual releases and A/B test all of your new features without redeploying your app.', + ), key: MODULES.FEATURE_FLAGS, - icon: 'toggles' + icon: 'toggles', }, { - label: 'Recommendations', - description: 'Get personalized recommendations for sessions to watch, based on your replay history and search preferences.', + label: t('Recommendations'), + description: t( + 'Get personalized recommendations for sessions to watch, based on your replay history and search preferences.', + ), key: MODULES.RECOMMENDATIONS, icon: 'magic', - hidden: true + hidden: true, }, { - label: 'Usability Tests', - description: 'Get feedback from your users by creating usability tests and sharing them with your team.', + label: t('Usability Tests'), + description: t( + 'Get feedback from your users by creating usability tests and sharing them with your team.', + ), key: MODULES.USABILITY_TESTS, icon: 'clipboard-check', }, diff --git a/frontend/app/components/Client/Notifications/Notifications.tsx b/frontend/app/components/Client/Notifications/Notifications.tsx index 0a2d12b23..fc4088e2f 100644 --- a/frontend/app/components/Client/Notifications/Notifications.tsx +++ b/frontend/app/components/Client/Notifications/Notifications.tsx @@ -1,40 +1,47 @@ import React, { useEffect } from 'react'; import cn from 'classnames'; -import stl from './notifications.module.css'; -import { Switch } from 'antd' -import { useStore } from "App/mstore"; -import { observer } from 'mobx-react-lite' +import { Switch } from 'antd'; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; import withPageTitle from 'HOCs/withPageTitle'; +import stl from './notifications.module.css'; +import { useTranslation } from 'react-i18next'; function Notifications() { - const { weeklyReportStore } = useStore() - + const { weeklyReportStore } = useStore(); + const { t } = useTranslation(); useEffect(() => { - void weeklyReportStore.fetchReport() + void weeklyReportStore.fetchReport(); }, []); const onChange = () => { - const newValue = !weeklyReportStore.weeklyReport - void weeklyReportStore.fetchEditReport(newValue) + const newValue = !weeklyReportStore.weeklyReport; + void weeklyReportStore.fetchEditReport(newValue); }; return (
    -
    {

    {'Weekly Report'}

    }
    +
    +

    {t('Weekly Report')}

    +
    -
    Weekly project summary
    -
    Receive weekly report for each project on email.
    -
    +
    {t('Weekly project summary')}
    +
    + {t('Receive weekly report for each project on email.')} +
    +
    - {weeklyReportStore.weeklyReport ? 'Yes' : 'No'} + checked={weeklyReportStore.weeklyReport} + onChange={onChange} + /> + {weeklyReportStore.weeklyReport ? t('Yes') : t('No')}
    ); } -export default withPageTitle('Weekly Report - OpenReplay Preferences')(observer(Notifications)) \ No newline at end of file +export default withPageTitle('Weekly Report - OpenReplay Preferences')( + observer(Notifications), +); diff --git a/frontend/app/components/Client/Notifications/index.js b/frontend/app/components/Client/Notifications/index.js index 1dc1a1351..7e9063f3a 100644 --- a/frontend/app/components/Client/Notifications/index.js +++ b/frontend/app/components/Client/Notifications/index.js @@ -1 +1 @@ -export { default } from './Notifications' \ No newline at end of file +export { default } from './Notifications'; diff --git a/frontend/app/components/Client/ProfileSettings/Api.js b/frontend/app/components/Client/ProfileSettings/Api.tsx similarity index 55% rename from frontend/app/components/Client/ProfileSettings/Api.js rename to frontend/app/components/Client/ProfileSettings/Api.tsx index 47e5c10dc..75e24b26c 100644 --- a/frontend/app/components/Client/ProfileSettings/Api.js +++ b/frontend/app/components/Client/ProfileSettings/Api.tsx @@ -1,26 +1,28 @@ import React from 'react'; -import { observer } from 'mobx-react-lite' +import { observer } from 'mobx-react-lite'; import { useStore } from 'App/mstore'; import { CopyButton, Form, Input } from 'UI'; +import { useTranslation } from 'react-i18next'; function ApiKeySettings() { + const { t } = useTranslation(); const { userStore } = useStore(); - const apiKey = userStore.account.apiKey; + const { apiKey } = userStore.account; return ( - + } /> ); } -export default observer(ApiKeySettings); \ No newline at end of file +export default observer(ApiKeySettings); diff --git a/frontend/app/components/Client/ProfileSettings/ChangePassword.tsx b/frontend/app/components/Client/ProfileSettings/ChangePassword.tsx index 207525f34..f19b8d53a 100644 --- a/frontend/app/components/Client/ProfileSettings/ChangePassword.tsx +++ b/frontend/app/components/Client/ProfileSettings/ChangePassword.tsx @@ -1,34 +1,44 @@ import React, { useState, useCallback } from 'react'; import { Message, Form, Input } from 'UI'; -import { Button } from 'antd' -import styles from './profileSettings.module.css'; +import { Button } from 'antd'; import { validatePassword } from 'App/validate'; import { PASSWORD_POLICY } from 'App/constants'; import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; +import styles from './profileSettings.module.css'; +import { useTranslation } from 'react-i18next'; -const ERROR_DOESNT_MATCH = 'Passwords don\'t match'; +const ERROR_DOESNT_MATCH = (t) => "Passwords don't match"; const MIN_LENGTH = 8; -const ChangePassword = () => { +function ChangePassword() { + const { t } = useTranslation(); const { userStore } = useStore(); - const updatePassword = userStore.updatePassword; + const { updatePassword } = userStore; const passwordErrors = userStore.updatePasswordRequest.errors; - const loading = userStore.updatePasswordRequest.loading; + const { loading } = userStore.updatePasswordRequest; const [oldPassword, setOldPassword] = useState(''); - const [newPassword, setNewPassword] = useState<{ value: string; error: boolean }>({ + const [newPassword, setNewPassword] = useState<{ + value: string; + error: boolean; + }>({ value: '', - error: false + error: false, }); - const [newPasswordRepeat, setNewPasswordRepeat] = useState<{ value: string; error: boolean }>({ + const [newPasswordRepeat, setNewPasswordRepeat] = useState<{ + value: string; + error: boolean; + }>({ value: '', - error: false + error: false, }); const [show, setShow] = useState(false); - const checkDoesntMatch = useCallback((newPassword: string, newPasswordRepeat: string) => { - return newPasswordRepeat.length > 0 && newPasswordRepeat !== newPassword; - }, []); + const checkDoesntMatch = useCallback( + (newPassword: string, newPasswordRepeat: string) => + newPasswordRepeat.length > 0 && newPasswordRepeat !== newPassword, + [], + ); const isSubmitDisabled = useCallback(() => { if ( @@ -53,17 +63,17 @@ const ChangePassword = () => { updatePassword({ oldPassword, - newPassword: newPassword.value - }).then(() => { - setShow(false); - setOldPassword(''); - setNewPassword({ value: '', error: false }); - setNewPasswordRepeat({ value: '', error: false }); - }).catch((e) => { - - }); + newPassword: newPassword.value, + }) + .then(() => { + setShow(false); + setOldPassword(''); + setNewPassword({ value: '', error: false }); + setNewPasswordRepeat({ value: '', error: false }); + }) + .catch((e) => {}); }, - [isSubmitDisabled, oldPassword, newPassword, updatePassword] + [isSubmitDisabled, oldPassword, newPassword, updatePassword], ); return show ? ( @@ -75,7 +85,9 @@ const ChangePassword = () => { name="oldPassword" value={oldPassword} type="password" - onChange={(e: React.ChangeEvent) => setOldPassword(e.target.value)} + onChange={(e: React.ChangeEvent) => + setOldPassword(e.target.value) + } /> @@ -94,14 +106,17 @@ const ChangePassword = () => { /> - + ) => { const newValue = e.target.value; @@ -115,15 +130,23 @@ const ChangePassword = () => { {err} ))} -
    } />
    -

    {'Profile'}

    -
    {'Your email address is your identity on OpenReplay and is used to login.'}
    +

    {t('Profile')}

    +
    + {t( + 'Your email address is your identity on OpenReplay and is used to login.', + )} +
    @@ -34,8 +40,10 @@ function ProfileSettings() { <>
    -

    {'Change Password'}

    -
    {'Updating your password from time to time enhances your account’s security.'}
    +

    {t('Change Password')}

    +
    + {t('Updating your password from time to time enhances your account’s security.')} +
    @@ -48,8 +56,10 @@ function ProfileSettings() {
    -

    {'Organization API Key'}

    -
    {'Your API key gives you access to an extra set of services.'}
    +

    {t('Organization API Key')}

    +
    + {t('Your API key gives you access to an extra set of services.')} +
    @@ -61,8 +71,10 @@ function ProfileSettings() {
    -

    {'Tenant Key'}

    -
    {'For SSO (SAML) authentication.'}
    +

    {t('Tenant Key')}

    +
    + {t('For SSO (SAML) authentication.')} +
    @@ -76,9 +88,9 @@ function ProfileSettings() {
    -

    {'Data Collection'}

    +

    {t('Data Collection')}

    - {'Enables you to control how OpenReplay captures data on your organization’s usage to improve our product.'} + {t('Enables you to control how OpenReplay captures data on your organization’s usage to improve our product.')}
    @@ -94,8 +106,10 @@ function ProfileSettings() {
    -

    {'License'}

    -
    {'License key and expiration date.'}
    +

    {t('License')}

    +
    + {t('License key and expiration date.')} +
    @@ -107,4 +121,6 @@ function ProfileSettings() { ); } -export default withPageTitle('Account - OpenReplay Preferences')(observer(ProfileSettings)); +export default withPageTitle('Account - OpenReplay Preferences')( + observer(ProfileSettings), +); diff --git a/frontend/app/components/Client/ProfileSettings/Settings.js b/frontend/app/components/Client/ProfileSettings/Settings.tsx similarity index 66% rename from frontend/app/components/Client/ProfileSettings/Settings.js rename to frontend/app/components/Client/ProfileSettings/Settings.tsx index 290f8b0f3..25f8bdc0f 100644 --- a/frontend/app/components/Client/ProfileSettings/Settings.js +++ b/frontend/app/components/Client/ProfileSettings/Settings.tsx @@ -1,45 +1,53 @@ import React from 'react'; import { Input, Form } from 'UI'; -import { Button } from 'antd' -import styles from './profileSettings.module.css'; +import { Button } from 'antd'; import { observer } from 'mobx-react-lite'; import { useStore } from 'App/mstore'; import { toast } from 'react-toastify'; +import styles from './profileSettings.module.css'; +import { useTranslation } from 'react-i18next'; function Settings() { + const { t } = useTranslation(); const { userStore } = useStore(); - const updateClient = userStore.updateClient; + const { updateClient } = userStore; const storeAccountName = userStore.account.name; const storeOrganizationName = userStore.account.tenantName; - const loading = userStore.loading; + const { loading } = userStore; const [accountName, setAccountName] = React.useState(storeAccountName); - const [organizationName, setOrganizationName] = React.useState(storeOrganizationName); + const [organizationName, setOrganizationName] = React.useState( + storeOrganizationName, + ); const [changed, setChanged] = React.useState(false); const onAccNameChange = (e) => { setAccountName(e.target.value); setChanged(true); - } + }; const onOrgNameChange = (e) => { setOrganizationName(e.target.value); setChanged(true); - } + }; const handleSubmit = async (e) => { e.preventDefault(); - await updateClient({ name: accountName, tenantName: organizationName }).then(() => { - setChanged(false); - toast('Profile settings updated successfully', { type: 'success' }); - }).catch((e) => { - toast(e.message || 'Failed to update account settings', { type: 'error' }); - }); - } + await updateClient({ name: accountName, tenantName: organizationName }) + .then(() => { + setChanged(false); + toast(t('Profile settings updated successfully'), { type: 'success' }); + }) + .catch((e) => { + toast(e.message || t('Failed to update account settings'), { + type: 'error', + }); + }); + }; return (
    - + - + - ); diff --git a/frontend/app/components/Client/ProfileSettings/TenantKey.js b/frontend/app/components/Client/ProfileSettings/TenantKey.js deleted file mode 100644 index 937e59b42..000000000 --- a/frontend/app/components/Client/ProfileSettings/TenantKey.js +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import copy from 'copy-to-clipboard'; -import { Form, Input } from "UI"; -import { Button } from 'antd'; -import { observer } from 'mobx-react-lite'; -import { useStore } from 'App/mstore'; - - -function TenantKey() { - const [ copied, setCopied ] = React.useState(false); - const { userStore } = useStore(); - const tenantKey = userStore.account.tenantKey; - - const copyHandler = () => { - setCopied(true); - copy(tenantKey); - setTimeout(() => { - setCopied(false); - }, 1000); - } - return ( - - - - { copied ? 'Copied' : 'Copy' } - - } - /> - - ); -} - -export default observer(TenantKey); diff --git a/frontend/app/components/Client/ProfileSettings/TenantKey.tsx b/frontend/app/components/Client/ProfileSettings/TenantKey.tsx new file mode 100644 index 000000000..f6f159c4d --- /dev/null +++ b/frontend/app/components/Client/ProfileSettings/TenantKey.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import copy from 'copy-to-clipboard'; +import { Form, Input } from 'UI'; +import { Button } from 'antd'; +import { observer } from 'mobx-react-lite'; +import { useStore } from 'App/mstore'; +import { useTranslation } from 'react-i18next'; + +function TenantKey() { + const { t } = useTranslation(); + const [copied, setCopied] = React.useState(false); + const { userStore } = useStore(); + const { tenantKey } = userStore.account; + + const copyHandler = () => { + setCopied(true); + copy(tenantKey); + setTimeout(() => { + setCopied(false); + }, 1000); + }; + return ( + + + + {copied ? t('Copied') : t('Copy')} + + } + /> + + ); +} + +export default observer(TenantKey); diff --git a/frontend/app/components/Client/Projects/ProjectCaptureRate.tsx b/frontend/app/components/Client/Projects/ProjectCaptureRate.tsx index f0464ab2a..9871cd138 100644 --- a/frontend/app/components/Client/Projects/ProjectCaptureRate.tsx +++ b/frontend/app/components/Client/Projects/ProjectCaptureRate.tsx @@ -7,18 +7,20 @@ import { Conditions } from '@/mstore/types/FeatureFlag'; import { useStore } from '@/mstore'; import Project from '@/mstore/types/project'; import { observer } from 'mobx-react-lite'; +import { useTranslation } from 'react-i18next'; interface Props { project: Project; } function ProjectCaptureRate(props: Props) { + const { t } = useTranslation(); const [conditions, setConditions] = React.useState([]); const { projectId, platform } = props.project; const isMobile = platform !== 'web'; const { settingsStore, userStore, customFieldStore } = useStore(); const isAdmin = userStore.account.admin || userStore.account.superAdmin; - const isEnterprise = userStore.isEnterprise; + const { isEnterprise } = userStore; const [changed, setChanged] = useState(false); const { sessionSettings: { @@ -26,11 +28,11 @@ function ProjectCaptureRate(props: Props) { changeCaptureRate, conditionalCapture, changeConditionalCapture, - captureConditions + captureConditions, }, loadingCaptureRate, updateCaptureConditions, - fetchCaptureConditions + fetchCaptureConditions, } = settingsStore; useEffect(() => { @@ -38,7 +40,7 @@ function ProjectCaptureRate(props: Props) { setChanged(false); const fetchData = async () => { if (isEnterprise) { - await customFieldStore.fetchListActive(projectId + ''); + await customFieldStore.fetchListActive(`${projectId}`); } void fetchCaptureConditions(projectId); }; @@ -47,7 +49,11 @@ function ProjectCaptureRate(props: Props) { }, [projectId]); useEffect(() => { - setConditions(captureConditions.map((condition: any) => new Conditions(condition, true, isMobile))); + setConditions( + captureConditions.map( + (condition: any) => new Conditions(condition, true, isMobile), + ), + ); }, [captureConditions]); const onCaptureRateChange = (input: string) => { @@ -67,27 +73,36 @@ function ProjectCaptureRate(props: Props) { const onUpdate = () => { updateCaptureConditions(projectId!, { rate: parseInt(captureRate, 10), - conditionalCapture: conditionalCapture, - conditions: isEnterprise ? conditions.map((c) => c.toCaptureCondition()) : [] + conditionalCapture, + conditions: isEnterprise + ? conditions.map((c) => c.toCaptureCondition()) + : [], }); setChanged(false); }; - const updateDisabled = !changed || !isAdmin || (isEnterprise && (conditionalCapture && conditions.length === 0)); + const updateDisabled = + !changed || + !isAdmin || + (isEnterprise && conditionalCapture && conditions.length === 0); return ( - +
    - Define percentage of sessions you want to capture + + {t('Define percentage of sessions you want to capture')} + - + @@ -95,9 +110,11 @@ function ProjectCaptureRate(props: Props) { {!conditionalCapture ? ( @@ -129,7 +146,7 @@ function ProjectCaptureRate(props: Props) { onClick={onUpdate} disabled={updateDisabled} > - Update + {t('Update')}
    diff --git a/frontend/app/components/Client/Projects/ProjectCodeSnippet.tsx b/frontend/app/components/Client/Projects/ProjectCodeSnippet.tsx index 5cc119380..cbe27d481 100644 --- a/frontend/app/components/Client/Projects/ProjectCodeSnippet.tsx +++ b/frontend/app/components/Client/Projects/ProjectCodeSnippet.tsx @@ -5,37 +5,37 @@ import { Loader } from 'UI'; import { Switch, Checkbox, Tag } from 'antd'; import GDPR from 'App/mstore/types/gdpr'; import cn from 'classnames'; -import stl from './projectCodeSnippet.module.css'; import Select from 'Shared/Select'; import CodeSnippet from 'Shared/CodeSnippet'; import CircleNumber from 'Components/Onboarding/components/CircleNumber'; import Project from '@/mstore/types/project'; +import stl from './projectCodeSnippet.module.css'; +import { useTranslation } from 'react-i18next'; +import { TFunction } from 'i18next'; interface InputModeOption { label: string; value: string; } -const inputModeOptions: InputModeOption[] = [ - { label: 'Record all inputs', value: 'plain' }, - { label: 'Ignore all inputs', value: 'obscured' }, - { label: 'Obscure all inputs', value: 'hidden' }, +const inputModeOptions: (t: TFunction) => InputModeOption[] = (t) => [ + { label: t('Record all inputs'), value: 'plain' }, + { label: t('Ignore all inputs'), value: 'obscured' }, + { label: t('Obscure all inputs'), value: 'hidden' }, ]; -const inputModeOptionsMap: Record = {}; -inputModeOptions.forEach((o, i) => (inputModeOptionsMap[o.value] = i)); - interface Props { project: Project; } const ProjectCodeSnippet: React.FC = (props) => { + const { t } = useTranslation(); const { projectsStore } = useStore(); - const siteId = projectsStore.siteId; + const { siteId } = projectsStore; const site = props.project; const gdpr = site.gdpr as GDPR; const sites = projectsStore.list; - const editGDPR = projectsStore.editGDPR; + const { editGDPR } = projectsStore; const onSaveGDPR = projectsStore.saveGDPR; const init = projectsStore.initProject; const [changed, setChanged] = useState(false); @@ -77,17 +77,17 @@ const ProjectCodeSnippet: React.FC = (props) => {
    - Choose data recording options + {t('Choose data recording options')}
    - +
    { // projectsStore.editInstance({ platform: value }); - setProject((prev: Project) => (new Project({ ...prev, platform: value }))); + setProject( + (prev: Project) => new Project({ ...prev, platform: value }), + ); }} />
    @@ -133,18 +153,18 @@ function ProjectForm(props: Props) { loading={loading} // disabled={!project.validate} > - {project.exists() ? 'Save' : 'Add'} + {project.exists() ? t('Save') : t('Add')}
    {project.exists() && ( - + } - > + title={ + + {t('Projects')} + + } + extra={ + + } + > - + - +
    - + {project?.name} @@ -78,8 +96,8 @@ function Projects() { @@ -95,18 +113,19 @@ export default observer(Projects); function ProjectKeyButton({ project }: { project: Project | null }) { const { message } = App.useApp(); + const { t } = useTranslation(); const copyKey = () => { if (!project || !project.projectKey) { - void message.error('Project key not found'); + void message.error(t('Project key not found')); return; } void navigator.clipboard.writeText(project?.projectKey || ''); - void message.success('Project key copied to clipboard'); + void message.success(t('Project key copied to clipboard')); }; return ( - + - - +
    ); diff --git a/frontend/app/components/Client/Roles/Roles.tsx b/frontend/app/components/Client/Roles/Roles.tsx index 5b0e86b67..92f8adb23 100644 --- a/frontend/app/components/Client/Roles/Roles.tsx +++ b/frontend/app/components/Client/Roles/Roles.tsx @@ -6,23 +6,25 @@ import React, { useEffect } from 'react'; import { useModal } from 'App/components/Modal'; import { useStore } from 'App/mstore'; import { Loader, NoContent, Tooltip, confirm } from 'UI'; -import { Button } from 'antd' +import { Button } from 'antd'; import RoleForm from './components/RoleForm'; import RoleItem from './components/RoleItem'; import stl from './roles.module.css'; +import { useTranslation } from 'react-i18next'; function Roles() { + const { t } = useTranslation(); const { roleStore, projectsStore, userStore } = useStore(); - const account = userStore.account; + const { account } = userStore; const projectsMap = projectsStore.list.reduce((acc: any, p: any) => { acc[p.id] = p.name; return acc; }, {}); const roles = roleStore.list; - const loading = roleStore.loading; - const init = roleStore.init; - const deleteRole = roleStore.deleteRole; + const { loading } = roleStore; + const { init } = roleStore; + const { deleteRole } = roleStore; const permissionsMap: any = {}; roleStore.permissions.forEach((p: any) => { permissionsMap[p.value] = p.text; @@ -42,15 +44,15 @@ function Roles() { permissionsMap={permissionsMap} deleteHandler={deleteHandler} />, - { right: true } + { right: true }, ); }; const deleteHandler = async (role: any) => { if ( await confirm({ - header: 'Roles', - confirmation: `Are you sure you want to remove this role?`, + header: t('Roles'), + confirmation: t('Are you sure you want to remove this role?'), }) ) { deleteRole(role.roleId).then(hideModal); @@ -58,60 +60,66 @@ function Roles() { }; return ( - - -
    -
    -
    -

    Roles and Access

    - - - -
    + +
    +
    +
    +

    + {t('Roles and Access')} +

    + + +
    - - -
    -
    -
    - Title -
    -
    - Project Access -
    -
    - Feature Access -
    -
    -
    - {roles.map((role) => ( - - ))} -
    -
    - - + + +
    +
    +
    + {t('Title')} +
    +
    + {t('Project Access')} +
    +
    + {t('Feature Access')} +
    +
    +
    + {roles.map((role) => ( + + ))} +
    + +
    + ); } export default withPageTitle('Roles & Access - OpenReplay Preferences')( - observer(Roles) + observer(Roles), ); diff --git a/frontend/app/components/Client/Roles/components/RoleForm/RoleForm.tsx b/frontend/app/components/Client/Roles/components/RoleForm/RoleForm.tsx index d952f441a..953380ee2 100644 --- a/frontend/app/components/Client/Roles/components/RoleForm/RoleForm.tsx +++ b/frontend/app/components/Client/Roles/components/RoleForm/RoleForm.tsx @@ -4,9 +4,10 @@ import React, { useEffect, useRef } from 'react'; import { useStore } from 'App/mstore'; import { Checkbox, Form, Icon, Input } from 'UI'; -import stl from './roleForm.module.css'; import { Select, Button } from 'antd'; import { SelectProps } from 'antd/es/select'; +import stl from './roleForm.module.css'; +import { useTranslation } from 'react-i18next'; interface Props { closeModal: (toastMessage?: string) => void; @@ -14,7 +15,8 @@ interface Props { deleteHandler: (id: any) => Promise; } -const RoleForm = (props: Props) => { +function RoleForm(props: Props) { + const { t } = useTranslation(); const { roleStore, projectsStore } = useStore(); const projects = projectsStore.list; const role = roleStore.instance; @@ -23,13 +25,15 @@ const RoleForm = (props: Props) => { const projectOptions: SelectProps['options'] = projects.map((p: any) => ({ value: p.projectId, - label: p.name + label: p.name, })); - const permissionOptions: SelectProps['options'] = roleStore.permissions.map((p: any) => ({ - value: p.value, - label: p.text - })); + const permissionOptions: SelectProps['options'] = roleStore.permissions.map( + (p: any) => ({ + value: p.value, + label: p.text, + }), + ); const selectProjects = (pros: { value: number; label: string }[]) => { const ids: any = pros.map((p) => p.value); @@ -41,8 +45,7 @@ const RoleForm = (props: Props) => { roleStore.editRole({ permissions: ids }); }; - - let focusElement = useRef(null); + const focusElement = useRef(null); // const permissions: {}[] = roleStore.permissions // .filter(({ value }) => !role.permissions.includes(value)) // .map((p) => ({ @@ -52,7 +55,7 @@ const RoleForm = (props: Props) => { const _save = () => { roleStore.saveRole(role).then(() => { - closeModal(role.exists() ? 'Role updated' : 'Role created'); + closeModal(role.exists() ? t('Role updated') : t('Role created')); }); }; @@ -113,7 +116,7 @@ const RoleForm = (props: Props) => {
    - + { maxLength={40} className={stl.input} id="name-field" - placeholder="Ex. Admin" + placeholder={t('Ex. Admin')} /> - +
    { type="checkbox" checked={role.allProjects} onClick={toggleAllProjects} - label={''} + label="" />
    -
    All Projects
    +
    {t('All Projects')}
    - (Uncheck to select specific projects) + ({t('Uncheck to select specific projects')})
    {!role.allProjects && ( - <> - + (option?.label ?? '') + .toLowerCase() + .includes(input.toLowerCase()) + } + mode="multiple" + allowClear + placeholder={t('Select')} + options={projectOptions.filter( + (option: any) => !role.projects.includes(option.value), // Exclude selected options + )} + onChange={selectProjects} + labelInValue + value={role.projects.map((projectId: string) => { + const matchingProject = projectOptions.find( + (opt) => opt.value === projectId, + ); + return matchingProject + ? { + value: matchingProject.value, + label: matchingProject.label, + } + : { value: projectId, label: String(projectId) }; // Fallback to projectId as label + })} + /> )}
    - + */} + {/* writeOption({ name: 'permissions', value: value.value }) */} + {/* } */} + {/* value={null} */} + {/* /> */} + {/* {role.permissions.length > 0 && ( */} + {/*
    */} + {/* {role.permissions.map((p: any) => */} + {/* OptionLabel(permissionsMap, p, onChangePermissions) */} + {/* )} */} + {/*
    */} + {/* )} */}
    @@ -225,7 +237,9 @@ const RoleForm = (props: Props) => { > {role.exists() ? 'Update' : 'Add'} - {role.exists() && } + {role.exists() && ( + + )}
    {role.exists() && (
    ); -}; +} export default observer(RoleForm); diff --git a/frontend/app/components/Client/Roles/components/RoleForm/index.ts b/frontend/app/components/Client/Roles/components/RoleForm/index.ts index 3bb62ee58..33c6eaaf6 100644 --- a/frontend/app/components/Client/Roles/components/RoleForm/index.ts +++ b/frontend/app/components/Client/Roles/components/RoleForm/index.ts @@ -1 +1 @@ -export { default } from './RoleForm'; \ No newline at end of file +export { default } from './RoleForm'; diff --git a/frontend/app/components/Client/Roles/components/RoleItem/RoleItem.tsx b/frontend/app/components/Client/Roles/components/RoleItem/RoleItem.tsx index 429100e30..352b0af5c 100644 --- a/frontend/app/components/Client/Roles/components/RoleItem/RoleItem.tsx +++ b/frontend/app/components/Client/Roles/components/RoleItem/RoleItem.tsx @@ -1,58 +1,83 @@ import React from 'react'; import { Icon, Link } from 'UI'; -import { Button } from 'antd' -import stl from './roleItem.module.css'; +import { Button } from 'antd'; import cn from 'classnames'; import { CLIENT_TABS, client as clientRoute } from 'App/routes'; +import stl from './roleItem.module.css'; function PermisionLabel({ label }: any) { - return
    {label}
    ; + return
    {label}
    ; } function PermisionLabelLinked({ label, route }: any) { - return ( - -
    {label}
    - - ); + return ( + +
    + {label} +
    + + ); } interface Props { - role: any; - editHandler?: (role: any) => void; - permissions: any; - isAdmin: boolean; - projects: any; + role: any; + editHandler?: (role: any) => void; + permissions: any; + isAdmin: boolean; + projects: any; } -function RoleItem({ role, editHandler, isAdmin, permissions, projects }: Props) { - return ( -
    -
    - - {role.name} -
    -
    - {role.allProjects ? ( - - ) : ( - role.projects.map((p: any) => ) - )} -
    -
    -
    - {role.permissions.map((permission: any) => ( - - ))} -
    - -
    - {isAdmin && !!editHandler && ( -
    -
    +function RoleItem({ + role, + editHandler, + isAdmin, + permissions, + projects, +}: Props) { + return ( +
    +
    + + {role.name} +
    +
    + {role.allProjects ? ( + + ) : ( + role.projects.map((p: any) => ) + )} +
    +
    +
    + {role.permissions.map((permission: any) => ( + + ))}
    - ); + +
    + {isAdmin && !!editHandler && ( +
    +
    +
    + ); } export default RoleItem; diff --git a/frontend/app/components/Client/Roles/components/RoleItem/index.ts b/frontend/app/components/Client/Roles/components/RoleItem/index.ts index 645d37fd1..674f5dfd9 100644 --- a/frontend/app/components/Client/Roles/components/RoleItem/index.ts +++ b/frontend/app/components/Client/Roles/components/RoleItem/index.ts @@ -1 +1 @@ -export { default } from './RoleItem' \ No newline at end of file +export { default } from './RoleItem'; diff --git a/frontend/app/components/Client/Roles/index.ts b/frontend/app/components/Client/Roles/index.ts index 9e6fe3912..062358c81 100644 --- a/frontend/app/components/Client/Roles/index.ts +++ b/frontend/app/components/Client/Roles/index.ts @@ -1 +1 @@ -export { default } from './Roles'; \ No newline at end of file +export { default } from './Roles'; diff --git a/frontend/app/components/Client/SessionsListingSettings.tsx b/frontend/app/components/Client/SessionsListingSettings.tsx index b825c565c..02875ed0c 100644 --- a/frontend/app/components/Client/SessionsListingSettings.tsx +++ b/frontend/app/components/Client/SessionsListingSettings.tsx @@ -6,12 +6,14 @@ import DefaultPlaying from 'Shared/SessionSettings/components/DefaultPlaying'; import DefaultTimezone from 'Shared/SessionSettings/components/DefaultTimezone'; import ListingVisibility from 'Shared/SessionSettings/components/ListingVisibility'; import MouseTrailSettings from 'Shared/SessionSettings/components/MouseTrailSettings'; -import DebugLog from "./DebugLog"; +import DebugLog from './DebugLog'; +import { useTranslation } from 'react-i18next'; function SessionsListingSettings() { + const { t } = useTranslation(); return (
    - Sessions Listing
    } /> + {t('Sessions Listing')}
    } />
    @@ -30,7 +32,7 @@ function SessionsListingSettings() {
    -
    +
    @@ -40,5 +42,5 @@ function SessionsListingSettings() { } export default withPageTitle('Sessions Listings - OpenReplay Preferences')( - SessionsListingSettings + SessionsListingSettings, ); diff --git a/frontend/app/components/Client/Sites/AddProjectButton/AddProjectButton.tsx b/frontend/app/components/Client/Sites/AddProjectButton/AddProjectButton.tsx index b0e8fd52f..2d1c51952 100644 --- a/frontend/app/components/Client/Sites/AddProjectButton/AddProjectButton.tsx +++ b/frontend/app/components/Client/Sites/AddProjectButton/AddProjectButton.tsx @@ -1,20 +1,25 @@ import React from 'react'; import { Tooltip } from 'UI'; -import { Button } from 'antd' +import { Button } from 'antd'; import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; import { useModal } from 'App/components/Modal'; import NewSiteForm from '../NewSiteForm'; +import { TFunction } from 'i18next'; +import { useTranslation } from 'react-i18next'; -const PERMISSION_WARNING = 'You don’t have the permissions to perform this action.'; -const LIMIT_WARNING = 'You have reached site limit.'; +const PERMISSION_WARNING = (t: TFunction) => + t('You don’t have the permissions to perform this action.'); +const LIMIT_WARNING = (t: TFunction) => t('You have reached site limit.'); function AddProjectButton({ isAdmin = false }: any) { + const { t } = useTranslation(); const { userStore, projectsStore } = useStore(); const init = projectsStore.initProject; const { showModal, hideModal } = useModal(); - const limits = userStore.limits; - const canAddProject = isAdmin && (limits.projects === -1 || limits.projects > 0) + const { limits } = userStore; + const canAddProject = + isAdmin && (limits.projects === -1 || limits.projects > 0); const onClick = () => { init({}); @@ -22,11 +27,15 @@ function AddProjectButton({ isAdmin = false }: any) { }; return ( - ); diff --git a/frontend/app/components/Client/Sites/AddProjectButton/index.ts b/frontend/app/components/Client/Sites/AddProjectButton/index.ts index 631dd2a10..d42615c11 100644 --- a/frontend/app/components/Client/Sites/AddProjectButton/index.ts +++ b/frontend/app/components/Client/Sites/AddProjectButton/index.ts @@ -1 +1 @@ -export { default } from './AddProjectButton'; \ No newline at end of file +export { default } from './AddProjectButton'; diff --git a/frontend/app/components/Client/Sites/GDPRForm.js b/frontend/app/components/Client/Sites/GDPRForm.js deleted file mode 100644 index 0a35f7399..000000000 --- a/frontend/app/components/Client/Sites/GDPRForm.js +++ /dev/null @@ -1,131 +0,0 @@ -import React from 'react'; -import { observer } from 'mobx-react-lite'; -import { useStore } from "App/mstore"; -import { Form, Input, Icon } from 'UI'; -import { Button } from 'antd' -import { validateNumber } from 'App/validate'; -import styles from './siteForm.module.css'; -import Select from 'Shared/Select'; - -const inputModeOptions = [ - { label: 'Record all inputs', value: 'plain' }, - { label: 'Ignore all inputs', value: 'obscured' }, - { label: 'Obscure all inputs', value: 'hidden' }, -]; - -function GDPRForm(props) { - const { projectsStore } = useStore(); - const site = projectsStore.instance; - const gdpr = site.gdpr; - const saving = false //projectsStore.; - const editGDPR = projectsStore.editGDPR; - const saveGDPR = projectsStore.saveGDPR; - - - const onChange = ({ target: { name, value } }) => { - if (name === "sampleRate") { - if (!validateNumber(value, { min: 0, max: 100 })) return; - if (value.length > 1 && value[0] === "0") { - value = value.slice(1); - } - } - editGDPR({ [ name ]: value }); - } - - const onSampleRateBlur = ({ target: { name, value } }) => { //TODO: editState hoc - if (value === ''){ - editGDPR({ sampleRate: 100 }); - } - } - - const onChangeSelect = ({ name, value }) => { - props.editGDPR({ [ name ]: value }); - }; - - const onChangeOption = ({ target: { checked, name } }) => { - editGDPR({ [ name ]: checked }); - } - - const onSubmit = (e) => { - e.preventDefault(); - void saveGDPR(site.id); - } - - return ( -
    -
    - - -
    { site.host }
    -
    - - - - - - - - - { 'Do not record any numeric text' } -
    { 'If enabled, OpenReplay will not record or store any numeric text for all sessions.' }
    - -
    - - - - - -
    -
    - { 'Block IP' } -
    -
    -
    - -
    - - -
    -
    - ) -} - -export default observer(GDPRForm); \ No newline at end of file diff --git a/frontend/app/components/Client/Sites/GDPRForm.tsx b/frontend/app/components/Client/Sites/GDPRForm.tsx new file mode 100644 index 000000000..4393f9a7c --- /dev/null +++ b/frontend/app/components/Client/Sites/GDPRForm.tsx @@ -0,0 +1,140 @@ +import React from 'react'; +import { observer } from 'mobx-react-lite'; +import { useStore } from 'App/mstore'; +import { Form, Input, Icon } from 'UI'; +import { Button } from 'antd'; +import { validateNumber } from 'App/validate'; +import Select from 'Shared/Select'; +import styles from './siteForm.module.css'; +import { TFunction } from 'i18next'; +import { useTranslation } from 'react-i18next'; + +const inputModeOptions = (t: TFunction) => [ + { label: t('Record all inputs'), value: 'plain' }, + { label: t('Ignore all inputs'), value: 'obscured' }, + { label: t('Obscure all inputs'), value: 'hidden' }, +]; + +function GDPRForm(props) { + const { t } = useTranslation(); + const { projectsStore } = useStore(); + const site = projectsStore.instance; + const { gdpr } = site; + const saving = false; // projectsStore.; + const { editGDPR } = projectsStore; + const { saveGDPR } = projectsStore; + + const onChange = ({ target: { name, value } }) => { + if (name === 'sampleRate') { + if (!validateNumber(value, { min: 0, max: 100 })) return; + if (value.length > 1 && value[0] === '0') { + value = value.slice(1); + } + } + editGDPR({ [name]: value }); + }; + + const onSampleRateBlur = ({ target: { name, value } }) => { + // TODO: editState hoc + if (value === '') { + editGDPR({ sampleRate: 100 }); + } + }; + + const onChangeSelect = ({ name, value }) => { + props.editGDPR({ [name]: value }); + }; + + const onChangeOption = ({ target: { checked, name } }) => { + editGDPR({ [name]: checked }); + }; + + const onSubmit = (e) => { + e.preventDefault(); + void saveGDPR(site.id); + }; + + return ( +
    +
    + + +
    {site.host}
    +
    + + + + + + + + + {t('Do not record any numeric text')} +
    + {t( + 'If enabled, OpenReplay will not record or store any numeric text for all sessions.', + )} +
    + +
    + + + + + +
    +
    + {t('Block IP')} +
    +
    +
    + +
    + + +
    +
    + ); +} + +export default observer(GDPRForm); diff --git a/frontend/app/components/Client/Sites/InstallButton/InstallButton.tsx b/frontend/app/components/Client/Sites/InstallButton/InstallButton.tsx index f3ce8b8df..2646b68d1 100644 --- a/frontend/app/components/Client/Sites/InstallButton/InstallButton.tsx +++ b/frontend/app/components/Client/Sites/InstallButton/InstallButton.tsx @@ -2,24 +2,31 @@ import { useModal } from 'App/components/Modal'; import React from 'react'; import TrackingCodeModal from 'Shared/TrackingCodeModal'; import { Button } from 'antd'; +import { useTranslation } from 'react-i18next'; interface Props { - site: any; + site: any; } function InstallButton(props: Props) { - const { site } = props; - const { showModal, hideModal } = useModal(); - const onClick = () => { - showModal( - , - { right: true, width: 700 } - ); - }; - return ( - + const { t } = useTranslation(); + const { site } = props; + const { showModal, hideModal } = useModal(); + const onClick = () => { + showModal( + , + { right: true, width: 700 }, ); + }; + return ( + + ); } export default InstallButton; diff --git a/frontend/app/components/Client/Sites/InstallButton/index.ts b/frontend/app/components/Client/Sites/InstallButton/index.ts index c64b2ff6c..99716e369 100644 --- a/frontend/app/components/Client/Sites/InstallButton/index.ts +++ b/frontend/app/components/Client/Sites/InstallButton/index.ts @@ -1 +1 @@ -export { default } from './InstallButton' \ No newline at end of file +export { default } from './InstallButton'; diff --git a/frontend/app/components/Client/Sites/NewSiteForm.tsx b/frontend/app/components/Client/Sites/NewSiteForm.tsx index 6445fc700..b10849ea5 100644 --- a/frontend/app/components/Client/Sites/NewSiteForm.tsx +++ b/frontend/app/components/Client/Sites/NewSiteForm.tsx @@ -1,13 +1,13 @@ -import { Segmented } from 'antd'; +import { Segmented, Button } from 'antd'; import React, { ChangeEvent, FormEvent, useEffect, useState } from 'react'; import { RouteComponentProps, withRouter } from 'react-router-dom'; import { toast } from 'react-toastify'; import { useStore } from 'App/mstore'; import { confirm, Form, Icon, Input } from 'UI'; -import { Button } from 'antd' import { observer } from 'mobx-react-lite'; import styles from './siteForm.module.css'; +import { useTranslation } from 'react-i18next'; type OwnProps = { onClose: (arg: any) => void; @@ -15,17 +15,18 @@ type OwnProps = { type Props = RouteComponentProps & OwnProps; -const NewSiteForm = ({ location: { pathname }, onClose }: Props) => { +function NewSiteForm({ location: { pathname }, onClose }: Props) { + const { t } = useTranslation(); const mstore = useStore(); const { projectsStore } = mstore; const activeSiteId = projectsStore.active?.id; const site = projectsStore.instance; const siteList = projectsStore.list; - const loading = projectsStore.loading; + const { loading } = projectsStore; const canDelete = siteList.length > 1; - const setSiteId = projectsStore.setSiteId; + const { setSiteId } = projectsStore; const saveProject = projectsStore.save; - const fetchList = projectsStore.fetchList; + const { fetchList } = projectsStore; const [existsError, setExistsError] = useState(false); const { searchStore } = useStore(); @@ -41,14 +42,17 @@ const NewSiteForm = ({ location: { pathname }, onClose }: Props) => { if (!projectsStore.instance) return; if (projectsStore.instance.id && projectsStore.instance.exists()) { projectsStore - .updateProject(projectsStore.instance.id, projectsStore.instance.toData()) + .updateProject( + projectsStore.instance.id, + projectsStore.instance.toData(), + ) .then((response: any) => { if (!response || !response.errors || response.errors.size === 0) { onClose(null); if (!pathname.includes('onboarding')) { void fetchList(); } - toast.success('Project updated successfully'); + toast.success(t('Project updated successfully')); } else { toast.error(response.errors[0]); } @@ -60,7 +64,7 @@ const NewSiteForm = ({ location: { pathname }, onClose }: Props) => { searchStore.clearSearch(); mstore.searchStoreLive.clearSearch(); mstore.initClient(); - toast.success('Project added successfully'); + toast.success(t('Project added successfully')); } else { toast.error(response.errors[0]); } @@ -71,10 +75,12 @@ const NewSiteForm = ({ location: { pathname }, onClose }: Props) => { const handleRemove = async () => { if ( (await confirm({ - header: 'Project Deletion Alert', - confirmation: `Are you sure you want to delete this project? Deleting it will permanently remove the project, along with all associated sessions and data.`, - confirmButton: 'Yes, delete', - cancelButton: 'Cancel', + header: t('Project Deletion Alert'), + confirmation: t( + 'Are you sure you want to delete this project? Deleting it will permanently remove the project, along with all associated sessions and data.', + ), + confirmButton: t('Yes, delete'), + cancelButton: t('Cancel'), })) && site?.id ) { @@ -103,14 +109,14 @@ const NewSiteForm = ({ location: { pathname }, onClose }: Props) => { style={{ width: '350px' }} >

    - {site.exists() ? 'Edit Project' : 'New Project'} + {site.exists() ? t('Edit Project') : t('New Project')}

    - + { /> - +
    { loading={loading} disabled={!site.validate} > - {site?.exists() ? 'Update' : 'Add'} + {site?.exists() ? t('Update') : t('Add')} {site.exists() && ( - )}
    {existsError && (
    - {'Project exists already.'} + {t('Project exists already.')}
    )}
    ); -}; +} export default withRouter(observer(NewSiteForm)); diff --git a/frontend/app/components/Client/Sites/ProjectKey.tsx b/frontend/app/components/Client/Sites/ProjectKey.tsx index a00b32d07..cb35e63e6 100644 --- a/frontend/app/components/Client/Sites/ProjectKey.tsx +++ b/frontend/app/components/Client/Sites/ProjectKey.tsx @@ -1,13 +1,15 @@ import { withCopy } from 'HOCs'; import React from 'react'; -import { Tag } from "antd"; +import { Tag } from 'antd'; function ProjectKey({ value }: any) { - return
    - - {value} - -
    ; + return ( +
    + + {value} + +
    + ); } export default withCopy(ProjectKey); diff --git a/frontend/app/components/Client/Sites/SiteSearch/SiteSearch.tsx b/frontend/app/components/Client/Sites/SiteSearch/SiteSearch.tsx index 9ff6aa454..af288cf60 100644 --- a/frontend/app/components/Client/Sites/SiteSearch/SiteSearch.tsx +++ b/frontend/app/components/Client/Sites/SiteSearch/SiteSearch.tsx @@ -2,34 +2,38 @@ 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; + onChange: (value: string) => void; } function SiteSearch(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 ( -
    - - -
    - ); + const write = ({ target: { name, value } }) => { + debounceUpdate(value); + }; + + return ( +
    + + +
    + ); } -export default SiteSearch; \ No newline at end of file +export default SiteSearch; diff --git a/frontend/app/components/Client/Sites/SiteSearch/index.ts b/frontend/app/components/Client/Sites/SiteSearch/index.ts index 54376cf15..57ae4bed3 100644 --- a/frontend/app/components/Client/Sites/SiteSearch/index.ts +++ b/frontend/app/components/Client/Sites/SiteSearch/index.ts @@ -1 +1 @@ -export { default } from './SiteSearch'; \ No newline at end of file +export { default } from './SiteSearch'; diff --git a/frontend/app/components/Client/Sites/Sites.tsx b/frontend/app/components/Client/Sites/Sites.tsx index c378d6e91..4fc871d07 100644 --- a/frontend/app/components/Client/Sites/Sites.tsx +++ b/frontend/app/components/Client/Sites/Sites.tsx @@ -1,22 +1,30 @@ import React, { useState } from 'react'; -import { Tag } from 'antd'; +import { Tag, Button } from 'antd'; import cn from 'classnames'; -import { Loader, TextLink, NoContent, Pagination, PageTitle, Divider, Icon } from 'UI'; -import { Button } from 'antd'; +import { + Loader, + TextLink, + NoContent, + Pagination, + PageTitle, + Divider, + Icon, +} from 'UI'; import withPageTitle from 'HOCs/withPageTitle'; -import stl from './sites.module.css'; -import NewSiteForm from './NewSiteForm'; -import SiteSearch from './SiteSearch'; -import AddProjectButton from './AddProjectButton'; -import InstallButton from './InstallButton'; -import ProjectKey from './ProjectKey'; import { sliceListPerPage } from 'App/utils'; import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; import { useModal } from 'App/components/Modal'; import CaptureRate from 'Shared/SessionSettings/components/CaptureRate'; import { BranchesOutlined } from '@ant-design/icons'; import { observer } from 'mobx-react-lite'; -import { useStore } from 'App/mstore' +import { useStore } from 'App/mstore'; +import ProjectKey from './ProjectKey'; +import InstallButton from './InstallButton'; +import AddProjectButton from './AddProjectButton'; +import SiteSearch from './SiteSearch'; +import NewSiteForm from './NewSiteForm'; +import stl from './sites.module.css'; +import { useTranslation } from 'react-i18next'; type Project = { id: string; @@ -28,12 +36,13 @@ type Project = { sampleRate: number; }; -const Sites = () => { +function Sites() { + const { t } = useTranslation(); const { projectsStore, userStore } = useStore(); const user = userStore.account; const sites = projectsStore.list; const loading = projectsStore.sitesLoading; - const init = projectsStore.initProject + const init = projectsStore.initProject; const [searchQuery, setSearchQuery] = useState(''); const [showCaptureRate, setShowCaptureRate] = useState(true); const [activeProject, setActiveProject] = useState(null); @@ -42,19 +51,32 @@ const Sites = () => { const isAdmin = user.admin || user.superAdmin; const filteredSites = sites.filter((site: { name: string }) => - site.name.toLowerCase().includes(searchQuery.toLowerCase()) + site.name.toLowerCase().includes(searchQuery.toLowerCase()), ); const { showModal, hideModal } = useModal(); - const EditButton = ({ isAdmin, onClick }: { isAdmin: boolean; onClick: () => void }) => { + function EditButton({ + isAdmin, + onClick, + }: { + isAdmin: boolean; + onClick: () => void; + }) { const _onClick = () => { onClick(); showModal(, { right: true }); }; - return - {project.conditionsCount > 0 ? ( - - ) : null} -
    -
    -
    - + {project.conditionsCount > 0 ? ( + + ) : null}
    -
    - init(project)} /> +
    +
    + +
    +
    + init(project)} /> +
    -
    - ); + ); + } return (
    Projects
    } + title={
    {t('Projects')}
    } actionButton={ { title={
    -
    No matching results
    +
    + {t('No matching results')} +
    } size="small" show={!loading && filteredSites.length === 0} >
    -
    Project Name
    -
    Key
    -
    Capture Rate
    -
    +
    {t('Project Name')}
    +
    {t('Key')}
    +
    {t('Capture Rate')}
    +
    - {sliceListPerPage(filteredSites, page - 1, pageSize).map((project: Project) => ( - - - - - ))} + {sliceListPerPage(filteredSites, page - 1, pageSize).map( + (project: Project) => ( + + + + + ), + )}
    { /> ); -}; +} -export default withPageTitle('Projects - OpenReplay Preferences')(observer(Sites)); +export default withPageTitle('Projects - OpenReplay Preferences')( + observer(Sites), +); diff --git a/frontend/app/components/Client/TabItem.js b/frontend/app/components/Client/TabItem.js index 8d97eb060..cc8b32483 100644 --- a/frontend/app/components/Client/TabItem.js +++ b/frontend/app/components/Client/TabItem.js @@ -1,15 +1,15 @@ import React from 'react'; import { Icon } from 'UI'; -const TabItem = ({ active = false, onClick, icon, label }) => { +function TabItem({ active = false, onClick, icon, label }) { return (
  • - - - { label } + + + {label}
  • ); -}; +} export default TabItem; diff --git a/frontend/app/components/Client/Users/UsersView.tsx b/frontend/app/components/Client/Users/UsersView.tsx index faaa78ab4..9dee542a5 100644 --- a/frontend/app/components/Client/Users/UsersView.tsx +++ b/frontend/app/components/Client/Users/UsersView.tsx @@ -1,22 +1,23 @@ import React, { useEffect } from 'react'; -import UserList from './components/UserList'; import { PageTitle } from 'UI'; import { useStore } from 'App/mstore'; -import { useObserver } from 'mobx-react-lite'; -import UserSearch from './components/UserSearch'; +import { useObserver, observer } from 'mobx-react-lite'; import { useModal } from 'App/components/Modal'; -import UserForm from './components/UserForm'; -import { observer } from 'mobx-react-lite'; -import AddUserButton from './components/AddUserButton'; import withPageTitle from 'HOCs/withPageTitle'; +import UserSearch from './components/UserSearch'; +import UserForm from './components/UserForm'; +import AddUserButton from './components/AddUserButton'; +import UserList from './components/UserList'; +import { useTranslation } from 'react-i18next'; interface Props { isOnboarding?: boolean; } function UsersView({ isOnboarding = false }: Props) { + const { t } = useTranslation(); const { userStore, roleStore } = useStore(); - const account = userStore.account; - const isEnterprise = userStore.isEnterprise; + const { account } = userStore; + const { isEnterprise } = userStore; const userCount = useObserver(() => userStore.list.length); const roles = useObserver(() => roleStore.list); const { showModal } = useModal(); @@ -40,7 +41,8 @@ function UsersView({ isOnboarding = false }: Props) { - Team {userCount} + {t('Team')}  + {userCount}
    } /> @@ -59,4 +61,6 @@ function UsersView({ isOnboarding = false }: Props) { ); } -export default withPageTitle('Team - OpenReplay Preferences')(observer(UsersView)); +export default withPageTitle('Team - OpenReplay Preferences')( + observer(UsersView), +); diff --git a/frontend/app/components/Client/Users/components/AddUserButton/AddUserButton.tsx b/frontend/app/components/Client/Users/components/AddUserButton/AddUserButton.tsx index 92741053d..9a1951b3b 100644 --- a/frontend/app/components/Client/Users/components/AddUserButton/AddUserButton.tsx +++ b/frontend/app/components/Client/Users/components/AddUserButton/AddUserButton.tsx @@ -1,25 +1,37 @@ import React from 'react'; import { Tooltip } from 'UI'; -import { Button } from 'antd' +import { Button } from 'antd'; import { useStore } from 'App/mstore'; import { useObserver } from 'mobx-react-lite'; +import { useTranslation } from 'react-i18next'; +import { TFunction } from 'i18next'; -const PERMISSION_WARNING = 'You don’t have the permissions to perform this action.'; -const LIMIT_WARNING = 'You have reached users limit.'; +const PERMISSION_WARNING = (t: TFunction) => + t('You don’t have the permissions to perform this action.'); +const LIMIT_WARNING = (t: TFunction) => t('You have reached users limit.'); -function AddUserButton({ isAdmin = false, onClick, btnVariant = 'primary' }: any) { +function AddUserButton({ + isAdmin = false, + onClick, + btnVariant = 'primary', +}: any) { + const { t } = useTranslation(); const { userStore } = useStore(); const limtis = useObserver(() => userStore.limits); const cannAddUser = useObserver( - () => isAdmin && (limtis.teamMember === -1 || limtis.teamMember > 0) + () => isAdmin && (limtis.teamMember === -1 || limtis.teamMember > 0), ); return ( - ); diff --git a/frontend/app/components/Client/Users/components/AddUserButton/index.ts b/frontend/app/components/Client/Users/components/AddUserButton/index.ts index 66beb9cf8..96732fa06 100644 --- a/frontend/app/components/Client/Users/components/AddUserButton/index.ts +++ b/frontend/app/components/Client/Users/components/AddUserButton/index.ts @@ -1 +1 @@ -export { default } from './AddUserButton'; \ No newline at end of file +export { default } from './AddUserButton'; diff --git a/frontend/app/components/Client/Users/components/UserForm/UserForm.tsx b/frontend/app/components/Client/Users/components/UserForm/UserForm.tsx index dd495a2c8..3fca31f02 100644 --- a/frontend/app/components/Client/Users/components/UserForm/UserForm.tsx +++ b/frontend/app/components/Client/Users/components/UserForm/UserForm.tsx @@ -1,19 +1,20 @@ import cn from 'classnames'; -import { useObserver } from 'mobx-react-lite'; -import { observer } from 'mobx-react-lite'; +import { useObserver, observer } from 'mobx-react-lite'; import React from 'react'; import { useModal } from 'App/components/Modal'; import { useStore } from 'App/mstore'; import { confirm, CopyButton, Form, Icon, Input } from 'UI'; -import { Button } from 'antd' +import { Button } from 'antd'; import Select from 'Shared/Select'; +import { useTranslation } from 'react-i18next'; function UserForm() { + const { t } = useTranslation(); const { hideModal } = useModal(); const { userStore, roleStore } = useStore(); - const isEnterprise = userStore.isEnterprise; + const { isEnterprise } = userStore; const isSmtp = userStore.account.smtp; const isSaving = userStore.saving; const user: any = userStore.instance || userStore.initUser(); @@ -39,9 +40,11 @@ function UserForm() { const deleteHandler = async () => { if ( await confirm({ - header: 'Confirm', - confirmButton: 'Yes, delete', - confirmation: `Are you sure you want to permanently delete this user?`, + header: t('Confirm'), + confirmButton: t('Yes, delete'), + confirmation: t( + 'Are you sure you want to permanently delete this user?', + ), }) ) { userStore.deleteUser(user.userId).then(() => { @@ -54,13 +57,13 @@ function UserForm() { return useObserver(() => (
    -

    {`${ - user.exists() ? 'Update' : 'Invite' - } User`}

    +

    + {`${user.exists() ? 'Update' : 'Invite'} User`} +

    - +
    - + {!isSmtp && (
    - SMTP is not configured (see{' '} + {t('SMTP is not configured (see')}  - here + {t('here')} {' '} - how to set it up). You can still add new users, but you’d have to - manually copy then send them the invitation link. + {t( + 'how to set it up). You can still add new users, but you’d have to manually copy then send them the invitation link.', + )}
    )} @@ -108,9 +113,9 @@ function UserForm() { className="mt-1" />
    - Admin Privileges + {t('Admin Privileges')}
    - {'Can manage Projects and team members.'} + {t('Can manage Projects and team members.')}
    @@ -118,9 +123,9 @@ function UserForm() { {isEnterprise && ( - + -
    - )); + const write = ({ target: { name, value } }) => { + setQuery(value); + debounceUpdate(name, value); + }; + + return useObserver(() => ( +
    + + +
    + )); } -export default UserSearch; \ No newline at end of file +export default UserSearch; diff --git a/frontend/app/components/Client/Users/components/UserSearch/index.ts b/frontend/app/components/Client/Users/components/UserSearch/index.ts index 3810a4030..5bb176865 100644 --- a/frontend/app/components/Client/Users/components/UserSearch/index.ts +++ b/frontend/app/components/Client/Users/components/UserSearch/index.ts @@ -1 +1 @@ -export { default } from './UserSearch'; \ No newline at end of file +export { default } from './UserSearch'; diff --git a/frontend/app/components/Client/Webhooks/WebhookForm.tsx b/frontend/app/components/Client/Webhooks/WebhookForm.tsx index d3fe7b2c3..3a963c4cd 100644 --- a/frontend/app/components/Client/Webhooks/WebhookForm.tsx +++ b/frontend/app/components/Client/Webhooks/WebhookForm.tsx @@ -5,6 +5,7 @@ import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; import { toast } from 'react-toastify'; import { TrashIcon } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; interface Props { onClose: () => void; @@ -12,8 +13,14 @@ interface Props { } function WebhookForm({ onClose, onDelete }: Props) { + const { t } = useTranslation(); const { settingsStore } = useStore(); - const { webhookInst: webhook, saveWebhook, editWebhook, saving } = settingsStore; + const { + webhookInst: webhook, + saveWebhook, + editWebhook, + saving, + } = settingsStore; const write = ({ target: { value, name } }) => editWebhook({ [name]: value }); const save = () => { @@ -22,35 +29,40 @@ function WebhookForm({ onClose, onDelete }: Props) { onClose(); }) .catch((e) => { - toast.error(e.message || 'Failed to save webhook'); + toast.error(e.message || t('Failed to save webhook')); }); }; return ( - + - - + + - + @@ -64,16 +76,16 @@ function WebhookForm({ onClose, onDelete }: Props) { htmlType="submit" className="float-left mr-2" > - {webhook.exists() ? 'Update' : 'Add'} + {webhook.exists() ? t('Update') : t('Add')} - {webhook.exists() && } + {webhook.exists() && }
    {webhook.exists() && ( + /> )}
    diff --git a/frontend/app/components/Client/Webhooks/Webhooks.tsx b/frontend/app/components/Client/Webhooks/Webhooks.tsx index c83f40526..6db974102 100644 --- a/frontend/app/components/Client/Webhooks/Webhooks.tsx +++ b/frontend/app/components/Client/Webhooks/Webhooks.tsx @@ -1,6 +1,5 @@ import React, { useEffect } from 'react'; import { Loader, NoContent, Icon } from 'UI'; -import WebhookForm from './WebhookForm'; import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; import { toast } from 'react-toastify'; import { useStore } from 'App/mstore'; @@ -10,8 +9,11 @@ import { App, List, Button, Typography, Space } from 'antd'; import { PencilIcon } from '.store/lucide-react-virtual-b029c146a4/package'; import usePageTitle from '@/hooks/usePageTitle'; import { useModal } from 'Components/ModalContext'; +import WebhookForm from './WebhookForm'; +import { useTranslation } from 'react-i18next'; function Webhooks() { + const { t } = useTranslation(); const { settingsStore } = useStore(); const { webhooks, hooksLoading: loading } = settingsStore; const { openModal, closeModal } = useModal(); @@ -24,19 +26,22 @@ function Webhooks() { }, []); const init = (w?: Partial) => { - settingsStore.initWebhook({...w}); - openModal(, { title: w ? 'Edit Webhook' : 'Add Webhook' }); + settingsStore.initWebhook({ ...w }); + openModal(, { + title: w ? t('Edit Webhook') : t('Add Webhook'), + }); }; const removeWebhook = async (id: string) => { modal.confirm({ - title: 'Confirm', - content: 'Are you sure you want to remove this webhook?', + title: t('Confirm'), + content: t('Are you sure you want to remove this webhook?'), onOk: () => { - settingsStore.removeWebhook(id).then(() => toast.success('Webhook removed successfully')); + settingsStore + .removeWebhook(id) + .then(() => toast.success(t('Webhook removed successfully'))); closeModal(); - } + }, }); }; @@ -44,13 +49,21 @@ function Webhooks() {
    - Webhooks + + {t('Webhooks')} + - - Leverage webhook notifications on alerts to trigger custom callbacks. + + + {t( + 'Leverage webhook notifications on alerts to trigger custom callbacks.', + )} +
    - +
    @@ -58,7 +71,7 @@ function Webhooks() { title={
    -
    None added yet
    +
    {t('None added yet')}
    } size="small" @@ -73,7 +86,9 @@ function Webhooks() { className="p-2! group flex justify-between items-center cursor-pointer hover:bg-active-blue transition" > - {w.name} + + {w.name} + {w.endpoint} -
    )} -
    ); } diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/AreaChart.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/AreaChart.tsx index e858f4f2e..bebb3f905 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/AreaChart.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/AreaChart.tsx @@ -1,6 +1,4 @@ import React, { useState } from 'react'; -import CustomTooltip from "./CustomChartTooltip"; -import { Styles } from '../common'; import { ResponsiveContainer, XAxis, @@ -11,6 +9,8 @@ import { Area, Legend, } from 'recharts'; +import CustomTooltip from './CustomChartTooltip'; +import { Styles } from '../common'; interface Props { data: { chart: any[]; namesMap: string[] }; @@ -58,17 +58,15 @@ function CustomAreaChart(props: Props) { > {!hideLegend && ( ({ - value: key, - type: 'line', - color: colors[index], - id: key, - })) - } + payload={data.namesMap.map((key, index) => ({ + value: key, + type: 'line', + color: colors[index], + id: key, + }))} /> )} - + void; } function BigNumChart(props: Props) { @@ -19,8 +24,8 @@ function BigNumChart(props: Props) { hideLegend, } = props; return ( -
    -
    +
    +
    {values.map((val, i) => (
    - ) + ); } -function BigNum({ color, series, value, label, compData, valueLabel, onSeriesFocus, hideLegend }: { - color: string, - series: string, - value: number, - label: string, - compData?: number, - valueLabel?: string, - onSeriesFocus?: (name: string) => void - hideLegend?: boolean +function BigNum({ + color, + series, + value, + label, + compData, + valueLabel, + onSeriesFocus, + hideLegend, +}: { + color: string; + series: string; + value: number; + label: string; + compData?: number; + valueLabel?: string; + onSeriesFocus?: (name: string) => void; + hideLegend?: boolean; }) { - const formattedNumber = (num: number) => { - return Intl.NumberFormat().format(num); - } + const formattedNumber = (num: number) => Intl.NumberFormat().format(num); const changePercent = React.useMemo(() => { if (!compData || compData === 0) return '0'; return `${(((value - compData) / compData) * 100).toFixed(2)}`; - }, [value, compData]) + }, [value, compData]); const change = React.useMemo(() => { if (!compData) return 0; return value - compData; - }, [value, compData]) + }, [value, compData]); return (
    onSeriesFocus?.(series)} className={cn( 'flex flex-col flex-auto justify-center items-center rounded-lg transition-all', - 'hover:transition-all ease-in-out hover:ease-in-out hover:bg-teal/5 hover:cursor-pointer' + 'hover:transition-all ease-in-out hover:ease-in-out hover:bg-teal/5 hover:cursor-pointer', )} > - {hideLegend ? null : -
    -
    + {hideLegend ? null : ( +
    +
    {series}
    - } -
    + )} +
    {formattedNumber(value)} {valueLabel ? `${valueLabel}` : null}
    -
    {label}
    +
    {label}
    {compData ? ( compData} @@ -93,4 +103,4 @@ function BigNum({ color, series, value, label, compData, valueLabel, onSeriesFoc ); } -export default BigNumChart; \ No newline at end of file +export default BigNumChart; diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx index 3eed642e6..8b8db2396 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx @@ -3,24 +3,26 @@ import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; import ClickMapRenderer from 'App/components/Session/Player/ClickMapRenderer'; import { NoContent } from 'App/components/ui'; -import {InfoCircleOutlined} from '@ant-design/icons'; +import { InfoCircleOutlined } from '@ant-design/icons'; +import { useTranslation } from 'react-i18next'; function ClickMapCard() { + const { t } = useTranslation(); const [customSession, setCustomSession] = React.useState(null); const { metricStore, dashboardStore, sessionStore } = useStore(); - const fetchInsights = sessionStore.fetchInsights; - const insights = sessionStore.insights; + const { fetchInsights } = sessionStore; + const { insights } = sessionStore; const onMarkerClick = (s: string, innerText: string) => { metricStore.changeClickMapSearch(s, innerText); }; - const sessionId = metricStore.instance.data.sessionId; + const { sessionId } = metricStore.instance.data; const url = metricStore.instance.data.path; - const operator = metricStore.instance.series[0]?.filter.filters[0]?.operator ? metricStore.instance.series[0].filter.filters[0].operator : 'startsWith' + const operator = metricStore.instance.series[0]?.filter.filters[0]?.operator + ? metricStore.instance.series[0].filter.filters[0].operator + : 'startsWith'; - React.useEffect(() => { - return () => setCustomSession(null); - }, []); + React.useEffect(() => () => setCustomSession(null), []); React.useEffect(() => { if ( @@ -37,7 +39,7 @@ function ClickMapCard() { React.useEffect(() => { if (!url || !sessionId) return; - const rangeValue = dashboardStore.drillDownPeriod.rangeValue; + const { rangeValue } = dashboardStore.drillDownPeriod; const startDate = dashboardStore.drillDownPeriod.start; const endDate = dashboardStore.drillDownPeriod.end; void fetchInsights({ @@ -63,24 +65,26 @@ function ClickMapCard() { style={{ minHeight: 220 }} title={
    - - Set a start point to visualize the heatmap. If set, try adjusting filters. + + {t( + 'Set a start point to visualize the heatmap. If set, try adjusting filters.', + )}
    } - show={true} + show /> ); } if (!metricStore.instance.data?.sessionId || !customSession) { - return
    Loading session
    ; + return
    {t('Loading session')}
    ; } const jumpToEvent = metricStore.instance.data.events.find( (evt: Record) => { if (url) return evt.path.includes(url); return evt; - } + }, ) || { timestamp: metricStore.instance.data.startTs }; const ts = jumpToEvent.timestamp ?? metricStore.instance.data.startTs; const domTime = jumpToEvent.domBuildingTime ?? 0; diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/index.ts b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/index.ts index c72a4090b..c7c0fa79e 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/index.ts +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/index.ts @@ -1 +1 @@ -export { default } from './ClickMapCard' +export { default } from './ClickMapCard'; diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CohortCard/CohortCard.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CohortCard/CohortCard.tsx index cdbbb295c..3a4c18fcf 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CohortCard/CohortCard.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CohortCard/CohortCard.tsx @@ -1,152 +1,175 @@ import React from 'react'; import styles from './CohortCard.module.css'; - +import { useTranslation } from 'react-i18next'; interface Props { - data: any + data: any; } function CohortCard(props: Props) { - // const { data } = props; - const data = [ - { - cohort: '2022-01-01', - users: 100, - data: [100, 95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30, 25, 20, 15, 10, 5], - }, - { - cohort: '2022-01-08', - users: 100, - data: [100, 95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30, 25, 20, 15], - }, - { - cohort: '2022-01-08', - users: 100, - data: [100, 95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30, 25, 20, 15], - }, - { - cohort: '2022-01-08', - users: 100, - data: [100, 95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30], - }, - { - cohort: '2022-01-08', - users: 100, - data: [90, 70, 50, 30], - }, - { - cohort: '2022-01-08', - users: 100, - data: [90, 70, 50, 30], - }, - { - cohort: '2022-01-08', - users: 100, - data: [90, 70, 50, 30], - }, - { - cohort: '2022-01-08', - users: 100, - data: [90, 70, 50, 30], - }, - { - cohort: '2022-01-08', - users: 100, - data: [90, 70, 50, 30], - }, - { - cohort: '2022-01-08', - users: 100, - data: [90, 70, 50, 30], - }, - { - cohort: '2022-01-08', - users: 100, - data: [90, 70, 50, 30], - }, - { - cohort: '2022-01-08', - users: 100, - data: [90, 70, 50, 30], - }, - { - cohort: '2022-01-08', - users: 100, - data: [90, 70, 50, 30], - }, - { - cohort: '2022-01-08', - users: 100, - data: [90, 70, 50, 30], - }, - { - cohort: '2022-01-08', - users: 100, - data: [90, 70, 50, 30], - }, - { - cohort: '2022-01-08', - users: 100, - data: [90, 70, 50, 30], - }, - // ... more rows - ]; + const { t } = useTranslation(); + // const { data } = props; + const data = [ + { + cohort: '2022-01-01', + users: 100, + data: [ + 100, 95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30, 25, 20, 15, + 10, 5, + ], + }, + { + cohort: '2022-01-08', + users: 100, + data: [ + 100, 95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30, 25, 20, 15, + ], + }, + { + cohort: '2022-01-08', + users: 100, + data: [ + 100, 95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30, 25, 20, 15, + ], + }, + { + cohort: '2022-01-08', + users: 100, + data: [100, 95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30], + }, + { + cohort: '2022-01-08', + users: 100, + data: [90, 70, 50, 30], + }, + { + cohort: '2022-01-08', + users: 100, + data: [90, 70, 50, 30], + }, + { + cohort: '2022-01-08', + users: 100, + data: [90, 70, 50, 30], + }, + { + cohort: '2022-01-08', + users: 100, + data: [90, 70, 50, 30], + }, + { + cohort: '2022-01-08', + users: 100, + data: [90, 70, 50, 30], + }, + { + cohort: '2022-01-08', + users: 100, + data: [90, 70, 50, 30], + }, + { + cohort: '2022-01-08', + users: 100, + data: [90, 70, 50, 30], + }, + { + cohort: '2022-01-08', + users: 100, + data: [90, 70, 50, 30], + }, + { + cohort: '2022-01-08', + users: 100, + data: [90, 70, 50, 30], + }, + { + cohort: '2022-01-08', + users: 100, + data: [90, 70, 50, 30], + }, + { + cohort: '2022-01-08', + users: 100, + data: [90, 70, 50, 30], + }, + { + cohort: '2022-01-08', + users: 100, + data: [90, 70, 50, 30], + }, + // ... more rows + ]; - const getCellColor = (value: number) => { - const maxValue = 100; // Adjust this based on the maximum value in your data - const maxOpacity = 0.5; - const opacity = (value / maxValue) * maxOpacity; - return `rgba(62, 170, 175, ${opacity})`; - }; - - return ( -
    -
    - - - - - - - - - - - - - {data.map((row, rowIndex) => ( - - - - - ))} - -
    DateUsers
    {row.cohort}{row.users}
    -
    -
    - - - - - - - {data[0].data.map((_, index) => ( - - ))} - - - - {data.map((row, rowIndex) => ( - - {row.data.map((cell, cellIndex) => ( - - ))} - + const getCellColor = (value: number) => { + const maxValue = 100; // Adjust this based on the maximum value in your data + const maxOpacity = 0.5; + const opacity = (value / maxValue) * maxOpacity; + return `rgba(62, 170, 175, ${opacity})`; + }; + + return ( +
    +
    +
    Weeks later users retained
    {`${index + 1}`}
    {cell}%
    + + + + + + + + + + {data.map((row, rowIndex) => ( + + + + + ))} + +
    {t('Date')}{t('Users')}
    + +
    {row.cohort}{row.users}
    +
    +
    + + + + + + + {data[0].data.map((_, index) => ( + + ))} + + + + {data.map((row, rowIndex) => ( + + {row.data.map((cell, cellIndex) => ( + ))} - -
    + {t('Weeks later users retained')} +
    {`${index + 1}`}
    + {cell}% +
    -
    -
    - ); + + ))} + + +
    +
    + ); } -export default CohortCard; \ No newline at end of file +export default CohortCard; diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CohortCard/index.ts b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CohortCard/index.ts index a570376dd..be99d203f 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CohortCard/index.ts +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CohortCard/index.ts @@ -1 +1 @@ -export { default } from './CohortCard'; \ No newline at end of file +export { default } from './CohortCard'; diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomChartTooltip.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomChartTooltip.tsx index ba76974c7..c73d46e2f 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomChartTooltip.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomChartTooltip.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { formatTimeOrDate } from 'App/date'; import cn from 'classnames'; import { ArrowUp, ArrowDown } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; interface PayloadItem { hide?: boolean; @@ -20,6 +21,7 @@ interface Props { } function CustomTooltip(props: Props) { + const { t } = useTranslation(); const { active, payload, label, hoveredSeries = null } = props; // Return null if tooltip is not active or there is no valid payload @@ -30,7 +32,7 @@ function CustomTooltip(props: Props) { const comparisonPayload = payload.find( (p) => p.name === `${hoveredSeries.replace(' (Comparison)', '')} (Comparison)` || - p.name === `${hoveredSeries} (Comparison)` + p.name === `${hoveredSeries} (Comparison)`, ); if (!currentPayload) return null; @@ -68,9 +70,13 @@ function CustomTooltip(props: Props) { >
    {label},{' '} - {p.payload?.timestamp - ? formatTimeOrDate(p.payload.timestamp) - :
    'Timestamp is not Applicable'
    } + {p.payload?.timestamp ? ( + formatTimeOrDate(p.payload.timestamp) + ) : ( +
    + '{t('Timestamp is not Applicable')}' +
    + )}
    {p.value}
    @@ -97,6 +103,7 @@ export function CompareTag({ absDelta?: number | string | null; delta?: string | null; }) { + const { t } = useTranslation(); return (
    {isHigher === null ? ( -
    No Comparison
    +
    {t('No Comparison')}
    ) : ( <> {!isHigher ? : }
    {absDelta}
    -
    ({delta}%)
    +
    + ({delta} + %) +
    )}
    ); } -export default CustomTooltip; \ No newline at end of file +export default CustomTooltip; diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomLegend.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomLegend.tsx index 17c22b214..08d652160 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomLegend.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomLegend.tsx @@ -7,15 +7,27 @@ interface CustomLegendProps { function CustomLegend({ payload }: CustomLegendProps) { return ( -
    +
    {payload?.map((entry) => ( -
    +
    {entry.value.includes('(Comparison)') ? (
    )} - {entry.value} + {entry.value}
    ))}
    ); } -export default CustomLegend; \ No newline at end of file +export default CustomLegend; diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricOverviewChart/CustomMetricOverviewChart.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricOverviewChart/CustomMetricOverviewChart.tsx index 4b23cea29..365c950e4 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricOverviewChart/CustomMetricOverviewChart.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricOverviewChart/CustomMetricOverviewChart.tsx @@ -1,73 +1,84 @@ -import React from 'react' -import {Styles} from '../../common'; -import {AreaChart, ResponsiveContainer, XAxis, YAxis, Area, Tooltip} from 'recharts'; +import React from 'react'; +import { + AreaChart, + ResponsiveContainer, + XAxis, + YAxis, + Area, + Tooltip, +} from 'recharts'; +import { numberWithCommas } from 'App/utils'; +import { Styles } from '../../common'; import CountBadge from '../../common/CountBadge'; -import {numberWithCommas} from 'App/utils'; interface Props { - data: any; + data: any; } function CustomMetricOverviewChart(props: Props) { - const {data} = props; - const gradientDef = Styles.gradientDef(); + const { data } = props; + const gradientDef = Styles.gradientDef(); - return ( -
    -
    -
    -
    -
    - -
    -
    - - - {gradientDef} - - - - - - + return ( +
    +
    +
    +
    +
    - ) +
    + + + {gradientDef} + + + + + + +
    + ); } -export default CustomMetricOverviewChart - +export default CustomMetricOverviewChart; const countView = (avg: any, unit: any) => { - if (unit === 'mb') { - if (!avg) return 0; - const count = Math.trunc(avg / 1024 / 1024); - return numberWithCommas(count); - } - if (unit === 'min') { - if (!avg) return 0; - const count = Math.trunc(avg); - return numberWithCommas(count > 1000 ? count + 'k' : count); - } - return avg ? numberWithCommas(avg) : 0; -} + if (unit === 'mb') { + if (!avg) return 0; + const count = Math.trunc(avg / 1024 / 1024); + return numberWithCommas(count); + } + if (unit === 'min') { + if (!avg) return 0; + const count = Math.trunc(avg); + return numberWithCommas(count > 1000 ? `${count}k` : count); + } + return avg ? numberWithCommas(avg) : 0; +}; diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricOverviewChart/index.ts b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricOverviewChart/index.ts index 2aa2ad492..ccf833592 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricOverviewChart/index.ts +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricOverviewChart/index.ts @@ -1 +1 @@ -export { default } from './CustomMetricOverviewChart'; \ No newline at end of file +export { default } from './CustomMetricOverviewChart'; diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPercentage/CustomMetricPercentage.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPercentage/CustomMetricPercentage.tsx index 916911d84..57a5cd928 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPercentage/CustomMetricPercentage.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPercentage/CustomMetricPercentage.tsx @@ -1,21 +1,26 @@ -import React from 'react' +import React from 'react'; import { numberWithCommas } from 'App/utils'; +import { useTranslation } from 'react-i18next'; interface Props { - data: any; - params: any; - colors: any; - onClick?: (event, index) => void; + data: any; + params: any; + colors: any; + onClick?: (event, index) => void; } function CustomMetriPercentage(props: Props) { - const { data = {} } = props; - return ( -
    -
    {numberWithCommas(data.count)}
    -
    {`${parseInt(data.previousCount || 0)} ( ${Math.floor(parseInt(data.countProgress || 0))}% )`}
    -
    from previous period.
    -
    - ) + const { t } = useTranslation(); + const { data = {} } = props; + return ( +
    +
    {numberWithCommas(data.count)}
    +
    {`${parseInt(data.previousCount || 0)} ( ${Math.floor(parseInt(data.countProgress || 0))}% )`}
    +
    {t('from previous period.')}
    +
    + ); } export default CustomMetriPercentage; diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPercentage/index.ts b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPercentage/index.ts index 0e1f80170..9ca36cb4a 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPercentage/index.ts +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPercentage/index.ts @@ -1 +1 @@ -export { default } from './CustomMetricPercentage'; \ No newline at end of file +export { default } from './CustomMetricPercentage'; diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart/CustomMetricPieChart.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart/CustomMetricPieChart.tsx index b0d8029c0..6641fde9d 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart/CustomMetricPieChart.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart/CustomMetricPieChart.tsx @@ -1,11 +1,17 @@ import React, { useState } from 'react'; -import { ResponsiveContainer, Tooltip } from 'recharts'; -import { PieChart, Pie, Cell, Legend } from 'recharts'; -import { Styles } from '../../common'; +import { + ResponsiveContainer, + Tooltip, + PieChart, + Pie, + Cell, + Legend, +} from 'recharts'; import { NoContent } from 'UI'; import { filtersMap } from 'Types/filter/newFilter'; import { numberWithCommas } from 'App/utils'; -import CustomTooltip from '../CustomChartTooltip'; +import { Styles } from '../../common'; +import CustomTooltip from '../CustomChartTooltip'; interface Props { metric: { @@ -29,7 +35,7 @@ function CustomMetricPieChart(props: Props) { const onClickHandler = (event) => { if (event && !event.payload.group) { const filters = Array(); - let filter = { ...filtersMap[metric.metricOf] }; + const filter = { ...filtersMap[metric.metricOf] }; filter.value = [event.payload.name]; filter.type = filter.key; delete filter.key; @@ -57,7 +63,7 @@ function CustomMetricPieChart(props: Props) { const highest = values.reduce( (acc, curr) => (acc.value > curr.value ? acc : curr), - { name: '', value: 0 } + { name: '', value: 0 }, ); return ( @@ -69,10 +75,11 @@ function CustomMetricPieChart(props: Props) { > - - } + + } /> { const RADIAN = Math.PI / 180; - let radius1 = 15 + innerRadius + (outerRadius - innerRadius); - let radius2 = innerRadius + (outerRadius - innerRadius); - let x2 = cx + radius1 * Math.cos(-midAngle * RADIAN); - let y2 = cy + radius1 * Math.sin(-midAngle * RADIAN); - let x1 = cx + radius2 * Math.cos(-midAngle * RADIAN); - let y1 = cy + radius2 * Math.sin(-midAngle * RADIAN); + const radius1 = 15 + innerRadius + (outerRadius - innerRadius); + const radius2 = innerRadius + (outerRadius - innerRadius); + const x2 = cx + radius1 * Math.cos(-midAngle * RADIAN); + const y2 = cy + radius1 * Math.sin(-midAngle * RADIAN); + const x1 = cx + radius2 * Math.cos(-midAngle * RADIAN); + const y1 = cy + radius2 * Math.sin(-midAngle * RADIAN); const percentage = (value * 100) / highest.value; @@ -127,12 +134,12 @@ function CustomMetricPieChart(props: Props) { index, }) => { const RADIAN = Math.PI / 180; - let radius = 20 + innerRadius + (outerRadius - innerRadius); - let x = cx + radius * Math.cos(-midAngle * RADIAN); - let y = cy + radius * Math.sin(-midAngle * RADIAN); + const radius = 20 + innerRadius + (outerRadius - innerRadius); + const x = cx + radius * Math.cos(-midAngle * RADIAN); + const y = cy + radius * Math.sin(-midAngle * RADIAN); const percentage = (value / highest.value) * 100; let name = values[index].name || 'Unidentified'; - name = name.length > 20 ? name.substring(0, 20) + '...' : name; + name = name.length > 20 ? `${name.substring(0, 20)}...` : name; if (percentage < 3) { return null; } @@ -164,4 +171,4 @@ function CustomMetricPieChart(props: Props) { ); } -export default CustomMetricPieChart; \ No newline at end of file +export default CustomMetricPieChart; diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart/index.ts b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart/index.ts index 6bdaf2270..5250c9eb9 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart/index.ts +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart/index.ts @@ -1 +1 @@ -export { default } from './CustomMetricPieChart'; \ No newline at end of file +export { default } from './CustomMetricPieChart'; diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTable/CustomMetricTable.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTable/CustomMetricTable.tsx index d876ee5ba..7571962e7 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTable/CustomMetricTable.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTable/CustomMetricTable.tsx @@ -1,79 +1,84 @@ -import React from 'react' -import {Table} from '../../common'; -import {List} from 'immutable'; -import {filtersMap} from 'Types/filter/newFilter'; -import {NoContent, Icon} from 'UI'; -import {tableColumnName} from 'App/constants/filterOptions'; -import {numberWithCommas} from 'App/utils'; +import React from 'react'; +import { List } from 'immutable'; +import { filtersMap } from 'Types/filter/newFilter'; +import { NoContent, Icon } from 'UI'; +import { tableColumnName } from 'App/constants/filterOptions'; +import { numberWithCommas } from 'App/utils'; +import { Table } from '../../common'; +import { useTranslation } from 'react-i18next'; -const getColumns = (metric) => { - return [ - { - key: 'name', - title: tableColumnName[metric.metricOf], - toText: name => name || 'Unidentified', - width: '70%', - icon: true, - }, - { - key: 'sessionCount', - title: 'Sessions', - toText: sessions => numberWithCommas(sessions), - width: '30%', - }, - ] -} +const getColumns = (metric) => [ + { + key: 'name', + title: tableColumnName[metric.metricOf], + toText: (name) => name || 'Unidentified', + width: '70%', + icon: true, + }, + { + key: 'sessionCount', + title: 'Sessions', + toText: (sessions) => numberWithCommas(sessions), + width: '30%', + }, +]; interface Props { - metric?: any, - data: any; - onClick?: (filters: any) => void; - isTemplate?: boolean; + metric?: any; + data: any; + onClick?: (filters: any) => void; + isTemplate?: boolean; } function CustomMetricTable(props: Props) { - const {metric = {}, data = {values: []}, onClick = () => null, isTemplate} = props; - const rows = List(data.values); + const { + metric = {}, + data = { values: [] }, + onClick = () => null, + isTemplate, + } = props; + const { t } = useTranslation(); + const rows = List(data.values); - const onClickHandler = (event: any, data: any) => { - const filters = Array(); - let filter = {...filtersMap[metric.metricOf]} - filter.value = [data.name] - filter.type = filter.key - delete filter.key - delete filter.operatorOptions - delete filter.category - delete filter.icon - delete filter.label - delete filter.options + const onClickHandler = (event: any, data: any) => { + const filters = Array(); + const filter = { ...filtersMap[metric.metricOf] }; + filter.value = [data.name]; + filter.type = filter.key; + delete filter.key; + delete filter.operatorOptions; + delete filter.category; + delete filter.icon; + delete filter.label; + delete filter.options; - filters.push(filter); - onClick(filters); - } - return ( -
    - - - No data available for the selected period. -
    - } - > - - - - ); + filters.push(filter); + onClick(filters); + }; + return ( +
    + + + {t('No data available for the selected period.')} +
    + } + > +
    + + + ); } export default CustomMetricTable; diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTable/index.ts b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTable/index.ts index dc43c93b4..02e170234 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTable/index.ts +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTable/index.ts @@ -1 +1 @@ -export { default } from './CustomMetricTable'; \ No newline at end of file +export { default } from './CustomMetricTable'; diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableErrors/CustomMetricTableErrors.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableErrors/CustomMetricTableErrors.tsx index 75c70aaef..3b88c6433 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableErrors/CustomMetricTableErrors.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableErrors/CustomMetricTableErrors.tsx @@ -4,93 +4,91 @@ import ErrorListItem from "App/components/Dashboard/components/Errors/ErrorListI import { withRouter, RouteComponentProps } from "react-router-dom"; import { useModal } from "App/components/Modal"; import ErrorDetailsModal from "App/components/Dashboard/components/Errors/ErrorDetailsModal"; +import { useTranslation } from "react-i18next"; interface Props { - metric: any; - data: any; - isEdit: any; - history: any; - location: any; + metric: any; + data: any; + isEdit: any; + history: any; + location: any; } function CustomMetricTableErrors(props: RouteComponentProps & Props) { const { metric, data } = props; const errorId = new URLSearchParams(props.location.search).get("errorId"); const { showModal, hideModal } = useModal(); - const onErrorClick = (e: any, error: any) => { - e.stopPropagation(); - props.history.replace({ - search: new URLSearchParams({ errorId: error.errorId }).toString(), - }); + const onErrorClick = (e: any, error: any) => { + e.stopPropagation(); + props.history.replace({ + search: new URLSearchParams({ errorId: error.errorId }).toString(), + }); + }; + + useEffect(() => { + if (!errorId) return; + + showModal(, { + right: true, + width: 1200, + onClose: () => { + if ( + props.history.location.pathname.includes('/dashboard') || + props.history.location.pathname.includes('/metrics/') + ) { + props.history.replace({ search: '' }); + } + }, + }); + + return () => { + hideModal(); }; + }, [errorId]); - useEffect(() => { - if (!errorId) return; - - showModal(, { - right: true, - width: 1200, - onClose: () => { - if (props.history.location.pathname.includes("/dashboard") || props.history.location.pathname.includes("/metrics/")) { - props.history.replace({ search: "" }); - } - }, - }); - - return () => { - hideModal(); - }; - }, [errorId]); - - return ( - No data available for the selected period.} - show={!data.errors || data.errors.length === 0} - size="small" - style={{ minHeight: 220 }} - > -
    - {data.errors && - data.errors.map((error: any, index: any) => ( -
    - onErrorClick(e, error)} - /> -
    - ))} - - {/*{isEdit && (*/} -
    - - metric.updateKey("page", page) - } - limit={5} - debounceRequest={500} - /> -
    - {/*)}*/} - - {/*{!isEdit && (*/} - {/* */} - {/*)}*/} + return ( + + + {t('No data available for the selected period.')} +
    + } + show={!data.errors || data.errors.length === 0} + size="small" + style={{ minHeight: 220 }} + > +
    + {data.errors && + data.errors.map((error: any, index: any) => ( +
    + onErrorClick(e, error)} + />
    - - ); + ))} + + {/* {isEdit && ( */} +
    + metric.updateKey('page', page)} + limit={5} + debounceRequest={500} + /> +
    + {/* )} */} + + {/* {!isEdit && ( */} + {/* */} + {/* )} */} +
    +
    + ); } -export default withRouter(CustomMetricTableErrors); - -const ViewMore = ({ total, limit }: any) => - total > limit && ( -
    -
    -
    - All {total} errors -
    -
    -
    - ); +export default withRouter( + CustomMetricTableErrors, +); diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableErrors/index.ts b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableErrors/index.ts index 78590d267..2ab082bc3 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableErrors/index.ts +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableErrors/index.ts @@ -1 +1 @@ -export { default } from './CustomMetricTableErrors'; \ No newline at end of file +export { default } from './CustomMetricTableErrors'; diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions/CustomMetricTableSessions.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions/CustomMetricTableSessions.tsx index 5622b8139..c9ae9791f 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions/CustomMetricTableSessions.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions/CustomMetricTableSessions.tsx @@ -5,6 +5,7 @@ import { Pagination, NoContent } from 'UI'; import { useStore } from 'App/mstore'; import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; import Session from 'App/mstore/types/session'; +import { useTranslation } from 'react-i18next'; interface Props { metric: any; @@ -14,11 +15,16 @@ interface Props { } function CustomMetricTableSessions(props: Props) { + const { t } = useTranslation(); const { isEdit = false, metric, data } = props; - const sessions = useMemo(() => { - return data && data.sessions ? data.sessions.map((session: any) => new Session().fromJson(session)) : []; - }, []); + const sessions = useMemo( + () => + data && data.sessions + ? data.sessions.map((session: any) => new Session().fromJson(session)) + : [], + [], + ); return useObserver(() => (
    - No relevant sessions found for the selected time period + {t('No relevant sessions found for the selected time period')}
    } @@ -62,13 +68,16 @@ function CustomMetricTableSessions(props: Props) { export default observer(CustomMetricTableSessions); -const ViewMore = ({ total, limit }: any) => - total > limit ? ( +function ViewMore({ total, limit }: any) { + const { t } = useTranslation(); + return total > limit ? (
    - All {total} sessions + {t('All')} {total}  + {t('sessions')}
    ) : null; +} diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions/index.ts b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions/index.ts index 46889345c..f678a07a5 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions/index.ts +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions/index.ts @@ -1 +1 @@ -export { default } from './CustomMetricTableSessions'; \ No newline at end of file +export { default } from './CustomMetricTableSessions'; diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/InsightItem.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/InsightItem.tsx index 9e845c1e8..7858409c2 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/InsightItem.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/InsightItem.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { Icon } from 'UI'; import cn from 'classnames'; import { numberWithCommas } from 'App/utils'; +import { useTranslation } from 'react-i18next'; interface Props { item: any; @@ -17,11 +18,15 @@ function InsightItem(props: Props) { case IssueCategory.RAGE: return ; case IssueCategory.RESOURCES: - return ; + return ( + + ); case IssueCategory.ERRORS: return ; case IssueCategory.NETWORK: - return ; + return ( + + ); default: return null; } @@ -48,30 +53,36 @@ function Change({ change, isIncreased, unit = '%' }: any) { } function ErrorItem({ item, className, onClick }: any) { + const { t } = useTranslation(); return (
    - + {item.isNew ? (
    -
    Users are encountering a new error called:
    +
    {t('Users are encountering a new error called:')}
    {item.name}
    -
    This error has occurred a total of
    +
    {t('This error has occurred a total of')}
    {item.value}
    -
    times
    +
    {t('times')}
    ) : (
    -
    There has been an
    -
    {item.isIncreased ? 'increase' : 'decrease'}
    -
    in the error
    +
    {t('There has been an')}
    +
    {item.isIncreased ? t('increase') : t('decrease')}
    +
    {t('in the error')}
    {item.name}
    -
    from
    +
    {t('from')}
    {item.oldValue}
    -
    to
    +
    {t('to')}
    {item.value},
    -
    representing a
    +
    {t('representing a')}
    -
    across all sessions.
    +
    {t('across all sessions.')}
    )}
    @@ -79,29 +90,48 @@ function ErrorItem({ item, className, onClick }: any) { } function NetworkItem({ item, className, onClick }: any) { + const { t } = useTranslation(); return (
    - +
    -
    Network request to path
    +
    {t('Network request to path')}
    {item.name}
    -
    has {item.change > 0 ? 'increased' : 'decreased'}
    - +
    + {t('has')} + {item.change > 0 ? t('increased') : t('decreased')} +
    +
    ); } function ResourcesItem({ item, className, onClick }: any) { + const { t } = useTranslation(); return (
    - +
    -
    There has been
    +
    {t('There has been')}
    {item.change > 0 ? 'Increase' : 'Decrease'}
    -
    in
    +
    {t('in')}
    {item.name}
    -
    usage by
    +
    {t('usage by')}
    @@ -109,26 +139,34 @@ function ResourcesItem({ item, className, onClick }: any) { } function RageItem({ item, className, onClick }: any) { + const { t } = useTranslation(); return (
    - + {item.isNew ? (
    -
    New Click Rage detected
    +
    {t('New Click Rage detected')}
    {item.value}
    -
    times on
    +
    {t('times on')}
    {item.name}
    ) : (
    -
    Click rage has
    -
    {item.isIncreased ? 'increased' : 'decreased'} on
    +
    {t('Click rage has')}
    +
    + {item.isIncreased ? 'increased' : 'decreased'} {t('on')} +
    {item.name}
    -
    passing from
    +
    {t('passing from')}
    {item.oldValue}
    -
    to
    +
    {t('to')}
    {item.value}
    -
    representing a
    +
    {t('representing a')}
    )} diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/InsightsCard.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/InsightsCard.tsx index b3219ae3b..0f28719d7 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/InsightsCard.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/InsightsCard.tsx @@ -2,23 +2,34 @@ import { NoContent, Icon } from 'UI'; import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; import React from 'react'; -import InsightItem from './InsightItem'; import { InishtIssue } from 'App/mstore/types/widget'; import FilterItem from 'App/mstore/types/filterItem'; -import { FilterKey, IssueCategory, IssueType } from 'App/types/filter/filterType'; +import { + FilterKey, + IssueCategory, + IssueType, +} from 'App/types/filter/filterType'; import { filtersMap } from 'Types/filter/newFilter'; +import InsightItem from './InsightItem'; +import { useTranslation } from 'react-i18next'; function InsightsCard({ data }: any) { const { dashboardStore } = useStore(); - const drillDownFilter = dashboardStore.drillDownFilter; + const { drillDownFilter } = dashboardStore; + const { t } = useTranslation(); - const clickHanddler = (e: React.MouseEvent, item: InishtIssue) => { + const clickHanddler = ( + e: React.MouseEvent, + item: InishtIssue, + ) => { let filter: any = {}; switch (item.category) { case IssueCategory.RESOURCES: filter = { ...filtersMap[ - item.name === IssueType.MEMORY ? FilterKey.AVG_MEMORY_USAGE : FilterKey.AVG_CPU_LOAD + item.name === IssueType.MEMORY + ? FilterKey.AVG_MEMORY_USAGE + : FilterKey.AVG_CPU_LOAD ], }; filter.source = [item.oldValue]; @@ -61,7 +72,7 @@ function InsightsCard({ data }: any) { title={
    - No data available for the selected period. + {t('No data available for the selected period.')}
    } show={data.issues && data.issues.length === 0} @@ -69,7 +80,11 @@ function InsightsCard({ data }: any) {
    {data.issues && data.issues.map((item: any) => ( - clickHanddler(e, item)} /> + clickHanddler(e, item)} + /> ))}
    diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/index.ts b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/index.ts index bd85f2e4b..401ea42ba 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/index.ts +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/index.ts @@ -1 +1 @@ -export { default } from './InsightsCard' \ No newline at end of file +export { default } from './InsightsCard'; diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/SessionsBy.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/SessionsBy.tsx index b54b8c563..baa26b6ea 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/SessionsBy.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/SessionsBy.tsx @@ -1,12 +1,11 @@ import React from 'react'; -import { Button, Space } from 'antd'; +import { Button, Space, Empty } from 'antd'; import { filtersMap } from 'Types/filter/newFilter'; -import { Empty } from 'antd'; -import { Info } from 'lucide-react'; -import { ArrowRight } from 'lucide-react'; +import { Info, ArrowRight } from 'lucide-react'; import CardSessionsByList from 'Components/Dashboard/Widgets/CardSessionsByList'; import { useModal } from 'Components/ModalContext'; import Widget from '@/mstore/types/widget'; +import { useTranslation } from 'react-i18next'; interface Props { metric?: any; @@ -16,11 +15,20 @@ interface Props { } function SessionsBy(props: Props) { - const { metric = {}, data = { values: [] }, onClick = () => null, isTemplate } = props; + const { + metric = {}, + data = { values: [] }, + onClick = () => null, + isTemplate, + } = props; + const { t } = useTranslation(); const [selected, setSelected] = React.useState(null); - const total = data.total; + const { total } = data; const { openModal, closeModal } = useModal(); - const modalMetric = React.useMemo(() => Object.assign(new Widget(), metric), [metric]); + const modalMetric = React.useMemo( + () => Object.assign(new Widget(), metric), + [metric], + ); const onClickHandler = (event: any, data: any) => { const baseFilter = { @@ -28,12 +36,28 @@ function SessionsBy(props: Props) { value: [data.name], type: filtersMap[metric.metricOf].key, filters: filtersMap[metric.metricOf].filters?.map((f: any) => { - const { key, operatorOptions, category, icon, label, options, ...cleaned } = f; + const { + key, + operatorOptions, + category, + icon, + label, + options, + ...cleaned + } = f; return { ...cleaned, type: f.key, value: [] }; - }) + }), }; - const { key, operatorOptions, category, icon, label, options, ...finalFilter } = baseFilter; + const { + key, + operatorOptions, + category, + icon, + label, + options, + ...finalFilter + } = baseFilter; setSelected(data.name); onClick([finalFilter]); @@ -43,17 +67,21 @@ function SessionsBy(props: Props) { e.stopPropagation(); openModal( { closeModal(); onClickHandler(null, item); - }} selected={selected} />, { + }} + selected={selected} + />, + { title: metric.name, - width: 600 - }); + width: 600, + }, + ); }; return ( @@ -67,20 +95,27 @@ function SessionsBy(props: Props) { description={
    - No data available for the selected period. + {t('No data available for the selected period.')}
    } /> ) : ( -
    - {metric && } +
    + {metric && ( + + )} {total > 3 && (
    diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/index.ts b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/index.ts index 54d9a4192..d46de225a 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/index.ts +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/index.ts @@ -1 +1 @@ -export { default } from './CustomMetricsWidgets'; \ No newline at end of file +export { default } from './CustomMetricsWidgets'; diff --git a/frontend/app/components/Dashboard/Widgets/ListWithIcons.tsx b/frontend/app/components/Dashboard/Widgets/ListWithIcons.tsx index 958329dfa..06ec63c2f 100644 --- a/frontend/app/components/Dashboard/Widgets/ListWithIcons.tsx +++ b/frontend/app/components/Dashboard/Widgets/ListWithIcons.tsx @@ -25,18 +25,23 @@ function ListWithIcons({ list = [] }: Props) { style={{ borderBottom: '1px dotted rgba(0, 0, 0, 0.05)', padding: '4px 10px', - lineHeight: '1px' + lineHeight: '1px', }} className={cn('rounded')} // Remove hover:bg-active-blue and cursor-pointer >
    - {row.name} - {row.value} + + {row.name} + + + {' '} + {row.value} +
    - )} + } /> )} diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CPULoad/CPULoad.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CPULoad/CPULoad.tsx index 1bd165878..0f59f35e1 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CPULoad/CPULoad.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CPULoad/CPULoad.tsx @@ -1,63 +1,73 @@ import React from 'react'; import { NoContent, Icon } from 'UI'; +import { + AreaChart, + Area, + CartesianGrid, + Tooltip, + ResponsiveContainer, + XAxis, + YAxis, +} from 'recharts'; import { Styles } from '../../common'; -import { - AreaChart, Area, - CartesianGrid, Tooltip, - ResponsiveContainer, - XAxis, YAxis - } from 'recharts'; +import { useTranslation } from 'react-i18next'; interface Props { - data: any - metric?: any + data: any; + metric?: any; } function CPULoad(props: Props) { - const { data, metric } = props; - const gradientDef = Styles.gradientDef(); + const { data, metric } = props; + const gradientDef = Styles.gradientDef(); + const { t } = useTranslation(); - return ( - - - No data available for the selected period. -
    - } - show={ metric.data.chart && metric.data.chart.length === 0 } - style={ { height: '240px' } } - > - - - {gradientDef} - - - Styles.tickFormatter(val)} - label={{ ...Styles.axisLabelLeft, value: "CPU Load (%)" }} - /> - - - - - - ); + return ( + + + {t('No data available for the selected period.')} +
    + } + show={metric.data.chart && metric.data.chart.length === 0} + style={{ height: '240px' }} + > + + + {gradientDef} + + + Styles.tickFormatter(val)} + label={{ ...Styles.axisLabelLeft, value: 'CPU Load (%)' }} + /> + + + + + + ); } export default CPULoad; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CPULoad/index.ts b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CPULoad/index.ts index 37cec8b40..ece44323e 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CPULoad/index.ts +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CPULoad/index.ts @@ -1 +1 @@ -export { default } from './CPULoad' \ No newline at end of file +export { default } from './CPULoad'; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallWithErrors/CallWithErrors.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallWithErrors/CallWithErrors.tsx index 84b6472e9..45be5fbf0 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallWithErrors/CallWithErrors.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallWithErrors/CallWithErrors.tsx @@ -1,14 +1,14 @@ import React from 'react'; import { NoContent } from 'UI'; -import { Table } from '../../common'; import { getRE } from 'App/utils'; -import ImageInfo from './ImageInfo'; -import MethodType from './MethodType'; import cn from 'classnames'; -import stl from './callWithErrors.module.css'; -import { NO_METRIC_DATA } from 'App/constants/messages' +import { NO_METRIC_DATA } from 'App/constants/messages'; import { List } from 'immutable'; -import { InfoCircleOutlined } from '@ant-design/icons' +import { InfoCircleOutlined } from '@ant-design/icons'; +import stl from './callWithErrors.module.css'; +import MethodType from './MethodType'; +import ImageInfo from './ImageInfo'; +import { Table } from '../../common'; const cols = [ { @@ -42,47 +42,55 @@ const cols = [ title: '5xx', className: 'text-left', width: '15%', - } + }, ]; interface Props { - data: any - isTemplate?: boolean + data: any; + isTemplate?: boolean; } function CallWithErrors(props: Props) { - const { data } = props; - const [search, setSearch] = React.useState('') - const test = (value = '', serach: any) => getRE(serach, 'i').test(value); - const _data = search ? data.chart.filter((i: any) => test(i.urlHostpath, search)) : data.chart; + const { data } = props; + const [search, setSearch] = React.useState(''); + const test = (value = '', serach: any) => getRE(serach, 'i').test(value); + const _data = search + ? data.chart.filter((i: any) => test(i.urlHostpath, search)) + : data.chart; - const write = ({ target: { name, value } }: any) => { - setSearch(value) - }; + const write = ({ target: { name, value } }: any) => { + setSearch(value); + }; - return ( - - { NO_METRIC_DATA } -
    - } - show={ data.chart.length === 0 } - style={{ height: '240px'}} - > -
    -
    - -
    -
    - - - ); + return ( + + {NO_METRIC_DATA} + + } + show={data.chart.length === 0} + style={{ height: '240px' }} + > +
    +
    + +
    +
    + + + ); } export default CallWithErrors; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallWithErrors/Chart.js b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallWithErrors/Chart.js index 11f184df2..cae02fc10 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallWithErrors/Chart.js +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallWithErrors/Chart.js @@ -2,12 +2,18 @@ import React from 'react'; import { AreaChart, Area } from 'recharts'; import { Styles } from '../../common'; -const Chart = ({ data, compare }) => { +function Chart({ data, compare }) { const colors = compare ? Styles.compareColors : Styles.colors; return ( - - + + ); } diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallWithErrors/ImageInfo.js b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallWithErrors/ImageInfo.js index 80a2010b9..712e8fddd 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallWithErrors/ImageInfo.js +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallWithErrors/ImageInfo.js @@ -2,11 +2,13 @@ import React from 'react'; import { TextEllipsis } from 'UI'; import styles from './imageInfo.module.css'; -const ImageInfo = ({ data }) => ( -
    - -
    -); +function ImageInfo({ data }) { + return ( +
    + +
    + ); +} ImageInfo.displayName = 'ImageInfo'; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallWithErrors/MethodType.js b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallWithErrors/MethodType.js index 029894fdb..e9da55110 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallWithErrors/MethodType.js +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallWithErrors/MethodType.js @@ -1,12 +1,12 @@ import React from 'react'; import { Tag } from 'antd'; -const MethodType = ({ data }) => { +function MethodType({ data }) { return ( {data.method} ); -}; +} export default MethodType; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallWithErrors/index.ts b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallWithErrors/index.ts index 4d3ba4df8..6b0db1bd0 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallWithErrors/index.ts +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallWithErrors/index.ts @@ -1 +1 @@ -export { default } from './CallWithErrors' \ No newline at end of file +export { default } from './CallWithErrors'; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors4xx/CallsErrors4xx.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors4xx/CallsErrors4xx.tsx index 56b821045..f827dd220 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors4xx/CallsErrors4xx.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors4xx/CallsErrors4xx.tsx @@ -1,57 +1,74 @@ import React from 'react'; import { NoContent } from 'UI'; -import { Styles } from '../../common'; import { - CartesianGrid, Tooltip, - LineChart, Line, Legend, ResponsiveContainer, - XAxis, YAxis - } from 'recharts'; + CartesianGrid, + Tooltip, + LineChart, + Line, + Legend, + ResponsiveContainer, + XAxis, + YAxis, +} from 'recharts'; import { NO_METRIC_DATA } from 'App/constants/messages'; import { InfoCircleOutlined } from '@ant-design/icons'; +import { Styles } from '../../common'; interface Props { - data: any - metric?: any + data: any; + metric?: any; } function CallsErrors4xx(props: Props) { - const { data, metric } = props; - return ( - - { NO_METRIC_DATA } + const { data, metric } = props; + return ( + + {NO_METRIC_DATA} - } - show={ metric.data.chart.length === 0 } - style={ { height: '240px' } } - > - - - - + + + + + + {/* */} + + {Array.isArray(metric.data.namesMap) && + metric.data.namesMap.map((key, index) => ( + - - {/**/} - - { Array.isArray(metric.data.namesMap) && metric.data.namesMap.map((key, index) => ( - - ))} - - - - ); + ))} + + + + ); } export default CallsErrors4xx; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors4xx/index.ts b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors4xx/index.ts index a21e4a950..78fa75b53 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors4xx/index.ts +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors4xx/index.ts @@ -1 +1 @@ -export { default } from './CallsErrors4xx' \ No newline at end of file +export { default } from './CallsErrors4xx'; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors5xx/CallsErrors5xx.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors5xx/CallsErrors5xx.tsx index 7a8fa1150..1feeb041c 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors5xx/CallsErrors5xx.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors5xx/CallsErrors5xx.tsx @@ -1,55 +1,75 @@ import React from 'react'; import { NoContent, Icon } from 'UI'; -import { Styles } from '../../common'; import { - CartesianGrid, Tooltip, - LineChart, Line, Legend, ResponsiveContainer, - XAxis, YAxis - } from 'recharts'; + CartesianGrid, + Tooltip, + LineChart, + Line, + Legend, + ResponsiveContainer, + XAxis, + YAxis, +} from 'recharts'; +import { Styles } from '../../common'; +import { useTranslation } from 'react-i18next'; interface Props { - data: any - metric?: any + data: any; + metric?: any; } function CallsErrors5xx(props: Props) { - const { data, metric } = props; - return ( - - - No data available for the selected period. - - } - show={ metric.data.chart.length === 0 } - style={ { height: '240px' } } - > - - - - + + {t('No data available for the selected period.')} + + } + show={metric.data.chart.length === 0} + style={{ height: '240px' }} + > + + + + + + {/* */} + + {Array.isArray(metric.data.namesMap) && + metric.data.namesMap.map((key, index) => ( + - - {/**/} - - { Array.isArray(metric.data.namesMap) && metric.data.namesMap.map((key, index) => ( - - ))} - - - - ); + ))} + + + + ); } export default CallsErrors5xx; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors5xx/index.ts b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors5xx/index.ts index 661204c0d..61b9900d7 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors5xx/index.ts +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors5xx/index.ts @@ -1 +1 @@ -export { default } from './CallsErrors5xx' \ No newline at end of file +export { default } from './CallsErrors5xx'; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/Crashes/Crashes.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/Crashes/Crashes.tsx index 6d260dfac..4fccd1469 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/Crashes/Crashes.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/Crashes/Crashes.tsx @@ -1,62 +1,70 @@ import React from 'react'; import { NoContent } from 'UI'; -import { Styles } from '../../common'; -import { - AreaChart, Area, - CartesianGrid, Tooltip, - ResponsiveContainer, - XAxis, YAxis - } from 'recharts'; +import { + AreaChart, + Area, + CartesianGrid, + Tooltip, + ResponsiveContainer, + XAxis, + YAxis, +} from 'recharts'; import { NO_METRIC_DATA } from 'App/constants/messages'; import { InfoCircleOutlined } from '@ant-design/icons'; +import { Styles } from '../../common'; interface Props { - data: any - metric?: any + data: any; + metric?: any; } function Crashes(props: Props) { - const { data, metric } = props; - const gradientDef = Styles.gradientDef(); - return ( - - { NO_METRIC_DATA } - - } - show={ metric.data.chart.length === 0 } - style={ { height: '240px' } } - > - - - {gradientDef} - - - Styles.tickFormatter(val)} - label={{ ...Styles.axisLabelLeft, value: "Number of Crashes" }} - /> - - - - - - ); + const { data, metric } = props; + const gradientDef = Styles.gradientDef(); + return ( + + {NO_METRIC_DATA} + + } + show={metric.data.chart.length === 0} + style={{ height: '240px' }} + > + + + {gradientDef} + + + Styles.tickFormatter(val)} + label={{ ...Styles.axisLabelLeft, value: 'Number of Crashes' }} + /> + + + + + + ); } export default Crashes; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/Crashes/index.ts b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/Crashes/index.ts index ba5ce0764..cb869ef09 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/Crashes/index.ts +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/Crashes/index.ts @@ -1 +1 @@ -export { default } from './Crashes' \ No newline at end of file +export { default } from './Crashes'; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/DomBuildingTime/DomBuildingTime.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/DomBuildingTime/DomBuildingTime.tsx index 1d4ab85f3..1e1623d4b 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/DomBuildingTime/DomBuildingTime.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/DomBuildingTime/DomBuildingTime.tsx @@ -1,83 +1,96 @@ import React from 'react'; import { NoContent } from 'UI'; -import { Styles, AvgLabel } from '../../common'; -import { withRequest } from 'HOCs' -import { - AreaChart, Area, - CartesianGrid, Tooltip, - ResponsiveContainer, - XAxis, YAxis - } from 'recharts'; +import { withRequest } from 'HOCs'; +import { + AreaChart, + Area, + CartesianGrid, + Tooltip, + ResponsiveContainer, + XAxis, + YAxis, +} from 'recharts'; import { toUnderscore } from 'App/utils'; -import { NO_METRIC_DATA } from 'App/constants/messages' +import { NO_METRIC_DATA } from 'App/constants/messages'; import { InfoCircleOutlined } from '@ant-design/icons'; +import { Styles, AvgLabel } from '../../common'; const WIDGET_KEY = 'pagesDomBuildtime'; interface Props { - data: any - optionsLoading: any - fetchOptions: any - options: any - metric?: any + data: any; + optionsLoading: any; + fetchOptions: any; + options: any; + metric?: any; } function DomBuildingTime(props: Props) { - const { data, metric } = props; - const gradientDef = Styles.gradientDef(); + const { data, metric } = props; + const gradientDef = Styles.gradientDef(); - return ( - - { NO_METRIC_DATA } - } - - - show={ metric.data.chart.length === 0 } - > - <> -
    - -
    - - - {gradientDef} - - - Styles.tickFormatter(val)} - label={{ ...Styles.axisLabelLeft, value: "DOM Build Time (ms)" }} - /> - - - - - -
    - ); + return ( + + {NO_METRIC_DATA} + + } + show={metric.data.chart.length === 0} + > + <> +
    + +
    + + + {gradientDef} + + + Styles.tickFormatter(val)} + label={{ ...Styles.axisLabelLeft, value: 'DOM Build Time (ms)' }} + /> + + + + + +
    + ); } export default withRequest({ - dataName: "options", + dataName: 'options', initialData: [], - dataWrapper: data => data, + dataWrapper: (data) => data, loadingName: 'optionsLoading', - requestName: "fetchOptions", - endpoint: '/dashboard/' + toUnderscore(WIDGET_KEY) + '/search', - method: 'GET' -})(DomBuildingTime) + requestName: 'fetchOptions', + endpoint: `/dashboard/${toUnderscore(WIDGET_KEY)}/search`, + method: 'GET', +})(DomBuildingTime); diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/DomBuildingTime/index.ts b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/DomBuildingTime/index.ts index a3191aaf7..b5d6c5682 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/DomBuildingTime/index.ts +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/DomBuildingTime/index.ts @@ -1 +1 @@ -export { default } from './DomBuildingTime' \ No newline at end of file +export { default } from './DomBuildingTime'; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsByOrigin/ErrorsByOrigin.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsByOrigin/ErrorsByOrigin.tsx index 6f3cd24a7..fed6f57f6 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsByOrigin/ErrorsByOrigin.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsByOrigin/ErrorsByOrigin.tsx @@ -1,58 +1,88 @@ -//@ts-nocheck +// @ts-nocheck +/* eslint-disable i18next/no-literal-string */ import React from 'react'; import { NoContent } from 'UI'; -import { Styles } from '../../common'; -import { - BarChart, Bar, CartesianGrid, Tooltip, - Legend, ResponsiveContainer, - XAxis, YAxis - } from 'recharts'; +import { + BarChart, + Bar, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, + XAxis, + YAxis, +} from 'recharts'; import { NO_METRIC_DATA } from 'App/constants/messages'; import { InfoCircleOutlined } from '@ant-design/icons'; +import { Styles } from '../../common'; +import { useTranslation } from 'react-i18next'; interface Props { - data: any - metric?: any + data: any; + metric?: any; } function ErrorsByOrigin(props: Props) { - const { metric } = props; + const { metric } = props; + const { t } = useTranslation(); - return ( - - { NO_METRIC_DATA } - } - show={ metric.data.chart && metric.data.chart.length === 0 } - style={ { height: '240px' } } - > - - - - - Styles.tickFormatter(val)} - label={{ ...Styles.axisLabelLeft, value: "Number of Errors" }} - allowDecimals={false} - /> - - - 1st Party} dataKey="firstParty" stackId="a" fill={Styles.compareColors[0]} /> - 3rd Party} dataKey="thirdParty" stackId="a" fill={Styles.compareColors[2]} /> - {/* 1st Party} dataKey="firstParty" stackId="a" fill={Styles.colors[0]} /> + return ( + + {NO_METRIC_DATA} + + } + show={metric.data.chart && metric.data.chart.length === 0} + style={{ height: '240px' }} + > + + + + + Styles.tickFormatter(val)} + label={{ ...Styles.axisLabelLeft, value: t('Number of Errors') }} + allowDecimals={false} + /> + + + + st Party + + } + dataKey="firstParty" + stackId="a" + fill={Styles.compareColors[0]} + /> + + rd Party + + } + dataKey="thirdParty" + stackId="a" + fill={Styles.compareColors[2]} + /> + {/* 1st Party} dataKey="firstParty" stackId="a" fill={Styles.colors[0]} /> 3rd Party} dataKey="thirdParty" stackId="a" fill={Styles.colors[2]} /> */} - - - - ); + + + + ); } export default ErrorsByOrigin; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsByOrigin/index.ts b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsByOrigin/index.ts index 18a8b9ec3..9ef5b0117 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsByOrigin/index.ts +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsByOrigin/index.ts @@ -1 +1 @@ -export { default } from './ErrorsByOrigin' \ No newline at end of file +export { default } from './ErrorsByOrigin'; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsByType/ErrorsByType.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsByType/ErrorsByType.tsx index 5ccafee9c..458bcf73d 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsByType/ErrorsByType.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsByType/ErrorsByType.tsx @@ -1,58 +1,85 @@ import React from 'react'; import { NoContent } from 'UI'; -import { Styles } from '../../common'; -import { - BarChart, Bar, CartesianGrid, Tooltip, - Legend, ResponsiveContainer, - XAxis, YAxis - } from 'recharts'; +import { + BarChart, + Bar, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, + XAxis, + YAxis, +} from 'recharts'; import { NO_METRIC_DATA } from 'App/constants/messages'; import { InfoCircleOutlined } from '@ant-design/icons'; +import { Styles } from '../../common'; interface Props { - data: any - metric?: any + data: any; + metric?: any; } function ErrorsByType(props: Props) { - const { data, metric } = props; - return ( - - { NO_METRIC_DATA } + const { data, metric } = props; + return ( + + {NO_METRIC_DATA} - } - show={ metric.data.chart.length === 0 } - style={ { height: '240px' } } - > - - - - - Styles.tickFormatter(val)} - label={{ ...Styles.axisLabelLeft, value: "Number of Errors" }} - allowDecimals={false} - /> - - - - - - - - - - ); + } + show={metric.data.chart.length === 0} + style={{ height: '240px' }} + > + + + + + Styles.tickFormatter(val)} + label={{ ...Styles.axisLabelLeft, value: 'Number of Errors' }} + allowDecimals={false} + /> + + + + + + + + + + ); } export default ErrorsByType; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsByType/index.ts b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsByType/index.ts index f889ccec7..53f976df3 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsByType/index.ts +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsByType/index.ts @@ -1 +1 @@ -export { default } from './ErrorsByType' \ No newline at end of file +export { default } from './ErrorsByType'; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/Bar.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/Bar.tsx index 163c7d0a5..9ba177c32 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/Bar.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/Bar.tsx @@ -1,18 +1,21 @@ -import React from 'react' -import stl from './bar.module.css' +import React from 'react'; +import stl from './bar.module.css'; -const Bar = ({ className = '', width = 0, avg, domain, color }) => { +function Bar({ className = '', width = 0, avg, domain, color }) { return (
    -
    0 ? width : 5 }%`, backgroundColor: color }}>
    +
    0 ? width : 5}%`, backgroundColor: color }} + />
    {`${avg}`}
    {domain}
    - ) + ); } -export default Bar \ No newline at end of file +export default Bar; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/ErrorsPerDomain.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/ErrorsPerDomain.tsx index 51d4c4e31..403e580b4 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/ErrorsPerDomain.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/ErrorsPerDomain.tsx @@ -15,7 +15,7 @@ function ErrorsPerDomain(props: Props) { name: item.domain, icon: , value: Math.round(item.errorsCount), - progress: Math.round((item.errorsCount * 100) / highest) + progress: Math.round((item.errorsCount * 100) / highest), })); return ( @@ -24,9 +24,9 @@ function ErrorsPerDomain(props: Props) { show={data.chart.length === 0} style={{ height: '240px' }} title={ -
    - { NO_METRIC_DATA } -
    +
    + {NO_METRIC_DATA} +
    } >
    diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/index.ts b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/index.ts index d08e3867b..3a60d15c0 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/index.ts +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/index.ts @@ -1 +1 @@ -export { default } from './ErrorsPerDomain' \ No newline at end of file +export { default } from './ErrorsPerDomain'; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/FPS/FPS.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/FPS/FPS.tsx index fe710a827..93de5b764 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/FPS/FPS.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/FPS/FPS.tsx @@ -1,67 +1,75 @@ import React from 'react'; import { NoContent } from 'UI'; -import { Styles, AvgLabel } from '../../common'; -import { - AreaChart, Area, - CartesianGrid, Tooltip, - ResponsiveContainer, - XAxis, YAxis - } from 'recharts'; +import { + AreaChart, + Area, + CartesianGrid, + Tooltip, + ResponsiveContainer, + XAxis, + YAxis, +} from 'recharts'; import { NO_METRIC_DATA } from 'App/constants/messages'; import { InfoCircleOutlined } from '@ant-design/icons'; +import { Styles, AvgLabel } from '../../common'; interface Props { - data: any - metric?: any + data: any; + metric?: any; } function FPS(props: Props) { - const { data, metric } = props; - const gradientDef = Styles.gradientDef(); + const { data, metric } = props; + const gradientDef = Styles.gradientDef(); - return ( - - { NO_METRIC_DATA } + return ( + + {NO_METRIC_DATA}
    - } - show={ metric.data.chart.length === 0 } - > - <> -
    - -
    - - - {gradientDef} - - - Styles.tickFormatter(val)} - label={{ ...Styles.axisLabelLeft, value: "Frames Per Second" }} - /> - - - - - - - ); + } + show={metric.data.chart.length === 0} + > + <> +
    + +
    + + + {gradientDef} + + + Styles.tickFormatter(val)} + label={{ ...Styles.axisLabelLeft, value: 'Frames Per Second' }} + /> + + + + + + + ); } export default FPS; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/FPS/index.ts b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/FPS/index.ts index 85a43ba5e..55394ee20 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/FPS/index.ts +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/FPS/index.ts @@ -1 +1 @@ -export { default } from './FPS' \ No newline at end of file +export { default } from './FPS'; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/MemoryConsumption/MemoryConsumption.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/MemoryConsumption/MemoryConsumption.tsx index e12061997..e885dc9c2 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/MemoryConsumption/MemoryConsumption.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/MemoryConsumption/MemoryConsumption.tsx @@ -1,70 +1,78 @@ import React from 'react'; import { NoContent } from 'UI'; -import { Styles, AvgLabel } from '../../common'; -import { - AreaChart, Area, - CartesianGrid, Tooltip, - ResponsiveContainer, - XAxis, YAxis - } from 'recharts'; +import { + AreaChart, + Area, + CartesianGrid, + Tooltip, + ResponsiveContainer, + XAxis, + YAxis, +} from 'recharts'; import { NO_METRIC_DATA } from 'App/constants/messages'; import { InfoCircleOutlined } from '@ant-design/icons'; +import { Styles, AvgLabel } from '../../common'; interface Props { - data: any - metric?: any + data: any; + metric?: any; } function MemoryConsumption(props: Props) { - const { data, metric } = props; - const gradientDef = Styles.gradientDef(); - // covert the data to mb - const avgValue = Math.round(data.value / 1024 / 1024); + const { data, metric } = props; + const gradientDef = Styles.gradientDef(); + // covert the data to mb + const avgValue = Math.round(data.value / 1024 / 1024); - return ( - - { NO_METRIC_DATA } + return ( + + {NO_METRIC_DATA}
    - } - > - <> -
    - -
    - - - {gradientDef} - - - Styles.tickFormatter(val)} - label={{ ...Styles.axisLabelLeft, value: "JS Heap Size (mb)" }} - /> - - - - - - - ); + } + > + <> +
    + +
    + + + {gradientDef} + + + Styles.tickFormatter(val)} + label={{ ...Styles.axisLabelLeft, value: 'JS Heap Size (mb)' }} + /> + + + + + + + ); } export default MemoryConsumption; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/MemoryConsumption/index.ts b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/MemoryConsumption/index.ts index 7d426259c..9e6644855 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/MemoryConsumption/index.ts +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/MemoryConsumption/index.ts @@ -1 +1 @@ -export { default } from './MemoryConsumption' \ No newline at end of file +export { default } from './MemoryConsumption'; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ResponseTime/ResponseTime.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ResponseTime/ResponseTime.tsx index 83b78cc81..3c4bb2b45 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ResponseTime/ResponseTime.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ResponseTime/ResponseTime.tsx @@ -1,89 +1,106 @@ import React from 'react'; import { NoContent } from 'UI'; -import { Styles, AvgLabel } from '../../common'; -import { withRequest } from 'HOCs' -import { - AreaChart, Area, - CartesianGrid, Tooltip, - ResponsiveContainer, - XAxis, YAxis - } from 'recharts';import { toUnderscore } from 'App/utils'; +import { withRequest } from 'HOCs'; +import { + AreaChart, + Area, + CartesianGrid, + Tooltip, + ResponsiveContainer, + XAxis, + YAxis, +} from 'recharts'; +import { toUnderscore } from 'App/utils'; import { NO_METRIC_DATA } from 'App/constants/messages'; import { InfoCircleOutlined } from '@ant-design/icons'; +import { Styles, AvgLabel } from '../../common'; const WIDGET_KEY = 'pagesResponseTime'; interface Props { - data: any - optionsLoading: any - fetchOptions: any - options: any - metric?: any + data: any; + optionsLoading: any; + fetchOptions: any; + options: any; + metric?: any; } function ResponseTime(props: Props) { - const { data, metric } = props; - const gradientDef = Styles.gradientDef(); + const { data, metric } = props; + const gradientDef = Styles.gradientDef(); - return ( - - { NO_METRIC_DATA } + return ( + + {NO_METRIC_DATA} - } - show={ metric.data.chart.length === 0 } - > - <> -
    - {/* + <> +
    + {/* */} - -
    - - - {gradientDef} - - - Styles.tickFormatter(val)} - label={{ ...Styles.axisLabelLeft, value: "Page Response Time (ms)" }} - /> - - - - - - - ); + +
    + + + {gradientDef} + + + Styles.tickFormatter(val)} + label={{ + ...Styles.axisLabelLeft, + value: 'Page Response Time (ms)', + }} + /> + + + + + +
    + ); } export default withRequest({ - dataName: "options", + dataName: 'options', initialData: [], - dataWrapper: data => data, + dataWrapper: (data) => data, loadingName: 'optionsLoading', - requestName: "fetchOptions", - endpoint: '/dashboard/' + toUnderscore(WIDGET_KEY) + '/search', - method: 'GET' -})(ResponseTime) + requestName: 'fetchOptions', + endpoint: `/dashboard/${toUnderscore(WIDGET_KEY)}/search`, + method: 'GET', +})(ResponseTime); diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ResponseTime/index.ts b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ResponseTime/index.ts index 95effcb83..c4228f56e 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ResponseTime/index.ts +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ResponseTime/index.ts @@ -1 +1 @@ -export { default } from './ResponseTime' \ No newline at end of file +export { default } from './ResponseTime'; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ResponseTimeDistribution/ResponseTimeDistribution.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ResponseTimeDistribution/ResponseTimeDistribution.tsx index d167137cb..ff23d9c3c 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ResponseTimeDistribution/ResponseTimeDistribution.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ResponseTimeDistribution/ResponseTimeDistribution.tsx @@ -1,21 +1,27 @@ import React from 'react'; import { NoContent } from 'UI'; -import { Styles, AvgLabel } from '../../common'; import { - ComposedChart, Bar, BarChart, CartesianGrid, ResponsiveContainer, - XAxis, YAxis, ReferenceLine, Tooltip + ComposedChart, + Bar, + BarChart, + CartesianGrid, + ResponsiveContainer, + XAxis, + YAxis, + ReferenceLine, + Tooltip, } from 'recharts'; import { NO_METRIC_DATA } from 'App/constants/messages'; import { InfoCircleOutlined } from '@ant-design/icons'; +import { Styles, AvgLabel } from '../../common'; - -const PercentileLine = props => { +function PercentileLine(props) { const { viewBox: { x, y }, xoffset, yheight, height, - label + label, } = props; return ( @@ -38,97 +44,128 @@ const PercentileLine = props => { ); -}; +} interface Props { - data: any + data: any; } function ResponseTimeDistribution(props: Props) { - const { data } = props; - const colors = Styles.colors; + const { data } = props; + const { colors } = Styles; - return ( - - { NO_METRIC_DATA } + return ( + + {NO_METRIC_DATA} - } - show={ data.chart.length === 0 } - style={ { height: '240px' } } - > -
    - -
    -
    - - - - - - - 'Page Response Time: ' + val} /> - { data.percentiles && data.percentiles.map((item: any, i: number) => ( - - } - // allowDecimals={false} - x={item.responseTime} - strokeWidth={0} + } + show={data.chart.length === 0} + style={{ height: '240px' }} + > +
    + +
    +
    + + + + + + + `Page Response Time: ${val}`} + /> + {data.percentiles && + data.percentiles.map((item: any, i: number) => ( + - ))} - - - - - - - - - - - -
    - - ); + } + // allowDecimals={false} + x={item.responseTime} + strokeWidth={0} + strokeOpacity={1} + /> + ))} +
    +
    + + + + + + + + + +
    +
    + ); } export default ResponseTimeDistribution; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ResponseTimeDistribution/index.ts b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ResponseTimeDistribution/index.ts index 163efa255..2ddc12a3c 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ResponseTimeDistribution/index.ts +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ResponseTimeDistribution/index.ts @@ -1 +1 @@ -export { default } from './ResponseTimeDistribution' \ No newline at end of file +export { default } from './ResponseTimeDistribution'; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsAffectedByJSErrors/SessionsAffectedByJSErrors.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsAffectedByJSErrors/SessionsAffectedByJSErrors.tsx index a60fb9803..1c28b9a66 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsAffectedByJSErrors/SessionsAffectedByJSErrors.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsAffectedByJSErrors/SessionsAffectedByJSErrors.tsx @@ -1,54 +1,66 @@ import React from 'react'; import { NoContent } from 'UI'; +import { + BarChart, + Bar, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, + XAxis, + YAxis, +} from 'recharts'; +import { NO_METRIC_DATA } from 'App/constants/messages'; +import { InfoCircleOutlined } from '@ant-design/icons'; import { Styles } from '../../common'; -import { - BarChart, Bar, CartesianGrid, Tooltip, - Legend, ResponsiveContainer, - XAxis, YAxis - } from 'recharts'; - import { NO_METRIC_DATA } from 'App/constants/messages'; - import { InfoCircleOutlined } from '@ant-design/icons'; interface Props { - data: any - metric?: any + data: any; + metric?: any; } function SessionsAffectedByJSErrors(props: Props) { - const { data, metric } = props; - return ( - - { NO_METRIC_DATA } + const { data, metric } = props; + return ( + + {NO_METRIC_DATA} - } - size="small" - show={ metric.data.chart.length === 0 } - style={ { height: '240px' } } - > - - - - - - - - - - - - ); + } + size="small" + show={metric.data.chart.length === 0} + style={{ height: '240px' }} + > + + + + + + + + + + + + ); } export default SessionsAffectedByJSErrors; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsAffectedByJSErrors/index.ts b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsAffectedByJSErrors/index.ts index b160b1af1..2af7f10db 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsAffectedByJSErrors/index.ts +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsAffectedByJSErrors/index.ts @@ -1 +1 @@ -export { default } from './SessionsAffectedByJSErrors' \ No newline at end of file +export { default } from './SessionsAffectedByJSErrors'; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsImpactedBySlowRequests/SessionsImpactedBySlowRequests.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsImpactedBySlowRequests/SessionsImpactedBySlowRequests.tsx index 08717ff1f..31461cf31 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsImpactedBySlowRequests/SessionsImpactedBySlowRequests.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsImpactedBySlowRequests/SessionsImpactedBySlowRequests.tsx @@ -1,62 +1,70 @@ import React from 'react'; import { NoContent } from 'UI'; -import { Styles } from '../../common'; -import { - AreaChart, Area, - CartesianGrid, Tooltip, - ResponsiveContainer, - XAxis, YAxis - } from 'recharts'; +import { + AreaChart, + Area, + CartesianGrid, + Tooltip, + ResponsiveContainer, + XAxis, + YAxis, +} from 'recharts'; import { NO_METRIC_DATA } from 'App/constants/messages'; import { InfoCircleOutlined } from '@ant-design/icons'; +import { Styles } from '../../common'; interface Props { - data: any - metric?: any + data: any; + metric?: any; } function SessionsImpactedBySlowRequests(props: Props) { - const { data, metric } = props; - const gradientDef = Styles.gradientDef(); + const { data, metric } = props; + const gradientDef = Styles.gradientDef(); - return ( - - { NO_METRIC_DATA } - - } - size="small" - show={ metric.data.chart.length === 0 } - > - - - {gradientDef} - - - Styles.tickFormatter(val)} - label={{ ...Styles.axisLabelLeft, value: "Number of Sessions" }} - /> - - - - - - ); + return ( + + {NO_METRIC_DATA} + + } + size="small" + show={metric.data.chart.length === 0} + > + + + {gradientDef} + + + Styles.tickFormatter(val)} + label={{ ...Styles.axisLabelLeft, value: 'Number of Sessions' }} + /> + + + + + + ); } export default SessionsImpactedBySlowRequests; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsImpactedBySlowRequests/index.ts b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsImpactedBySlowRequests/index.ts index d950b82ae..7e4adcc74 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsImpactedBySlowRequests/index.ts +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsImpactedBySlowRequests/index.ts @@ -1 +1 @@ -export { default } from './SessionsImpactedBySlowRequests' \ No newline at end of file +export { default } from './SessionsImpactedBySlowRequests'; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser/Bar.js b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser/Bar.js deleted file mode 100644 index 0894e7426..000000000 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser/Bar.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react' -import stl from './Bar.module.css' -// import { Styles } from '../common' -import { TextEllipsis } from 'UI'; - -const Bar = ({ className = '', versions = [], width = 0, avg, domain, colors }) => { - return ( -
    -
    -
    - {versions.map((v, i) => { - const w = (v.value * 100)/ avg; - return ( -
    - -
    Version: {v.key}
    -
    Sessions: {v.value}
    -
    - } /> -
    - ) - })} -
    -
    - {`${avg}`} -
    -
    -
    {domain}
    - - ) -} - -export default Bar \ No newline at end of file diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser/Bar.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser/Bar.tsx new file mode 100644 index 000000000..102f1cbcd --- /dev/null +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser/Bar.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { TextEllipsis } from 'UI'; +import stl from './Bar.module.css'; +import { useTranslation } from 'react-i18next'; +// import { Styles } from '../common' + +function Bar({ + className = '', + versions = [], + width = 0, + avg, + domain, + colors, +}) { + const { t } = useTranslation(); + return ( +
    +
    +
    + {versions.map((v, i) => { + const w = (v.value * 100) / avg; + return ( +
    + +
    + {t('Version:')} + {v.key} +
    +
    + {t('Sessions:')} + {v.value} +
    +
    + } + /> +
    + ); + })} +
    +
    + {`${avg}`} +
    +
    +
    {domain}
    + + ); +} + +export default Bar; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser/SessionsPerBrowser.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser/SessionsPerBrowser.tsx index 3b32f62e5..126db532e 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser/SessionsPerBrowser.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser/SessionsPerBrowser.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { NoContent } from 'UI'; -import { Styles } from '../../common'; -import Bar from './Bar'; import { NO_METRIC_DATA } from 'App/constants/messages'; import { InfoCircleOutlined } from '@ant-design/icons'; +import { Styles } from '../../common'; +import Bar from './Bar'; interface Props { data: any; @@ -13,24 +13,26 @@ function SessionsPerBrowser(props: Props) { const { data } = props; const firstAvg = data.chart[0] && data.chart[0].count; - const getVersions = item => { - return Object.keys(item) - .filter(i => i !== 'browser' && i !== 'count' && i !== 'time' && i !== 'timestamp') - .map(i => ({ key: 'v' + i, value: item[i] })); - }; + const getVersions = (item) => + Object.keys(item) + .filter( + (i) => + i !== 'browser' && i !== 'count' && i !== 'time' && i !== 'timestamp', + ) + .map((i) => ({ key: `v${i}`, value: item[i] })); return ( - { NO_METRIC_DATA } - +
    + {NO_METRIC_DATA} +
    } show={data.chart.length === 0} style={{ minHeight: 220 }} >
    - {data.chart.map((item, i) => + {data.chart.map((item, i) => ( - )} + ))}
    ); diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser/index.ts b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser/index.ts index 06f0656a1..facd495bd 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser/index.ts +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser/index.ts @@ -1 +1 @@ -export { default } from './SessionsPerBrowser' \ No newline at end of file +export { default } from './SessionsPerBrowser'; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/Bar.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/Bar.tsx index 179bca846..c5f6bac1a 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/Bar.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/Bar.tsx @@ -1,19 +1,24 @@ -import React from 'react' -import stl from './Bar.module.css' +import React from 'react'; +import stl from './Bar.module.css'; +import { useTranslation } from 'react-i18next'; -const Bar = ({ className = '', width = 0, avg, domain, color }) => { +function Bar({ className = '', width = 0, avg, domain, color }) { + const { t } = useTranslation(); return (
    -
    +
    {avg} - ms +  {t('ms')}
    {domain}
    - ) + ); } -export default Bar \ No newline at end of file +export default Bar; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/SlowestDomains.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/SlowestDomains.tsx index 1c9e75c8e..7c31b18e6 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/SlowestDomains.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/SlowestDomains.tsx @@ -15,8 +15,8 @@ function SlowestDomains(props: Props) { const list = data.chart.slice(0, 4).map((item: any) => ({ name: item.domain, icon: , - value: Math.round(item.value) + 'ms', - progress: Math.round((item.value * 100) / highest) + value: `${Math.round(item.value)}ms`, + progress: Math.round((item.value * 100) / highest), })); return ( @@ -25,9 +25,9 @@ function SlowestDomains(props: Props) { show={list.length === 0} style={{ minHeight: 220 }} title={ -
    - { NO_METRIC_DATA } -
    +
    + {NO_METRIC_DATA} +
    } >
    diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/index.ts b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/index.ts index 311262347..87f1e002a 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/index.ts +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/index.ts @@ -1 +1 @@ -export { default } from './SlowestDomains' \ No newline at end of file +export { default } from './SlowestDomains'; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SpeedIndexByLocation/Scale.js b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SpeedIndexByLocation/Scale.tsx similarity index 50% rename from frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SpeedIndexByLocation/Scale.js rename to frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SpeedIndexByLocation/Scale.tsx index 707f21eb9..3d7b588fa 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SpeedIndexByLocation/Scale.js +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SpeedIndexByLocation/Scale.tsx @@ -1,21 +1,28 @@ import React from 'react'; -import { Styles } from '../../common'; import cn from 'classnames'; +import { Styles } from '../../common'; import stl from './scale.module.css'; +import { useTranslation } from 'react-i18next'; function Scale({ colors }) { - const lastIndex = (Styles.compareColors.length - 1); + const { t } = useTranslation(); + const lastIndex = Styles.compareColors.length - 1; return (
    {Styles.compareColors.map((c, i) => (
    - {i === 0 &&
    Slow
    } - {i === lastIndex &&
    Fast
    } + {i === 0 &&
    {t('Slow')}
    } + {i === lastIndex &&
    {t('Fast')}
    }
    ))}
    diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SpeedIndexByLocation/SpeedIndexByLocation.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SpeedIndexByLocation/SpeedIndexByLocation.tsx index 039d02e04..ae343f7c5 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SpeedIndexByLocation/SpeedIndexByLocation.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SpeedIndexByLocation/SpeedIndexByLocation.tsx @@ -1,21 +1,23 @@ import React from 'react'; import { NoContent } from 'UI'; -import { Styles, AvgLabel } from '../../common'; -import Scale from './Scale'; import { observer } from 'mobx-react-lite'; import { numberWithCommas, positionOfTheNumber } from 'App/utils'; import WorldMap from '@svg-maps/world'; import { SVGMap } from 'react-svg-map'; -import stl from './SpeedIndexByLocation.module.css'; import cn from 'classnames'; import { NO_METRIC_DATA } from 'App/constants/messages'; import { InfoCircleOutlined } from '@ant-design/icons'; +import stl from './SpeedIndexByLocation.module.css'; +import Scale from './Scale'; +import { Styles, AvgLabel } from '../../common'; +import { useTranslation } from 'react-i18next'; interface Props { data?: any; } function SpeedIndexByLocation(props: Props) { + const { t } = useTranslation(); const { data } = props; const wrapper: any = React.useRef(null); const [tooltipStyle, setTooltipStyle] = React.useState({ display: 'none' }); @@ -23,8 +25,14 @@ function SpeedIndexByLocation(props: Props) { const dataMap: any = React.useMemo(() => { const _data: any = {}; - const max = data.chart?.reduce((acc: any, item: any) => Math.max(acc, item.value), 0); - const min = data.chart?.reduce((acc: any, item: any) => Math.min(acc, item.value), 0); + const max = data.chart?.reduce( + (acc: any, item: any) => Math.max(acc, item.value), + 0, + ); + const min = data.chart?.reduce( + (acc: any, item: any) => Math.min(acc, item.value), + 0, + ); data.chart?.forEach((item: any) => { if (!item || !item.userCountry) { return; @@ -37,7 +45,7 @@ function SpeedIndexByLocation(props: Props) { const getLocationClassName = (location: any) => { const i = dataMap[location.id] ? dataMap[location.id].perNumber : 0; - const cls = stl['heat_index' + i]; + const cls = stl[`heat_index${i}`]; return cn(stl.location, cls); }; @@ -63,29 +71,33 @@ function SpeedIndexByLocation(props: Props) { const tooltipStyle = { display: 'block', top: event.clientY + 10, - left: event.clientX - 100 + left: event.clientX - 100, }; setTooltipStyle(tooltipStyle); }; return ( - - { NO_METRIC_DATA } -
    - }> + + {NO_METRIC_DATA} +
    + } + >
    -
    +
    @@ -103,7 +115,14 @@ function SpeedIndexByLocation(props: Props) { <>
    {pointedLocation.name}
    - Avg: {dataMap[pointedLocation.id] ? numberWithCommas(parseInt(dataMap[pointedLocation.id].value)) : 0} + {t('Avg:')}{' '} + + {dataMap[pointedLocation.id] + ? numberWithCommas( + parseInt(dataMap[pointedLocation.id].value), + ) + : 0} +
    )} diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SpeedIndexByLocation/index.ts b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SpeedIndexByLocation/index.ts index 1cbdfe2f8..79ab01b34 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SpeedIndexByLocation/index.ts +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SpeedIndexByLocation/index.ts @@ -1 +1 @@ -export { default } from './SpeedIndexByLocation' \ No newline at end of file +export { default } from './SpeedIndexByLocation'; diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/TimeToRender/TimeToRender.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/TimeToRender/TimeToRender.tsx index 19a0f7233..1e903ce07 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/TimeToRender/TimeToRender.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/TimeToRender/TimeToRender.tsx @@ -1,97 +1,109 @@ import React from 'react'; import { NoContent } from 'UI'; -import { Styles, AvgLabel } from '../../common'; -import { withRequest } from 'HOCs' -import { - AreaChart, Area, - CartesianGrid, Tooltip, - ResponsiveContainer, - XAxis, YAxis - } from 'recharts'; +import { withRequest } from 'HOCs'; +import { + AreaChart, + Area, + CartesianGrid, + Tooltip, + ResponsiveContainer, + XAxis, + YAxis, +} from 'recharts'; import { toUnderscore } from 'App/utils'; import { NO_METRIC_DATA } from 'App/constants/messages'; import { InfoCircleOutlined } from '@ant-design/icons'; +import { Styles, AvgLabel } from '../../common'; const WIDGET_KEY = 'timeToRender'; interface Props { - data: any - optionsLoading: any - fetchOptions: any - options: any - metric?: any + data: any; + optionsLoading: any; + fetchOptions: any; + options: any; + metric?: any; } function TimeToRender(props: Props) { - const { data, optionsLoading, metric } = props; - const gradientDef = Styles.gradientDef(); + const { data, optionsLoading, metric } = props; + const gradientDef = Styles.gradientDef(); + const onSelect = (params) => { + // const _params = { density: 70 } + // TODO reload the data with new params; + // this.props.fetchWidget(WIDGET_KEY, dashbaordStore.period, props.platform, { ..._params, url: params.value }) + }; - const onSelect = (params) => { - // const _params = { density: 70 } - // TODO reload the data with new params; - // this.props.fetchWidget(WIDGET_KEY, dashbaordStore.period, props.platform, { ..._params, url: params.value }) - } - - return ( - - { NO_METRIC_DATA } + return ( + + {NO_METRIC_DATA}
    - } - > - <> -
    - {/* + <> +
    + {/* */} - -
    - - - {gradientDef} - - - Styles.tickFormatter(val)} - label={{ ...Styles.axisLabelLeft, value: "Time to Render (ms)" }} - /> - - - - - - - ); + +
    + + + {gradientDef} + + + Styles.tickFormatter(val)} + label={{ ...Styles.axisLabelLeft, value: 'Time to Render (ms)' }} + /> + + + + + + + ); } export default withRequest({ - dataName: "options", + dataName: 'options', initialData: [], - dataWrapper: data => data, + dataWrapper: (data) => data, loadingName: 'optionsLoading', - requestName: "fetchOptions", - endpoint: '/dashboard/' + toUnderscore(WIDGET_KEY) + '/search', - method: 'GET' -})(TimeToRender) + requestName: 'fetchOptions', + endpoint: `/dashboard/${toUnderscore(WIDGET_KEY)}/search`, + method: 'GET', +})(TimeToRender); diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/TimeToRender/index.ts b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/TimeToRender/index.ts index 0e806bf6d..9c4c77c12 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/TimeToRender/index.ts +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/TimeToRender/index.ts @@ -1 +1 @@ -export { default } from './TimeToRender' \ No newline at end of file +export { default } from './TimeToRender'; diff --git a/frontend/app/components/Dashboard/Widgets/common/AvgLabel.js b/frontend/app/components/Dashboard/Widgets/common/AvgLabel.js index 70be2ea8f..087e3b3e0 100644 --- a/frontend/app/components/Dashboard/Widgets/common/AvgLabel.js +++ b/frontend/app/components/Dashboard/Widgets/common/AvgLabel.js @@ -1,7 +1,8 @@ -import React from 'react' +import React from 'react'; import { numberWithCommas } from 'App/utils'; -const AvgLabel = ({ className = '', text, count, unit}) => +function AvgLabel({ className = '', text, count, unit }) { + return (
    {text} @@ -9,5 +10,7 @@ const AvgLabel = ({ className = '', text, count, unit}) => {unit && {unit}}
    + ); +} -export default AvgLabel +export default AvgLabel; diff --git a/frontend/app/components/Dashboard/Widgets/common/CountBadge.js b/frontend/app/components/Dashboard/Widgets/common/CountBadge.js index 18d986ffa..ad639642f 100644 --- a/frontend/app/components/Dashboard/Widgets/common/CountBadge.js +++ b/frontend/app/components/Dashboard/Widgets/common/CountBadge.js @@ -1,19 +1,18 @@ import React from 'react'; import { Icon } from 'UI'; -import styles from './countBadge.module.css'; import cn from 'classnames'; +import styles from './countBadge.module.css'; const getFixedValue = (val) => { let accuracy = 0; - while (Math.trunc(val * Math.pow(10, accuracy)) === 0) { + while (Math.trunc(val * 10 ** accuracy) === 0) { accuracy += 1; } const parsed = parseFloat(val).toFixed(accuracy).toString(); return parsed; }; -// eslint-disable-next-line complexity -const CountBadge = ({ +function CountBadge({ title, icon, count = '', @@ -21,26 +20,33 @@ const CountBadge = ({ change, oppositeColors = false, component, - className -}) => { - const viewChange = typeof change === 'number' && change !== 0 ; + className, +}) { + const viewChange = typeof change === 'number' && change !== 0; const changeIncrease = change > 0; const colorGreen = oppositeColors ? !changeIncrease : changeIncrease; return ( -
    -
    - { icon && } - { component || count } - { unit } +
    +
    + {icon && ( + + )} + {component || count} + {unit}
    -
    - { viewChange && +
    + {viewChange && (
    - - { `${ getFixedValue(change) }%` } + + {`${getFixedValue(change)}%`}
    - } -
    + )} +
    ); } diff --git a/frontend/app/components/Dashboard/Widgets/common/CustomTooltip.js b/frontend/app/components/Dashboard/Widgets/common/CustomTooltip.tsx similarity index 55% rename from frontend/app/components/Dashboard/Widgets/common/CustomTooltip.js rename to frontend/app/components/Dashboard/Widgets/common/CustomTooltip.tsx index 035388fda..7ac8af6e1 100644 --- a/frontend/app/components/Dashboard/Widgets/common/CustomTooltip.js +++ b/frontend/app/components/Dashboard/Widgets/common/CustomTooltip.tsx @@ -1,23 +1,27 @@ import React from 'react'; import { numberWithCommas } from 'App/utils'; +import { useTranslation } from 'react-i18next'; -const TooltipLabel = ({ payload, unit = false }) => { +function TooltipLabel({ payload, unit = false }) { + const { t } = useTranslation(); if (!payload) return ''; const value = numberWithCommas(Math.round(payload.value)); return (
    {`${payload.name}: ${value}`} - { unit && ms} + {unit && {t('ms')}}
    - ) + ); } -const CustomTooltip = ({ active, payload, label, unit }) => { +function CustomTooltip({ active, payload, label, unit }) { if (active && payload && payload[0]) { return (
    {`${label}`}
    - {payload.map(p => ( ))} + {payload.map((p) => ( + + ))}
    ); } @@ -25,4 +29,4 @@ const CustomTooltip = ({ active, payload, label, unit }) => { return null; } -export default CustomTooltip +export default CustomTooltip; diff --git a/frontend/app/components/Dashboard/Widgets/common/Divider.js b/frontend/app/components/Dashboard/Widgets/common/Divider.js index 29d5aae00..75ce4948a 100644 --- a/frontend/app/components/Dashboard/Widgets/common/Divider.js +++ b/frontend/app/components/Dashboard/Widgets/common/Divider.js @@ -1,12 +1,15 @@ -import React from 'react' -const Divider = () => ( -
    -); +import React from 'react'; + +function Divider() { + return ( +
    + ); +} export default Divider; diff --git a/frontend/app/components/Dashboard/Widgets/common/SessionLine.js b/frontend/app/components/Dashboard/Widgets/common/SessionLine.js index a13bcd676..a36c55b32 100644 --- a/frontend/app/components/Dashboard/Widgets/common/SessionLine.js +++ b/frontend/app/components/Dashboard/Widgets/common/SessionLine.js @@ -2,36 +2,23 @@ import cn from 'classnames'; import { session as sessionRoute } from 'App/routes'; import { Link, Icon, TextEllipsis } from 'UI'; import stl from './sessionLine.module.css'; +import React from 'react'; -const FeedbackLine = ({ - icon, - info, - subInfo, - sessionId, -}) => { - - return ( -
    - { icon && } -
    - { info && - - { info } - - } - { subInfo && -
    - { subInfo } -
    - } -
    - - - -
    - ); +function FeedbackLine({ icon, info, subInfo, sessionId }) { + return ( +
    + {icon && } +
    + {info && {info}} + {subInfo &&
    {subInfo}
    } +
    + + + +
    + ); } FeedbackLine.displayName = 'FeedbackLine'; -export default FeedbackLine; \ No newline at end of file +export default FeedbackLine; diff --git a/frontend/app/components/Dashboard/Widgets/common/Styles.js b/frontend/app/components/Dashboard/Widgets/common/Styles.js index 2bf9c6090..b4f90cf68 100644 --- a/frontend/app/components/Dashboard/Widgets/common/Styles.js +++ b/frontend/app/components/Dashboard/Widgets/common/Styles.js @@ -1,95 +1,133 @@ import React from 'react'; -import {numberWithCommas} from 'App/utils'; +import { numberWithCommas } from 'App/utils'; const colorsTeal = ['#1E889A', '#239DB2', '#28B2C9', '#36C0D7', '#65CFE1']; -const colors = ['#6774E2', '#929ACD', '#3EAAAF', '#565D97', '#8F9F9F', '#376F72']; -const colorsx = ['#256669', '#38999e', '#3eaaaf', '#51b3b7', '#78c4c7', '#9fd5d7', '#c5e6e7'].reverse(); +const colors = [ + '#6774E2', + '#929ACD', + '#3EAAAF', + '#565D97', + '#8F9F9F', + '#376F72', +]; +const colorsx = [ + '#256669', + '#38999e', + '#3eaaaf', + '#51b3b7', + '#78c4c7', + '#9fd5d7', + '#c5e6e7', +].reverse(); const compareColors = ['#192EDB', '#6272FF', '#808DFF', '#B3BBFF', '#C9CFFF']; -const compareColorsx = ["#222F99", "#2E3ECC", "#394EFF", "#6171FF", "#8895FF", "#B0B8FF", "#D7DCFF"].reverse(); +const compareColorsx = [ + '#222F99', + '#2E3ECC', + '#394EFF', + '#6171FF', + '#8895FF', + '#B0B8FF', + '#D7DCFF', +].reverse(); const customMetricColors = ['#394EFF', '#3EAAAF', '#565D97']; -const colorsPie = colors.concat(["#DDDDDD"]); -const safeColors = ['#394EFF', '#3EAAAF', '#9276da', '#ceba64', "#bc6f9d", '#966fbc', '#64ce86', '#e06da3', '#6dabe0']; +const colorsPie = colors.concat(['#DDDDDD']); +const safeColors = [ + '#394EFF', + '#3EAAAF', + '#9276da', + '#ceba64', + '#bc6f9d', + '#966fbc', + '#64ce86', + '#e06da3', + '#6dabe0', +]; -const countView = count => { - const isMoreThanK = count >= 1000; - return numberWithCommas(isMoreThanK ? Math.trunc(count / 1000) + 'k' : count); -} +const countView = (count) => { + const isMoreThanK = count >= 1000; + return numberWithCommas(isMoreThanK ? `${Math.trunc(count / 1000)}k` : count); +}; export default { - customMetricColors, - colors, - colorsTeal, - colorsPie, - colorsx, - compareColors, - compareColorsx, - safeColors, - lineColor: '#2A7B7F', - lineColorCompare: '#394EFF', - strokeColor: compareColors[0], - xaxis: { - axisLine: {stroke: '#CCCCCC'}, - interval: 0, - dataKey: "time", - tick: {fill: '#000000', fontSize: 9}, - tickLine: {stroke: '#CCCCCC'}, - strokeWidth: 0.5 + customMetricColors, + colors, + colorsTeal, + colorsPie, + colorsx, + compareColors, + compareColorsx, + safeColors, + lineColor: '#2A7B7F', + lineColorCompare: '#394EFF', + strokeColor: compareColors[0], + xaxis: { + axisLine: { stroke: '#CCCCCC' }, + interval: 0, + dataKey: 'time', + tick: { fill: '#000000', fontSize: 9 }, + tickLine: { stroke: '#CCCCCC' }, + strokeWidth: 0.5, + }, + yaxis: { + axisLine: { stroke: '#CCCCCC' }, + tick: { fill: '#000000', fontSize: 9 }, + tickLine: { stroke: '#CCCCCC' }, + }, + axisLabelLeft: { + angle: -90, + fill: '#999999', + offset: 10, + style: { textAnchor: 'middle' }, + position: 'insideLeft', + fontSize: 11, + }, + tickFormatter: (val) => `${countView(val)}`, + tickFormatterBytes: (val) => Math.round(val / 1024 / 1024), + chartMargins: { + left: 0, + right: 20, + top: 10, + bottom: 5, + }, + tooltip: { + wrapperStyle: { + zIndex: 999, }, - yaxis: { - axisLine: {stroke: '#CCCCCC'}, - tick: {fill: '#000000', fontSize: 9}, - tickLine: {stroke: '#CCCCCC'}, + contentStyle: { + padding: '5px', + background: 'white', + border: '1px solid #DDD', + borderRadius: '3px', + lineHeight: '1.25rem', + color: '#888', + fontSize: '10px', }, - axisLabelLeft: { - angle: -90, - fill: '#999999', - offset: 10, - style: {textAnchor: 'middle'}, - position: 'insideLeft', - fontSize: 11 + labelStyle: {}, + formatter: (value, name, { unit }) => { + if (unit && unit.trim() === 'mb') { + return numberWithCommas(Math.round(value / 1024 / 1024)); + } + return numberWithCommas(Math.round(value)); }, - tickFormatter: val => `${countView(val)}`, - tickFormatterBytes: val => Math.round(val / 1024 / 1024), - chartMargins: {left: 0, right: 20, top: 10, bottom: 5}, - tooltip: { - wrapperStyle: { - zIndex: 999, - }, - contentStyle: { - padding: '5px', - background: 'white', - border: '1px solid #DDD', - borderRadius: '3px', - lineHeight: '1.25rem', - color: '#888', - fontSize: '10px' - }, - labelStyle: {}, - formatter: (value, name, {unit}) => { - if (unit && unit.trim() === 'mb') { - return numberWithCommas(Math.round(value / 1024 / 1024)) - } - return numberWithCommas(Math.round(value)) - }, - itemStyle: { - lineHeight: '0.75rem', - color: '#000', - fontSize: '12px' - }, - cursor: { - fill: '#eee' - } + itemStyle: { + lineHeight: '0.75rem', + color: '#000', + fontSize: '12px', }, - gradientDef: () => ( - - - - - - - - - - - ) + cursor: { + fill: '#eee', + }, + }, + gradientDef: () => ( + + + + + + + + + + + ), }; diff --git a/frontend/app/components/Dashboard/Widgets/common/Table.js b/frontend/app/components/Dashboard/Widgets/common/Table.js index b37c19560..c1974d76f 100644 --- a/frontend/app/components/Dashboard/Widgets/common/Table.js +++ b/frontend/app/components/Dashboard/Widgets/common/Table.js @@ -8,7 +8,7 @@ export default class Table extends React.PureComponent { onLoadMoreClick = () => { this.setState({ showAll: true }); - } + }; render() { const { @@ -24,44 +24,68 @@ export default class Table extends React.PureComponent { } = this.props; const { showAll } = this.state; - const isShowMoreButtonVisible = !isTemplate && rows.size > (small ? 3 : 5) && !showAll + const isShowMoreButtonVisible = + !isTemplate && rows.size > (small ? 3 : 5) && !showAll; return (
    - { - cols.map(({ - key, title, width, - }) => -
    { title }
    ) - } + {cols.map(({ key, title, width }) => ( +
    + {title} +
    + ))}
    -
    - { rows.take(showAll ? rows.size : (small ? 3 : 5)).map(row => ( +
    + {rows.take(showAll ? rows.size : small ? 3 : 5).map((row) => (
    onRowClick(e, row) : () => null} > - { cols.map(({ cellClass = '', className = '', Component, key, toText = t => t, width }) => ( -
    { Component - ? - :
    { toText(row[ key ]) }
    - } -
    - )) } + {cols.map( + ({ + cellClass = '', + className = '', + Component, + key, + toText = (t) => t, + width, + }) => ( +
    + {' '} + {Component ? ( + + ) : ( +
    + {' '} + {toText(row[key])}{' '} +
    + )} +
    + ), + )}
    - )) } + ))}
    - {isShowMoreButtonVisible && -
    - -
    - } + {isShowMoreButtonVisible && ( +
    + +
    + )}
    ); } diff --git a/frontend/app/components/Dashboard/Widgets/common/Title.js b/frontend/app/components/Dashboard/Widgets/common/Title.js index 77c33ad84..ee369e6f8 100644 --- a/frontend/app/components/Dashboard/Widgets/common/Title.js +++ b/frontend/app/components/Dashboard/Widgets/common/Title.js @@ -1,11 +1,14 @@ import styles from './title.module.css'; +import React from 'react'; -const Title = ({ title, sub }) => ( -
    -

    { title }

    - { sub } -
    -); +function Title({ title, sub }) { + return ( +
    +

    {title}

    + {sub} +
    + ); +} Title.displayName = 'Title'; diff --git a/frontend/app/components/Dashboard/Widgets/common/domain.js b/frontend/app/components/Dashboard/Widgets/common/domain.js index f00ac7395..f17ccfb1d 100644 --- a/frontend/app/components/Dashboard/Widgets/common/domain.js +++ b/frontend/app/components/Dashboard/Widgets/common/domain.js @@ -1,5 +1,8 @@ -export default [ 0, dataMax => { - if (dataMax === 0) return 10; - if (dataMax > 100 || dataMax < 0) return dataMax; - return dataMax * (5.7 - Math.log(dataMax)); -} ]; \ No newline at end of file +export default [ + 0, + (dataMax) => { + if (dataMax === 0) return 10; + if (dataMax > 100 || dataMax < 0) return dataMax; + return dataMax * (5.7 - Math.log(dataMax)); + }, +]; diff --git a/frontend/app/components/Dashboard/Widgets/common/index.js b/frontend/app/components/Dashboard/Widgets/common/index.js index 21ef76ea0..5cc6fa481 100644 --- a/frontend/app/components/Dashboard/Widgets/common/index.js +++ b/frontend/app/components/Dashboard/Widgets/common/index.js @@ -2,4 +2,4 @@ export { default as Title } from './Title'; export { default as Table } from './Table'; export { default as domain } from './domain'; export { default as Styles } from './Styles'; -export { default as AvgLabel } from './AvgLabel'; \ No newline at end of file +export { default as AvgLabel } from './AvgLabel'; diff --git a/frontend/app/components/Dashboard/components/AddCardModal/AddCardModal.tsx b/frontend/app/components/Dashboard/components/AddCardModal/AddCardModal.tsx index 630a0fea1..cb5f3b09f 100644 --- a/frontend/app/components/Dashboard/components/AddCardModal/AddCardModal.tsx +++ b/frontend/app/components/Dashboard/components/AddCardModal/AddCardModal.tsx @@ -11,7 +11,11 @@ function AddCardModal(props: Props) { <> - + ); diff --git a/frontend/app/components/Dashboard/components/AddCardSection/AddCardSection.tsx b/frontend/app/components/Dashboard/components/AddCardSection/AddCardSection.tsx index 6781aeedb..4cccdb14b 100644 --- a/frontend/app/components/Dashboard/components/AddCardSection/AddCardSection.tsx +++ b/frontend/app/components/Dashboard/components/AddCardSection/AddCardSection.tsx @@ -28,8 +28,10 @@ import { import { useHistory } from 'react-router-dom'; import { dashboardMetricCreate, withSiteId, metricCreate } from 'App/routes'; import { FilterKey } from 'Types/filter/filterType'; -import MetricsLibraryModal from '../MetricsLibraryModal/MetricsLibraryModal'; import { observer } from 'mobx-react-lite'; +import MetricsLibraryModal from '../MetricsLibraryModal/MetricsLibraryModal'; +import { useTranslation } from 'react-i18next'; +import { TFunction } from 'i18next'; interface TabItem { icon: React.ReactNode; @@ -38,104 +40,102 @@ interface TabItem { type: string; } -export const tabItems: Record = { +export const tabItems: (t: TFunction) => Record = (t) => ({ [CATEGORIES.product_analytics]: [ { icon: , - title: 'Trends', + title: t('Trends'), type: TIMESERIES, - description: 'Track session and user trends over time.', + description: t('Track session and user trends over time.'), }, { icon: , - title: 'Funnels', + title: t('Funnels'), type: FUNNEL, - description: 'Visualize user progression through critical steps.', + description: t('Visualize user progression through critical steps.'), }, { - icon: ( - - ), - title: 'Journeys', + icon: , + title: t('Journeys'), type: USER_PATH, - description: 'Understand the paths users take through your product.', + description: t('Understand the paths users take through your product.'), }, { - icon: , - title: 'Heatmaps', + icon: , + title: t('Heatmaps'), type: HEATMAP, - description: 'Visualize user interaction patterns on your pages.', + description: t('Visualize user interaction patterns on your pages.'), }, ], [CATEGORIES.monitors]: [ { - icon: ( - - ), - title: 'JS Errors', + icon: , + title: t('JS Errors'), type: FilterKey.ERRORS, - description: 'Monitor JS errors affecting user experience.', + description: t('Monitor JS errors affecting user experience.'), }, { icon: , - title: 'Top Network Requests', + title: t('Top Network Requests'), type: FilterKey.FETCH, - description: 'Identify the most frequent network requests.', + description: t('Identify the most frequent network requests.'), }, { icon: , - title: '4xx/5xx Requests', - type: TIMESERIES + '_4xx_requests', - description: 'Track client and server errors for performance issues.', + title: t('4xx/5xx Requests'), + type: `${TIMESERIES}_4xx_requests`, + description: t('Track client and server errors for performance issues.'), }, { icon: , - title: 'Slow Network Requests', - type: TIMESERIES + '_slow_network_requests', - description: 'Pinpoint the slowest network requests causing delays.', + title: t('Slow Network Requests'), + type: `${TIMESERIES}_slow_network_requests`, + description: t('Pinpoint the slowest network requests causing delays.'), }, ], [CATEGORIES.web_analytics]: [ { icon: , - title: 'Top Pages', + title: t('Top Pages'), type: FilterKey.LOCATION, - description: 'Discover the most visited pages on your site.', + description: t('Discover the most visited pages on your site.'), }, { icon: , - title: 'Top Browsers', + title: t('Top Browsers'), type: FilterKey.USER_BROWSER, - description: 'Analyze the browsers your visitors are using the most.', + description: t('Analyze the browsers your visitors are using the most.'), }, { icon: , - title: 'Top Referrer', + title: t('Top Referrer'), type: FilterKey.REFERRER, - description: 'See where your traffic is coming from.', + description: t('See where your traffic is coming from.'), }, { icon: , - title: 'Top Users', + title: t('Top Users'), type: FilterKey.USERID, - description: 'Identify the users with the most interactions.', + description: t('Identify the users with the most interactions.'), }, { icon: , - title: 'Top Countries', + title: t('Top Countries'), type: FilterKey.USER_COUNTRY, - description: 'Track the geographical distribution of your audience.', + description: t('Track the geographical distribution of your audience.'), }, { icon: , - title: 'Top Devices', + title: t('Top Devices'), type: FilterKey.USER_DEVICE, - description: 'Explore the devices used by your users.', + description: t('Explore the devices used by your users.'), }, ], -}; +}); -export const mobileTabItems: Record = { +export const mobileTabItems: (t: TFunction) => Record = ( + t, +) => ({ // [CATEGORIES.product_analytics]: [ // { // icon: , @@ -153,24 +153,24 @@ export const mobileTabItems: Record = { [CATEGORIES.web_analytics]: [ { icon: , - title: 'Top Users', + title: t('Top Users'), type: FilterKey.USERID, - description: 'Identify the users with the most interactions.', + description: t('Identify the users with the most interactions.'), }, { icon: , - title: 'Top Countries', + title: t('Top Countries'), type: FilterKey.USER_COUNTRY, - description: 'Track the geographical distribution of your audience.', + description: t('Track the geographical distribution of your audience.'), }, { icon: , - title: 'Top Devices', + title: t('Top Devices'), type: FilterKey.USER_DEVICE, - description: 'Explore the devices used by your users.', + description: t('Explore the devices used by your users.'), }, ], -}; +}); function CategoryTab({ tab, @@ -181,7 +181,8 @@ function CategoryTab({ isMobile?: boolean; inCards?: boolean; }) { - const items = isMobile ? mobileTabItems[tab] : tabItems[tab]; + const { t } = useTranslation(); + const items = isMobile ? mobileTabItems(t)[tab] : tabItems(t)[tab]; const { projectsStore, dashboardStore } = useStore(); const history = useHistory(); @@ -189,34 +190,30 @@ function CategoryTab({ if (projectsStore.activeSiteId) { if (inCards) { history.push( - withSiteId(metricCreate(), projectsStore.activeSiteId) + `?mk=${card}` + `${withSiteId(metricCreate(), projectsStore.activeSiteId)}?mk=${card}`, ); } else if (dashboardStore.selectedDashboard) { history.push( - withSiteId( + `${withSiteId( dashboardMetricCreate(dashboardStore.selectedDashboard.dashboardId), - projectsStore.activeSiteId - ) + `?mk=${card}` + projectsStore.activeSiteId, + )}?mk=${card}`, ); } } }; return ( -
    +
    {items.map((item, index) => (
    handleCardSelection(item.type)} key={index} - className={ - 'flex items-start gap-2 p-2 hover:bg-active-blue rounded-xl hover:text-teal group cursor-pointer' - } + className="flex items-start gap-2 p-2 hover:bg-active-blue rounded-xl hover:text-teal group cursor-pointer" > {item.icon} -
    +
    {item.title}
    -
    +
    {item.description}
    @@ -234,22 +231,23 @@ const AddCardSection = observer( inCards?: boolean; handleOpenChange?: (isOpen: boolean) => void; }) => { + const { t } = useTranslation(); const { showModal } = useModal(); const { metricStore, dashboardStore, projectsStore } = useStore(); - const isMobile = projectsStore.isMobile; + const { isMobile } = projectsStore; const [tab, setTab] = React.useState( - isMobile ? 'web_analytics' : 'product_analytics' + isMobile ? 'web_analytics' : 'product_analytics', ); const options = isMobile ? [ // { label: 'Product Analytics', value: 'product_analytics' }, - { label: 'Mobile Analytics', value: 'web_analytics' }, + { label: t('Mobile Analytics'), value: 'web_analytics' }, ] : [ - { label: 'Product Analytics', value: 'product_analytics' }, - { label: 'Monitors', value: 'monitors' }, - { label: 'Web Analytics', value: 'web_analytics' }, + { label: t('Product Analytics'), value: 'product_analytics' }, + { label: t('Monitors'), value: 'monitors' }, + { label: t('Web Analytics'), value: 'web_analytics' }, ]; const originStr = window.env.ORIGIN || window.location.origin; @@ -265,26 +263,20 @@ const AddCardSection = observer( onClose: () => { metricStore.updateKey('metricsSearch', ''); }, - } + }, ); handleOpenChange?.(false); }; return ( -
    -
    -
    - What do you want to visualize? +
    +
    +
    + {t('What do you want to visualize?')}
    {isSaas ? ( -
    - -
    Ask AI
    +
    + +
    {t('Ask AI')}
    ) : null}
    @@ -302,24 +294,21 @@ const AddCardSection = observer(
    {inCards ? null : ( -
    +
    )}
    ); - } + }, ); export default AddCardSection; diff --git a/frontend/app/components/Dashboard/components/AddCardSelectionModal.tsx b/frontend/app/components/Dashboard/components/AddCardSelectionModal.tsx index 5add19f6f..dd2de2cf0 100644 --- a/frontend/app/components/Dashboard/components/AddCardSelectionModal.tsx +++ b/frontend/app/components/Dashboard/components/AddCardSelectionModal.tsx @@ -6,6 +6,7 @@ import { useStore } from 'App/mstore'; import NewDashboardModal from 'Components/Dashboard/components/DashboardList/NewDashModal'; import AiQuery from './DashboardView/AiQuery'; +import { useTranslation } from 'react-i18next'; interface Props { open: boolean; @@ -13,6 +14,7 @@ interface Props { } function AddCardSelectionModal(props: Props) { + const { t } = useTranslation(); const { metricStore } = useStore(); const [open, setOpen] = React.useState(false); const [isLibrary, setIsLibrary] = React.useState(false); @@ -35,7 +37,7 @@ function AddCardSelectionModal(props: Props) { return ( <> -
    - or +
    + {t('or')}
    ) : null} @@ -64,7 +62,7 @@ function AddCardSelectionModal(props: Props) { onClick={() => onClick(true)} > - Add from library + {t('Add from library')}
    @@ -74,7 +72,7 @@ function AddCardSelectionModal(props: Props) { onClick={() => onClick(false)} > - Create New + {t('Create New')} diff --git a/frontend/app/components/Dashboard/components/AddToDashboardButton.tsx b/frontend/app/components/Dashboard/components/AddToDashboardButton.tsx index 509c66426..0ba8f644d 100644 --- a/frontend/app/components/Dashboard/components/AddToDashboardButton.tsx +++ b/frontend/app/components/Dashboard/components/AddToDashboardButton.tsx @@ -6,12 +6,17 @@ import { Button, Modal } from 'antd'; import Select from 'Shared/Select/Select'; import { Form } from 'UI'; import { useStore } from 'App/mstore'; +import { useTranslation } from 'react-i18next'; interface Props { metricId: string; } -export const showAddToDashboardModal = (metricId: string, dashboardStore: any) => { +export const showAddToDashboardModal = ( + metricId: string, + dashboardStore: any, +) => { + const { t } = useTranslation(); const dashboardOptions = dashboardStore.dashboards.map((i: any) => ({ key: i.id, label: i.name, @@ -27,7 +32,7 @@ export const showAddToDashboardModal = (metricId: string, dashboardStore: any) = }; Modal.confirm({ - title: 'Add to selected dashboard', + title: t('Add to selected dashboard'), icon: null, content: ( @@ -38,9 +43,9 @@ export const showAddToDashboardModal = (metricId: string, dashboardStore: any) = /> ), - cancelText: 'Cancel', + cancelText: t('Cancel'), onOk: onSave, - okText: 'Add', + okText: t('Add'), footer: (_, { OkBtn, CancelBtn }) => ( <> @@ -50,7 +55,8 @@ export const showAddToDashboardModal = (metricId: string, dashboardStore: any) = }); }; -const AddToDashboardButton = ({ metricId }: Props) => { +function AddToDashboardButton({ metricId }: Props) { + const { t } = useTranslation(); const { dashboardStore } = useStore(); return ( @@ -59,9 +65,9 @@ const AddToDashboardButton = ({ metricId }: Props) => { onClick={() => showAddToDashboardModal(metricId, dashboardStore)} icon={} > - Add to Dashboard + {t('Add to Dashboard')} ); -}; +} -export default AddToDashboardButton; \ No newline at end of file +export default AddToDashboardButton; diff --git a/frontend/app/components/Dashboard/components/Alerts/AlertForm/BottomButtons.tsx b/frontend/app/components/Dashboard/components/Alerts/AlertForm/BottomButtons.tsx index 5dbb460bd..1acc57fd8 100644 --- a/frontend/app/components/Dashboard/components/Alerts/AlertForm/BottomButtons.tsx +++ b/frontend/app/components/Dashboard/components/Alerts/AlertForm/BottomButtons.tsx @@ -1,42 +1,50 @@ -import React from 'react' -import { Icon } from 'UI' -import { Button } from 'antd' +import React from 'react'; +import { Icon } from 'UI'; +import { Button } from 'antd'; +import { useTranslation } from 'react-i18next'; interface IBottomButtons { - loading: boolean - deleting: boolean - instance: Alert - onDelete: (instance: Alert) => void + loading: boolean; + deleting: boolean; + instance: Alert; + onDelete: (instance: Alert) => void; } -function BottomButtons({ loading, instance, deleting, onDelete }: IBottomButtons) { +function BottomButtons({ + loading, + instance, + deleting, + onDelete, +}: IBottomButtons) { + const { t } = useTranslation(); return ( <> -
    - -
    -
    - {instance.exists() && ( +
    - )} -
    +
    +
    + {instance.exists() && ( + + )} +
    - ) + ); } -export default BottomButtons +export default BottomButtons; diff --git a/frontend/app/components/Dashboard/components/Alerts/AlertForm/Condition.tsx b/frontend/app/components/Dashboard/components/Alerts/AlertForm/Condition.tsx index 80a900895..e57530aee 100644 --- a/frontend/app/components/Dashboard/components/Alerts/AlertForm/Condition.tsx +++ b/frontend/app/components/Dashboard/components/Alerts/AlertForm/Condition.tsx @@ -2,20 +2,22 @@ import React from 'react'; import { Input } from 'UI'; import Select from 'Shared/Select'; import { alertConditions as conditions } from 'App/constants'; -import Alert from 'Types/alert' +import Alert from 'Types/alert'; +import { useTranslation } from 'react-i18next'; +import { TFunction } from 'i18next'; -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 }, +const thresholdOptions = (t: TFunction) => [ + { label: t('15 minutes'), value: 15 }, + { label: t('30 minutes'), value: 30 }, + { label: t('1 hour'), value: 60 }, + { label: t('2 hours'), value: 120 }, + { label: t('4 hours'), value: 240 }, + { label: t('1 day'), value: 1440 }, ]; -const changeOptions = [ - { label: 'change', value: 'change' }, - { label: '% change', value: 'percent' }, +const changeOptions = (t: TFunction) => [ + { label: t('change'), value: 'change' }, + { label: t('% change'), value: 'percent' }, ]; interface ICondition { @@ -39,15 +41,18 @@ function Condition({ unit, changeUnit, }: ICondition) { + const { t } = useTranslation(); return (
    {!isThreshold && (
    - + i.value === instance.query.left) || ''} - onChange={({ value }) => writeQueryOption(null, { name: 'left', value: value.value })} + value={ + triggerOptions.find((i) => i.value === instance.query.left) || '' + } + onChange={({ value }) => + writeQueryOption(null, { name: 'left', value: value.value }) + } />
    - +
    writeOption(null, { name: 'currentPeriod', value })} + onChange={({ value }) => + writeOption(null, { name: 'currentPeriod', value }) + } />
    {!isThreshold && (
    - + - ) : ( - + ) : ( + - + +
    + + + + - {/* + {/* */} - -
    - dashboard.update({ 'isPublic': !dashboard.isPublic }) } - /> -
    dashboard.update({ 'isPublic': !dashboard.isPublic }) }> - - Team can see and edit the dashboard. -
    -
    -
    - -
    - -
    - - -
    -
    - - )); + +
    + + dashboard.update({ isPublic: !dashboard.isPublic }) + } + /> +
    + dashboard.update({ isPublic: !dashboard.isPublic }) + } + > + + + {' '} + {t('Team can see and edit the dashboard.')} + +
    +
    +
    + + + +
    + + +
    +
    + + )); } export default DashboardEditModal; diff --git a/frontend/app/components/Dashboard/components/DashboardEditModal/index.ts b/frontend/app/components/Dashboard/components/DashboardEditModal/index.ts index c7f4d7b17..f27d977d2 100644 --- a/frontend/app/components/Dashboard/components/DashboardEditModal/index.ts +++ b/frontend/app/components/Dashboard/components/DashboardEditModal/index.ts @@ -1 +1 @@ -export { default } from './DashboardEditModal' \ No newline at end of file +export { default } from './DashboardEditModal'; diff --git a/frontend/app/components/Dashboard/components/DashboardForm/DashboardForm.tsx b/frontend/app/components/Dashboard/components/DashboardForm/DashboardForm.tsx index dc06c7a53..c98612605 100644 --- a/frontend/app/components/Dashboard/components/DashboardForm/DashboardForm.tsx +++ b/frontend/app/components/Dashboard/components/DashboardForm/DashboardForm.tsx @@ -3,57 +3,74 @@ import React from 'react'; import { Input } from 'UI'; import cn from 'classnames'; import { useStore } from 'App/mstore'; +import { useTranslation } from 'react-i18next'; -interface Props { -} +interface Props {} function DashboardForm(props: Props) { - const { dashboardStore } = useStore(); - const dashboard = dashboardStore.dashboardInstance; + const { t } = useTranslation(); + const { dashboardStore } = useStore(); + const dashboard = dashboardStore.dashboardInstance; - const write = ({ target: { value, name } }) => dashboard.update({ [ name ]: value }) - const writeRadio = ({ target: { value, name } }) => { - dashboard.update({ [name]: value === 'team' }); - } + const write = ({ target: { value, name } }) => + dashboard.update({ [name]: value }); + const writeRadio = ({ target: { value, name } }) => { + dashboard.update({ [name]: value === 'team' }); + }; - return useObserver(() => ( -
    -
    - - -
    + return useObserver(() => ( +
    +
    + + +
    -
    - +
    + -
    - +
    + - -
    -
    +
    - )); +
    +
    + )); } export default DashboardForm; diff --git a/frontend/app/components/Dashboard/components/DashboardForm/index.ts b/frontend/app/components/Dashboard/components/DashboardForm/index.ts index 01c5b0072..a5b3a736d 100644 --- a/frontend/app/components/Dashboard/components/DashboardForm/index.ts +++ b/frontend/app/components/Dashboard/components/DashboardForm/index.ts @@ -1 +1 @@ -export { default } from './DashboardForm'; \ No newline at end of file +export { default } from './DashboardForm'; diff --git a/frontend/app/components/Dashboard/components/DashboardHeader/DashboardHeader.tsx b/frontend/app/components/Dashboard/components/DashboardHeader/DashboardHeader.tsx index 033fe55da..adee84b0c 100644 --- a/frontend/app/components/Dashboard/components/DashboardHeader/DashboardHeader.tsx +++ b/frontend/app/components/Dashboard/components/DashboardHeader/DashboardHeader.tsx @@ -3,15 +3,16 @@ import BackButton from 'Shared/Breadcrumb/BackButton'; import { withSiteId } from 'App/routes'; import { withRouter, RouteComponentProps } from 'react-router-dom'; import { PageTitle, confirm } from 'UI'; -import { Tooltip, Popover, Button } from 'antd'; +import { Tooltip, Popover, Button } from 'antd'; import { PlusOutlined } from '@ant-design/icons'; import SelectDateRange from 'Shared/SelectDateRange'; import { useStore } from 'App/mstore'; -import DashboardOptions from '../DashboardOptions'; import withModal from 'App/components/Modal/withModal'; import { observer } from 'mobx-react-lite'; +import DashboardOptions from '../DashboardOptions'; import DashboardEditModal from '../DashboardEditModal'; import AddCardSection from '../AddCardSection/AddCardSection'; +import { useTranslation } from 'react-i18next'; interface IProps { siteId: string; @@ -21,6 +22,7 @@ interface IProps { type Props = IProps & RouteComponentProps; function DashboardHeader(props: Props) { + const { t } = useTranslation(); const { siteId } = props; const [popoverOpen, setPopoverOpen] = React.useState(false); const handleOpenChange = (open: boolean) => { @@ -29,7 +31,7 @@ function DashboardHeader(props: Props) { const { dashboardStore } = useStore(); const [focusTitle, setFocusedInput] = React.useState(true); const [showEditModal, setShowEditModal] = React.useState(false); - const period = dashboardStore.period; + const { period } = dashboardStore; const dashboard: any = dashboardStore.selectedDashboard; @@ -42,13 +44,15 @@ function DashboardHeader(props: Props) { const onDelete = async () => { if ( await confirm({ - header: 'Delete Dashboard', - confirmButton: 'Yes, delete', - confirmation: `Are you sure you want to permanently delete this Dashboard?`, + header: t('Delete Dashboard'), + confirmButton: t('Yes, delete'), + confirmation: t( + 'Are you sure you want to permanently delete this Dashboard?', + ), }) ) { dashboardStore.deleteDashboard(dashboard).then(() => { - props.history.push(withSiteId(`/dashboard`, siteId)); + props.history.push(withSiteId('/dashboard', siteId)); }); } }; @@ -60,19 +64,17 @@ function DashboardHeader(props: Props) { focusTitle={focusTitle} /> -
    -
    {dashboard?.name}
    + +
    + {' '} + {dashboard?.name} +
    } onClick={() => onEdit(true)} @@ -83,16 +85,15 @@ function DashboardHeader(props: Props) { className="flex items-center gap-2" style={{ flex: 1, justifyContent: 'end' }} > - - } overlayInnerStyle={{ padding: 0, borderRadius: '0.75rem' }} > - @@ -100,10 +101,10 @@ function DashboardHeader(props: Props) { style={{ width: '300px' }} period={period} onChange={(period: any) => dashboardStore.setPeriod(period)} - right={true} - isAnt={true} - useButtonStyle={true} - className='h-full' + right + isAnt + useButtonStyle + className="h-full" /> (null); const [focusTitle, setFocusedInput] = React.useState(true); const [showEditModal, setShowEditModal] = React.useState(false); @@ -51,9 +53,11 @@ function DashboardList() { if (!dashboard) return; if ( await confirm({ - header: 'Delete Dashboard', - confirmButton: 'Yes, delete', - confirmation: `Are you sure you want to permanently delete this Dashboard?`, + header: t('Delete Dashboard'), + confirmButton: t('Yes, delete'), + confirmation: t( + 'Are you sure you want to permanently delete this Dashboard?', + ), }) ) { void dashboardStore.deleteDashboard(dashboard); @@ -62,7 +66,7 @@ function DashboardList() { const tableConfig: TableColumnsType = [ { - title: 'Title', + title: t('Title'), dataIndex: 'name', width: '25%', sorter: (a, b) => a.name?.localeCompare(b.name), @@ -70,7 +74,7 @@ function DashboardList() { render: (t) =>
    {t}
    , }, { - title: 'Owner', + title: t('Owner'), dataIndex: 'owner', width: '16.67%', sorter: (a, b) => a.owner?.localeCompare(b.owner), @@ -78,7 +82,7 @@ function DashboardList() { render: (owner) =>
    {owner}
    , }, { - title: 'Last Modified', + title: t('Last Modified'), dataIndex: 'updatedAt', width: '16.67%', sorter: (a, b) => a.updatedAt.toMillis() - b.updatedAt.toMillis(), @@ -88,10 +92,10 @@ function DashboardList() { { title: ( -
    -
    Visibility
    +
    +
    {t('Visibility')}
    @@ -117,7 +121,7 @@ function DashboardList() { bordered={false} className="rounded-lg" > - {isPublic ? 'Team' : 'Private'} + {isPublic ? t('Team') : t('Private')} ), }, @@ -131,23 +135,23 @@ function DashboardList() { , + icon: , key: 'rename', - label: 'Rename', + label: t('Rename'), }, { - icon: , + icon: , key: 'access', - label: 'Visibility & Access', + label: t('Visibility & Access'), }, { - icon: , + icon: , key: 'delete', - label: 'Delete', + label: t('Delete'), }, ], onClick: async ({ key }) => { @@ -162,7 +166,7 @@ function DashboardList() { }} > : null} -
    - {metric.name} -
    - - -
    - {props.extra} - {/**/} - - -
    + const addCardToDashboard = async (dashboardId: string, metricId: string) => + dashboardStore.addWidgetToDashboard( + dashboardStore.getDashboard(parseInt(dashboardId, 10))!, + [metricId], ); + + const createCard = async () => { + const isClickMap = metric.metricType === HEATMAP; + if (isClickMap) { + try { + metric.thumbnail = await renderClickmapThumbnail(); + } catch (e) { + console.error(e); + } + } + + const savedMetric = await metricStore.save(metric); + return savedMetric.metricId; + }; + + const createDashboardAndAddCard = async () => { + const cardId = await createCard(); + + if (dashboardId) { + await addCardToDashboard(dashboardId, cardId); + void dashboardStore.fetch(dashboardId); + props.onAdded?.(); + } else if (isItDashboard) { + const dashboardId = await createNewDashboard(); + await addCardToDashboard(dashboardId, cardId); + history.replace(`${history.location.pathname}/${dashboardId}`); + } else { + history.replace(`${history.location.pathname}/${cardId}`); + } + }; + + return ( +
    +
    + + {props.onBack ? ( + + ) : null} +
    {metric.name}
    +
    + +
    + {props.extra} + {/* */} + + +
    + ); } export default CreateCard; diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/ExampleCards.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/ExampleCards.tsx index 17945fbfb..f5c01655b 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/ExampleCards.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/ExampleCards.tsx @@ -1,10 +1,3 @@ -import ExampleFunnel from './Examples/Funnel'; -import ExamplePath from './Examples/Path'; -import ExampleTrend from './Examples/Trend'; -import ByBrowser from './Examples/SessionsBy/ByBrowser'; -import BySystem from './Examples/SessionsBy/BySystem'; -import ByCountry from './Examples/SessionsBy/ByCountry'; -import ByUrl from './Examples/SessionsBy/ByUrl'; import { HEATMAP, ERRORS, @@ -12,7 +5,7 @@ import { TABLE, TIMESERIES, USER_PATH, -} from "App/constants/card"; +} from 'App/constants/card'; import { FilterKey } from 'Types/filter/filterType'; import { BarChart, TrendingUp, SearchSlash } from 'lucide-react'; import ByUser from 'Components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByUser'; @@ -20,6 +13,14 @@ import HeatmapsExample from 'Components/Dashboard/components/DashboardList/NewDa import ByReferrer from 'Components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByRferrer'; import ByFetch from 'Components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByFecth'; import TableOfErrors from 'Components/Dashboard/components/DashboardList/NewDashModal/Examples/TableOfErrors'; +import ByUrl from './Examples/SessionsBy/ByUrl'; +import ByCountry from './Examples/SessionsBy/ByCountry'; +import BySystem from './Examples/SessionsBy/BySystem'; +import ByBrowser from './Examples/SessionsBy/ByBrowser'; +import ExampleTrend from './Examples/Trend'; +import ExamplePath from './Examples/Path'; +import ExampleFunnel from './Examples/Funnel'; +import { TFunction } from 'i18next'; export const CARD_CATEGORY = { PRODUCT_ANALYTICS: 'product-analytics', @@ -27,10 +28,25 @@ export const CARD_CATEGORY = { ERROR_TRACKING: 'error-tracking', }; -export const CARD_CATEGORIES = [ - { key: CARD_CATEGORY.PRODUCT_ANALYTICS, label: 'Product Analytics', icon: TrendingUp, types: [USER_PATH, ERRORS] }, - { key: CARD_CATEGORY.WEB_ANALYTICS, label: 'Web Analytics', icon: BarChart, types: [TABLE] }, - { key: CARD_CATEGORY.ERROR_TRACKING, label: 'Monitors', icon: SearchSlash, types: [] }, +export const CARD_CATEGORIES = (t: TFunction) => [ + { + key: CARD_CATEGORY.PRODUCT_ANALYTICS, + label: t('Product Analytics'), + icon: TrendingUp, + types: [USER_PATH, ERRORS], + }, + { + key: CARD_CATEGORY.WEB_ANALYTICS, + label: t('Web Analytics'), + icon: BarChart, + types: [TABLE], + }, + { + key: CARD_CATEGORY.ERROR_TRACKING, + label: t('Monitors'), + icon: SearchSlash, + types: [], + }, ]; export interface CardType { @@ -48,45 +64,43 @@ export interface CardType { viewType?: string; } -export const CARD_LIST: CardType[] = [ +export const CARD_LIST: (t: TFunction) => CardType[] = (t) => [ { title: 'Untitled Funnel', key: FUNNEL, cardType: FUNNEL, - category: CARD_CATEGORIES[0].key, + category: CARD_CATEGORIES(t)[0].key, example: ExampleFunnel, width: 4, height: 300, data: { stages: [ { - 'value': [ - '/sessions' - ], - 'type': 'location', - 'operator': 'contains', - 'count': 1586, - 'dropPct': null, - 'dropDueToIssues': 0 + value: ['/sessions'], + type: 'location', + operator: 'contains', + count: 1586, + dropPct: null, + dropDueToIssues: 0, }, { - 'value': [], - 'type': 'click', - 'operator': 'onAny', - 'count': 1292, - 'dropPct': 18, - 'dropDueToIssues': 294 - } + value: [], + type: 'click', + operator: 'onAny', + count: 1292, + dropPct: 18, + dropDueToIssues: 294, + }, ], // totalDropDueToIssues: 294 - } + }, }, { title: 'Untitled Heatmaps', key: HEATMAP, cardType: HEATMAP, metricOf: 'heatMapUrl', - category: CARD_CATEGORIES[0].key, + category: CARD_CATEGORIES(t)[0].key, example: HeatmapsExample, viewType: 'chart', }, @@ -94,48 +108,43 @@ export const CARD_LIST: CardType[] = [ title: 'Untitled Journey', key: USER_PATH, cardType: USER_PATH, - category: CARD_CATEGORIES[0].key, - example: ExamplePath + category: CARD_CATEGORIES(t)[0].key, + example: ExamplePath, }, { title: 'Untitled Trend', key: TIMESERIES, cardType: TIMESERIES, metricOf: 'sessionCount', - category: CARD_CATEGORIES[0].key, + category: CARD_CATEGORIES(t)[0].key, data: { - chart: generateTimeSeriesData(), + chart: generateTimeSeriesData(t), label: 'Number of Sessions', - namesMap: [ - 'Series 1' - ] + namesMap: ['Series 1'], }, - example: ExampleTrend + example: ExampleTrend, }, { title: 'Untitled Users Trend', - key: TIMESERIES + '_userCount', + key: `${TIMESERIES}_userCount`, cardType: TIMESERIES, metricOf: 'userCount', - category: CARD_CATEGORIES[0].key, + category: CARD_CATEGORIES(t)[0].key, data: { - chart: generateTimeSeriesData(), + chart: generateTimeSeriesData(t), label: 'Number of Users', - namesMap: [ - 'Series 1' - ] + namesMap: ['Series 1'], }, - example: ExampleTrend + example: ExampleTrend, }, - // Web analytics { title: 'Untitled Top Users', key: FilterKey.USERID, cardType: TABLE, metricOf: FilterKey.USERID, - category: CARD_CATEGORIES[1].key, + category: CARD_CATEGORIES(t)[1].key, example: ByUser, viewType: 'table', }, @@ -145,7 +154,7 @@ export const CARD_LIST: CardType[] = [ key: FilterKey.USER_BROWSER, cardType: TABLE, metricOf: FilterKey.USER_BROWSER, - category: CARD_CATEGORIES[1].key, + category: CARD_CATEGORIES(t)[1].key, example: ByBrowser, viewType: 'table', }, @@ -154,7 +163,7 @@ export const CARD_LIST: CardType[] = [ // key: TYPE.SESSIONS_BY_SYSTEM, // cardType: TABLE, // metricOf: FilterKey.USER_OS, - // category: CARD_CATEGORIES[1].key, + // category: CARD_CATEGORIES(t)[1].key, // example: BySystem, // }, { @@ -162,7 +171,7 @@ export const CARD_LIST: CardType[] = [ key: FilterKey.USER_COUNTRY, cardType: TABLE, metricOf: FilterKey.USER_COUNTRY, - category: CARD_CATEGORIES[1].key, + category: CARD_CATEGORIES(t)[1].key, example: ByCountry, viewType: 'table', }, @@ -172,7 +181,7 @@ export const CARD_LIST: CardType[] = [ key: FilterKey.USER_DEVICE, cardType: TABLE, metricOf: FilterKey.USER_DEVICE, - category: CARD_CATEGORIES[1].key, + category: CARD_CATEGORIES(t)[1].key, example: BySystem, viewType: 'table', }, @@ -181,7 +190,7 @@ export const CARD_LIST: CardType[] = [ key: FilterKey.LOCATION, cardType: TABLE, metricOf: FilterKey.LOCATION, - category: CARD_CATEGORIES[1].key, + category: CARD_CATEGORIES(t)[1].key, example: ByUrl, viewType: 'table', }, @@ -191,7 +200,7 @@ export const CARD_LIST: CardType[] = [ key: FilterKey.REFERRER, cardType: TABLE, metricOf: FilterKey.REFERRER, - category: CARD_CATEGORIES[1].key, + category: CARD_CATEGORIES(t)[1].key, example: ByReferrer, viewType: 'table', }, @@ -202,11 +211,11 @@ export const CARD_LIST: CardType[] = [ key: FilterKey.ERRORS, cardType: TABLE, metricOf: FilterKey.ERRORS, - category: CARD_CATEGORIES[2].key, + category: CARD_CATEGORIES(t)[2].key, data: { - chart: generateBarChartData(), + chart: generateBarChartData(t), hideLegend: true, - label: 'Number of Sessions' + label: 'Number of Sessions', }, width: 4, height: 336, @@ -218,105 +227,115 @@ export const CARD_LIST: CardType[] = [ key: FilterKey.FETCH, cardType: TABLE, metricOf: FilterKey.FETCH, - category: CARD_CATEGORIES[2].key, + category: CARD_CATEGORIES(t)[2].key, example: ByFetch, viewType: 'table', }, { title: 'Untitled Sessions with 4xx/5xx Requests', - key: TIMESERIES + '_4xx_requests', + key: `${TIMESERIES}_4xx_requests`, cardType: TIMESERIES, metricOf: 'sessionCount', category: CARD_CATEGORY.ERROR_TRACKING, data: { - chart: generateTimeSeriesData(), + chart: generateTimeSeriesData(t), label: 'Number of Sessions', - namesMap: [ - 'Series 1' - ] + namesMap: ['Series 1'], }, filters: [ { - "type": "fetch", - "isEvent": true, - "value": [], - "operator": "is", - "filters": [ + type: 'fetch', + isEvent: true, + value: [], + operator: 'is', + filters: [ { - "type": "fetchStatusCode", - "isEvent": false, - "value": [ - "400" - ], - "operator": ">=", - "filters": [] + type: 'fetchStatusCode', + isEvent: false, + value: ['400'], + operator: '>=', + filters: [], }, - ] - } + ], + }, ], - example: ExampleTrend + example: ExampleTrend, }, { title: 'Untitled Sessions with Slow Network Requests', - key: TIMESERIES + '_slow_network_requests', + key: `${TIMESERIES}_slow_network_requests`, cardType: TIMESERIES, metricOf: 'sessionCount', category: CARD_CATEGORY.ERROR_TRACKING, data: { - chart: generateTimeSeriesData(), + chart: generateTimeSeriesData(t), label: 'Number of Sessions', - namesMap: [ - 'Series 1' - ] + namesMap: ['Series 1'], }, filters: [ { - "type": "fetch", - "isEvent": true, - "value": [], - "operator": "is", - "filters": [ + type: 'fetch', + isEvent: true, + value: [], + operator: 'is', + filters: [ { - "type": "fetchDuration", - "isEvent": false, - "value": [ - "5000" - ], - "operator": ">=", - "filters": [] + type: 'fetchDuration', + isEvent: false, + value: ['5000'], + operator: '>=', + filters: [], }, - ] - } + ], + }, ], - example: ExampleTrend + example: ExampleTrend, }, ]; -function generateTimeSeriesData(): any[] { - const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul']; +function generateTimeSeriesData(t: TFunction): any[] { + const months = [ + t('Jan'), + t('Feb'), + t('Mar'), + t('Apr'), + t('May'), + t('Jun'), + t('Jul'), + ]; const pointsPerMonth = 3; // Number of points for each month const data = months.flatMap((month, monthIndex) => Array.from({ length: pointsPerMonth }, (_, pointIndex) => ({ time: month, 'Series 1': Math.floor(Math.random() * 90), - timestamp: Date.now() + (monthIndex * pointsPerMonth + pointIndex) * 86400000 - })) + timestamp: + Date.now() + (monthIndex * pointsPerMonth + pointIndex) * 86400000, + })), ); return data; } -function generateAreaData(): any[] { - const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul']; +function generateAreaData(t: TFunction): any[] { + const months = [ + t('Jan'), + t('Feb'), + t('Mar'), + t('Apr'), + t('May'), + t('Jun'), + t('Jul'), + ]; const pointsPerMonth = 3; // Number of points for each month const data = months.flatMap((month, monthIndex) => Array.from({ length: pointsPerMonth }, (_, pointIndex) => ({ time: month, - 'value': Math.floor(Math.random() * 90), - timestamp: Date.now() + (monthIndex * pointsPerMonth + pointIndex) * 86400000 - })) + value: Math.floor(Math.random() * 90), + timestamp: + Date.now() + (monthIndex * pointsPerMonth + pointIndex) * 86400000, + })), ); return data; @@ -326,21 +345,37 @@ function generateRandomValue(min: number, max: number): number { return Math.floor(Math.random() * (max - min + 1)) + min; } -function generateBarChartData(): any[] { - const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul']; - return months.map(month => ({ +function generateBarChartData(t: TFunction): any[] { + const months = [ + t('Jan'), + t('Feb'), + t('Mar'), + t('Apr'), + t('May'), + t('Jun'), + t('Jul'), + ]; + return months.map((month) => ({ time: month, - value: generateRandomValue(1000, 5000) + value: generateRandomValue(1000, 5000), })); } -function generateStackedBarChartData(keys: any): any[] { - const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul']; - return months.map(month => ({ +function generateStackedBarChartData(keys: any, t: TFunction): any[] { + const months = [ + t('Jan'), + t('Feb'), + t('Mar'), + t('Apr'), + t('May'), + t('Jun'), + t('Jul'), + ]; + return months.map((month) => ({ time: month, ...keys.reduce((acc: any, key: any) => { acc[key] = generateRandomValue(1000, 5000); return acc; - }, {}) + }, {}), })); } diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/AreaChartCard.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/AreaChartCard.tsx index 988311c54..4d2880e66 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/AreaChartCard.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/AreaChartCard.tsx @@ -1,23 +1,26 @@ import React from 'react'; -import {NoContent} from 'UI'; +import { NoContent } from 'UI'; import { InfoCircleOutlined } from '@ant-design/icons'; import { - AreaChart, Area, - CartesianGrid, Tooltip, - ResponsiveContainer, - XAxis, YAxis + AreaChart, + Area, + CartesianGrid, + Tooltip, + ResponsiveContainer, + XAxis, + YAxis, } from 'recharts'; -import {NO_METRIC_DATA} from 'App/constants/messages' -import {AvgLabel, Styles} from "Components/Dashboard/Widgets/common"; -import ExCard from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/ExCard"; +import { NO_METRIC_DATA } from 'App/constants/messages'; +import { AvgLabel, Styles } from 'Components/Dashboard/Widgets/common'; +import ExCard from 'Components/Dashboard/components/DashboardList/NewDashModal/Examples/ExCard'; interface Props { - title: string; - type: string; - onCard: (card: string) => void; - onClick?: any; - data?: any, + title: string; + type: string; + onCard: (card: string) => void; + onClick?: any; + data?: any; } // interface Props { @@ -26,62 +29,63 @@ interface Props { // } function AreaChartCard(props: Props) { - const {data} = props; - const gradientDef = Styles.gradientDef(); + const { data } = props; + const gradientDef = Styles.gradientDef(); - return ( - -
    {props.title}
    -
    - } - > - - { NO_METRIC_DATA } -
    - } - show={data?.chart.length === 0} - > - <> - {/*
    */} - {/* */} - {/*
    */} - - - {gradientDef} - - - Styles.tickFormatter(val)} - label={{...Styles.axisLabelLeft, value: data?.label}} - /> - - - - - - - - ); + return ( + +
    {props.title}
    +
    + } + > + + {NO_METRIC_DATA} +
    + } + show={data?.chart.length === 0} + > + <> + {/*
    */} + {/* */} + {/*
    */} + + + {gradientDef} + + + Styles.tickFormatter(val)} + label={{ ...Styles.axisLabelLeft, value: data?.label }} + /> + + + + + + + + ); } export default AreaChartCard; diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/BarChart.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/BarChart.tsx index 8d426ec6f..1f32fa3ac 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/BarChart.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/BarChart.tsx @@ -1,30 +1,48 @@ import React from 'react'; -import ExCard from './ExCard'; -import { Bar, BarChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; +import { + Bar, + BarChart, + CartesianGrid, + Legend, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; import { Styles } from 'Components/Dashboard/Widgets/common'; +import ExCard from './ExCard'; interface Props { title: string; type: string; onCard: (card: string) => void; onClick?: any; - data?: any, - hideLegend?: boolean, + data?: any; + hideLegend?: boolean; } function BarChartCard(props: Props) { - const keys = props.data ? Object.keys(props.data.chart[0]).filter(key => key !== 'time') : []; + const keys = props.data + ? Object.keys(props.data.chart[0]).filter((key) => key !== 'time') + : []; return ( - + Styles.tickFormatter(val)} - label={{ ...Styles.axisLabelLeft, value: props.data?.label || 'Number of Errors' }} + tickFormatter={(val) => Styles.tickFormatter(val)} + label={{ + ...Styles.axisLabelLeft, + value: props.data?.label || 'Number of Errors', + }} allowDecimals={false} /> {!props.hideLegend && } diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Bars.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Bars.tsx index e77764c0f..d71583ace 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Bars.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Bars.tsx @@ -1,61 +1,58 @@ import React from 'react'; -import ExCard from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/ExCard"; -import {List, Progress} from "antd"; - +import ExCard from 'Components/Dashboard/components/DashboardList/NewDashModal/Examples/ExCard'; +import { List, Progress } from 'antd'; interface Props { - title: string; - type: string; - onCard: (card: string) => void; - data?: any; + title: string; + type: string; + onCard: (card: string) => void; + data?: any; } function Bars(props: Props) { - const _data = props.data || { - total: 90, - values: [ - { - "label": "company.domain.com", - "value": 89 - }, - { - "label": "openreplay.com", - "value": 15 - } - ] - } - return ( - - - ( - - - {item.label} - {item.value} -
    - )} - description={( - - )} - /> - - )} + const _data = props.data || { + total: 90, + values: [ + { + label: 'company.domain.com', + value: 89, + }, + { + label: 'openreplay.com', + value: 15, + }, + ], + }; + return ( + + ( + + + {item.label} + {item.value} +
    + } + description={ + + } /> - - ); + + )} + /> + + ); } export default Bars; diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/CallsWithErrorsExample.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/CallsWithErrorsExample.tsx index b0f056569..30ac5248d 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/CallsWithErrorsExample.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/CallsWithErrorsExample.tsx @@ -1,23 +1,23 @@ import React from 'react'; -import ExCard from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/ExCard"; -import CallWithErrors from "Components/Dashboard/Widgets/PredefinedWidgets/CallWithErrors"; +import ExCard from 'Components/Dashboard/components/DashboardList/NewDashModal/Examples/ExCard'; +import CallWithErrors from 'Components/Dashboard/Widgets/PredefinedWidgets/CallWithErrors'; interface Props { - title: string; - type: string; - onCard: (card: string) => void; - onClick?: any; - data?: any, + title: string; + type: string; + onCard: (card: string) => void; + onClick?: any; + data?: any; } function CallsWithErrorsExample(props: Props) { - return ( - -
    -
    - ); + return ( + +
    + +
    +
    + ); } export default CallsWithErrorsExample; diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Count.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Count.tsx index 6dd084040..f127d4f39 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Count.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Count.tsx @@ -9,35 +9,38 @@ import { import React from 'react'; import ExCard from './ExCard'; +import { useTranslation } from 'react-i18next'; +import { TFunction } from 'i18next'; -const TYPES = { - Frustrations: 'frustrations', - Errors: 'errors', - Users: 'users', -}; +const TYPES = (t: TFunction) => ({ + Frustrations: t('frustrations'), + Errors: t('errors'), + Users: t('users'), +}); function ExampleCount(props: any) { - const [type, setType] = React.useState(TYPES.Frustrations); + const { t } = useTranslation(); + const [type, setType] = React.useState(TYPES(t).Frustrations); const el = { - [TYPES.Frustrations]: , - [TYPES.Errors]: , - [TYPES.Users]: , + [TYPES(t).Frustrations]: , + [TYPES(t).Errors]: , + [TYPES(t).Users]: , }; return ( +
    {props.title}
    -
    +
    setType(v)} />
    @@ -50,33 +53,34 @@ function ExampleCount(props: any) { } export function Frustrations() { + const { t } = useTranslation(); const rows = [ { - label: 'Rage Clicks', + label: t('Rage Clicks'), progress: 25, value: 100, icon: , }, { - label: 'Dead Clicks', + label: t('Dead Clicks'), progress: 75, value: 75, icon: , }, { - label: '4XX Pages', + label: t('4XX Pages'), progress: 50, value: 50, icon: , }, { - label: 'Mouse Trashing', + label: t('Mouse Trashing'), progress: 10, value: 25, icon: , }, { - label: 'Excessive Scrolling', + label: t('Excessive Scrolling'), progress: 10, value: 10, icon: , @@ -85,13 +89,9 @@ export function Frustrations() { const lineWidth = 140; return ( -
    +
    {rows.map((r) => ( -
    +
    {r.icon}
    {r.label}
    @@ -101,7 +101,7 @@ export function Frustrations() { width: lineWidth * (0.01 * r.progress), background: '#394EFF', }} - className={'rounded-l'} + className="rounded-l" />
    -
    {r.value}
    +
    {r.value}
    ))}
    @@ -120,50 +120,47 @@ export function Frustrations() { } export function Errors() { + const { t } = useTranslation(); const rows = [ { - label: 'HTTP response status code (404 Not Found)', + label: t('HTTP response status code (404 Not Found)'), value: 500, progress: 90, - icon:
    4XX
    , + icon:
    {t('4XX')}
    , }, { - label: 'Cross-origin request blocked', + label: t('Cross-origin request blocked'), value: 300, progress: 60, - icon:
    CROS
    , + icon:
    {t('CROS')}
    , }, { - label: 'Reference error', + label: t('Reference error'), value: 200, progress: 40, - icon:
    RE
    , + icon:
    {t('RE')}
    , }, { label: 'Unhandled Promise Rejection', value: 50, progress: 20, - icon:
    NULL
    , + icon:
    {t('NULL')}
    , }, { label: 'Failed Network Request', value: 10, progress: 5, - icon:
    XHR
    , + icon:
    {t('XHR')}
    , }, ]; const lineWidth = 270; return ( -
    +
    {rows.map((r) => ( -
    +
    {r.icon} -
    +
    {r.label}
    -
    {r.value}
    +
    {r.value}
    ))}
    @@ -216,18 +213,14 @@ export function Users() { ]; return ( -
    +
    {rows.map((r) => ( -
    +
    {r.label[0].toUpperCase()} -
    +
    {r.label}
    -
    {r.value}
    +
    {r.value}
    ))}
    @@ -254,7 +247,7 @@ export function Circle({ return (
    {children} diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/ExCard.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/ExCard.tsx index c846eb794..566bed126 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/ExCard.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/ExCard.tsx @@ -1,30 +1,37 @@ -import React from 'react' +import React from 'react'; function ExCard({ - title, - children, - type, - onCard, - height, - }: { - title: React.ReactNode; - children: React.ReactNode; - type: string; - onCard: (card: string) => void; - height?: number; + title, + children, + type, + onCard, + height, +}: { + title: React.ReactNode; + children: React.ReactNode; + type: string; + onCard: (card: string) => void; + height?: number; }) { - return ( -
    -
    onCard(type)}>
    -
    {title}
    -
    onCard(type)}>{children}
    -
    - ); + return ( +
    +
    onCard(type)} + /> +
    {title}
    +
    onCard(type)} + > + {children} +
    +
    + ); } -export default ExCard +export default ExCard; diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Funnel.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Funnel.tsx index c44920110..bf838d553 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Funnel.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Funnel.tsx @@ -1,25 +1,23 @@ import React from 'react'; -import ExCard from './ExCard'; import FunnelWidget from 'Components/Funnels/FunnelWidget/FunnelWidget'; import Funnel from 'App/mstore/types/funnel'; +import ExCard from './ExCard'; interface Props { title: string; type: string; onCard: (card: string) => void; - data?: any, + data?: any; } function ExampleFunnel(props: Props) { const _data = { - funnel: new Funnel().fromJSON(props.data) + funnel: new Funnel().fromJSON(props.data), }; return ( - - + + ); } diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/HeatmapsExample.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/HeatmapsExample.tsx index 625d109df..7a6937faa 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/HeatmapsExample.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/HeatmapsExample.tsx @@ -12,7 +12,10 @@ function HeatmapsExample(props: Props) { const canvasRef = React.useRef(null); useEffect(() => { - const pointMap: Record = {}; + const pointMap: Record< + string, + { times: number; data: number[]; original: any } + > = {}; let maxIntensity = 0; for (let i = 0; i < 20; i++) { @@ -23,7 +26,7 @@ function HeatmapsExample(props: Props) { pointMap[key] = { times: Math.floor(Math.random() * 100), data: [x, y], - original: { x, y } + original: { x, y }, }; } @@ -45,14 +48,18 @@ function HeatmapsExample(props: Props) { .draw(); }, []); - // const data = {}; return ( - - + + ); } diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/InsightsExample.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/InsightsExample.tsx index b3271aa33..90cf82f5e 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/InsightsExample.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/InsightsExample.tsx @@ -1,56 +1,62 @@ import React from 'react'; -import ExCard from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/ExCard"; -import InsightsCard from "Components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard"; -import {InsightIssue} from "App/mstore/types/widget"; +import ExCard from 'Components/Dashboard/components/DashboardList/NewDashModal/Examples/ExCard'; +import InsightsCard from 'Components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard'; +import { InsightIssue } from 'App/mstore/types/widget'; interface Props { - title: string; - type: string; - onCard: (card: string) => void; + title: string; + type: string; + onCard: (card: string) => void; } function InsightsExample(props: Props) { - const data = { - issues: [ - { - "category": "errors", - "name": "Error: Invalid unit value NaN", - "value": 562, - "oldValue": null, - "ratio": 7.472410583698976, - "change": 1, - "isNew": true - }, - { - "category": "errors", - "name": "TypeError: e.node.getContext is not a function", - "value": 128, - "oldValue": 1, - "ratio": 1.7019013429065284, - "change": 12700.0, - "isNew": false - }, - { - "category": "errors", - "name": "Unhandled Promise Rejection: {\"message\":\"! POST error on /client/members; 400\",\"response\":{}}", - "value": 26, - "oldValue": null, - "ratio": 0.34569871027788857, - "change": 1, - "isNew": true - } - ].map( - (i: any) => - new InsightIssue(i.category, i.name, i.ratio, i.oldValue, i.value, i.change, i.isNew) - ) - } - return ( - - - - ); + const data = { + issues: [ + { + category: 'errors', + name: 'Error: Invalid unit value NaN', + value: 562, + oldValue: null, + ratio: 7.472410583698976, + change: 1, + isNew: true, + }, + { + category: 'errors', + name: 'TypeError: e.node.getContext is not a function', + value: 128, + oldValue: 1, + ratio: 1.7019013429065284, + change: 12700.0, + isNew: false, + }, + { + category: 'errors', + name: 'Unhandled Promise Rejection: {"message":"! POST error on /client/members; 400","response":{}}', + value: 26, + oldValue: null, + ratio: 0.34569871027788857, + change: 1, + isNew: true, + }, + ].map( + (i: any) => + new InsightIssue( + i.category, + i.name, + i.ratio, + i.oldValue, + i.value, + i.change, + i.isNew, + ), + ), + }; + return ( + + + + ); } export default InsightsExample; diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/PageResponseTimeDistributionExample.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/PageResponseTimeDistributionExample.tsx index e0feafe0a..50c59b6e6 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/PageResponseTimeDistributionExample.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/PageResponseTimeDistributionExample.tsx @@ -3,7 +3,6 @@ import ExCard from 'Components/Dashboard/components/DashboardList/NewDashModal/E import CustomMetricOverviewChart from 'Components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricOverviewChart'; import ResponseTimeDistribution from 'Components/Dashboard/Widgets/PredefinedWidgets/ResponseTimeDistribution'; - interface Props { title: string; type: string; @@ -12,12 +11,10 @@ interface Props { function PageResponseTimeDistributionExample(props: Props) { const data = { - chart: [] - } + chart: [], + }; return ( - + ); diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Path.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Path.tsx index 70044e102..3bb0d4d78 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Path.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Path.tsx @@ -1,59 +1,57 @@ import React from 'react'; -import {ResponsiveContainer, Sankey} from 'recharts'; +import { ResponsiveContainer, Sankey } from 'recharts'; import CustomLink from 'App/components/shared/Insights/SankeyChart/CustomLink'; import CustomNode from 'App/components/shared/Insights/SankeyChart/CustomNode'; +import { USER_PATH } from 'App/constants/card'; import ExCard from './ExCard'; -import {USER_PATH} from "App/constants/card"; function ExamplePath(props: any) { - const data = { - nodes: [ - {idd: 0, name: 'Home'}, - {idd: 1, name: 'Google'}, - {idd: 2, name: 'Facebook'}, - {idd: 3, name: 'Search'}, - {idd: 4, name: 'Product'}, - {idd: 5, name: 'Chart'}, - ], - links: [ - {source: 0, target: 3, value: 40}, - {source: 0, target: 4, value: 60}, + const data = { + nodes: [ + { idd: 0, name: 'Home' }, + { idd: 1, name: 'Google' }, + { idd: 2, name: 'Facebook' }, + { idd: 3, name: 'Search' }, + { idd: 4, name: 'Product' }, + { idd: 5, name: 'Chart' }, + ], + links: [ + { source: 0, target: 3, value: 40 }, + { source: 0, target: 4, value: 60 }, - {source: 1, target: 3, value: 100}, - {source: 2, target: 3, value: 100}, + { source: 1, target: 3, value: 100 }, + { source: 2, target: 3, value: 100 }, - {source: 3, target: 4, value: 50}, - {source: 3, target: 5, value: 50}, + { source: 3, target: 4, value: 50 }, + { source: 3, target: 5, value: 50 }, - {source: 4, target: 5, value: 15}, - ], - }; + { source: 4, target: 5, value: 15 }, + ], + }; - return ( - + + } + link={(linkProps) => } + data={data} > - - } - link={(linkProps) => } - data={data} - > - - - - - - - - - - ); + + + + + + + + + + ); } -export default ExamplePath +export default ExamplePath; diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/PerfBreakdown.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/PerfBreakdown.tsx index d6c1abfd0..dbe7bbfbe 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/PerfBreakdown.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/PerfBreakdown.tsx @@ -2,8 +2,10 @@ import { GitCommitHorizontal } from 'lucide-react'; import React from 'react'; import ExCard from './ExCard'; +import { useTranslation } from 'react-i18next'; function PerfBreakdown(props: any) { + const { t } = useTranslation(); const rows = [ ['5K', '1K'], ['4K', '750'], @@ -11,7 +13,7 @@ function PerfBreakdown(props: any) { ['2K', '250'], ['1K', '0'], ]; - const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May']; + const months = [t('Jan'), t('Feb'), t('Mar'), t('Apr'), t('May')]; const values = [ [3, 1, 9], [2, 4, 10], @@ -21,32 +23,30 @@ function PerfBreakdown(props: any) { ]; const bgs = ['#E2E4F6', '#A7BFFF', '#394EFF']; return ( - -
    -
    + +
    +
    {rows.map((r) => ( -
    -
    {r[0]}
    -
    -
    {r[1]}
    +
    +
    {r[0]}
    +
    +
    {r[1]}
    ))}
    -
    +
    {months.map((m, i) => ( -
    +
    {m}
    {values[i].map((v, bg) => (
    @@ -83,18 +83,18 @@ function PerfBreakdown(props: any) {
    -
    -
    -
    -
    XHR
    +
    +
    +
    +
    {t('XHR')}
    -
    -
    -
    Other
    +
    +
    +
    {t('Other')}
    -
    - -
    Response End
    +
    + +
    {t('Response End')}
    diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByBrowser.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByBrowser.tsx index 3f83e8e0a..48aac4cef 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByBrowser.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByBrowser.tsx @@ -11,42 +11,36 @@ function ByBrowser(props: any) { label: 'Chrome', progress: 85, value: '2.5K', - icon: , + icon: , }, { label: 'Edge', progress: 25, value: '405', - icon: , + icon: , }, { label: 'Safari', progress: 5, value: '302', - icon: , + icon: , }, { label: 'Firefox', progress: 3, value: '194', - icon: , + icon: , }, { label: 'Opera', progress: 1, value: '57', - icon: , + icon: , }, ]; const lineWidth = 200; - return ( - - ); + return ; } export default ByBrowser; diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByCountry.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByCountry.tsx index 61381824b..7829a33e7 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByCountry.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByCountry.tsx @@ -10,41 +10,35 @@ function ByCountry(props: any) { label: 'United States', progress: 70, value: '165K', - icon: , + icon: , }, { label: 'India', progress: 25, value: '100K', - icon: , + icon: , }, { label: 'United Kingdom', progress: 10, value: '50K', - icon: , + icon: , }, { label: 'France', progress: 7, value: '30K', - icon: , + icon: , }, { label: 'Germany', progress: 4, value: '20K', - icon: , + icon: , }, ]; - return ( - - ); + return ; } export default ByCountry; diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByFecth.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByFecth.tsx index 5a4e4f5a8..d190b739f 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByFecth.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByFecth.tsx @@ -10,39 +10,33 @@ function ByFetch(props: any) { ptitle: 'Dresses', value: '500', progress: 75, - icon: + icon: , }, { label: 'https://www.reddit.com', ptitle: 'Search: summer dresses', value: '306', progress: 60, - icon: + icon: , }, { label: 'https://www.company.com/account/orders', ptitle: 'Account: Orders', value: '198', progress: 30, - icon: + icon: , }, { label: 'android-app://com.slack/', ptitle: 'Checkout: Confirmation', value: '47', progress: 15, - icon: - } + icon: , + }, ]; const lineWidth = 240; - return ( - - ); + return ; } export default ByFetch; diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByIssues.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByIssues.tsx index a8af2fd31..d96d4466d 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByIssues.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByIssues.tsx @@ -1,46 +1,40 @@ import React from 'react'; -import {Icon} from 'UI'; +import { Icon } from 'UI'; import ExCard from '../ExCard'; import ByComponent from './Component'; function ByIssues(props: any) { - const rows = [ - { - label: 'Dead Click', - progress: 85, - value: '2.5K', - icon: , - }, - { - label: 'Click Rage', - progress: 25, - value: '405', - icon: , - }, - { - label: 'Bad Request', - progress: 5, - value: '302', - icon: , - }, - { - label: 'Mouse Thrashing', - progress: 3, - value: '194', - icon: , - }, - ]; + const rows = [ + { + label: 'Dead Click', + progress: 85, + value: '2.5K', + icon: , + }, + { + label: 'Click Rage', + progress: 25, + value: '405', + icon: , + }, + { + label: 'Bad Request', + progress: 5, + value: '302', + icon: , + }, + { + label: 'Mouse Thrashing', + progress: 3, + value: '194', + icon: , + }, + ]; - const lineWidth = 200; - return ( - - ); + const lineWidth = 200; + return ; } export default ByIssues; diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByRferrer.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByRferrer.tsx index c582f23dc..f8a313049 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByRferrer.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByRferrer.tsx @@ -10,39 +10,33 @@ function ByReferrer(props: any) { ptitle: 'Dresses', value: '500', progress: 75, - icon: + icon: , }, { label: 'https://www.reddit.com', ptitle: 'Search: summer dresses', value: '306', progress: 60, - icon: + icon: , }, { label: 'https://www.company.com/account/orders', ptitle: 'Account: Orders', value: '198', progress: 30, - icon: + icon: , }, { label: 'android-app://com.slack/', ptitle: 'Checkout: Confirmation', value: '47', progress: 15, - icon: - } + icon: , + }, ]; const lineWidth = 240; - return ( - - ); + return ; } export default ByReferrer; diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/BySystem.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/BySystem.tsx index 21a138315..c8d977455 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/BySystem.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/BySystem.tsx @@ -10,42 +10,36 @@ function BySystem(props: any) { label: 'Windows', progress: 75, value: '2.5K', - icon: , + icon: , }, { label: 'MacOS', progress: 25, value: '405', - icon: , + icon: , }, { label: 'Ubuntu', progress: 10, value: '302', - icon: , + icon: , }, { label: 'Fedora', progress: 7, value: '302', - icon: , + icon: , }, { label: 'Unknown', progress: 4, value: '194', - icon: , + icon: , }, ]; const lineWidth = 200; - return ( - - ); + return ; } export default BySystem; diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByUrl.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByUrl.tsx index 35f58e33a..820886209 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByUrl.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByUrl.tsx @@ -2,10 +2,10 @@ import { LinkOutlined } from '@ant-design/icons'; import { Segmented } from 'antd'; import React from 'react'; -import { Circle } from '../Count'; -import ExCard from '../ExCard'; import ByComponent from 'Components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/Component'; import { Icon } from 'UI'; +import { Circle } from '../Count'; +import ExCard from '../ExCard'; function ByUrl(props: any) { const [mode, setMode] = React.useState(0); @@ -15,46 +15,40 @@ function ByUrl(props: any) { ptitle: 'Dresses', value: '500', progress: 75, - icon: + icon: , }, { label: '/search?q=summer+dresses', ptitle: 'Search: summer dresses', value: '306', progress: 60, - icon: + icon: , }, { label: '/account/orders', ptitle: 'Account: Orders', value: '198', progress: 30, - icon: + icon: , }, { label: '/checkout/confirmation', ptitle: 'Checkout: Confirmation', value: '47', progress: 15, - icon: + icon: , }, { label: '/checkout/payment', ptitle: 'Checkout: Payment', value: '5', progress: 5, - icon: - } + icon: , + }, ]; const lineWidth = 240; - return ( - - ); + return ; } export default ByUrl; diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByUser.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByUser.tsx index 074731dd7..950e97a7b 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByUser.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByUser.tsx @@ -1,53 +1,47 @@ import React from 'react'; -import {Avatar, Icon} from 'UI'; +import { Avatar, Icon } from 'UI'; +import { hashString } from 'Types/session/session'; import ExCard from '../ExCard'; import ByComponent from './Component'; -import {hashString} from "Types/session/session"; function ByUser(props: any) { - const rows = [ - { - label: 'Demo User', - progress: 85, - value: '2.5K', - icon: , - }, - { - label: 'Admin User', - progress: 25, - value: '405', - icon: , - }, - { - label: 'Management User', - progress: 5, - value: '302', - icon: , - }, - { - label: 'Sales User', - progress: 3, - value: '194', - icon: , - }, - { - label: 'Marketing User', - progress: 1, - value: '57', - icon: , - }, - ]; + const rows = [ + { + label: 'Demo User', + progress: 85, + value: '2.5K', + icon: , + }, + { + label: 'Admin User', + progress: 25, + value: '405', + icon: , + }, + { + label: 'Management User', + progress: 5, + value: '302', + icon: , + }, + { + label: 'Sales User', + progress: 3, + value: '194', + icon: , + }, + { + label: 'Marketing User', + progress: 1, + value: '57', + icon: , + }, + ]; - const lineWidth = 200; - return ( - - ); + const lineWidth = 200; + return ; } export default ByUser; diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/Component.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/Component.tsx index d7be1c9f3..4ef1cb72b 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/Component.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/Component.tsx @@ -1,34 +1,42 @@ -import ExCard from '../ExCard'; import React from 'react'; import CardSessionsByList from 'Components/Dashboard/Widgets/CardSessionsByList'; +import ExCard from '../ExCard'; -function ByComponent({ title, rows, lineWidth, onCard, type }: { - title: string +function ByComponent({ + title, + rows, + lineWidth, + onCard, + type, +}: { + title: string; rows: { - label: string - progress: number - value: string - icon: React.ReactNode - }[] - onCard: (card: string) => void - type: string - lineWidth: number + label: string; + progress: number; + value: string; + icon: React.ReactNode; + }[]; + onCard: (card: string) => void; + type: string; + lineWidth: number; }) { - const _rows = rows.map((r) => ({ - ...r, - name: r.label, - displayName: r.label, - sessionCount: r.value - })).slice(0, 4); + const _rows = rows + .map((r) => ({ + ...r, + name: r.label, + displayName: r.label, + sessionCount: r.value, + })) + .slice(0, 4); return ( - -
    - null} /> + +
    + null} + />
    ); diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/SlowestDomains.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/SlowestDomains.tsx index 92fd4a3bf..3f426fd62 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/SlowestDomains.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/SlowestDomains.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import ByComponent from './Component'; import { LinkOutlined } from '@ant-design/icons'; import { Icon } from 'UI'; +import ByComponent from './Component'; function SlowestDomains(props: any) { const rows = [ @@ -9,42 +9,36 @@ function SlowestDomains(props: any) { label: 'res.cloudinary.com', value: '500', progress: 75, - icon: + icon: , }, { label: 'mintbase.vercel.app', value: '306', progress: 60, - icon: + icon: , }, { label: 'downloads.intercomcdn.com', value: '198', progress: 30, - icon: + icon: , }, { label: 'static.intercomassets.com', value: '47', progress: 15, - icon: + icon: , }, { label: 'mozbar.moz.com', value: '5', progress: 5, - icon: - } + icon: , + }, ]; const lineWidth = 200; - return ( - - ); + return ; } export default SlowestDomains; diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsByErrors.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsByErrors.tsx index 75f0bf400..05231b3f0 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsByErrors.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsByErrors.tsx @@ -1,15 +1,13 @@ -import React from 'react' -import ExCard from "./ExCard"; -import { Errors } from "./Count"; +import React from 'react'; +import ExCard from './ExCard'; +import { Errors } from './Count'; function SessionsByErrors(props: any) { return ( - + ); } -export default SessionsByErrors +export default SessionsByErrors; diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsByIssues.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsByIssues.tsx index ab8ec58c2..e62777be9 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsByIssues.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsByIssues.tsx @@ -1,15 +1,13 @@ -import React from 'react' -import ExCard from "./ExCard"; -import { Frustrations } from "./Count"; +import React from 'react'; +import ExCard from './ExCard'; +import { Frustrations } from './Count'; function SessionsByIssues(props: any) { return ( - + ); } -export default SessionsByIssues +export default SessionsByIssues; diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsPerBrowserExample.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsPerBrowserExample.tsx index f5710b759..e20e14224 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsPerBrowserExample.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsPerBrowserExample.tsx @@ -14,22 +14,20 @@ function SessionsPerBrowserExample(props: Props) { const data = { chart: [ { - 'browser': 'Chrome', - 'count': 1524, + browser: 'Chrome', + count: 1524, '126.0.0': 1157, - '125.0.0': 224 + '125.0.0': 224, }, { - 'browser': 'Edge', - 'count': 159, - '126.0.0': 145 - } - ] + browser: 'Edge', + count: 159, + '126.0.0': 145, + }, + ], }; return ( - + ); diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SlowestDomain.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SlowestDomain.tsx index baded8f84..79ba220a2 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SlowestDomain.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SlowestDomain.tsx @@ -42,20 +42,12 @@ function SlowestDomain(props: any) { const lineWidth = 240; return ( - -
    + +
    {rows.map((r) => ( -
    - - {r.icon} - -
    +
    + {r.icon} +
    {r.label}
    -
    {r.value}
    +
    {r.value}
    ))}
    diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SpeedIndexByLocationExample.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SpeedIndexByLocationExample.tsx index 98a88eef7..69027b892 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SpeedIndexByLocationExample.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SpeedIndexByLocationExample.tsx @@ -13,79 +13,77 @@ interface Props { function SpeedIndexByLocationExample(props: Props) { const data = { - 'value': 1480, - 'chart': [ + value: 1480, + chart: [ { - 'userCountry': 'AT', - 'value': 415 + userCountry: 'AT', + value: 415, }, { - 'userCountry': 'PL', - 'value': 433.1666666666667 + userCountry: 'PL', + value: 433.1666666666667, }, { - 'userCountry': 'FR', - 'value': 502 + userCountry: 'FR', + value: 502, }, { - 'userCountry': 'IT', - 'value': 540.4117647058823 + userCountry: 'IT', + value: 540.4117647058823, }, { - 'userCountry': 'TH', - 'value': 662.0 + userCountry: 'TH', + value: 662.0, }, { - 'userCountry': 'ES', - 'value': 740.5454545454545 + userCountry: 'ES', + value: 740.5454545454545, }, { - 'userCountry': 'SG', - 'value': 889.6666666666666 + userCountry: 'SG', + value: 889.6666666666666, }, { - 'userCountry': 'TW', - 'value': 1008.0 + userCountry: 'TW', + value: 1008.0, }, { - 'userCountry': 'HU', - 'value': 1027.0 + userCountry: 'HU', + value: 1027.0, }, { - 'userCountry': 'DE', - 'value': 1054.4583333333333 + userCountry: 'DE', + value: 1054.4583333333333, }, { - 'userCountry': 'BE', - 'value': 1126.0 + userCountry: 'BE', + value: 1126.0, }, { - 'userCountry': 'TR', - 'value': 1174.0 + userCountry: 'TR', + value: 1174.0, }, { - 'userCountry': 'US', - 'value': 1273.3015873015872 + userCountry: 'US', + value: 1273.3015873015872, }, { - 'userCountry': 'GB', - 'value': 1353.8095238095239 + userCountry: 'GB', + value: 1353.8095238095239, }, { - 'userCountry': 'VN', - 'value': 1473.8181818181818 + userCountry: 'VN', + value: 1473.8181818181818, }, { - 'userCountry': 'HK', - 'value': 1654.6666666666667 + userCountry: 'HK', + value: 1654.6666666666667, }, ], - 'unit': 'ms' + unit: 'ms', }; return ( - + ); diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/TableOfErrors.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/TableOfErrors.tsx index 5ca12cfce..382ccd6b9 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/TableOfErrors.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/TableOfErrors.tsx @@ -4,217 +4,214 @@ import { Errors } from 'Components/Dashboard/components/DashboardList/NewDashMod import CustomMetricTableErrors from 'Components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableErrors'; function TableOfErrors(props: any) { - const data = - { - 'total': 53, - 'errors': [ - { - 'errorId': '915785df4535216f2911bbb21a1afc9658c', - 'name': 'TypeError', - 'message': 'e.update is not a function', - 'users': 14, - 'sessions': 15, - 'lastOccurrence': 1725014275603, - 'firstOccurrence': 1722623575416, - 'chart': [ - { - 'count': 1, - 'timestamp': 1724371200000 - }, - { - 'count': 0, - 'timestamp': 1724486399833 - }, - { - 'count': 0, - 'timestamp': 1724601599666 - }, - { - 'count': 6, - 'timestamp': 1724716799499 - }, - { - 'count': 3, - 'timestamp': 1724831999332 - }, - { - 'count': 5, - 'timestamp': 1724947199165 - }, - { - 'count': 0, - 'timestamp': 1725062398998 - } - ], - 'viewed': false - }, - { - 'errorId': '915d0c598f4e14456af9ab9f4b992329729', - 'name': 'Unhandled Promise Rejection', - 'message': '"Timeout (b)"', - 'users': 2, - 'sessions': 2, - 'lastOccurrence': 1725013879988, - 'firstOccurrence': 1718110576163, - 'chart': [ - { - 'count': 0, - 'timestamp': 1724371200000 - }, - { - 'count': 0, - 'timestamp': 1724486399833 - }, - { - 'count': 0, - 'timestamp': 1724601599666 - }, - { - 'count': 0, - 'timestamp': 1724716799499 - }, - { - 'count': 1, - 'timestamp': 1724831999332 - }, - { - 'count': 1, - 'timestamp': 1724947199165 - }, - { - 'count': 0, - 'timestamp': 1725062398998 - } - ], - 'viewed': false - }, - { - 'errorId': '915c22f1195ec3067dbd0a75638a2d64f0b', - 'name': 'Unhandled Promise Rejection', - 'message': '{"message":"! GET error on /cards; 403","response":{}}', - 'users': 2, - 'sessions': 2, - 'lastOccurrence': 1725013720866, - 'firstOccurrence': 1709287604526, - 'chart': [ - { - 'count': 1, - 'timestamp': 1724371200000 - }, - { - 'count': 0, - 'timestamp': 1724486399833 - }, - { - 'count': 0, - 'timestamp': 1724601599666 - }, - { - 'count': 0, - 'timestamp': 1724716799499 - }, - { - 'count': 0, - 'timestamp': 1724831999332 - }, - { - 'count': 1, - 'timestamp': 1724947199165 - }, - { - 'count': 0, - 'timestamp': 1725062398998 - } - ], - 'viewed': false - }, - { - 'errorId': '915206415c1c4b79e8f5f55bba62544c3c5', - 'name': 'Unhandled Promise Rejection', - 'message': '{"message":"! GET error on /dashboards; 403","response":{}}', - 'users': 1, - 'sessions': 1, - 'lastOccurrence': 1725013689204, - 'firstOccurrence': 1715788328614, - 'chart': [ - { - 'count': 0, - 'timestamp': 1724371200000 - }, - { - 'count': 0, - 'timestamp': 1724486399833 - }, - { - 'count': 0, - 'timestamp': 1724601599666 - }, - { - 'count': 0, - 'timestamp': 1724716799499 - }, - { - 'count': 0, - 'timestamp': 1724831999332 - }, - { - 'count': 1, - 'timestamp': 1724947199165 - }, - { - 'count': 0, - 'timestamp': 1725062398998 - } - ], - 'viewed': false - }, - { - 'errorId': '91514ac2304acfca5d82cd518fb36e5fc22', - 'name': 'TypeError', - 'message': 'Cannot read properties of undefined (reading \'status\')', - 'users': 1, - 'sessions': 1, - 'lastOccurrence': 1725013072800, - 'firstOccurrence': 1725013072800, - 'chart': [ - { - 'count': 0, - 'timestamp': 1724371200000 - }, - { - 'count': 0, - 'timestamp': 1724486399833 - }, - { - 'count': 0, - 'timestamp': 1724601599666 - }, - { - 'count': 0, - 'timestamp': 1724716799499 - }, - { - 'count': 0, - 'timestamp': 1724831999332 - }, - { - 'count': 1, - 'timestamp': 1724947199165 - }, - { - 'count': 0, - 'timestamp': 1725062398998 - } - ], - 'viewed': false - }, - ] - }; + const data = { + total: 53, + errors: [ + { + errorId: '915785df4535216f2911bbb21a1afc9658c', + name: 'TypeError', + message: 'e.update is not a function', + users: 14, + sessions: 15, + lastOccurrence: 1725014275603, + firstOccurrence: 1722623575416, + chart: [ + { + count: 1, + timestamp: 1724371200000, + }, + { + count: 0, + timestamp: 1724486399833, + }, + { + count: 0, + timestamp: 1724601599666, + }, + { + count: 6, + timestamp: 1724716799499, + }, + { + count: 3, + timestamp: 1724831999332, + }, + { + count: 5, + timestamp: 1724947199165, + }, + { + count: 0, + timestamp: 1725062398998, + }, + ], + viewed: false, + }, + { + errorId: '915d0c598f4e14456af9ab9f4b992329729', + name: 'Unhandled Promise Rejection', + message: '"Timeout (b)"', + users: 2, + sessions: 2, + lastOccurrence: 1725013879988, + firstOccurrence: 1718110576163, + chart: [ + { + count: 0, + timestamp: 1724371200000, + }, + { + count: 0, + timestamp: 1724486399833, + }, + { + count: 0, + timestamp: 1724601599666, + }, + { + count: 0, + timestamp: 1724716799499, + }, + { + count: 1, + timestamp: 1724831999332, + }, + { + count: 1, + timestamp: 1724947199165, + }, + { + count: 0, + timestamp: 1725062398998, + }, + ], + viewed: false, + }, + { + errorId: '915c22f1195ec3067dbd0a75638a2d64f0b', + name: 'Unhandled Promise Rejection', + message: '{"message":"! GET error on /cards; 403","response":{}}', + users: 2, + sessions: 2, + lastOccurrence: 1725013720866, + firstOccurrence: 1709287604526, + chart: [ + { + count: 1, + timestamp: 1724371200000, + }, + { + count: 0, + timestamp: 1724486399833, + }, + { + count: 0, + timestamp: 1724601599666, + }, + { + count: 0, + timestamp: 1724716799499, + }, + { + count: 0, + timestamp: 1724831999332, + }, + { + count: 1, + timestamp: 1724947199165, + }, + { + count: 0, + timestamp: 1725062398998, + }, + ], + viewed: false, + }, + { + errorId: '915206415c1c4b79e8f5f55bba62544c3c5', + name: 'Unhandled Promise Rejection', + message: '{"message":"! GET error on /dashboards; 403","response":{}}', + users: 1, + sessions: 1, + lastOccurrence: 1725013689204, + firstOccurrence: 1715788328614, + chart: [ + { + count: 0, + timestamp: 1724371200000, + }, + { + count: 0, + timestamp: 1724486399833, + }, + { + count: 0, + timestamp: 1724601599666, + }, + { + count: 0, + timestamp: 1724716799499, + }, + { + count: 0, + timestamp: 1724831999332, + }, + { + count: 1, + timestamp: 1724947199165, + }, + { + count: 0, + timestamp: 1725062398998, + }, + ], + viewed: false, + }, + { + errorId: '91514ac2304acfca5d82cd518fb36e5fc22', + name: 'TypeError', + message: "Cannot read properties of undefined (reading 'status')", + users: 1, + sessions: 1, + lastOccurrence: 1725013072800, + firstOccurrence: 1725013072800, + chart: [ + { + count: 0, + timestamp: 1724371200000, + }, + { + count: 0, + timestamp: 1724486399833, + }, + { + count: 0, + timestamp: 1724601599666, + }, + { + count: 0, + timestamp: 1724716799499, + }, + { + count: 0, + timestamp: 1724831999332, + }, + { + count: 1, + timestamp: 1724947199165, + }, + { + count: 0, + timestamp: 1725062398998, + }, + ], + viewed: false, + }, + ], + }; return ( - - + + ); } diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Tabs/CoreWebVitals.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Tabs/CoreWebVitals.tsx index 3cd3d7603..7ebc05c87 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Tabs/CoreWebVitals.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Tabs/CoreWebVitals.tsx @@ -5,16 +5,16 @@ import SessionsByIssues from '../SessionsByIssues'; import SessionsByErrors from '../SessionsByErrors'; interface ExampleProps { - onCard: (card: string) => void; + onCard: (card: string) => void; } -const CoreWebVitals: React.FC = ({onCard}) => ( - <> - - - - - +const CoreWebVitals: React.FC = ({ onCard }) => ( + <> + + + + + ); export default CoreWebVitals; diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Tabs/PerformanceMonitoring.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Tabs/PerformanceMonitoring.tsx index 2836f706a..496aefc37 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Tabs/PerformanceMonitoring.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Tabs/PerformanceMonitoring.tsx @@ -5,16 +5,16 @@ import SessionsByErrors from '../SessionsByErrors'; import SessionsByIssues from '../SessionsByIssues'; interface ExampleProps { - onCard: (card: string) => void; + onCard: (card: string) => void; } -const PerformanceMonitoring: React.FC = ({onCard}) => ( - <> - - - - - +const PerformanceMonitoring: React.FC = ({ onCard }) => ( + <> + + + + + ); export default PerformanceMonitoring; diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Tabs/ProductAnalytics.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Tabs/ProductAnalytics.tsx index d0f0c1caf..f5517dcc3 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Tabs/ProductAnalytics.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Tabs/ProductAnalytics.tsx @@ -5,16 +5,16 @@ import ExampleTrend from '../Trend'; import ExampleCount from '../Count'; interface ExampleProps { - onCard: (card: string) => void; + onCard: (card: string) => void; } const ProductAnalytics: React.FC = ({ onCard }) => ( - <> - - - - - + <> + + + + + ); export default ProductAnalytics; diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Tabs/WebAnalytics.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Tabs/WebAnalytics.tsx index 3fef1ab7a..70d972dc4 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Tabs/WebAnalytics.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Tabs/WebAnalytics.tsx @@ -5,16 +5,16 @@ import ByCountry from '../SessionsBy/ByCountry'; import ByUrl from '../SessionsBy/ByUrl'; interface ExampleProps { - onCard: (card: string) => void; + onCard: (card: string) => void; } -const WebAnalytics: React.FC = ({onCard}) => ( - <> - - - - - +const WebAnalytics: React.FC = ({ onCard }) => ( + <> + + + + + ); export default WebAnalytics; diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Trend.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Trend.tsx index 7c77782d4..cd5ea77de 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Trend.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Trend.tsx @@ -1,43 +1,41 @@ import React from 'react'; +import LineChart from 'App/components/Charts/LineChart'; +import { Styles } from 'Components/Dashboard/Widgets/common'; import ExCard from './ExCard'; -import LineChart from 'App/components/Charts/LineChart' -import {Styles} from "Components/Dashboard/Widgets/common"; interface Props { - title: string; - type: string; - onCard: (card: string) => void; - onClick?: any; - data?: any, + title: string; + type: string; + onCard: (card: string) => void; + onClick?: any; + data?: any; } function ExampleTrend(props: Props) { - return ( - -
    {props.title}
    -
    - } - > - {/**/} - - - ); + return ( + +
    {props.title}
    +
    + } + > + {/* */} + + + ); } export default ExampleTrend; diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/WebVital.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/WebVital.tsx index 1b23a24f4..f27c465ab 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/WebVital.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/WebVital.tsx @@ -1,56 +1,54 @@ import React from 'react'; -import CustomMetricOverviewChart from "Components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricOverviewChart"; -import ExCard from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/ExCard"; +import CustomMetricOverviewChart from 'Components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricOverviewChart'; +import ExCard from 'Components/Dashboard/components/DashboardList/NewDashModal/Examples/ExCard'; interface Props { - title: string; - type: string; - onCard: (card: string) => void; - data?: any, + title: string; + type: string; + onCard: (card: string) => void; + data?: any; } function WebVital(props: Props) { - const data = props.data || { - "value": 8.33316146432396, - "chart": [ - { - "timestamp": 1718755200000, - "value": 9.37317620650954 - }, - { - "timestamp": 1718870399833, - "value": 6.294931643881294 - }, - { - "timestamp": 1718985599666, - "value": 7.103504928806133 - }, - { - "timestamp": 1719100799499, - "value": 7.946568201563857 - }, - { - "timestamp": 1719215999332, - "value": 8.941158674935712 - }, - { - "timestamp": 1719331199165, - "value": 10.180191693290734 - }, - { - "timestamp": 1719446398998, - "value": 0 - } - ], - "unit": "%" - } - return ( - - - - ); + const data = props.data || { + value: 8.33316146432396, + chart: [ + { + timestamp: 1718755200000, + value: 9.37317620650954, + }, + { + timestamp: 1718870399833, + value: 6.294931643881294, + }, + { + timestamp: 1718985599666, + value: 7.103504928806133, + }, + { + timestamp: 1719100799499, + value: 7.946568201563857, + }, + { + timestamp: 1719215999332, + value: 8.941158674935712, + }, + { + timestamp: 1719331199165, + value: 10.180191693290734, + }, + { + timestamp: 1719446398998, + value: 0, + }, + ], + unit: '%', + }; + return ( + + + + ); } export default WebVital; diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/NewDashboardModal.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/NewDashboardModal.tsx index 7e8b0d0ae..905044083 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/NewDashboardModal.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/NewDashboardModal.tsx @@ -20,57 +20,56 @@ const NewDashboardModal: React.FC = ({ isAddingFromLibrary = false, }) => { const { projectsStore, userStore } = useStore(); - const isEnterprise = userStore.isEnterprise; - const isMobile = projectsStore.isMobile; + const { isEnterprise } = userStore; + const { isMobile } = projectsStore; const [step, setStep] = React.useState(0); const [selectedCategory, setSelectedCategory] = React.useState('product-analytics'); - useEffect(() => { - return () => { + useEffect( + () => () => { setStep(0); - }; - }, [open]); + }, + [open], + ); return ( - <> - +
    -
    - {step === 0 && ( - setStep(step + 1)} - isLibrary={isAddingFromLibrary} - isMobile={isMobile} - isEnterprise={isEnterprise} - /> - )} - {step === 1 && setStep(0)} />} -
    - - + {step === 0 && ( + setStep(step + 1)} + isLibrary={isAddingFromLibrary} + isMobile={isMobile} + isEnterprise={isEnterprise} + /> + )} + {step === 1 && setStep(0)} />} +
    +
    ); }; diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Option.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Option.tsx index 7c08512ca..57b2a974d 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Option.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Option.tsx @@ -1,16 +1,16 @@ import React from 'react'; -import {LucideIcon} from "lucide-react"; +import { LucideIcon } from 'lucide-react'; interface OptionProps { - label: string; - Icon: LucideIcon; + label: string; + Icon: LucideIcon; } -const Option: React.FC = ({label, Icon}) => ( -
    - -
    {label}
    -
    +const Option: React.FC = ({ label, Icon }) => ( +
    + +
    {label}
    +
    ); export default Option; diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/SelectCard.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/SelectCard.tsx index 1804e029f..df8ec2b4d 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/SelectCard.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/SelectCard.tsx @@ -2,14 +2,15 @@ import React, { useMemo, useState, useEffect } from 'react'; import { Button, Input, Segmented, Space } from 'antd'; import { RightOutlined } from '@ant-design/icons'; import { ArrowRight, Info } from 'lucide-react'; -import { CARD_LIST, CARD_CATEGORIES, CardType } from './ExampleCards'; import { useStore } from 'App/mstore'; -import Option from './Option'; import CardsLibrary from 'Components/Dashboard/components/DashboardList/NewDashModal/CardsLibrary'; import { FUNNEL } from 'App/constants/card'; import { useHistory } from 'react-router'; import { FilterKey } from 'Types/filter/filterType'; import FilterSeries from '@/mstore/types/filterSeries'; +import Option from './Option'; +import { CARD_LIST, CARD_CATEGORIES, CardType } from './ExampleCards'; +import { useTranslation } from 'react-i18next'; interface SelectCardProps { onClose: (refresh?: boolean) => void; @@ -22,23 +23,32 @@ interface SelectCardProps { } const SelectCard: React.FC = (props: SelectCardProps) => { - const { onCard, isLibrary = false, selected, setSelectedCategory, isEnterprise, isMobile } = props; + const { + onCard, + isLibrary = false, + selected, + setSelectedCategory, + isEnterprise, + isMobile, + } = props; + const { t } = useTranslation(); const [selectedCards, setSelectedCards] = React.useState([]); const { metricStore, dashboardStore } = useStore(); const siteId: string = location.pathname.split('/')[1]; const dashboardId = location.pathname.split('/')[3]; const [libraryQuery, setLibraryQuery] = React.useState(''); const [headerText, setHeaderText] = useState(''); - const isCreatingDashboard = !dashboardId && location.pathname.includes('dashboard'); + const isCreatingDashboard = + !dashboardId && location.pathname.includes('dashboard'); const [dashboardCreating, setDashboardCreating] = useState(false); const [dashboardUpdating, setDashboardUpdating] = useState(false); const history = useHistory(); useEffect(() => { if (dashboardId) { - setHeaderText(isLibrary ? 'Your Library' : 'Create Card'); + setHeaderText(isLibrary ? t('Your Library') : t('Create Card')); } else { - setHeaderText('Select a card template to start your dashboard'); + setHeaderText(t('Select a card template to start your dashboard')); } }, [dashboardId, isLibrary]); @@ -50,30 +60,31 @@ const SelectCard: React.FC = (props: SelectCardProps) => { .then(async (syncedDashboard) => { dashboardStore.selectDashboardById(syncedDashboard.dashboardId); history.push(`/${siteId}/dashboard/${syncedDashboard.dashboardId}`); - //return syncedDashboard.dashboardId; - }).finally(() => { + // return syncedDashboard.dashboardId; + }) + .finally(() => { setDashboardCreating(false); }); }; const handleCardSelection = (card: string) => { metricStore.init(); - const selectedCard = CARD_LIST.find((c) => c.key === card) as CardType; + const selectedCard = CARD_LIST(t).find((c) => c.key === card) as CardType; const cardData: any = { metricType: selectedCard.cardType, name: selectedCard.title, - metricOf: selectedCard.metricOf + metricOf: selectedCard.metricOf, }; if (selectedCard.filters) { cardData.series = [ new FilterSeries().fromJson({ - name: "Series 1", + name: 'Series 1', filter: { filters: selectedCard.filters, - } - }) + }, + }), ]; } @@ -87,24 +98,33 @@ const SelectCard: React.FC = (props: SelectCardProps) => { onCard(); }; - const cardItems = useMemo(() => { - return CARD_LIST.filter((card) => - card.category === selected && - (!card.isEnterprise || (card.isEnterprise && isEnterprise)) && - (!isMobile || (isMobile && ![FilterKey.USER_BROWSER].includes(card.key))) - ).map((card) => ( -
    - -
    - )); - }, [selected, isEnterprise, isMobile]); + const cardItems = useMemo( + () => + CARD_LIST(t) + .filter( + (card) => + card.category === selected && + (!card.isEnterprise || (card.isEnterprise && isEnterprise)) && + (!isMobile || + (isMobile && ![FilterKey.USER_BROWSER].includes(card.key))), + ) + .map((card) => ( +
    + +
    + )), + [selected, isEnterprise, isMobile], + ); const onCardClick = (cardId: number) => { if (selectedCards.includes(cardId)) { @@ -117,11 +137,13 @@ const SelectCard: React.FC = (props: SelectCardProps) => { const onAddSelected = () => { setDashboardUpdating(true); const dashboard = dashboardStore.getDashboard(dashboardId); - dashboardStore.addWidgetToDashboard(dashboard!, selectedCards).finally(() => { - setDashboardUpdating(false); - dashboardStore.fetch(dashboardId); - props.onClose(true); - }); + dashboardStore + .addWidgetToDashboard(dashboard!, selectedCards) + .finally(() => { + setDashboardUpdating(false); + dashboardStore.fetch(dashboardId); + props.onClose(true); + }); }; return ( @@ -131,14 +153,22 @@ const SelectCard: React.FC = (props: SelectCardProps) => { {headerText} {headerText === 'Select a card template to start your dashboard' && (
    - Following card previews are based on mock data for illustrative purposes only. + {' '} + {t( + 'Following card previews are based on mock data for illustrative purposes only.', + )}
    )}
    {isCreatingDashboard && ( - @@ -146,8 +176,12 @@ const SelectCard: React.FC = (props: SelectCardProps) => { {isLibrary && ( {selectedCards.length > 0 && ( - )} = (props: SelectCardProps) => { )} - {!isLibrary && } + {!isLibrary && ( + + )} {isLibrary ? ( = ({ setSelected, selected }) => ( +const CategorySelector: React.FC = ({ + setSelected, + selected, +}) => ( ({ label:
    diff --git a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/AddMetricContainer.tsx b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/AddMetricContainer.tsx index b1d84f54e..7f768002e 100644 --- a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/AddMetricContainer.tsx +++ b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/AddMetricContainer.tsx @@ -3,108 +3,136 @@ import { observer } from 'mobx-react-lite'; import { Icon } from 'UI'; import { useModal } from 'App/components/Modal'; import { useStore } from 'App/mstore'; +import cn from 'classnames'; import AddMetric from './AddMetric'; import AddPredefinedMetric from './AddPredefinedMetric'; -import cn from 'classnames'; interface AddMetricButtonProps { - iconName: "bar-pencil" | "grid-check"; - title: string; - description: string; - isPremade?: boolean; - isPopup?: boolean; - onClick: () => void; + iconName: 'bar-pencil' | 'grid-check'; + title: string; + description: string; + isPremade?: boolean; + isPopup?: boolean; + onClick: () => void; } -function AddMetricButton({ iconName, title, description, onClick, isPremade, isPopup }: AddMetricButtonProps) { - return ( -
    -
    - -
    -
    -
    {title}
    -
    - {description} -
    -
    +function AddMetricButton({ + iconName, + title, + description, + onClick, + isPremade, + isPopup, +}: AddMetricButtonProps) { + return ( +
    +
    + +
    +
    +
    + {title}
    - ); +
    + {description} +
    +
    +
    + ); } interface Props { - siteId: string - isPopup?: boolean - onAction?: () => void + siteId: string; + isPopup?: boolean; + onAction?: () => void; } function AddMetricContainer({ siteId, isPopup, onAction }: Props) { - const { showModal } = useModal(); - const { dashboardStore } = useStore(); + const { showModal } = useModal(); + const { dashboardStore } = useStore(); - const onAddCustomMetrics = () => { - onAction?.() - dashboardStore.initDashboard(dashboardStore.selectedDashboard); - showModal( - , - { right: true } - ); - }; - - const onAddPredefinedMetrics = () => { - onAction?.() - dashboardStore.initDashboard(dashboardStore.selectedDashboard); - showModal( - , - { right: true } - ); - }; - - const classes = isPopup - ? 'bg-white border rounded p-4 grid grid-rows-2 gap-4' - : 'bg-white border border-dashed hover:!border-gray-medium rounded p-8 grid grid-cols-2 gap-8'; - return ( -
    - - -
    + const onAddCustomMetrics = () => { + onAction?.(); + dashboardStore.initDashboard(dashboardStore.selectedDashboard); + showModal( + , + { right: true }, ); + }; + + const onAddPredefinedMetrics = () => { + onAction?.(); + dashboardStore.initDashboard(dashboardStore.selectedDashboard); + showModal( + , + { right: true }, + ); + }; + + const classes = isPopup + ? 'bg-white border rounded p-4 grid grid-rows-2 gap-4' + : 'bg-white border border-dashed hover:!border-gray-medium rounded p-8 grid grid-cols-2 gap-8'; + return ( +
    + + +
    + ); } export default observer(AddMetricContainer); diff --git a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/AddPredefinedMetric.tsx b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/AddPredefinedMetric.tsx index 70fbf4040..ffeb45183 100644 --- a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/AddPredefinedMetric.tsx +++ b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/AddPredefinedMetric.tsx @@ -1,13 +1,14 @@ import React from 'react'; import { observer } from 'mobx-react-lite'; import { Loader } from 'UI'; -import { Button } from 'antd' +import { Button } from 'antd'; import WidgetWrapper from 'App/components/Dashboard/components/WidgetWrapper'; import { useStore } from 'App/mstore'; import { useModal } from 'App/components/Modal'; import { dashboardMetricCreate, withSiteId } from 'App/routes'; import { withRouter, RouteComponentProps } from 'react-router-dom'; import { WidgetCategoryItem } from 'App/components/Dashboard/components/DashboardMetricSelection/DashboardMetricSelection'; +import { useTranslation } from 'react-i18next'; interface IProps extends RouteComponentProps { siteId: string; @@ -16,21 +17,30 @@ interface IProps extends RouteComponentProps { } function AddPredefinedMetric({ history, siteId, title, description }: IProps) { + const { t } = useTranslation(); const [categories, setCategories] = React.useState([]); const { dashboardStore } = useStore(); const { hideModal } = useModal(); - const [activeCategory, setActiveCategory] = React.useState>(); + const [activeCategory, setActiveCategory] = + React.useState>(); const scrollContainer = React.useRef(null); const dashboard = dashboardStore.selectedDashboard; - const selectedWidgetIds = dashboardStore.selectedWidgets.map((widget: any) => widget.metricId); + const selectedWidgetIds = dashboardStore.selectedWidgets.map( + (widget: any) => widget.metricId, + ); const queryParams = new URLSearchParams(location.search); - const totalMetricCount = categories.reduce((acc, category) => acc + category.widgets.length, 0); + const totalMetricCount = categories.reduce( + (acc, category) => acc + category.widgets.length, + 0, + ); React.useEffect(() => { dashboardStore?.fetchTemplates(true).then((categories: any[]) => { - const predefinedCategories = categories.filter((category) => category.name !== 'custom'); + const predefinedCategories = categories.filter( + (category) => category.name !== 'custom', + ); const defaultCategory = predefinedCategories[0]; setActiveCategory(defaultCategory); setCategories(predefinedCategories); @@ -61,7 +71,10 @@ function AddPredefinedMetric({ history, siteId, title, description }: IProps) { }; const onCreateNew = () => { - const path = withSiteId(dashboardMetricCreate(dashboard.dashboardId), siteId); + const path = withSiteId( + dashboardMetricCreate(dashboard.dashboardId), + siteId, + ); if (!queryParams.has('modal')) history.push('?modal=addMetric'); history.push(path); hideModal(); @@ -75,20 +88,29 @@ function AddPredefinedMetric({ history, siteId, title, description }: IProps) { >
    -

    {title}

    +

    + {title} +

    {description}
    - -
    Past 7 Days
    +
    {t('Past 7 Days')}
    -
    +
    dashboardStore.toggleWidgetSelection(metric)} + onClick={() => + dashboardStore.toggleWidgetSelection(metric) + } /> ))} @@ -146,8 +170,12 @@ function AddPredefinedMetric({ history, siteId, title, description }: IProps) { {' out of '} {totalMetricCount}
    -
    diff --git a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx index 5cf0697b1..12201b63e 100644 --- a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx +++ b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx @@ -2,11 +2,11 @@ import React from 'react'; import { useStore } from 'App/mstore'; import WidgetWrapperNew from 'Components/Dashboard/components/WidgetWrapper/WidgetWrapperNew'; import { observer } from 'mobx-react-lite'; -import AddCardSection from '../AddCardSection/AddCardSection'; import cn from 'classnames'; -import { Button, Popover, Tooltip } from 'antd' -import { PlusOutlined } from '@ant-design/icons' +import { Button, Popover, Tooltip } from 'antd'; +import { PlusOutlined } from '@ant-design/icons'; import { Loader } from 'UI'; +import AddCardSection from '../AddCardSection/AddCardSection'; interface Props { siteId: string; @@ -26,7 +26,7 @@ function DashboardWidgetGrid(props: Props) { {list?.length === 0 ? (
    @@ -61,7 +61,10 @@ function GridItem({ item, index, dashboard, dashboardId, siteId }: any) { return (
    } - trigger={'click'} + trigger="click" > -
    - ) + ); } - export default observer(DashboardWidgetGrid); diff --git a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/index.ts b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/index.ts index 410933285..0793238e7 100644 --- a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/index.ts +++ b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/index.ts @@ -1 +1 @@ -export { default } from './DashboardWidgetGrid'; \ No newline at end of file +export { default } from './DashboardWidgetGrid'; diff --git a/frontend/app/components/Dashboard/components/Errors/ErrorDetailsModal/ErrorDetailsModal.tsx b/frontend/app/components/Dashboard/components/Errors/ErrorDetailsModal/ErrorDetailsModal.tsx index b8513580b..2b175e01d 100644 --- a/frontend/app/components/Dashboard/components/Errors/ErrorDetailsModal/ErrorDetailsModal.tsx +++ b/frontend/app/components/Dashboard/components/Errors/ErrorDetailsModal/ErrorDetailsModal.tsx @@ -2,17 +2,17 @@ import React from 'react'; import ErrorInfo from '../../../../Errors/Error/ErrorInfo'; interface Props { - errorId: any + errorId: any; } function ErrorDetailsModal(props: Props) { - return ( -
    - -
    - ); + return ( +
    + +
    + ); } -export default ErrorDetailsModal; \ No newline at end of file +export default ErrorDetailsModal; diff --git a/frontend/app/components/Dashboard/components/Errors/ErrorDetailsModal/index.ts b/frontend/app/components/Dashboard/components/Errors/ErrorDetailsModal/index.ts index b66002f27..d23975e59 100644 --- a/frontend/app/components/Dashboard/components/Errors/ErrorDetailsModal/index.ts +++ b/frontend/app/components/Dashboard/components/Errors/ErrorDetailsModal/index.ts @@ -1 +1 @@ -export { default } from './ErrorDetailsModal' \ No newline at end of file +export { default } from './ErrorDetailsModal'; diff --git a/frontend/app/components/Dashboard/components/Errors/ErrorLabel/ErrorLabel.tsx b/frontend/app/components/Dashboard/components/Errors/ErrorLabel/ErrorLabel.tsx index 1899ccbb9..6656dcb97 100644 --- a/frontend/app/components/Dashboard/components/Errors/ErrorLabel/ErrorLabel.tsx +++ b/frontend/app/components/Dashboard/components/Errors/ErrorLabel/ErrorLabel.tsx @@ -1,23 +1,38 @@ import React from 'react'; -import cn from "classnames"; +import cn from 'classnames'; interface Props { - className?: string; - topValue: string; - topValueSize?: string; - bottomValue: string; - topMuted?: boolean; - bottomMuted?: boolean; + className?: string; + topValue: string; + topValueSize?: string; + bottomValue: string; + topMuted?: boolean; + bottomMuted?: boolean; } -function ErrorLabel({ className, topValue, topValueSize = 'text-base', bottomValue, topMuted = false, bottomMuted = false }: Props) { +function ErrorLabel({ + className, + topValue, + topValueSize = 'text-base', + bottomValue, + topMuted = false, + bottomMuted = false, +}: Props) { return ( -
    -
    { topValue }
    -
    { bottomValue }
    -
    - ) +
    +
    + {topValue} +
    +
    + {bottomValue} +
    +
    + ); } -ErrorLabel.displayName = "ErrorLabel"; +ErrorLabel.displayName = 'ErrorLabel'; -export default ErrorLabel; \ No newline at end of file +export default ErrorLabel; diff --git a/frontend/app/components/Dashboard/components/Errors/ErrorLabel/index.ts b/frontend/app/components/Dashboard/components/Errors/ErrorLabel/index.ts index 716e5986c..103611f22 100644 --- a/frontend/app/components/Dashboard/components/Errors/ErrorLabel/index.ts +++ b/frontend/app/components/Dashboard/components/Errors/ErrorLabel/index.ts @@ -1 +1 @@ -export { default } from './ErrorLabel' \ No newline at end of file +export { default } from './ErrorLabel'; diff --git a/frontend/app/components/Dashboard/components/Errors/ErrorListItem/ErrorListItem.tsx b/frontend/app/components/Dashboard/components/Errors/ErrorListItem/ErrorListItem.tsx index 68b30b16e..6600b92b8 100644 --- a/frontend/app/components/Dashboard/components/Errors/ErrorListItem/ErrorListItem.tsx +++ b/frontend/app/components/Dashboard/components/Errors/ErrorListItem/ErrorListItem.tsx @@ -1,86 +1,109 @@ import React from 'react'; import cn from 'classnames'; -import { DateTime } from 'luxon' +import { DateTime } from 'luxon'; import { IGNORED, RESOLVED } from 'Types/errorInfo'; +import { BarChart, Bar, YAxis, Tooltip, XAxis } from 'recharts'; +import { diffFromNowString } from 'App/date'; import ErrorName from '../ErrorName'; import ErrorLabel from '../ErrorLabel'; -import { BarChart, Bar, YAxis, Tooltip, XAxis } from 'recharts'; import { Styles } from '../../../Widgets/common'; -import { diffFromNowString } from 'App/date'; +import { useTranslation } from 'react-i18next'; interface Props { - error: any; - className?: string; - onClick: (e: any) => void; + error: any; + className?: string; + onClick: (e: any) => void; } function ErrorListItem(props: Props) { - const { error, className = '' } = props; - // const { showModal } = useModal(); + const { t } = useTranslation(); + const { error, className = '' } = props; + // const { showModal } = useModal(); - - return ( -
    -
    -
    - -
    - { error.message } -
    -
    -
    -
    - - - - } /> - - -
    - - - -
    - ); + return ( +
    +
    +
    + +
    + {error.message} +
    +
    +
    +
    + + + + } + /> + + +
    + + + +
    + ); } export default ErrorListItem; -const CustomTooltip = ({ active, payload, label }: any) => { - if (active) { - const p = payload[0].payload; - const dateStr = p.timestamp ? DateTime.fromMillis(p.timestamp).toFormat('l') : '' - return ( -
    -

    {dateStr}

    -

    Sessions: {p.count}

    -
    - ); - } +function CustomTooltip({ active, payload, label }: any) { + const { t } = useTranslation(); + if (active) { + const p = payload[0].payload; + const dateStr = p.timestamp + ? DateTime.fromMillis(p.timestamp).toFormat('l') + : ''; + return ( +
    +

    {dateStr}

    +

    + {t('Sessions:')} + {p.count} +

    +
    + ); + } - return null; - }; + return null; +} diff --git a/frontend/app/components/Dashboard/components/Errors/ErrorListItem/index.ts b/frontend/app/components/Dashboard/components/Errors/ErrorListItem/index.ts index b37bfbbca..d3b748384 100644 --- a/frontend/app/components/Dashboard/components/Errors/ErrorListItem/index.ts +++ b/frontend/app/components/Dashboard/components/Errors/ErrorListItem/index.ts @@ -1 +1 @@ -export { default } from './ErrorListItem' \ No newline at end of file +export { default } from './ErrorListItem'; diff --git a/frontend/app/components/Dashboard/components/Errors/ErrorName/ErrorName.tsx b/frontend/app/components/Dashboard/components/Errors/ErrorName/ErrorName.tsx index 646b5c68f..4b2664e35 100644 --- a/frontend/app/components/Dashboard/components/Errors/ErrorName/ErrorName.tsx +++ b/frontend/app/components/Dashboard/components/Errors/ErrorName/ErrorName.tsx @@ -1,15 +1,34 @@ import React from 'react'; -import cn from "classnames"; +import cn from 'classnames'; -function ErrorText({ className, icon, name, message, bold, lineThrough = false }: any) { - return ( -
    - { name } - { message } +function ErrorText({ + className, + icon, + name, + message, + bold, + lineThrough = false, +}: any) { + return ( +
    + + {name} + + + {message} +
    - ); + ); } -ErrorText.displayName = "ErrorText"; +ErrorText.displayName = 'ErrorText'; -export default ErrorText; \ No newline at end of file +export default ErrorText; diff --git a/frontend/app/components/Dashboard/components/Errors/ErrorName/index.ts b/frontend/app/components/Dashboard/components/Errors/ErrorName/index.ts index 548c10f38..70cfc1734 100644 --- a/frontend/app/components/Dashboard/components/Errors/ErrorName/index.ts +++ b/frontend/app/components/Dashboard/components/Errors/ErrorName/index.ts @@ -1 +1 @@ -export { default } from './ErrorName'; \ No newline at end of file +export { default } from './ErrorName'; diff --git a/frontend/app/components/Dashboard/components/FilterSeries/AddStepButton.tsx b/frontend/app/components/Dashboard/components/FilterSeries/AddStepButton.tsx index 72b4dece6..54fbb60ee 100644 --- a/frontend/app/components/Dashboard/components/FilterSeries/AddStepButton.tsx +++ b/frontend/app/components/Dashboard/components/FilterSeries/AddStepButton.tsx @@ -1,33 +1,40 @@ import React from 'react'; -import FilterSelection from "Shared/Filters/FilterSelection/FilterSelection"; -import {PlusIcon} from "lucide-react"; -import {Button} from "antd"; -import {useStore} from "App/mstore"; +import FilterSelection from 'Shared/Filters/FilterSelection/FilterSelection'; +import { PlusIcon } from 'lucide-react'; +import { Button } from 'antd'; +import { useStore } from 'App/mstore'; +import { useTranslation } from 'react-i18next'; interface Props { - series: any; - excludeFilterKeys: Array; + series: any; + excludeFilterKeys: Array; } -function AddStepButton({series, excludeFilterKeys}: Props) { - const {metricStore} = useStore(); - const metric: any = metricStore.instance; +function AddStepButton({ series, excludeFilterKeys }: Props) { + const { t } = useTranslation(); + const { metricStore } = useStore(); + const metric: any = metricStore.instance; - const onAddFilter = (filter: any) => { - series.filter.addFilter(filter); - metric.updateKey('hasChanged', true) - } - return ( - - - - ); + const onAddFilter = (filter: any) => { + series.filter.addFilter(filter); + metric.updateKey('hasChanged', true); + }; + return ( + + + + ); } export default AddStepButton; diff --git a/frontend/app/components/Dashboard/components/FilterSeries/ExcludeFilters.tsx b/frontend/app/components/Dashboard/components/FilterSeries/ExcludeFilters.tsx index 6ad96b5d4..532176702 100644 --- a/frontend/app/components/Dashboard/components/FilterSeries/ExcludeFilters.tsx +++ b/frontend/app/components/Dashboard/components/FilterSeries/ExcludeFilters.tsx @@ -6,11 +6,13 @@ import FilterItem from 'Shared/Filters/FilterItem'; import cn from 'classnames'; import { Button } from 'antd'; +import { useTranslation } from 'react-i18next'; interface Props { filter: Filter; } function ExcludeFilters(props: Props) { + const { t } = useTranslation(); const { filter } = props; const hasExcludes = filter.excludes.length > 0; @@ -28,13 +30,15 @@ function ExcludeFilters(props: Props) { }; return ( -
    +
    {filter.excludes.length > 0 ? (
    -
    EXCLUDES
    +
    + {t('EXCLUDES')} +
    {filter.excludes.map((f: any, index: number) => ( ) : ( )}
    diff --git a/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx b/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx index fa9249183..b80a456cb 100644 --- a/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx +++ b/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx @@ -1,11 +1,11 @@ import React, { useEffect, useState } from 'react'; import { EventsList, FilterList } from 'Shared/Filters/FilterList'; -import SeriesName from './SeriesName'; import cn from 'classnames'; import { observer } from 'mobx-react-lite'; -import ExcludeFilters from './ExcludeFilters'; import { Button, Space } from 'antd'; import { ChevronDown, ChevronUp, Trash } from 'lucide-react'; +import ExcludeFilters from './ExcludeFilters'; +import SeriesName from './SeriesName'; const FilterCountLabels = observer( (props: { filters: any; toggleExpand: any }) => { @@ -19,7 +19,7 @@ const FilterCountLabels = observer( type="text" size="small" onClick={props.toggleExpand} - className='btn-series-event-count' + className="btn-series-event-count" > {`${events} Event${events > 1 ? 's' : ''}`} @@ -30,7 +30,7 @@ const FilterCountLabels = observer( type="text" size="small" onClick={props.toggleExpand} - className='btn-series-filter-count' + className="btn-series-filter-count" > {`${filters} Filter${filters > 1 ? 's' : ''}`} @@ -38,7 +38,7 @@ const FilterCountLabels = observer(
    ); - } + }, ); const FilterSeriesHeader = observer( @@ -58,10 +58,13 @@ const FilterSeriesHeader = observer( }; return (
    - {!props.expanded && ( + {!props.expanded && ( } - type='text' - className={cn( - 'btn-delete-series', 'disabled:hidden' - )} - /> + type="text" + className={cn('btn-delete-series', 'disabled:hidden')} + />
    ); - } + }, ); interface Props { @@ -118,7 +119,7 @@ interface Props { emptyMessage?: any; observeChanges?: () => void; excludeFilterKeys?: Array; - excludeCategory?: string[] + excludeCategory?: string[]; canExclude?: boolean; expandable?: boolean; isHeatmap?: boolean; @@ -141,10 +142,10 @@ function FilterSeries(props: Props) { removeEvents, collapseState, onToggleCollapse, - excludeCategory + excludeCategory, } = props; - const expanded = isHeatmap || !collapseState - const setExpanded = onToggleCollapse + const expanded = isHeatmap || !collapseState; + const setExpanded = onToggleCollapse; const { series, seriesIndex } = props; const onUpdateFilter = (filterIndex: any, filter: any) => { @@ -171,7 +172,7 @@ function FilterSeries(props: Props) { filter.autoOpen = true; series.filter.addFilter(filter); observeChanges(); - } + }; return (
    @@ -214,7 +215,7 @@ function FilterSeries(props: Props) { {expanded ? ( <> - {removeEvents ? null : + {removeEvents ? null : ( - } + )} ) : (
    setEditing(true)} - data-event='input-rename-series' + data-event="input-rename-series" > - {name && name.trim() === '' ? 'Series ' + (seriesIndex + 1) : name} + {name && name.trim() === '' ? `Series ${seriesIndex + 1}` : name}
    )} @@ -70,4 +70,4 @@ function SeriesName(props: Props) { ); } -export default SeriesName; \ No newline at end of file +export default SeriesName; diff --git a/frontend/app/components/Dashboard/components/FilterSeries/SeriesName/index.ts b/frontend/app/components/Dashboard/components/FilterSeries/SeriesName/index.ts index 90e63cdb6..a2eef9a6c 100644 --- a/frontend/app/components/Dashboard/components/FilterSeries/SeriesName/index.ts +++ b/frontend/app/components/Dashboard/components/FilterSeries/SeriesName/index.ts @@ -1 +1 @@ -export { default } from './SeriesName'; \ No newline at end of file +export { default } from './SeriesName'; diff --git a/frontend/app/components/Dashboard/components/FilterSeries/index.ts b/frontend/app/components/Dashboard/components/FilterSeries/index.ts index 5882e382a..fcd290791 100644 --- a/frontend/app/components/Dashboard/components/FilterSeries/index.ts +++ b/frontend/app/components/Dashboard/components/FilterSeries/index.ts @@ -1 +1 @@ -export { default } from './FilterSeries' \ No newline at end of file +export { default } from './FilterSeries'; diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssueDetails/FunnelIssueDetails.tsx b/frontend/app/components/Dashboard/components/Funnels/FunnelIssueDetails/FunnelIssueDetails.tsx index 16bd8abd0..2c02e23e0 100644 --- a/frontend/app/components/Dashboard/components/Funnels/FunnelIssueDetails/FunnelIssueDetails.tsx +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssueDetails/FunnelIssueDetails.tsx @@ -2,59 +2,64 @@ import React, { useEffect, useState } from 'react'; import { useStore } from 'App/mstore'; import { useObserver } from 'mobx-react-lite'; import { Loader } from 'UI'; -import FunnelIssuesListItem from '../FunnelIssuesListItem'; import SessionItem from 'App/components/shared/SessionItem/SessionItem'; +import FunnelIssuesListItem from '../FunnelIssuesListItem'; interface Props { - issueId: string; + issueId: string; } function FunnelIssueDetails(props: Props) { - const { dashboardStore, metricStore } = useStore(); - const { issueId } = props; - const filter = useObserver(() => dashboardStore.drillDownFilter); - const widget = useObserver(() => metricStore.instance); - const [loading, setLoading] = useState(false); - const [funnelIssue, setFunnelIssue] = useState(null); - const [sessions, setSessions] = useState([]); + const { dashboardStore, metricStore } = useStore(); + const { issueId } = props; + const filter = useObserver(() => dashboardStore.drillDownFilter); + const widget = useObserver(() => metricStore.instance); + const [loading, setLoading] = useState(false); + const [funnelIssue, setFunnelIssue] = useState(null); + const [sessions, setSessions] = useState([]); - useEffect(() => { - setLoading(true); - const _filters = { ...filter, series: widget.data.stages ? widget.series.map((item: any) => { - return { - ...item, - filter: { - ...item.filter, - filters: item.filter.filters.filter((filter: any, index: any) => { - const stage = widget.data.funnel.stages[index]; - return stage &&stage.isActive - }).map((f: any) => f.toJson()) - } - } - }) : [], }; - widget.fetchIssue(widget.metricId, issueId, _filters).then((resp: any) => { - setFunnelIssue(resp.issue); - setSessions(resp.sessions); - }).finally(() => { - setLoading(false); - }); - }, []); + useEffect(() => { + setLoading(true); + const _filters = { + ...filter, + series: widget.data.stages + ? widget.series.map((item: any) => ({ + ...item, + filter: { + ...item.filter, + filters: item.filter.filters + .filter((filter: any, index: any) => { + const stage = widget.data.funnel.stages[index]; + return stage && stage.isActive; + }) + .map((f: any) => f.toJson()), + }, + })) + : [], + }; + widget + .fetchIssue(widget.metricId, issueId, _filters) + .then((resp: any) => { + setFunnelIssue(resp.issue); + setSessions(resp.sessions); + }) + .finally(() => { + setLoading(false); + }); + }, []); - return ( - - {funnelIssue && } + return ( + + {funnelIssue && } -
    - {sessions.map((session: any) => ( -
    - -
    - ))} -
    -
    - ); +
    + {sessions.map((session: any) => ( +
    + +
    + ))} +
    +
    + ); } -export default FunnelIssueDetails; \ No newline at end of file +export default FunnelIssueDetails; diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssueDetails/index.ts b/frontend/app/components/Dashboard/components/Funnels/FunnelIssueDetails/index.ts index 486b120d5..11f0a7d06 100644 --- a/frontend/app/components/Dashboard/components/Funnels/FunnelIssueDetails/index.ts +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssueDetails/index.ts @@ -1 +1 @@ -export { default } from './FunnelIssueDetails'; \ No newline at end of file +export { default } from './FunnelIssueDetails'; diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssueGraph/FunnelIssueGraph.tsx b/frontend/app/components/Dashboard/components/Funnels/FunnelIssueGraph/FunnelIssueGraph.tsx index 08bb072bc..66300a29a 100644 --- a/frontend/app/components/Dashboard/components/Funnels/FunnelIssueGraph/FunnelIssueGraph.tsx +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssueGraph/FunnelIssueGraph.tsx @@ -11,13 +11,19 @@ function FunnelIssueGraph(props: Props) { return (
    - +
    {issue.unaffectedSessions} @@ -25,13 +31,16 @@ function FunnelIssueGraph(props: Props) {
    - +
    {issue.affectedSessions} @@ -39,10 +48,10 @@ function FunnelIssueGraph(props: Props) {
    - +
    - -
    - ); + const { issueId } = props; + return ( +
    + +
    + ); } -export default FunnelIssueModal; \ No newline at end of file +export default FunnelIssueModal; diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssueModal/index.ts b/frontend/app/components/Dashboard/components/Funnels/FunnelIssueModal/index.ts index 0261453c0..b5efb6390 100644 --- a/frontend/app/components/Dashboard/components/Funnels/FunnelIssueModal/index.ts +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssueModal/index.ts @@ -1 +1 @@ -export { default } from './FunnelIssueModal'; \ No newline at end of file +export { default } from './FunnelIssueModal'; diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssues/FunnelIssues.tsx b/frontend/app/components/Dashboard/components/Funnels/FunnelIssues/FunnelIssues.tsx index 172fef923..629aeeeca 100644 --- a/frontend/app/components/Dashboard/components/Funnels/FunnelIssues/FunnelIssues.tsx +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssues/FunnelIssues.tsx @@ -2,76 +2,100 @@ import React, { useEffect, useState } from 'react'; import { useStore } from 'App/mstore'; import { useObserver } from 'mobx-react-lite'; import { Loader } from 'UI'; +import { debounce } from 'App/utils'; +import useIsMounted from 'App/hooks/useIsMounted'; import FunnelIssuesDropdown from '../FunnelIssuesDropdown'; import FunnelIssuesSort from '../FunnelIssuesSort'; import FunnelIssuesList from '../FunnelIssuesList'; -import { debounce } from 'App/utils'; -import useIsMounted from 'App/hooks/useIsMounted'; +import { useTranslation } from 'react-i18next'; function FunnelIssues() { - const { metricStore, dashboardStore } = useStore(); - const [data, setData] = useState({ issues: [] }); - const [loading, setLoading] = useState(false); - const widget: any = useObserver(() => metricStore.instance); - const funnel = useObserver(() => widget.data.funnel || { stages: [] }); - const stages = useObserver(() => funnel.stages.filter((stage: any) => stage.isActive)); - const isMounted = useIsMounted() + const { t } = useTranslation(); + const { metricStore, dashboardStore } = useStore(); + const [data, setData] = useState({ issues: [] }); + const [loading, setLoading] = useState(false); + const widget: any = useObserver(() => metricStore.instance); + const funnel = useObserver(() => widget.data.funnel || { stages: [] }); + const stages = useObserver(() => + funnel.stages.filter((stage: any) => stage.isActive), + ); + const isMounted = useIsMounted(); - const fetchIssues = (filter: any) => { - if (!isMounted()) return; - setLoading(true) - - const newFilter = { - ...filter, - metricType: widget.metricType, - metricFormat: widget.metricFormat, - metricOf: widget.metricOf, - metricValue: widget.metricValue, - series: filter.series.map((item: any) => { - return { - ...item, - filter: { - ...item.filter, - filters: item.filter.filters.filter((filter: any, index: any) => { - const stage = widget.data.funnel?.stages[index]; - return stage &&stage.isActive - }).map((f: any) => f.toJson()) - } - } - }), - } - widget.fetchIssues(newFilter).then((res: any) => { - setData(res) - }).finally(() => { - setLoading(false) - }); - } + const fetchIssues = (filter: any) => { + if (!isMounted()) return; + setLoading(true); - const filter = useObserver(() => dashboardStore.drillDownFilter); - const drillDownPeriod = useObserver(() => dashboardStore.drillDownPeriod); - const debounceRequest: any = React.useCallback(debounce(fetchIssues, 1000), []); - const depsString = JSON.stringify(widget.series); + const newFilter = { + ...filter, + metricType: widget.metricType, + metricFormat: widget.metricFormat, + metricOf: widget.metricOf, + metricValue: widget.metricValue, + series: filter.series.map((item: any) => ({ + ...item, + filter: { + ...item.filter, + filters: item.filter.filters + .filter((filter: any, index: any) => { + const stage = widget.data.funnel?.stages[index]; + return stage && stage.isActive; + }) + .map((f: any) => f.toJson()), + }, + })), + }; + widget + .fetchIssues(newFilter) + .then((res: any) => { + setData(res); + }) + .finally(() => { + setLoading(false); + }); + }; - useEffect(() => { - debounceRequest({ ...filter, series: widget.series, page: metricStore.sessionsPage, limit: metricStore.sessionsPageSize }); - }, [stages.length, drillDownPeriod, filter.filters, depsString, metricStore.sessionsPage]); + const filter = useObserver(() => dashboardStore.drillDownFilter); + const drillDownPeriod = useObserver(() => dashboardStore.drillDownPeriod); + const debounceRequest: any = React.useCallback( + debounce(fetchIssues, 1000), + [], + ); + const depsString = JSON.stringify(widget.series); - return useObserver(() => ( -
    -
    -

    Significant issues in this funnel

    -
    -
    - -
    - -
    -
    - - - + useEffect(() => { + debounceRequest({ + ...filter, + series: widget.series, + page: metricStore.sessionsPage, + limit: metricStore.sessionsPageSize, + }); + }, [ + stages.length, + drillDownPeriod, + filter.filters, + depsString, + metricStore.sessionsPage, + ]); + + return useObserver(() => ( +
    +
    +

    + {t('Significant issues')} +  {t('in this funnel')} +

    +
    +
    + +
    +
    - )); +
    + + + +
    + )); } export default FunnelIssues; diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesDropdown/FunnelIssuesDropdown.tsx b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesDropdown/FunnelIssuesDropdown.tsx index 988b5a876..a928ac009 100644 --- a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesDropdown/FunnelIssuesDropdown.tsx +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesDropdown/FunnelIssuesDropdown.tsx @@ -1,120 +1,124 @@ import React, { useEffect } from 'react'; -import Select from 'Shared/Select' +import Select from 'Shared/Select'; import { components } from 'react-select'; -import { Icon } from 'UI'; import { Button } from 'antd'; import { FunnelPlotOutlined } from '@ant-design/icons'; -import FunnelIssuesSelectedFilters from '../FunnelIssuesSelectedFilters'; import { useStore } from 'App/mstore'; import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv'; +import FunnelIssuesSelectedFilters from '../FunnelIssuesSelectedFilters'; +import { useTranslation } from 'react-i18next'; +import { TFunction } from 'i18next'; -const options = [ - { value: "click_rage", label: "Click Rage" }, - { value: "dead_click", label: "Dead Click" }, - { value: "excessive_scrolling", label: "Excessive Scrolling" }, - { value: "bad_request", label: "Bad Request" }, - { value: "missing_resource", label: "Missing Image" }, - { value: "memory", label: "High Memory Usage" }, - { value: "cpu", label: "High CPU" }, - { value: "slow_resource", label: "Slow Resource" }, - { value: "slow_page_load", label: "Slow Page" }, - { value: "crash", label: "Crash" }, - { value: "custom_event_error", label: "Custom Error" }, - { value: "js_error", label: "Error" } -] +const options = (t: TFunction) => [ + { value: 'click_rage', label: t('Click Rage') }, + { value: 'dead_click', label: t('Dead Click') }, + { value: 'excessive_scrolling', label: t('Excessive Scrolling') }, + { value: 'bad_request', label: t('Bad Request') }, + { value: 'missing_resource', label: t('Missing Image') }, + { value: 'memory', label: t('High Memory Usage') }, + { value: 'cpu', label: t('High CPU') }, + { value: 'slow_resource', label: t('Slow Resource') }, + { value: 'slow_page_load', label: t('Slow Page') }, + { value: 'crash', label: t('Crash') }, + { value: 'custom_event_error', label: t('Custom Error') }, + { value: 'js_error', label: t('Error') }, +]; function FunnelIssuesDropdown() { - const { funnelStore } = useStore(); - const [isOpen, setIsOpen] = React.useState(false); - const [selectedValues, setSelectedValues] = React.useState([]); - const filteredOptions = options.filter((option: any) => { - return !selectedValues.includes(option.value); - }); + const { t } = useTranslation(); + const { funnelStore } = useStore(); + const [isOpen, setIsOpen] = React.useState(false); + const [selectedValues, setSelectedValues] = React.useState([]); + const filteredOptions = options(t).filter( + (option: any) => !selectedValues.includes(option.value), + ); - const selectedOptions = options.filter((option: any) => { - return selectedValues.includes(option.value); - }); + const selectedOptions = options(t).filter((option: any) => + selectedValues.includes(option.value), + ); - useEffect(() => { - funnelStore.updateKey('issuesFilter', selectedOptions); - }, [selectedOptions]); + useEffect(() => { + funnelStore.updateKey('issuesFilter', selectedOptions); + }, [selectedOptions]); - const handleChange = ({ value }: any) => { - toggleSelectedValue(value.value); + const handleChange = ({ value }: any) => { + toggleSelectedValue(value.value); + }; + + const toggleSelectedValue = (value: string) => { + if (selectedValues.includes(value)) { + setSelectedValues(selectedValues.filter((v) => v !== value)); + } else { + setSelectedValues([...selectedValues, value]); } + }; - const toggleSelectedValue = (value: string) => { - if (selectedValues.includes(value)) { - setSelectedValues(selectedValues.filter(v => v !== value)); - } else { - setSelectedValues([...selectedValues, value]); - } + const onClickOutside = (e: any) => { + if (e.target.id === 'dd-button') return; + if (isOpen) { + setTimeout(() => { + setIsOpen(false); + }, 0); } + }; - const onClickOutside = (e: any) => { - if (e.target.id === 'dd-button') return; - if (isOpen) { - setTimeout(() => { - setIsOpen(false); - }, 0); - } - } - - return ( -
    - setIsOpen(true)} + // onMenuClose={() => setIsOpen(false)} + options={filteredOptions} + onChange={handleChange} + styles={{ + control: (provided: any) => ({ + ...provided, + border: 'transparent', + borderColor: 'transparent', + backgroundColor: 'transparent', + minHeight: 'unset', + }), + menuList: (provided: any) => ({ + ...provided, + padding: 0, + minWidth: '190px', + }), + }} + components={{ + ValueContainer: (): any => null, + DropdownIndicator: (): any => null, + IndicatorSeparator: (): any => null, + IndicatorsContainer: (): any => null, + Control: ({ children, ...props }: any) => ( + + + {children} + + + + ), + Placeholder: (): any => null, + SingleValue: (): any => null, + }} + /> + +
    + ); } -export default FunnelIssuesDropdown; \ No newline at end of file +export default FunnelIssuesDropdown; diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesDropdown/index.ts b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesDropdown/index.ts index 7b8b3555e..450584e58 100644 --- a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesDropdown/index.ts +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesDropdown/index.ts @@ -1 +1 @@ -export { default } from './FunnelIssuesDropdown'; \ No newline at end of file +export { default } from './FunnelIssuesDropdown'; diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesList/FunnelIssuesList.tsx b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesList/FunnelIssuesList.tsx index 224654d5f..8c4b50fa0 100644 --- a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesList/FunnelIssuesList.tsx +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesList/FunnelIssuesList.tsx @@ -11,6 +11,9 @@ import { InfoCircleOutlined } from '@ant-design/icons'; import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; import FunnelIssueModal from '../FunnelIssueModal'; import FunnelIssuesListItem from '../FunnelIssuesListItem'; +import { useTranslation } from 'react-i18next'; +import { TFunction } from 'i18next'; + const { Text } = Typography; interface Issue { @@ -31,39 +34,44 @@ interface Issue { lostConversionsPer: string; } // Issue | #Users Affected | Conversion Impact | Lost Conversions -const columns: TableProps['columns'] = [ +const columns: (t: TFunction) => TableProps['columns'] = (t) => [ { - title: 'Issue', + title: t('Issue'), dataIndex: 'title', key: 'title', }, { - title: 'Page / Element', + title: t('Page / Element'), dataIndex: 'contextString', key: 'contextString', render: (text: string) => ( - {text} + + {text} + ), width: 200, }, { - title: '# Users Affected', + title: t('# Users Affected'), dataIndex: 'affectedUsers', key: 'affectedUsers', }, { - title: 'Conversion Impact', + title: t('Conversion Impact'), dataIndex: 'conversionImpact', key: 'conversionImpact', render: (text: string) => {text}%, }, { - title: 'Lost Conversions', + title: t('Lost Conversions'), dataIndex: 'lostConversions', key: 'lostConversions', render: (text: string) => {text}, @@ -77,11 +85,12 @@ interface Props extends RouteComponentProps { location: any; } function FunnelIssuesList(props: Props) { + const { t } = useTranslation(); const { issues, loading } = props; const { funnelStore } = useStore(); const issuesSort = useObserver(() => funnelStore.issuesSort); const issuesFilter = useObserver(() => - funnelStore.issuesFilter.map((issue: any) => issue.value) + funnelStore.issuesFilter.map((issue: any) => issue.value), ); const { showModal } = useModal(); const issueId = new URLSearchParams(props.location.search).get('issueId'); @@ -109,7 +118,7 @@ function FunnelIssuesList(props: Props) { let filteredIssues = useObserver(() => issuesFilter.length > 0 ? issues.filter((issue: any) => issuesFilter.includes(issue.type)) - : issues + : issues, ); filteredIssues = useObserver(() => issuesSort.sort @@ -117,12 +126,12 @@ function FunnelIssuesList(props: Props) { .slice() .sort( (a: { [x: string]: number }, b: { [x: string]: number }) => - a[issuesSort.sort] - b[issuesSort.sort] + a[issuesSort.sort] - b[issuesSort.sort], ) - : filteredIssues + : filteredIssues, ); filteredIssues = useObserver(() => - issuesSort.order === 'desc' ? filteredIssues.reverse() : filteredIssues + issuesSort.order === 'desc' ? filteredIssues.reverse() : filteredIssues, ); return useObserver(() => ( @@ -131,17 +140,20 @@ function FunnelIssuesList(props: Props) { title={
    -
    No issues found
    +
    + +  {t('No issues found')} +
    } >
    ({ onClick: () => onIssueClick(rec), })} - rowClassName={'cursor-pointer'} + rowClassName="cursor-pointer" /> )); diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesList/index.ts b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesList/index.ts index 8bab257bb..499dc0e25 100644 --- a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesList/index.ts +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesList/index.ts @@ -1 +1 @@ -export { default } from './FunnelIssuesList'; \ No newline at end of file +export { default } from './FunnelIssuesList'; diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesListItem/FunnelIssuesListItem.tsx b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesListItem/FunnelIssuesListItem.tsx index b692ca30b..730805f29 100644 --- a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesListItem/FunnelIssuesListItem.tsx +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesListItem/FunnelIssuesListItem.tsx @@ -2,88 +2,119 @@ import React from 'react'; import cn from 'classnames'; import { Icon, TextEllipsis } from 'UI'; import FunnelIssueGraph from '../FunnelIssueGraph'; +import { useTranslation } from 'react-i18next'; interface Props { - issue: any; - inDetails?: boolean; - onClick?: () => void; + issue: any; + inDetails?: boolean; + onClick?: () => void; } function FunnelIssuesListItem(props: Props) { - const { issue, inDetails = false, onClick } = props; - // const { showModal } = useModal(); - // const onClick = () => { - // showModal(, { right: true }); - // } - return ( -
    null}> - {/* {inDetails && ( + const { t } = useTranslation(); + const { issue, inDetails = false, onClick } = props; + // const { showModal } = useModal(); + // const onClick = () => { + // showModal(, { right: true }); + // } + return ( +
    null} + > + {/* {inDetails && ( )} */} -
    -
    -
    - -
    -
    - - {inDetails && ( -
    -
    {issue.title}
    -
    - -
    -
    - )} - - {!inDetails && ( -
    -
    {issue.title}
    -
    - -
    -
    - )} - -
    -
    {issue.affectedUsers}
    -
    Affected Users
    -
    - -
    -
    {issue.conversionImpact}%
    -
    Conversion Impact
    -
    - -
    -
    {issue.lostConversions}
    -
    Lost Conversions
    -
    -
    - {inDetails && ( -
    - -
    - - - -
    -
    - )} +
    +
    +
    + +
    - ); + + {inDetails && ( +
    +
    + {issue.title} +
    +
    + +
    +
    + )} + + {!inDetails && ( +
    +
    {issue.title}
    +
    + +
    +
    + )} + +
    +
    {issue.affectedUsers}
    +
    + {t('Affected Users')} +
    +
    + +
    +
    + {issue.conversionImpact} + % +
    +
    + {t('Conversion Impact')} +
    +
    + +
    +
    {issue.lostConversions}
    +
    + {t('Lost Conversions')} +
    +
    +
    + {inDetails && ( +
    + +
    + + + +
    +
    + )} +
    + ); } export default FunnelIssuesListItem; -const Info = ({ label = '', color = 'red'}) => { - return ( -
    -
    -
    -
    { label }
    -
    +function Info({ label = '', color = 'red' }) { + return ( +
    +
    +
    +
    {label}
    - ) - } \ No newline at end of file +
    + ); +} diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesListItem/index.ts b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesListItem/index.ts index f8237e361..9e1a2f544 100644 --- a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesListItem/index.ts +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesListItem/index.ts @@ -1 +1 @@ -export { default } from './FunnelIssuesListItem'; \ No newline at end of file +export { default } from './FunnelIssuesListItem'; diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSelectedFilters/FunnelIssuesSelectedFilters.tsx b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSelectedFilters/FunnelIssuesSelectedFilters.tsx index b3eaf3c2b..f54c1a17d 100644 --- a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSelectedFilters/FunnelIssuesSelectedFilters.tsx +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSelectedFilters/FunnelIssuesSelectedFilters.tsx @@ -1,33 +1,33 @@ import React from 'react'; import { Icon } from 'UI'; -import{Tag} from 'antd'; -import {CloseOutlined} from '@ant-design/icons' +import { Tag } from 'antd'; +import { CloseOutlined } from '@ant-design/icons'; import { useStore } from 'App/mstore'; import { useObserver } from 'mobx-react-lite'; interface Props { - removeSelectedValue: (value: string) => void; + removeSelectedValue: (value: string) => void; } function FunnelIssuesSelectedFilters(props: Props) { - const { funnelStore } = useStore(); - const issuesFilter = useObserver(() => funnelStore.issuesFilter); - const { removeSelectedValue } = props; + const { funnelStore } = useStore(); + const issuesFilter = useObserver(() => funnelStore.issuesFilter); + const { removeSelectedValue } = props; - return ( -
    - {issuesFilter.map((option, index) => ( - removeSelectedValue(option.value)} - className="select-none rounded-lg text-base gap-1 bg-indigo-50 flex items-center" - > - {option.label} - - ))} -
    - ); + return ( +
    + {issuesFilter.map((option, index) => ( + removeSelectedValue(option.value)} + className="select-none rounded-lg text-base gap-1 bg-indigo-50 flex items-center" + > + {option.label} + + ))} +
    + ); } -export default FunnelIssuesSelectedFilters; \ No newline at end of file +export default FunnelIssuesSelectedFilters; diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSelectedFilters/index.ts b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSelectedFilters/index.ts index a35f23d5f..e66191863 100644 --- a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSelectedFilters/index.ts +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSelectedFilters/index.ts @@ -1 +1 @@ -export { default } from './FunnelIssuesSelectedFilters'; \ No newline at end of file +export { default } from './FunnelIssuesSelectedFilters'; diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSort/FunnelIssuesSort.tsx b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSort/FunnelIssuesSort.tsx index eae83c842..1da46c36b 100644 --- a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSort/FunnelIssuesSort.tsx +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSort/FunnelIssuesSort.tsx @@ -1,45 +1,45 @@ import { useStore } from 'App/mstore'; import React from 'react'; import { Select } from 'antd'; - + const sortOptions = [ - { value: 'afectedUsers-desc', label: 'Affected Users (High)' }, - { value: 'afectedUsers-asc', label: 'Affected Users (Low)' }, - { value: 'conversionImpact-desc', label: 'Conversion Impact (High)' }, - { value: 'conversionImpact-asc', label: 'Conversion Impact (Low)' }, - { value: 'lostConversions-desc', label: 'Lost Conversions (High)' }, - { value: 'lostConversions-asc', label: 'Lost Conversions (Low)' }, -] + { value: 'afectedUsers-desc', label: 'Affected Users (High)' }, + { value: 'afectedUsers-asc', label: 'Affected Users (Low)' }, + { value: 'conversionImpact-desc', label: 'Conversion Impact (High)' }, + { value: 'conversionImpact-asc', label: 'Conversion Impact (Low)' }, + { value: 'lostConversions-desc', label: 'Lost Conversions (High)' }, + { value: 'lostConversions-asc', label: 'Lost Conversions (Low)' }, +]; interface Props { - // onChange?: (value: string) => void; + // onChange?: (value: string) => void; } function FunnelIssuesSort(props: Props) { - const { funnelStore } = useStore(); - - const onSortChange = (opt: any) => { - const [ sort, order ] = opt.value.value.split('-'); - funnelStore.updateKey('issuesSort', { sort, order }); - } + const { funnelStore } = useStore(); - return ( -
    - {/* */} - +
    + ); } -export default FunnelIssuesSort; \ No newline at end of file +export default FunnelIssuesSort; diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSort/index.ts b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSort/index.ts index 895b1eddd..2d371d773 100644 --- a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSort/index.ts +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSort/index.ts @@ -1 +1 @@ -export { default } from './FunnelIssuesSort'; \ No newline at end of file +export { default } from './FunnelIssuesSort'; diff --git a/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx b/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx index 8cb522b24..d29bda562 100644 --- a/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx +++ b/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx @@ -23,7 +23,7 @@ import { observer } from 'mobx-react-lite'; import { toast } from 'react-toastify'; import { useHistory } from 'react-router'; import { EllipsisVertical } from 'lucide-react'; -import cn from 'classnames' +import cn from 'classnames'; interface Props extends RouteComponentProps { metric: any; @@ -140,22 +140,23 @@ const MetricListItem: React.FC = ({ let hours = date.getHours(); const minutes = date.getMinutes().toString().padStart(2, '0'); const ampm = hours >= 12 ? 'PM' : 'AM'; - hours = hours % 12; - hours = hours ? hours : 12; // the hour '0' should be '12' + hours %= 12; + hours = hours || 12; // the hour '0' should be '12' return `${hours}:${minutes} ${ampm}`; }; if (diffDays <= 1) { return `Today at ${formatTime(date)}`; - } else if (diffDays <= 2) { - return `Yesterday at ${formatTime(date)}`; - } else if (diffDays <= 3) { - return `${diffDays} days ago at ${formatTime(date)}`; - } else { - return `${date.getDate()}/${ - date.getMonth() + 1 - }/${date.getFullYear()} at ${formatTime(date)}`; } + if (diffDays <= 2) { + return `Yesterday at ${formatTime(date)}`; + } + if (diffDays <= 3) { + return `${diffDays} days ago at ${formatTime(date)}`; + } + return `${date.getDate()}/${ + date.getMonth() + 1 + }/${date.getFullYear()} at ${formatTime(date)}`; }; const menuItems = [ @@ -179,7 +180,11 @@ const MetricListItem: React.FC = ({ onClick={inLibrary ? undefined : onItemClick} > -
    {metric.name}
    +
    + {metric.name} +
    {renderModal()} @@ -211,7 +216,7 @@ const MetricListItem: React.FC = ({ trigger={['click']} > @@ -79,8 +84,12 @@ function MetricViewHeader() { content={} trigger="click" > - @@ -93,4 +102,4 @@ function MetricViewHeader() { ); } -export default observer(MetricViewHeader); \ No newline at end of file +export default observer(MetricViewHeader); diff --git a/frontend/app/components/Dashboard/components/MetricsGrid/MetricsGrid.tsx b/frontend/app/components/Dashboard/components/MetricsGrid/MetricsGrid.tsx index a98a972f5..f72138f44 100644 --- a/frontend/app/components/Dashboard/components/MetricsGrid/MetricsGrid.tsx +++ b/frontend/app/components/Dashboard/components/MetricsGrid/MetricsGrid.tsx @@ -1,14 +1,8 @@ import React from 'react'; -interface Props { - -} +interface Props {} function MetricsGrid(props: Props) { - return ( -
    - -
    - ); + return
    ; } -export default MetricsGrid; \ No newline at end of file +export default MetricsGrid; diff --git a/frontend/app/components/Dashboard/components/MetricsGrid/index.ts b/frontend/app/components/Dashboard/components/MetricsGrid/index.ts index 6e16e72d9..69ac7c66b 100644 --- a/frontend/app/components/Dashboard/components/MetricsGrid/index.ts +++ b/frontend/app/components/Dashboard/components/MetricsGrid/index.ts @@ -1 +1 @@ -export { default } from './MetricsGrid' \ No newline at end of file +export { default } from './MetricsGrid'; diff --git a/frontend/app/components/Dashboard/components/MetricsLibraryModal/FooterContent.tsx b/frontend/app/components/Dashboard/components/MetricsLibraryModal/FooterContent.tsx index 6c3f6bf4e..1389af0b3 100644 --- a/frontend/app/components/Dashboard/components/MetricsLibraryModal/FooterContent.tsx +++ b/frontend/app/components/Dashboard/components/MetricsLibraryModal/FooterContent.tsx @@ -3,40 +3,59 @@ import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; import React, { useMemo } from 'react'; import { Button } from 'antd'; +import { useTranslation } from 'react-i18next'; function FooterContent({ dashboardId, selected }: any) { - const { hideModal } = useModal(); - const { metricStore, dashboardStore } = useStore(); - const dashboard = useMemo(() => dashboardStore.getDashboard(dashboardId), [dashboardId]); - - const existingCardIds = useMemo(() => dashboard?.widgets?.map(i => parseInt(i.metricId)), [dashboard]); - const total = useMemo(() => metricStore.filteredCards.filter(i => !existingCardIds?.includes(parseInt(i.metricId))).length, [metricStore.filteredCards]); - - const addSelectedToDashboard = () => { - if (!dashboard || !dashboard.dashboardId) return; - dashboardStore.addWidgetToDashboard(dashboard, selected).then(() => { - hideModal(); - dashboardStore.fetch(dashboard.dashboardId!); - }); - }; - - return ( -
    -
    - Selected {selected.length} of{' '} - {total} -
    -
    - - -
    -
    - ); - } + const { t } = useTranslation(); + const { hideModal } = useModal(); + const { metricStore, dashboardStore } = useStore(); + const dashboard = useMemo( + () => dashboardStore.getDashboard(dashboardId), + [dashboardId], + ); - export default observer(FooterContent); - \ No newline at end of file + const existingCardIds = useMemo( + () => dashboard?.widgets?.map((i) => parseInt(i.metricId)), + [dashboard], + ); + const total = useMemo( + () => + metricStore.filteredCards.filter( + (i) => !existingCardIds?.includes(parseInt(i.metricId)), + ).length, + [metricStore.filteredCards], + ); + + const addSelectedToDashboard = () => { + if (!dashboard || !dashboard.dashboardId) return; + dashboardStore.addWidgetToDashboard(dashboard, selected).then(() => { + hideModal(); + dashboardStore.fetch(dashboard.dashboardId!); + }); + }; + + return ( +
    +
    + {t('Selected')}  + {selected.length} {t('of')} +   + {total} +
    +
    + + +
    +
    + ); +} + +export default observer(FooterContent); diff --git a/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx b/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx index 0ef6a279e..775a594e7 100644 --- a/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx +++ b/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx @@ -1,28 +1,30 @@ import Modal from 'App/components/Modal/Modal'; import React, { useEffect, useState } from 'react'; -import MetricsList from '../MetricsList'; import { Icon } from 'UI'; import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; +import { Input } from 'antd'; import FooterContent from './FooterContent'; -import { Input } from 'antd' +import MetricsList from '../MetricsList'; +import { useTranslation } from 'react-i18next'; interface Props { dashboardId?: number; siteId: string; } function MetricsLibraryModal(props: Props) { + const { t } = useTranslation(); const { metricStore } = useStore(); const { siteId, dashboardId } = props; const [selectedList, setSelectedList] = useState([]); useEffect(() => { - metricStore.updateKey('page', 1) + metricStore.updateKey('page', 1); metricStore.updateKey('listView', true); return () => { - metricStore.updateKey('filter', { ...metricStore.filter, query: '' }) - } + metricStore.updateKey('filter', { ...metricStore.filter, query: '' }); + }; }, []); const onSelectionChange = (list: any) => { @@ -30,15 +32,15 @@ function MetricsLibraryModal(props: Props) { }; const onChange = ({ target: { value } }: any) => { - metricStore.updateKey('filter', { ...metricStore.filter, query: value }) + metricStore.updateKey('filter', { ...metricStore.filter, query: value }); }; return ( <> - +
    -
    Cards Library
    +
    {t('Cards Library')}
    @@ -47,7 +49,11 @@ function MetricsLibraryModal(props: Props) {
    - +
    @@ -60,13 +66,14 @@ function MetricsLibraryModal(props: Props) { export default observer(MetricsLibraryModal); function MetricSearch({ onChange }: any) { + const { t } = useTranslation(); return (
    ); diff --git a/frontend/app/components/Dashboard/components/MetricsList/GridView.tsx b/frontend/app/components/Dashboard/components/MetricsList/GridView.tsx index 195a6f65f..a7b989871 100644 --- a/frontend/app/components/Dashboard/components/MetricsList/GridView.tsx +++ b/frontend/app/components/Dashboard/components/MetricsList/GridView.tsx @@ -2,6 +2,7 @@ import React from 'react'; import WidgetWrapper from 'App/components/Dashboard/components/WidgetWrapper'; import { withRouter, RouteComponentProps } from 'react-router-dom'; import { withSiteId } from 'App/routes'; + interface Props extends RouteComponentProps { list: any; siteId: any; @@ -14,7 +15,7 @@ function GridView(props: Props) { const path = withSiteId(`/metrics/${metricId}`, siteId); history.push(path); }; - + return (
    {list.map((metric: any) => ( @@ -22,9 +23,9 @@ function GridView(props: Props) { onItemClick(parseInt(metric.metricId))} /> diff --git a/frontend/app/components/Dashboard/components/MetricsList/ListView.tsx b/frontend/app/components/Dashboard/components/MetricsList/ListView.tsx index 706079811..0c576ca52 100644 --- a/frontend/app/components/Dashboard/components/MetricsList/ListView.tsx +++ b/frontend/app/components/Dashboard/components/MetricsList/ListView.tsx @@ -8,9 +8,14 @@ import { Button, Dropdown, Modal as AntdModal, - Avatar + Avatar, } from 'antd'; -import { TeamOutlined, LockOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'; +import { + TeamOutlined, + LockOutlined, + EditOutlined, + DeleteOutlined, +} from '@ant-design/icons'; import { EllipsisVertical } from 'lucide-react'; import { TablePaginationConfig, SorterResult } from 'antd/lib/table/interface'; import { useStore } from 'App/mstore'; @@ -21,6 +26,7 @@ import { Icon } from 'UI'; import cn from 'classnames'; import { TYPE_ICONS, TYPE_NAMES } from 'App/constants/card'; import Widget from 'App/mstore/types/widget'; +import { useTranslation } from 'react-i18next'; const { Text } = Typography; @@ -43,32 +49,41 @@ const ListView: React.FC = ({ }) => { const [sorter, setSorter] = useState<{ field: string; order: 'ascend' | 'descend' }>({ field: 'lastModified', - order: 'descend' + order: 'descend', + }); + const [pagination, setPagination] = useState({ + current: 1, + pageSize: 10, }); - const [pagination, setPagination] = useState({ current: 1, pageSize: 10 }); const [editingMetricId, setEditingMetricId] = useState(null); const [newName, setNewName] = useState(''); const { metricStore } = useStore(); const history = useHistory(); - const sortedData = useMemo(() => { - return [...list].sort((a, b) => { - if (sorter.field === 'lastModified') { - return sorter.order === 'ascend' - ? new Date(a.lastModified).getTime() - new Date(b.lastModified).getTime() - : new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime(); - } else if (sorter.field === 'name') { - return sorter.order === 'ascend' - ? (a.name?.localeCompare(b.name) || 0) - : (b.name?.localeCompare(a.name) || 0); - } else if (sorter.field === 'owner') { - return sorter.order === 'ascend' - ? (a.owner?.localeCompare(b.owner) || 0) - : (b.owner?.localeCompare(a.owner) || 0); - } - return 0; - }); - }, [list, sorter]); + const sortedData = useMemo( + () => + [...list].sort((a, b) => { + if (sorter.field === 'lastModified') { + return sorter.order === 'ascend' + ? new Date(a.lastModified).getTime() - + new Date(b.lastModified).getTime() + : new Date(b.lastModified).getTime() - + new Date(a.lastModified).getTime(); + } + if (sorter.field === 'name') { + return sorter.order === 'ascend' + ? a.name?.localeCompare(b.name) || 0 + : b.name?.localeCompare(a.name) || 0; + } + if (sorter.field === 'owner') { + return sorter.order === 'ascend' + ? a.owner?.localeCompare(b.owner) || 0 + : b.owner?.localeCompare(a.owner) || 0; + } + return 0; + }), + [list, sorter], + ); const paginatedData = useMemo(() => { const start = ((pagination.current || 1) - 1) * (pagination.pageSize || 10); @@ -77,27 +92,30 @@ const ListView: React.FC = ({ const totalMessage = ( <> - Showing{' '} + {t('Showing')}{' '} {(pagination.pageSize || 10) * ((pagination.current || 1) - 1) + 1} {' '} - to{' '} + {t('to')}{' '} - {Math.min((pagination.pageSize || 10) * (pagination.current || 1), list.length)} + {Math.min( + (pagination.pageSize || 10) * (pagination.current || 1), + list.length, + )} {' '} - of {list.length} cards + {t('of')} {list.length} {t('cards')} ); const handleTableChange = ( pag: TablePaginationConfig, _filters: Record, - sorterParam: SorterResult | SorterResult[] + sorterParam: SorterResult | SorterResult[], ) => { const sortRes = sorterParam as SorterResult; setSorter({ field: sortRes.field as string, - order: sortRes.order as 'ascend' | 'descend' + order: sortRes.order as 'ascend' | 'descend', }); setPagination(pag); }; @@ -121,17 +139,23 @@ const ListView: React.FC = ({ hours = hours % 12 || 12; return `${hours}:${minutes} ${ampm}`; }; - if (diffDays <= 1) return `Today at ${formatTime(date)}`; - if (diffDays === 2) return `Yesterday at ${formatTime(date)}`; - if (diffDays <= 3) return `${diffDays} days ago at ${formatTime(date)}`; - return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()} at ${formatTime(date)}`; + if (diffDays <= 1) return `${t('Today at')} ${formatTime(date)}`; + if (diffDays === 2) return `${t('Yesterday at')} ${formatTime(date)}`; + if (diffDays <= 3) + return `${diffDays} ${t('days ago at')} ${formatTime(date)}`; + return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()} ${t('at')} ${formatTime(date)}`; }; const MetricTypeIcon: React.FC<{ type: string }> = ({ type }) => ( - {TYPE_NAMES[type]}
    }> + {TYPE_NAMES(t)[type]}
    }> + } size="default" className="bg-tealx-lightest text-tealx mr-2 cursor-default avatar-card-list-item" @@ -151,13 +175,13 @@ const ListView: React.FC = ({ const onMenuClick = async (metric: Widget, { key }: { key: string }) => { if (key === 'delete') { AntdModal.confirm({ - title: 'Confirm', - content: 'Are you sure you want to permanently delete this card?', - okText: 'Yes, delete', - cancelText: 'No', + title: t('Confirm'), + content: t('Are you sure you want to permanently delete this card?'), + okText: t('Yes, delete'), + cancelText: t('No'), onOk: async () => { await metricStore.delete(metric); - } + }, }); } if (key === 'rename') { @@ -175,17 +199,20 @@ const ListView: React.FC = ({ // await metricStore.fetchList(); setEditingMetricId(null); } catch (e) { - toast.error('Failed to rename card'); + toast.error(t('Failed to rename card')); } }; const menuItems = [ - { key: 'rename', icon: , label: 'Rename' }, - { key: 'delete', icon: , label: 'Delete' } + { key: 'rename', icon: , label: t('Rename') }, + { key: 'delete', icon: , label: t('Delete') }, ]; const renderTitle = (_text: string, metric: Widget) => ( -
    onItemClick(metric)}> +
    onItemClick(metric)} + >
    {metric.name} @@ -193,7 +220,9 @@ const ListView: React.FC = ({
    ); - const renderOwner = (_text: string, metric: Widget) =>
    {metric.owner}
    ; + const renderOwner = (_text: string, metric: Widget) => ( +
    {metric.owner}
    + ); const renderLastModified = (_text: string, metric: Widget) => { const date = parseDate(metric.lastModified); @@ -217,31 +246,31 @@ const ListView: React.FC = ({ const columns = [ { - title: 'Title', + title: t('Title'), dataIndex: 'name', key: 'title', className: 'cap-first pl-4', sorter: true, width: inLibrary ? '31%' : '25%', - render: renderTitle + render: renderTitle, }, { - title: 'Owner', + title: t('Owner'), dataIndex: 'owner', key: 'owner', className: 'capitalize', sorter: true, width: inLibrary ? '31%' : '25%', - render: renderOwner + render: renderOwner, }, { - title: 'Last Modified', + title: t('Last Modified'), dataIndex: 'lastModified', key: 'lastModified', sorter: true, width: inLibrary ? '31%' : '25%', - render: renderLastModified - } + render: renderLastModified, + }, ]; if (!inLibrary) { columns.push({ @@ -249,7 +278,7 @@ const ListView: React.FC = ({ key: 'options', className: 'text-right', width: '5%', - render: renderOptions + render: renderOptions, }); } @@ -264,19 +293,19 @@ const ListView: React.FC = ({ onRow={ inLibrary ? (record) => ({ - onClick: () => { - if (!disableSelection) toggleSelection?.(record.metricId); - } - }) + onClick: () => { + if (!disableSelection) toggleSelection?.(record?.metricId); + }, + }) : undefined } rowSelection={ !disableSelection ? { - selectedRowKeys: selectedList, - onChange: (keys) => toggleSelection && toggleSelection(keys), - columnWidth: 16 - } + selectedRowKeys: selectedList, + onChange: (keys) => toggleSelection && toggleSelection(keys), + columnWidth: 16, + } : undefined } pagination={{ @@ -288,19 +317,19 @@ const ListView: React.FC = ({ showLessItems: true, showTotal: () => totalMessage, size: 'small', - simple: true + simple: true, }} /> setEditingMetricId(null)} > setNewName(e.target.value)} /> diff --git a/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx b/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx index 41117be4e..e775bd9af 100644 --- a/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx +++ b/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx @@ -3,12 +3,13 @@ import React, { useEffect, useMemo, useState } from 'react'; import { NoContent, Loader, Pagination } from 'UI'; import { useStore } from 'App/mstore'; import { sliceListPerPage } from 'App/utils'; -import GridView from './GridView'; -import ListView from './ListView'; import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; -import AddCardSection from '../AddCardSection/AddCardSection'; import { Popover, Button } from 'antd'; import { PlusOutlined } from '@ant-design/icons'; +import GridView from './GridView'; +import ListView from './ListView'; +import AddCardSection from '../AddCardSection/AddCardSection'; +import { useTranslation } from 'react-i18next'; function MetricsList({ siteId, @@ -19,6 +20,7 @@ function MetricsList({ onSelectionChange?: (selected: any[]) => void; inLibrary?: boolean; }) { + const { t } = useTranslation(); const { metricStore, dashboardStore } = useStore(); const metricsSearch = metricStore.filter.query; const listView = inLibrary ? true : metricStore.listView; @@ -27,16 +29,16 @@ function MetricsList({ const dashboard = dashboardStore.selectedDashboard; const existingCardIds = useMemo( () => dashboard?.widgets?.map((i) => parseInt(i.metricId)), - [dashboard] + [dashboard], ); const cards = useMemo( () => - !!onSelectionChange + onSelectionChange ? metricStore.filteredCards.filter( - (i) => !existingCardIds?.includes(parseInt(i.metricId)) + (i) => !existingCardIds?.includes(parseInt(i.metricId)), ) : metricStore.filteredCards, - [metricStore.filteredCards, existingCardIds, onSelectionChange] + [metricStore.filteredCards, existingCardIds, onSelectionChange], ); const loading = metricStore.isLoading; @@ -61,7 +63,7 @@ function MetricsList({ } }; - const length = cards.length; + const { length } = cards; useEffect(() => { metricStore.updateKey('sessionsPage', 1); @@ -72,52 +74,57 @@ function MetricsList({ metricStore.updateKey('showMine', !showOwn); }; - const isFiltered = - metricsSearch !== '' || (metricStore.filter.type && metricStore.filter.type !== 'all'); + metricsSearch !== '' || + (metricStore.filter.type && metricStore.filter.type !== 'all'); const searchImageDimensions = { width: 60, height: 'auto' }; const defaultImageDimensions = { width: 600, height: 'auto' }; const emptyImage = isFiltered ? ICONS.NO_RESULTS : ICONS.NO_CARDS; - const imageDimensions = isFiltered ? searchImageDimensions : defaultImageDimensions; - + const imageDimensions = isFiltered + ? searchImageDimensions + : defaultImageDimensions; return ( - -
    - {isFiltered - ? 'No matching results' - : 'Unlock insights with data cards'} -
    -
    - } - subtext={ - isFiltered ? ( - '' - ) : ( -
    -
    - Create and customize cards to analyze trends and user behavior effectively. + show={length === 0} + title={ +
    + +
    + {isFiltered + ? t('No matching results') + : t('Unlock insights with data cards')}
    - } - trigger="click" - > - -
    - ) - } - > + } + subtext={ + isFiltered ? ( + '' + ) : ( +
    +
    + {t('Create and customize cards to analyze trends and user behavior effectively.')} +
    + } + trigger="click" + > + + +
    + ) + } + > {listView ? ( setSelectedMetrics( checked - ? cards.map((i: any) => i.metricId).slice(0, 30 - (existingCardIds?.length || 0)) - : [] + ? cards + .map((i: any) => i.metricId) + .slice(0, 30 - (existingCardIds?.length || 0)) + : [], ) } /> @@ -142,17 +151,23 @@ function MetricsList({ <>
    - Showing{' '} + {t('Showing')}{' '} {Math.min(cards.length, metricStore.pageSize)} {' '} - out of {cards.length} cards + {t('out of')}  + {cards.length}  + {t('cards')}
    {}; function MetricsSearch() { + const { t } = useTranslation(); const { metricStore } = useStore(); const [query, setQuery] = useState(metricStore.filter.query); useEffect(() => { debounceUpdate = debounce( - (key: any, value: any) => metricStore.updateKey('filter', { ...metricStore.filter, query: value }), - 500 + (key: any, value: any) => + metricStore.updateKey('filter', { + ...metricStore.filter, + query: value, + }), + 500, ); }, []); @@ -28,7 +34,7 @@ function MetricsSearch() { allowClear name="metricsSearch" className="w-full input-search-card" - placeholder="Filter by title or owner" + placeholder={t('Filter by title or owner')} onChange={write} />
    diff --git a/frontend/app/components/Dashboard/components/MetricsSearch/index.ts b/frontend/app/components/Dashboard/components/MetricsSearch/index.ts index cf23f645d..e180db4b8 100644 --- a/frontend/app/components/Dashboard/components/MetricsSearch/index.ts +++ b/frontend/app/components/Dashboard/components/MetricsSearch/index.ts @@ -1 +1 @@ -export { default } from './MetricsSearch'; \ No newline at end of file +export { default } from './MetricsSearch'; diff --git a/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx b/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx index 98fa79052..380f205ea 100644 --- a/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx +++ b/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx @@ -1,7 +1,7 @@ import React from 'react'; import withPageTitle from 'HOCs/withPageTitle'; -import MetricsList from '../MetricsList'; import { observer } from 'mobx-react-lite'; +import MetricsList from '../MetricsList'; import MetricViewHeader from '../MetricViewHeader'; interface Props { @@ -9,10 +9,13 @@ interface Props { } function MetricsView({ siteId }: Props) { return ( -
    +
    -
    - +
    +
    ); diff --git a/frontend/app/components/Dashboard/components/MetricsView/index.ts b/frontend/app/components/Dashboard/components/MetricsView/index.ts index bfebac6b9..56a24a9c8 100644 --- a/frontend/app/components/Dashboard/components/MetricsView/index.ts +++ b/frontend/app/components/Dashboard/components/MetricsView/index.ts @@ -1 +1 @@ -export { default } from './MetricsView'; \ No newline at end of file +export { default } from './MetricsView'; diff --git a/frontend/app/components/Dashboard/components/SessionsModal/SessionsModal.tsx b/frontend/app/components/Dashboard/components/SessionsModal/SessionsModal.tsx index 8655c09a8..d067dd7f0 100644 --- a/frontend/app/components/Dashboard/components/SessionsModal/SessionsModal.tsx +++ b/frontend/app/components/Dashboard/components/SessionsModal/SessionsModal.tsx @@ -6,12 +6,14 @@ import { Loader, Modal, NoContent, Pagination } from 'UI'; import SessionItem from 'Shared/SessionItem'; import Session from 'App/mstore/types/session'; import { useModal } from 'Components/Modal'; +import { useTranslation } from 'react-i18next'; interface Props { - issue: any, + issue: any; } function SessionsModal(props: Props) { + const { t } = useTranslation(); const { issue } = props; const { metricStore, dashboardStore } = useStore(); const [loading, setLoading] = React.useState(false); @@ -20,13 +22,13 @@ function SessionsModal(props: Props) { const [list, setList] = React.useState([]); const { hideModal } = useModal(); - const length = list.length; + const { length } = list; const fetchSessions = async (filter: any) => { setLoading(true); const _filter = { ...filter, - filters: [...filter.filters] + filters: [...filter.filters], }; if (issue) { @@ -45,27 +47,33 @@ function SessionsModal(props: Props) { }; useEffect(() => { - fetchSessions({ ...dashboardStore.drillDownFilter, ...metricStore.instance.toJson(), limit: 10, page: page }); + fetchSessions({ + ...dashboardStore.drillDownFilter, + ...metricStore.instance.toJson(), + limit: 10, + page, + }); }, [page]); return ( -
    - - {issue ? 'Sessions with selected issue' : 'All sessions'} +
    + + {issue ? t('Sessions with selected issue') : t('All sessions')} - + {list.map((item: any) => ( ))} -
    -
    - Showing {Math.min(length, 10)} out of{' '} - {total} Issues +
    +
    + {t('Showing')}  + {Math.min(length, 10)}{' '} + {t('out of')} {total}  + {t('Issues')}
    void }) { + const { t } = useTranslation(); return ( -
    -
    +
    +
    -
    Processing data...
    +
    {t('Processing data...')}
    +
    {t('This is taking longer than expected.')}
    - This is taking longer than expected. + {t('Use sample data to speed up query and get a faster response.')}
    {/*
    */} {/* Use sample data to speed up query and get a faster response.*/} @@ -30,7 +36,7 @@ function LongLoader({ onClick }: { onClick: () => void }) { {/* Use Sample Data*/} {/**/}
    - ) + ); } -export default LongLoader; \ No newline at end of file +export default LongLoader; diff --git a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx index 6d282096c..577670e44 100644 --- a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx +++ b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx @@ -1,8 +1,8 @@ import React, { useState, useRef, useEffect } from 'react'; -import LineChart from 'App/components/Charts/LineChart' -import BarChart from 'App/components/Charts/BarChart' -import PieChart from 'App/components/Charts/PieChart' -import ColumnChart from 'App/components/Charts/ColumnChart' +import LineChart from 'App/components/Charts/LineChart'; +import BarChart from 'App/components/Charts/BarChart'; +import PieChart from 'App/components/Charts/PieChart'; +import ColumnChart from 'App/components/Charts/ColumnChart'; import SankeyChart from 'Components/Charts/SankeyChart'; import CustomMetricPercentage from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPercentage'; @@ -10,10 +10,6 @@ import { Styles } from 'App/components/Dashboard/Widgets/common'; import { observer } from 'mobx-react-lite'; import { Icon, Loader } from 'UI'; import { useStore } from 'App/mstore'; -import FunnelTable from "../../../Funnels/FunnelWidget/FunnelTable"; -import BugNumChart from '../../Widgets/CustomMetricsWidgets/BigNumChart'; -import WidgetDatatable from '../WidgetDatatable/WidgetDatatable'; -import WidgetPredefinedChart from '../WidgetPredefinedChart'; import { getStartAndEndTimestampsByDensity } from 'Types/dashboard/helper'; import { debounce } from 'App/utils'; import useIsMounted from 'App/hooks/useIsMounted'; @@ -33,10 +29,15 @@ import CustomMetricTableSessions from 'App/components/Dashboard/Widgets/CustomMe import CustomMetricTableErrors from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableErrors'; import ClickMapCard from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard'; import InsightsCard from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard'; -import CohortCard from '../../Widgets/CustomMetricsWidgets/CohortCard'; import SessionsBy from 'Components/Dashboard/Widgets/CustomMetricsWidgets/SessionsBy'; import { useInView } from 'react-intersection-observer'; -import LongLoader from "./LongLoader"; +import CohortCard from '../../Widgets/CustomMetricsWidgets/CohortCard'; +import WidgetPredefinedChart from '../WidgetPredefinedChart'; +import WidgetDatatable from '../WidgetDatatable/WidgetDatatable'; +import BugNumChart from '../../Widgets/CustomMetricsWidgets/BigNumChart'; +import FunnelTable from '../../../Funnels/FunnelWidget/FunnelTable'; +import LongLoader from './LongLoader'; +import { useTranslation } from 'react-i18next'; interface Props { metric: any; @@ -46,6 +47,7 @@ interface Props { } function WidgetChart(props: Props) { + const { t } = useTranslation(); const { ref, inView } = useInView({ triggerOnce: true, rootMargin: '200px 0px', @@ -53,10 +55,10 @@ function WidgetChart(props: Props) { const { isSaved = false, metric, isTemplate } = props; const { dashboardStore, metricStore } = useStore(); const _metric: any = props.isPreview ? metricStore.instance : props.metric; - const data = _metric.data; - const period = dashboardStore.period; - const drillDownPeriod = dashboardStore.drillDownPeriod; - const drillDownFilter = dashboardStore.drillDownFilter; + const { data } = _metric; + const { period } = dashboardStore; + const { drillDownPeriod } = dashboardStore; + const { drillDownFilter } = dashboardStore; const colors = Styles.safeColors; const [loading, setLoading] = useState(true); const [stale, setStale] = useState(false); @@ -65,18 +67,21 @@ function WidgetChart(props: Props) { const prevMetricRef = useRef(); const isMounted = useIsMounted(); const [compData, setCompData] = useState(null); - const [enabledRows, setEnabledRows] = useState(_metric.series.map(s => s.name)); + const [enabledRows, setEnabledRows] = useState( + _metric.series.map((s) => s.name), + ); const isTableWidget = _metric.metricType === 'table' && _metric.viewType === 'table'; const isPieChart = _metric.metricType === 'table' && _metric.viewType === 'pieChart'; - useEffect(() => { - return () => { + useEffect( + () => () => { dashboardStore.setComparisonPeriod(null, _metric.metricId); dashboardStore.resetDrillDownFilter(); - }; - }, []); + }, + [], + ); useEffect(() => { if (enabledRows.length !== _metric.series.length) { @@ -87,13 +92,13 @@ function WidgetChart(props: Props) { } else { metricStore.setDisabledSeries([]); } - }, [enabledRows]) + }, [enabledRows]); useEffect(() => { if (!data.chart) return; const series = data.chart[0] ? Object.keys(data.chart[0]).filter( - (key) => key !== 'time' && key !== 'timestamp' + (key) => key !== 'time' && key !== 'timestamp', ) : []; if (series.length) { @@ -114,13 +119,13 @@ function WidgetChart(props: Props) { }); } else { // get the filter of clicked chart point - const payload = event.activePayload[0].payload; - const timestamp = payload.timestamp; + const { payload } = event.activePayload[0]; + const { timestamp } = payload; const periodTimestamps = getStartAndEndTimestampsByDensity( timestamp, drillDownPeriod.start, drillDownPeriod.end, - params.density + params.density, ); drillDownFilter.merge({ @@ -131,7 +136,7 @@ function WidgetChart(props: Props) { } }; - const loadSample = () => console.log('clicked') + const loadSample = () => console.log('clicked'); const depsString = JSON.stringify({ ..._metric.series, @@ -144,19 +149,19 @@ function WidgetChart(props: Props) { payload: any, isSaved: any, period: any, - isComparison?: boolean + isComparison?: boolean, ) => { if (!isMounted()) return; setLoading(true); const tm = setTimeout(() => { - setStale(true) - }, 4000) + setStale(true); + }, 4000); dashboardStore .fetchMetricChartData(metric, payload, isSaved, period, isComparison) .then((res: any) => { if (isComparison) setCompData(res); - clearTimeout(tm) - setStale(false) + clearTimeout(tm); + setStale(false); }) .finally(() => { if (metric.metricId === 1014) return; @@ -166,7 +171,7 @@ function WidgetChart(props: Props) { const debounceRequest: any = React.useCallback( debounce(fetchMetricChartData, 500), - [] + [], ); const loadPage = () => { if (!inView) return; @@ -183,12 +188,13 @@ function WidgetChart(props: Props) { _metric, payload, isSaved, - !isSaved ? drillDownPeriod : period + !isSaved ? drillDownPeriod : period, ); }; const loadComparisonData = () => { - if (!dashboardStore.comparisonPeriods[_metric.metricId]) return setCompData(null); + if (!dashboardStore.comparisonPeriods[_metric.metricId]) + return setCompData(null); // TODO: remove after backend adds support for more view types const payload = { @@ -201,7 +207,7 @@ function WidgetChart(props: Props) { payload, isSaved, dashboardStore.comparisonPeriods[_metric.metricId], - true + true, ); }; useEffect(() => { @@ -216,12 +222,12 @@ function WidgetChart(props: Props) { period, depsString, dashboardStore.selectedDensity, - _metric.metricOf + _metric.metricOf, ]); useEffect(() => { setCompData(null); _metric.updateKey('page', 1); - _metric.updateKey() + _metric.updateKey(); loadPage(); }, [ drillDownPeriod, @@ -240,19 +246,17 @@ function WidgetChart(props: Props) { const onFocus = (seriesName: string) => { metricStore.setFocusedSeriesName(seriesName); - metricStore.setDrillDown(true) - } + metricStore.setDrillDown(true); + }; const renderChart = React.useCallback(() => { const { metricType, metricOf } = _metric; - const viewType = _metric.viewType; + const { viewType } = _metric; const metricWithData = { ..._metric, data }; if (metricType === FUNNEL) { if (viewType === 'table') { - return ( - - ) + return ; } if (viewType === 'metric') { const values: { @@ -267,7 +271,7 @@ function WidgetChart(props: Props) { ? compData.funnel.totalConversionsPercentage : undefined, series: 'Dynamic', - valueLabel: '%' + valueLabel: '%', }, ]; @@ -278,9 +282,7 @@ function WidgetChart(props: Props) { colors={colors} hideLegend onClick={onChartClick} - label={ - 'Conversion' - } + label={t('Conversion')} /> ); } @@ -317,9 +319,7 @@ function WidgetChart(props: Props) { : chartData.namesMap; const compDataCopy = { ...compData }; compDataCopy.namesMap = Array.isArray(compDataCopy.namesMap) - ? compDataCopy.namesMap.map((n) => - enabledRows.includes(n) ? n : null - ) + ? compDataCopy.namesMap.map((n) => (enabledRows.includes(n) ? n : null)) : compDataCopy.namesMap; if (viewType === 'lineChart') { @@ -333,8 +333,8 @@ function WidgetChart(props: Props) { onClick={onChartClick} label={ _metric.metricOf === 'sessionCount' - ? 'Number of Sessions' - : 'Number of Users' + ? t('Number of Sessions') + : t('Number of Users') } /> ); @@ -350,8 +350,8 @@ function WidgetChart(props: Props) { onSeriesFocus={onFocus} label={ _metric.metricOf === 'sessionCount' - ? 'Number of Sessions' - : 'Number of Users' + ? t('Number of Sessions') + : t('Number of Users') } /> ); @@ -368,13 +368,13 @@ function WidgetChart(props: Props) { onClick={onChartClick} label={ _metric.metricOf === 'sessionCount' - ? 'Number of Sessions' - : 'Number of Users' + ? t('Number of Sessions') + : t('Number of Users') } /> ); } - + if (viewType === 'progressChart') { return ( ); @@ -401,8 +401,8 @@ function WidgetChart(props: Props) { onSeriesFocus={onFocus} label={ _metric.metricOf === 'sessionCount' - ? 'Number of Sessions' - : 'Number of Users' + ? t('Number of Sessions') + : t('Number of Users') } /> ); @@ -416,8 +416,8 @@ function WidgetChart(props: Props) { params={params} label={ _metric.metricOf === 'sessionCount' - ? 'Number of Sessions' - : 'Number of Users' + ? t('Number of Sessions') + : t('Number of Users') } /> ); @@ -426,15 +426,24 @@ function WidgetChart(props: Props) { return null; } if (viewType === 'metric') { - const values: { value: number, compData?: number, series: string }[] = []; + const values: { value: number; compData?: number; series: string }[] = + []; for (let i = 0; i < data.namesMap.length; i++) { if (!data.namesMap[i]) { continue; } values.push({ - value: data.chart.reduce((acc, curr) => acc + curr[data.namesMap[i]], 0), - compData: compData ? compData.chart.reduce((acc, curr) => acc + curr[compData.namesMap[i]], 0) : undefined, + value: data.chart.reduce( + (acc, curr) => acc + curr[data.namesMap[i]], + 0, + ), + compData: compData + ? compData.chart.reduce( + (acc, curr) => acc + curr[compData.namesMap[i]], + 0, + ) + : undefined, series: data.namesMap[i], }); } @@ -447,8 +456,8 @@ function WidgetChart(props: Props) { onSeriesFocus={onFocus} label={ _metric.metricOf === 'sessionCount' - ? 'Number of Sessions' - : 'Number of Users' + ? t('Number of Sessions') + : t('Number of Users') } /> ); @@ -505,7 +514,7 @@ function WidgetChart(props: Props) { style={{ height: '229px' }} > - No data available for the selected period. + {t('No data available for the selected period.')}
    ); } @@ -517,7 +526,9 @@ function WidgetChart(props: Props) { } if (metricType === USER_PATH && data && data.links) { - const isUngrouped = props.isPreview ? (!(_metric.hideExcess ?? true)) : false; + const isUngrouped = props.isPreview + ? !(_metric.hideExcess ?? true) + : false; const height = props.isPreview ? 550 : 240; return ( ); - } else if (viewType === 'cohort') { + } + if (viewType === 'cohort') { return ; } } console.log('Unknown metric type', metricType); - return
    Unknown metric type
    ; + return
    {t('Unknown metric type')}
    ; }, [data, compData, enabledRows, _metric]); - - const showTable = _metric.metricType === TIMESERIES && (props.isPreview || _metric.viewType === TABLE) - const tableMode = _metric.viewType === 'table' && _metric.metricType === TIMESERIES + const showTable = + _metric.metricType === TIMESERIES && + (props.isPreview || _metric.viewType === TABLE); + const tableMode = + _metric.viewType === 'table' && _metric.metricType === TIMESERIES; return (
    - {loading ? stale ? : : ( + {loading ? ( + stale ? ( + + ) : ( + + ) + ) : (
    {renderChart()} {showTable ? ( (initTableProps); const data = React.useMemo(() => { - const dataObj = { ...props.data } + const dataObj = { ...props.data }; if (props.compData) { dataObj.chart = dataObj.chart.map((item, i) => { const compItem = props.compData!.chart[i]; @@ -52,22 +55,20 @@ function WidgetDatatable(props: Props) { return newItem; }); const blank = new Array(dataObj.namesMap.length * 2).fill(''); - dataObj.namesMap = blank.map((_, i) => { - return i % 2 !== 0 + dataObj.namesMap = blank.map((_, i) => + i % 2 !== 0 ? `Previous ${dataObj.namesMap[i / 2]}` - : dataObj.namesMap[i / 2]; - }) + : dataObj.namesMap[i / 2], + ); } - return dataObj + return dataObj; }, [props.data, props.compData]); const [showTable, setShowTable] = useState(props.defaultOpen); const [tableData, setTableData] = useState([]); const columnNames = []; - const series = !data.chart[0] - ? [] - : data.namesMap; + const series = !data.chart[0] ? [] : data.namesMap; React.useEffect(() => { if (!data.chart) return; @@ -91,24 +92,26 @@ function WidgetDatatable(props: Props) { }[] = []; columnNames.forEach((name: string, i) => { tableCols.push({ - title: {name}, - dataIndex: name+'_'+i, - key: name+'_'+i, - sorter: (a, b) => a[name+'_'+i] - b[name+'_'+i], + title: {name}, + dataIndex: `${name}_${i}`, + key: `${name}_${i}`, + sorter: (a, b) => a[`${name}_${i}`] - b[`${name}_${i}`], }); const values = data.chart[i]; series.forEach((s) => { const ind = items.findIndex((item) => item.seriesName === s); if (ind === -1) return; - items[ind][name+'_'+i] = values[s]; + items[ind][`${name}_${i}`] = values[s]; }); }); // calculating averages for each row items.forEach((item) => { const itemsLen = columnNames.length; - const keys = Object.keys(item).filter(k => !['seriesName', 'key', 'average'].includes(k)); + const keys = Object.keys(item).filter( + (k) => !['seriesName', 'key', 'average'].includes(k), + ); let sum = 0; - const values = keys.map(k => item[k]); + const values = keys.map((k) => item[k]); values.forEach((v) => { sum += v; }); @@ -146,25 +149,25 @@ function WidgetDatatable(props: Props) { >
    )} {showTable || isTableOnlyMode ? ( -
    +
    {props.inBuilder ? ( diff --git a/frontend/app/components/Dashboard/components/WidgetDateRange/RangeGranularity.tsx b/frontend/app/components/Dashboard/components/WidgetDateRange/RangeGranularity.tsx index 588843656..02d9a970c 100644 --- a/frontend/app/components/Dashboard/components/WidgetDateRange/RangeGranularity.tsx +++ b/frontend/app/components/Dashboard/components/WidgetDateRange/RangeGranularity.tsx @@ -1,29 +1,27 @@ -import React from 'react' -import { DownOutlined } from "@ant-design/icons"; +import React from 'react'; +import { DownOutlined } from '@ant-design/icons'; import { Button, Dropdown, MenuProps } from 'antd'; - function RangeGranularity({ period, density, - onDensityChange + onDensityChange, }: { period: { getDuration(): number; - }, - density: number, - onDensityChange: (density: number) => void + }; + density: number; + onDensityChange: (density: number) => void; }) { const granularityOptions = React.useMemo(() => { - if (!period) return [] + if (!period) return []; return calculateGranularities(period.getDuration()); }, [period]); - const menuProps: MenuProps = { items: granularityOptions, - onClick: (item: any) => onDensityChange(Number(item.key)), - } + onClick: (item: any) => onDensityChange(Number(item.key)), + }; const selected = React.useMemo(() => { let selected = 'Custom'; for (const option of granularityOptions) { @@ -33,7 +31,7 @@ function RangeGranularity({ } } return selected; - }, [period, density]) + }, [period, density]); React.useEffect(() => { if (granularityOptions.length === 0) return; @@ -43,15 +41,20 @@ function RangeGranularity({ return ( - - - ) + + ); } -const PAST_24_HR_MS = 24 * 60 * 60 * 1000 +const PAST_24_HR_MS = 24 * 60 * 60 * 1000; function calculateGranularities(periodDurationMs: number) { const granularities = [ { label: 'Hourly', durationMs: 60 * 60 * 1000 }, @@ -64,14 +67,14 @@ function calculateGranularities(periodDurationMs: number) { const result = []; if (periodDurationMs === PAST_24_HR_MS) { // if showing for 1 day, show by minute split as well - granularities.unshift( - { label: 'By minute', durationMs: 60 * 1000 }, - ) + granularities.unshift({ label: 'By minute', durationMs: 60 * 1000 }); } for (const granularity of granularities) { if (periodDurationMs >= granularity.durationMs) { - const density = Math.floor(Number(BigInt(periodDurationMs) / BigInt(granularity.durationMs))); + const density = Math.floor( + Number(BigInt(periodDurationMs) / BigInt(granularity.durationMs)), + ); result.push({ label: granularity.label, key: density }); } } @@ -79,4 +82,4 @@ function calculateGranularities(periodDurationMs: number) { return result; } -export default RangeGranularity; \ No newline at end of file +export default RangeGranularity; diff --git a/frontend/app/components/Dashboard/components/WidgetDateRange/WidgetDateRange.tsx b/frontend/app/components/Dashboard/components/WidgetDateRange/WidgetDateRange.tsx index 26e7c5cf7..b905dda57 100644 --- a/frontend/app/components/Dashboard/components/WidgetDateRange/WidgetDateRange.tsx +++ b/frontend/app/components/Dashboard/components/WidgetDateRange/WidgetDateRange.tsx @@ -3,12 +3,10 @@ import SelectDateRange from 'Shared/SelectDateRange'; import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; import { Space } from 'antd'; -import RangeGranularity from "./RangeGranularity"; -import { - CUSTOM_RANGE, - DATE_RANGE_COMPARISON_OPTIONS, -} from 'App/dateRange'; +import { CUSTOM_RANGE, DATE_RANGE_COMPARISON_OPTIONS } from 'App/dateRange'; import Period from 'Types/app/period'; +import RangeGranularity from './RangeGranularity'; +import { useTranslation } from 'react-i18next'; function WidgetDateRange({ label = 'Time Range', @@ -17,22 +15,24 @@ function WidgetDateRange({ hasComparison = false, presetComparison = null, }: any) { + const { t } = useTranslation(); const { dashboardStore, metricStore } = useStore(); - const density = dashboardStore.selectedDensity + const density = dashboardStore.selectedDensity; const onDensityChange = (density: number) => { dashboardStore.setDensity(density); - } - const period = dashboardStore.drillDownPeriod; - const compPeriod = dashboardStore.comparisonPeriods[metricStore.instance.metricId]; - const drillDownFilter = dashboardStore.drillDownFilter; + }; + const period = dashboardStore.drillDownPeriod; + const compPeriod = + dashboardStore.comparisonPeriods[metricStore.instance.metricId]; + const { drillDownFilter } = dashboardStore; const onChangePeriod = (period: any) => { - dashboardStore.setDrillDownPeriod(period); - const periodTimestamps = period.toTimestamps(); - drillDownFilter.merge({ - startTimestamp: periodTimestamps.startTimestamp, - endTimestamp: periodTimestamps.endTimestamp, - }); + dashboardStore.setDrillDownPeriod(period); + const periodTimestamps = period.toTimestamps(); + drillDownFilter.merge({ + startTimestamp: periodTimestamps.startTimestamp, + endTimestamp: periodTimestamps.endTimestamp, + }); }; const onChangeComparison = (period: any) => { @@ -42,11 +42,11 @@ function WidgetDateRange({ } } dashboardStore.setComparisonPeriod(period, metricStore.instance.metricId); - } + }; React.useEffect(() => { if (presetComparison && presetComparison.length) { - const option = DATE_RANGE_COMPARISON_OPTIONS.find((option: any) => option.value === presetComparison[0]); + const option = DATE_RANGE_COMPARISON_OPTIONS(t).find((option: any) => option.value === presetComparison[0]); if (option) { // @ts-ignore const newPeriod = new Period({ @@ -56,7 +56,7 @@ function WidgetDateRange({ }); setTimeout(() => { onChangeComparison(newPeriod); - }, 1) + }, 1); } else { const start = parseInt(presetComparison[0], 10); const end = parseInt(presetComparison[1], 10); @@ -69,15 +69,17 @@ function WidgetDateRange({ }); setTimeout(() => { onChangeComparison(compRange); - }, 1) + }, 1); } } - }, [presetComparison]) + }, [presetComparison]); - const updateInstComparison = (range: [start: string, end?: string] | null) => { + const updateInstComparison = ( + range: [start: string, end?: string] | null, + ) => { metricStore.instance.setComparisonRange(range); - metricStore.instance.updateKey('hasChanged', true) - } + metricStore.instance.updateKey('hasChanged', true); + }; return ( @@ -85,8 +87,8 @@ function WidgetDateRange({ {hasGranularSettings ? ( <> @@ -97,19 +99,19 @@ function WidgetDateRange({ onDensityChange={onDensityChange} /> ) : null} - {hasComparison ? + {hasComparison ? ( - : null} + ) : null} ) : null} diff --git a/frontend/app/components/Dashboard/components/WidgetForm/CardBuilder.tsx b/frontend/app/components/Dashboard/components/WidgetForm/CardBuilder.tsx index 65009e97c..40fc79e9c 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/CardBuilder.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/CardBuilder.tsx @@ -1,322 +1,420 @@ -import React, {useEffect, useState, useCallback} from 'react'; -import {observer} from 'mobx-react-lite'; -import {useStore} from 'App/mstore'; -import {metricOf, issueOptions, issueCategories} from 'App/constants/filterOptions'; -import {FilterKey} from 'Types/filter/filterType'; -import {withSiteId, dashboardMetricDetails, metricDetails} from 'App/routes'; -import {Icon, confirm} from 'UI'; -import {Card, Input, Space, Button, Segmented, Alert} from 'antd'; -import {AudioWaveform} from "lucide-react"; -import FilterSeries from '../FilterSeries'; +import React, { useEffect, useState, useCallback } from 'react'; +import { observer } from 'mobx-react-lite'; +import { useStore } from 'App/mstore'; +import { + metricOf, + issueOptions, + issueCategories, +} from 'App/constants/filterOptions'; +import { FilterKey } from 'Types/filter/filterType'; +import { withSiteId, dashboardMetricDetails, metricDetails } from 'App/routes'; +import { Icon, confirm } from 'UI'; +import { Card, Input, Space, Button, Segmented, Alert } from 'antd'; +import { AudioWaveform } from 'lucide-react'; import Select from 'Shared/Select'; -import MetricTypeDropdown from './components/MetricTypeDropdown'; -import MetricSubtypeDropdown from './components/MetricSubtypeDropdown'; -import {eventKeys} from 'App/types/filter/newFilter'; -import {renderClickmapThumbnail} from './renderMap'; +import { eventKeys } from 'App/types/filter/newFilter'; import FilterItem from 'Shared/Filters/FilterItem'; import { - TIMESERIES, TABLE, HEATMAP, FUNNEL, ERRORS, INSIGHTS, USER_PATH, RETENTION + TIMESERIES, + TABLE, + HEATMAP, + FUNNEL, + ERRORS, + INSIGHTS, + USER_PATH, + RETENTION, } from 'App/constants/card'; -import {useHistory} from "react-router"; +import { useHistory } from 'react-router'; +import { renderClickmapThumbnail } from './renderMap'; +import MetricSubtypeDropdown from './components/MetricSubtypeDropdown'; +import MetricTypeDropdown from './components/MetricTypeDropdown'; +import FilterSeries from '../FilterSeries'; +import { useTranslation } from 'react-i18next'; const tableOptions = metricOf.filter((i) => i.type === 'table'); -const AIInput = ({value, setValue, placeholder, onEnter}) => ( +function AIInput({ value, setValue, placeholder, onEnter }) { + return ( setValue(e.target.value)} - className='w-full mb-2 bg-white' - onKeyDown={(e) => e.key === 'Enter' && onEnter()} + placeholder={placeholder} + value={value} + onChange={(e) => setValue(e.target.value)} + className="w-full mb-2 bg-white" + onKeyDown={(e) => e.key === 'Enter' && onEnter()} /> -); - -const PredefinedMessage = () => ( - -); - -const MetricTabs = ({metric, writeOption}: any) => { - if (![TABLE].includes(metric.metricType)) return null; - - const onChange = (value: string) => { - writeOption({ - value: { - value - }, name: 'metricOf' - }); - } - - return ( - - ) + ); } -const MetricOptions = ({metric, writeOption}: any) => { - const isUserPath = metric.metricType === USER_PATH; +function PredefinedMessage() { + const { t } = useTranslation(); + return ( + + ); +} - return ( -
    -
    - Card showing - - - {isUserPath && ( - <> - - - - )} - {metric.metricOf === FilterKey.ISSUE && metric.metricType === TABLE && ( - <> - issue type - - - )} - {metric.metricType === TABLE && - !(metric.metricOf === FilterKey.ERRORS || metric.metricOf === FilterKey.SESSIONS) && ( - <> - showing - -
    - + {t('showing')} + + + )} + {metric.metricType === INSIGHTS && ( + <> + {t('of')} + + + )} +
    + + ); +} + +const PathAnalysisFilter = observer(({ metric }: any) => ( + +
    + {/* {metric.startType === 'start' ? 'Start Point' : 'End Point'} */} + metric.updateStartPoint(val)} + onRemoveFilter={() => {}} + /> +
    +
    )); const SeriesList = observer(() => { - const {metricStore, dashboardStore, aiFiltersStore} = useStore(); - const metric = metricStore.instance; - const excludeFilterKeys = [HEATMAP, USER_PATH].includes(metric.metricType) ? eventKeys : []; - const hasSeries = ![TABLE, FUNNEL, HEATMAP, INSIGHTS, USER_PATH, RETENTION].includes(metric.metricType); - const canAddSeries = metric.series.length < 3; + const { t } = useTranslation(); + const { metricStore, dashboardStore, aiFiltersStore } = useStore(); + const metric = metricStore.instance; + const excludeFilterKeys = [HEATMAP, USER_PATH].includes(metric.metricType) + ? eventKeys + : []; + const hasSeries = ![ + TABLE, + FUNNEL, + HEATMAP, + INSIGHTS, + USER_PATH, + RETENTION, + ].includes(metric.metricType); + const canAddSeries = metric.series.length < 3; - return ( -
    - {metric.series.length > 0 && metric.series - .slice(0, hasSeries ? metric.series.length : 1) - .map((series, index) => ( -
    - metric.updateKey('hasChanged', true)} - hideHeader={[TABLE, HEATMAP, INSIGHTS, USER_PATH, FUNNEL].includes(metric.metricType)} - seriesIndex={index} - series={series} - onRemoveSeries={() => metric.removeSeries(index)} - canDelete={metric.series.length > 1} - emptyMessage={ - metric.metricType === TABLE - ? 'Filter data using any event or attribute. Use Add Step button below to do so.' - : 'Add an event or filter step to define the series.' - } - /> -
    - ))} - {hasSeries && ( - - - - )} -
    - ); + return ( +
    + {metric.series.length > 0 && + metric.series + .slice(0, hasSeries ? metric.series.length : 1) + .map((series, index) => ( +
    + metric.updateKey('hasChanged', true)} + hideHeader={[ + TABLE, + HEATMAP, + INSIGHTS, + USER_PATH, + FUNNEL, + ].includes(metric.metricType)} + seriesIndex={index} + series={series} + onRemoveSeries={() => metric.removeSeries(index)} + canDelete={metric.series.length > 1} + emptyMessage={ + metric.metricType === TABLE + ? t( + 'Filter data using any event or attribute. Use Add Step button below to do so.', + ) + : t('Add an event or filter step to define the series.') + } + /> +
    + ))} + {hasSeries && ( + + + + )} +
    + ); }); interface RouteParams { - siteId: string; - dashboardId: string; - metricId: string; + siteId: string; + dashboardId: string; + metricId: string; } interface CardBuilderProps { - siteId: string; - dashboardId?: string; - metricId?: string; + siteId: string; + dashboardId?: string; + metricId?: string; } const CardBuilder = observer((props: CardBuilderProps) => { - const history = useHistory(); - const {siteId, dashboardId, metricId} = props; - const {metricStore, dashboardStore, aiFiltersStore} = useStore(); - const [aiQuery, setAiQuery] = useState(''); - const [aiAskChart, setAiAskChart] = useState(''); - const [initialInstance, setInitialInstance] = useState(null); - const metric = metricStore.instance; - const timeseriesOptions = metricOf.filter(i => i.type === 'timeseries'); - const tableOptions = metricOf.filter(i => i.type === 'table'); - const isPredefined = metric.metricType === ERRORS - const testingKey = localStorage.getItem('__mauricio_testing_access') === 'true'; + const { t } = useTranslation(); + const history = useHistory(); + const { siteId, dashboardId, metricId } = props; + const { metricStore, dashboardStore, aiFiltersStore } = useStore(); + const [aiQuery, setAiQuery] = useState(''); + const [aiAskChart, setAiAskChart] = useState(''); + const [initialInstance, setInitialInstance] = useState(null); + const metric = metricStore.instance; + const timeseriesOptions = metricOf.filter((i) => i.type === 'timeseries'); + const tableOptions = metricOf.filter((i) => i.type === 'table'); + const isPredefined = metric.metricType === ERRORS; + const testingKey = + localStorage.getItem('__mauricio_testing_access') === 'true'; + useEffect(() => { + if (metric && !initialInstance) setInitialInstance(metric.toJson()); + }, [metric]); - useEffect(() => { - if (metric && !initialInstance) setInitialInstance(metric.toJson()); - }, [metric]); + const writeOption = useCallback( + ({ value, name }) => { + value = Array.isArray(value) ? value : value.value; + const obj: any = { [name]: value }; + if (name === 'metricType') { + if (value === TIMESERIES) obj.metricOf = timeseriesOptions[0].value; + if (value === TABLE) obj.metricOf = tableOptions[0].value; + } + metricStore.merge(obj); + }, + [metricStore, timeseriesOptions, tableOptions], + ); - const writeOption = useCallback(({value, name}) => { - value = Array.isArray(value) ? value : value.value; - const obj: any = {[name]: value}; - if (name === 'metricType') { - if (value === TIMESERIES) obj.metricOf = timeseriesOptions[0].value; - if (value === TABLE) obj.metricOf = tableOptions[0].value; - } - metricStore.merge(obj); - }, [metricStore, timeseriesOptions, tableOptions]); + const onSave = useCallback(async () => { + const wasCreating = !metric.exists(); + if (metric.metricType === HEATMAP) { + try { + metric.thumbnail = await renderClickmapThumbnail(); + } catch (e) { + console.error(e); + } + } + const savedMetric = await metricStore.save(metric); + setInitialInstance(metric.toJson()); + if (wasCreating) { + const route = + parseInt(dashboardId, 10) > 0 + ? withSiteId( + dashboardMetricDetails(dashboardId, savedMetric.metricId), + siteId, + ) + : withSiteId(metricDetails(savedMetric.metricId), siteId); + history.replace(route); + if (parseInt(dashboardId, 10) > 0) { + dashboardStore.addWidgetToDashboard( + dashboardStore.getDashboard(parseInt(dashboardId, 10)), + [savedMetric.metricId], + ); + } + } + }, [dashboardId, dashboardStore, history, metric, metricStore, siteId]); - const onSave = useCallback(async () => { - const wasCreating = !metric.exists(); - if (metric.metricType === HEATMAP) { - try { - metric.thumbnail = await renderClickmapThumbnail(); - } catch (e) { - console.error(e); - } - } - const savedMetric = await metricStore.save(metric); - setInitialInstance(metric.toJson()); - if (wasCreating) { - const route = parseInt(dashboardId, 10) > 0 - ? withSiteId(dashboardMetricDetails(dashboardId, savedMetric.metricId), siteId) - : withSiteId(metricDetails(savedMetric.metricId), siteId); - history.replace(route); - if (parseInt(dashboardId, 10) > 0) { - dashboardStore.addWidgetToDashboard( - dashboardStore.getDashboard(parseInt(dashboardId, 10)), - [savedMetric.metricId] - ); - } - } - }, [dashboardId, dashboardStore, history, metric, metricStore, siteId]); + const onDelete = useCallback(async () => { + if ( + await confirm({ + header: t('Confirm'), + confirmButton: t('Yes, delete'), + confirmation: t( + 'Are you sure you want to permanently delete this card?', + ), + }) + ) { + metricStore.delete(metric).then(onDelete); + } + }, [metric, metricStore]); - const onDelete = useCallback(async () => { - if (await confirm({ - header: 'Confirm', - confirmButton: 'Yes, delete', - confirmation: 'Are you sure you want to permanently delete this card?' - })) { - metricStore.delete(metric).then(onDelete); - } - }, [metric, metricStore]); + // const undoChanges = useCallback(() => { + // const w = new Widget(); + // metricStore.merge(w.fromJson(initialInstance), false); + // }, [initialInstance, metricStore]); - // const undoChanges = useCallback(() => { - // const w = new Widget(); - // metricStore.merge(w.fromJson(initialInstance), false); - // }, [initialInstance, metricStore]); + const fetchResults = useCallback( + () => + aiFiltersStore + .getCardFilters(aiQuery, metric.metricType) + .then((f) => metric.createSeries(f.filters)), + [aiFiltersStore, aiQuery, metric], + ); - const fetchResults = useCallback(() => aiFiltersStore.getCardFilters(aiQuery, metric.metricType) - .then(f => metric.createSeries(f.filters)), [aiFiltersStore, aiQuery, metric]); + const fetchChartData = useCallback( + () => aiFiltersStore.getCardData(aiAskChart, metric.toJson()), + [aiAskChart, aiFiltersStore, metric], + ); - const fetchChartData = useCallback(() => aiFiltersStore.getCardData(aiAskChart, metric.toJson()), - [aiAskChart, aiFiltersStore, metric]); + return ( +
    + {/* */} - return ( -
    - {/**/} + {/* */} - {/**/} - - {metric.metricType === USER_PATH && } - {isPredefined && } - {testingKey && ( - <> - - - - )} - {aiFiltersStore.isLoading && ( -
    -
    Loading
    -
    - )} - {!isPredefined && } + {metric.metricType === USER_PATH && ( + + )} + {isPredefined && } + {testingKey && ( + <> + + + + )} + {aiFiltersStore.isLoading && ( +
    +
    + {t('Loading')} +
    - ); + )} + {!isPredefined && } +
    + ); }); export default CardBuilder; diff --git a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx index 39b2f192e..9230a62b0 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx @@ -1,158 +1,169 @@ -import React, {useEffect, useState} from 'react'; +import React, { useEffect, useState } from 'react'; import { metricOf } from 'App/constants/filterOptions'; -import {FilterKey} from 'Types/filter/filterType'; -import {useStore} from 'App/mstore'; -import {observer} from 'mobx-react-lite'; +import { FilterKey } from 'Types/filter/filterType'; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; import { Icon, confirm, Tooltip } from 'UI'; -import { Input, Alert, Button } from 'antd' -import FilterSeries from '../FilterSeries'; +import { Input, Alert, Button } from 'antd'; import { withSiteId, dashboardMetricDetails, metricDetails } from 'App/routes'; import { - TIMESERIES, - TABLE, - HEATMAP, - FUNNEL, - ERRORS, - INSIGHTS, - USER_PATH, - RETENTION + TIMESERIES, + TABLE, + HEATMAP, + FUNNEL, + ERRORS, + INSIGHTS, + USER_PATH, + RETENTION, } from 'App/constants/card'; -import {eventKeys} from 'App/types/filter/newFilter'; -import {renderClickmapThumbnail} from './renderMap'; +import { eventKeys } from 'App/types/filter/newFilter'; import Widget from 'App/mstore/types/widget'; import FilterItem from 'Shared/Filters/FilterItem'; +import { renderClickmapThumbnail } from './renderMap'; +import FilterSeries from '../FilterSeries'; +import { useTranslation } from 'react-i18next'; interface Props { - history: any; - match: any; - onDelete: () => void; - expanded?: boolean; + history: any; + match: any; + onDelete: () => void; + expanded?: boolean; } function WidgetForm(props: Props) { - const { - history, - match: { - params: {siteId, dashboardId} - } - } = props; - const [aiQuery, setAiQuery] = useState('') - const [aiAskChart, setAiAskChart] = useState('') - const {metricStore, dashboardStore, aiFiltersStore} = useStore(); - const isSaving = metricStore.isSaving; - const metric: any = metricStore.instance; - const [initialInstance, setInitialInstance] = useState(); + const { t } = useTranslation(); + const { + history, + match: { + params: { siteId, dashboardId }, + }, + } = props; + const [aiQuery, setAiQuery] = useState(''); + const [aiAskChart, setAiAskChart] = useState(''); + const { metricStore, dashboardStore, aiFiltersStore } = useStore(); + const { isSaving } = metricStore; + const metric: any = metricStore.instance; + const [initialInstance, setInitialInstance] = useState(); - const timeseriesOptions = metricOf.filter((i) => i.type === 'timeseries'); - const tableOptions = metricOf.filter((i) => i.type === 'table'); - const isTable = metric.metricType === TABLE; - const isClickmap = metric.metricType === HEATMAP; - const isFunnel = metric.metricType === FUNNEL; - const isInsights = metric.metricType === INSIGHTS; - const isPathAnalysis = metric.metricType === USER_PATH; - const isRetention = metric.metricType === RETENTION; - const canAddSeries = metric.series.length < 3; - const eventsLength = metric.series[0].filter.filters.filter((i: any) => i && i.isEvent).length; - const cannotSaveFunnel = isFunnel && (!metric.series[0] || eventsLength <= 1); + const timeseriesOptions = metricOf.filter((i) => i.type === 'timeseries'); + const tableOptions = metricOf.filter((i) => i.type === 'table'); + const isTable = metric.metricType === TABLE; + const isClickmap = metric.metricType === HEATMAP; + const isFunnel = metric.metricType === FUNNEL; + const isInsights = metric.metricType === INSIGHTS; + const isPathAnalysis = metric.metricType === USER_PATH; + const isRetention = metric.metricType === RETENTION; + const canAddSeries = metric.series.length < 3; + const eventsLength = metric.series[0].filter.filters.filter( + (i: any) => i && i.isEvent, + ).length; + const cannotSaveFunnel = isFunnel && (!metric.series[0] || eventsLength <= 1); - const isPredefined = metric.metricType === ERRORS + const isPredefined = metric.metricType === ERRORS; - const excludeFilterKeys = isClickmap || isPathAnalysis ? eventKeys : []; + const excludeFilterKeys = isClickmap || isPathAnalysis ? eventKeys : []; - useEffect(() => { - if (!!metric && !initialInstance) { - setInitialInstance(metric.toJson()); - } - }, [metric]); + useEffect(() => { + if (!!metric && !initialInstance) { + setInitialInstance(metric.toJson()); + } + }, [metric]); - const writeOption = ({value, name}: { value: any; name: any }) => { - value = Array.isArray(value) ? value : value.value; - const obj: any = {[name]: value}; + const writeOption = ({ value, name }: { value: any; name: any }) => { + value = Array.isArray(value) ? value : value.value; + const obj: any = { [name]: value }; - if (name === 'metricType') { - switch (value) { - case TIMESERIES: - obj.metricOf = timeseriesOptions[0].value; - break; - case TABLE: - obj.metricOf = tableOptions[0].value; - break; - } - } - - metricStore.merge(obj); - }; - - const onSave = async () => { - const wasCreating = !metric.exists(); - if (isClickmap) { - try { - metric.thumbnail = await renderClickmapThumbnail(); - } catch (e) { - console.error(e); - } - } - const savedMetric = await metricStore.save(metric); - setInitialInstance(metric.toJson()); - if (wasCreating) { - if (parseInt(dashboardId, 10) > 0) { - history.replace( - withSiteId(dashboardMetricDetails(dashboardId, savedMetric.metricId), siteId) - ); - void dashboardStore.addWidgetToDashboard( - dashboardStore.getDashboard(parseInt(dashboardId, 10))!, - [savedMetric.metricId] - ); - } else { - history.replace(withSiteId(metricDetails(savedMetric.metricId), siteId)); - } - } - }; - - const onDelete = async () => { - if ( - await confirm({ - header: 'Confirm', - confirmButton: 'Yes, delete', - confirmation: `Are you sure you want to permanently delete this card?` - }) - ) { - metricStore.delete(metric).then(props.onDelete); - } - }; - - const undoChanges = () => { - const w = new Widget(); - metricStore.merge(w.fromJson(initialInstance), false); - }; - - const fetchResults = () => { - aiFiltersStore.getCardFilters(aiQuery, metric.metricType) - .then((f) => { - metric.createSeries(f.filters); - }) - }; - - const fetchChartData = () => { - void aiFiltersStore.getCardData(aiAskChart, metric.toJson()) + if (name === 'metricType') { + switch (value) { + case TIMESERIES: + obj.metricOf = timeseriesOptions[0].value; + break; + case TABLE: + obj.metricOf = tableOptions[0].value; + break; + } } - const handleKeyDown = (event: any) => { - if (event.key === 'Enter') { - fetchResults(); - } - }; - const handleChartKeyDown = (event: any) => { - if (event.key === 'Enter') { - fetchChartData(); - } - }; + metricStore.merge(obj); + }; - const testingKey = localStorage.getItem('__mauricio_testing_access') === 'true'; - return ( -
    - {/* + const onSave = async () => { + const wasCreating = !metric.exists(); + if (isClickmap) { + try { + metric.thumbnail = await renderClickmapThumbnail(); + } catch (e) { + console.error(e); + } + } + const savedMetric = await metricStore.save(metric); + setInitialInstance(metric.toJson()); + if (wasCreating) { + if (parseInt(dashboardId, 10) > 0) { + history.replace( + withSiteId( + dashboardMetricDetails(dashboardId, savedMetric.metricId), + siteId, + ), + ); + void dashboardStore.addWidgetToDashboard( + dashboardStore.getDashboard(parseInt(dashboardId, 10))!, + [savedMetric.metricId], + ); + } else { + history.replace( + withSiteId(metricDetails(savedMetric.metricId), siteId), + ); + } + } + }; + + const onDelete = async () => { + if ( + await confirm({ + header: t('Confirm'), + confirmButton: t('Yes, delete'), + confirmation: t( + 'Are you sure you want to permanently delete this card?', + ), + }) + ) { + metricStore.delete(metric).then(props.onDelete); + } + }; + + const undoChanges = () => { + const w = new Widget(); + metricStore.merge(w.fromJson(initialInstance), false); + }; + + const fetchResults = () => { + aiFiltersStore.getCardFilters(aiQuery, metric.metricType).then((f) => { + metric.createSeries(f.filters); + }); + }; + + const fetchChartData = () => { + void aiFiltersStore.getCardData(aiAskChart, metric.toJson()); + }; + + const handleKeyDown = (event: any) => { + if (event.key === 'Enter') { + fetchResults(); + } + }; + const handleChartKeyDown = (event: any) => { + if (event.key === 'Enter') { + fetchChartData(); + } + }; + + const testingKey = + localStorage.getItem('__mauricio_testing_access') === 'true'; + return ( +
    + {/*
    Card showing @@ -237,118 +248,169 @@ function WidgetForm(props: Props) { */} + {isPathAnalysis && ( +
    + {metric.startType === 'start' ? 'Start Point' : 'End Point'} - {isPathAnalysis && ( -
    - {metric.startType === 'start' ? 'Start Point' : 'End Point'} - - { - metric.updateStartPoint(val); - }} onRemoveFilter={() => { - }}/> -
    - )} - - {isPredefined && ( - - )} - {testingKey ? setAiQuery(e.target.value)} - className="w-full mb-2" - onKeyDown={handleKeyDown} - /> : null} - {testingKey ? setAiAskChart(e.target.value)} - className="w-full mb-2" - onKeyDown={handleChartKeyDown} - /> : null} - {aiFiltersStore.isLoading ? ( -
    -
    - Loading -
    -
    - ) : null} - {!isPredefined && ( -
    -
    - {`${isTable || isFunnel || isClickmap || isInsights || isPathAnalysis || isRetention ? 'Filter by' : 'Chart Series'}`} - {!isTable && !isFunnel && !isClickmap && !isInsights && !isPathAnalysis && !isRetention && ( - - )} -
    - - {metric.series.length > 0 && - metric.series - .slice(0, isTable || isFunnel || isClickmap || isInsights || isRetention ? 1 : metric.series.length) - .map((series: any, index: number) => ( -
    - metric.updateKey('hasChanged', true)} - hideHeader={isTable || isClickmap || isInsights || isPathAnalysis || isFunnel} - seriesIndex={index} - series={series} - onRemoveSeries={() => metric.removeSeries(index)} - canDelete={metric.series.length > 1} - emptyMessage={ - isTable - ? 'Filter data using any event or attribute. Use Add Step button below to do so.' - : 'Add an event or filter step to define the series.' - } - /> -
    - ))} -
    - )} - -
    - -
    - - {metric.exists() && metric.hasChanged && ( - - )} -
    -
    -
    - {metric.exists() && ( - - )} -
    -
    + { + metric.updateStartPoint(val); + }} + onRemoveFilter={() => {}} + />
    - ); + )} + + {isPredefined && ( + + )} + {testingKey ? ( + setAiQuery(e.target.value)} + className="w-full mb-2" + onKeyDown={handleKeyDown} + /> + ) : null} + {testingKey ? ( + setAiAskChart(e.target.value)} + className="w-full mb-2" + onKeyDown={handleChartKeyDown} + /> + ) : null} + {aiFiltersStore.isLoading ? ( +
    +
    + {t('Loading')} +
    +
    + ) : null} + {!isPredefined && ( +
    +
    + {`${isTable || isFunnel || isClickmap || isInsights || isPathAnalysis || isRetention ? t('Filter by') : t('Chart Series')}`} + {!isTable && + !isFunnel && + !isClickmap && + !isInsights && + !isPathAnalysis && + !isRetention && ( + + )} +
    + + {metric.series.length > 0 && + metric.series + .slice( + 0, + isTable || isFunnel || isClickmap || isInsights || isRetention + ? 1 + : metric.series.length, + ) + .map((series: any, index: number) => ( +
    + metric.updateKey('hasChanged', true)} + hideHeader={ + isTable || + isClickmap || + isInsights || + isPathAnalysis || + isFunnel + } + seriesIndex={index} + series={series} + onRemoveSeries={() => metric.removeSeries(index)} + canDelete={metric.series.length > 1} + emptyMessage={ + isTable + ? t( + 'Filter data using any event or attribute. Use Add Step button below to do so.', + ) + : t('Add an event or filter step to define the series.') + } + /> +
    + ))} +
    + )} + +
    + +
    + + {metric.exists() && metric.hasChanged && ( + + )} +
    +
    +
    + {metric.exists() && ( + + )} +
    +
    +
    + ); } export default observer(WidgetForm); diff --git a/frontend/app/components/Dashboard/components/WidgetForm/WidgetFormNew.tsx b/frontend/app/components/Dashboard/components/WidgetForm/WidgetFormNew.tsx index ebf2acd7e..039c2970a 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/WidgetFormNew.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/WidgetFormNew.tsx @@ -17,6 +17,7 @@ import { PlusIcon, ChevronUp } from 'lucide-react'; import { observer } from 'mobx-react-lite'; import FilterItem from 'Shared/Filters/FilterItem'; import { FilterKey, FilterCategory } from 'Types/filter/filterType'; +import { useTranslation } from 'react-i18next'; const getExcludedKeys = (metricType: string) => { switch (metricType) { @@ -26,17 +27,17 @@ const getExcludedKeys = (metricType: string) => { default: return []; } -} +}; const getExcludedCategories = (metricType: string) => { switch (metricType) { case USER_PATH: case FUNNEL: - return [FilterCategory.DEVTOOLS] + return [FilterCategory.DEVTOOLS]; default: return []; } -} +}; function WidgetFormNew({ layout }: { layout: string }) { const { metricStore } = useStore(); @@ -63,148 +64,162 @@ function WidgetFormNew({ layout }: { layout: string }) { export default observer(WidgetFormNew); -const FilterSection = observer(({ layout, metric, excludeFilterKeys, excludeCategory }: any) => { - const allOpen = layout.startsWith('flex-row'); - const defaultClosed = React.useRef(!allOpen && metric.exists()); - const [seriesCollapseState, setSeriesCollapseState] = React.useState>({}); +const FilterSection = observer( + ({ layout, metric, excludeFilterKeys, excludeCategory }: any) => { + const { t } = useTranslation(); + const allOpen = layout.startsWith('flex-row'); + const defaultClosed = React.useRef(!allOpen && metric.exists()); + const [seriesCollapseState, setSeriesCollapseState] = React.useState< + Record + >({}); - React.useEffect(() => { - const defaultSeriesCollapseState: Record = {}; - metric.series.forEach((s: any) => { - defaultSeriesCollapseState[s.seriesId] = isTable ? false : (allOpen ? false : defaultClosed.current); - }); - setSeriesCollapseState(defaultSeriesCollapseState); - }, [metric.series]); - const isTable = metric.metricType === TABLE; - const isHeatMap = metric.metricType === HEATMAP; - const isFunnel = metric.metricType === FUNNEL; - const isInsights = metric.metricType === INSIGHTS; - const isPathAnalysis = metric.metricType === USER_PATH; - const isRetention = metric.metricType === RETENTION; - const canAddSeries = metric.series.length < 3; - - const isSingleSeries = - isTable || - isFunnel || - isHeatMap || - isInsights || - isRetention || - isPathAnalysis; - - const collapseAll = () => { - setSeriesCollapseState((seriesCollapseState) => { - const newState = { ...seriesCollapseState }; - Object.keys(newState).forEach((key) => { - newState[key] = true; + React.useEffect(() => { + const defaultSeriesCollapseState: Record = {}; + metric.series.forEach((s: any) => { + defaultSeriesCollapseState[s.seriesId] = isTable + ? false + : allOpen + ? false + : defaultClosed.current; }); - return newState; - }); - } - const expandAll = () => { - setSeriesCollapseState((seriesCollapseState) => { - const newState = { ...seriesCollapseState }; - Object.keys(newState).forEach((key) => { - newState[key] = false; - }); - return newState; - }); - } + setSeriesCollapseState(defaultSeriesCollapseState); + }, [metric.series]); + const isTable = metric.metricType === TABLE; + const isHeatMap = metric.metricType === HEATMAP; + const isFunnel = metric.metricType === FUNNEL; + const isInsights = metric.metricType === INSIGHTS; + const isPathAnalysis = metric.metricType === USER_PATH; + const isRetention = metric.metricType === RETENTION; + const canAddSeries = metric.series.length < 3; - const allCollapsed = Object.values(seriesCollapseState).every((v) => v); - return ( - <> - {metric.series.length > 0 && - metric.series - .slice(0, isSingleSeries ? 1 : metric.series.length) - .map((series: any, index: number) => ( -
    - metric.updateKey('hasChanged', true)} - hideHeader={ - isTable || - isHeatMap || - isInsights || - isPathAnalysis || - isFunnel - } - seriesIndex={index} - series={series} - onRemoveSeries={() => metric.removeSeries(index)} - canDelete={metric.series.length > 1} - collapseState={seriesCollapseState[series.seriesId]} - onToggleCollapse={() => { - setSeriesCollapseState((seriesCollapseState) => ({ - ...seriesCollapseState, - [series.seriesId]: !seriesCollapseState[series.seriesId], - })); - }} - emptyMessage={ - isTable - ? 'Filter data using any event or attribute. Use Add Step button below to do so.' - : 'Add an event or filter step to define the series.' - } - expandable={isSingleSeries} - /> -
    - ))} - {isSingleSeries ? null : -
    - - + + - - -
    - } - - ); -}); +
    + )} + + ); + }, +); const PathAnalysisFilter = observer(({ metric, writeOption }: any) => { + const { t } = useTranslation(); const metricValueOptions = [ - { value: 'location', label: 'Pages' }, - { value: 'click', label: 'Clicks' }, - { value: 'input', label: 'Input' }, - { value: 'custom', label: 'Custom Events' }, + { value: 'location', label: t('Pages') }, + { value: 'click', label: t('Clicks') }, + { value: 'input', label: t('Input') }, + { value: 'custom', label: t('Custom Events') }, ]; const onPointChange = (value: any) => { - writeOption({ name: 'startType', value: { value } }) - } + writeOption({ name: 'startType', value: { value } }); + }; return (
    - Journeys With + {t('Journeys With')}
    { options={metricValueOptions} value={metric.metricValue || []} onChange={(value) => writeOption({ name: 'metricValue', value })} - placeholder="Select Metrics" + placeholder={t('Select Metrics')} maxTagCount={'responsive'} showSearch={false} />
    -
    -
    - + +
    - { - metric.startType === 'start' - ? 'Start Point' - : 'End Point' - } - - metric.updateStartPoint(val)} - onRemoveFilter={() => {}} - /> - + + {metric.startType === 'start' ? 'Start Point' : 'End Point'} + + + metric.updateStartPoint(val)} + onRemoveFilter={() => {}} + /> +
    @@ -264,6 +276,7 @@ const PathAnalysisFilter = observer(({ metric, writeOption }: any) => { }); const InsightsFilter = observer(({ metric, writeOption }: any) => { + const { t } = useTranslation(); return ( @@ -274,7 +287,7 @@ const InsightsFilter = observer(({ metric, writeOption }: any) => { value={metric.metricValue} onChange={writeOption} isMulti - placeholder="All Categories" + placeholder={t('All Categories')} allowClear /> @@ -305,12 +318,15 @@ const AdditionalFilters = observer(() => { ); }); -const PredefinedMessage = () => ( - -); +function PredefinedMessage() { + const { t } = useTranslation(); + return ( + + ); +} diff --git a/frontend/app/components/Dashboard/components/WidgetForm/components/MetricSubtypeDropdown/MetricSubtypeDropdown.tsx b/frontend/app/components/Dashboard/components/WidgetForm/components/MetricSubtypeDropdown/MetricSubtypeDropdown.tsx index 37e2b46ad..9d197aae8 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/components/MetricSubtypeDropdown/MetricSubtypeDropdown.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/components/MetricSubtypeDropdown/MetricSubtypeDropdown.tsx @@ -6,13 +6,15 @@ import React from 'react'; import Select from 'Shared/Select'; import { components } from 'react-select'; import CustomDropdownOption from 'Shared/CustomDropdownOption'; +import { useTranslation } from 'react-i18next'; interface Props { onSelect: any; } function MetricSubtypeDropdown(props: Props) { + const { t } = useTranslation(); const { metricStore } = useStore(); - const metric: any = metricStore.instance; + const metric: any = metricStore.instance; const options: any = React.useMemo(() => { const type = TYPES.find((i: MetricType) => i.slug === metric.metricType); @@ -30,14 +32,21 @@ function MetricSubtypeDropdown(props: Props) { React.useEffect(() => { // @ts-ignore - if (options && !options.map(i => i.value).includes(metric.metricOf)) { - setTimeout(() => props.onSelect({ name: 'metricOf', value: { value: options[0].value }}), 0) + if (options && !options.map((i) => i.value).includes(metric.metricOf)) { + setTimeout( + () => + props.onSelect({ + name: 'metricOf', + value: { value: options[0].value }, + }), + 0, + ); } - }, [metric.metricType]) + }, [metric.metricType]); return options ? ( <> -
    of
    +
    {t('of')}
    +
    {featureFlagsStore.sort.query === '' - ? 'You haven\'t created any feature flags yet' - : 'No matching results'} + ? t("You haven't created any feature flags yet") + : t('No matching results')}
    } subtext={ featureFlagsStore.sort.query === '' ? (
    - Use feature flags to deploy and rollback new functionality with ease. + {t( + 'Use feature flags to deploy and rollback new functionality with ease.', + )}
    ) : null } >
    -
    -
    Key
    -
    Last modified
    -
    By
    -
    Status
    +
    +
    {t('Key')}
    +
    {t('Last modified')}
    +
    {t('By')}
    +
    + {t('Status')} +
    {featureFlagsStore.flags.map((flag) => ( @@ -95,17 +101,21 @@ function FFlagsList({ siteId }: { siteId: string }) {
    - Showing{' '} + {t('Showing')}{' '} - {(featureFlagsStore.page - 1) * featureFlagsStore.pageSize + 1} + {(featureFlagsStore.page - 1) * featureFlagsStore.pageSize + + 1} {' '} - to{' '} + {t('to')}{' '} {(featureFlagsStore.page - 1) * featureFlagsStore.pageSize + featureFlagsStore.flags.length} {' '} - of {numberWithCommas(featureFlagsStore.total)}{' '} - Feature Flags. + {t('of')}{' '} + + {numberWithCommas(featureFlagsStore.total)} + {' '} + {t('Feature Flags.')}
    - +
    - -
    +
    - ) + ); } -export default FFlagsListHeader; \ No newline at end of file +export default FFlagsListHeader; diff --git a/frontend/app/components/FFlags/FFlagsSearch.tsx b/frontend/app/components/FFlags/FFlagsSearch.tsx index 7d87708e4..c86a0bcb0 100644 --- a/frontend/app/components/FFlags/FFlagsSearch.tsx +++ b/frontend/app/components/FFlags/FFlagsSearch.tsx @@ -11,24 +11,30 @@ function FFlagsSearch() { const [query, setQuery] = useState(featureFlagsStore.sort.query); useEffect(() => { - debounceUpdate = debounce( - (value: string) => { - featureFlagsStore.setSort({ order: featureFlagsStore.sort.order, query: value }) - featureFlagsStore.setPage(1) - void featureFlagsStore.fetchFlags() - }, - 250 - ); + debounceUpdate = debounce((value: string) => { + featureFlagsStore.setSort({ + order: featureFlagsStore.sort.order, + query: value, + }); + featureFlagsStore.setPage(1); + void featureFlagsStore.fetchFlags(); + }, 250); }, []); - const write = ({ target: { value } }: React.ChangeEvent) => { + const write = ({ + target: { value }, + }: React.ChangeEvent) => { setQuery(value.replace(/\s/g, '-')); debounceUpdate(value.replace(/\s/g, '-')); }; return (
    - + ; - if (!current) return ; + if (featureFlagsStore.isLoading) return ; + if (!current) return ; const deleteHandler = () => { featureFlagsStore.deleteFlag(current.featureFlagId).then(() => { - toast.success('Feature flag deleted.'); + toast.success(t('Feature flag deleted.')); history.push(withSiteId(fflags(), siteId)); }); }; - const menuItems = [{ icon: 'trash', text: 'Delete', onClick: deleteHandler }]; + const menuItems = [ + { icon: 'trash', text: t('Delete'), onClick: deleteHandler }, + ]; const toggleActivity = () => { const newValue = !current.isActive; @@ -39,68 +43,70 @@ function FlagView({ siteId, fflagId }: { siteId: string; fflagId: string }) { featureFlagsStore .updateFlagStatus(current.featureFlagId, newValue) .then(() => { - toast.success('Feature flag status has been updated.'); + toast.success(t('Feature flag status has been updated.')); }) .catch(() => { current.setIsEnabled(!newValue); - toast.error('Something went wrong, please try again.'); + toast.error(t('Something went wrong, please try again.')); }); }; return ( -
    +
    -
    -
    -
    {current.flagKey}
    +
    +
    +
    {current.flagKey}
    -
    +
    {current.description || 'There is no description for this feature flag.'}
    -
    - -
    +
    + +
    -
    {current.isActive ? 'Enabled' : 'Disabled'}
    +
    {current.isActive ? t('Enabled') : t('Disabled')}
    -
    - +
    +
    {current.isPersist - ? 'This flag maintains its state through successive authentication events.' - : 'This flag is not persistent across authentication events.'} + ? t( + 'This flag maintains its state through successive authentication events.', + ) + : t('This flag is not persistent across authentication events.')}
    {!current.isSingleOption ? : null} {current.conditions.length > 0 ? (
    - + {current.conditions.map((condition, index) => ( -
    +
    ))}
    diff --git a/frontend/app/components/FFlags/NewFFlag/Description.tsx b/frontend/app/components/FFlags/NewFFlag/Description.tsx index 41073dd84..85cee5ef3 100644 --- a/frontend/app/components/FFlags/NewFFlag/Description.tsx +++ b/frontend/app/components/FFlags/NewFFlag/Description.tsx @@ -4,6 +4,7 @@ import { Icon } from 'UI'; import { Button } from 'antd'; import cn from 'classnames'; import FeatureFlag from 'App/mstore/types/FeatureFlag'; +import { useTranslation } from 'react-i18next'; function Description({ isDescrEditing, @@ -16,15 +17,17 @@ function Description({ current: FeatureFlag; setEditing: ({ isDescrEditing }: { isDescrEditing: boolean }) => void; }) { + const { t } = useTranslation(); return ( <> {isDescrEditing ? (