From 2b1a9f3378d6f096bc138f493c5b13d06f496d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=91=D0=B0=D0=B1?= =?UTF-8?q?=D1=83=D1=88=D0=BA=D0=B8=D0=BD?= Date: Wed, 5 Mar 2025 16:09:18 +0100 Subject: [PATCH] add locales and lint the project --- frontend/.prettierrc | 6 +- frontend/app/AdditionalRoutes.tsx | 4 +- frontend/app/ErrorBoundary.tsx | 31 + frontend/app/PrivateRoutes.tsx | 6 +- frontend/app/PublicRoutes.tsx | 16 +- frontend/app/Router.tsx | 36 +- frontend/app/api_client.ts | 68 +- frontend/app/components/Alerts/AlertForm.js | 397 --- frontend/app/components/Alerts/AlertForm.tsx | 462 +++ .../Alerts/AlertFormModal/AlertFormModal.tsx | 10 +- .../Alerts/DropdownChips/DropdownChips.js | 15 +- frontend/app/components/Assist/Assist.tsx | 11 +- .../AssistSearchActions.tsx | 26 +- .../Assist/ChatControls/ChatControls.tsx | 71 +- .../Assist/ChatWindow/ChatWindow.tsx | 46 +- .../RecordingsList/EditRecordingModal.tsx | 40 +- .../Assist/RecordingsList/Recordings.tsx | 11 +- .../Assist/RecordingsList/RecordingsList.tsx | 35 +- .../RecordingsList/RecordingsSearch.tsx | 11 +- .../Assist/RecordingsList/RecordsListItem.tsx | 29 +- .../RequestingWindow/RequestingWindow.tsx | 43 +- .../AssistActions/AssistActions.tsx | 111 +- .../components/SessionList/SessionList.tsx | 49 +- .../VideoContainer/VideoContainer.tsx | 12 +- .../components/AssistStats/AssistStats.tsx | 100 +- .../AssistStats/components/Charts.tsx | 14 +- .../AssistStats/components/Table.tsx | 152 +- .../AssistStats/components/TeamMembers.tsx | 74 +- .../AssistStats/components/UserSearch.tsx | 10 +- .../components/AssistStats/pdfGenerator.ts | 9 +- frontend/app/components/Charts/BarChart.tsx | 31 +- .../app/components/Charts/ColumnChart.tsx | 6 +- frontend/app/components/Charts/LineChart.tsx | 31 +- frontend/app/components/Charts/PieChart.tsx | 21 +- .../app/components/Charts/SankeyChart.tsx | 91 +- frontend/app/components/Charts/barUtils.ts | 14 +- frontend/app/components/Charts/init.ts | 15 +- frontend/app/components/Charts/pieUtils.ts | 4 +- frontend/app/components/Charts/sankeyUtils.ts | 17 +- frontend/app/components/Charts/utils.ts | 26 +- .../AuditDetailModal/AuditDetailModal.tsx | 35 +- .../Client/Audit/AuditList/AuditList.tsx | 25 +- .../Audit/AuditListItem/AuditListItem.tsx | 16 +- .../AuditSearchField/AuditSearchField.tsx | 10 +- .../Client/Audit/AuditView/AuditView.tsx | 50 +- frontend/app/components/Client/Client.tsx | 77 +- .../Client/CustomFields/CustomFieldForm.tsx | 56 +- .../Client/CustomFields/CustomFields.tsx | 53 +- .../Client/CustomFields/ListItem.js | 5 +- frontend/app/components/Client/DebugLog.tsx | 8 +- .../Backend/DatadogForm/DatadogFormModal.tsx | 47 +- .../DynatraceForm/DynatraceFormModal.tsx | 68 +- .../Backend/ElasticForm/ElasticFormModal.tsx | 53 +- .../Backend/SentryForm/SentryFormModal.tsx | 55 +- .../Client/Integrations/FormField.tsx | 2 +- .../{GithubForm.js => GithubForm.tsx} | 23 +- .../Client/Integrations/IntegrationForm.tsx | 49 +- .../Client/Integrations/IntegrationItem.tsx | 39 +- .../Integrations/IntegrationModalCard.tsx | 14 +- .../Client/Integrations/Integrations.tsx | 169 +- .../JiraForm/{JiraForm.js => JiraForm.tsx} | 27 +- .../{ProfilerDoc.js => ProfilerDoc.tsx} | 30 +- .../{SlackAddForm.js => SlackAddForm.tsx} | 33 +- ...ackChannelList.js => SlackChannelList.tsx} | 18 +- .../Client/Integrations/SlackForm.tsx | 16 +- .../Integrations/Teams/TeamsAddForm.tsx | 28 +- .../Integrations/Teams/TeamsChannelList.tsx | 11 +- .../Client/Integrations/Teams/index.tsx | 16 +- .../AssistDoc/{AssistDoc.js => AssistDoc.tsx} | 34 +- .../Tracker/AssistDoc/AssistNpm.tsx | 10 +- .../Tracker/AssistDoc/AssistScript.tsx | 8 +- .../Tracker/GraphQLDoc/GraphQLDoc.js | 75 - .../Tracker/GraphQLDoc/GraphQLDoc.tsx | 91 + .../MobxDoc/{MobxDoc.js => MobxDoc.tsx} | 42 +- .../NgRxDoc/{NgRxDoc.js => NgRxDoc.tsx} | 59 +- .../Tracker/PiniaDoc/PiniaDoc.tsx | 24 +- .../ReduxDoc/{ReduxDoc.js => ReduxDoc.tsx} | 55 +- .../Integrations/Tracker/ReduxDoc/index.js | 2 +- .../Tracker/VueDoc/{VueDoc.js => VueDoc.tsx} | 44 +- .../{ZustandDoc.js => ZustandDoc.tsx} | 48 +- .../Client/Integrations/apiMethods.ts | 3 +- .../app/components/Client/Modules/Modules.tsx | 38 +- .../app/components/Client/Modules/index.ts | 52 +- .../Client/Notifications/Notifications.tsx | 18 +- .../ProfileSettings/{Api.js => Api.tsx} | 4 +- .../Client/ProfileSettings/ChangePassword.tsx | 75 +- .../{Licenses.js => Licenses.tsx} | 8 +- .../Client/ProfileSettings/OptOut.js | 16 +- ...ProfileSettings.js => ProfileSettings.tsx} | 44 +- .../{Settings.js => Settings.tsx} | 35 +- .../{TenantKey.js => TenantKey.tsx} | 15 +- .../Client/Projects/ProjectCaptureRate.tsx | 43 +- .../Client/Projects/ProjectCodeSnippet.tsx | 76 +- .../Client/Projects/ProjectForm.tsx | 76 +- .../Client/Projects/ProjectList.tsx | 47 +- .../Client/Projects/ProjectTabContent.tsx | 6 +- .../Client/Projects/ProjectTabTracking.tsx | 24 +- .../Client/Projects/ProjectTabs.tsx | 22 +- .../Client/Projects/ProjectTags.tsx | 33 +- .../components/Client/Projects/Projects.tsx | 42 +- .../components/Client/Projects/TagForm.tsx | 21 +- .../app/components/Client/Roles/Roles.tsx | 32 +- .../Roles/components/RoleForm/RoleForm.tsx | 66 +- .../Roles/components/RoleItem/RoleItem.tsx | 45 +- .../Client/SessionsListingSettings.tsx | 4 +- .../AddProjectButton/AddProjectButton.tsx | 21 +- .../Sites/{GDPRForm.js => GDPRForm.tsx} | 58 +- .../Sites/InstallButton/InstallButton.tsx | 13 +- .../components/Client/Sites/NewSiteForm.tsx | 55 +- .../Client/Sites/SiteSearch/SiteSearch.tsx | 12 +- .../app/components/Client/Sites/Sites.tsx | 90 +- frontend/app/components/Client/TabItem.js | 6 +- .../app/components/Client/Users/UsersView.tsx | 13 +- .../AddUserButton/AddUserButton.tsx | 24 +- .../Users/components/UserForm/UserForm.tsx | 62 +- .../Users/components/UserList/UserList.tsx | 67 +- .../components/UserListItem/UserListItem.tsx | 29 +- .../components/UserSearch/UserSearch.tsx | 13 +- .../Client/Webhooks/WebhookForm.tsx | 30 +- .../components/Client/Webhooks/Webhooks.tsx | 49 +- .../app/components/Dashboard/NewDashboard.tsx | 11 +- .../Dashboard/Widgets/CardSessionsByList.tsx | 37 +- .../CustomMetricsWidgets/AreaChart.tsx | 44 +- .../CustomMetricsWidgets/BigNumChart.tsx | 47 +- .../ClickMapCard/ClickMapCard.tsx | 22 +- .../CohortCard/CohortCard.tsx | 43 +- .../CustomChartTooltip.tsx | 37 +- .../CustomMetricsWidgets/CustomLegend.tsx | 13 +- .../CustomMetricOverviewChart.tsx | 24 +- .../CustomMetricPercentage.tsx | 17 +- .../CustomMetricPieChart.tsx | 21 +- .../CustomMetricTable/CustomMetricTable.tsx | 21 +- .../CustomMetricTableErrors.tsx | 65 +- .../CustomMetricTableSessions.tsx | 28 +- .../InsightsCard/InsightItem.tsx | 103 +- .../InsightsCard/InsightsCard.tsx | 33 +- .../CustomMetricsWidgets/SessionsBy.tsx | 84 +- .../Dashboard/Widgets/ListWithIcons.tsx | 8 +- .../PredefinedWidgets/CPULoad/CPULoad.tsx | 38 +- .../CallWithErrors/CallWithErrors.tsx | 24 +- .../PredefinedWidgets/CallWithErrors/Chart.js | 8 +- .../CallsErrors4xx/CallsErrors4xx.tsx | 52 +- .../CallsErrors5xx/CallsErrors5xx.tsx | 52 +- .../PredefinedWidgets/Crashes/Crashes.tsx | 38 +- .../DomBuildingTime/DomBuildingTime.tsx | 54 +- .../ErrorsByOrigin/ErrorsByOrigin.tsx | 57 +- .../ErrorsByType/ErrorsByType.tsx | 63 +- .../PredefinedWidgets/ErrorsPerDomain/Bar.tsx | 9 +- .../ErrorsPerDomain/ErrorsPerDomain.tsx | 8 +- .../Widgets/PredefinedWidgets/FPS/FPS.tsx | 38 +- .../MemoryConsumption/MemoryConsumption.tsx | 38 +- .../ResponseTime/ResponseTime.tsx | 59 +- .../ResponseTimeDistribution.tsx | 104 +- .../SessionsAffectedByJSErrors.tsx | 42 +- .../SessionsImpactedBySlowRequests.tsx | 38 +- .../SessionsPerBrowser/{Bar.js => Bar.tsx} | 28 +- .../SessionsPerBrowser/SessionsPerBrowser.tsx | 18 +- .../PredefinedWidgets/SlowestDomains/Bar.tsx | 13 +- .../SlowestDomains/SlowestDomains.tsx | 8 +- .../{Scale.js => Scale.tsx} | 13 +- .../SpeedIndexByLocation.tsx | 31 +- .../TimeToRender/TimeToRender.tsx | 53 +- .../Dashboard/Widgets/common/AvgLabel.js | 4 +- .../Dashboard/Widgets/common/CountBadge.js | 26 +- .../{CustomTooltip.js => CustomTooltip.tsx} | 12 +- .../Dashboard/Widgets/common/SessionLine.js | 24 +- .../Dashboard/Widgets/common/Styles.js | 46 +- .../Dashboard/Widgets/common/Table.js | 81 +- .../Dashboard/Widgets/common/Title.js | 5 +- .../components/AddCardModal/AddCardModal.tsx | 6 +- .../AddCardSection/AddCardSection.tsx | 129 +- .../components/AddCardSelectionModal.tsx | 18 +- .../components/AddToDashboardButton.tsx | 16 +- .../Alerts/AlertForm/BottomButtons.tsx | 38 +- .../components/Alerts/AlertForm/Condition.tsx | 83 +- .../Alerts/AlertForm/NotifyHooks.tsx | 32 +- .../components/Alerts/AlertListItem.tsx | 81 +- .../components/Alerts/AlertsList.tsx | 52 +- .../components/Alerts/AlertsSearch.tsx | 15 +- .../components/Alerts/AlertsView.tsx | 17 +- .../Alerts/DropdownChips/DropdownChips.js | 15 +- .../Dashboard/components/Alerts/NewAlert.tsx | 101 +- .../components/CardIssues/CardIssues.tsx | 71 +- .../components/CardUserList/CardUserItem.tsx | 13 +- .../components/CardUserList/CardUserList.tsx | 31 +- .../SessionsModal/SessionsModal.tsx | 71 +- .../ClickMapRagePicker/ClickMapRagePicker.tsx | 20 +- .../Dashboard/components/CreateCardButton.tsx | 6 +- .../components/CreateDashboardButton.tsx | 4 +- .../DashbaordListModal/DashbaordListModal.tsx | 25 +- .../DashboardEditModal/DashboardEditModal.tsx | 50 +- .../DashboardForm/DashboardForm.tsx | 33 +- .../DashboardHeader/DashboardHeader.tsx | 44 +- .../DashboardList/DashboardList.tsx | 107 +- .../DashboardList/DashboardSearch.tsx | 13 +- .../DashboardList/DashboardsView.tsx | 5 +- .../components/DashboardList/Header.tsx | 12 +- .../NewDashModal/CardsLibrary.tsx | 12 +- .../DashboardList/NewDashModal/CreateCard.tsx | 34 +- .../NewDashModal/ExampleCards.tsx | 160 +- .../NewDashModal/Examples/AreaChartCard.tsx | 42 +- .../NewDashModal/Examples/BarChart.tsx | 28 +- .../NewDashModal/Examples/Bars.tsx | 21 +- .../Examples/CallsWithErrorsExample.tsx | 18 +- .../NewDashModal/Examples/Count.tsx | 71 +- .../NewDashModal/Examples/ExCard.tsx | 15 +- .../NewDashModal/Examples/Funnel.tsx | 6 +- .../NewDashModal/Examples/HeatmapsExample.tsx | 14 +- .../NewDashModal/Examples/InsightsExample.tsx | 21 +- .../PageResponseTimeDistributionExample.tsx | 4 +- .../NewDashModal/Examples/Path.tsx | 4 +- .../NewDashModal/Examples/PerfBreakdown.tsx | 14 +- .../Examples/SessionsBy/ByBrowser.tsx | 8 +- .../Examples/SessionsBy/ByCountry.tsx | 8 +- .../Examples/SessionsBy/ByFecth.tsx | 8 +- .../Examples/SessionsBy/ByIssues.tsx | 8 +- .../Examples/SessionsBy/ByRferrer.tsx | 8 +- .../Examples/SessionsBy/BySystem.tsx | 8 +- .../Examples/SessionsBy/ByUrl.tsx | 8 +- .../Examples/SessionsBy/ByUser.tsx | 8 +- .../Examples/SessionsBy/Component.tsx | 50 +- .../Examples/SessionsBy/SlowestDomains.tsx | 8 +- .../Examples/SessionsByErrors.tsx | 4 +- .../Examples/SessionsByIssues.tsx | 4 +- .../Examples/SessionsPerBrowserExample.tsx | 4 +- .../NewDashModal/Examples/SlowestDomain.tsx | 12 +- .../Examples/SpeedIndexByLocationExample.tsx | 4 +- .../NewDashModal/Examples/TableOfErrors.tsx | 6 +- .../Examples/Tabs/CoreWebVitals.tsx | 2 +- .../Examples/Tabs/PerformanceMonitoring.tsx | 2 +- .../Examples/Tabs/ProductAnalytics.tsx | 2 +- .../Examples/Tabs/WebAnalytics.tsx | 2 +- .../NewDashModal/Examples/Trend.tsx | 18 +- .../NewDashModal/Examples/WebVital.tsx | 12 +- .../NewDashModal/NewDashboardModal.tsx | 30 +- .../DashboardList/NewDashModal/Option.tsx | 4 +- .../DashboardList/NewDashModal/SelectCard.tsx | 118 +- .../DashboardMetricSelection.tsx | 147 +- .../DashboardModal/DashboardModal.tsx | 68 +- .../DashboardOptions/DashboardOptions.tsx | 16 +- .../DashboardRouter/DashboardRouter.tsx | 26 +- .../DashboardSideMenu/DashboardSideMenu.tsx | 4 +- .../components/DashboardView/AiQuery.tsx | 15 +- .../DashboardView/DashboardView.tsx | 23 +- .../DashboardWidgetGrid/AddMetric.tsx | 40 +- .../AddMetricContainer.tsx | 56 +- .../AddPredefinedMetric.tsx | 66 +- .../DashboardWidgetGrid.tsx | 19 +- .../ErrorDetailsModal/ErrorDetailsModal.tsx | 4 +- .../Errors/ErrorLabel/ErrorLabel.tsx | 31 +- .../Errors/ErrorListItem/ErrorListItem.tsx | 59 +- .../components/Errors/ErrorName/ErrorName.tsx | 23 +- .../components/FilterSeries/AddStepButton.tsx | 15 +- .../FilterSeries/ExcludeFilters.tsx | 8 +- .../components/FilterSeries/FilterSeries.tsx | 54 +- .../FunnelIssueDetails/FunnelIssueDetails.tsx | 48 +- .../FunnelIssueGraph/FunnelIssueGraph.tsx | 15 +- .../FunnelIssueModal/FunnelIssueModal.tsx | 2 +- .../Funnels/FunnelIssues/FunnelIssues.tsx | 51 +- .../FunnelIssuesDropdown.tsx | 49 +- .../FunnelIssuesList/FunnelIssuesList.tsx | 64 +- .../FunnelIssuesListItem.tsx | 80 +- .../FunnelIssuesSelectedFilters.tsx | 2 +- .../FunnelIssuesSort/FunnelIssuesSort.tsx | 2 +- .../MetricListItem/MetricListItem.tsx | 16 +- .../MetricTypeItem/MetricTypeItem.tsx | 21 +- .../MetricTypeList/MetricTypeList.tsx | 52 +- .../components/MetricTypeSelector.tsx | 30 +- .../MetricViewHeader/MetricViewHeader.tsx | 35 +- .../components/MetricsGrid/MetricsGrid.tsx | 8 +- .../MetricsLibraryModal/FooterContent.tsx | 39 +- .../MetricsLibraryModal.tsx | 15 +- .../components/MetricsList/GridView.tsx | 4 +- .../components/MetricsList/ListView.tsx | 151 +- .../components/MetricsList/MetricsList.tsx | 105 +- .../MetricsSearch/MetricsSearch.tsx | 6 +- .../components/MetricsView/MetricsView.tsx | 5 +- .../SessionsModal/SessionsModal.tsx | 36 +- .../components/WidgetChart/LongLoader.tsx | 19 +- .../components/WidgetChart/WidgetChart.tsx | 112 +- .../WidgetDatatable/WidgetDatatable.tsx | 24 +- .../WidgetDateRange/RangeGranularity.tsx | 21 +- .../WidgetDateRange/WidgetDateRange.tsx | 46 +- .../components/WidgetForm/CardBuilder.tsx | 347 +- .../components/WidgetForm/WidgetForm.tsx | 193 +- .../components/WidgetForm/WidgetFormNew.tsx | 285 +- .../MetricSubtypeDropdown.tsx | 17 +- .../MetricTypeDropdown/MetricTypeDropdown.tsx | 24 +- .../components/WidgetForm/renderMap.ts | 37 +- .../components/WidgetName/WidgetName.tsx | 4 +- .../Dashboard/components/WidgetOptions.tsx | 39 +- .../WidgetPredefinedChart.tsx | 24 +- .../WidgetPreview/WidgetPreview.tsx | 21 +- .../WidgetSessions/WidgetSessions.tsx | 232 +- .../WidgetSubDetailsView.tsx | 22 +- .../components/WidgetView/CardViewMenu.tsx | 15 +- .../components/WidgetView/WidgetView.tsx | 63 +- .../WidgetView/WidgetViewHeader.tsx | 23 +- .../components/WidgetWrapper/AlertButton.tsx | 12 +- .../components/WidgetWrapper/CardMenu.tsx | 14 +- .../WidgetWrapper/TemplateOverlay.tsx | 11 +- .../WidgetWrapper/WidgetWrapper.tsx | 11 +- .../WidgetWrapper/WidgetWrapperNew.tsx | 51 +- .../app/components/Errors/Error/DateAgo.js | 6 +- .../Errors/Error/DistributionBar.js | 9 +- .../Error/{ErrorInfo.js => ErrorInfo.tsx} | 10 +- .../app/components/Errors/Error/IconCard.js | 19 +- .../Error/{MainSection.js => MainSection.tsx} | 26 +- .../app/components/Errors/Error/SessionBar.js | 12 +- .../Error/{SideSection.js => SideSection.tsx} | 22 +- frontend/app/components/Errors/Error/Trend.js | 49 +- .../app/components/Errors/ui/ErrorName.js | 23 +- frontend/app/components/Errors/ui/Label.js | 34 +- .../components/FFlags/FFlagItem/FFlagItem.tsx | 20 +- frontend/app/components/FFlags/FFlagsList.tsx | 61 +- .../components/FFlags/FFlagsListHeader.tsx | 9 +- .../app/components/FFlags/FFlagsSearch.tsx | 26 +- .../components/FFlags/FlagView/FlagView.tsx | 52 +- .../FFlags/NewFFlag/Description.tsx | 11 +- .../app/components/FFlags/NewFFlag/Header.tsx | 18 +- .../components/FFlags/NewFFlag/Helpers.tsx | 24 +- .../app/components/FFlags/NewFFlag/HowTo.tsx | 9 +- .../FFlags/NewFFlag/Multivariant.tsx | 73 +- .../components/FFlags/NewFFlag/NewFFlag.tsx | 125 +- .../ForgotPassword/CreatePassword.tsx | 49 +- .../ForgotPassword/ForgotPassword.tsx | 19 +- .../components/ForgotPassword/ReCaptcha.js | 20 +- .../ForgotPassword/ResetPasswordRequest.tsx | 58 +- .../FunnelGraphSmall/FunnelGraphSmall.js | 8 +- .../Funnels/FunnelItem/FunnelItem.tsx | 12 +- .../Funnels/FunnelMenuItem/FunnelMenuItem.js | 24 +- .../Funnels/FunnelOverview/FunnelOverview.js | 43 - .../Funnels/FunnelOverview/FunnelOverview.jsx | 48 + .../Funnels/FunnelSearch/FunnelSearch.tsx | 11 +- .../FunnelSessionsHeader.js | 14 +- .../Funnels/FunnelWidget/FunnelBar.tsx | 47 +- .../Funnels/FunnelWidget/FunnelStepText.tsx | 25 +- .../Funnels/FunnelWidget/FunnelTable.tsx | 35 +- .../Funnels/FunnelWidget/FunnelWidget.tsx | 144 +- .../Funnels/IssueItem/IssueGraph.js | 25 +- .../components/Funnels/IssueItem/IssueItem.js | 59 +- frontend/app/components/Header/AlertItem.js | 4 +- .../DefaultMenuView/DefaultMenuView.tsx | 31 +- .../components/Header/Discover/FeatureItem.js | 12 +- .../HealthStatus/HealthModal/Footer.tsx | 19 +- .../HealthStatus/HealthModal/HealthModal.tsx | 40 +- .../Header/HealthStatus/HealthStatus.tsx | 21 +- .../Header/HealthStatus/HealthWidget.tsx | 65 +- .../Header/HealthStatus/ServiceCategory.tsx | 22 +- .../SubserviceHealth/SubserviceHealth.tsx | 20 +- .../Header/HealthStatus/getHealth.ts | 14 +- .../NewProjectButton/NewProjectButton.tsx | 4 +- .../app/components/Header/NotificationItem.js | 20 +- .../Header/OnboardingExplore/FeatureItem.js | 17 +- .../PreferencesView/PreferencesView.tsx | 13 +- .../Header/SettingsMenu/SettingsMenu.tsx | 48 +- .../components/Header/UserMenu/UserMenu.tsx | 22 +- .../app/components/Highlights/EditHlModal.tsx | 10 +- .../components/Highlights/HighlightsList.tsx | 34 +- .../Highlights/HighlightsListHeader.tsx | 18 +- .../LanguageSwitcher/LanguageSwitcher.tsx | 20 +- frontend/app/components/Login/Login.tsx | 65 +- frontend/app/components/Modal/Modal.tsx | 47 +- .../app/components/Modal/ModalOverlay.tsx | 20 +- frontend/app/components/Modal/index.tsx | 10 +- frontend/app/components/Modal/withModal.tsx | 9 +- frontend/app/components/ModalContext.tsx | 25 +- .../app/components/Onboarding/Onboarding.tsx | 12 +- .../components/CircleNumber/CircleNumber.js | 4 +- .../IdentifyUsersTab/IdentifyUsersTab.tsx | 113 +- .../InstallOpenReplayTab.tsx | 64 +- .../IntegrationsTab/IntegrationsTab.tsx | 9 +- .../ManageUsersTab/ManageUsersTab.tsx | 36 +- .../components/MetadataList/MetadataList.js | 66 - .../components/MetadataList/MetadataList.tsx | 20 +- .../OnboardingMenu/OnboardingMenu.js | 4 +- .../OnboardingNavButton.js | 28 +- .../OnboardingTabs/CopyButton/CopyButton.js | 7 +- .../InstallDocs/AndroidInstallDocs.tsx | 20 +- .../OnboardingTabs/InstallDocs/InstallDocs.js | 58 +- .../InstallDocs/MobileInstallDocs.tsx | 26 +- .../OnboardingTabs/OnboardingMobileTabs.tsx | 16 +- .../OnboardingTabs/OnboardingTabs.tsx | 19 +- .../ProjectCodeSnippet/ProjectCodeSnippet.js | 59 +- .../OnboardingTabs/SegmentTab/SegmentTab.js | 33 +- .../Onboarding/components/SideMenu.tsx | 32 +- .../Onboarding/components/withOnboarding.tsx | 17 +- frontend/app/components/Overview/Overview.tsx | 24 +- .../app/components/ScopeForm/ScopeForm.tsx | 24 +- .../app/components/Session/ClipsPlayer.tsx | 34 +- .../app/components/Session/LivePlayer.tsx | 34 +- .../app/components/Session/LiveSession.js | 9 +- .../components/Session/MobileClipsPlayer.tsx | 26 +- .../app/components/Session/MobilePlayer.tsx | 38 +- .../Player/ClickMapRenderer/Renderer.tsx | 14 +- .../Player/ClickMapRenderer/ThinPlayer.tsx | 40 +- .../ClickMapRenderer/ThinPlayerContent.tsx | 10 +- .../Player/ClipPlayer/AutoPlayTimer.tsx | 15 +- .../Player/ClipPlayer/AutoplayToggle.tsx | 13 +- .../Player/ClipPlayer/ClipFeedback.tsx | 90 +- .../Player/ClipPlayer/ClipPlayerContent.tsx | 14 +- .../Player/ClipPlayer/ClipPlayerControls.tsx | 4 +- .../Player/ClipPlayer/ClipPlayerHeader.tsx | 18 +- .../Player/ClipPlayer/ClipPlayerOverlay.tsx | 8 +- .../ClipPlayer/MobileClipPlayerContent.tsx | 14 +- .../Player/ClipPlayer/QueueControls.tsx | 27 +- .../Session/Player/ClipPlayer/TimeTracker.tsx | 7 +- .../Session/Player/ClipPlayer/Timeline.tsx | 54 +- .../Player/ClipPlayer/TimelineTracker.tsx | 8 +- .../Session/Player/ClipPlayer/UserCard.tsx | 24 +- .../Player/LivePlayer/AssistDuration.tsx | 24 +- .../AssistSessionsTabs/AssistSessionsTabs.tsx | 73 +- .../Player/LivePlayer/LiveControls.tsx | 81 +- .../Player/LivePlayer/LivePlayerBlock.tsx | 14 +- .../LivePlayer/LivePlayerBlockHeader.tsx | 54 +- .../Player/LivePlayer/LivePlayerInst.tsx | 3 +- .../Player/LivePlayer/LivePlayerSubHeader.tsx | 5 +- .../Player/LivePlayer/LiveTag/LiveTag.tsx | 4 +- .../Player/LivePlayer/Overlay/LiveOverlay.tsx | 30 +- .../LivePlayer/Overlay/LiveStatusText.tsx | 57 +- .../Session/Player/LivePlayer/Timeline.tsx | 41 +- .../Player/MobilePlayer/MobileControls.tsx | 234 +- .../Player/MobilePlayer/MobileOverlay.tsx | 8 +- .../MobilePlayer/MobilePlayerHeader.tsx | 27 +- .../MobilePlayer/MobilePlayerSubheader.tsx | 28 +- .../Player/MobilePlayer/PerfWarnings.tsx | 25 +- .../Player/MobilePlayer/PlayerBlock.tsx | 18 +- .../Player/MobilePlayer/PlayerContent.tsx | 36 +- .../Player/MobilePlayer/PlayerInst.tsx | 32 +- .../Player/MobilePlayer/ReplayWindow.tsx | 37 +- .../Session/Player/PlayerErrorBoundary.tsx | 1 + .../Player/ReplayPlayer/AudioPlayer.tsx | 60 +- .../EventsBlock/EventSearch/EventSearch.js | 9 +- .../EventsBlock/Metadata/Metadata.js | 26 +- .../EventsBlock/Metadata/MetadataItem.js | 41 +- .../EventsBlock/Metadata/SessionLine.js | 18 +- .../EventsBlock/Metadata/SessionList.js | 27 +- .../EventsBlock/UserCard/UserCard.js | 107 +- .../Player/ReplayPlayer/PlayerBlock.tsx | 12 +- .../Player/ReplayPlayer/PlayerBlockHeader.tsx | 47 +- .../Player/ReplayPlayer/PlayerContent.tsx | 32 +- .../Player/ReplayPlayer/PlayerInst.tsx | 15 +- .../ReplayPlayer/SummaryBlock/index.tsx | 65 +- .../Player/ReplayPlayer/useShortcuts.ts | 8 +- .../BackendLogs/BackendLogsPanel.tsx | 35 +- .../BackendLogs/LogsButton.tsx | 10 +- .../BackendLogs/StatusMessages.tsx | 26 +- .../SharedComponents/BackendLogs/Table.tsx | 24 +- .../SharedComponents/BackendLogs/utils.ts | 51 +- .../Player/SharedComponents/SessionTabs.tsx | 47 +- .../Session/Player/SharedComponents/Tab.tsx | 8 +- .../Session/Player/TagWatch/SaveModal.tsx | 32 +- .../Session/Player/TagWatch/TagWatch.tsx | 30 +- frontend/app/components/Session/Session.tsx | 21 +- frontend/app/components/Session/Tabs/Tabs.tsx | 56 +- frontend/app/components/Session/WebPlayer.tsx | 23 +- .../app/components/Session/playerContext.ts | 8 +- .../app/components/Session_/Autoscroll.tsx | 58 +- .../Session_/BottomBlock/BottomBlock.tsx | 2 +- .../Session_/BottomBlock/Content.js | 8 +- .../components/Session_/BottomBlock/Header.js | 24 +- .../Session_/BottomBlock/InfoLine.js | 26 +- .../components/Session_/BottomBlock/index.ts | 5 +- .../Console/ConsoleRow/ConsoleRow.tsx | 9 +- .../components/Session_/EventsBlock/Event.tsx | 92 +- .../Session_/EventsBlock/EventGroupWrapper.js | 18 +- .../EventsBlock/EventSearch/EventSearch.js | 4 +- .../Session_/EventsBlock/EventsBlock.tsx | 64 +- .../Session_/EventsBlock/LoadInfo.js | 69 +- .../Session_/EventsBlock/Metadata/Metadata.js | 26 +- .../EventsBlock/Metadata/MetadataItem.js | 41 +- .../{SessionLine.js => SessionLine.tsx} | 22 +- .../{SessionList.js => SessionList.tsx} | 27 +- .../Session_/EventsBlock/NoteEvent.tsx | 3 +- .../Session_/EventsBlock/UserCard/UserCard.js | 83 +- .../Session_/EventsBlock/UxtEvent.tsx | 14 +- .../Session_/Exceptions/Exceptions.tsx | 61 +- .../components/Session_/Fetch/FetchDetails.js | 186 -- .../Session_/Fetch/FetchDetails.tsx | 190 ++ .../Fetch/components/Headers/Headers.tsx | 52 +- .../components/Session_/GraphQL/GQLDetails.js | 35 +- .../components/Session_/GraphQL/GraphQL.tsx | 58 +- .../Session_/Highlight/HighlightPanel.tsx | 93 +- .../Session_/Inspector/AttrView.tsx | 47 +- .../Session_/Inspector/ElementView.tsx | 126 +- .../Session_/Inspector/InlineInput.tsx | 10 +- .../Session_/Inspector/TextView.tsx | 13 +- .../components/Session_/Issues/IssueForm.tsx | 320 +- .../Session_/Issues/IssuesModal.tsx | 15 +- .../Session_/Multiview/EmptyTile.tsx | 4 +- .../Session_/Multiview/Multiview.tsx | 84 +- .../Session_/Multiview/SessionTileFooter.tsx | 4 +- .../Session_/OverviewPanel/OverviewPanel.tsx | 95 +- .../components/EventRow/EventRow.tsx | 170 +- .../FeatureSelection/FeatureSelection.tsx | 55 +- .../OverviewPanelContainer.tsx | 5 +- .../PerformanceGraph/PerformanceGraph.tsx | 39 +- .../StackEventModal/StackEventModal.tsx | 22 +- .../components/JsonViewer/JsonViewer.js | 8 +- .../components/Sentry/Sentry.js | 85 +- .../components/TimelinePointer/Dots.tsx | 59 +- .../TimelinePointer/TimelinePointer.tsx | 23 +- .../TimelineScale/TimelineScale.tsx | 6 +- .../components/VerticalLine/VerticalLine.tsx | 20 +- .../VerticalPointerLine.tsx | 8 +- .../Session_/OverviewPanel/fakeData.ts | 19 +- .../PageInsightsPanel/PageInsightsPanel.tsx | 49 +- .../components/SelectorCard/SelectorCard.tsx | 35 +- .../SelectorsList/SelectorsList.tsx | 17 +- .../Session_/Performance/Performance.tsx | 158 +- .../AssistSessionsModal.tsx | 56 +- .../AssistSessionsTabs/AssistSessionsTabs.tsx | 44 +- .../Player/Controls/ControlButton.tsx | 13 +- .../Session_/Player/Controls/Controls.tsx | 71 +- .../Session_/Player/Controls/EventsList.tsx | 15 +- .../Session_/Player/Controls/NotesList.tsx | 3 +- .../Session_/Player/Controls/Time.tsx | 27 +- .../Session_/Player/Controls/TimeTracker.js | 9 +- .../Session_/Player/Controls/Timeline.tsx | 38 +- .../Player/Controls/TimelineTracker.tsx | 8 +- .../Player/Controls/components/Circle.tsx | 4 +- .../components/ControlsComponents.tsx | 45 +- .../Controls/components/CustomDragLayer.tsx | 12 +- .../Controls/components/DraggableCircle.tsx | 27 +- .../Controls/components/KeyboardHelp.tsx | 1 - .../Controls/components/PlayerControls.tsx | 24 +- .../Controls/components/PlayingTime.tsx | 18 +- .../Player/Controls/components/ReadNote.tsx | 46 +- .../Controls/components/TimeTooltip.tsx | 12 +- .../components/TimelineZoomButton.tsx | 28 +- .../Controls/components/TooltipContainer.tsx | 4 +- .../Controls/components/ZoomDragLayer.tsx | 25 +- .../components/Session_/Player/Overlay.tsx | 20 +- .../Session_/Player/Overlay/AutoplayTimer.tsx | 17 +- .../Player/Overlay/ElementsMarker.tsx | 23 +- .../Player/Overlay/ElementsMarker/Marker.tsx | 10 +- .../Player/Overlay/LiveStatusText.tsx | 59 +- .../Session_/Player/Overlay/Loader.tsx | 6 +- .../Session_/Player/Overlay/PlayIconLayer.tsx | 7 +- .../Session_/QueueControls/QueueControls.tsx | 14 +- .../ScreenRecorder/ScreenRecorder.tsx | 58 +- .../SessionInfoItem/SessionInfoItem.tsx | 25 +- .../StackEvents/UserEvent/JsonViewer.js | 13 +- .../Session_/StackEvents/UserEvent/Sentry.js | 85 +- .../components/Session_/Storage/DiffRow.tsx | 57 +- .../Session_/Storage/ReduxViewer.tsx | 37 +- .../components/Session_/Storage/Storage.tsx | 43 +- .../app/components/Session_/Subheader.tsx | 106 +- .../Session_/UnitStepsModal/index.tsx | 40 +- .../app/components/Session_/WarnBadge.tsx | 60 +- frontend/app/components/Signup/Signup.tsx | 13 +- .../Signup/SignupForm/SignupForm.tsx | 61 +- .../Spots/SpotPlayer/SpotPlayer.tsx | 15 +- .../SpotPlayer/components/AccessError.tsx | 10 +- .../SpotPlayer/components/AccessModal.tsx | 52 +- .../SpotPlayer/components/CommentsSection.tsx | 23 +- .../components/Panels/SpotConsole.tsx | 32 +- .../components/Panels/SpotNetwork.tsx | 10 +- .../SpotPlayer/components/SpotActivity.tsx | 4 +- .../SpotPlayer/components/SpotLocation.tsx | 10 +- .../components/SpotPlayerControls.tsx | 18 +- .../components/SpotPlayerHeader.tsx | 64 +- .../SpotPlayer/components/SpotTimeline.tsx | 3 +- .../components/SpotVideoContainer.tsx | 14 +- .../Spots/SpotPlayer/spotPlayerStore.ts | 39 +- .../Spots/SpotsList/EditItemModal.tsx | 14 +- .../components/Spots/SpotsList/EmptyPage.tsx | 6 +- .../components/Spots/SpotsList/InstallCTA.tsx | 35 +- .../Spots/SpotsList/SpotListItem.tsx | 16 +- .../Spots/SpotsList/SpotsListHeader.tsx | 20 +- .../app/components/Spots/SpotsList/index.tsx | 49 +- .../UsabilityTesting/LiveTestsModal.tsx | 42 +- .../UsabilityTesting/ParticipantOverview.tsx | 4 +- .../UsabilityTesting/ResponsesOverview.tsx | 89 +- .../components/UsabilityTesting/SidePanel.tsx | 65 +- .../UsabilityTesting/StepsModal.tsx | 41 +- .../components/UsabilityTesting/TestEdit.tsx | 238 +- .../UsabilityTesting/TestOverview.tsx | 387 ++- .../UsabilityTesting/UsabilityTesting.tsx | 118 +- frontend/app/components/hocs/withCopy.tsx | 5 +- .../components/hocs/withLocationHandlers.js | 20 +- frontend/app/components/hocs/withOverlay.js | 117 +- frontend/app/components/hocs/withPageTitle.js | 17 +- .../app/components/hocs/withPermissions.js | 50 +- frontend/app/components/hocs/withReport.tsx | 84 +- frontend/app/components/hocs/withRequest.js | 121 +- .../app/components/hocs/withSiteIdRouter.js | 32 +- .../app/components/hocs/withSiteIdUpdater.js | 17 +- frontend/app/components/hocs/withToggle.js | 32 +- .../AlertTriggersModal/AlertTriggersModal.tsx | 42 +- .../AlertTypeLabel/AlertTypeLabel.tsx | 10 +- .../AlertTriggersModal/ListItem/ListItem.tsx | 21 +- .../shared/AnimatedSVG/AnimatedSVG.tsx | 66 +- .../shared/BannerMessage/BannerMessage.js | 9 +- .../components/shared/Bookmark/Bookmark.tsx | 5 +- .../shared/Breadcrumb/BackButton.tsx | 17 +- .../shared/Breadcrumb/Breadcrumb.tsx | 20 +- .../ChromePluginMessage.js | 14 +- .../shared/CodeSnippet/CodeSnippet.tsx | 26 +- .../shared/ConditionSet/ConditionSet.tsx | 18 +- .../shared/ConditionSet/Conditions.tsx | 4 +- .../components/shared/CopyText/CopyText.tsx | 5 +- frontend/app/components/shared/Copyright.tsx | 27 +- .../app/components/shared/CountryFlagIcon.tsx | 5 +- .../CustomDropdownOption.tsx | 17 +- .../components/shared/DatePicker/config.ts | 58 +- frontend/app/components/shared/DateRange.js | 9 +- .../DateRangeDropdown/DateOptionLabel.js | 5 +- .../DateRangeDropdown/DateRangePopup.tsx | 43 +- .../shared/DevTools/BottomBlock/Content.tsx | 7 +- .../shared/DevTools/BottomBlock/InfoLine.js | 26 +- .../DevTools/ConsolePanel/ConsolePanel.tsx | 98 +- .../ConsolePanel/MobileConsolePanel.tsx | 21 +- .../shared/DevTools/ConsoleRow/ConsoleRow.tsx | 57 +- .../DevTools/NetworkPanel/NetworkPanel.tsx | 146 +- .../shared/DevTools/NetworkPanel/WSModal.tsx | 32 +- .../shared/DevTools/NetworkPanel/WSPanel.tsx | 42 +- .../DevTools/ProfilerModal/ProfilerModal.tsx | 12 +- .../DevTools/ProfilerPanel/ProfilerPanel.tsx | 21 +- .../StackEventModal/StackEventModal.tsx | 24 +- .../StackEventPanel/StackEventPanel.tsx | 37 +- .../DevTools/StackEventRow/StackEventRow.tsx | 8 +- .../shared/DevTools/TabSelector.tsx | 8 +- .../app/components/shared/DevTools/TabTag.tsx | 17 +- .../shared/DevTools/TimeTable/BarRow.tsx | 10 +- .../shared/DevTools/TimeTable/TimeTable.tsx | 50 +- .../shared/DevTools/useListFilter.ts | 18 +- .../app/components/shared/DocCard/DocCard.tsx | 6 +- .../app/components/shared/DocLink/DocLink.js | 8 +- .../app/components/shared/Dropdown/index.tsx | 9 +- .../shared/DropdownPlain/DropdownPlain.tsx | 36 +- .../EmailVerificationMessage.js | 11 +- .../shared/ErrorsBadge/ErrorsBadge.js | 4 +- .../EventSearchInput/EventSearchInput.tsx | 4 +- .../FetchDetailsModal/FetchDetailsModal.tsx | 51 +- .../FetchBasicDetails/FetchBasicDetails.tsx | 20 +- .../components/FetchTabs/FetchTabs.tsx | 115 +- .../components/Headers/Headers.tsx | 64 +- .../shared/FilterDropdown/FilterDropdown.js | 140 +- .../FilterAutoComplete/AutocompleteModal.tsx | 30 +- .../FilterAutoComplete/FilterAutoComplete.tsx | 34 +- .../FilterAutoCompleteLocal.tsx | 21 +- .../{FilterDuration.js => FilterDuration.tsx} | 6 +- .../shared/Filters/FilterItem/FilterItem.tsx | 79 +- .../shared/Filters/FilterList/EventsOrder.tsx | 15 +- .../shared/Filters/FilterList/FilterList.tsx | 205 +- .../Filters/FilterModal/FilterModal.tsx | 120 +- .../Filters/FilterOperator/FilterOperator.tsx | 12 +- .../FilterSelection/FilterSelection.tsx | 4 +- .../Filters/FilterSource/FilterSource.tsx | 30 +- .../Filters/FilterValue/FilterValue.tsx | 17 +- .../FilterValueDropdown.tsx | 21 +- .../LiveFilterModal/LiveFilterModal.tsx | 180 +- .../Filters/SubFilterItem/SubFilterItem.tsx | 20 +- .../shared/GettingStarted/CircleProgress.tsx | 2 +- .../GettingStarted/GettingStartedModal.tsx | 12 +- .../GettingStarted/GettingStartedProgress.tsx | 13 +- .../shared/GettingStarted/StepList.tsx | 35 +- .../GraphQLDetailsModal.tsx | 31 +- .../shared/GuidePopup/GuidePopup.tsx | 26 +- .../Insights/SankeyChart/CustomLink.tsx | 8 +- .../Insights/SankeyChart/CustomNode.tsx | 25 +- .../Insights/SankeyChart/NodeButton.tsx | 25 +- .../Insights/SankeyChart/NodeDropdown.tsx | 10 +- .../Insights/SankeyChart/SankeyChart.tsx | 58 +- .../shared/Insights/SankeyChart/utils.ts | 48 +- .../Insights/ScatterChart/ScatterChart.tsx | 11 +- ...lackButton.js => IntegrateSlackButton.tsx} | 4 +- .../shared/LiveSearchBar/LiveSearchBar.tsx | 7 +- .../LiveSessionList/LiveSessionList.tsx | 105 +- .../LiveSessionReloadButton.tsx | 8 +- .../LiveSessionSearch/LiveSessionSearch.tsx | 4 +- .../LiveSessionSearchField.tsx | 12 +- .../MainSearchBar/components/TagList.tsx | 89 +- .../components/shared/NewBadge/NewBadge.js | 6 +- ...ssionsMessage.js => NoSessionsMessage.tsx} | 21 +- .../app/components/shared/NotFoundPage.tsx | 9 +- .../OutsideClickDetectingDiv.js | 6 +- .../ProjectDropdown/ProjectDropdown.tsx | 57 +- .../shared/ReloadButton/ReloadButton.tsx | 4 +- .../shared/ResultTimings/Barwrapper.js | 4 +- .../shared/ResultTimings/ResultTimings.js | 90 - .../shared/ResultTimings/ResultTimings.tsx | 119 + .../shared/ResultTimings/SectionWrapper.js | 4 +- .../SaveSearchModal/SaveSearchModal.tsx | 57 +- .../shared/SavedSearch/SavedSearch.tsx | 35 +- .../SavedSearchModal/SavedSearchModal.tsx | 72 +- .../shared/SearchActions/SearchActions.tsx | 21 +- .../app/components/shared/Select/Select.tsx | 23 +- .../SelectDateRange/SelectDateRange.tsx | 41 +- .../SessionFilters/AiSessionSearchField.tsx | 23 +- .../shared/SessionFilters/SessionFilters.tsx | 19 +- .../components/shared/SessionItem/Counter.tsx | 15 +- .../SessionItem/ErrorBars/ErrorBars.tsx | 31 +- .../shared/SessionItem/MetaItem/MetaItem.tsx | 35 +- .../MetaMoreButton/MetaMoreButton.tsx | 8 +- .../shared/SessionItem/PlayLink/PlayLink.tsx | 12 +- .../shared/SessionItem/SessionItem.tsx | 74 +- .../SessionMetaList/SessionMetaList.tsx | 4 +- .../SessionSettings/SessionSettings.tsx | 7 +- .../components/CaptureRate.tsx | 53 +- .../ConditionalRecordingSettings.tsx | 23 +- .../components/DefaultPlaying.tsx | 10 +- .../components/DefaultTimezone.tsx | 36 +- .../components/ListingVisibility.tsx | 34 +- .../components/MouseTrailSettings.tsx | 8 +- .../SessionsTabOverview.tsx | 10 +- .../components/Bookmarks/Bookmarks.tsx | 41 +- .../LatestSessionsMessage.tsx | 11 +- .../components/Notes/NoteItem.tsx | 56 +- .../components/Notes/NoteList.tsx | 45 +- .../components/Notes/NoteTags.tsx | 42 +- .../components/Notes/NotesRoute.tsx | 5 +- .../components/Notes/TeamBadge.tsx | 5 +- .../components/RecordingStatus.tsx | 16 +- .../SessionHeader/SessionHeader.tsx | 13 +- .../SessionList/SessionDateRange.tsx | 19 +- .../components/SessionList/SessionList.tsx | 77 +- .../SessionSettingButton.tsx | 6 +- .../components/SessionSort/SessionSort.tsx | 40 +- .../components/SessionTags/SessionTags.tsx | 12 +- .../SessionCopyLink/SessionCopyLink.tsx | 13 +- .../shared/SharePopup/SharePopup.tsx | 81 +- .../components/SupportList/SupportList.tsx | 14 +- .../shared/ToggleContent/ToggleContent.js | 13 +- .../CopyButton/CopyButton.js | 7 +- .../InstallDocs/InstallDocs.js | 30 +- .../TrackingCodeModal/InstallIosDocs.tsx | 44 +- .../ProjectCodeSnippet/ProjectCodeSnippet.js | 80 +- .../TrackingCodeModal/TrackingCodeModal.js | 3 +- .../UserSessionsModal/UserSessionsModal.tsx | 75 +- .../WidgetAutoComplete/WidgetAutoComplete.js | 38 +- .../shared/XRayButton/XRayButton.tsx | 17 +- frontend/app/components/ui/Avatar/Avatar.js | 14 +- .../app/components/ui/BackLink/BackLink.js | 50 +- frontend/app/components/ui/Button/Button.tsx | 32 +- .../app/components/ui/Checkbox/Checkbox.tsx | 12 +- .../ui/CircularLoader/CircularLoader.js | 35 +- .../components/ui/CloseButton/CloseButton.tsx | 18 +- .../app/components/ui/CodeBlock/CodeBlock.tsx | 4 +- .../ui/Confirmation/Confirmation.js | 20 +- .../app/components/ui/Confirmation/index.ts | 6 +- .../components/ui/CopyButton/CopyButton.js | 6 +- .../components/ui/CountryFlag/CountryFlag.tsx | 4 +- frontend/app/components/ui/Divider.tsx | 4 +- .../ui/ErrorDetails/ErrorDetails.tsx | 50 +- .../components/ui/ErrorFrame/ErrorFrame.js | 95 +- .../app/components/ui/ErrorItem/ErrorItem.tsx | 29 +- frontend/app/components/ui/Form/Form.tsx | 10 +- frontend/app/components/ui/Icon/Icon.tsx | 29 +- frontend/app/components/ui/Icon/Os.js | 1 + .../components/ui/IconButton/IconButton.js | 10 +- frontend/app/components/ui/Icons/activity.tsx | 16 +- .../app/components/ui/Icons/alarm_clock.tsx | 12 +- .../app/components/ui/Icons/alarm_plus.tsx | 16 +- .../app/components/ui/Icons/all_sessions.tsx | 18 +- .../app/components/ui/Icons/analytics.tsx | 16 +- frontend/app/components/ui/Icons/anchor.tsx | 16 +- .../ui/Icons/arrow_alt_square_right.tsx | 16 +- .../components/ui/Icons/arrow_bar_left.tsx | 16 +- .../components/ui/Icons/arrow_clockwise.tsx | 12 +- .../ui/Icons/arrow_counterclockwise.tsx | 12 +- .../app/components/ui/Icons/arrow_down.tsx | 16 +- .../components/ui/Icons/arrow_down_short.tsx | 16 +- .../app/components/ui/Icons/arrow_down_up.tsx | 16 +- .../app/components/ui/Icons/arrow_repeat.tsx | 12 +- .../components/ui/Icons/arrow_right_short.tsx | 16 +- .../components/ui/Icons/arrow_square_left.tsx | 16 +- .../ui/Icons/arrow_square_right.tsx | 16 +- frontend/app/components/ui/Icons/arrow_up.tsx | 16 +- .../components/ui/Icons/arrow_up_short.tsx | 16 +- .../ui/Icons/arrows_angle_extend.tsx | 19 +- .../ui/Icons/avatar_icn_avatar1.tsx | 29 +- .../ui/Icons/avatar_icn_avatar10.tsx | 19 +- .../ui/Icons/avatar_icn_avatar11.tsx | 25 +- .../ui/Icons/avatar_icn_avatar12.tsx | 24 +- .../ui/Icons/avatar_icn_avatar13.tsx | 24 +- .../ui/Icons/avatar_icn_avatar14.tsx | 24 +- .../ui/Icons/avatar_icn_avatar15.tsx | 24 +- .../ui/Icons/avatar_icn_avatar16.tsx | 24 +- .../ui/Icons/avatar_icn_avatar17.tsx | 24 +- .../ui/Icons/avatar_icn_avatar18.tsx | 45 +- .../ui/Icons/avatar_icn_avatar19.tsx | 25 +- .../ui/Icons/avatar_icn_avatar2.tsx | 24 +- .../ui/Icons/avatar_icn_avatar20.tsx | 24 +- .../ui/Icons/avatar_icn_avatar21.tsx | 19 +- .../ui/Icons/avatar_icn_avatar22.tsx | 24 +- .../ui/Icons/avatar_icn_avatar23.tsx | 24 +- .../ui/Icons/avatar_icn_avatar3.tsx | 24 +- .../ui/Icons/avatar_icn_avatar4.tsx | 19 +- .../ui/Icons/avatar_icn_avatar5.tsx | 24 +- .../ui/Icons/avatar_icn_avatar6.tsx | 19 +- .../ui/Icons/avatar_icn_avatar7.tsx | 44 +- .../ui/Icons/avatar_icn_avatar8.tsx | 24 +- .../ui/Icons/avatar_icn_avatar9.tsx | 24 +- frontend/app/components/ui/Icons/ban.tsx | 16 +- .../components/ui/Icons/bar_chart_line.tsx | 16 +- .../app/components/ui/Icons/bar_pencil.tsx | 36 +- frontend/app/components/ui/Icons/battery.tsx | 16 +- .../components/ui/Icons/battery_charging.tsx | 12 +- frontend/app/components/ui/Icons/bell.tsx | 16 +- .../app/components/ui/Icons/bell_fill.tsx | 16 +- .../app/components/ui/Icons/bell_plus.tsx | 16 +- .../app/components/ui/Icons/bell_slash.tsx | 16 +- .../app/components/ui/Icons/binoculars.tsx | 16 +- frontend/app/components/ui/Icons/book.tsx | 12 +- frontend/app/components/ui/Icons/book_doc.tsx | 22 +- frontend/app/components/ui/Icons/bookmark.tsx | 16 +- .../app/components/ui/Icons/broadcast.tsx | 16 +- .../components/ui/Icons/browser_browser.tsx | 16 +- .../components/ui/Icons/browser_chrome.tsx | 16 +- .../app/components/ui/Icons/browser_edge.tsx | 16 +- .../components/ui/Icons/browser_electron.tsx | 12 +- .../components/ui/Icons/browser_facebook.tsx | 16 +- .../components/ui/Icons/browser_firefox.tsx | 16 +- .../app/components/ui/Icons/browser_ie.tsx | 16 +- .../app/components/ui/Icons/browser_opera.tsx | 16 +- .../components/ui/Icons/browser_safari.tsx | 16 +- .../app/components/ui/Icons/buildings.tsx | 12 +- frontend/app/components/ui/Icons/bullhorn.tsx | 16 +- .../app/components/ui/Icons/business_time.tsx | 16 +- frontend/app/components/ui/Icons/calendar.tsx | 16 +- .../app/components/ui/Icons/calendar_alt.tsx | 12 +- .../components/ui/Icons/calendar_check.tsx | 16 +- .../app/components/ui/Icons/calendar_day.tsx | 16 +- frontend/app/components/ui/Icons/call.tsx | 18 +- frontend/app/components/ui/Icons/camera.tsx | 16 +- .../app/components/ui/Icons/camera_alt.tsx | 16 +- .../app/components/ui/Icons/camera_video.tsx | 16 +- .../components/ui/Icons/camera_video_off.tsx | 16 +- .../components/ui/Icons/card_checklist.tsx | 12 +- .../app/components/ui/Icons/card_list.tsx | 12 +- .../app/components/ui/Icons/card_text.tsx | 12 +- .../components/ui/Icons/caret_down_fill.tsx | 16 +- .../components/ui/Icons/caret_left_fill.tsx | 16 +- .../components/ui/Icons/caret_right_fill.tsx | 16 +- .../app/components/ui/Icons/caret_up_fill.tsx | 16 +- .../app/components/ui/Icons/chat_dots.tsx | 12 +- .../components/ui/Icons/chat_left_text.tsx | 12 +- .../components/ui/Icons/chat_right_text.tsx | 12 +- .../components/ui/Icons/chat_square_quote.tsx | 12 +- frontend/app/components/ui/Icons/check.tsx | 16 +- .../app/components/ui/Icons/check_circle.tsx | 16 +- .../components/ui/Icons/check_circle_fill.tsx | 16 +- .../ui/Icons/chevron_double_left.tsx | 12 +- .../ui/Icons/chevron_double_right.tsx | 12 +- .../app/components/ui/Icons/chevron_down.tsx | 16 +- .../app/components/ui/Icons/chevron_left.tsx | 16 +- .../app/components/ui/Icons/chevron_right.tsx | 16 +- .../app/components/ui/Icons/chevron_up.tsx | 16 +- frontend/app/components/ui/Icons/circle.tsx | 16 +- .../app/components/ui/Icons/circle_fill.tsx | 16 +- .../components/ui/Icons/click_hesitation.tsx | 24 +- .../app/components/ui/Icons/click_rage.tsx | 29 +- .../components/ui/Icons/clipboard_check.tsx | 19 +- .../ui/Icons/clipboard_list_check.tsx | 12 +- frontend/app/components/ui/Icons/clock.tsx | 16 +- .../app/components/ui/Icons/clock_history.tsx | 12 +- frontend/app/components/ui/Icons/close.tsx | 12 +- .../components/ui/Icons/cloud_fog2_fill.tsx | 16 +- frontend/app/components/ui/Icons/code.tsx | 16 +- frontend/app/components/ui/Icons/cog.tsx | 12 +- frontend/app/components/ui/Icons/cogs.tsx | 16 +- .../app/components/ui/Icons/collection.tsx | 16 +- .../components/ui/Icons/collection_play.tsx | 12 +- .../app/components/ui/Icons/color_apple.tsx | 23 +- .../components/ui/Icons/color_browser_Tor.tsx | 70 +- .../ui/Icons/color_browser_applebot.tsx | 22 +- .../color_browser_browser_icons_color_Tor.tsx | 70 +- ...r_browser_browser_icons_color_applebot.tsx | 22 +- ...lor_browser_browser_icons_color_chrome.tsx | 61 +- ...wser_browser_icons_color_chrome_mobile.tsx | 61 +- ..._browser_icons_color_chrome_mobile_ios.tsx | 61 +- ...owser_browser_icons_color_duck_duck_go.tsx | 129 +- ..._browser_icons_color_duckduckgo_mobile.tsx | 129 +- ...color_browser_browser_icons_color_edge.tsx | 73 +- ...rowser_browser_icons_color_edge_mobile.tsx | 73 +- ...r_browser_browser_icons_color_facebook.tsx | 26 +- ...or_browser_browser_icons_color_firefox.tsx | 180 +- ...rowser_browser_icons_color_firefox_ios.tsx | 180 +- ...ser_browser_icons_color_firefox_mobile.tsx | 180 +- ...lor_browser_browser_icons_color_google.tsx | 32 +- ..._browser_browser_icons_color_googlebot.tsx | 32 +- ...ser_browser_icons_color_huawei_browser.tsx | 17 +- ..._browser_icons_color_internet_explorer.tsx | 17 +- ...owser_browser_icons_color_miui_browser.tsx | 17 +- ...wser_browser_icons_color_mobile_safari.tsx | 721 ++++- ...r_browser_icons_color_mobile_safari_ui.tsx | 721 ++++- ...olor_browser_browser_icons_color_opera.tsx | 40 +- ...lor_browser_browser_icons_color_safari.tsx | 721 ++++- ...r_browser_icons_color_samsung_internet.tsx | 22 +- ...browser_browser_icons_color_uc_browser.tsx | 111 +- ...or_browser_browser_icons_color_unknown.tsx | 42 +- ...olor_browser_browser_icons_color_whale.tsx | 92 +- ...ser_browser_icons_color_yandex_browser.tsx | 22 +- .../ui/Icons/color_browser_chrome.tsx | 61 +- .../ui/Icons/color_browser_chrome_mobile.tsx | 61 +- .../Icons/color_browser_chrome_mobile_ios.tsx | 61 +- .../ui/Icons/color_browser_duck_duck_go.tsx | 129 +- .../Icons/color_browser_duckduckgo_mobile.tsx | 129 +- .../ui/Icons/color_browser_edge.tsx | 73 +- .../ui/Icons/color_browser_edge_mobile.tsx | 73 +- .../ui/Icons/color_browser_facebook.tsx | 26 +- .../ui/Icons/color_browser_firefox.tsx | 180 +- .../ui/Icons/color_browser_firefox_ios.tsx | 180 +- .../ui/Icons/color_browser_firefox_mobile.tsx | 180 +- .../ui/Icons/color_browser_google.tsx | 32 +- .../ui/Icons/color_browser_googlebot.tsx | 32 +- .../ui/Icons/color_browser_huawei_browser.tsx | 17 +- .../Icons/color_browser_internet_explorer.tsx | 17 +- .../ui/Icons/color_browser_miui_browser.tsx | 17 +- .../ui/Icons/color_browser_mobile_safari.tsx | 721 ++++- .../Icons/color_browser_mobile_safari_ui.tsx | 721 ++++- .../ui/Icons/color_browser_opera.tsx | 40 +- .../ui/Icons/color_browser_safari.tsx | 721 ++++- .../Icons/color_browser_samsung_internet.tsx | 22 +- .../ui/Icons/color_browser_uc_browser.tsx | 111 +- .../ui/Icons/color_browser_unknown.tsx | 42 +- .../ui/Icons/color_browser_whale.tsx | 92 +- .../ui/Icons/color_browser_yandex_browser.tsx | 22 +- .../app/components/ui/Icons/color_chrome.tsx | 62 +- .../components/ui/Icons/color_country_de.tsx | 34 +- .../components/ui/Icons/color_country_fr.tsx | 34 +- .../components/ui/Icons/color_country_gb.tsx | 75 +- .../components/ui/Icons/color_country_in.tsx | 64 +- .../components/ui/Icons/color_country_us.tsx | 76 +- frontend/app/components/ui/Icons/color_de.tsx | 34 +- .../ui/Icons/color_device_desktop.tsx | 28 +- .../ui/Icons/color_device_mobile.tsx | 51 +- .../ui/Icons/color_device_other_phone.tsx | 35 +- .../ui/Icons/color_device_tablet.tsx | 28 +- .../ui/Icons/color_device_unkown.tsx | 44 +- .../app/components/ui/Icons/color_edge.tsx | 74 +- .../app/components/ui/Icons/color_fedora.tsx | 28 +- .../app/components/ui/Icons/color_firefox.tsx | 181 +- frontend/app/components/ui/Icons/color_fr.tsx | 34 +- frontend/app/components/ui/Icons/color_gb.tsx | 75 +- frontend/app/components/ui/Icons/color_in.tsx | 64 +- .../ui/Icons/color_issues_bad_request.tsx | 68 +- .../ui/Icons/color_issues_click_rage.tsx | 44 +- .../components/ui/Icons/color_issues_cpu.tsx | 92 +- .../ui/Icons/color_issues_crash.tsx | 52 +- .../ui/Icons/color_issues_custom.tsx | 44 +- .../ui/Icons/color_issues_dead_click.tsx | 92 +- .../ui/Icons/color_issues_errors.tsx | 36 +- .../color_issues_excessive_scrolling.tsx | 36 +- .../ui/Icons/color_issues_js_exception.tsx | 36 +- .../ui/Icons/color_issues_memory.tsx | 84 +- .../Icons/color_issues_missing_resource.tsx | 60 +- .../ui/Icons/color_issues_mouse_thrashing.tsx | 28 +- .../ui/Icons/color_issues_mouse_trashing.tsx | 28 +- .../ui/Icons/color_issues_slow_page_load.tsx | 99 +- .../components/ui/Icons/color_microsoft.tsx | 13 +- .../app/components/ui/Icons/color_opera.tsx | 41 +- .../components/ui/Icons/color_os_android.tsx | 34 +- .../components/ui/Icons/color_os_apple.tsx | 22 +- .../ui/Icons/color_os_blackberry.tsx | 52 +- .../ui/Icons/color_os_chrome_os.tsx | 61 +- .../ui/Icons/color_os_elementary.tsx | 17 +- .../components/ui/Icons/color_os_fedora.tsx | 27 +- .../components/ui/Icons/color_os_freebsd.tsx | 32 +- .../components/ui/Icons/color_os_gnome.tsx | 19 +- .../app/components/ui/Icons/color_os_ios.tsx | 32 +- .../components/ui/Icons/color_os_linux.tsx | 283 +- .../ui/Icons/color_os_linux_mint.tsx | 63 +- .../components/ui/Icons/color_os_macos.tsx | 31 +- .../ui/Icons/color_os_microsoft.tsx | 12 +- .../components/ui/Icons/color_os_ubuntu.tsx | 22 +- .../components/ui/Icons/color_os_unkown.tsx | 44 +- .../app/components/ui/Icons/color_safari.tsx | 722 ++++- .../app/components/ui/Icons/color_ubuntu.tsx | 23 +- frontend/app/components/ui/Icons/color_us.tsx | 76 +- .../app/components/ui/Icons/columns_gap.tsx | 16 +- .../ui/Icons/columns_gap_filled.tsx | 16 +- frontend/app/components/ui/Icons/console.tsx | 12 +- .../app/components/ui/Icons/console_error.tsx | 23 +- .../components/ui/Icons/console_exception.tsx | 16 +- .../app/components/ui/Icons/console_info.tsx | 12 +- .../components/ui/Icons/console_warning.tsx | 16 +- .../app/components/ui/Icons/controller.tsx | 12 +- frontend/app/components/ui/Icons/cookies.tsx | 12 +- frontend/app/components/ui/Icons/copy.tsx | 16 +- .../ui/Icons/credit_card_2_back.tsx | 19 +- .../components/ui/Icons/credit_card_front.tsx | 12 +- frontend/app/components/ui/Icons/cross.tsx | 16 +- frontend/app/components/ui/Icons/cubes.tsx | 12 +- .../app/components/ui/Icons/cursor_trash.tsx | 42 +- frontend/app/components/ui/Icons/dash.tsx | 16 +- .../app/components/ui/Icons/dashboard_icn.tsx | 252 +- .../ui/Icons/dashboards_circle_alert.tsx | 27 +- .../ui/Icons/dashboards_cohort_chart.tsx | 39 +- .../ui/Icons/dashboards_heatmap_2.tsx | 30 +- .../ui/Icons/dashboards_user_journey.tsx | 30 +- .../ui/Icons/db_icons_icn_card_clickMap.tsx | 153 +- .../ui/Icons/db_icons_icn_card_errors.tsx | 63 +- .../ui/Icons/db_icons_icn_card_funnel.tsx | 177 +- .../ui/Icons/db_icons_icn_card_funnels.tsx | 232 +- .../ui/Icons/db_icons_icn_card_insights.tsx | 117 +- .../ui/Icons/db_icons_icn_card_library.tsx | 145 +- .../ui/Icons/db_icons_icn_card_mapchart.tsx | 102 +- .../Icons/db_icons_icn_card_pathAnalysis.tsx | 141 +- .../Icons/db_icons_icn_card_performance.tsx | 86 +- .../ui/Icons/db_icons_icn_card_resources.tsx | 100 +- .../ui/Icons/db_icons_icn_card_table.tsx | 244 +- .../ui/Icons/db_icons_icn_card_timeseries.tsx | 82 +- .../ui/Icons/db_icons_icn_card_webVitals.tsx | 328 +- frontend/app/components/ui/Icons/desktop.tsx | 16 +- frontend/app/components/ui/Icons/device.tsx | 12 +- .../app/components/ui/Icons/diagram_3.tsx | 16 +- frontend/app/components/ui/Icons/dice_3.tsx | 12 +- frontend/app/components/ui/Icons/dizzy.tsx | 16 +- .../app/components/ui/Icons/door_closed.tsx | 12 +- .../app/components/ui/Icons/doublecheck.tsx | 16 +- frontend/app/components/ui/Icons/download.tsx | 12 +- frontend/app/components/ui/Icons/drag.tsx | 16 +- frontend/app/components/ui/Icons/edit.tsx | 16 +- .../app/components/ui/Icons/ellipsis_v.tsx | 16 +- .../app/components/ui/Icons/emoji_dizzy.tsx | 12 +- frontend/app/components/ui/Icons/enter.tsx | 16 +- frontend/app/components/ui/Icons/envelope.tsx | 16 +- .../components/ui/Icons/envelope_check.tsx | 12 +- .../components/ui/Icons/envelope_paper.tsx | 21 +- .../app/components/ui/Icons/envelope_x.tsx | 12 +- .../app/components/ui/Icons/errors_icon.tsx | 12 +- .../app/components/ui/Icons/event_click.tsx | 16 +- .../ui/Icons/event_click_hesitation.tsx | 24 +- .../components/ui/Icons/event_clickrage.tsx | 12 +- .../app/components/ui/Icons/event_code.tsx | 16 +- .../components/ui/Icons/event_i_cursor.tsx | 16 +- .../app/components/ui/Icons/event_input.tsx | 12 +- .../ui/Icons/event_input_hesitation.tsx | 23 +- .../app/components/ui/Icons/event_link.tsx | 12 +- .../components/ui/Icons/event_location.tsx | 16 +- .../ui/Icons/event_mouse_thrashing.tsx | 42 +- .../app/components/ui/Icons/event_resize.tsx | 16 +- .../app/components/ui/Icons/event_view.tsx | 12 +- .../ui/Icons/exclamation_circle.tsx | 23 +- .../ui/Icons/exclamation_circle_fill.tsx | 29 +- .../ui/Icons/exclamation_triangle.tsx | 25 +- .../app/components/ui/Icons/expand_wide.tsx | 12 +- .../app/components/ui/Icons/explosion.tsx | 16 +- .../components/ui/Icons/external_link_alt.tsx | 12 +- frontend/app/components/ui/Icons/eye.tsx | 16 +- .../app/components/ui/Icons/eye_slash.tsx | 16 +- .../components/ui/Icons/eye_slash_fill.tsx | 12 +- frontend/app/components/ui/Icons/fetch.tsx | 16 +- .../app/components/ui/Icons/fetch_request.tsx | 26 +- .../app/components/ui/Icons/fflag_multi.tsx | 12 +- .../app/components/ui/Icons/fflag_single.tsx | 17 +- frontend/app/components/ui/Icons/file.tsx | 16 +- .../components/ui/Icons/file_bar_graph.tsx | 18 +- .../app/components/ui/Icons/file_code.tsx | 12 +- .../components/ui/Icons/file_medical_alt.tsx | 12 +- frontend/app/components/ui/Icons/file_pdf.tsx | 27 +- frontend/app/components/ui/Icons/files.tsx | 16 +- .../app/components/ui/Icons/filetype_js.tsx | 16 +- .../app/components/ui/Icons/filetype_pdf.tsx | 16 +- frontend/app/components/ui/Icons/filter.tsx | 25 +- .../ui/Icons/filters_arrow_return_right.tsx | 16 +- .../components/ui/Icons/filters_browser.tsx | 16 +- .../ui/Icons/filters_chevrons_up_down.tsx | 25 +- .../app/components/ui/Icons/filters_click.tsx | 16 +- .../components/ui/Icons/filters_clickrage.tsx | 12 +- .../app/components/ui/Icons/filters_code.tsx | 16 +- .../components/ui/Icons/filters_console.tsx | 16 +- .../components/ui/Icons/filters_country.tsx | 12 +- .../components/ui/Icons/filters_cpu_load.tsx | 28 +- .../components/ui/Icons/filters_custom.tsx | 16 +- .../components/ui/Icons/filters_device.tsx | 12 +- .../ui/Icons/filters_dom_complete.tsx | 17 +- .../components/ui/Icons/filters_duration.tsx | 16 +- .../app/components/ui/Icons/filters_error.tsx | 23 +- .../app/components/ui/Icons/filters_fetch.tsx | 16 +- .../ui/Icons/filters_fetch_failed.tsx | 27 +- .../components/ui/Icons/filters_file_code.tsx | 16 +- .../components/ui/Icons/filters_graphql.tsx | 16 +- .../components/ui/Icons/filters_i_cursor.tsx | 16 +- .../app/components/ui/Icons/filters_input.tsx | 12 +- .../app/components/ui/Icons/filters_lcpt.tsx | 17 +- .../app/components/ui/Icons/filters_link.tsx | 12 +- .../components/ui/Icons/filters_location.tsx | 16 +- .../ui/Icons/filters_memory_load.tsx | 17 +- .../components/ui/Icons/filters_metadata.tsx | 16 +- .../app/components/ui/Icons/filters_os.tsx | 16 +- .../filters_perfromance_network_request.tsx | 36 +- .../components/ui/Icons/filters_platform.tsx | 16 +- .../components/ui/Icons/filters_referrer.tsx | 12 +- .../components/ui/Icons/filters_resize.tsx | 16 +- .../components/ui/Icons/filters_rev_id.tsx | 16 +- .../components/ui/Icons/filters_screen.tsx | 23 +- .../ui/Icons/filters_state_action.tsx | 16 +- .../ui/Icons/filters_tag_element.tsx | 25 +- .../app/components/ui/Icons/filters_ttfb.tsx | 23 +- .../components/ui/Icons/filters_user_alt.tsx | 12 +- .../components/ui/Icons/filters_userid.tsx | 16 +- .../app/components/ui/Icons/filters_view.tsx | 12 +- frontend/app/components/ui/Icons/flag_na.tsx | 20 +- frontend/app/components/ui/Icons/folder2.tsx | 16 +- .../app/components/ui/Icons/folder_plus.tsx | 12 +- .../app/components/ui/Icons/fullscreen.tsx | 16 +- frontend/app/components/ui/Icons/funnel.tsx | 25 +- .../app/components/ui/Icons/funnel_cpu.tsx | 16 +- .../components/ui/Icons/funnel_cpu_fill.tsx | 12 +- .../app/components/ui/Icons/funnel_dizzy.tsx | 16 +- .../ui/Icons/funnel_emoji_angry.tsx | 12 +- .../ui/Icons/funnel_emoji_angry_fill.tsx | 16 +- .../ui/Icons/funnel_emoji_dizzy_fill.tsx | 16 +- .../ui/Icons/funnel_exclamation_circle.tsx | 23 +- .../Icons/funnel_exclamation_circle_fill.tsx | 16 +- .../ui/Icons/funnel_file_earmark_break.tsx | 16 +- .../Icons/funnel_file_earmark_break_fill.tsx | 16 +- .../ui/Icons/funnel_file_earmark_minus.tsx | 12 +- .../Icons/funnel_file_earmark_minus_fill.tsx | 16 +- .../ui/Icons/funnel_file_medical_alt.tsx | 12 +- .../app/components/ui/Icons/funnel_file_x.tsx | 12 +- .../app/components/ui/Icons/funnel_fill.tsx | 16 +- .../components/ui/Icons/funnel_hdd_fill.tsx | 16 +- .../ui/Icons/funnel_hourglass_top.tsx | 16 +- .../app/components/ui/Icons/funnel_image.tsx | 12 +- .../components/ui/Icons/funnel_image_fill.tsx | 16 +- .../components/ui/Icons/funnel_microchip.tsx | 12 +- .../app/components/ui/Icons/funnel_mouse.tsx | 16 +- .../app/components/ui/Icons/funnel_new.tsx | 16 +- .../Icons/funnel_patch_exclamation_fill.tsx | 16 +- .../components/ui/Icons/funnel_sd_card.tsx | 12 +- frontend/app/components/ui/Icons/gear.tsx | 12 +- .../app/components/ui/Icons/gear_fill.tsx | 16 +- .../ui/Icons/geo_alt_fill_custom.tsx | 16 +- frontend/app/components/ui/Icons/github.tsx | 16 +- frontend/app/components/ui/Icons/graph_up.tsx | 23 +- .../components/ui/Icons/graph_up_arrow.tsx | 16 +- frontend/app/components/ui/Icons/grid.tsx | 16 +- frontend/app/components/ui/Icons/grid_1x2.tsx | 16 +- frontend/app/components/ui/Icons/grid_3x3.tsx | 16 +- .../app/components/ui/Icons/grid_check.tsx | 16 +- .../components/ui/Icons/grid_horizontal.tsx | 16 +- .../components/ui/Icons/grip_horizontal.tsx | 16 +- frontend/app/components/ui/Icons/hash.tsx | 16 +- .../app/components/ui/Icons/hdd_stack.tsx | 12 +- frontend/app/components/ui/Icons/headset.tsx | 16 +- .../app/components/ui/Icons/heart_rate.tsx | 16 +- .../components/ui/Icons/high_engagement.tsx | 16 +- frontend/app/components/ui/Icons/history.tsx | 16 +- .../components/ui/Icons/hourglass_start.tsx | 16 +- .../app/components/ui/Icons/ic_errors.tsx | 23 +- .../app/components/ui/Icons/ic_network.tsx | 12 +- frontend/app/components/ui/Icons/ic_rage.tsx | 12 +- .../app/components/ui/Icons/ic_resources.tsx | 12 +- .../components/ui/Icons/icn_fetch_request.tsx | 37 +- .../app/components/ui/Icons/icn_referrer.tsx | 38 +- frontend/app/components/ui/Icons/icn_url.tsx | 38 +- frontend/app/components/ui/Icons/id_card.tsx | 12 +- frontend/app/components/ui/Icons/image.tsx | 12 +- frontend/app/components/ui/Icons/info.tsx | 12 +- .../app/components/ui/Icons/info_circle.tsx | 12 +- .../components/ui/Icons/info_circle_fill.tsx | 16 +- .../app/components/ui/Icons/info_square.tsx | 12 +- .../components/ui/Icons/input_hesitation.tsx | 23 +- frontend/app/components/ui/Icons/inspect.tsx | 16 +- .../ui/Icons/integrations_apple.tsx | 30 +- .../ui/Icons/integrations_assist.tsx | 19 +- .../ui/Icons/integrations_bugsnag.tsx | 30 +- .../ui/Icons/integrations_bugsnag_text.tsx | 21 +- .../ui/Icons/integrations_chrome.tsx | 69 +- .../ui/Icons/integrations_cloudwatch.tsx | 34 +- .../ui/Icons/integrations_cloudwatch_text.tsx | 39 +- .../ui/Icons/integrations_datadog.tsx | 25 +- .../ui/Icons/integrations_dynatrace.tsx | 55 +- .../components/ui/Icons/integrations_edge.tsx | 81 +- .../ui/Icons/integrations_elasticsearch.tsx | 55 +- .../Icons/integrations_elasticsearch_text.tsx | 47 +- .../ui/Icons/integrations_fedora.tsx | 35 +- .../ui/Icons/integrations_firefox.tsx | 188 +- .../ui/Icons/integrations_github.tsx | 21 +- .../ui/Icons/integrations_graphql.tsx | 24 +- .../components/ui/Icons/integrations_jira.tsx | 50 +- .../ui/Icons/integrations_jira_text.tsx | 37 +- .../ui/Icons/integrations_microsoft.tsx | 20 +- .../components/ui/Icons/integrations_mobx.tsx | 45 +- .../ui/Icons/integrations_newrelic.tsx | 42 +- .../ui/Icons/integrations_newrelic_text.tsx | 29 +- .../components/ui/Icons/integrations_ngrx.tsx | 34 +- .../ui/Icons/integrations_openreplay.tsx | 29 +- .../ui/Icons/integrations_openreplay_text.tsx | 34 +- .../ui/Icons/integrations_opera.tsx | 48 +- .../ui/Icons/integrations_redux.tsx | 19 +- .../ui/Icons/integrations_rollbar.tsx | 34 +- .../ui/Icons/integrations_rollbar_text.tsx | 39 +- .../ui/Icons/integrations_safari.tsx | 724 ++++- .../ui/Icons/integrations_segment.tsx | 30 +- .../ui/Icons/integrations_sentry.tsx | 21 +- .../ui/Icons/integrations_sentry_text.tsx | 21 +- .../ui/Icons/integrations_slack.tsx | 39 +- .../ui/Icons/integrations_slack_bw.tsx | 21 +- .../ui/Icons/integrations_stackdriver.tsx | 31 +- .../ui/Icons/integrations_sumologic.tsx | 19 +- .../ui/Icons/integrations_sumologic_text.tsx | 21 +- .../ui/Icons/integrations_teams.tsx | 89 +- .../ui/Icons/integrations_teams_white.tsx | 19 +- .../ui/Icons/integrations_ubuntu.tsx | 30 +- .../ui/Icons/integrations_vuejs.tsx | 34 +- .../ui/Icons/integrations_zustand.tsx | 92 +- .../app/components/ui/Icons/journal_code.tsx | 12 +- frontend/app/components/ui/Icons/key.tsx | 12 +- frontend/app/components/ui/Icons/keyboard.tsx | 12 +- .../app/components/ui/Icons/layer_group.tsx | 16 +- .../app/components/ui/Icons/layers_half.tsx | 16 +- .../app/components/ui/Icons/lightbulb.tsx | 16 +- .../app/components/ui/Icons/lightbulb_on.tsx | 12 +- .../app/components/ui/Icons/link_45deg.tsx | 12 +- frontend/app/components/ui/Icons/list.tsx | 16 +- frontend/app/components/ui/Icons/list_alt.tsx | 25 +- .../app/components/ui/Icons/list_arrow.tsx | 21 +- frontend/app/components/ui/Icons/list_ul.tsx | 16 +- frontend/app/components/ui/Icons/lock_alt.tsx | 16 +- .../components/ui/Icons/low_disc_space.tsx | 22 +- frontend/app/components/ui/Icons/magic.tsx | 16 +- .../components/ui/Icons/map_marker_alt.tsx | 12 +- frontend/app/components/ui/Icons/memory.tsx | 16 +- .../app/components/ui/Icons/memory_ios.tsx | 16 +- frontend/app/components/ui/Icons/mic.tsx | 12 +- frontend/app/components/ui/Icons/mic_mute.tsx | 12 +- frontend/app/components/ui/Icons/minus.tsx | 16 +- frontend/app/components/ui/Icons/mobile.tsx | 12 +- .../app/components/ui/Icons/mouse_alt.tsx | 16 +- .../ui/Icons/mouse_pointer_click.tsx | 32 +- frontend/app/components/ui/Icons/network.tsx | 23 +- frontend/app/components/ui/Icons/next1.tsx | 16 +- .../app/components/ui/Icons/no_dashboard.tsx | 22 +- .../app/components/ui/Icons/no_metrics.tsx | 18 +- .../components/ui/Icons/no_metrics_chart.tsx | 38 +- .../app/components/ui/Icons/no_recordings.tsx | 74 +- frontend/app/components/ui/Icons/orIcn.tsx | 35 +- frontend/app/components/ui/Icons/orSpot.tsx | 35 +- .../app/components/ui/Icons/orspotOutline.tsx | 23 +- frontend/app/components/ui/Icons/os.tsx | 16 +- .../app/components/ui/Icons/os_android.tsx | 16 +- .../app/components/ui/Icons/os_chrome_os.tsx | 16 +- .../app/components/ui/Icons/os_fedora.tsx | 16 +- frontend/app/components/ui/Icons/os_ios.tsx | 16 +- frontend/app/components/ui/Icons/os_linux.tsx | 16 +- .../app/components/ui/Icons/os_mac_os_x.tsx | 16 +- frontend/app/components/ui/Icons/os_other.tsx | 16 +- .../app/components/ui/Icons/os_ubuntu.tsx | 16 +- .../app/components/ui/Icons/os_windows.tsx | 16 +- frontend/app/components/ui/Icons/pause.tsx | 16 +- .../components/ui/Icons/pause_circle_fill.tsx | 16 +- .../app/components/ui/Icons/pause_fill.tsx | 16 +- .../app/components/ui/Icons/pdf_download.tsx | 17 +- frontend/app/components/ui/Icons/pencil.tsx | 16 +- .../app/components/ui/Icons/pencil_stop.tsx | 23 +- frontend/app/components/ui/Icons/people.tsx | 16 +- frontend/app/components/ui/Icons/percent.tsx | 16 +- .../components/ui/Icons/performance_icon.tsx | 12 +- frontend/app/components/ui/Icons/person.tsx | 16 +- .../app/components/ui/Icons/person_border.tsx | 12 +- .../app/components/ui/Icons/person_fill.tsx | 16 +- .../components/ui/Icons/pie_chart_fill.tsx | 16 +- frontend/app/components/ui/Icons/pin_fill.tsx | 16 +- frontend/app/components/ui/Icons/play.tsx | 16 +- .../app/components/ui/Icons/play_circle.tsx | 16 +- .../components/ui/Icons/play_circle_bold.tsx | 12 +- .../components/ui/Icons/play_circle_light.tsx | 16 +- .../app/components/ui/Icons/play_fill.tsx | 16 +- .../app/components/ui/Icons/play_fill_new.tsx | 16 +- .../app/components/ui/Icons/play_hover.tsx | 16 +- frontend/app/components/ui/Icons/plug.tsx | 16 +- frontend/app/components/ui/Icons/plus.tsx | 16 +- .../app/components/ui/Icons/plus_circle.tsx | 12 +- frontend/app/components/ui/Icons/plus_lg.tsx | 16 +- .../ui/Icons/pointer_sessions_search.tsx | 16 +- frontend/app/components/ui/Icons/prev1.tsx | 16 +- frontend/app/components/ui/Icons/pulse.tsx | 20 +- frontend/app/components/ui/Icons/puzzle.tsx | 16 +- .../app/components/ui/Icons/puzzle_piece.tsx | 16 +- .../components/ui/Icons/question_circle.tsx | 12 +- .../app/components/ui/Icons/question_lg.tsx | 16 +- .../app/components/ui/Icons/quote_left.tsx | 16 +- .../app/components/ui/Icons/quote_right.tsx | 16 +- frontend/app/components/ui/Icons/quotes.tsx | 19 +- frontend/app/components/ui/Icons/record2.tsx | 12 +- .../app/components/ui/Icons/record_btn.tsx | 12 +- .../app/components/ui/Icons/record_circle.tsx | 12 +- .../ui/Icons/record_circle_fill.tsx | 16 +- frontend/app/components/ui/Icons/redo.tsx | 16 +- .../app/components/ui/Icons/redo_back.tsx | 16 +- frontend/app/components/ui/Icons/redux.tsx | 19 +- frontend/app/components/ui/Icons/referrer.tsx | 42 +- .../components/ui/Icons/remote_control.tsx | 16 +- .../app/components/ui/Icons/replay_10.tsx | 16 +- .../components/ui/Icons/resources_icon.tsx | 16 +- frontend/app/components/ui/Icons/safe.tsx | 12 +- .../app/components/ui/Icons/safe_fill.tsx | 12 +- .../app/components/ui/Icons/sandglass.tsx | 16 +- frontend/app/components/ui/Icons/search.tsx | 16 +- .../ui/Icons/search_notification.tsx | 16 +- frontend/app/components/ui/Icons/server.tsx | 16 +- .../app/components/ui/Icons/share_alt.tsx | 16 +- .../app/components/ui/Icons/shield_lock.tsx | 12 +- .../components/ui/Icons/side_menu_closed.tsx | 25 +- .../components/ui/Icons/side_menu_open.tsx | 23 +- .../components/ui/Icons/signpost_split.tsx | 25 +- frontend/app/components/ui/Icons/signup.tsx | 19 +- .../app/components/ui/Icons/skip_forward.tsx | 16 +- .../components/ui/Icons/skip_forward_fill.tsx | 16 +- frontend/app/components/ui/Icons/slack.tsx | 16 +- .../app/components/ui/Icons/slash_circle.tsx | 12 +- frontend/app/components/ui/Icons/sleep.tsx | 23 +- frontend/app/components/ui/Icons/sliders.tsx | 16 +- .../app/components/ui/Icons/social_slack.tsx | 19 +- .../app/components/ui/Icons/social_trello.tsx | 21 +- frontend/app/components/ui/Icons/sparkles.tsx | 29 +- .../app/components/ui/Icons/speedometer2.tsx | 12 +- frontend/app/components/ui/Icons/spinner.tsx | 16 +- .../ui/Icons/square_mouse_pointer.tsx | 23 +- frontend/app/components/ui/Icons/star.tsx | 16 +- .../app/components/ui/Icons/star_solid.tsx | 16 +- .../app/components/ui/Icons/step_forward.tsx | 16 +- frontend/app/components/ui/Icons/stickies.tsx | 12 +- .../ui/Icons/stop_record_circle.tsx | 20 +- .../app/components/ui/Icons/stopwatch.tsx | 12 +- frontend/app/components/ui/Icons/store.tsx | 16 +- frontend/app/components/ui/Icons/sync_alt.tsx | 12 +- frontend/app/components/ui/Icons/table.tsx | 16 +- .../app/components/ui/Icons/table_new.tsx | 16 +- .../components/ui/Icons/tablet_android.tsx | 12 +- .../components/ui/Icons/tachometer_slow.tsx | 12 +- .../ui/Icons/tachometer_slowest.tsx | 16 +- frontend/app/components/ui/Icons/tags.tsx | 12 +- .../app/components/ui/Icons/team_funnel.tsx | 12 +- .../app/components/ui/Icons/telephone.tsx | 16 +- .../components/ui/Icons/telephone_fill.tsx | 16 +- frontend/app/components/ui/Icons/terminal.tsx | 12 +- .../components/ui/Icons/text_paragraph.tsx | 16 +- .../components/ui/Icons/thermometer_sun.tsx | 12 +- frontend/app/components/ui/Icons/toggles.tsx | 22 +- frontend/app/components/ui/Icons/tools.tsx | 16 +- frontend/app/components/ui/Icons/trash.tsx | 25 +- frontend/app/components/ui/Icons/turtle.tsx | 16 +- frontend/app/components/ui/Icons/user_alt.tsx | 16 +- .../app/components/ui/Icons/user_circle.tsx | 16 +- .../app/components/ui/Icons/user_friends.tsx | 16 +- .../app/components/ui/Icons/user_journey.tsx | 25 +- .../app/components/ui/Icons/user_switch.tsx | 25 +- frontend/app/components/ui/Icons/users.tsx | 16 +- .../components/ui/Icons/vendors_graphql.tsx | 16 +- .../app/components/ui/Icons/vendors_mobx.tsx | 20 +- .../app/components/ui/Icons/vendors_ngrx.tsx | 23 +- .../app/components/ui/Icons/vendors_redux.tsx | 12 +- .../app/components/ui/Icons/vendors_vuex.tsx | 18 +- .../app/components/ui/Icons/web_vitals.tsx | 16 +- frontend/app/components/ui/Icons/wifi.tsx | 12 +- frontend/app/components/ui/Icons/window.tsx | 16 +- .../app/components/ui/Icons/window_alt.tsx | 16 +- .../components/ui/Icons/window_restore.tsx | 16 +- frontend/app/components/ui/Icons/window_x.tsx | 12 +- frontend/app/components/ui/Icons/zoom_in.tsx | 12 +- .../components/ui/Information/Information.js | 4 +- .../app/components/ui/ItemMenu/ItemMenu.tsx | 38 +- frontend/app/components/ui/Label/Label.js | 6 +- frontend/app/components/ui/Link/Link.js | 4 +- frontend/app/components/ui/Loader/Loader.tsx | 29 +- frontend/app/components/ui/Message/Message.js | 5 +- frontend/app/components/ui/Modal/Modal.tsx | 41 +- .../app/components/ui/NoContent/NoContent.js | 0 .../app/components/ui/NoContent/NoContent.tsx | 38 +- .../ui/NoPermission/NoPermission.tsx | 11 +- .../NoSessionPermission.tsx | 26 +- .../ui/Notification/Notification.js | 5 +- .../app/components/ui/PageTitle/PageTitle.tsx | 36 +- .../components/ui/Pagination/Pagination.tsx | 9 +- .../app/components/ui/Popover/Popover.tsx | 10 +- frontend/app/components/ui/SVG.tsx | 2128 ++++++++++--- .../ui/SegmentSelection/SegmentSelection.tsx | 45 +- .../ui/SideMenuHeader/SideMenuHeader.tsx | 4 +- .../ui/SideMenuItem/SideMenuItem.tsx | 18 +- .../components/ui/SlideModal/SlideModal.js | 24 +- frontend/app/components/ui/Switch.tsx | 4 +- frontend/app/components/ui/Tabs/Tabs.js | 23 +- .../app/components/ui/TagBadge/TagBadge.js | 13 +- .../app/components/ui/TagInput/TagInput.js | 40 +- frontend/app/components/ui/TagList/TagList.js | 27 +- .../ui/TextEllipsis/TextEllipsis.js | 36 +- .../app/components/ui/TextLink/TextLink.js | 15 +- .../ui/TimelinePointer/TimelinePointer.js | 4 +- .../ui/ToggleButton/ToggleButton.tsx | 26 +- frontend/app/constants/alertMetrics.ts | 60 +- frontend/app/constants/card.ts | 80 +- frontend/app/constants/filterOptions.js | 35 +- frontend/app/constants/index.js | 8 +- frontend/app/date.ts | 103 +- frontend/app/dateRange.js | 24 +- frontend/app/declaration.d.ts | 8 +- .../app/dev/components/CrashReactAppButton.js | 13 - frontend/app/dev/components/ErrorGenPanel.tsx | 31 - frontend/app/dev/components/EvalErrorBtn.js | 16 - .../app/dev/components/EventErrorButton.js | 12 - .../app/dev/components/InternalErrorButton.js | 17 - .../app/dev/components/MemoryCrushButton.js | 20 - .../app/dev/components/PromiseErrorButton.js | 16 - frontend/app/hooks/useCancelableTimeout.ts | 6 +- frontend/app/hooks/useForm.ts | 9 +- frontend/app/hooks/useInputState.ts | 11 +- frontend/app/hooks/useLatestRef.ts | 4 +- .../app/hooks/useSessionSearchQueryHandler.ts | 10 +- frontend/app/hooks/useToggle.ts | 4 +- frontend/app/i18n.js | 12 +- frontend/app/initialize.tsx | 44 +- frontend/app/layout/InitORCard.tsx | 18 +- frontend/app/layout/Layout.tsx | 20 +- frontend/app/layout/Logo.tsx | 5 +- frontend/app/layout/SideMenu.tsx | 62 +- .../app/layout/SpotToOpenReplayPrompt.tsx | 75 +- frontend/app/layout/SupportModal.tsx | 37 +- frontend/app/layout/TopHeader.tsx | 11 +- frontend/app/layout/data.ts | 111 +- frontend/app/locales/en.json | 1430 ++++++++- frontend/app/locales/es.json | 1430 +++++++++ frontend/app/locales/extractTranslations.js | 95 + frontend/app/locales/fr.json | 1430 ++++++++- frontend/app/locales/ru.json | 1430 +++++++++ frontend/app/locales/zh.json | 1430 +++++++++ frontend/app/mstore/aiFiltersStore.ts | 8 +- frontend/app/mstore/aiSummaryStore.ts | 16 +- frontend/app/mstore/alertsStore.ts | 60 +- frontend/app/mstore/assistMultiviewStore.ts | 26 +- frontend/app/mstore/auditStore.ts | 80 +- frontend/app/mstore/customFieldStore.ts | 11 +- frontend/app/mstore/dashboardStore.ts | 100 +- frontend/app/mstore/featureFlagsStore.ts | 23 +- frontend/app/mstore/filterStore.ts | 4 +- frontend/app/mstore/funnelStore.ts | 252 +- frontend/app/mstore/index.tsx | 23 +- frontend/app/mstore/integrationsStore.ts | 53 +- frontend/app/mstore/issueReportingStore.ts | 9 +- frontend/app/mstore/metricStore.ts | 66 +- frontend/app/mstore/notesStore.ts | 6 +- frontend/app/mstore/notificationStore.ts | 34 +- frontend/app/mstore/projectsStore.ts | 9 +- frontend/app/mstore/recordingsStore.ts | 3 +- frontend/app/mstore/roleStore.ts | 6 +- frontend/app/mstore/searchStore.ts | 21 +- frontend/app/mstore/searchStoreLive.ts | 46 +- frontend/app/mstore/sessionStore.ts | 128 +- frontend/app/mstore/settingsStore.ts | 31 +- frontend/app/mstore/spotStore.ts | 16 +- frontend/app/mstore/tagWatchStore.ts | 4 +- frontend/app/mstore/types/FeatureFlag.ts | 54 +- frontend/app/mstore/types/IconProvider.tsx | 24 +- frontend/app/mstore/types/audit.ts | 11 +- frontend/app/mstore/types/dashboard.ts | 38 +- frontend/app/mstore/types/filter.ts | 39 +- frontend/app/mstore/types/filterItem.ts | 26 +- frontend/app/mstore/types/filterSeries.ts | 9 +- frontend/app/mstore/types/funnel.ts | 29 +- frontend/app/mstore/types/funnelIssue.ts | 12 +- frontend/app/mstore/types/funnelStage.ts | 16 +- frontend/app/mstore/types/gettingStarted.ts | 10 +- .../app/mstore/types/integrations/consts.ts | 3 +- .../mstore/types/integrations/messengers.ts | 6 +- .../app/mstore/types/integrations/services.ts | 26 +- frontend/app/mstore/types/issue.ts | 72 +- frontend/app/mstore/types/moduleSettings.ts | 6 +- frontend/app/mstore/types/notification.ts | 8 +- frontend/app/mstore/types/role.ts | 6 +- frontend/app/mstore/types/search.ts | 40 +- frontend/app/mstore/types/session.ts | 19 +- frontend/app/mstore/types/sessionSettings.ts | 29 +- frontend/app/mstore/types/sessionsCardData.ts | 20 +- frontend/app/mstore/types/spot.ts | 4 +- frontend/app/mstore/types/user.ts | 10 +- frontend/app/mstore/types/widget.ts | 213 +- frontend/app/mstore/uiPlayerStore.ts | 5 +- frontend/app/mstore/userStore.ts | 97 +- frontend/app/mstore/uxtestingStore.ts | 122 +- .../app/mstore/weeklyReportConfigStore.ts | 4 +- frontend/app/player-ui/FullScreenButton.tsx | 9 +- frontend/app/player-ui/PlayButton.tsx | 4 +- frontend/app/player-ui/PlayTime.tsx | 16 +- frontend/app/player-ui/ProgressBar.tsx | 4 +- frontend/app/player/common/ListWalker.ts | 40 +- .../app/player/common/ListWalkerWithMarks.ts | 24 +- frontend/app/player/common/SimpleStore.ts | 15 +- frontend/app/player/common/common.d.ts | 8 +- frontend/app/player/common/tarball.ts | 5 +- frontend/app/player/common/types.ts | 64 +- frontend/app/player/common/unpack.ts | 3 +- frontend/app/player/create.ts | 24 +- frontend/app/player/mobile/IOSLists.ts | 91 +- .../app/player/mobile/IOSMessageManager.ts | 34 +- frontend/app/player/mobile/IOSPlayer.ts | 32 +- .../managers/IOSPerformanceTrackManager.ts | 15 +- .../player/mobile/managers/SnapshotManager.ts | 7 +- .../player/mobile/managers/TouchManager.ts | 13 +- frontend/app/player/mobile/utils.ts | 21 +- frontend/app/player/player/Animator.ts | 86 +- frontend/app/player/player/Player.ts | 16 +- frontend/app/player/player/_LSCache.ts | 17 +- frontend/app/player/web/Lists.ts | 17 +- frontend/app/player/web/MessageLoader.ts | 93 +- frontend/app/player/web/MessageManager.ts | 7 +- frontend/app/player/web/Screen/Cursor.ts | 2 +- frontend/app/player/web/Screen/Inspector.ts | 5 +- frontend/app/player/web/Screen/Marker.ts | 14 +- frontend/app/player/web/Screen/Screen.ts | 28 +- frontend/app/player/web/Screen/types.ts | 8 +- frontend/app/player/web/TabManager.ts | 48 +- frontend/app/player/web/WebLivePlayer.ts | 6 +- frontend/app/player/web/WebPlayer.ts | 59 +- .../player/web/addons/InspectorController.ts | 5 +- frontend/app/player/web/addons/MouseTrail.ts | 41 +- .../app/player/web/addons/TargetMarker.ts | 39 +- .../app/player/web/addons/clickmapStyles.ts | 24 +- .../app/player/web/assist/AnnotationCanvas.ts | 23 +- .../app/player/web/assist/AssistManager.ts | 75 +- frontend/app/player/web/assist/Call.ts | 238 +- .../app/player/web/assist/CanvasReceiver.ts | 74 +- frontend/app/player/web/assist/LocalStream.ts | 16 +- .../app/player/web/assist/RemoteControl.ts | 23 +- .../app/player/web/assist/ScreenRecording.ts | 42 +- frontend/app/player/web/assist/types.ts | 6 +- .../player/web/managers/ActivityManager.ts | 7 +- .../app/player/web/managers/DOM/DOMManager.ts | 69 +- .../player/web/managers/DOM/FocusManager.ts | 17 +- .../web/managers/DOM/SelectionManager.ts | 26 +- .../player/web/managers/DOM/StylesManager.ts | 16 +- .../app/player/web/managers/DOM/VirtualDOM.ts | 265 +- .../player/web/managers/DOM/safeCSSRules.ts | 2 +- .../player/web/managers/MouseMoveManager.ts | 13 +- .../web/managers/PerformanceTrackManager.ts | 38 +- .../player/web/managers/TabClosingManager.ts | 5 +- .../player/web/managers/WindowNodeCounter.ts | 4 +- .../web/messages/JSONRawMessageReader.ts | 11 +- .../app/player/web/messages/MFileReader.ts | 22 +- .../app/player/web/messages/MStreamReader.ts | 13 +- .../player/web/messages/PrimitiveReader.ts | 24 +- .../web/messages/RawMessageReader.gen.ts | 2822 +++++++++++------ .../app/player/web/messages/filters.gen.ts | 18 +- .../app/player/web/messages/message.gen.ts | 169 +- frontend/app/player/web/messages/raw.gen.ts | 770 +++-- .../web/messages/rewriter/rewriteMessage.ts | 28 +- frontend/app/player/web/messages/timed.ts | 4 +- .../app/player/web/messages/tracker.gen.ts | 707 ++--- frontend/app/player/web/network/crypto.ts | 14 +- frontend/app/player/web/network/loadFiles.ts | 24 +- frontend/app/player/web/storageSelectors.ts | 24 +- frontend/app/player/web/types/log.ts | 12 +- frontend/app/player/web/types/resource.ts | 94 +- frontend/app/routes.ts | 147 +- frontend/app/services/AiService.ts | 55 +- frontend/app/services/AlertsService.ts | 20 +- frontend/app/services/AssistStatsService.ts | 25 +- frontend/app/services/AuditService.ts | 6 +- frontend/app/services/ConfigService.ts | 20 +- frontend/app/services/CustomFieldService.ts | 30 +- frontend/app/services/DashboardService.ts | 111 +- frontend/app/services/ErrorService.ts | 4 +- frontend/app/services/FFlagsService.ts | 10 +- frontend/app/services/FunnelService.ts | 21 +- frontend/app/services/IntegrationsService.ts | 19 +- frontend/app/services/IssueReportsService.ts | 5 +- frontend/app/services/MetricService.ts | 63 +- frontend/app/services/NotesService.ts | 92 +- frontend/app/services/RecordingsService.ts | 35 +- frontend/app/services/SearchService.ts | 5 +- frontend/app/services/SessionService.ts | 29 +- frontend/app/services/UserService.ts | 20 +- frontend/app/services/UxtestingService.ts | 25 +- frontend/app/services/WebhookService.ts | 9 +- frontend/app/services/loginService.ts | 12 +- frontend/app/services/spotService.ts | 45 +- frontend/app/styles/colors-autogen.css | 5 + frontend/app/types/Record.js | 14 +- frontend/app/types/account/account.ts | 36 +- frontend/app/types/account/limit.ts | 8 +- frontend/app/types/alert.js | 0 frontend/app/types/alert.ts | 132 +- frontend/app/types/app/period.js | 8 +- frontend/app/types/customMetric.js | 8 +- frontend/app/types/dashboard/helper.ts | 16 +- frontend/app/types/filter/event.js | 6 +- frontend/app/types/filter/filter.js | 18 +- frontend/app/types/filter/filterType.ts | 2 +- frontend/app/types/filter/index.js | 12 +- frontend/app/types/filter/newFilter.js | 85 +- frontend/app/types/funnel.js | 14 +- frontend/app/types/funnelIssue.js | 3 +- .../app/types/integrations/bugsnagConfig.js | 3 +- .../types/integrations/cloudwatchConfig.js | 8 +- .../types/integrations/elasticsearchConfig.js | 20 +- .../app/types/integrations/sentryConfig.js | 6 +- frontend/app/types/member.ts | 47 +- frontend/app/types/notification.js | 19 +- frontend/app/types/session/activity.ts | 13 +- frontend/app/types/session/assignment.ts | 26 +- frontend/app/types/session/error.ts | 24 +- frontend/app/types/session/errorStack.ts | 14 +- frontend/app/types/session/event.ts | 33 +- frontend/app/types/session/issue.ts | 82 +- frontend/app/types/session/session.ts | 112 +- frontend/app/types/session/stackEvent.ts | 18 +- frontend/app/types/ts/search.ts | 18 +- frontend/app/types/watchdog.js | 9 +- frontend/app/types/webhook.ts | 17 +- frontend/app/utils/index.ts | 140 +- frontend/app/utils/screenRecorder.ts | 21 +- frontend/app/utils/search.ts | 46 +- frontend/app/validate.js | 20 +- frontend/app/window.d.ts | 15 - frontend/eslint.config.js | 186 +- frontend/package.json | 2 + frontend/translations.json | 1430 +++++++++ frontend/tsconfig.json | 9 +- frontend/window.d.ts | 18 + frontend/yarn.lock | 36 + 1615 files changed, 51106 insertions(+), 23044 deletions(-) create mode 100644 frontend/app/ErrorBoundary.tsx delete mode 100644 frontend/app/components/Alerts/AlertForm.js create mode 100644 frontend/app/components/Alerts/AlertForm.tsx rename frontend/app/components/Client/Integrations/{GithubForm.js => GithubForm.tsx} (54%) rename frontend/app/components/Client/Integrations/JiraForm/{JiraForm.js => JiraForm.tsx} (64%) rename frontend/app/components/Client/Integrations/ProfilerDoc/{ProfilerDoc.js => ProfilerDoc.tsx} (69%) rename frontend/app/components/Client/Integrations/SlackAddForm/{SlackAddForm.js => SlackAddForm.tsx} (74%) rename frontend/app/components/Client/Integrations/SlackChannelList/{SlackChannelList.js => SlackChannelList.tsx} (71%) rename frontend/app/components/Client/Integrations/Tracker/AssistDoc/{AssistDoc.js => AssistDoc.tsx} (55%) delete mode 100644 frontend/app/components/Client/Integrations/Tracker/GraphQLDoc/GraphQLDoc.js create mode 100644 frontend/app/components/Client/Integrations/Tracker/GraphQLDoc/GraphQLDoc.tsx rename frontend/app/components/Client/Integrations/Tracker/MobxDoc/{MobxDoc.js => MobxDoc.tsx} (55%) rename frontend/app/components/Client/Integrations/Tracker/NgRxDoc/{NgRxDoc.js => NgRxDoc.tsx} (57%) rename frontend/app/components/Client/Integrations/Tracker/ReduxDoc/{ReduxDoc.js => ReduxDoc.tsx} (55%) rename frontend/app/components/Client/Integrations/Tracker/VueDoc/{VueDoc.js => VueDoc.tsx} (57%) rename frontend/app/components/Client/Integrations/Tracker/ZustandDoc/{ZustandDoc.js => ZustandDoc.tsx} (63%) rename frontend/app/components/Client/ProfileSettings/{Api.js => Api.tsx} (79%) rename frontend/app/components/Client/ProfileSettings/{Licenses.js => Licenses.tsx} (69%) rename frontend/app/components/Client/ProfileSettings/{ProfileSettings.js => ProfileSettings.tsx} (62%) rename frontend/app/components/Client/ProfileSettings/{Settings.js => Settings.tsx} (71%) rename frontend/app/components/Client/ProfileSettings/{TenantKey.js => TenantKey.tsx} (73%) rename frontend/app/components/Client/Sites/{GDPRForm.js => GDPRForm.tsx} (64%) rename frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser/{Bar.js => Bar.tsx} (62%) rename frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SpeedIndexByLocation/{Scale.js => Scale.tsx} (53%) rename frontend/app/components/Dashboard/Widgets/common/{CustomTooltip.js => CustomTooltip.tsx} (65%) rename frontend/app/components/Errors/Error/{ErrorInfo.js => ErrorInfo.tsx} (85%) rename frontend/app/components/Errors/Error/{MainSection.js => MainSection.tsx} (86%) rename frontend/app/components/Errors/Error/{SideSection.js => SideSection.tsx} (83%) delete mode 100644 frontend/app/components/Funnels/FunnelOverview/FunnelOverview.js create mode 100644 frontend/app/components/Funnels/FunnelOverview/FunnelOverview.jsx delete mode 100644 frontend/app/components/Onboarding/components/MetadataList/MetadataList.js rename frontend/app/components/Session_/EventsBlock/Metadata/{SessionLine.js => SessionLine.tsx} (68%) rename frontend/app/components/Session_/EventsBlock/Metadata/{SessionList.js => SessionList.tsx} (70%) delete mode 100644 frontend/app/components/Session_/Fetch/FetchDetails.js create mode 100644 frontend/app/components/Session_/Fetch/FetchDetails.tsx rename frontend/app/components/shared/Filters/FilterDuration/{FilterDuration.js => FilterDuration.tsx} (95%) rename frontend/app/components/shared/IntegrateSlackButton/{IntegrateSlackButton.js => IntegrateSlackButton.tsx} (84%) rename frontend/app/components/shared/NoSessionsMessage/{NoSessionsMessage.js => NoSessionsMessage.tsx} (72%) delete mode 100644 frontend/app/components/shared/ResultTimings/ResultTimings.js create mode 100644 frontend/app/components/shared/ResultTimings/ResultTimings.tsx delete mode 100644 frontend/app/components/ui/NoContent/NoContent.js delete mode 100644 frontend/app/dev/components/CrashReactAppButton.js delete mode 100644 frontend/app/dev/components/ErrorGenPanel.tsx delete mode 100644 frontend/app/dev/components/EvalErrorBtn.js delete mode 100644 frontend/app/dev/components/EventErrorButton.js delete mode 100644 frontend/app/dev/components/InternalErrorButton.js delete mode 100644 frontend/app/dev/components/MemoryCrushButton.js delete mode 100644 frontend/app/dev/components/PromiseErrorButton.js create mode 100644 frontend/app/locales/es.json create mode 100644 frontend/app/locales/extractTranslations.js create mode 100644 frontend/app/locales/ru.json create mode 100644 frontend/app/locales/zh.json delete mode 100644 frontend/app/types/alert.js delete mode 100644 frontend/app/window.d.ts create mode 100644 frontend/translations.json create mode 100644 frontend/window.d.ts 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 e6f992916..a380834c6 100644 --- a/frontend/app/AdditionalRoutes.tsx +++ b/frontend/app/AdditionalRoutes.tsx @@ -7,9 +7,7 @@ interface Props { function AdditionalRoutes(props: Props) { const { redirect } = props; - return ( - - ); + return ; } export default AdditionalRoutes; diff --git a/frontend/app/ErrorBoundary.tsx b/frontend/app/ErrorBoundary.tsx new file mode 100644 index 000000000..c8d26d97b --- /dev/null +++ b/frontend/app/ErrorBoundary.tsx @@ -0,0 +1,31 @@ +import React, { Component } from 'react'; + +class ErrorBoundary extends Component { + constructor(props) { + super(props); + this.state = { hasError: false, error: null }; + } + + static getDerivedStateFromError(error) { + return { hasError: true, error }; + } + + componentDidCatch(error, errorInfo) { + console.error('Error caught:', error, errorInfo); + // Здесь можно отправить ошибку в сервис аналитики + } + + render() { + if (this.state.hasError) { + return this.props.fallback ? ( + this.props.fallback(this.state.error) + ) : ( +
Произошла ошибка: {this.state.error?.message}
+ ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/frontend/app/PrivateRoutes.tsx b/frontend/app/PrivateRoutes.tsx index 7ae942e07..2d0ae77c9 100644 --- a/frontend/app/PrivateRoutes.tsx +++ b/frontend/app/PrivateRoutes.tsx @@ -107,7 +107,11 @@ function PrivateRoutes() { const { siteId } = projectsStore; const hasRecordings = sites.some((s) => s.recorded); const redirectToSetup = scope === 0; - const redirectToOnboarding = !onboarding && (localStorage.getItem(GLOBAL_HAS_NO_RECORDINGS) === 'true' || (sites.length > 0 && !hasRecordings)) && scope > 0; + const redirectToOnboarding = + !onboarding && + (localStorage.getItem(GLOBAL_HAS_NO_RECORDINGS) === 'true' || + (sites.length > 0 && !hasRecordings)) && + scope > 0; const siteIdList: any = sites.map(({ id }) => id); React.useEffect(() => { diff --git a/frontend/app/PublicRoutes.tsx b/frontend/app/PublicRoutes.tsx index 94af9fcea..11dbbc754 100644 --- a/frontend/app/PublicRoutes.tsx +++ b/frontend/app/PublicRoutes.tsx @@ -13,14 +13,19 @@ const FORGOT_PASSWORD = routes.forgotPassword(); const SPOT_PATH = routes.spot(); const Login = lazy(() => 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; const { isEnterprise } = userStore; - const hideSupport = isEnterprise || location.pathname.includes('spots') || location.pathname.includes('view-spot'); + const hideSupport = + isEnterprise || + location.pathname.includes('spots') || + location.pathname.includes('view-spot'); const [loading, setLoading] = React.useState(true); useEffect(() => { @@ -36,7 +41,12 @@ function PublicRoutes() { }> - + diff --git a/frontend/app/Router.tsx b/frontend/app/Router.tsx index 76eb55ae9..f89d409d1 100644 --- a/frontend/app/Router.tsx +++ b/frontend/app/Router.tsx @@ -28,13 +28,14 @@ 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, + customFieldStore, + projectsStore, + sessionStore, + searchStore, + userStore, } = mstore; const { jwt } = userStore; const { changePassword } = userStore.account; @@ -50,7 +51,9 @@ const Router: React.FC = (props) => { 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; @@ -108,10 +111,10 @@ const Router: React.FC = (props) => { const destinationPath = localStorage.getItem(GLOBAL_DESTINATION_PATH); if ( - destinationPath - && !destinationPath.includes(routes.login()) - && !destinationPath.includes(routes.signup()) - && destinationPath !== '/' + destinationPath && + !destinationPath.includes(routes.login()) && + !destinationPath.includes(routes.signup()) && + destinationPath !== '/' ) { const url = new URL(destinationPath, window.location.origin); checkParams(url.search); @@ -192,12 +195,13 @@ const Router: React.FC = (props) => { const prevIsLoggedIn = usePrevious(isLoggedIn); const previousLocation = usePrevious(location); - const hideHeader = (location.pathname && location.pathname.includes('/session/')) - || location.pathname.includes('/assist/') - || location.pathname.includes('multiview') - || location.pathname.includes('/view-spot/') - || location.pathname.includes('/spots/') - || location.pathname.includes('/scope-setup'); + const hideHeader = + (location.pathname && location.pathname.includes('/session/')) || + location.pathname.includes('/assist/') || + location.pathname.includes('multiview') || + location.pathname.includes('/view-spot/') || + location.pathname.includes('/spots/') || + location.pathname.includes('/scope-setup'); if (isIframe) { return ( diff --git a/frontend/app/api_client.ts b/frontend/app/api_client.ts index 62c73ac83..467811f3d 100644 --- a/frontend/app/api_client.ts +++ b/frontend/app/api_client.ts @@ -33,7 +33,10 @@ const siteIdRequiredPaths: string[] = [ '/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); @@ -59,7 +62,7 @@ export default class APIClient { 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; @@ -78,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; } @@ -90,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({ @@ -162,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; @@ -172,16 +185,18 @@ export default class APIClient { delete init.body; } - const noChalice = path.includes('v1/integrations') || path.includes('/spot') && !path.includes('/login'); + const noChalice = + path.includes('v1/integrations') || + (path.includes('/spot') && !path.includes('/login')); let edp = window.env.API_EDP || `${window.location.origin}/api`; if (noChalice && !edp.includes('api.openreplay.com')) { edp = edp.replace('/api', ''); } if ( - path !== '/targets_temp' - && !path.includes('/metadata/session_search') - && !path.includes('/assist/credentials') - && siteIdRequiredPaths.some((sidPath) => path.startsWith(sidPath)) + path !== '/targets_temp' && + !path.includes('/metadata/session_search') && + !path.includes('/assist/credentials') && + siteIdRequiredPaths.some((sidPath) => path.startsWith(sidPath)) ) { edp = `${edp}/${this.siteId ?? ''}`; } @@ -208,9 +223,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'); @@ -227,12 +247,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 db6f50944..000000000 --- a/frontend/app/components/Alerts/AlertForm.js +++ /dev/null @@ -1,397 +0,0 @@ -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'; - -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' }, -]; - -function Circle({ text }) { - return ( -
- {text} -
- ); -} - -function Section({ - index, title, description, content, -}) { - return ( -
-
- -
- {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) => 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: '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 05a751919..096fa9e42 100644 --- a/frontend/app/components/Alerts/AlertFormModal/AlertFormModal.tsx +++ b/frontend/app/components/Alerts/AlertFormModal/AlertFormModal.tsx @@ -6,14 +6,14 @@ import { SLACK, TEAMS, WEBHOOK } from 'App/constants/schedule'; import AlertForm from '../AlertForm'; 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) { diff --git a/frontend/app/components/Alerts/DropdownChips/DropdownChips.js b/frontend/app/components/Alerts/DropdownChips/DropdownChips.js index a753fab1c..de2fde6a8 100644 --- a/frontend/app/components/Alerts/DropdownChips/DropdownChips.js +++ b/frontend/app/components/Alerts/DropdownChips/DropdownChips.js @@ -38,7 +38,16 @@ function DropdownChips({ const renderBadge = (item) => { const val = typeof item === 'string' ? item : item.value; const text = typeof item === 'string' ? item : item.label; - return onRemove(val)} outline />; + return ( + onRemove(val)} + outline + /> + ); }; return ( @@ -57,7 +66,9 @@ function DropdownChips({ /> )}
- {textFiled ? selected.map(renderBadge) : options.filter((i) => selected.includes(i.value)).map(renderBadge)} + {textFiled + ? selected.map(renderBadge) + : options.filter((i) => selected.includes(i.value)).map(renderBadge)}
); diff --git a/frontend/app/components/Assist/Assist.tsx b/frontend/app/components/Assist/Assist.tsx index 6784c6c56..91ea854d4 100644 --- a/frontend/app/components/Assist/Assist.tsx +++ b/frontend/app/components/Assist/Assist.tsx @@ -4,11 +4,14 @@ import withPermissions from 'HOCs/withPermissions'; import AssistRouter from './AssistRouter'; function Assist() { - return ( - - ); + return ; } export default withPageTitle('Assist - OpenReplay')( - withPermissions(['ASSIST_LIVE', 'SERVICE_ASSIST_LIVE'], '', false, false)(Assist), + withPermissions( + ['ASSIST_LIVE', 'SERVICE_ASSIST_LIVE'], + '', + false, + false, + )(Assist), ); diff --git a/frontend/app/components/Assist/AssistSearchActions/AssistSearchActions.tsx b/frontend/app/components/Assist/AssistSearchActions/AssistSearchActions.tsx index d38cff50b..70031b104 100644 --- a/frontend/app/components/Assist/AssistSearchActions/AssistSearchActions.tsx +++ b/frontend/app/components/Assist/AssistSearchActions/AssistSearchActions.tsx @@ -7,13 +7,17 @@ import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; import AssistStats from '../../AssistStats'; import Recordings from '../RecordingsList/Recordings'; +import { useTranslation } from 'react-i18next'; function AssistSearchActions() { + const { t } = useTranslation(); const { searchStoreLive, userStore } = useStore(); const modules = userStore.account.settings?.modules ?? []; const { isEnterprise } = userStore; - const hasEvents = searchStoreLive.instance.filters.filter((i: any) => i.isEvent).length > 0; - const hasFilters = searchStoreLive.instance.filters.filter((i: any) => !i.isEvent).length > 0; + const hasEvents = + searchStoreLive.instance.filters.filter((i: any) => i.isEvent).length > 0; + const hasFilters = + searchStoreLive.instance.filters.filter((i: any) => !i.isEvent).length > 0; const { showModal } = useModal(); const showStats = () => { @@ -24,25 +28,31 @@ function AssistSearchActions() { }; return (
- {isEnterprise && !modules.includes(MODULES.OFFLINE_RECORDINGS) - ? : null} + {isEnterprise && !modules.includes(MODULES.OFFLINE_RECORDINGS) ? ( + + ) : null} {isEnterprise && userStore.account?.admin && ( )} - +
diff --git a/frontend/app/components/Assist/ChatControls/ChatControls.tsx b/frontend/app/components/Assist/ChatControls/ChatControls.tsx index 171e91fd1..f69f2ec6d 100644 --- a/frontend/app/components/Assist/ChatControls/ChatControls.tsx +++ b/frontend/app/components/Assist/ChatControls/ChatControls.tsx @@ -6,26 +6,33 @@ import type { LocalStream } from 'Player'; import stl from './ChatControls.module.css'; interface Props { - stream: LocalStream | null, - endCall: () => void, - videoEnabled: boolean, - isPrestart?: boolean, - setVideoEnabled: (isEnabled: boolean) => void + stream: LocalStream | null; + endCall: () => void; + videoEnabled: boolean; + isPrestart?: boolean; + setVideoEnabled: (isEnabled: boolean) => void; } function ChatControls({ - stream, endCall, videoEnabled, setVideoEnabled, isPrestart, -} : Props) { + stream, + endCall, + videoEnabled, + setVideoEnabled, + isPrestart, +}: Props) { const [audioEnabled, setAudioEnabled] = useState(true); const toggleAudio = () => { - if (!stream) { return; } + if (!stream) { + return; + } setAudioEnabled(stream.toggleAudio()); }; const toggleVideo = () => { - if (!stream) { return; } - stream.toggleVideo() - .then((v) => setVideoEnabled(v)); + if (!stream) { + return; + } + stream.toggleVideo().then((v) => setVideoEnabled(v)); }; /** muting user if he is auto connected to the call */ @@ -36,17 +43,49 @@ function ChatControls({ }, []); return ( -
+
-
-
diff --git a/frontend/app/components/Assist/ChatWindow/ChatWindow.tsx b/frontend/app/components/Assist/ChatWindow/ChatWindow.tsx index 5597a6c59..b7fe24127 100644 --- a/frontend/app/components/Assist/ChatWindow/ChatWindow.tsx +++ b/frontend/app/components/Assist/ChatWindow/ChatWindow.tsx @@ -7,9 +7,10 @@ import { PlayerContext } from 'App/components/Session/playerContext'; import ChatControls from '../ChatControls/ChatControls'; import stl from './chatWindow.module.css'; import VideoContainer from '../components/VideoContainer'; +import { useTranslation } from 'react-i18next'; export interface Props { - incomeStream: { stream: MediaStream, isAgent: boolean }[] | null; + incomeStream: { stream: MediaStream; isAgent: boolean }[] | null; localStream: LocalStream | null; userId: string; isPrestart?: boolean; @@ -17,8 +18,13 @@ export interface Props { } function ChatWindow({ - userId, incomeStream, localStream, endCall, isPrestart, + userId, + incomeStream, + localStream, + endCall, + isPrestart, }: Props) { + const { t } = useTranslation(); const { player } = React.useContext(PlayerContext); const { toggleVideoLocalStream } = player.assistManager; @@ -33,20 +39,27 @@ function ChatWindow({ }, [localVideoEnabled]); return ( - +
- Call with - {' '} - {userId || 'Anonymous User'} + {t('Call with')}  {userId || t('Anonymous User')}
- {incomeStream && incomeStream.length > 2 ? ' (+ other agents in the call)' : ''} + {incomeStream && incomeStream.length > 2 + ? t(' (+ other agents in the call)') + : ''}
- +
( - + )) ) : ( -
Error obtaining incoming streams
+
+ {t('Error obtaining incoming streams')} +
)} -
+
void; - onSave: (title: string) => void; + show: boolean; + title: string; + closeHandler?: () => void; + onSave: (title: string) => void; } function EditRecordingModal(props: Props) { - const { - show, closeHandler, title, onSave, - } = props; + const { t } = useTranslation(); + const { show, closeHandler, title, onSave } = props; const [text, setText] = React.useState(title); React.useEffect(() => { @@ -33,20 +31,16 @@ function EditRecordingModal(props: Props) { return useObserver(() => ( -
Rename
+
{t('Rename')}
- +
- +
- + -
diff --git a/frontend/app/components/Assist/RecordingsList/Recordings.tsx b/frontend/app/components/Assist/RecordingsList/Recordings.tsx index 4a8b36048..3de24c7c3 100644 --- a/frontend/app/components/Assist/RecordingsList/Recordings.tsx +++ b/frontend/app/components/Assist/RecordingsList/Recordings.tsx @@ -21,13 +21,20 @@ function Recordings() { }; return ( -
+
- + { 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 498708e77..bae8359ec 100644 --- a/frontend/app/components/Assist/RequestingWindow/RequestingWindow.tsx +++ b/frontend/app/components/Assist/RequestingWindow/RequestingWindow.tsx @@ -5,6 +5,8 @@ import { Button } from 'antd'; import { PlayerContext } from 'App/components/Session/playerContext'; 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,28 +25,29 @@ 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; const windowType = getWindowType(); @@ -52,11 +55,7 @@ function RequestingWindow({ getWindowType }: Props) { const { player } = React.useContext(PlayerContext); const { - assistManager: { - initiateCallEnd, - releaseRemoteControl, - stopRecording, - }, + assistManager: { initiateCallEnd, releaseRemoteControl, stopRecording }, } = player; const actions = { @@ -67,19 +66,29 @@ function RequestingWindow({ getWindowType }: Props) { return (
- +
- Waiting for - {' '} + {t('Waiting for')}{' '} {userDisplayName}
- {WIN_VARIANTS[windowType].text} + {WIN_VARIANTS(t)[windowType].text} -
diff --git a/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx b/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx index d58ca274c..37f3fb5e3 100644 --- a/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx +++ b/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx @@ -2,10 +2,16 @@ import React, { useState, useEffect } from 'react'; import { Button } from 'antd'; import cn from 'classnames'; import { - CallingState, ConnectionStatus, RemoteControlStatus, RequestLocalStream, + 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'; @@ -14,17 +20,7 @@ import { audioContextManager } from 'App/utils/screenRecorder'; import { useStore } from 'App/mstore'; import stl from './AassistActions.module.css'; import ChatWindow from '../../ChatWindow'; - -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 { useTranslation } from 'react-i18next'; function onError(e: any) { console.log(e); @@ -49,16 +45,15 @@ const AssistActionsPing = { }, } 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 hasPermission = + permissions.includes('ASSIST_CALL') || + permissions.includes('SERVICE_ASSIST_CALL'); const { isEnterprise } = userStore; const agentId = userStore.account.id; const { userDisplayName } = sessionStore.current; @@ -82,15 +77,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); + const cannotCall = + peerConnectionStatus !== ConnectionStatus.Connected || + (isEnterprise && !hasPermission); - const remoteRequesting = remoteControlStatus === RemoteControlStatus.Requesting; + const remoteRequesting = + remoteControlStatus === RemoteControlStatus.Requesting; const remoteActive = remoteControlStatus === RemoteControlStatus.Enabled; useEffect(() => { @@ -126,14 +129,18 @@ function AssistActions({ 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 }]; } @@ -144,10 +151,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) => { @@ -157,7 +178,7 @@ 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); }, @@ -177,9 +198,9 @@ function AssistActions({ if ( await confirm({ - header: 'Start Call', - confirmButton: 'Call', - confirmation: `Are you sure you want to call ${userId || 'User'}?`, + header: t('Start Call'), + confirmButton: t('Call'), + confirmation: `${t('Are you sure you want to call')} ${userId || t('User')}?`, }) ) { call(agentIds); @@ -220,12 +241,14 @@ function AssistActions({ role="button" >
@@ -240,18 +263,24 @@ function AssistActions({
@@ -260,8 +289,8 @@ function AssistActions({ @@ -275,10 +304,12 @@ function AssistActions({
diff --git a/frontend/app/components/Assist/components/SessionList/SessionList.tsx b/frontend/app/components/Assist/components/SessionList/SessionList.tsx index 935b460e4..3a95ee5d1 100644 --- a/frontend/app/components/Assist/components/SessionList/SessionList.tsx +++ b/frontend/app/components/Assist/components/SessionList/SessionList.tsx @@ -1,3 +1,4 @@ +/* eslint-disable i18next/no-literal-string */ import React, { useEffect } from 'react'; import { observer } from 'mobx-react-lite'; import { useStore } from 'App/mstore'; @@ -5,19 +6,25 @@ 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 { 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 list = sessionStore.liveSessions.filter( + (i: any) => + i.userId === session.userId && i.sessionId !== session.sessionId, + ); const loading = sessionStore.loadingLiveSessions; useEffect(() => { const params: any = {}; @@ -31,38 +38,44 @@ function SessionList(props: Props) {
{props.userId} - 's - Live Sessions - {' '} + 's + {t('Live Sessions')}{' '}
-
No live sessions found.
+
+ {t('No live sessions found.')} +
- )} + } >
{list.map((session: any) => (
{session.pageTitle && session.pageTitle !== '' && ( -
- - {session.pageTitle} -
+
+ + + {session.pageTitle} + +
)}
diff --git a/frontend/app/components/Assist/components/VideoContainer/VideoContainer.tsx b/frontend/app/components/Assist/components/VideoContainer/VideoContainer.tsx index a0b7ca8d9..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); @@ -34,9 +36,9 @@ function VideoContainer({ const track = stream.getVideoTracks()[0]; const settings = track?.getSettings(); const isDummyVideoTrack = settings - ? settings.width === 2 - || settings.frameRate === 0 - || (!settings.frameRate && !settings.width) + ? settings.width === 2 || + settings.frameRate === 0 || + (!settings.frameRate && !settings.width) : true; const shouldBeEnabled = track.enabled && !isDummyVideoTrack; @@ -59,14 +61,14 @@ function VideoContainer({ transform: local ? 'scaleX(-1)' : undefined, }} > -
diff --git a/frontend/app/components/AssistStats/AssistStats.tsx b/frontend/app/components/AssistStats/AssistStats.tsx index c667c0f1f..9a4ac190a 100644 --- a/frontend/app/components/AssistStats/AssistStats.tsx +++ b/frontend/app/components/AssistStats/AssistStats.tsx @@ -21,26 +21,34 @@ import { getPdf2 } from 'Components/AssistStats/pdfGenerator'; import UserSearch from './components/UserSearch'; import Chart from './components/Charts'; import StatsTable from './components/Table'; +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, }); @@ -148,7 +156,8 @@ function AssistStats() { order: 'desc', page: 1, limit: 10000, - }).then((sessions) => { + }) + .then((sessions) => { const data = sessions.list.map((s) => ({ ...s, members: `"${s.teamMembers.map((m) => m.name).join(', ')}"`, @@ -158,15 +167,19 @@ function AssistStats() { 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' }, + { 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()}`, + ); }); }; @@ -204,16 +217,32 @@ function AssistStats() { return (
-
+
- Co-browsing Reports + {t('Co-browsing Reports')}
- - + +
} + title={ +
{t('No data available')}
+ } show={data && data.length === 0} style={{ height: '100px' }} > diff --git a/frontend/app/components/AssistStats/components/Table.tsx b/frontend/app/components/AssistStats/components/Table.tsx index a9bbe8304..8875425c9 100644 --- a/frontend/app/components/AssistStats/components/Table.tsx +++ b/frontend/app/components/AssistStats/components/Table.tsx @@ -1,15 +1,26 @@ -import { DownOutlined, CloudDownloadOutlined, TableOutlined } 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 { Button, Dropdown, Space, Typography, Tooltip } from 'antd'; 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; @@ -21,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', @@ -45,12 +56,18 @@ const sortItems = [ ]; function StatsTable({ - onSort, isLoading, onPageChange, page, sessions, exportCSV, + onSort, + isLoading, + onPageChange, + page, + sessions, + exportCSV, }: Props) { - const [sortValue, setSort] = React.useState(sortItems[0].label); + 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); }; @@ -58,10 +75,10 @@ function StatsTable({
- 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
} + title={ +
+ {t('No data available')} +
+ } show={sessions.list && sessions.list.length === 0} style={{ height: '100px' }} > @@ -103,35 +124,24 @@ function StatsTable({
{sessions.total > 0 ? (
- Showing - {' '} + {t('Showing')}{' '} {(page - 1) * PER_PAGE + 1} - {' '} - to - {' '} - {(page - 1) * PER_PAGE + sessions.list.length} - {' '} - of - {' '} - {numberWithCommas(sessions.total)} - {' '} - sessions. +  {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} +
))}
@@ -172,28 +186,52 @@ function Row({ session }: { session: AssistStatsSession }) { key: recording.recordId, label: recording.name.slice(0, 20), })), - onClick: (item) => recordingsService.fetchRecording(item.key as unknown as number), + onClick: (item) => + recordingsService.fetchRecording( + item.key as unknown as number, + ), }} > - + ) : (
recordingsService.fetchRecording(session.recordings[0].recordId)} + 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 7dc7da4f5..6d61fb69b 100644 --- a/frontend/app/components/AssistStats/components/TeamMembers.tsx +++ b/frontend/app/components/AssistStats/components/TeamMembers.tsx @@ -1,28 +1,28 @@ import { DownOutlined, TableOutlined } from '@ant-design/icons'; -import { - Button, Dropdown, Space, Typography, Tooltip, -} from 'antd'; +import { Button, Dropdown, Space, Typography, Tooltip } from 'antd'; import { durationFromMsFormatted } from 'App/date'; import { Member } from 'App/services/AssistStatsService'; import { getInitials, exportCSVFile } from 'App/utils'; +import { TFunction } from 'i18next'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import { Loader, NoContent } from 'UI'; -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', }, ]; @@ -38,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) => ({ @@ -62,14 +63,18 @@ 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')}
@@ -80,7 +85,13 @@ function TeamMembers({ - +
- + No data available
} + 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}
@@ -117,7 +141,7 @@ 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 3765f3924..f2ad84a96 100644 --- a/frontend/app/components/AssistStats/components/UserSearch.tsx +++ b/frontend/app/components/AssistStats/components/UserSearch.tsx @@ -5,7 +5,9 @@ import { observer } from 'mobx-react-lite'; import { useStore } from 'App/mstore'; function UserSearch({ onUserSelect }: { onUserSelect: (id: any) => void }) { - const [selectedValue, setSelectedValue] = useState(undefined); + const [selectedValue, setSelectedValue] = useState( + undefined, + ); const { userStore } = useStore(); const allUsers = userStore.list.map((user) => ({ value: user.userId, @@ -28,7 +30,11 @@ function 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()), + ) + : [], ); }; diff --git a/frontend/app/components/AssistStats/pdfGenerator.ts b/frontend/app/components/AssistStats/pdfGenerator.ts index e8bbf0b5c..628b5dd90 100644 --- a/frontend/app/components/AssistStats/pdfGenerator.ts +++ b/frontend/app/components/AssistStats/pdfGenerator.ts @@ -38,7 +38,14 @@ 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); + 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', diff --git a/frontend/app/components/Charts/BarChart.tsx b/frontend/app/components/Charts/BarChart.tsx index f2666dbfb..9e81eb43d 100644 --- a/frontend/app/components/Charts/BarChart.tsx +++ b/frontend/app/components/Charts/BarChart.tsx @@ -1,10 +1,6 @@ import React from 'react'; import { BarChart } from 'echarts/charts'; -import { - DataProps, - buildCategories, - customTooltipFormatter, -} from './utils'; +import { DataProps, buildCategories, customTooltipFormatter } from './utils'; import { buildBarDatasetsAndSeries } from './barUtils'; import { defaultOptions, echarts, initWindowStorages } from './init'; @@ -17,7 +13,9 @@ 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(() => { @@ -29,9 +27,15 @@ function ORBarChart(props: BarChartProps) { 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,7 +45,8 @@ 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]; }); }); @@ -61,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, @@ -79,7 +86,9 @@ function ORBarChart(props: BarChartProps) { }); chart.on('click', (event) => { const index = event.dataIndex; - const timestamp = (window as any).__timestampMap?.[chartUuid.current]?.[index]; + const timestamp = (window as any).__timestampMap?.[chartUuid.current]?.[ + index + ]; props.onClick?.({ activePayload: [{ payload: { timestamp } }] }); setTimeout(() => { props.onSeriesFocus?.(event.seriesName); diff --git a/frontend/app/components/Charts/ColumnChart.tsx b/frontend/app/components/Charts/ColumnChart.tsx index b19d84abe..5a8f678c5 100644 --- a/frontend/app/components/Charts/ColumnChart.tsx +++ b/frontend/app/components/Charts/ColumnChart.tsx @@ -44,7 +44,11 @@ function ColumnChart(props: ColumnChartProps) { (window as any).__seriesColorMap[chartUuid.current] = {}; (window as any).__yAxisData = (window as any).__yAxisData ?? {}; - const { yAxisData, series } = buildColumnChart(chartUuid.current, data, compData); + const { yAxisData, series } = buildColumnChart( + chartUuid.current, + data, + compData, + ); (window as any).__yAxisData[chartUuid.current] = yAxisData; chart.setOption({ diff --git a/frontend/app/components/Charts/LineChart.tsx b/frontend/app/components/Charts/LineChart.tsx index da73fa7f2..93e505b23 100644 --- a/frontend/app/components/Charts/LineChart.tsx +++ b/frontend/app/components/Charts/LineChart.tsx @@ -1,7 +1,11 @@ import React from 'react'; import { LineChart } from 'echarts/charts'; import { echarts, defaultOptions, initWindowStorages } from './init'; -import { customTooltipFormatter, buildCategories, buildDatasetsAndSeries } from './utils'; +import { + customTooltipFormatter, + buildCategories, + buildDatasetsAndSeries, +} from './utils'; import type { DataProps } from './utils'; echarts.use([LineChart]); @@ -16,7 +20,9 @@ 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(() => { @@ -28,7 +34,12 @@ function ORLineChart(props: Props) { 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,7 +60,8 @@ 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]; }); }); @@ -61,7 +74,9 @@ function ORLineChart(props: Props) { 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', @@ -91,7 +106,9 @@ function ORLineChart(props: Props) { }); chart.on('click', (event) => { const index = event.dataIndex; - const timestamp = (window as any).__timestampMap?.[chartUuid.current]?.[index]; + const timestamp = (window as any).__timestampMap?.[chartUuid.current]?.[ + index + ]; props.onClick?.({ activePayload: [{ payload: { timestamp } }] }); setTimeout(() => { props.onSeriesFocus?.(event.seriesName); diff --git a/frontend/app/components/Charts/PieChart.tsx b/frontend/app/components/Charts/PieChart.tsx index 1e86da7ab..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]); @@ -22,15 +26,14 @@ interface PieChartProps { } function PieChart(props: PieChartProps) { - const { - data, label, onClick = () => {}, inGrid = false, - } = props; + const { data, label, onClick = () => {}, inGrid = false } = props; const chartRef = useRef(null); 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; } @@ -38,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; } @@ -119,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 a0c7e9c8e..b6e697ead 100644 --- a/frontend/app/components/Charts/SankeyChart.tsx +++ b/frontend/app/components/Charts/SankeyChart.tsx @@ -4,6 +4,7 @@ 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,22 +37,22 @@ interface Props { } const EChartsSankey: React.FC = (props) => { - const { - data, height = 240, onChartClick, isUngrouped, - } = props; + const { t } = useTranslation(); + const { data, height = 240, onChartClick, isUngrouped } = props; const chartRef = React.useRef(null); if (data.nodes.length === 0 || data.links.length === 0) { return ( - Set a start or end point to visualize the journey. If set, try - adjusting filters. + {t( + 'Set a start or end point to visualize the journey. If set, try adjusting filters.', + )}
- )} + } show /> ); @@ -73,8 +74,8 @@ const EChartsSankey: React.FC = (props) => { const sourceNode = finalNodes.find((n) => n.id === l.source); const targetNode = finalNodes.find((n) => n.id === l.target); return ( - (sourceNode?.depth ?? 0) <= maxDepth - && (targetNode?.depth ?? 0) <= maxDepth + (sourceNode?.depth ?? 0) <= maxDepth && + (targetNode?.depth ?? 0) <= maxDepth ); }); @@ -91,11 +92,12 @@ const EChartsSankey: React.FC = (props) => { } else { nodeValues[i] = 0; } - const itemColor = computedName === 'Others' - ? 'rgba(34,44,154,.9)' - : n.eventType === 'DROP' - ? '#B5B7C8' - : '#394eff'; + const itemColor = + computedName === 'Others' + ? 'rgba(34,44,154,.9)' + : n.eventType === 'DROP' + ? '#B5B7C8' + : '#394eff'; return { name: computedName, @@ -181,17 +183,19 @@ const EChartsSankey: React.FC = (props) => { : '0%'; const maxLen = 20; - const safeName = params.name.length > maxLen - ? `${params.name.slice(0, maxLen / 2 - 2) - }...${ - params.name.slice(-(maxLen / 2 - 2))}` - : params.name; + const safeName = + params.name.length > maxLen + ? `${params.name.slice( + 0, + maxLen / 2 - 2, + )}...${params.name.slice(-(maxLen / 2 - 2))}` + : params.name; const nodeType = params.data.type; const icon = getIcon(nodeType); return ( - `${icon}{header| ${safeName}}\n` - + `{body|}{percentage|${percentage}} {sessions|${nodeVal}}` + `${icon}{header| ${safeName}}\n` + + `{body|}{percentage|${percentage}} {sessions|${nodeVal}}` ); }, rich: { @@ -223,42 +227,48 @@ 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, @@ -324,9 +334,10 @@ const EChartsSankey: React.FC = (props) => { const updatedNodes = echartNodes.map((node, idx) => { const baseOpacity = connectedChain.has(idx) ? 1 : 0.35; - const extraStyle = idx === hoveredIndex - ? { borderColor: '#000', borderWidth: 1, borderType: 'dotted' } - : {}; + const extraStyle = + idx === hoveredIndex + ? { borderColor: '#000', borderWidth: 1, borderType: 'dotted' } + : {}; return { ...node, itemStyle: { @@ -401,8 +412,8 @@ const EChartsSankey: React.FC = (props) => { const firstNodeType = firstNode?.eventType?.toLowerCase() ?? 'location'; const lastNodeType = lastNode?.eventType?.toLowerCase() ?? 'location'; if ( - unsupported.includes(firstNodeType) - || unsupported.includes(lastNodeType) + unsupported.includes(firstNodeType) || + unsupported.includes(lastNodeType) ) { return; } @@ -455,15 +466,15 @@ const EChartsSankey: React.FC = (props) => { } return ( -
-
+
); }; diff --git a/frontend/app/components/Charts/barUtils.ts b/frontend/app/components/Charts/barUtils.ts index 8dd7f197f..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, @@ -113,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 f840bc454..3869ab4ca 100644 --- a/frontend/app/components/Charts/init.ts +++ b/frontend/app/components/Charts/init.ts @@ -67,7 +67,12 @@ const defaultOptions = { }, }; -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,10 +89,14 @@ 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, + ); } } diff --git a/frontend/app/components/Charts/pieUtils.ts b/frontend/app/components/Charts/pieUtils.ts index 0d441e480..96567b0ec 100644 --- a/frontend/app/components/Charts/pieUtils.ts +++ b/frontend/app/components/Charts/pieUtils.ts @@ -16,9 +16,7 @@ export function buildPieData( } export function pieTooltipFormatter(params: any) { - const { - name, value, marker, percent, - } = params; + const { name, value, marker, percent } = params; return `
${marker} ${name}
diff --git a/frontend/app/components/Charts/sankeyUtils.ts b/frontend/app/components/Charts/sankeyUtils.ts index 56fa2ea60..56b9947c0 100644 --- a/frontend/app/components/Charts/sankeyUtils.ts +++ b/frontend/app/components/Charts/sankeyUtils.ts @@ -25,8 +25,8 @@ export function sankeyTooltip(
${params.data.value} ( ${params.data.percentage.toFixed( - 2, -)}% ) + 2, + )}% ) Sessions
@@ -49,12 +49,13 @@ const shortenString = (str: string) => { const limit = 60; const leftPart = 25; const rightPart = 20; - const safeStr = str.length > limit - ? `${str.slice(0, leftPart)}...${str.slice( - str.length - rightPart, - str.length, - )}` - : str; + const safeStr = + str.length > limit + ? `${str.slice(0, leftPart)}...${str.slice( + str.length - rightPart, + str.length, + )}` + : str; return safeStr; }; diff --git a/frontend/app/components/Charts/utils.ts b/frontend/app/components/Charts/utils.ts index 1b63f5a71..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 ]; @@ -112,8 +111,8 @@ export function customTooltipFormatter(uuid: string) {
+ params.color + };" class="flex flex-col px-2 ml-2">
Total:
@@ -124,7 +123,8 @@ export function customTooltipFormatter(uuid: string) {
`; if (partnerValue !== undefined) { - const partnerColor = (window as any).__seriesColorMap?.[uuid]?.[partnerName] || '#999'; + const partnerColor = + (window as any).__seriesColorMap?.[uuid]?.[partnerName] || '#999'; str += `
@@ -177,8 +177,8 @@ export function customTooltipFormatter(uuid: string) {
+ params.color + };" class="flex flex-col px-2 ml-2">
${firstTs ? formatTimeOrDate(firstTs) : categoryLabel}
@@ -190,7 +190,8 @@ export function customTooltipFormatter(uuid: string) { `; if (partnerVal !== undefined) { - const partnerColor = (window as any).__seriesColorMap?.[uuid]?.[partnerName] || '#999'; + const partnerColor = + (window as any).__seriesColorMap?.[uuid]?.[partnerName] || '#999'; tooltipContent += `
@@ -260,7 +261,8 @@ export function createDataset(id: string, data: DataProps['data']) { const source = data.chart.map((item, idx) => { const row: (number | undefined)[] = [idx]; data.namesMap.forEach((name) => { - const val = typeof item[name] === 'number' ? (item[name] as number) : undefined; + const val = + typeof item[name] === 'number' ? (item[name] as number) : undefined; row.push(val); }); return row; diff --git a/frontend/app/components/Client/Audit/AuditDetailModal/AuditDetailModal.tsx b/frontend/app/components/Client/Audit/AuditDetailModal/AuditDetailModal.tsx index f6230296b..756f76009 100644 --- a/frontend/app/components/Client/Audit/AuditDetailModal/AuditDetailModal.tsx +++ b/frontend/app/components/Client/Audit/AuditDetailModal/AuditDetailModal.tsx @@ -1,49 +1,56 @@ 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 { t } = useTranslation(); const { audit } = props; // const jsonResponse = typeof audit.payload === 'string' ? JSON.parse(audit.payload) : audit.payload; // console.log('jsonResponse', jsonResponse) return (
-

Audit Details

+

{t('Audit Details')}

-
URL
-
{ audit.endPoint }
+
{t('URL')}
+
+ {audit.endPoint} +
-
Username
+
{t('Username')}
{audit.username}
-
Created At
-
{audit.createdAt && checkForRecent(audit.createdAt, 'LLL dd, yyyy, hh:mm a')}
+
{t('Created At')}
+
+ {audit.createdAt && + checkForRecent(audit.createdAt, 'LLL dd, yyyy, hh:mm a')} +
-
Action
+
{t('Action')}
{audit.action}
-
Method
+
{t('Method')}
{audit.method}
- { audit.payload && ( -
-
Payload
- -
+ {audit.payload && ( +
+
{t('Payload')}
+ +
)}
diff --git a/frontend/app/components/Client/Audit/AuditList/AuditList.tsx b/frontend/app/components/Client/Audit/AuditList/AuditList.tsx index 1f2fb4b83..c9a38c8ef 100644 --- a/frontend/app/components/Client/Audit/AuditList/AuditList.tsx +++ b/frontend/app/components/Client/Audit/AuditList/AuditList.tsx @@ -6,11 +6,11 @@ import { Loader, Pagination, NoContent } from 'UI'; import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; import AuditDetailModal from '../AuditDetailModal'; import AuditListItem from '../AuditListItem'; +import { useTranslation } from 'react-i18next'; -interface Props { - -} +interface Props {} function AuditList(props: Props) { + const { t } = useTranslation(); const { auditStore } = useStore(); const loading = useObserver(() => auditStore.isLoading); const list = useObserver(() => auditStore.list); @@ -35,26 +35,31 @@ function AuditList(props: Props) { return useObserver(() => ( -
No data available
+
{t('No data available')}
- )} + } size="small" show={list.length === 0} >
-
Name
-
Action
-
Time
+
{t('Name')}
+
{t('Action')}
+
{t('Time')}
{list.map((item, index) => ( showModal(, { right: true, width: 500 })} + onShowDetails={() => + showModal(, { + right: true, + width: 500, + }) + } /> ))} diff --git a/frontend/app/components/Client/Audit/AuditListItem/AuditListItem.tsx b/frontend/app/components/Client/Audit/AuditListItem/AuditListItem.tsx index 993e9dbf6..064532a89 100644 --- a/frontend/app/components/Client/Audit/AuditListItem/AuditListItem.tsx +++ b/frontend/app/components/Client/Audit/AuditListItem/AuditListItem.tsx @@ -2,16 +2,24 @@ 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')}
+
+ {audit.action} +
+
+ {audit.createdAt && + checkForRecent(audit.createdAt, 'LLL dd, yyyy, hh:mm a')} +
); } diff --git a/frontend/app/components/Client/Audit/AuditSearchField/AuditSearchField.tsx b/frontend/app/components/Client/Audit/AuditSearchField/AuditSearchField.tsx index 0dc368f6e..7272370c6 100644 --- a/frontend/app/components/Client/Audit/AuditSearchField/AuditSearchField.tsx +++ b/frontend/app/components/Client/Audit/AuditSearchField/AuditSearchField.tsx @@ -4,7 +4,7 @@ import { debounce } from 'App/utils'; let debounceUpdate: any = () => {}; interface Props { - onChange: (value: string) => void; + onChange: (value: string) => void; } function AuditSearchField(props: Props) { const { onChange } = props; @@ -19,10 +19,14 @@ function AuditSearchField(props: Props) { return (
- + auditStore.order); const total = useObserver(() => numberWithCommas(auditStore.total)); - useEffect(() => () => { - auditStore.updateKey('searchQuery', ''); - }, []); + useEffect( + () => () => { + auditStore.updateKey('searchQuery', ''); + }, + [], + ); const exportToCsv = () => { auditStore.exportToCsv(); @@ -30,12 +35,13 @@ function AuditView() { return useObserver(() => (
- - Audit Trail - {total} -
- )} + + {t('Audit Trail')} + {total} +
+ } />
@@ -48,22 +54,30 @@ function AuditView() {
@@ -78,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 85061f84e..5330eda01 100644 --- a/frontend/app/components/Client/CustomFields/CustomFields.tsx +++ b/frontend/app/components/Client/CustomFields/CustomFields.tsx @@ -3,19 +3,19 @@ import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; import { useModal } from 'App/components/Modal'; import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; -import { - List, Space, Typography, Button, Tooltip, Empty, -} from 'antd'; +import { List, Space, Typography, Button, Tooltip, Empty } from 'antd'; import { PlusIcon, Tags } from 'lucide-react'; import { EditOutlined } from '@ant-design/icons'; import usePageTitle from '@/hooks/usePageTitle'; import CustomFieldForm from './CustomFieldForm'; +import { useTranslation } from 'react-i18next'; 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); @@ -29,7 +29,8 @@ function CustomFields() { const handleInit = (field?: any) => { store.init(field); showModal(, { - title: field ? 'Edit Metadata' : 'Add Metadata', right: true, + title: field ? t('Edit Metadata') : t('Add Metadata'), + right: true, }); }; @@ -38,16 +39,24 @@ function CustomFields() { return (
- Attach key-value pairs to session replays for enhanced filtering, searching, and identifying relevant user - sessions. - - Learn more + {t( + 'Attach key-value pairs to session replays for enhanced filtering, searching, and identifying relevant user sessions.', + )} + + {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 ? '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} @@ -76,13 +92,14 @@ function CustomFields() { onClick={() => handleInit(field)} className="cursor-pointer group hover:bg-active-blue !px-4" actions={[ -
diff --git a/frontend/app/components/Client/DebugLog.tsx b/frontend/app/components/Client/DebugLog.tsx index 006080bd7..84c8ce18f 100644 --- a/frontend/app/components/Client/DebugLog.tsx +++ b/frontend/app/components/Client/DebugLog.tsx @@ -1,6 +1,7 @@ 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); @@ -12,6 +13,7 @@ function getDefaults() { } function DebugLog() { + const { t } = useTranslation(); const [showLogs, setShowLogs] = React.useState(getDefaults); const onChange = (checked: boolean) => { @@ -20,8 +22,10 @@ function DebugLog() { }; return (
-

Player Debug Logs

-
Show debug information in browser console.
+

{t('Player Debug Logs')}

+
+ {t('Show debug information in browser console.')} +
diff --git a/frontend/app/components/Client/Integrations/Backend/DatadogForm/DatadogFormModal.tsx b/frontend/app/components/Client/Integrations/Backend/DatadogForm/DatadogFormModal.tsx index 1a5f04dd0..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; @@ -30,6 +31,7 @@ function DatadogFormModal({ onClose: () => void; integrated: boolean; }) { + const { t } = useTranslation(); const { integrationsStore } = useStore(); const { siteId } = integrationsStore.integrations; @@ -39,19 +41,20 @@ function 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 () => { @@ -85,20 +88,20 @@ function 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')}
- {exists ? 'Update' : 'Add'} + {exists ? t('Update') : t('Add')} {integrated && ( )}
diff --git a/frontend/app/components/Client/Integrations/Backend/DynatraceForm/DynatraceFormModal.tsx b/frontend/app/components/Client/Integrations/Backend/DynatraceForm/DynatraceFormModal.tsx index 3054b5cd4..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; @@ -31,6 +32,7 @@ function DynatraceFormModal({ onClose: () => void; integrated: boolean; }) { + const { t } = useTranslation(); const { integrationsStore } = useStore(); const { siteId } = integrationsStore.integrations; const { @@ -39,22 +41,23 @@ function DynatraceFormModal({ 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 () => { @@ -83,33 +86,40 @@ function 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.", + )}
- {exists ? 'Update' : 'Add'} + {exists ? t('Update') : t('Add')} {integrated && ( )}
diff --git a/frontend/app/components/Client/Integrations/Backend/ElasticForm/ElasticFormModal.tsx b/frontend/app/components/Client/Integrations/Backend/ElasticForm/ElasticFormModal.tsx index 3feb1087c..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,6 +33,7 @@ function ElasticsearchForm({ onClose: () => void; integrated: boolean; }) { + const { t } = useTranslation(); const { integrationsStore } = useStore(); const { siteId } = integrationsStore.integrations; const { @@ -40,19 +42,20 @@ function ElasticsearchForm({ 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 () => { @@ -83,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')}
- {exists ? 'Update' : 'Add'} + {exists ? t('Update') : t('Add')} {integrated && ( )}
diff --git a/frontend/app/components/Client/Integrations/Backend/SentryForm/SentryFormModal.tsx b/frontend/app/components/Client/Integrations/Backend/SentryForm/SentryFormModal.tsx index 4aa7d482e..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,6 +33,7 @@ function SentryForm({ onClose: () => void; integrated: boolean; }) { + const { t } = useTranslation(); const { integrationsStore } = useStore(); const { siteId } = integrationsStore.integrations; const { @@ -40,22 +42,23 @@ function SentryForm({ 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 () => { @@ -89,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')}
- {exists ? 'Update' : 'Add'} + {exists ? t('Update') : t('Add')} {integrated && ( )}
diff --git a/frontend/app/components/Client/Integrations/FormField.tsx b/frontend/app/components/Client/Integrations/FormField.tsx index 463bc41c0..96b93051a 100644 --- a/frontend/app/components/Client/Integrations/FormField.tsx +++ b/frontend/app/components/Client/Integrations/FormField.tsx @@ -10,7 +10,7 @@ export function FormField({ errors, }: { label: string; - name: string + name: string; value: string; onChange: (e: React.ChangeEvent) => void; autoFocus?: boolean; diff --git a/frontend/app/components/Client/Integrations/GithubForm.js b/frontend/app/components/Client/Integrations/GithubForm.tsx similarity index 54% rename from frontend/app/components/Client/Integrations/GithubForm.js rename to frontend/app/components/Client/Integrations/GithubForm.tsx index 060fa3062..d8d1f4d4c 100644 --- a/frontend/app/components/Client/Integrations/GithubForm.js +++ b/frontend/app/components/Client/Integrations/GithubForm.tsx @@ -2,19 +2,34 @@ 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 ( -
+
-
Integrate GitHub with OpenReplay and create issues directly from the recording page.
+
+ {t( + 'Integrate GitHub with OpenReplay and create issues directly from the recording page.', + )} +
- +
{ + const write = ({ target: { value, name: key, type, checked } }) => { if (type === 'checkbox') edit({ [key]: checked }); else edit({ [key]: value }); }; const save = () => { const { name, customPath } = props; - onSave(customPath || name).then(() => { - fetchList(); - props.onClose(); - }).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 = () => { @@ -71,9 +69,10 @@ function IntegrationForm(props: any) { type = 'text', checkIfDisplayed, autoFocus = false, - }) => (typeof checkIfDisplayed !== 'function' - || checkIfDisplayed(config)) - && (type === 'checkbox' ? ( + }) => + (typeof checkIfDisplayed !== 'function' || + checkIfDisplayed(config)) && + (type === 'checkbox' ? ( - {config?.exists() ? 'Update' : 'Add'} + {config?.exists() ? t('Update') : t('Add')} {integrated && ( )} diff --git a/frontend/app/components/Client/Integrations/IntegrationItem.tsx b/frontend/app/components/Client/Integrations/IntegrationItem.tsx index 2dd3041b8..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 { Tooltip } from 'antd'; -import stl from './integrationItem.module.css'; +import { useTranslation } from 'react-i18next'; interface Props { integration: any; @@ -13,30 +12,46 @@ interface Props { } function IntegrationItem(props: Props) { - const { - integration, integrated, hide = false, useIcon, - } = 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.subtitle && integration.subtitle} +

{integrated && ( -
- - Integrated -
+
+ + {t('Integrated')} +
)}
); diff --git a/frontend/app/components/Client/Integrations/IntegrationModalCard.tsx b/frontend/app/components/Client/Integrations/IntegrationModalCard.tsx index 5bc4cfee9..1ac2fe6e8 100644 --- a/frontend/app/components/Client/Integrations/IntegrationModalCard.tsx +++ b/frontend/app/components/Client/Integrations/IntegrationModalCard.tsx @@ -9,13 +9,19 @@ interface Props { } function IntegrationModalCard(props: Props) { - const { - title, icon, description, useIcon, - } = props; + const { title, icon, description, useIcon } = props; return (
- {useIcon ? : integration} + {useIcon ? ( + + ) : ( + integration + )}

{title}

diff --git a/frontend/app/components/Client/Integrations/Integrations.tsx b/frontend/app/components/Client/Integrations/Integrations.tsx index a7ee142bd..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,6 +37,7 @@ interface Props { } function Integrations(props: Props) { + const { t } = useTranslation(); const { integrationsStore, projectsStore } = useStore(); const initialSiteId = projectsStore.siteId; const { siteId } = integrationsStore.integrations; @@ -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]); @@ -61,9 +65,9 @@ function Integrations(props: Props) { const onClick = (integration: any, width: number) => { if ( - integration.slug - && integration.slug !== 'slack' - && integration.slug !== 'msteams' + integration.slug && + integration.slug !== 'slack' && + integration.slug !== 'msteams' ) { const intName = integration.slug as | 'sentry' @@ -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, @@ -121,7 +125,7 @@ function Integrations(props: Props) { <>
- {!hideHeader && Integrations
} />} + {!hideHeader && {t('Integrations')}
} />}
-
+
{allIntegrations.map((integration, i) => ( onClick( - integration, - filteredIntegrations.find((cat) => cat.integrations.includes(integration))?.title === 'Plugins' - ? 500 - : 350, - )} + onClick={() => + onClick( + integration, + filteredIntegrations.find((cat) => + cat.integrations.includes(integration), + )?.title === 'Plugins' + ? 500 + : 350, + ) + } hide={ - (integration.slug === 'github' - && integratedList.includes('jira')) - || (integration.slug === 'jira' - && integratedList.includes('github')) + (integration.slug === 'github' && + integratedList.includes('jira')) || + (integration.slug === 'jira' && + integratedList.includes('github')) } /> @@ -166,28 +172,31 @@ export default withPageTitle('Integrations - OpenReplay Preferences')( 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', @@ -196,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, @@ -250,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', @@ -268,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', @@ -288,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.tsx similarity index 64% rename from frontend/app/components/Client/Integrations/JiraForm/JiraForm.js rename to frontend/app/components/Client/Integrations/JiraForm/JiraForm.tsx index bb182b9b3..5e10ff540 100644 --- a/frontend/app/components/Client/Integrations/JiraForm/JiraForm.js +++ b/frontend/app/components/Client/Integrations/JiraForm/JiraForm.tsx @@ -3,27 +3,34 @@ 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 ( -
+
-
How it works?
+
{t('How it works?')}
    -
  1. Create a new API token
  2. -
  3. Enter the token below
  4. +
  5. {t('Create a new API token')}
  6. +
  7. {t('Enter the token below')}
@@ -38,16 +45,16 @@ function JiraForm(props) { formFields={[ { key: 'username', - label: 'Username', + label: t('Username'), autoFocus: true, }, { key: 'token', - label: 'API Token', + label: t('API Token'), }, { key: 'url', - label: 'JIRA URL', + label: t('JIRA URL'), placeholder: 'E.x. https://myjira.atlassian.net', }, ]} diff --git a/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js b/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.tsx similarity index 69% rename from frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js rename to frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.tsx index a319a7820..eb6013a7c 100644 --- a/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js +++ b/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.tsx @@ -5,12 +5,16 @@ import { CodeBlock } from 'UI'; import DocLink from 'Shared/DocLink/DocLink'; import ToggleContent from 'Shared/ToggleContent'; +import { useTranslation } from 'react-i18next'; function ProfilerDoc() { + 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 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,37 +52,39 @@ 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={} />
diff --git a/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js b/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.tsx similarity index 74% rename from frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js rename to frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.tsx index 5e2cfe029..74203a900 100644 --- a/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js +++ b/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.tsx @@ -1,12 +1,12 @@ import React from 'react'; -import { - Form, Input, Message, confirm, -} from 'UI'; +import { Form, Input, Message, confirm } from 'UI'; 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; @@ -31,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); @@ -47,22 +49,22 @@ function SlackAddForm(props) {
- + - + @@ -75,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/SlackChannelList/SlackChannelList.js b/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.tsx similarity index 71% rename from frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js rename to frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.tsx index 04597dcf3..cb138096d 100644 --- a/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js +++ b/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.tsx @@ -3,8 +3,10 @@ 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; @@ -17,14 +19,18 @@ function SlackChannelList(props) { return (
- Integrate Slack with OpenReplay and share insights with the rest of the team, directly from the recording page. + {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} > @@ -36,7 +42,9 @@ function SlackChannelList(props) { >
{c.name}
-
{c.endpoint}
+
+ {c.endpoint} +
))} diff --git a/frontend/app/components/Client/Integrations/SlackForm.tsx b/frontend/app/components/Client/Integrations/SlackForm.tsx index 8c01f16f0..ecc3487e6 100644 --- a/frontend/app/components/Client/Integrations/SlackForm.tsx +++ b/frontend/app/components/Client/Integrations/SlackForm.tsx @@ -5,8 +5,10 @@ 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'; function SlackForm() { + const { t } = useTranslation(); const { integrationsStore } = useStore(); const { init } = integrationsStore.slack; const fetchList = integrationsStore.slack.fetchIntegrations; @@ -26,7 +28,10 @@ function SlackForm() { }, []); return ( -
+
{active && (
setActive(false)} /> @@ -34,8 +39,13 @@ function SlackForm() { )}
-

Slack

-
diff --git a/frontend/app/components/Client/Integrations/Teams/TeamsAddForm.tsx b/frontend/app/components/Client/Integrations/Teams/TeamsAddForm.tsx index 9e8e27d3f..8061740a8 100644 --- a/frontend/app/components/Client/Integrations/Teams/TeamsAddForm.tsx +++ b/frontend/app/components/Client/Integrations/Teams/TeamsAddForm.tsx @@ -2,16 +2,16 @@ import { observer } from 'mobx-react-lite'; import React from 'react'; import { useStore } from 'App/mstore'; -import { - confirm, Form, Input, Message, -} from 'UI'; +import { confirm, Form, Input, Message } from 'UI'; 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; const saving = integrationsStore.msteams.loading; @@ -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); @@ -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 a2d7f8c11..ce7dee91d 100644 --- a/frontend/app/components/Client/Integrations/Teams/TeamsChannelList.tsx +++ b/frontend/app/components/Client/Integrations/Teams/TeamsChannelList.tsx @@ -5,8 +5,10 @@ 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; const { edit } = integrationsStore.msteams; @@ -19,19 +21,18 @@ function TeamsChannelList(props: { onEdit: () => void }) { return (
- 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.')}
- )} + } size="small" show={list.length === 0} > diff --git a/frontend/app/components/Client/Integrations/Teams/index.tsx b/frontend/app/components/Client/Integrations/Teams/index.tsx index 417e6944a..3d665c8e7 100644 --- a/frontend/app/components/Client/Integrations/Teams/index.tsx +++ b/frontend/app/components/Client/Integrations/Teams/index.tsx @@ -6,8 +6,10 @@ import { Button } from 'antd'; import TeamsChannelList from './TeamsChannelList'; import TeamsAddForm from './TeamsAddForm'; +import { useTranslation } from 'react-i18next'; function MSTeams() { + const { t } = useTranslation(); const { integrationsStore } = useStore(); const fetchList = integrationsStore.msteams.fetchIntegrations; const { init } = integrationsStore.msteams; @@ -27,7 +29,10 @@ function MSTeams() { }, []); return ( -
+
{active && (
setActive(false)} /> @@ -35,8 +40,13 @@ function MSTeams() { )}
-

Microsoft Teams

-
diff --git a/frontend/app/components/Client/Integrations/Tracker/AssistDoc/AssistDoc.js b/frontend/app/components/Client/Integrations/Tracker/AssistDoc/AssistDoc.tsx similarity index 55% rename from frontend/app/components/Client/Integrations/Tracker/AssistDoc/AssistDoc.js rename to frontend/app/components/Client/Integrations/Tracker/AssistDoc/AssistDoc.tsx index b08e9bb29..c2b1f6d2b 100644 --- a/frontend/app/components/Client/Integrations/Tracker/AssistDoc/AssistDoc.js +++ b/frontend/app/components/Client/Integrations/Tracker/AssistDoc/AssistDoc.tsx @@ -5,6 +5,7 @@ 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'; @@ -14,10 +15,13 @@ const TABS = [ ]; 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 projectKey = siteId + ? sites.find((site) => site.id === siteId)?.projectKey + : sites[0]?.projectKey; const [activeTab, setActiveTab] = useState(SCRIPT); const renderActiveTab = () => { @@ -31,24 +35,36 @@ function AssistDoc() { }; return ( -
-

Assist

+
+

{t('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. + {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.', + )}
-
Installation
+
{t('Installation')}
-
Usage
- setActiveTab(tab)} /> +
{t('Usage')}
+ setActiveTab(tab)} + />
{renderActiveTab()}
- +
); diff --git a/frontend/app/components/Client/Integrations/Tracker/AssistDoc/AssistNpm.tsx b/frontend/app/components/Client/Integrations/Tracker/AssistDoc/AssistNpm.tsx index 3c769359e..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({ @@ -60,17 +62,19 @@ type ButtonOptions = HTMLButtonElement | string | { 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 e53e4d6de..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 { 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:', + )} +

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 d00208b28..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'; - -function GraphQLDoc() { - 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 ( -
-

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/MobxDoc/MobxDoc.js b/frontend/app/components/Client/Integrations/Tracker/MobxDoc/MobxDoc.tsx similarity index 55% rename from frontend/app/components/Client/Integrations/Tracker/MobxDoc/MobxDoc.js rename to frontend/app/components/Client/Integrations/Tracker/MobxDoc/MobxDoc.tsx index 159d4665b..e31a25b86 100644 --- a/frontend/app/components/Client/Integrations/Tracker/MobxDoc/MobxDoc.js +++ b/frontend/app/components/Client/Integrations/Tracker/MobxDoc/MobxDoc.tsx @@ -4,12 +4,16 @@ 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 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'; @@ -36,32 +40,44 @@ function SomeFunctionalComponent() { }`; return ( -
-

MobX

+
+

{t('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. + {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.', + )}
-
Installation
- +
{t('Installation')}
+ -
Usage
+
{t('Usage')}

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

-
Usage
+
{t('Usage')}
} second={} /> - +
); diff --git a/frontend/app/components/Client/Integrations/Tracker/NgRxDoc/NgRxDoc.js b/frontend/app/components/Client/Integrations/Tracker/NgRxDoc/NgRxDoc.tsx similarity index 57% rename from frontend/app/components/Client/Integrations/Tracker/NgRxDoc/NgRxDoc.js rename to frontend/app/components/Client/Integrations/Tracker/NgRxDoc/NgRxDoc.tsx index 44b9b7d21..d1401899f 100644 --- a/frontend/app/components/Client/Integrations/Tracker/NgRxDoc/NgRxDoc.js +++ b/frontend/app/components/Client/Integrations/Tracker/NgRxDoc/NgRxDoc.tsx @@ -4,12 +4,16 @@ 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 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'; @@ -48,33 +52,44 @@ const metaReducers = [tracker.use(trackerNgRx())]; // check list of ava export class AppModule {} }`; return ( -
-

NgRx

+
+

{t('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. + {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.', + )}
-
Installation
- - -
Usage
-

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

-
- -
Usage
- - } - second={ - - } +
{t('Installation')}
+ - +
{t('Usage')}
+

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

+
+ +
{t('Usage')}
+ } + second={} + /> + +
); diff --git a/frontend/app/components/Client/Integrations/Tracker/PiniaDoc/PiniaDoc.tsx b/frontend/app/components/Client/Integrations/Tracker/PiniaDoc/PiniaDoc.tsx index 539645942..8c18077f2 100644 --- a/frontend/app/components/Client/Integrations/Tracker/PiniaDoc/PiniaDoc.tsx +++ b/frontend/app/components/Client/Integrations/Tracker/PiniaDoc/PiniaDoc.tsx @@ -6,8 +6,10 @@ import ToggleContent from 'Components/shared/ToggleContent'; import { CodeBlock } from 'UI'; import DocLink from 'Shared/DocLink/DocLink'; +import { useTranslation } from 'react-i18next'; function PiniaDoc() { + const { t } = useTranslation(); const { integrationsStore, projectsStore } = useStore(); const sites = projectsStore.list; const { siteId } = integrationsStore.integrations; @@ -67,37 +69,37 @@ 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={} />
diff --git a/frontend/app/components/Client/Integrations/Tracker/ReduxDoc/ReduxDoc.js b/frontend/app/components/Client/Integrations/Tracker/ReduxDoc/ReduxDoc.tsx similarity index 55% rename from frontend/app/components/Client/Integrations/Tracker/ReduxDoc/ReduxDoc.js rename to frontend/app/components/Client/Integrations/Tracker/ReduxDoc/ReduxDoc.tsx index 8d356059c..35c2ad37a 100644 --- a/frontend/app/components/Client/Integrations/Tracker/ReduxDoc/ReduxDoc.js +++ b/frontend/app/components/Client/Integrations/Tracker/ReduxDoc/ReduxDoc.tsx @@ -4,12 +4,16 @@ 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 projectKey = siteId + ? sites.find((site) => site.id === siteId)?.projectKey + : sites[0]?.projectKey; const usage = `import { applyMiddleware, createStore } from 'redux'; import OpenReplay from '@openreplay/tracker'; @@ -43,32 +47,43 @@ const store = createStore( ); }`; return ( -
-

Redux

+
+

{t('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. + {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.', + )}
-
Installation
- - -
Usage
-

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

-
- - } - second={ - - } +
{t('Installation')}
+ - +
{t('Usage')}
+

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

+
+ } + second={} + /> + +
); diff --git a/frontend/app/components/Client/Integrations/Tracker/ReduxDoc/index.js b/frontend/app/components/Client/Integrations/Tracker/ReduxDoc/index.js index 248102d25..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'; +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 57% rename from frontend/app/components/Client/Integrations/Tracker/VueDoc/VueDoc.js rename to frontend/app/components/Client/Integrations/Tracker/VueDoc/VueDoc.tsx index 2576d1349..b32e9f1f4 100644 --- a/frontend/app/components/Client/Integrations/Tracker/VueDoc/VueDoc.js +++ b/frontend/app/components/Client/Integrations/Tracker/VueDoc/VueDoc.tsx @@ -4,12 +4,16 @@ 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 VueDoc() { + 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 projectKey = siteId + ? sites.find((site) => site.id === siteId)?.projectKey + : sites[0]?.projectKey; const usage = `import Vuex from 'vuex' import OpenReplay from '@openreplay/tracker'; @@ -43,37 +47,41 @@ const store = new Vuex.Store({ }); }`; 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={} />
diff --git a/frontend/app/components/Client/Integrations/Tracker/ZustandDoc/ZustandDoc.js b/frontend/app/components/Client/Integrations/Tracker/ZustandDoc/ZustandDoc.tsx similarity index 63% rename from frontend/app/components/Client/Integrations/Tracker/ZustandDoc/ZustandDoc.js rename to frontend/app/components/Client/Integrations/Tracker/ZustandDoc/ZustandDoc.tsx index 170c0effb..b25f40dd6 100644 --- a/frontend/app/components/Client/Integrations/Tracker/ZustandDoc/ZustandDoc.js +++ b/frontend/app/components/Client/Integrations/Tracker/ZustandDoc/ZustandDoc.tsx @@ -4,12 +4,16 @@ 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 projectKey = siteId + ? sites.find((site) => site.id === siteId)?.projectKey + : sites[0]?.projectKey; const usage = `import create from "zustand"; import Tracker from '@openreplay/tracker'; @@ -65,35 +69,43 @@ const useBearStore = create( ) )`; return ( -
-

Zustand

+
+

{t('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. + {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.', + )}
-
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={} /> - +
); diff --git a/frontend/app/components/Client/Integrations/apiMethods.ts b/frontend/app/components/Client/Integrations/apiMethods.ts index c870ccdee..6d70fdd94 100644 --- a/frontend/app/components/Client/Integrations/apiMethods.ts +++ b/frontend/app/components/Client/Integrations/apiMethods.ts @@ -56,7 +56,8 @@ export function useIntegration( }) => saveIntegration(name, values, siteId, exists), }); const removeMutation = useMutation({ - mutationFn: ({ siteId }: { siteId: string }) => removeIntegration(name, siteId), + mutationFn: ({ siteId }: { siteId: string }) => + removeIntegration(name, siteId), }); return { diff --git a/frontend/app/components/Client/Modules/Modules.tsx b/frontend/app/components/Client/Modules/Modules.tsx index bd801d3aa..cca204eef 100644 --- a/frontend/app/components/Client/Modules/Modules.tsx +++ b/frontend/app/components/Client/Modules/Modules.tsx @@ -6,8 +6,10 @@ import { toast } from 'react-toastify'; 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; const modules = userStore.account.settings?.modules ?? []; @@ -24,29 +26,45 @@ 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

+

{t('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( + "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', + )} +
@@ -61,4 +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 9269b247a..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', }, { - 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', }, { - 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, }, { - 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, }, { - 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', }, { - 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', }, { - 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, }, { - 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 723cdfafc..fc4088e2f 100644 --- a/frontend/app/components/Client/Notifications/Notifications.tsx +++ b/frontend/app/components/Client/Notifications/Notifications.tsx @@ -5,9 +5,11 @@ 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 { t } = useTranslation(); useEffect(() => { void weeklyReportStore.fetchReport(); @@ -20,20 +22,26 @@ function Notifications() { 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'} + {weeklyReportStore.weeklyReport ? t('Yes') : t('No')}
); } -export default withPageTitle('Weekly Report - OpenReplay Preferences')(observer(Notifications)); +export default withPageTitle('Weekly Report - OpenReplay Preferences')( + observer(Notifications), +); diff --git a/frontend/app/components/Client/ProfileSettings/Api.js b/frontend/app/components/Client/ProfileSettings/Api.tsx similarity index 79% rename from frontend/app/components/Client/ProfileSettings/Api.js rename to frontend/app/components/Client/ProfileSettings/Api.tsx index 0348b1a42..75e24b26c 100644 --- a/frontend/app/components/Client/ProfileSettings/Api.js +++ b/frontend/app/components/Client/ProfileSettings/Api.tsx @@ -2,14 +2,16 @@ import React from 'react'; 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; return ( - + "Passwords don't match"; const MIN_LENGTH = 8; function ChangePassword() { + const { t } = useTranslation(); const { userStore } = useStore(); const { updatePassword } = userStore; const passwordErrors = userStore.updatePasswordRequest.errors; 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, }); - const [newPasswordRepeat, setNewPasswordRepeat] = useState<{ value: string; error: boolean }>({ + const [newPasswordRepeat, setNewPasswordRepeat] = useState<{ + value: string; + error: boolean; + }>({ value: '', error: false, }); const [show, setShow] = useState(false); - const checkDoesntMatch = useCallback((newPassword: string, newPasswordRepeat: string) => newPasswordRepeat.length > 0 && newPasswordRepeat !== newPassword, []); + const checkDoesntMatch = useCallback( + (newPassword: string, newPasswordRepeat: string) => + newPasswordRepeat.length > 0 && newPasswordRepeat !== newPassword, + [], + ); const isSubmitDisabled = useCallback(() => { if ( - newPassword.value !== newPasswordRepeat.value - || newPassword.value.length < MIN_LENGTH - || oldPassword.length === 0 + newPassword.value !== newPasswordRepeat.value || + newPassword.value.length < MIN_LENGTH || + oldPassword.length === 0 ) { return true; } @@ -52,14 +64,14 @@ function ChangePassword() { updatePassword({ oldPassword, newPassword: newPassword.value, - }).then(() => { - setShow(false); - setOldPassword(''); - setNewPassword({ value: '', error: false }); - setNewPasswordRepeat({ value: '', error: false }); - }).catch((e) => { - - }); + }) + .then(() => { + setShow(false); + setOldPassword(''); + setNewPassword({ value: '', error: false }); + setNewPasswordRepeat({ value: '', error: false }); + }) + .catch((e) => {}); }, [isSubmitDisabled, oldPassword, newPassword, updatePassword], ); @@ -73,7 +85,9 @@ function ChangePassword() { name="oldPassword" value={oldPassword} type="password" - onChange={(e: React.ChangeEvent) => setOldPassword(e.target.value)} + onChange={(e: React.ChangeEvent) => + setOldPassword(e.target.value) + } /> @@ -92,14 +106,17 @@ function ChangePassword() { /> - + ) => { const newValue = e.target.value; @@ -113,15 +130,23 @@ function ChangePassword() { {err} ))} -
diff --git a/frontend/app/components/Client/ProfileSettings/OptOut.js b/frontend/app/components/Client/ProfileSettings/OptOut.js index 820fb6afe..25f09a0f0 100644 --- a/frontend/app/components/Client/ProfileSettings/OptOut.js +++ b/frontend/app/components/Client/ProfileSettings/OptOut.js @@ -11,12 +11,16 @@ function OptOut() { const onChange = () => { setOptOut(!optOut); - void updateClient({ optOut: !optOut }).then(() => { - toast('Account settings updated successfully', { type: 'success' }); - }).catch((e) => { - toast(e.message || 'Failed to update account settings', { type: 'error' }); - setOptOut(optOut); - }); + void updateClient({ optOut: !optOut }) + .then(() => { + toast('Account settings updated successfully', { type: 'success' }); + }) + .catch((e) => { + toast(e.message || 'Failed to update account settings', { + type: 'error', + }); + setOptOut(optOut); + }); }; return ( diff --git a/frontend/app/components/Client/ProfileSettings/ProfileSettings.js b/frontend/app/components/Client/ProfileSettings/ProfileSettings.tsx similarity index 62% rename from frontend/app/components/Client/ProfileSettings/ProfileSettings.js rename to frontend/app/components/Client/ProfileSettings/ProfileSettings.tsx index 1e5f9695f..ab7c31460 100644 --- a/frontend/app/components/Client/ProfileSettings/ProfileSettings.js +++ b/frontend/app/components/Client/ProfileSettings/ProfileSettings.tsx @@ -10,18 +10,24 @@ import Api from './Api'; import TenantKey from './TenantKey'; import OptOut from './OptOut'; import Licenses from './Licenses'; +import { useTranslation } from 'react-i18next'; function ProfileSettings() { + const { t } = useTranslation(); const { userStore } = useStore(); const { account } = userStore; const { isEnterprise } = userStore; return (
- Account
} /> + {t('Account')}
} />
-

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 71% rename from frontend/app/components/Client/ProfileSettings/Settings.js rename to frontend/app/components/Client/ProfileSettings/Settings.tsx index 23c608b61..25f8bdc0f 100644 --- a/frontend/app/components/Client/ProfileSettings/Settings.js +++ b/frontend/app/components/Client/ProfileSettings/Settings.tsx @@ -5,15 +5,19 @@ 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; const storeAccountName = userStore.account.name; const storeOrganizationName = userStore.account.tenantName; 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) => { @@ -28,18 +32,22 @@ function Settings() { 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.tsx similarity index 73% rename from frontend/app/components/Client/ProfileSettings/TenantKey.js rename to frontend/app/components/Client/ProfileSettings/TenantKey.tsx index 8109606ca..f6f159c4d 100644 --- a/frontend/app/components/Client/ProfileSettings/TenantKey.js +++ b/frontend/app/components/Client/ProfileSettings/TenantKey.tsx @@ -4,8 +4,10 @@ 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; @@ -19,7 +21,7 @@ function TenantKey() { }; return ( - + - { copied ? 'Copied' : 'Copy' } + leadingButton={ + - )} + } /> ); diff --git a/frontend/app/components/Client/Projects/ProjectCaptureRate.tsx b/frontend/app/components/Client/Projects/ProjectCaptureRate.tsx index dede8a06e..9871cd138 100644 --- a/frontend/app/components/Client/Projects/ProjectCaptureRate.tsx +++ b/frontend/app/components/Client/Projects/ProjectCaptureRate.tsx @@ -1,7 +1,5 @@ import React, { useEffect, useMemo, useState } from 'react'; -import { - Button, Space, Switch, Tooltip, Input, Typography, -} from 'antd'; +import { Button, Space, Switch, Tooltip, Input, Typography } from 'antd'; import { Icon, Loader } from 'UI'; import cn from 'classnames'; import ConditionalRecordingSettings from 'Shared/SessionSettings/components/ConditionalRecordingSettings'; @@ -9,12 +7,14 @@ 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'; @@ -40,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); }; @@ -49,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) => { @@ -70,23 +74,32 @@ function ProjectCaptureRate(props: Props) { updateCaptureConditions(projectId!, { rate: parseInt(captureRate, 10), conditionalCapture, - conditions: isEnterprise ? conditions.map((c) => c.toCaptureCondition()) : [], + 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')} + @@ -97,9 +110,11 @@ function ProjectCaptureRate(props: Props) { {!conditionalCapture ? ( @@ -131,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 f037d72d5..cbe27d481 100644 --- a/frontend/app/components/Client/Projects/ProjectCodeSnippet.tsx +++ b/frontend/app/components/Client/Projects/ProjectCodeSnippet.tsx @@ -10,26 +10,26 @@ 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; const site = props.project; @@ -77,15 +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 }), + ); }} />
@@ -135,18 +153,18 @@ function ProjectForm(props: Props) { loading={loading} // disabled={!project.validate} > - {project.exists() ? 'Save' : 'Add'} + {project.exists() ? t('Save') : t('Add')}
{project.exists() && ( - +
diff --git a/frontend/app/components/Client/Projects/ProjectTabContent.tsx b/frontend/app/components/Client/Projects/ProjectTabContent.tsx index 86ef51eae..d3cbba444 100644 --- a/frontend/app/components/Client/Projects/ProjectTabContent.tsx +++ b/frontend/app/components/Client/Projects/ProjectTabContent.tsx @@ -30,11 +30,7 @@ const ProjectTabContent: React.FC = () => { [project], ); - return ( -
- {tabContent[tab] || } -
- ); + return
{tabContent[tab] || }
; }; export default observer(ProjectTabContent); diff --git a/frontend/app/components/Client/Projects/ProjectTabTracking.tsx b/frontend/app/components/Client/Projects/ProjectTabTracking.tsx index 7729f9a23..50f87716c 100644 --- a/frontend/app/components/Client/Projects/ProjectTabTracking.tsx +++ b/frontend/app/components/Client/Projects/ProjectTabTracking.tsx @@ -2,7 +2,10 @@ import React from 'react'; import Project from '@/mstore/types/project'; import { Tabs } from 'UI'; import { - AppleOutlined, AndroidOutlined, CodeOutlined, JavaScriptOutlined, + AppleOutlined, + AndroidOutlined, + CodeOutlined, + JavaScriptOutlined, } from '@ant-design/icons'; import usePageTitle from '@/hooks/usePageTitle'; import InstallDocs from 'Components/Onboarding/components/OnboardingTabs/InstallDocs'; @@ -10,13 +13,7 @@ import ProjectCodeSnippet from 'Components/Client/Projects/ProjectCodeSnippet'; import MobileInstallDocs from 'Components/Onboarding/components/OnboardingTabs/InstallDocs/MobileInstallDocs'; import { Segmented } from 'antd'; import AndroidInstallDocs from 'Components/Onboarding/components/OnboardingTabs/InstallDocs/AndroidInstallDocs'; - -const JAVASCRIPT = 'Using Script'; -const NPM = 'Using NPM'; -const TABS = [ - { key: NPM, text: NPM }, - { key: JAVASCRIPT, text: JAVASCRIPT }, -]; +import { useTranslation } from 'react-i18next'; interface Props { project: Project; @@ -40,6 +37,7 @@ function ProjectTabTracking(props: Props) { export default ProjectTabTracking; function WebSnippet({ project }: { project: Project }) { + const { t } = useTranslation(); const [isNpm, setIsNpm] = React.useState(true); return ( @@ -50,7 +48,7 @@ function WebSnippet({ project }: { project: Project }) { label: (
- NPM + {t('NPM')}
), value: true, @@ -59,7 +57,7 @@ function WebSnippet({ project }: { project: Project }) { label: (
- Script + {t('Script')}
), value: false, @@ -80,6 +78,7 @@ function WebSnippet({ project }: { project: Project }) { } function MobileSnippet({ project }: { project: Project }) { + const { t } = useTranslation(); const [isIos, setIsIos] = React.useState(true); const ingestPoint = `https://${window.location.hostname}/ingest`; @@ -91,7 +90,7 @@ function MobileSnippet({ project }: { project: Project }) { label: (
- iOS + {t('iOS')}
), value: true, @@ -100,7 +99,7 @@ function MobileSnippet({ project }: { project: Project }) { label: (
- Android + {t('Android')}
), value: false, @@ -116,7 +115,6 @@ function MobileSnippet({ project }: { project: Project }) { ) : ( )} -
); } diff --git a/frontend/app/components/Client/Projects/ProjectTabs.tsx b/frontend/app/components/Client/Projects/ProjectTabs.tsx index ae71dc641..d21303923 100644 --- a/frontend/app/components/Client/Projects/ProjectTabs.tsx +++ b/frontend/app/components/Client/Projects/ProjectTabs.tsx @@ -2,20 +2,34 @@ import React from 'react'; import { Tabs, TabsProps } from 'antd'; import { useStore } from '@/mstore'; import { observer } from 'mobx-react-lite'; +import { useTranslation } from 'react-i18next'; const customTabBar: TabsProps['renderTabBar'] = (props, DefaultTabBar) => ( ); function ProjectTabs() { + const { t } = useTranslation(); const { projectsStore } = useStore(); const activeTab = projectsStore.config.tab; const tabItems = [ - { key: 'installation', label: 'Installation', content:
Installation Content
}, - { key: 'captureRate', label: 'Capture Rate', content:
Capture Rate Content
}, - { key: 'metadata', label: 'Metadata', content:
Metadata Content
}, - { key: 'tags', label: 'Tags', content:
Tags Content
}, + { + key: 'installation', + label: t('Installation'), + content:
{t('Installation Content')}
, + }, + { + key: 'captureRate', + label: t('Capture Rate'), + content:
{t('Capture Rate Content')}
, + }, + { + key: 'metadata', + label: t('Metadata'), + content:
{t('Metadata Content')}
, + }, + { key: 'tags', label: t('Tags'), content:
{t('Tags Content')}
}, // { key: 'groupKeys', label: 'Group Keys', content:
Group Keys Content
} ]; diff --git a/frontend/app/components/Client/Projects/ProjectTags.tsx b/frontend/app/components/Client/Projects/ProjectTags.tsx index 035679bd7..aef5311dd 100644 --- a/frontend/app/components/Client/Projects/ProjectTags.tsx +++ b/frontend/app/components/Client/Projects/ProjectTags.tsx @@ -1,16 +1,16 @@ import React, { useEffect } from 'react'; import { useStore } from '@/mstore'; -import { - List, Button, Typography, Space, Empty, -} from 'antd'; +import { List, Button, Typography, Space, Empty } from 'antd'; import { observer } from 'mobx-react-lite'; import { ScanSearch } from 'lucide-react'; import { EditOutlined } from '@ant-design/icons'; import { useModal } from 'Components/ModalContext'; import TagForm from 'Components/Client/Projects/TagForm'; import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; +import { useTranslation } from 'react-i18next'; function ProjectTags() { + const { t } = useTranslation(); const { tagWatchStore, projectsStore } = useStore(); const list = tagWatchStore.tags; const { openModal } = useModal(); @@ -22,7 +22,7 @@ function ProjectTags() { const handleInit = (tag?: any) => { openModal(, { - title: tag ? 'Edit Tag' : 'Add Tag', + title: tag ? t('Edit Tag') : t('Add Tag'), }); }; @@ -30,20 +30,31 @@ function ProjectTags() {
- Manage Tag Elements here. Rename tags for easy identification or delete those you no longer need. + {t( + 'Manage Tag Elements here. Rename tags for easy identification or delete those you no longer need.', + )}
  • - To create new tags, navigate to the Tags tab while playing a session. + + {t(' To create new tags, navigate to the Tags tab while playing a session')} +
  • - Use tags in OmniSearch to quickly find relevant sessions. + + {t('Use tags in OmniSearch to quickly find relevant sessions.')} +
} />, + emptyText: ( + } + /> + ), }} loading={tagWatchStore.isLoading} dataSource={list} @@ -51,7 +62,11 @@ function ProjectTags() { } />, + } + title={ + + {t('Projects')} + + } + extra={ + + } > - + @@ -104,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 9526ea796..92f8adb23 100644 --- a/frontend/app/components/Client/Roles/Roles.tsx +++ b/frontend/app/components/Client/Roles/Roles.tsx @@ -5,16 +5,16 @@ import React, { useEffect } from 'react'; import { useModal } from 'App/components/Modal'; import { useStore } from 'App/mstore'; -import { - Loader, NoContent, Tooltip, confirm, -} from 'UI'; +import { Loader, NoContent, Tooltip, confirm } from 'UI'; 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; const projectsMap = projectsStore.list.reduce((acc: any, p: any) => { @@ -51,8 +51,8 @@ function Roles() { 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); @@ -64,19 +64,27 @@ function Roles() {
-

Roles and Access

+

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

- +
- Title + {t('Title')}
- Project Access + {t('Project Access')}
- Feature Access + {t('Feature Access')}
diff --git a/frontend/app/components/Client/Roles/components/RoleForm/RoleForm.tsx b/frontend/app/components/Client/Roles/components/RoleForm/RoleForm.tsx index 343dbda15..953380ee2 100644 --- a/frontend/app/components/Client/Roles/components/RoleForm/RoleForm.tsx +++ b/frontend/app/components/Client/Roles/components/RoleForm/RoleForm.tsx @@ -2,13 +2,12 @@ import { observer } from 'mobx-react-lite'; import React, { useEffect, useRef } from 'react'; import { useStore } from 'App/mstore'; -import { - Checkbox, Form, Icon, Input, -} from 'UI'; +import { Checkbox, Form, Icon, Input } from 'UI'; 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; @@ -17,6 +16,7 @@ interface Props { } function RoleForm(props: Props) { + const { t } = useTranslation(); const { roleStore, projectsStore } = useStore(); const projects = projectsStore.list; const role = roleStore.instance; @@ -28,10 +28,12 @@ function RoleForm(props: Props) { 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); @@ -53,11 +55,12 @@ function 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')); }); }; - const write = ({ target: { value, name } }: any) => roleStore.editRole({ [name]: value }); + const write = ({ target: { value, name } }: any) => + roleStore.editRole({ [name]: value }); const onChangePermissions = (e: any) => { const { permissions } = role; @@ -113,7 +116,7 @@ function RoleForm(props: Props) {
- + - +
-
All Projects
+
{t('All Projects')}
- (Uncheck to select specific projects) + ({t('Uncheck to select specific projects')})
{!role.allProjects && ( (option?.label ?? '').toLowerCase().includes(input.toLowerCase())} + filterOption={(input, option) => + (option?.label ?? '') + .toLowerCase() + .includes(input.toLowerCase()) + } mode="multiple" allowClear - placeholder="Select" + placeholder={t('Select')} options={permissionOptions.filter( (option: any) => !role.permissions.includes(option.value), // Exclude selected options )} onChange={selectPermissions} labelInValue value={role.permissions.map((id: string) => { - const matching = permissionOptions.find((opt) => opt.value === id); + const matching = permissionOptions.find( + (opt) => opt.value === id, + ); return matching ? { value: matching.value, label: matching.label } : { value: id, label: String(id) }; // Fallback to projectId as label @@ -219,7 +237,9 @@ function RoleForm(props: Props) { > {role.exists() ? 'Update' : 'Add'} - {role.exists() && } + {role.exists() && ( + + )}
{role.exists() && (
diff --git a/frontend/app/components/Client/SessionsListingSettings.tsx b/frontend/app/components/Client/SessionsListingSettings.tsx index 8cd679e37..02875ed0c 100644 --- a/frontend/app/components/Client/SessionsListingSettings.tsx +++ b/frontend/app/components/Client/SessionsListingSettings.tsx @@ -7,11 +7,13 @@ 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 { useTranslation } from 'react-i18next'; function SessionsListingSettings() { + const { t } = useTranslation(); return (
- Sessions Listing
} /> + {t('Sessions Listing')}
} />
diff --git a/frontend/app/components/Client/Sites/AddProjectButton/AddProjectButton.tsx b/frontend/app/components/Client/Sites/AddProjectButton/AddProjectButton.tsx index c81f59886..2d1c51952 100644 --- a/frontend/app/components/Client/Sites/AddProjectButton/AddProjectButton.tsx +++ b/frontend/app/components/Client/Sites/AddProjectButton/AddProjectButton.tsx @@ -5,16 +5,21 @@ 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; - const canAddProject = isAdmin && (limits.projects === -1 || limits.projects > 0); + 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/GDPRForm.js b/frontend/app/components/Client/Sites/GDPRForm.tsx similarity index 64% rename from frontend/app/components/Client/Sites/GDPRForm.js rename to frontend/app/components/Client/Sites/GDPRForm.tsx index ecd4a8d5b..4393f9a7c 100644 --- a/frontend/app/components/Client/Sites/GDPRForm.js +++ b/frontend/app/components/Client/Sites/GDPRForm.tsx @@ -6,14 +6,17 @@ 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 = [ - { label: 'Record all inputs', value: 'plain' }, - { label: 'Ignore all inputs', value: 'obscured' }, - { label: 'Obscure all inputs', value: 'hidden' }, +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; @@ -31,7 +34,8 @@ function GDPRForm(props) { editGDPR({ [name]: value }); }; - const onSampleRateBlur = ({ target: { name, value } }) => { // TODO: editState hoc + const onSampleRateBlur = ({ target: { name, value } }) => { + // TODO: editState hoc if (value === '') { editGDPR({ sampleRate: 100 }); } @@ -54,11 +58,11 @@ function GDPRForm(props) {
- -
{ site.host }
+ +
{site.host}
- + - + - +
- {site?.exists() ? 'Update' : 'Add'} + {site?.exists() ? t('Update') : t('Add')} {site.exists() && ( - )}
{existsError && (
- Project exists already. + {t('Project exists already.')}
)}
diff --git a/frontend/app/components/Client/Sites/SiteSearch/SiteSearch.tsx b/frontend/app/components/Client/Sites/SiteSearch/SiteSearch.tsx index 5c9c84a56..af288cf60 100644 --- a/frontend/app/components/Client/Sites/SiteSearch/SiteSearch.tsx +++ b/frontend/app/components/Client/Sites/SiteSearch/SiteSearch.tsx @@ -4,7 +4,7 @@ import { debounce } from 'App/utils'; let debounceUpdate: any = () => {}; interface Props { - onChange: (value: string) => void; + onChange: (value: string) => void; } function SiteSearch(props: Props) { const { onChange } = props; @@ -19,11 +19,15 @@ function SiteSearch(props: Props) { return (
- + site.name.toLowerCase().includes(searchQuery.toLowerCase())); + const filteredSites = sites.filter((site: { name: string }) => + site.name.toLowerCase().includes(searchQuery.toLowerCase()), + ); const { showModal, hideModal } = useModal(); - function EditButton({ isAdmin, onClick }: { isAdmin: boolean; onClick: () => void }) { + function EditButton({ + isAdmin, + onClick, + }: { + isAdmin: boolean; + onClick: () => void; + }) { const _onClick = () => { onClick(); showModal(, { right: true }); }; - return
{project.host}
- {project.platform === 'web' ? null : MOBILE BETA} + {project.platform === 'web' ? null : ( + + {t('MOBILE BETA')} + + )}
- +
{project.conditionsCount > 0 ? ( ) : null}
@@ -124,14 +150,14 @@ function Sites() {
Projects
} - actionButton={( + title={
{t('Projects')}
} + actionButton={ - )} + } />
@@ -143,29 +169,33 @@ function Sites() {
-
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) => ( + + + + + ), + )}
- { label } + {label} ); diff --git a/frontend/app/components/Client/Users/UsersView.tsx b/frontend/app/components/Client/Users/UsersView.tsx index 44482d2a3..9dee542a5 100644 --- a/frontend/app/components/Client/Users/UsersView.tsx +++ b/frontend/app/components/Client/Users/UsersView.tsx @@ -8,11 +8,13 @@ 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; const { isEnterprise } = userStore; @@ -37,13 +39,12 @@ function UsersView({ isOnboarding = false }: Props) {
- Team - {' '} + {t('Team')}  {userCount}
- )} + } />
+ 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( @@ -15,11 +23,15 @@ function AddUserButton({ isAdmin = false, onClick, btnVariant = 'primary' }: any ); return ( - ); diff --git a/frontend/app/components/Client/Users/components/UserForm/UserForm.tsx b/frontend/app/components/Client/Users/components/UserForm/UserForm.tsx index 7b3ecfee8..3fca31f02 100644 --- a/frontend/app/components/Client/Users/components/UserForm/UserForm.tsx +++ b/frontend/app/components/Client/Users/components/UserForm/UserForm.tsx @@ -4,14 +4,14 @@ import React from 'react'; import { useModal } from 'App/components/Modal'; import { useStore } from 'App/mstore'; -import { - confirm, CopyButton, Form, Icon, Input, -} from 'UI'; +import { confirm, CopyButton, Form, Icon, Input } from 'UI'; 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; @@ -40,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(() => { @@ -56,14 +58,12 @@ function UserForm() {

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

- +
- + {!isSmtp && (
- SMTP is not configured (see - {' '} + {t('SMTP is not configured (see')}  - 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('here')} + {' '} + {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.', + )}
)} @@ -114,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.')}
@@ -124,9 +123,9 @@ function UserForm() { {isEnterprise && ( - + void; @@ -12,9 +13,13 @@ interface Props { } function WebhookForm({ onClose, onDelete }: Props) { + const { t } = useTranslation(); const { settingsStore } = useStore(); const { - webhookInst: webhook, saveWebhook, editWebhook, saving, + webhookInst: webhook, + saveWebhook, + editWebhook, + saving, } = settingsStore; const write = ({ target: { value, name } }) => editWebhook({ [name]: value }); @@ -24,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 ( - + - - + + - + @@ -66,9 +76,9 @@ 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() && ( +
-
None added yet
+
{t('None added yet')}
- )} + } size="small" show={customWebhooks.length === 0} > @@ -79,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} + -
); } diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/AreaChart.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/AreaChart.tsx index bba77e02a..bebb3f905 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/AreaChart.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/AreaChart.tsx @@ -61,14 +61,12 @@ function CustomAreaChart(props: Props) { iconType="wye" className="font-normal" wrapperStyle={{ top: inGrid ? undefined : -18 }} - payload={ - data.namesMap.map((key, index) => ({ - value: key, - type: 'line', - color: colors[index], - id: key, - })) - } + payload={data.namesMap.map((key, index) => ({ + value: key, + type: 'line', + color: colors[index], + id: key, + }))} /> )} - + } // Pass hoveredSeries to tooltip /> - {Array.isArray(reorderedNamesMap) - && reorderedNamesMap.map((key, index) => ( + {Array.isArray(reorderedNamesMap) && + reorderedNamesMap.map((key, index) => ( void; } function BigNumChart(props: Props) { @@ -40,16 +45,23 @@ function BigNumChart(props: Props) { } function BigNum({ - color, series, value, label, compData, valueLabel, onSeriesFocus, hideLegend, + 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 + color: string; + series: string; + value: number; + label: string; + compData?: number; + valueLabel?: string; + onSeriesFocus?: (name: string) => void; + hideLegend?: boolean; }) { const formattedNumber = (num: number) => Intl.NumberFormat().format(num); @@ -69,15 +81,12 @@ function BigNum({ 'hover:transition-all ease-in-out hover:ease-in-out hover:bg-teal/5 hover:cursor-pointer', )} > - {hideLegend ? null - : ( -
-
-
{series}
-
- )} + {hideLegend ? null : ( +
+
+
{series}
+
+ )}
{formattedNumber(value)} {valueLabel ? `${valueLabel}` : null} diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx index 782eda809..8b8db2396 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx @@ -4,8 +4,10 @@ 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 { useTranslation } from 'react-i18next'; function ClickMapCard() { + const { t } = useTranslation(); const [customSession, setCustomSession] = React.useState(null); const { metricStore, dashboardStore, sessionStore } = useStore(); const { fetchInsights } = sessionStore; @@ -16,15 +18,17 @@ function ClickMapCard() { 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(() => () => setCustomSession(null), []); React.useEffect(() => { if ( - metricStore.instance.data.domURL - && sessionId - && sessionId !== customSession?.sessionId + metricStore.instance.data.domURL && + sessionId && + sessionId !== customSession?.sessionId ) { setCustomSession(null); setTimeout(() => { @@ -59,19 +63,21 @@ function ClickMapCard() { return ( - 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 /> ); } if (!metricStore.instance.data?.sessionId || !customSession) { - return
Loading session
; + return
{t('Loading session')}
; } const jumpToEvent = metricStore.instance.data.events.find( diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CohortCard/CohortCard.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CohortCard/CohortCard.tsx index a863fa0a3..3a4c18fcf 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CohortCard/CohortCard.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CohortCard/CohortCard.tsx @@ -1,26 +1,35 @@ 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 { 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], + 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], + 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], + data: [ + 100, 95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30, 25, 20, 15, + ], }, { cohort: '2022-01-08', @@ -103,8 +112,8 @@ function CohortCard(props: Props) { - - + +
DateUsers{t('Date')}{t('Users')}
@@ -125,11 +134,20 @@ function CohortCard(props: Props) { - + {data[0].data.map((_, index) => ( - + ))} @@ -137,9 +155,12 @@ function CohortCard(props: Props) { {data.map((row, rowIndex) => ( {row.data.map((cell, cellIndex) => ( - ))} diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomChartTooltip.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomChartTooltip.tsx index 47339ab50..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,9 +21,8 @@ interface Props { } function CustomTooltip(props: Props) { - const { - active, payload, label, hoveredSeries = null, - } = props; + const { t } = useTranslation(); + const { active, payload, label, hoveredSeries = null } = props; // Return null if tooltip is not active or there is no valid payload if (!active || !payload?.length || !hoveredSeries) return null; @@ -30,8 +30,9 @@ function CustomTooltip(props: Props) { // Find the current and comparison payloads const currentPayload = payload.find((p) => p.name === hoveredSeries); const comparisonPayload = payload.find( - (p) => p.name === `${hoveredSeries.replace(' (Comparison)', '')} (Comparison)` - || p.name === `${hoveredSeries} (Comparison)`, + (p) => + p.name === `${hoveredSeries.replace(' (Comparison)', '')} (Comparison)` || + p.name === `${hoveredSeries} (Comparison)`, ); if (!currentPayload) return null; @@ -44,9 +45,11 @@ function CustomTooltip(props: Props) { }, ]; - const isHigher = (item: { value: number; prevValue: number }) => item.prevValue !== null && item.prevValue < item.value; + const isHigher = (item: { value: number; prevValue: number }) => + item.prevValue !== null && item.prevValue < item.value; - const getPercentDelta = (val: number, prevVal: number) => (((val - prevVal) / prevVal) * 100).toFixed(2); + const getPercentDelta = (val: number, prevVal: number) => + (((val - prevVal) / prevVal) * 100).toFixed(2); return (
@@ -66,12 +69,14 @@ function CustomTooltip(props: Props) { className="flex flex-col px-2 ml-2" >
- {label} - , - {' '} - {p.payload?.timestamp - ? formatTimeOrDate(p.payload.timestamp) - :
'Timestamp is not Applicable'
} + {label},{' '} + {p.payload?.timestamp ? ( + formatTimeOrDate(p.payload.timestamp) + ) : ( +
+ '{t('Timestamp is not Applicable')}' +
+ )}
{p.value}
@@ -98,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} %)
diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomLegend.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomLegend.tsx index c35e46065..08d652160 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomLegend.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomLegend.tsx @@ -10,17 +10,24 @@ function CustomLegend({ payload }: CustomLegendProps) {
{payload?.map((entry) => ( -
+
{entry.value.includes('(Comparison)') ? (
- + {gradientDef} @@ -40,7 +52,7 @@ function CustomMetricOverviewChart(props: Props) { void; + data: any; + params: any; + colors: any; + onClick?: (event, index) => void; } function CustomMetriPercentage(props: Props) { + const { t } = useTranslation(); const { data = {} } = props; return ( -
+
{numberWithCommas(data.count)}
{`${parseInt(data.previousCount || 0)} ( ${Math.floor(parseInt(data.countProgress || 0))}% )`}
-
from previous period.
+
{t('from previous period.')}
); } diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart/CustomMetricPieChart.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart/CustomMetricPieChart.tsx index 1251e1d4f..6641fde9d 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart/CustomMetricPieChart.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart/CustomMetricPieChart.tsx @@ -1,6 +1,11 @@ import React, { useState } from 'react'; import { - ResponsiveContainer, Tooltip, PieChart, Pie, Cell, Legend, + ResponsiveContainer, + Tooltip, + PieChart, + Pie, + Cell, + Legend, } from 'recharts'; import { NoContent } from 'UI'; import { filtersMap } from 'Types/filter/newFilter'; @@ -23,9 +28,7 @@ interface Props { } function CustomMetricPieChart(props: Props) { - const { - metric, data, onClick = () => null, inGrid, - } = props; + const { metric, data, onClick = () => null, inGrid } = props; const [hoveredSeries, setHoveredSeries] = useState(null); @@ -50,7 +53,8 @@ function CustomMetricPieChart(props: Props) { const handleMouseOver = (name: string) => setHoveredSeries(name); const handleMouseLeave = () => setHoveredSeries(null); - const getTotalForSeries = (series: string) => (data.chart ? data.chart.reduce((acc, curr) => acc + curr[series], 0) : 0); + const getTotalForSeries = (series: string) => + data.chart ? data.chart.reduce((acc, curr) => acc + curr[series], 0) : 0; const values = data.namesMap.map((k) => ({ name: k, @@ -71,10 +75,11 @@ function CustomMetricPieChart(props: Props) { > - - } + + } /> [ { @@ -23,16 +24,20 @@ const getColumns = (metric) => [ ]; 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, + metric = {}, + data = { values: [] }, + onClick = () => null, + isTemplate, } = props; + const { t } = useTranslation(); const rows = List(data.values); const onClickHandler = (event: any, data: any) => { @@ -56,12 +61,12 @@ function CustomMetricTable(props: Props) { style={{ minHeight: 220 }} show={data.values && data.values.length === 0} size="small" - title={( + title={
- No data available for the selected period. + {t('No data available for the selected period.')}
- )} + } >
Weeks later users retained + {t('Weeks later users retained')} +
{`${index + 1}`}{`${index + 1}`}
- {cell} - % + + {cell}%
{ e.stopPropagation(); @@ -33,7 +33,10 @@ function CustomMetricTableErrors(props: RouteComponentProps & Props) { right: true, width: 1200, onClose: () => { - if (props.history.location.pathname.includes('/dashboard') || props.history.location.pathname.includes('/metrics/')) { + if ( + props.history.location.pathname.includes('/dashboard') || + props.history.location.pathname.includes('/metrics/') + ) { props.history.replace({ search: '' }); } }, @@ -46,26 +49,26 @@ function CustomMetricTableErrors(props: RouteComponentProps & Props) { return ( - No data available for the selected period. + {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)} - /> -
- ))} + {data.errors && + data.errors.map((error: any, index: any) => ( +
+ onErrorClick(e, error)} + /> +
+ ))} {/* {isEdit && ( */}
@@ -87,20 +90,6 @@ function CustomMetricTableErrors(props: RouteComponentProps & Props) { ); } -export default withRouter(CustomMetricTableErrors); - -function ViewMore({ total, limit }: any) { - return total > limit && ( -
-
-
- All - {' '} - {total} - {' '} - errors -
-
-
- ); -} +export default withRouter( + CustomMetricTableErrors, +); diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions/CustomMetricTableSessions.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions/CustomMetricTableSessions.tsx index e20dee7b1..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,27 +15,34 @@ interface Props { } function CustomMetricTableSessions(props: Props) { + const { t } = useTranslation(); const { isEdit = false, metric, data } = props; - const sessions = useMemo(() => (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')}
- )} + } >
- {sessions - && sessions.map((session: any, index: any) => ( + {sessions && + sessions.map((session: any, index: any) => (
@@ -61,15 +69,13 @@ function CustomMetricTableSessions(props: Props) { export default observer(CustomMetricTableSessions); function ViewMore({ total, limit }: any) { + const { t } = useTranslation(); return total > limit ? (
- All - {' '} - {total} - {' '} - sessions + {t('All')} {total}  + {t('sessions')}
diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/InsightItem.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/InsightItem.tsx index 18fed6423..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; @@ -10,17 +11,22 @@ interface Props { } function InsightItem(props: Props) { const { item, onClick = () => {} } = props; - const className = 'flex items-start py-3 hover:bg-active-blue -mx-4 px-4 border-b last:border-transparent cursor-pointer'; + const className = + 'flex items-start py-3 hover:bg-active-blue -mx-4 px-4 border-b last:border-transparent cursor-pointer'; switch (item.category) { case IssueCategory.RAGE: return ; case IssueCategory.RESOURCES: - return ; + return ( + + ); case IssueCategory.ERRORS: return ; case IssueCategory.NETWORK: - return ; + return ( + + ); default: return null; } @@ -47,33 +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
-
- {item.value} - , -
-
representing a
+
{t('to')}
+
{item.value},
+
{t('representing a')}
-
across all sessions.
+
{t('across all sessions.')}
)}
@@ -81,32 +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')}
@@ -114,30 +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
+
{t('Click rage has')}
- {item.isIncreased ? 'increased' : 'decreased'} - {' '} - on + {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 80ba646e8..0f28719d7 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/InsightsCard.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/InsightsCard.tsx @@ -4,21 +4,32 @@ import { observer } from 'mobx-react-lite'; import React from 'react'; 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; + 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]; @@ -58,18 +69,22 @@ function InsightsCard({ data }: any) { return ( - No data available for the selected period. + {t('No data available for the selected period.')}
- )} + } show={data.issues && data.issues.length === 0} >
- {data.issues - && data.issues.map((item: any) => ( - clickHanddler(e, item)} /> + {data.issues && + data.issues.map((item: any) => ( + clickHanddler(e, item)} + /> ))}
diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/SessionsBy.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/SessionsBy.tsx index 4584e5f3a..baa26b6ea 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/SessionsBy.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/SessionsBy.tsx @@ -5,6 +5,7 @@ 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; @@ -15,12 +16,19 @@ interface Props { function SessionsBy(props: Props) { const { - metric = {}, data = { values: [] }, onClick = () => null, isTemplate, + metric = {}, + data = { values: [] }, + onClick = () => null, + isTemplate, } = props; + const { t } = useTranslation(); const [selected, setSelected] = React.useState(null); 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 = { @@ -29,14 +37,26 @@ function SessionsBy(props: Props) { type: filtersMap[metric.metricOf].key, filters: filtersMap[metric.metricOf].filters?.map((f: any) => { const { - key, operatorOptions, category, icon, label, options, ...cleaned + key, + operatorOptions, + category, + icon, + label, + options, + ...cleaned } = f; return { ...cleaned, type: f.key, value: [] }; }), }; const { - key, operatorOptions, category, icon, label, options, ...finalFilter + key, + operatorOptions, + category, + icon, + label, + options, + ...finalFilter } = baseFilter; setSelected(data.name); @@ -45,20 +65,23 @@ function SessionsBy(props: Props) { const showMore = (e: any) => { e.stopPropagation(); - openModal( { - closeModal(); - onClickHandler(null, item); - }} - selected={selected} - />, { - title: metric.name, - width: 600, - }); + openModal( + { + closeModal(); + onClickHandler(null, item); + }} + selected={selected} + />, + { + title: metric.name, + width: 600, + }, + ); }; return ( @@ -69,29 +92,30 @@ function SessionsBy(props: Props) { style={{ minHeight: 220 }} className="flex flex-col items-center justify-center" imageStyle={{ height: 0 }} - description={( + description={
- No data available for the selected period. + {t('No data available for the selected period.')}
- )} + } /> ) : ( -
+
{metric && ( - + )} {total > 3 && (
diff --git a/frontend/app/components/Dashboard/Widgets/ListWithIcons.tsx b/frontend/app/components/Dashboard/Widgets/ListWithIcons.tsx index 100f0127a..06ec63c2f 100644 --- a/frontend/app/components/Dashboard/Widgets/ListWithIcons.tsx +++ b/frontend/app/components/Dashboard/Widgets/ListWithIcons.tsx @@ -32,10 +32,12 @@ function ListWithIcons({ list = [] }: Props) {
- {row.name} + + {row.name} + {' '} {row.value} @@ -57,7 +59,7 @@ function ListWithIcons({ list = [] }: Props) { }} />
- )} + } /> )} diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CPULoad/CPULoad.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CPULoad/CPULoad.tsx index 749bca38d..0f59f35e1 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CPULoad/CPULoad.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CPULoad/CPULoad.tsx @@ -1,41 +1,51 @@ import React from 'react'; import { NoContent, Icon } from 'UI'; import { - AreaChart, Area, - CartesianGrid, Tooltip, + AreaChart, + Area, + CartesianGrid, + Tooltip, ResponsiveContainer, - XAxis, YAxis, + XAxis, + YAxis, } from 'recharts'; import { Styles } from '../../common'; +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 { t } = useTranslation(); return ( - No data available for the selected period. + {t('No data available for the selected period.')}
- )} + } show={metric.data.chart && metric.data.chart.length === 0} style={{ height: '240px' }} > - + {gradientDef} - - + + getRE(serach, 'i').test(value); - const _data = search ? data.chart.filter((i: any) => test(i.urlHostpath, search)) : data.chart; + const _data = search + ? data.chart.filter((i: any) => test(i.urlHostpath, search)) + : data.chart; const write = ({ target: { name, value } }: any) => { setSearch(value); @@ -62,19 +64,23 @@ function CallWithErrors(props: Props) { return ( - - {' '} - { NO_METRIC_DATA } + {NO_METRIC_DATA}
- )} + } show={data.chart.length === 0} style={{ height: '240px' }} >
- +
- + ); } diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors4xx/CallsErrors4xx.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors4xx/CallsErrors4xx.tsx index caa1303cf..f827dd220 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors4xx/CallsErrors4xx.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors4xx/CallsErrors4xx.tsx @@ -1,39 +1,43 @@ import React from 'react'; import { NoContent } from 'UI'; import { - CartesianGrid, Tooltip, - LineChart, Line, Legend, ResponsiveContainer, - XAxis, YAxis, + 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 } + {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) => ( + + ))} diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors5xx/CallsErrors5xx.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors5xx/CallsErrors5xx.tsx index f34cd325c..1feeb041c 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors5xx/CallsErrors5xx.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/CallsErrors5xx/CallsErrors5xx.tsx @@ -1,36 +1,44 @@ import React from 'react'; import { NoContent, Icon } from 'UI'; import { - CartesianGrid, Tooltip, - LineChart, Line, Legend, ResponsiveContainer, - XAxis, YAxis, + 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 { t } = useTranslation(); const { data, metric } = props; return ( - No data available for the selected period. + {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) => ( + + ))} diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/Crashes/Crashes.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/Crashes/Crashes.tsx index 95bbb498b..4fccd1469 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/Crashes/Crashes.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/Crashes/Crashes.tsx @@ -1,18 +1,21 @@ import React from 'react'; import { NoContent } from 'UI'; import { - AreaChart, Area, - CartesianGrid, Tooltip, + AreaChart, + Area, + CartesianGrid, + Tooltip, ResponsiveContainer, - XAxis, YAxis, + 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; @@ -20,24 +23,27 @@ function Crashes(props: Props) { return ( - - {' '} - { NO_METRIC_DATA } + {NO_METRIC_DATA} - )} + } show={metric.data.chart.length === 0} style={{ height: '240px' }} > - + {gradientDef} - - + + - - {' '} - { NO_METRIC_DATA } + {NO_METRIC_DATA} -)} - + } show={metric.data.chart.length === 0} > <>
- +
- + {gradientDef} - - + + - - {' '} - { NO_METRIC_DATA } + {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' }} + label={{ ...Styles.axisLabelLeft, value: t('Number of Errors') }} allowDecimals={false} /> - 1 - st - {' '} - Party + st Party -)} + } dataKey="firstParty" stackId="a" fill={Styles.compareColors[0]} /> - 3 - rd - {' '} - Party + rd Party -)} + } dataKey="thirdParty" stackId="a" fill={Styles.compareColors[2]} diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsByType/ErrorsByType.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsByType/ErrorsByType.tsx index 053101237..458bcf73d 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsByType/ErrorsByType.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsByType/ErrorsByType.tsx @@ -1,39 +1,43 @@ import React from 'react'; import { NoContent } from 'UI'; import { - BarChart, Bar, CartesianGrid, Tooltip, - Legend, ResponsiveContainer, - XAxis, YAxis, + 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 } + {NO_METRIC_DATA} - )} + } show={metric.data.chart.length === 0} style={{ height: '240px' }} > - - + + - - - - + + + + diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/Bar.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/Bar.tsx index f5e3efb41..9ba177c32 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/Bar.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/Bar.tsx @@ -1,13 +1,14 @@ import React from 'react'; import stl from './bar.module.css'; -function 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}`}
diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/ErrorsPerDomain.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/ErrorsPerDomain.tsx index 4af3f23c4..403e580b4 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/ErrorsPerDomain.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsPerDomain/ErrorsPerDomain.tsx @@ -23,13 +23,11 @@ function ErrorsPerDomain(props: Props) { size="small" show={data.chart.length === 0} style={{ height: '240px' }} - title={( + title={
- - {' '} - { NO_METRIC_DATA } + {NO_METRIC_DATA}
- )} + } >
diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/FPS/FPS.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/FPS/FPS.tsx index faa6601ab..93de5b764 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/FPS/FPS.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/FPS/FPS.tsx @@ -1,18 +1,21 @@ import React from 'react'; import { NoContent } from 'UI'; import { - AreaChart, Area, - CartesianGrid, Tooltip, + AreaChart, + Area, + CartesianGrid, + Tooltip, ResponsiveContainer, - XAxis, YAxis, + 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; @@ -21,13 +24,11 @@ function FPS(props: Props) { return ( - - {' '} - { NO_METRIC_DATA } + {NO_METRIC_DATA}
- )} + } show={metric.data.chart.length === 0} > <> @@ -35,13 +36,18 @@ function FPS(props: Props) {
- + {gradientDef} - - + + - - {' '} - { NO_METRIC_DATA } + {NO_METRIC_DATA}
- )} + } > <>
- + {gradientDef} - - + + - - {' '} - { NO_METRIC_DATA } + {NO_METRIC_DATA}
- )} + } show={metric.data.chart.length === 0} > <> @@ -45,21 +47,34 @@ function ResponseTime(props: Props) { onSelect={onSelect} placeholder="Search for Page" /> */} - +
- + {gradientDef} - - + + Styles.tickFormatter(val)} - label={{ ...Styles.axisLabelLeft, value: 'Page Response Time (ms)' }} + label={{ + ...Styles.axisLabelLeft, + value: 'Page Response Time (ms)', + }} /> - - {' '} - { NO_METRIC_DATA } + {NO_METRIC_DATA} - )} + } show={data.chart.length === 0} style={{ height: '240px' }} > @@ -69,7 +74,11 @@ function ResponseTimeDistribution(props: Props) { margin={Styles.chartMargins} barSize={50} > - + - - `Page Response Time: ${val}`} /> - { data.percentiles && data.percentiles.map((item: any, i: number) => ( - - )} - // allowDecimals={false} - x={item.responseTime} - strokeWidth={0} - strokeOpacity={1} - /> - ))} + + `Page Response Time: ${val}`} + /> + {data.percentiles && + data.percentiles.map((item: any, i: number) => ( + + } + // allowDecimals={false} + x={item.responseTime} + strokeWidth={0} + strokeOpacity={1} + /> + ))} @@ -120,11 +140,27 @@ function ResponseTimeDistribution(props: Props) { margin={Styles.chartMargins} barSize={40} > - + - + - +
diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsAffectedByJSErrors/SessionsAffectedByJSErrors.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsAffectedByJSErrors/SessionsAffectedByJSErrors.tsx index 1a018bf0f..1c28b9a66 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsAffectedByJSErrors/SessionsAffectedByJSErrors.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsAffectedByJSErrors/SessionsAffectedByJSErrors.tsx @@ -1,39 +1,43 @@ import React from 'react'; import { NoContent } from 'UI'; import { - BarChart, Bar, CartesianGrid, Tooltip, - Legend, ResponsiveContainer, - XAxis, YAxis, + 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 SessionsAffectedByJSErrors(props: Props) { const { data, metric } = props; return ( - - {' '} - { NO_METRIC_DATA } + {NO_METRIC_DATA} - )} + } size="small" show={metric.data.chart.length === 0} style={{ height: '240px' }} > - - + + - + diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsImpactedBySlowRequests/SessionsImpactedBySlowRequests.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsImpactedBySlowRequests/SessionsImpactedBySlowRequests.tsx index a3f5960d4..31461cf31 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsImpactedBySlowRequests/SessionsImpactedBySlowRequests.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsImpactedBySlowRequests/SessionsImpactedBySlowRequests.tsx @@ -1,18 +1,21 @@ import React from 'react'; import { NoContent } from 'UI'; import { - AreaChart, Area, - CartesianGrid, Tooltip, + AreaChart, + Area, + CartesianGrid, + Tooltip, ResponsiveContainer, - XAxis, YAxis, + 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; @@ -20,24 +23,27 @@ function SessionsImpactedBySlowRequests(props: Props) { return ( - - {' '} - { NO_METRIC_DATA } + {NO_METRIC_DATA} - )} + } size="small" show={metric.data.chart.length === 0} > - + {gradientDef} - - + +
-
+
{versions.map((v, i) => { const w = (v.value * 100) / avg; return ( -
+
- Version: + {t('Version:')} {v.key}
- Sessions: + {t('Sessions:')} {v.value}
- )} + } />
); diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser/SessionsPerBrowser.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser/SessionsPerBrowser.tsx index 51b701ab2..126db532e 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser/SessionsPerBrowser.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser/SessionsPerBrowser.tsx @@ -13,19 +13,21 @@ function SessionsPerBrowser(props: Props) { const { data } = props; const firstAvg = data.chart[0] && data.chart[0].count; - const getVersions = (item) => 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 }} > diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/Bar.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/Bar.tsx index 9f172b607..c5f6bac1a 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/Bar.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/Bar.tsx @@ -1,16 +1,19 @@ import React from 'react'; import stl from './Bar.module.css'; +import { useTranslation } from 'react-i18next'; -function Bar({ - className = '', width = 0, avg, domain, color, -}) { +function Bar({ className = '', width = 0, avg, domain, color }) { + const { t } = useTranslation(); return (
-
+
{avg} - ms +  {t('ms')}
{domain}
diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/SlowestDomains.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/SlowestDomains.tsx index eb5232a40..7c31b18e6 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/SlowestDomains.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SlowestDomains/SlowestDomains.tsx @@ -24,13 +24,11 @@ function SlowestDomains(props: Props) { size="small" show={list.length === 0} style={{ minHeight: 220 }} - title={( + title={
- - {' '} - { NO_METRIC_DATA } + {NO_METRIC_DATA}
- )} + } >
diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SpeedIndexByLocation/Scale.js b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SpeedIndexByLocation/Scale.tsx similarity index 53% rename from frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SpeedIndexByLocation/Scale.js rename to frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SpeedIndexByLocation/Scale.tsx index 8d914c2af..3d7b588fa 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SpeedIndexByLocation/Scale.js +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SpeedIndexByLocation/Scale.tsx @@ -2,9 +2,11 @@ import React from 'react'; 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 (
@@ -12,12 +14,15 @@ function Scale({ colors }) {
- {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 ea50f20f4..ae343f7c5 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SpeedIndexByLocation/SpeedIndexByLocation.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SpeedIndexByLocation/SpeedIndexByLocation.tsx @@ -10,12 +10,14 @@ 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; @@ -73,13 +81,11 @@ function SpeedIndexByLocation(props: Props) { size="small" show={false} style={{ height: '240px' }} - title={( + title={
- - {' '} - { NO_METRIC_DATA } + {NO_METRIC_DATA}
- )} + } >
@@ -109,9 +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/TimeToRender/TimeToRender.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/TimeToRender/TimeToRender.tsx index 7f260189f..1e903ce07 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/TimeToRender/TimeToRender.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/TimeToRender/TimeToRender.tsx @@ -2,10 +2,13 @@ import React from 'react'; import { NoContent } from 'UI'; import { withRequest } from 'HOCs'; import { - AreaChart, Area, - CartesianGrid, Tooltip, + AreaChart, + Area, + CartesianGrid, + Tooltip, ResponsiveContainer, - XAxis, YAxis, + XAxis, + YAxis, } from 'recharts'; import { toUnderscore } from 'App/utils'; import { NO_METRIC_DATA } from 'App/constants/messages'; @@ -15,11 +18,11 @@ 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; @@ -35,13 +38,11 @@ function TimeToRender(props: Props) { - - {' '} - { NO_METRIC_DATA } + {NO_METRIC_DATA}
- )} + } > <>
@@ -52,19 +53,29 @@ function TimeToRender(props: Props) { onSelect={onSelect} placeholder="Search for Page" /> */} - +
- + {gradientDef} - - + + Styles.tickFormatter(val)} label={{ ...Styles.axisLabelLeft, value: 'Time to Render (ms)' }} /> diff --git a/frontend/app/components/Dashboard/Widgets/common/AvgLabel.js b/frontend/app/components/Dashboard/Widgets/common/AvgLabel.js index 13c68a87e..087e3b3e0 100644 --- a/frontend/app/components/Dashboard/Widgets/common/AvgLabel.js +++ b/frontend/app/components/Dashboard/Widgets/common/AvgLabel.js @@ -1,9 +1,7 @@ import React from 'react'; import { numberWithCommas } from 'App/utils'; -function AvgLabel({ - className = '', text, count, unit, -}) { +function AvgLabel({ className = '', text, count, unit }) { return (
{text} diff --git a/frontend/app/components/Dashboard/Widgets/common/CountBadge.js b/frontend/app/components/Dashboard/Widgets/common/CountBadge.js index 999592155..ad639642f 100644 --- a/frontend/app/components/Dashboard/Widgets/common/CountBadge.js +++ b/frontend/app/components/Dashboard/Widgets/common/CountBadge.js @@ -28,22 +28,24 @@ function CountBadge({ 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 65% rename from frontend/app/components/Dashboard/Widgets/common/CustomTooltip.js rename to frontend/app/components/Dashboard/Widgets/common/CustomTooltip.tsx index dc4eb27f2..7ac8af6e1 100644 --- a/frontend/app/components/Dashboard/Widgets/common/CustomTooltip.js +++ b/frontend/app/components/Dashboard/Widgets/common/CustomTooltip.tsx @@ -1,25 +1,27 @@ import React from 'react'; import { numberWithCommas } from 'App/utils'; +import { useTranslation } from 'react-i18next'; 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')}}
); } -function CustomTooltip({ - active, payload, label, unit, -}) { +function CustomTooltip({ active, payload, label, unit }) { if (active && payload && payload[0]) { return (
{`${label}`}
- {payload.map((p) => ())} + {payload.map((p) => ( + + ))}
); } diff --git a/frontend/app/components/Dashboard/Widgets/common/SessionLine.js b/frontend/app/components/Dashboard/Widgets/common/SessionLine.js index f2e862a57..a36c55b32 100644 --- a/frontend/app/components/Dashboard/Widgets/common/SessionLine.js +++ b/frontend/app/components/Dashboard/Widgets/common/SessionLine.js @@ -2,29 +2,15 @@ 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'; -function FeedbackLine({ - icon, - info, - subInfo, - sessionId, -}) { +function FeedbackLine({ icon, info, subInfo, sessionId }) { return (
- { icon && } + {icon && }
- { info - && ( - - { info } - - )} - { subInfo - && ( -
- { subInfo } -
- )} + {info && {info}} + {subInfo &&
{subInfo}
}
diff --git a/frontend/app/components/Dashboard/Widgets/common/Styles.js b/frontend/app/components/Dashboard/Widgets/common/Styles.js index 9e3c7fa94..b4f90cf68 100644 --- a/frontend/app/components/Dashboard/Widgets/common/Styles.js +++ b/frontend/app/components/Dashboard/Widgets/common/Styles.js @@ -2,13 +2,46 @@ import React from 'react'; 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 safeColors = [ + '#394EFF', + '#3EAAAF', + '#9276da', + '#ceba64', + '#bc6f9d', + '#966fbc', + '#64ce86', + '#e06da3', + '#6dabe0', +]; const countView = (count) => { const isMoreThanK = count >= 1000; @@ -51,7 +84,10 @@ export default { tickFormatter: (val) => `${countView(val)}`, tickFormatterBytes: (val) => Math.round(val / 1024 / 1024), chartMargins: { - left: 0, right: 20, top: 10, bottom: 5, + left: 0, + right: 20, + top: 10, + bottom: 5, }, tooltip: { wrapperStyle: { diff --git a/frontend/app/components/Dashboard/Widgets/common/Table.js b/frontend/app/components/Dashboard/Widgets/common/Table.js index a24355f78..c1974d76f 100644 --- a/frontend/app/components/Dashboard/Widgets/common/Table.js +++ b/frontend/app/components/Dashboard/Widgets/common/Table.js @@ -24,53 +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 - ? - : ( + {cols.map( + ({ + cellClass = '', + className = '', + Component, + key, + toText = (t) => t, + width, + }) => ( +
+ {' '} + {Component ? ( + + ) : (
{' '} - { toText(row[key]) } - {' '} + {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 aa3bf8f84..ee369e6f8 100644 --- a/frontend/app/components/Dashboard/Widgets/common/Title.js +++ b/frontend/app/components/Dashboard/Widgets/common/Title.js @@ -1,10 +1,11 @@ import styles from './title.module.css'; +import React from 'react'; function Title({ title, sub }) { return (
-

{ title }

- { sub } +

{title}

+ {sub}
); } diff --git a/frontend/app/components/Dashboard/components/AddCardModal/AddCardModal.tsx b/frontend/app/components/Dashboard/components/AddCardModal/AddCardModal.tsx index d0e583fba..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 8048be113..4cccdb14b 100644 --- a/frontend/app/components/Dashboard/components/AddCardSection/AddCardSection.tsx +++ b/frontend/app/components/Dashboard/components/AddCardSection/AddCardSection.tsx @@ -30,6 +30,8 @@ import { dashboardMetricCreate, withSiteId, metricCreate } from 'App/routes'; import { FilterKey } from 'Types/filter/filterType'; 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', + 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', + title: t('4xx/5xx Requests'), type: `${TIMESERIES}_4xx_requests`, - description: 'Track client and server errors for performance issues.', + description: t('Track client and server errors for performance issues.'), }, { icon: , - title: 'Slow Network Requests', + title: t('Slow Network Requests'), type: `${TIMESERIES}_slow_network_requests`, - description: 'Pinpoint the slowest network requests causing delays.', + 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(); @@ -212,9 +213,7 @@ function CategoryTab({ {item.icon}
{item.title}
-
+
{item.description}
@@ -232,6 +231,7 @@ const AddCardSection = observer( inCards?: boolean; handleOpenChange?: (isOpen: boolean) => void; }) => { + const { t } = useTranslation(); const { showModal } = useModal(); const { metricStore, dashboardStore, projectsStore } = useStore(); const { isMobile } = projectsStore; @@ -241,14 +241,14 @@ const AddCardSection = observer( const options = isMobile ? [ - // { label: 'Product Analytics', value: 'product_analytics' }, - { label: 'Mobile Analytics', value: 'web_analytics' }, - ] + // { label: 'Product Analytics', value: 'product_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; const isSaas = /api\.openreplay\.com/.test(originStr); @@ -268,19 +268,15 @@ const AddCardSection = observer( handleOpenChange?.(false); }; return ( -
+
- What do you want to visualize? + {t('What do you want to visualize?')}
{isSaas ? ( -
+
-
Ask AI
+
{t('Ask AI')}
) : null}
@@ -298,9 +294,7 @@ const AddCardSection = observer(
{inCards ? null : ( -
+
)} diff --git a/frontend/app/components/Dashboard/components/AddCardSelectionModal.tsx b/frontend/app/components/Dashboard/components/AddCardSelectionModal.tsx index bf539d11a..dd2de2cf0 100644 --- a/frontend/app/components/Dashboard/components/AddCardSelectionModal.tsx +++ b/frontend/app/components/Dashboard/components/AddCardSelectionModal.tsx @@ -1,6 +1,4 @@ -import { - Card, Col, Modal, Row, Typography, -} from 'antd'; +import { Card, Col, Modal, Row, Typography } from 'antd'; import { GalleryVertical, Plus } from 'lucide-react'; import React from 'react'; @@ -8,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; @@ -15,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); @@ -37,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 d02dca154..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 }) => ( <> @@ -51,6 +56,7 @@ export const showAddToDashboardModal = (metricId: string, dashboardStore: any) = }; function AddToDashboardButton({ metricId }: Props) { + const { t } = useTranslation(); const { dashboardStore } = useStore(); return ( @@ -59,7 +65,7 @@ function AddToDashboardButton({ metricId }: Props) { onClick={() => showAddToDashboardModal(metricId, dashboardStore)} icon={} > - Add to Dashboard + {t('Add to Dashboard')} ); } diff --git a/frontend/app/components/Dashboard/components/Alerts/AlertForm/BottomButtons.tsx b/frontend/app/components/Dashboard/components/Alerts/AlertForm/BottomButtons.tsx index ff2bbd0a7..1acc57fd8 100644 --- a/frontend/app/components/Dashboard/components/Alerts/AlertForm/BottomButtons.tsx +++ b/frontend/app/components/Dashboard/components/Alerts/AlertForm/BottomButtons.tsx @@ -1,17 +1,22 @@ 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, + loading, + instance, + deleting, + onDelete, }: IBottomButtons) { + const { t } = useTranslation(); return ( <>
@@ -21,22 +26,21 @@ function BottomButtons({ disabled={loading || !instance.validate()} id="submit-button" > - {instance.exists() ? 'Update' : 'Create'} + {instance.exists() ? t('Update') : t('Create')}
{instance.exists() && ( - + )}
diff --git a/frontend/app/components/Dashboard/components/Alerts/AlertForm/Condition.tsx b/frontend/app/components/Dashboard/components/Alerts/AlertForm/Condition.tsx index 66583c61b..e57530aee 100644 --- a/frontend/app/components/Dashboard/components/Alerts/AlertForm/Condition.tsx +++ b/frontend/app/components/Dashboard/components/Alerts/AlertForm/Condition.tsx @@ -3,19 +3,21 @@ import { Input } from 'UI'; import Select from 'Shared/Select'; import { alertConditions as conditions } from 'App/constants'; 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 })} + onClick={() => + dashboard.update({ isPublic: !dashboard.isPublic }) + } /> -
dashboard.update({ isPublic: !dashboard.isPublic })}> +
+ dashboard.update({ isPublic: !dashboard.isPublic }) + } + > - Team can see and edit the dashboard. + + {' '} + {t('Team can see and edit the dashboard.')} +
@@ -93,14 +103,12 @@ function DashboardEditModal(props: Props) {
- + -
diff --git a/frontend/app/components/Dashboard/components/DashboardForm/DashboardForm.tsx b/frontend/app/components/Dashboard/components/DashboardForm/DashboardForm.tsx index 520eb1623..c98612605 100644 --- a/frontend/app/components/Dashboard/components/DashboardForm/DashboardForm.tsx +++ b/frontend/app/components/Dashboard/components/DashboardForm/DashboardForm.tsx @@ -3,15 +3,17 @@ 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 { t } = useTranslation(); const { dashboardStore } = useStore(); const dashboard = dashboardStore.dashboardInstance; - const write = ({ target: { value, name } }) => dashboard.update({ [name]: value }); + const write = ({ target: { value, name } }) => + dashboard.update({ [name]: value }); const writeRadio = ({ target: { value, name } }) => { dashboard.update({ [name]: value === 'team' }); }; @@ -19,12 +21,23 @@ function DashboardForm(props: Props) { return useObserver(() => (
- - + +
- +
diff --git a/frontend/app/components/Dashboard/components/DashboardHeader/DashboardHeader.tsx b/frontend/app/components/Dashboard/components/DashboardHeader/DashboardHeader.tsx index f840ca367..adee84b0c 100644 --- a/frontend/app/components/Dashboard/components/DashboardHeader/DashboardHeader.tsx +++ b/frontend/app/components/Dashboard/components/DashboardHeader/DashboardHeader.tsx @@ -12,6 +12,7 @@ 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; + const { period } = dashboardStore; const dashboard: any = dashboardStore.selectedDashboard; @@ -42,9 +44,11 @@ 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(() => { @@ -66,14 +70,13 @@ function DashboardHeader(props: Props) { -
{dashboard?.name}
+ +
+ {' '} + {dashboard?.name} +
- )} + } onClick={() => onEdit(true)} className="mr-3 select-none border-b border-b-borderColor-transparent hover:border-dashed hover:border-gray-medium cursor-pointer" /> @@ -82,18 +85,17 @@ function DashboardHeader(props: Props) { className="flex items-center gap-2" style={{ flex: 1, justifyContent: 'end' }} > - } - overlayInnerStyle={{ padding: 0, borderRadius: '0.75rem' }} - > - - + (null); @@ -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(), @@ -89,17 +93,19 @@ function DashboardList() { { title: (
-
Visibility
+
{t('Visibility')}
dashboardStore.updateKey('filter', { - ...dashboardStore.filter, - showMine: !dashboardStore.filter.showMine, - })} + onChange={() => + dashboardStore.updateKey('filter', { + ...dashboardStore.filter, + showMine: !dashboardStore.filter.showMine, + }) + } checkedChildren="Team" unCheckedChildren="Private" className="toggle-team-private" @@ -115,7 +121,7 @@ function DashboardList() { bordered={false} className="rounded-lg" > - {isPublic ? 'Team' : 'Private'} + {isPublic ? t('Team') : t('Private')} ), }, @@ -135,17 +141,17 @@ function DashboardList() { { icon: , key: 'rename', - label: 'Rename', + label: t('Rename'), }, { icon: , key: 'access', - label: 'Visibility & Access', + label: t('Visibility & Access'), }, { icon: , key: 'delete', - label: 'Delete', + label: t('Delete'), }, ], onClick: async ({ key }) => { @@ -171,35 +177,42 @@ function DashboardList() { }, ]; - const emptyDescription = dashboardsSearch !== '' ? ( -
-
- - No matching results - -
- Try adjusting your search criteria or creating a new dashboard. + const emptyDescription = + dashboardsSearch !== '' ? ( +
+
+ + {t('No matching results')} + +
+ {t( + 'Try adjusting your search criteria or creating a new dashboard.', + )} +
-
- ) : ( -
-
- - Create and organize your insights - -
- Build dashboards to track key metrics and monitor performance in one place. -
-
- + ) : ( +
+
+ + {t('Create and organize your insights')} + +
+ {t( + 'Build dashboards to track key metrics and monitor performance in one place.', + )} +
+
+ +
-
- ); + ); - const emptyImage = dashboardsSearch !== '' ? ICONS.NO_RESULTS : ICONS.NO_DASHBOARDS; - const imageDimensions = dashboardsSearch !== '' ? searchImageDimensions : defaultImageDimensions; + const emptyImage = + dashboardsSearch !== '' ? ICONS.NO_RESULTS : ICONS.NO_DASHBOARDS; + const imageDimensions = + dashboardsSearch !== '' ? searchImageDimensions : defaultImageDimensions; return list.length === 0 && !dashboardStore.filter.showMine ? (
@@ -220,20 +233,22 @@ function DashboardList() { dataSource={list} columns={tableConfig} pagination={{ - showTotal: (total, range) => `Showing ${range[0]}-${range[1]} of ${total} items`, + showTotal: (total, range) => + `${t('Showing')} ${range[0]}-${range[1]} ${t('of')} ${total} ${t('items')}`, size: 'small', simple: 'true', className: 'px-4 pr-8 mb-0', }} onRow={(record) => ({ onClick: (e) => { - const possibleDropdown = document.querySelector('.ant-dropdown-menu'); + const possibleDropdown = + document.querySelector('.ant-dropdown-menu'); const btn = document.querySelector('#ignore-prop'); if ( - e.target.classList.contains('lucide') - || e.target.id === 'ignore-prop' - || possibleDropdown?.contains(e.target) - || btn?.contains(e.target) + e.target.classList.contains('lucide') || + e.target.id === 'ignore-prop' || + possibleDropdown?.contains(e.target) || + btn?.contains(e.target) ) { return; } diff --git a/frontend/app/components/Dashboard/components/DashboardList/DashboardSearch.tsx b/frontend/app/components/Dashboard/components/DashboardList/DashboardSearch.tsx index 00c4e8510..bff9d1fe0 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/DashboardSearch.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/DashboardSearch.tsx @@ -11,7 +11,11 @@ function DashboardSearch() { const [query, setQuery] = useState(dashboardStore.dashboardsSearch); useEffect(() => { debounceUpdate = debounce( - (key: string, value: any) => dashboardStore.updateKey('filter', { ...dashboardStore.filter, query: value }), + (key: string, value: any) => + dashboardStore.updateKey('filter', { + ...dashboardStore.filter, + query: value, + }), 500, ); }, []); @@ -30,7 +34,12 @@ function DashboardSearch() { className="w-full btn-search-dashboard" placeholder="Filter by dashboard title" onChange={write} - onSearch={(value) => dashboardStore.updateKey('filter', { ...dashboardStore.filter, query: value })} + onSearch={(value) => + dashboardStore.updateKey('filter', { + ...dashboardStore.filter, + query: value, + }) + } /> ); } diff --git a/frontend/app/components/Dashboard/components/DashboardList/DashboardsView.tsx b/frontend/app/components/Dashboard/components/DashboardList/DashboardsView.tsx index d283e8b69..5db043e89 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/DashboardsView.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/DashboardsView.tsx @@ -5,7 +5,10 @@ import Header from './Header'; function DashboardsView({ history, siteId }: { history: any; siteId: string }) { return ( -
+
diff --git a/frontend/app/components/Dashboard/components/DashboardList/Header.tsx b/frontend/app/components/Dashboard/components/DashboardList/Header.tsx index 3d7bcb7ce..c2cfaf53a 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/Header.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/Header.tsx @@ -17,13 +17,13 @@ function Header() {
{showSearch && ( -
- -
-
- +
+ +
+
+ +
-
)}
); diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/CardsLibrary.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/CardsLibrary.tsx index 864c1658a..f4766acc2 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/CardsLibrary.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/CardsLibrary.tsx @@ -10,7 +10,9 @@ const CARD_TYPES_MAP = CARD_CATEGORIES.reduce((acc: any, category: any) => { return acc; }, {}); -const WidgetChart = lazy(() => import('Components/Dashboard/components/WidgetChart/WidgetChart')); +const WidgetChart = lazy( + () => import('Components/Dashboard/components/WidgetChart/WidgetChart'), +); interface Props { category?: string; @@ -29,7 +31,13 @@ function CardsLibrary(props: Props) { // }); // }, [metricStore.filteredCards, props.category]); - const cards = useMemo(() => metricStore.filteredCards.filter((card: any) => card.name.toLowerCase().includes(query.toLowerCase())), [query, metricStore.filteredCards]); + const cards = useMemo( + () => + metricStore.filteredCards.filter((card: any) => + card.name.toLowerCase().includes(query.toLowerCase()), + ), + [query, metricStore.filteredCards], + ); useEffect(() => { metricStore.fetchList(); diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/CreateCard.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/CreateCard.tsx index fe1d8d182..90b92f80d 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/CreateCard.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/CreateCard.tsx @@ -1,31 +1,23 @@ import React from 'react'; import { Button, Space } from 'antd'; import { ArrowLeft, ArrowRight } from 'lucide-react'; -import CardBuilder from 'Components/Dashboard/components/WidgetForm/CardBuilder'; import { useHistory } from 'react-router'; import { useStore } from 'App/mstore'; import { HEATMAP } from 'App/constants/card'; import { renderClickmapThumbnail } from 'Components/Dashboard/components/WidgetForm/renderMap'; import WidgetPreview from 'Components/Dashboard/components/WidgetPreview/WidgetPreview'; import WidgetFormNew from 'Components/Dashboard/components/WidgetForm/WidgetFormNew'; - -const getTitleByType = (type: string) => { - switch (type) { - case HEATMAP: - return 'Heatmap'; - default: - return 'Trend Single'; - } -}; +import { useTranslation } from 'react-i18next'; interface Props { - // cardType: string, - onBack?: () => void - onAdded?: () => void - extra?: React.ReactNode + // cardType: string, + onBack?: () => void; + onAdded?: () => void; + extra?: React.ReactNode; } function CreateCard(props: Props) { + const { t } = useTranslation(); const history = useHistory(); const { metricStore, dashboardStore, aiFiltersStore } = useStore(); const metric = metricStore.instance; @@ -44,7 +36,11 @@ function CreateCard(props: Props) { }); }; - const addCardToDashboard = async (dashboardId: string, metricId: string) => dashboardStore.addWidgetToDashboard(dashboardStore.getDashboard(parseInt(dashboardId, 10))!, [metricId]); + const addCardToDashboard = async (dashboardId: string, metricId: string) => + dashboardStore.addWidgetToDashboard( + dashboardStore.getDashboard(parseInt(dashboardId, 10))!, + [metricId], + ); const createCard = async () => { const isClickMap = metric.metricType === HEATMAP; @@ -85,15 +81,11 @@ function CreateCard(props: Props) { ) : null} -
- {metric.name} -
+
{metric.name}
diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/ExampleCards.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/ExampleCards.tsx index 168181571..f5c01655b 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/ExampleCards.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/ExampleCards.tsx @@ -20,6 +20,7 @@ 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,15 +28,24 @@ export const CARD_CATEGORY = { ERROR_TRACKING: 'error-tracking', }; -export const CARD_CATEGORIES = [ +export const CARD_CATEGORIES = (t: TFunction) => [ { - key: CARD_CATEGORY.PRODUCT_ANALYTICS, label: 'Product Analytics', icon: TrendingUp, types: [USER_PATH, ERRORS], + key: CARD_CATEGORY.PRODUCT_ANALYTICS, + label: t('Product Analytics'), + icon: TrendingUp, + types: [USER_PATH, ERRORS], }, { - key: CARD_CATEGORY.WEB_ANALYTICS, label: 'Web Analytics', icon: BarChart, types: [TABLE], + key: CARD_CATEGORY.WEB_ANALYTICS, + label: t('Web Analytics'), + icon: BarChart, + types: [TABLE], }, { - key: CARD_CATEGORY.ERROR_TRACKING, label: 'Monitors', icon: SearchSlash, types: [], + key: CARD_CATEGORY.ERROR_TRACKING, + label: t('Monitors'), + icon: SearchSlash, + types: [], }, ]; @@ -54,21 +64,19 @@ 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', - ], + value: ['/sessions'], type: 'location', operator: 'contains', count: 1586, @@ -92,7 +100,7 @@ export const CARD_LIST: CardType[] = [ key: HEATMAP, cardType: HEATMAP, metricOf: 'heatMapUrl', - category: CARD_CATEGORIES[0].key, + category: CARD_CATEGORIES(t)[0].key, example: HeatmapsExample, viewType: 'chart', }, @@ -100,7 +108,7 @@ export const CARD_LIST: CardType[] = [ title: 'Untitled Journey', key: USER_PATH, cardType: USER_PATH, - category: CARD_CATEGORIES[0].key, + category: CARD_CATEGORIES(t)[0].key, example: ExamplePath, }, { @@ -108,13 +116,11 @@ export const CARD_LIST: CardType[] = [ 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, }, @@ -123,13 +129,11 @@ export const CARD_LIST: CardType[] = [ 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, }, @@ -140,7 +144,7 @@ export const CARD_LIST: CardType[] = [ key: FilterKey.USERID, cardType: TABLE, metricOf: FilterKey.USERID, - category: CARD_CATEGORIES[1].key, + category: CARD_CATEGORIES(t)[1].key, example: ByUser, viewType: 'table', }, @@ -150,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', }, @@ -159,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, // }, { @@ -167,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', }, @@ -177,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', }, @@ -186,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', }, @@ -196,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', }, @@ -207,9 +211,9 @@ 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', }, @@ -223,7 +227,7 @@ 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', }, @@ -234,11 +238,9 @@ export const CARD_LIST: CardType[] = [ metricOf: 'sessionCount', category: CARD_CATEGORY.ERROR_TRACKING, data: { - chart: generateTimeSeriesData(), + chart: generateTimeSeriesData(t), label: 'Number of Sessions', - namesMap: [ - 'Series 1', - ], + namesMap: ['Series 1'], }, filters: [ { @@ -250,9 +252,7 @@ export const CARD_LIST: CardType[] = [ { type: 'fetchStatusCode', isEvent: false, - value: [ - '400', - ], + value: ['400'], operator: '>=', filters: [], }, @@ -268,11 +268,9 @@ export const CARD_LIST: CardType[] = [ metricOf: 'sessionCount', category: CARD_CATEGORY.ERROR_TRACKING, data: { - chart: generateTimeSeriesData(), + chart: generateTimeSeriesData(t), label: 'Number of Sessions', - namesMap: [ - 'Series 1', - ], + namesMap: ['Series 1'], }, filters: [ { @@ -284,9 +282,7 @@ export const CARD_LIST: CardType[] = [ { type: 'fetchDuration', isEvent: false, - value: [ - '5000', - ], + value: ['5000'], operator: '>=', filters: [], }, @@ -297,28 +293,50 @@ export const CARD_LIST: CardType[] = [ }, ]; -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, - }))); + 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, + })), + ); 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, - }))); + 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, + })), + ); return data; } @@ -327,16 +345,32 @@ 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']; +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), })); } -function generateStackedBarChartData(keys: any): any[] { - const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul']; +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) => { 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 87ab8ad55..4d2880e66 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/AreaChartCard.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/AreaChartCard.tsx @@ -3,21 +3,24 @@ import { NoContent } from 'UI'; import { InfoCircleOutlined } from '@ant-design/icons'; import { - AreaChart, Area, - CartesianGrid, Tooltip, + AreaChart, + Area, + CartesianGrid, + Tooltip, ResponsiveContainer, - XAxis, YAxis, + 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'; 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 { @@ -32,21 +35,19 @@ function AreaChartCard(props: Props) { return (
{props.title}
- )} + } > - - {' '} - { NO_METRIC_DATA } + {NO_METRIC_DATA}
- )} + } show={data?.chart.length === 0} > <> @@ -54,12 +55,13 @@ function AreaChartCard(props: Props) { {/* */} {/*
*/} - + {gradientDef} - + 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' }} + 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 51304b9ac..d71583ace 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Bars.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Bars.tsx @@ -3,10 +3,10 @@ import ExCard from 'Components/Dashboard/components/DashboardList/NewDashModal/E 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) { @@ -24,23 +24,20 @@ function Bars(props: Props) { ], }; return ( - - + ( {item.label} {item.value}
- )} - description={( + } + description={ - )} + } /> )} 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 fd5a5a505..30ac5248d 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/CallsWithErrorsExample.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/CallsWithErrorsExample.tsx @@ -3,19 +3,19 @@ import ExCard from 'Components/Dashboard/components/DashboardList/NewDashModal/E 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 ( - -
+ +
+ +
); } 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 e96905bd1..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,40 +9,43 @@ 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)} />
- )} + } > {el[type]} @@ -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: , @@ -87,9 +91,7 @@ export function Frustrations() { return (
{rows.map((r) => ( -
+
{r.icon}
{r.label}
@@ -118,36 +120,37 @@ 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')}
, }, ]; @@ -155,9 +158,7 @@ export function Errors() { return (
{rows.map((r) => ( -
+
{r.icon}
{r.label}
@@ -214,9 +215,7 @@ export function Users() { return (
{rows.map((r) => ( -
+
{r.label[0].toUpperCase()}
{r.label}
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 f6e54bf6c..566bed126 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/ExCard.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/ExCard.tsx @@ -7,18 +7,21 @@ function ExCard({ onCard, height, }: { - title: React.ReactNode; - children: React.ReactNode; - type: string; - onCard: (card: string) => void; - height?: number; + title: React.ReactNode; + children: React.ReactNode; + type: string; + onCard: (card: string) => void; + height?: number; }) { return (
-
onCard(type)} /> +
onCard(type)} + />
{title}
void; - data?: any, + data?: any; } function ExampleFunnel(props: Props) { @@ -16,9 +16,7 @@ function ExampleFunnel(props: Props) { 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 af7c6932d..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++) { @@ -47,13 +50,14 @@ function HeatmapsExample(props: Props) { // 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 fdb7c62a4..90cf82f5e 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/InsightsExample.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/InsightsExample.tsx @@ -4,9 +4,9 @@ import InsightsCard from 'Components/Dashboard/Widgets/CustomMetricsWidgets/Insi 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) { @@ -40,13 +40,20 @@ function InsightsExample(props: Props) { isNew: true, }, ].map( - (i: any) => new InsightIssue(i.category, i.name, i.ratio, i.oldValue, i.value, i.change, i.isNew), + (i: any) => + new InsightIssue( + i.category, + i.name, + i.ratio, + i.oldValue, + i.value, + i.change, + i.isNew, + ), ), }; return ( - + ); 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 84f5602cf..50c59b6e6 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/PageResponseTimeDistributionExample.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/PageResponseTimeDistributionExample.tsx @@ -14,9 +14,7 @@ function PageResponseTimeDistributionExample(props: Props) { 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 2899cf9d6..3bb0d4d78 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Path.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Path.tsx @@ -32,9 +32,7 @@ function ExamplePath(props: any) { }; return ( - + +
{rows.map((r) => ( @@ -86,15 +86,15 @@ 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 efa120047..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 @@ -40,13 +40,7 @@ function ByBrowser(props: any) { ]; 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 01548f78e..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 @@ -38,13 +38,7 @@ function ByCountry(props: any) { }, ]; - 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 079ea51c4..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 @@ -36,13 +36,7 @@ function ByFetch(props: any) { ]; 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 ef6f01e27..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 @@ -34,13 +34,7 @@ function ByIssues(props: any) { ]; const lineWidth = 200; - return ( - - ); + 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 aaa1bb8b0..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 @@ -36,13 +36,7 @@ function ByReferrer(props: any) { ]; 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 ae17ac4c3..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 @@ -39,13 +39,7 @@ function BySystem(props: any) { ]; 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 4641121e2..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 @@ -48,13 +48,7 @@ function ByUrl(props: any) { ]; 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 495730f34..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 @@ -41,13 +41,7 @@ function ByUser(props: any) { ]; const lineWidth = 200; - return ( - - ); + 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 fdc0bedbe..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 @@ -3,34 +3,40 @@ import CardSessionsByList from 'Components/Dashboard/Widgets/CardSessionsByList' import ExCard from '../ExCard'; function ByComponent({ - title, rows, lineWidth, onCard, type, + title, + rows, + lineWidth, + onCard, + type, }: { - title: string + 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 da9de512f..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 @@ -38,13 +38,7 @@ function SlowestDomains(props: any) { ]; 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 c186ae896..05231b3f0 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsByErrors.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsByErrors.tsx @@ -4,9 +4,7 @@ import { Errors } from './Count'; function SessionsByErrors(props: any) { return ( - + ); 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 519331c39..e62777be9 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsByIssues.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsByIssues.tsx @@ -4,9 +4,7 @@ import { Frustrations } from './Count'; function SessionsByIssues(props: any) { return ( - + ); 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 f81623e30..e20e14224 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsPerBrowserExample.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsPerBrowserExample.tsx @@ -27,9 +27,7 @@ function SessionsPerBrowserExample(props: Props) { ], }; 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 399ab9d20..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,17 +42,11 @@ function SlowestDomain(props: any) { const lineWidth = 240; return ( - +
{rows.map((r) => ( -
- - {r.icon} - +
+ {r.icon}
{r.label}
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 b04a8bca7..69027b892 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SpeedIndexByLocationExample.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/SpeedIndexByLocationExample.tsx @@ -83,9 +83,7 @@ function SpeedIndexByLocationExample(props: Props) { 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 735fe5dfe..382ccd6b9 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/TableOfErrors.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/TableOfErrors.tsx @@ -170,7 +170,7 @@ function TableOfErrors(props: any) { { errorId: '91514ac2304acfca5d82cd518fb36e5fc22', name: 'TypeError', - message: 'Cannot read properties of undefined (reading \'status\')', + message: "Cannot read properties of undefined (reading 'status')", users: 1, sessions: 1, lastOccurrence: 1725013072800, @@ -210,9 +210,7 @@ function TableOfErrors(props: any) { ], }; 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 ed796488a..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,7 +5,7 @@ import SessionsByIssues from '../SessionsByIssues'; import SessionsByErrors from '../SessionsByErrors'; interface ExampleProps { - onCard: (card: string) => void; + onCard: (card: string) => void; } const CoreWebVitals: React.FC = ({ onCard }) => ( 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 bdc7ba41e..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,7 +5,7 @@ import SessionsByErrors from '../SessionsByErrors'; import SessionsByIssues from '../SessionsByIssues'; interface ExampleProps { - onCard: (card: string) => void; + onCard: (card: string) => void; } const PerformanceMonitoring: React.FC = ({ onCard }) => ( 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 6b29085e5..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,7 +5,7 @@ import ExampleTrend from '../Trend'; import ExampleCount from '../Count'; interface ExampleProps { - onCard: (card: string) => void; + onCard: (card: string) => void; } const ProductAnalytics: React.FC = ({ onCard }) => ( 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 2e7db411b..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,7 +5,7 @@ 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 }) => ( 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 74babba85..cd5ea77de 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Trend.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Examples/Trend.tsx @@ -5,22 +5,22 @@ 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, + title: string; + type: string; + onCard: (card: string) => void; + onClick?: any; + data?: any; } function ExampleTrend(props: Props) { return (
{props.title}
- )} + } > {/* */} void; - data?: any, + title: string; + type: string; + onCard: (card: string) => void; + data?: any; } function WebVital(props: Props) { @@ -45,9 +45,7 @@ function WebVital(props: Props) { unit: '%', }; return ( - + ); diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/NewDashboardModal.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/NewDashboardModal.tsx index c31b36eaf..905044083 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/NewDashboardModal.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/NewDashboardModal.tsx @@ -23,11 +23,15 @@ const NewDashboardModal: React.FC = ({ const { isEnterprise } = userStore; const { isMobile } = projectsStore; const [step, setStep] = React.useState(0); - const [selectedCategory, setSelectedCategory] = React.useState('product-analytics'); + const [selectedCategory, setSelectedCategory] = + React.useState('product-analytics'); - useEffect(() => () => { - setStep(0); - }, [open]); + useEffect( + () => () => { + setStep(0); + }, + [open], + ); return ( = ({ }} > {step === 0 && ( - setStep(step + 1)} - isLibrary={isAddingFromLibrary} - isMobile={isMobile} - isEnterprise={isEnterprise} - /> + 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 df1e3ffbd..57b2a974d 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Option.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/Option.tsx @@ -2,8 +2,8 @@ import React from 'react'; import { LucideIcon } from 'lucide-react'; interface OptionProps { - label: string; - Icon: LucideIcon; + label: string; + Icon: LucideIcon; } const Option: React.FC = ({ label, Icon }) => ( diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/SelectCard.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/SelectCard.tsx index 9062eae5d..df8ec2b4d 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/SelectCard.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/SelectCard.tsx @@ -1,7 +1,5 @@ import React, { useMemo, useState, useEffect } from 'react'; -import { - Button, Input, Segmented, Space, -} from 'antd'; +import { Button, Input, Segmented, Space } from 'antd'; import { RightOutlined } from '@ant-design/icons'; import { ArrowRight, Info } from 'lucide-react'; import { useStore } from 'App/mstore'; @@ -12,6 +10,7 @@ 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; @@ -25,24 +24,31 @@ interface SelectCardProps { const SelectCard: React.FC = (props: SelectCardProps) => { const { - onCard, isLibrary = false, selected, setSelectedCategory, isEnterprise, isMobile, + 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]); @@ -55,14 +61,15 @@ const SelectCard: React.FC = (props: SelectCardProps) => { dashboardStore.selectDashboardById(syncedDashboard.dashboardId); history.push(`/${siteId}/dashboard/${syncedDashboard.dashboardId}`); // return syncedDashboard.dashboardId; - }).finally(() => { + }) + .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, @@ -91,20 +98,33 @@ const SelectCard: React.FC = (props: SelectCardProps) => { onCard(); }; - const cardItems = useMemo(() => 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,16 +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 && ( - @@ -148,12 +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 6731105f0..7f768002e 100644 --- a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/AddMetricContainer.tsx +++ b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/AddMetricContainer.tsx @@ -8,23 +8,30 @@ import AddMetric from './AddMetric'; import AddPredefinedMetric from './AddPredefinedMetric'; 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, + iconName, + title, + description, + onClick, + isPremade, + isPopup, }: AddMetricButtonProps) { return (
-
-
{title}
-
+
+
+ {title} +
+
{description}
@@ -50,9 +70,9 @@ function AddMetricButton({ } interface Props { - siteId: string - isPopup?: boolean - onAction?: () => void + siteId: string; + isPopup?: boolean; + onAction?: () => void; } function AddMetricContainer({ siteId, isPopup, onAction }: Props) { @@ -89,7 +109,13 @@ function AddMetricContainer({ siteId, isPopup, onAction }: Props) { ? '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 [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); @@ -63,7 +71,10 @@ function AddPredefinedMetric({ }; 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(); @@ -77,20 +88,29 @@ function AddPredefinedMetric({ >
-

{title}

+

+ {title} +

{description}
- -
Past 7 Days
+
{t('Past 7 Days')}
-
+
- {activeCategory - && categories.map((category) => ( + {activeCategory && + categories.map((category) => ( - {activeCategory - && activeCategory.widgets.map((metric: any) => ( + {activeCategory && + activeCategory.widgets.map((metric: any) => ( dashboardStore.toggleWidgetSelection(metric)} + onClick={() => + dashboardStore.toggleWidgetSelection(metric) + } /> ))} @@ -148,8 +170,12 @@ function AddPredefinedMetric({ {' out of '} {totalMetricCount}
-
diff --git a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx index 0122263d7..12201b63e 100644 --- a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx +++ b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx @@ -52,9 +52,7 @@ function DashboardWidgetGrid(props: Props) { ); } -function GridItem({ - item, index, dashboard, dashboardId, siteId, -}: any) { +function GridItem({ item, index, dashboard, dashboardId, siteId }: any) { const [popoverOpen, setPopoverOpen] = React.useState(false); const handleOpenChange = (open: boolean) => { setPopoverOpen(open); @@ -63,12 +61,17 @@ function GridItem({ return (
dashboard?.swapWidgetPosition(dragIndex, hoverIndex)} + moveListItem={(dragIndex: any, hoverIndex: any) => + dashboard?.swapWidgetPosition(dragIndex, hoverIndex) + } dashboardId={dashboardId} siteId={siteId} grid="other" @@ -90,7 +93,11 @@ function GridItem({ trigger="click" > -
diff --git a/frontend/app/components/Dashboard/components/Errors/ErrorDetailsModal/ErrorDetailsModal.tsx b/frontend/app/components/Dashboard/components/Errors/ErrorDetailsModal/ErrorDetailsModal.tsx index 7d0c3f901..2b175e01d 100644 --- a/frontend/app/components/Dashboard/components/Errors/ErrorDetailsModal/ErrorDetailsModal.tsx +++ b/frontend/app/components/Dashboard/components/Errors/ErrorDetailsModal/ErrorDetailsModal.tsx @@ -2,12 +2,12 @@ import React from 'react'; import ErrorInfo from '../../../../Errors/Error/ErrorInfo'; interface Props { - errorId: any + errorId: any; } function ErrorDetailsModal(props: Props) { return (
diff --git a/frontend/app/components/Dashboard/components/Errors/ErrorLabel/ErrorLabel.tsx b/frontend/app/components/Dashboard/components/Errors/ErrorLabel/ErrorLabel.tsx index 71754746f..6656dcb97 100644 --- a/frontend/app/components/Dashboard/components/Errors/ErrorLabel/ErrorLabel.tsx +++ b/frontend/app/components/Dashboard/components/Errors/ErrorLabel/ErrorLabel.tsx @@ -2,20 +2,33 @@ import React from 'react'; 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, + className, + topValue, + topValueSize = 'text-base', + bottomValue, + topMuted = false, + bottomMuted = false, }: Props) { return (
-
{ topValue }
-
{ bottomValue }
+
+ {topValue} +
+
+ {bottomValue} +
); } diff --git a/frontend/app/components/Dashboard/components/Errors/ErrorListItem/ErrorListItem.tsx b/frontend/app/components/Dashboard/components/Errors/ErrorListItem/ErrorListItem.tsx index 419904f9e..6600b92b8 100644 --- a/frontend/app/components/Dashboard/components/Errors/ErrorListItem/ErrorListItem.tsx +++ b/frontend/app/components/Dashboard/components/Errors/ErrorListItem/ErrorListItem.tsx @@ -2,26 +2,29 @@ import React from 'react'; import cn from 'classnames'; import { DateTime } from 'luxon'; import { IGNORED, RESOLVED } from 'Types/errorInfo'; -import { - BarChart, Bar, YAxis, Tooltip, XAxis, -} from 'recharts'; +import { BarChart, Bar, YAxis, Tooltip, XAxis } from 'recharts'; import { diffFromNowString } from 'App/date'; import ErrorName from '../ErrorName'; import ErrorLabel from '../ErrorLabel'; import { Styles } from '../../../Widgets/common'; +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 { t } = useTranslation(); const { error, className = '' } = props; // const { showModal } = useModal(); return (
@@ -34,8 +37,12 @@ function ErrorListItem(props: Props) { message={error.stack0InfoString} bold={!error.viewed} /> -
- { error.message } +
+ {error.message}
@@ -43,26 +50,35 @@ function ErrorListItem(props: Props) { - } /> - + } + /> +
@@ -72,14 +88,17 @@ function ErrorListItem(props: Props) { export default ErrorListItem; 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') : ''; + const dateStr = p.timestamp + ? DateTime.fromMillis(p.timestamp).toFormat('l') + : ''; return (

{dateStr}

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

diff --git a/frontend/app/components/Dashboard/components/Errors/ErrorName/ErrorName.tsx b/frontend/app/components/Dashboard/components/Errors/ErrorName/ErrorName.tsx index 56551523d..4b2664e35 100644 --- a/frontend/app/components/Dashboard/components/Errors/ErrorName/ErrorName.tsx +++ b/frontend/app/components/Dashboard/components/Errors/ErrorName/ErrorName.tsx @@ -2,12 +2,29 @@ import React from 'react'; import cn from 'classnames'; function ErrorText({ - className, icon, name, message, bold, lineThrough = false, + className, + icon, + name, + message, + bold, + lineThrough = false, }: any) { return (
- { name } - { message } + + {name} + + + {message} +
); } diff --git a/frontend/app/components/Dashboard/components/FilterSeries/AddStepButton.tsx b/frontend/app/components/Dashboard/components/FilterSeries/AddStepButton.tsx index e9e7a7431..54fbb60ee 100644 --- a/frontend/app/components/Dashboard/components/FilterSeries/AddStepButton.tsx +++ b/frontend/app/components/Dashboard/components/FilterSeries/AddStepButton.tsx @@ -3,13 +3,15 @@ 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 { t } = useTranslation(); const { metricStore } = useStore(); const metric: any = metricStore.instance; @@ -23,8 +25,13 @@ function AddStepButton({ series, excludeFilterKeys }: Props) { onFilterClick={onAddFilter} excludeFilterKeys={excludeFilterKeys} > - ); diff --git a/frontend/app/components/Dashboard/components/FilterSeries/ExcludeFilters.tsx b/frontend/app/components/Dashboard/components/FilterSeries/ExcludeFilters.tsx index d8d6d4376..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; @@ -31,7 +33,9 @@ function ExcludeFilters(props: Props) {
{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 8ad23c693..b80a456cb 100644 --- a/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx +++ b/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx @@ -58,10 +58,13 @@ const FilterSeriesHeader = observer( }; return (
{!props.expanded && ( - + )}
({ onClick: () => onIssueClick(rec), diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesListItem/FunnelIssuesListItem.tsx b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesListItem/FunnelIssuesListItem.tsx index aede1dc9c..730805f29 100644 --- a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesListItem/FunnelIssuesListItem.tsx +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesListItem/FunnelIssuesListItem.tsx @@ -2,53 +2,68 @@ 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 { t } = useTranslation(); const { issue, inDetails = false, onClick } = props; // const { showModal } = useModal(); // const onClick = () => { // showModal(, { right: true }); // } return ( -
null}> +
null} + > {/* {inDetails && ( )} */}
-
- +
+
{inDetails && ( -
-
{issue.title}
-
- +
+
+ {issue.title} +
+
+ +
-
)} {!inDetails && ( -
-
{issue.title}
-
- +
+
{issue.title}
+
+ +
-
)}
{issue.affectedUsers}
-
Affected Users
+
+ {t('Affected Users')} +
@@ -56,21 +71,31 @@ function FunnelIssuesListItem(props: Props) { {issue.conversionImpact} %
-
Conversion Impact
+
+ {t('Conversion Impact')} +
{issue.lostConversions}
-
Lost Conversions
+
+ {t('Lost Conversions')} +
{inDetails && (
- - - + + +
)} @@ -84,8 +109,11 @@ function Info({ label = '', color = 'red' }) { return (
-
-
{ label }
+
+
{label}
); diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSelectedFilters/FunnelIssuesSelectedFilters.tsx b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSelectedFilters/FunnelIssuesSelectedFilters.tsx index 51b8fee48..f54c1a17d 100644 --- a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSelectedFilters/FunnelIssuesSelectedFilters.tsx +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSelectedFilters/FunnelIssuesSelectedFilters.tsx @@ -6,7 +6,7 @@ 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(); diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSort/FunnelIssuesSort.tsx b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSort/FunnelIssuesSort.tsx index cc65ccdfb..1da46c36b 100644 --- a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSort/FunnelIssuesSort.tsx +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSort/FunnelIssuesSort.tsx @@ -12,7 +12,7 @@ const sortOptions = [ ]; interface Props { - // onChange?: (value: string) => void; + // onChange?: (value: string) => void; } function FunnelIssuesSort(props: Props) { const { funnelStore } = useStore(); diff --git a/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx b/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx index 2e00d9601..d29bda562 100644 --- a/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx +++ b/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx @@ -38,14 +38,14 @@ function MetricTypeIcon({ type }: any) { return ( {TYPE_NAMES[type]}
}> - )} + } size="default" className="bg-tealx-lightest text-tealx mr-2 cursor-default avatar-card-list-item" /> @@ -147,9 +147,11 @@ const MetricListItem: React.FC = ({ if (diffDays <= 1) { return `Today at ${formatTime(date)}`; - } if (diffDays <= 2) { + } + if (diffDays <= 2) { return `Yesterday at ${formatTime(date)}`; - } if (diffDays <= 3) { + } + if (diffDays <= 3) { return `${diffDays} days ago at ${formatTime(date)}`; } return `${date.getDate()}/${ @@ -178,7 +180,11 @@ const MetricListItem: React.FC = ({ onClick={inLibrary ? undefined : onItemClick} > -
{metric.name}
+
+ {metric.name} +
{renderModal()} diff --git a/frontend/app/components/Dashboard/components/MetricTypeItem/MetricTypeItem.tsx b/frontend/app/components/Dashboard/components/MetricTypeItem/MetricTypeItem.tsx index 6255e7715..566182e15 100644 --- a/frontend/app/components/Dashboard/components/MetricTypeItem/MetricTypeItem.tsx +++ b/frontend/app/components/Dashboard/components/MetricTypeItem/MetricTypeItem.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { Icon, Tooltip } from 'UI'; import cn from 'classnames'; import { ENTERPRISE_REQUEIRED } from 'App/constants'; +import { useTranslation } from 'react-i18next'; export interface MetricType { title: string; @@ -20,16 +21,14 @@ interface Props { } function MetricTypeItem(props: Props) { + const { t } = useTranslation(); const { - metric: { - title, icon, description, slug, disabled, - }, - onClick = () => { - }, + metric: { title, icon, description, slug, disabled }, + onClick = () => {}, isList = false, } = props; return ( - +
-
+
{title}
-
{description}
+
+ {description} +
diff --git a/frontend/app/components/Dashboard/components/MetricTypeList/MetricTypeList.tsx b/frontend/app/components/Dashboard/components/MetricTypeList/MetricTypeList.tsx index bf2d66b8c..f3947a37f 100644 --- a/frontend/app/components/Dashboard/components/MetricTypeList/MetricTypeList.tsx +++ b/frontend/app/components/Dashboard/components/MetricTypeList/MetricTypeList.tsx @@ -8,6 +8,7 @@ import { observer } from 'mobx-react-lite'; import { ENTERPRISE_REQUEIRED } from 'App/constants'; import MetricTypeItem, { MetricType } from '../MetricTypeItem/MetricTypeItem'; import MetricsLibraryModal from '../MetricsLibraryModal'; +import { useTranslation } from 'react-i18next'; interface Props extends RouteComponentProps { dashboardId?: number; @@ -16,22 +17,25 @@ interface Props extends RouteComponentProps { } function MetricTypeList(props: Props) { - const { - dashboardId, siteId, history, isList = false, - } = props; + const { t } = useTranslation(); + const { dashboardId, siteId, history, isList = false } = props; const { metricStore, userStore } = useStore(); const { showModal, hideModal } = useModal(); const { isEnterprise } = userStore; - const list = React.useMemo(() => TYPES.map((metric: MetricType) => { - const disabled = metric.slug === INSIGHTS && !isEnterprise; - return { - ...metric, - icon: `db-icons/icn-card-${metric.slug}`, - disabled: metric.slug === INSIGHTS && !isEnterprise, - tooltipTitle: disabled ? ENTERPRISE_REQUEIRED : '', - }; - }), []); + const list = React.useMemo( + () => + TYPES.map((metric: MetricType) => { + const disabled = metric.slug === INSIGHTS && !isEnterprise; + return { + ...metric, + icon: `db-icons/icn-card-${metric.slug}`, + disabled: metric.slug === INSIGHTS && !isEnterprise, + tooltipTitle: disabled ? ENTERPRISE_REQUEIRED(t) : '', + }; + }), + [], + ); if (!dashboardId) { list.shift(); @@ -40,16 +44,20 @@ function MetricTypeList(props: Props) { const onClick = ({ slug }: MetricType) => { hideModal(); if (slug === LIBRARY) { - return showModal(, { - right: true, - width: 800, - onClose: () => { - metricStore.updateKey('metricsSearch', ''); + return showModal( + , + { + right: true, + width: 800, + onClose: () => { + metricStore.updateKey('metricsSearch', ''); + }, }, - }); + ); } - const path = dashboardId ? withSiteId(dashboardMetricCreate(`${dashboardId}`), siteId) + const path = dashboardId + ? withSiteId(dashboardMetricCreate(`${dashboardId}`), siteId) : withSiteId(metricCreate(), siteId); const queryString = new URLSearchParams({ type: slug }).toString(); history.push({ @@ -61,7 +69,11 @@ function MetricTypeList(props: Props) { return ( <> {list.map((metric: MetricType) => ( - onClick(metric)} isList={isList} /> + onClick(metric)} + isList={isList} + /> ))} ); diff --git a/frontend/app/components/Dashboard/components/MetricTypeSelector.tsx b/frontend/app/components/Dashboard/components/MetricTypeSelector.tsx index d9ddcb682..5c3708567 100644 --- a/frontend/app/components/Dashboard/components/MetricTypeSelector.tsx +++ b/frontend/app/components/Dashboard/components/MetricTypeSelector.tsx @@ -3,40 +3,42 @@ import { Segmented } from 'antd'; import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; import { tabItems } from 'Components/Dashboard/components/AddCardSection/AddCardSection'; -import { - CARD_LIST, -} from 'Components/Dashboard/components/DashboardList/NewDashModal/ExampleCards'; +import { CARD_LIST } from 'Components/Dashboard/components/DashboardList/NewDashModal/ExampleCards'; import FilterSeries from 'App/mstore/types/filterSeries'; import { FUNNEL, USER_PATH } from 'App/constants/card'; +import { useTranslation } from 'react-i18next'; function MetricTypeSelector() { + const { t } = useTranslation(); const { metricStore } = useStore(); - const [selectedCategory, setSelectedCategory] = React.useState(null); + const [selectedCategory, setSelectedCategory] = React.useState( + null, + ); const metric = metricStore.instance; const { cardCategory } = metricStore; if (!cardCategory) { return null; } - const options = tabItems[cardCategory].map((opt) => ({ + const options = tabItems(t)[cardCategory].map((opt) => ({ value: opt.type, icon: opt.icon, })); React.useEffect(() => { - const selected = metric.category ? { value: metric.category } : options.find( - (i) => { - if (metric.metricType === 'table') { - return i.value === metric.metricOf; - } - return i.value === metric.metricType; - }, - ); + const selected = metric.category + ? { value: metric.category } + : options.find((i) => { + if (metric.metricType === 'table') { + return i.value === metric.metricOf; + } + return i.value === metric.metricType; + }); if (selected) { setSelectedCategory(selected.value); } }, []); const onChange = (type: string) => { - const selectedCard = CARD_LIST.find((i) => i.key === type); + const selectedCard = CARD_LIST(t).find((i) => i.key === type); if (selectedCard) { setSelectedCategory(type); diff --git a/frontend/app/components/Dashboard/components/MetricViewHeader/MetricViewHeader.tsx b/frontend/app/components/Dashboard/components/MetricViewHeader/MetricViewHeader.tsx index 5036ee421..8fd11d04f 100644 --- a/frontend/app/components/Dashboard/components/MetricViewHeader/MetricViewHeader.tsx +++ b/frontend/app/components/Dashboard/components/MetricViewHeader/MetricViewHeader.tsx @@ -1,41 +1,43 @@ import React, { useEffect } from 'react'; import { PageTitle } from 'UI'; -import { - Button, Popover, Space, Dropdown, Menu, -} from 'antd'; +import { Button, Popover, Space, Dropdown, Menu } from 'antd'; import { PlusOutlined, DownOutlined } from '@ant-design/icons'; import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; import { DROPDOWN_OPTIONS } from 'App/constants/card'; import MetricsSearch from '../MetricsSearch'; import AddCardSection from '../AddCardSection/AddCardSection'; +import { TFunction } from 'i18next'; +import { useTranslation } from 'react-i18next'; -const options = [ +const options = (t: TFunction) => [ { key: 'all', - label: 'All Types', + label: t('All Types'), }, - ...DROPDOWN_OPTIONS.map((option) => ({ + ...DROPDOWN_OPTIONS(t).map((option) => ({ key: option.value, label: option.label, })), { key: 'monitors', - label: 'Monitors', + label: t('Monitors'), }, { key: 'web_analytics', - label: 'Web Analytics', + label: t('Web Analytics'), }, ]; function MetricViewHeader() { + const { t } = useTranslation(); const { metricStore } = useStore(); const { filter } = metricStore; const cardsLength = metricStore.filteredCards.length; // Determine if a filter is active (search query or metric type other than 'all') - const isFilterActive = filter.query !== '' || (filter.type && filter.type !== 'all'); + const isFilterActive = + filter.query !== '' || (filter.type && filter.type !== 'all'); // Show header if there are cards or if a filter is active const showHeader = cardsLength > 0 || isFilterActive; @@ -49,7 +51,7 @@ function MetricViewHeader() { const menu = ( - {options.map((option) => ( + {options(t).map((option) => ( {option.label} ))} @@ -59,13 +61,14 @@ function MetricViewHeader() {
- + {showHeader && ( @@ -81,8 +84,12 @@ function MetricViewHeader() { content={} trigger="click" > - diff --git a/frontend/app/components/Dashboard/components/MetricsGrid/MetricsGrid.tsx b/frontend/app/components/Dashboard/components/MetricsGrid/MetricsGrid.tsx index 449eeb1be..f72138f44 100644 --- a/frontend/app/components/Dashboard/components/MetricsGrid/MetricsGrid.tsx +++ b/frontend/app/components/Dashboard/components/MetricsGrid/MetricsGrid.tsx @@ -1,12 +1,8 @@ import React from 'react'; -interface Props { - -} +interface Props {} function MetricsGrid(props: Props) { - return ( -
- ); + return
; } export default MetricsGrid; diff --git a/frontend/app/components/Dashboard/components/MetricsLibraryModal/FooterContent.tsx b/frontend/app/components/Dashboard/components/MetricsLibraryModal/FooterContent.tsx index a3415f4d2..1389af0b3 100644 --- a/frontend/app/components/Dashboard/components/MetricsLibraryModal/FooterContent.tsx +++ b/frontend/app/components/Dashboard/components/MetricsLibraryModal/FooterContent.tsx @@ -3,14 +3,28 @@ 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 { t } = useTranslation(); const { hideModal } = useModal(); const { metricStore, dashboardStore } = useStore(); - const dashboard = useMemo(() => dashboardStore.getDashboard(dashboardId), [dashboardId]); + 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 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; @@ -23,20 +37,21 @@ function FooterContent({ dashboardId, selected }: any) { return (
- Selected - {' '} - {selected.length} - {' '} - of - {' '} + {t('Selected')}  + {selected.length} {t('of')} +   {total}
-
diff --git a/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx b/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx index 16e433786..775a594e7 100644 --- a/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx +++ b/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx @@ -6,12 +6,14 @@ import { observer } from 'mobx-react-lite'; import { Input } from 'antd'; import FooterContent from './FooterContent'; 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([]); @@ -35,10 +37,10 @@ function MetricsLibraryModal(props: Props) { return ( <> - +
-
Cards Library
+
{t('Cards Library')}
@@ -47,7 +49,11 @@ function MetricsLibraryModal(props: Props) {
- +
@@ -60,11 +66,12 @@ 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 a455b1fed..a7b989871 100644 --- a/frontend/app/components/Dashboard/components/MetricsList/GridView.tsx +++ b/frontend/app/components/Dashboard/components/MetricsList/GridView.tsx @@ -9,9 +9,7 @@ interface Props extends RouteComponentProps { selectedList: any; } function GridView(props: Props) { - const { - siteId, list, selectedList, history, - } = props; + const { siteId, list, selectedList, history } = props; const onItemClick = (metricId: number) => { const path = withSiteId(`/metrics/${metricId}`, siteId); diff --git a/frontend/app/components/Dashboard/components/MetricsList/ListView.tsx b/frontend/app/components/Dashboard/components/MetricsList/ListView.tsx index b39580bfc..f02a99497 100644 --- a/frontend/app/components/Dashboard/components/MetricsList/ListView.tsx +++ b/frontend/app/components/Dashboard/components/MetricsList/ListView.tsx @@ -11,7 +11,10 @@ import { Avatar, } from 'antd'; import { - TeamOutlined, LockOutlined, EditOutlined, DeleteOutlined, + TeamOutlined, + LockOutlined, + EditOutlined, + DeleteOutlined, } from '@ant-design/icons'; import { EllipsisVertical } from 'lucide-react'; import { TablePaginationConfig, SorterResult } from 'antd/lib/table/interface'; @@ -23,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 +47,47 @@ const ListView: React.FC = ({ disableSelection = false, inLibrary = false, }) => { - const [sorter, setSorter] = useState<{ field: string; order: 'ascend' | 'descend' }>({ + const { t } = useTranslation(); + const [sorter, setSorter] = useState<{ + field: string; + order: 'ascend' | 'descend'; + }>({ field: 'lastModified', 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(() => [...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 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,23 +96,18 @@ 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)} - - {' '} - of - {' '} - {list.length} - {' '} - cards + {Math.min( + (pagination.pageSize || 10) * (pagination.current || 1), + list.length, + )} + {' '} + {t('of')} {list.length} {t('cards')} ); @@ -129,17 +143,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" @@ -159,10 +179,10 @@ 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); }, @@ -183,17 +203,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} @@ -201,7 +224,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); @@ -225,7 +250,7 @@ const ListView: React.FC = ({ const columns = [ { - title: 'Title', + title: t('Title'), dataIndex: 'name', key: 'title', className: 'cap-first pl-4', @@ -234,7 +259,7 @@ const ListView: React.FC = ({ render: renderTitle, }, { - title: 'Owner', + title: t('Owner'), dataIndex: 'owner', key: 'owner', className: 'capitalize', @@ -243,7 +268,7 @@ const ListView: React.FC = ({ render: renderOwner, }, { - title: 'Last Modified', + title: t('Last Modified'), dataIndex: 'lastModified', key: 'lastModified', sorter: true, @@ -271,19 +296,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={{ @@ -299,15 +324,15 @@ const ListView: React.FC = ({ }} /> 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 60dda8216..e775bd9af 100644 --- a/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx +++ b/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx @@ -9,6 +9,7 @@ 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; @@ -30,11 +32,12 @@ function MetricsList({ [dashboard], ); const cards = useMemo( - () => (onSelectionChange - ? metricStore.filteredCards.filter( - (i) => !existingCardIds?.includes(parseInt(i.metricId)), - ) - : metricStore.filteredCards), + () => + onSelectionChange + ? metricStore.filteredCards.filter( + (i) => !existingCardIds?.includes(parseInt(i.metricId)), + ) + : metricStore.filteredCards, [metricStore.filteredCards, existingCardIds, onSelectionChange], ); const loading = metricStore.isLoading; @@ -71,48 +74,56 @@ function MetricsList({ metricStore.updateKey('showMine', !showOwn); }; - const isFiltered = metricsSearch !== '' || (metricStore.filter.type && metricStore.filter.type !== 'all'); + const isFiltered = + 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'} + ? t('No matching results') + : t('Unlock insights with data cards')}
- )} + } subtext={ - isFiltered ? ( - '' - ) : ( -
-
- Create and customize cards to analyze trends and user behavior effectively. + isFiltered ? ( + '' + ) : ( +
+
+ {t('Create and customize cards to analyze trends and user behavior effectively.')} +
+ } + trigger="click" + > + +
- } - trigger="click" - > - - -
- ) - } + ) + } > {listView ? ( setSelectedMetrics( - checked - ? cards.map((i: any) => i.metricId).slice(0, 30 - (existingCardIds?.length || 0)) - : [], - )} + toggleAll={({ target: { checked } }) => + setSelectedMetrics( + checked + ? cards + .map((i: any) => i.metricId) + .slice(0, 30 - (existingCardIds?.length || 0)) + : [], + ) + } /> ) : ( <>
- Showing - {' '} + {t('Showing')}{' '} {Math.min(cards.length, metricStore.pageSize)} - - {' '} - out of - {' '} - {cards.length} - {' '} - cards + {' '} + {t('out of')}  + {cards.length}  + {t('cards')}
{ debounceUpdate = debounce( - (key: any, value: any) => metricStore.updateKey('filter', { ...metricStore.filter, query: value }), + (key: any, value: any) => + metricStore.updateKey('filter', { + ...metricStore.filter, + query: value, + }), 500, ); }, []); diff --git a/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx b/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx index 7f74d98b0..380f205ea 100644 --- a/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx +++ b/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx @@ -9,7 +9,10 @@ interface Props { } function MetricsView({ siteId }: Props) { return ( -
+
diff --git a/frontend/app/components/Dashboard/components/SessionsModal/SessionsModal.tsx b/frontend/app/components/Dashboard/components/SessionsModal/SessionsModal.tsx index 2bace891a..d067dd7f0 100644 --- a/frontend/app/components/Dashboard/components/SessionsModal/SessionsModal.tsx +++ b/frontend/app/components/Dashboard/components/SessionsModal/SessionsModal.tsx @@ -2,18 +2,18 @@ import React, { useEffect } from 'react'; import { useStore } from 'App/mstore'; import { dashboardService, metricService } from 'App/services'; -import { - Loader, Modal, NoContent, Pagination, -} from 'UI'; +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); @@ -48,17 +48,20 @@ function SessionsModal(props: Props) { useEffect(() => { fetchSessions({ - ...dashboardStore.drillDownFilter, ...metricStore.instance.toJson(), limit: 10, page, + ...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) => ( ))} @@ -67,19 +70,10 @@ function SessionsModal(props: Props) {
- 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...')}
void }) { showInfo={false} />
+
{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. -
- +
); } diff --git a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx index b6bace093..577670e44 100644 --- a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx +++ b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx @@ -37,6 +37,7 @@ 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', @@ -65,14 +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 isTableWidget = _metric.metricType === 'table' && _metric.viewType === 'table'; - const isPieChart = _metric.metricType === 'table' && _metric.viewType === 'pieChart'; + 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(() => () => { - dashboardStore.setComparisonPeriod(null, _metric.metricId); - dashboardStore.resetDrillDownFilter(); - }, []); + useEffect( + () => () => { + dashboardStore.setComparisonPeriod(null, _metric.metricId); + dashboardStore.resetDrillDownFilter(); + }, + [], + ); useEffect(() => { if (enabledRows.length !== _metric.series.length) { @@ -89,8 +98,8 @@ function WidgetChart(props: Props) { 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) { setEnabledRows(series); @@ -184,7 +193,8 @@ function WidgetChart(props: Props) { }; 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 = { @@ -246,9 +256,7 @@ function WidgetChart(props: Props) { if (metricType === FUNNEL) { if (viewType === 'table') { - return ( - - ); + return ; } if (viewType === 'metric') { const values: { @@ -274,7 +282,7 @@ function WidgetChart(props: Props) { colors={colors} hideLegend onClick={onChartClick} - label="Conversion" + label={t('Conversion')} /> ); } @@ -290,9 +298,10 @@ function WidgetChart(props: Props) { } if (metricType === 'predefined' || metricType === ERRORS) { - const defaultMetric = _metric.data.chart && _metric.data.chart.length === 0 - ? metricWithData - : metric; + const defaultMetric = + _metric.data.chart && _metric.data.chart.length === 0 + ? metricWithData + : metric; return ( ); @@ -341,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') } /> ); @@ -359,8 +368,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') } /> ); @@ -378,8 +387,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') } /> ); @@ -392,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') } /> ); @@ -407,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') } /> ); @@ -417,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], }); } @@ -438,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') } /> ); @@ -496,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.')}
); } @@ -508,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 ( ); - } 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 ? ( diff --git a/frontend/app/components/Dashboard/components/WidgetDatatable/WidgetDatatable.tsx b/frontend/app/components/Dashboard/components/WidgetDatatable/WidgetDatatable.tsx index a545fff56..9fe00d3a8 100644 --- a/frontend/app/components/Dashboard/components/WidgetDatatable/WidgetDatatable.tsx +++ b/frontend/app/components/Dashboard/components/WidgetDatatable/WidgetDatatable.tsx @@ -1,3 +1,4 @@ +/* eslint-disable i18next/no-literal-string */ import { Button, Table, Divider } from 'antd'; import type { TableProps } from 'antd'; @@ -5,6 +6,7 @@ import { Eye, EyeOff } from 'lucide-react'; import cn from 'classnames'; import React, { useState } from 'react'; import { TableExporter } from 'Components/Funnels/FunnelWidget/FunnelTable'; +import { useTranslation } from 'react-i18next'; const initTableProps = [ { @@ -35,7 +37,9 @@ interface Props { } function WidgetDatatable(props: Props) { - const [tableProps, setTableProps] = useState(initTableProps); + const { t } = useTranslation(); + const [tableProps, setTableProps] = + useState(initTableProps); const data = React.useMemo(() => { const dataObj = { ...props.data }; if (props.compData) { @@ -51,9 +55,11 @@ function WidgetDatatable(props: Props) { return newItem; }); const blank = new Array(dataObj.namesMap.length * 2).fill(''); - dataObj.namesMap = blank.map((_, i) => (i % 2 !== 0 - ? `Previous ${dataObj.namesMap[i / 2]}` - : dataObj.namesMap[i / 2])); + dataObj.namesMap = blank.map((_, i) => + i % 2 !== 0 + ? `Previous ${dataObj.namesMap[i / 2]}` + : dataObj.namesMap[i / 2], + ); } return dataObj; }, [props.data, props.compData]); @@ -62,9 +68,7 @@ function WidgetDatatable(props: Props) { 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; @@ -103,7 +107,9 @@ function WidgetDatatable(props: Props) { // 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]); values.forEach((v) => { @@ -148,7 +154,7 @@ function WidgetDatatable(props: Props) { onClick={() => setShowTable(!showTable)} className="btn-show-hide-table" > - {showTable ? 'Hide Table' : 'Show Table'} + {showTable ? t('Hide Table') : t('Show Table')}
diff --git a/frontend/app/components/Dashboard/components/WidgetDateRange/RangeGranularity.tsx b/frontend/app/components/Dashboard/components/WidgetDateRange/RangeGranularity.tsx index e81d7ab24..02d9a970c 100644 --- a/frontend/app/components/Dashboard/components/WidgetDateRange/RangeGranularity.tsx +++ b/frontend/app/components/Dashboard/components/WidgetDateRange/RangeGranularity.tsx @@ -9,9 +9,9 @@ function RangeGranularity({ }: { period: { getDuration(): number; - }, - density: number, - onDensityChange: (density: number) => void + }; + density: number; + onDensityChange: (density: number) => void; }) { const granularityOptions = React.useMemo(() => { if (!period) return []; @@ -41,7 +41,12 @@ function RangeGranularity({ return ( - @@ -62,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 }); } } diff --git a/frontend/app/components/Dashboard/components/WidgetDateRange/WidgetDateRange.tsx b/frontend/app/components/Dashboard/components/WidgetDateRange/WidgetDateRange.tsx index 828b78384..7b8d6f59c 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 { - 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,13 +15,15 @@ function WidgetDateRange({ hasComparison = false, presetComparison = null, }: any) { + const { t } = useTranslation(); const { dashboardStore, metricStore } = useStore(); const density = dashboardStore.selectedDensity; const onDensityChange = (density: number) => { dashboardStore.setDensity(density); }; const period = dashboardStore.drillDownPeriod; - const compPeriod = dashboardStore.comparisonPeriods[metricStore.instance.metricId]; + const compPeriod = + dashboardStore.comparisonPeriods[metricStore.instance.metricId]; const { drillDownFilter } = dashboardStore; const onChangePeriod = (period: any) => { @@ -46,7 +46,9 @@ function WidgetDateRange({ React.useEffect(() => { if (presetComparison) { - 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({ @@ -74,7 +76,9 @@ function WidgetDateRange({ } }, [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); }; @@ -97,21 +101,19 @@ function WidgetDateRange({ onDensityChange={onDensityChange} /> ) : null} - {hasComparison - ? ( - - ) - : null} + {hasComparison ? ( + + ) : null} ) : null} diff --git a/frontend/app/components/Dashboard/components/WidgetForm/CardBuilder.tsx b/frontend/app/components/Dashboard/components/WidgetForm/CardBuilder.tsx index 38f78529d..40fc79e9c 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/CardBuilder.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/CardBuilder.tsx @@ -1,31 +1,39 @@ 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 { + 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 { Card, Input, Space, Button, Segmented, Alert } from 'antd'; import { AudioWaveform } from 'lucide-react'; import Select from 'Shared/Select'; 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 { 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'); -function AIInput({ - value, setValue, placeholder, onEnter, -}) { +function AIInput({ value, setValue, placeholder, onEnter }) { return ( ; + const { t } = useTranslation(); + return ( + + ); } function MetricTabs({ metric, writeOption }: any) { @@ -54,86 +71,94 @@ function MetricTabs({ metric, writeOption }: any) { }; return ( - + ); } function MetricOptions({ metric, writeOption }: any) { + const { t } = useTranslation(); const isUserPath = metric.metricType === USER_PATH; return (
- Card showing + {t('Card showing')} {isUserPath && ( - <> - - - + <> + + + )} {metric.metricOf === FilterKey.ISSUE && metric.metricType === TABLE && ( - <> - issue type - + )} {metric.metricType === INSIGHTS && ( - <> - of - - + <> + {t('of')} + + + )}
); @@ -144,7 +169,10 @@ const PathAnalysisFilter = observer(({ metric }: any) => ( styles={{ body: { padding: '4px 20px' }, header: { - padding: '4px 20px', fontSize: '14px', fontWeight: 'bold', borderBottom: 'none', + padding: '4px 20px', + fontSize: '14px', + fontWeight: 'bold', + borderBottom: 'none', }, }} title={metric.startType === 'start' ? 'Start Point' : 'End Point'} @@ -154,48 +182,76 @@ const PathAnalysisFilter = observer(({ metric }: any) => ( metric.updateStartPoint(val)} - onRemoveFilter={() => { - }} + onRemoveFilter={() => {}} />
)); const SeriesList = observer(() => { + 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 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.' - } - /> -
- ))} + {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 && ( - + @@ -215,18 +271,19 @@ const SeriesList = observer(() => { }); 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 { t } = useTranslation(); const history = useHistory(); const { siteId, dashboardId, metricId } = props; const { metricStore, dashboardStore, aiFiltersStore } = useStore(); @@ -237,21 +294,25 @@ const CardBuilder = observer((props: CardBuilderProps) => { 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 testingKey = + localStorage.getItem('__mauricio_testing_access') === 'true'; 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(); @@ -265,9 +326,13 @@ const CardBuilder = observer((props: CardBuilderProps) => { 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); + 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( @@ -279,11 +344,15 @@ const CardBuilder = observer((props: CardBuilderProps) => { }, [dashboardId, dashboardStore, history, metric, metricStore, siteId]); const onDelete = useCallback(async () => { - if (await confirm({ - header: 'Confirm', - confirmButton: 'Yes, delete', - confirmation: 'Are you sure you want to permanently delete this card?', - })) { + 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]); @@ -293,8 +362,13 @@ const CardBuilder = observer((props: CardBuilderProps) => { // 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()), @@ -311,22 +385,31 @@ const CardBuilder = observer((props: CardBuilderProps) => { {/* */} - {metric.metricType === USER_PATH && } + {metric.metricType === USER_PATH && ( + + )} {isPredefined && } {testingKey && ( <> - + )} {aiFiltersStore.isLoading && (
-
Loading
+
+ {t('Loading')} +
)} {!isPredefined && } diff --git a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx index e2b431aef..9230a62b0 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx @@ -22,15 +22,17 @@ 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 { t } = useTranslation(); const { history, match: { @@ -53,7 +55,9 @@ function WidgetForm(props: Props) { 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 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; @@ -98,14 +102,19 @@ function WidgetForm(props: Props) { if (wasCreating) { if (parseInt(dashboardId, 10) > 0) { history.replace( - withSiteId(dashboardMetricDetails(dashboardId, savedMetric.metricId), siteId), + withSiteId( + dashboardMetricDetails(dashboardId, savedMetric.metricId), + siteId, + ), ); void dashboardStore.addWidgetToDashboard( - dashboardStore.getDashboard(parseInt(dashboardId, 10))!, - [savedMetric.metricId], + dashboardStore.getDashboard(parseInt(dashboardId, 10))!, + [savedMetric.metricId], ); } else { - history.replace(withSiteId(metricDetails(savedMetric.metricId), siteId)); + history.replace( + withSiteId(metricDetails(savedMetric.metricId), siteId), + ); } } }; @@ -113,9 +122,11 @@ function WidgetForm(props: Props) { const onDelete = async () => { if ( await confirm({ - header: 'Confirm', - confirmButton: 'Yes, delete', - confirmation: 'Are you sure you want to permanently delete this card?', + 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); @@ -128,10 +139,9 @@ function WidgetForm(props: Props) { }; const fetchResults = () => { - aiFiltersStore.getCardFilters(aiQuery, metric.metricType) - .then((f) => { - metric.createSeries(f.filters); - }); + aiFiltersStore.getCardFilters(aiQuery, metric.metricType).then((f) => { + metric.createSeries(f.filters); + }); }; const fetchChartData = () => { @@ -149,7 +159,8 @@ function WidgetForm(props: Props) { } }; - const testingKey = localStorage.getItem('__mauricio_testing_access') === 'true'; + const testingKey = + localStorage.getItem('__mauricio_testing_access') === 'true'; return (
{/* @@ -244,22 +255,34 @@ function WidgetForm(props: Props) { { metric.updateStartPoint(val); }} - onRemoveFilter={() => { - }} + onRemoveFilter={() => {}} />
)} {isPredefined && ( - + )} {testingKey ? ( setAiQuery(e.target.value)} className="w-full mb-2" @@ -268,7 +291,7 @@ function WidgetForm(props: Props) { ) : null} {testingKey ? ( setAiAskChart(e.target.value)} className="w-full mb-2" @@ -278,76 +301,110 @@ function WidgetForm(props: Props) { {aiFiltersStore.isLoading ? (
- Loading + {t('Loading')}
) : null} {!isPredefined && (
- {`${isTable || isFunnel || isClickmap || isInsights || isPathAnalysis || isRetention ? 'Filter by' : 'Chart Series'}`} - {!isTable && !isFunnel && !isClickmap && !isInsights && !isPathAnalysis && !isRetention && ( - - )} + {`${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 - ? '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.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() && ( - )}
diff --git a/frontend/app/components/Dashboard/components/WidgetForm/WidgetFormNew.tsx b/frontend/app/components/Dashboard/components/WidgetForm/WidgetFormNew.tsx index 080e9cefc..8e236e6b9 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/WidgetFormNew.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/WidgetFormNew.tsx @@ -1,7 +1,5 @@ import React from 'react'; -import { - Card, Space, Button, Alert, Form, Select, Tooltip, -} from 'antd'; +import { Card, Space, Button, Alert, Form, Select, Tooltip } from 'antd'; import { useStore } from 'App/mstore'; import { eventKeys } from 'Types/filter/newFilter'; import { @@ -19,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) { @@ -65,101 +64,111 @@ 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 isSingleSeries = + isTable || + isFunnel || + isHeatMap || + isInsights || + isRetention || + isPathAnalysis; + + const collapseAll = () => { + setSeriesCollapseState((seriesCollapseState) => { + const newState = { ...seriesCollapseState }; + Object.keys(newState).forEach((key) => { + newState[key] = true; + }); + return newState; + }); + }; + const expandAll = () => { + setSeriesCollapseState((seriesCollapseState) => { + const newState = { ...seriesCollapseState }; + Object.keys(newState).forEach((key) => { + newState[key] = false; + }); + return newState; + }); + }; + + 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 + ? 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.') + } + 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) => { @@ -210,22 +219,22 @@ const PathAnalysisFilter = observer(({ metric, writeOption }: any) => {
- Journeys With + {t('Journeys With')} 
{ name="metricValue" options={metricValueOptions} value={metric.metricValue || []} - onChange={(value) => writeOption({ name: 'metricValue', value })} - placeholder="Select Metrics" + onChange={(value) => + writeOption({ name: 'metricValue', value }) + } + placeholder={t('Select Metrics')} size="small" maxTagCount="responsive" showSearch={false} @@ -246,11 +257,7 @@ const PathAnalysisFilter = observer(({ metric, writeOption }: any) => {
- { - metric.startType === 'start' - ? 'Start Point' - : 'End Point' - } + {metric.startType === 'start' ? 'Start Point' : 'End Point'} { ); }); -const InsightsFilter = observer(({ metric, writeOption }: any) => ( - - - - + + + + ); +}); const AdditionalFilters = observer(() => { const { metricStore } = useStore(); @@ -314,9 +324,10 @@ const AdditionalFilters = observer(() => { }); function PredefinedMessage() { + const { t } = useTranslation(); return ( { // @ts-ignore if (options && !options.map((i) => i.value).includes(metric.metricOf)) { - setTimeout(() => props.onSelect({ name: 'metricOf', value: { value: options[0].value } }), 0); + setTimeout( + () => + props.onSelect({ + name: 'metricOf', + value: { value: options[0].value }, + }), + 0, + ); } }, [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,24 +101,21 @@ function FFlagsList({ siteId }: { siteId: string }) {
- Showing - {' '} + {t('Showing')}{' '} - {(featureFlagsStore.page - 1) * featureFlagsStore.pageSize + 1} - - {' '} - to - {' '} + {(featureFlagsStore.page - 1) * featureFlagsStore.pageSize + + 1} + {' '} + {t('to')}{' '} - {(featureFlagsStore.page - 1) * featureFlagsStore.pageSize - + featureFlagsStore.flags.length} - - {' '} - of - {' '} - {numberWithCommas(featureFlagsStore.total)} - {' '} - Feature Flags. + {(featureFlagsStore.page - 1) * featureFlagsStore.pageSize + + featureFlagsStore.flags.length} + {' '} + {t('of')}{' '} + + {numberWithCommas(featureFlagsStore.total)} + {' '} + {t('Feature Flags.')}
-
diff --git a/frontend/app/components/FFlags/FFlagsSearch.tsx b/frontend/app/components/FFlags/FFlagsSearch.tsx index a2dccb0be..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 (!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,11 +43,11 @@ 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.')); }); }; @@ -51,7 +55,7 @@ function FlagView({ siteId, fflagId }: { siteId: string; fflagId: string }) {
@@ -62,43 +66,47 @@ function FlagView({ siteId, fflagId }: { siteId: string; fflagId: string }) {
- {current.description - || 'There is no description for this feature flag.'} + {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) => ( void; }) { + const { t } = useTranslation(); return ( <> {isDescrEditing ? (