From a71381da40a8b6bbc2b526e7b1bf89675c3624c9 Mon Sep 17 00:00:00 2001 From: Delirium Date: Thu, 3 Oct 2024 11:38:36 +0200 Subject: [PATCH] getting rid of redux for good (#2556) * start moving ui to redux tlk * remove unused reducer * changes for gdpr and site types * ui: migrating duck/roles to mobx * ui: drop unreferenced types * ui: drop unreferenced types * ui: move player slice reducer to mobx family * ui: move assignments to issueReportingStore.ts * remove issues store * some fixes after issues store * remove errors reducer, drop old components * finish removing errors reducer * start moving integrations state to mobx * change(ui): funnel duck cleanup * change(ui): custom fields * change(ui): customMetrics cleanup * change(ui): customMetrics cleanup * change(ui): duck/filters minor cleanup * change(ui): duck/filters cleanup * change(ui): duck/customMetrics cleanup and upgrades * fix integrations service, fix babel config to >.25 + not ie * refactoring integrations reducers etc WIP * finish removing integrations state * some fixes for integrated check * start of projects refactoring * move api and "few" files to new project store * new batch for site -> projects * fix setid context * move all critical components, drop site duck * remove all duck/site refs, remove old components * fixup for SessionTags.tsx, remove duck/sources (?) * move session store * init sessionstore outside of context * fix userfilter * replace simple actions for session store * sessions sotre * Rtm temp (#2597) * change(ui): duck/search wip * change(ui): duck/search wip * change(ui): duck/search wip * change(ui): duck/searchLive wip * change(ui): duck/searchLive wip * change(ui): duck/searchLive wip * change(ui): duck/searchLive wip * change(ui): search states * change(ui): search states * change(ui): search states * change(ui): fix savedSearch store * change(ui): fix savedSearch store * some fixes for session connector * change(ui): fix savedSearch store * change(ui): fix searchLive * change(ui): fix searchLive * fixes for session replay * change(ui): bookmark fetch * last components for sessions * add fetchautoplaylist * finish session reducer, remove deleted reducers * change(ui): fix the search fetch * change(ui): fix the search fetch * fix integrations call ctx * ensure ctx for sessionstore * fix(ui): checking for latest sessions path * start removing user reducer * removing user reducer pt2... * finish user store * remove rand log * fix crashes * tinkering workflow file for tracker test * making sure prefetched sessions work properly * fix conflict * fix router redirects during loading --------- Co-authored-by: Shekar Siri --- .github/workflows/tracker-tests.yaml | 10 +- frontend/.browserslistrc | 1 + frontend/app/IFrameRoutes.tsx | 28 +- frontend/app/PrivateRoutes.tsx | 48 +- frontend/app/PublicRoutes.tsx | 22 +- frontend/app/Router.tsx | 134 +--- frontend/app/api_client.ts | 38 +- frontend/app/api_middleware.ts | 60 -- frontend/app/components/Assist/Assist.tsx | 17 +- .../app/components/Assist/AssistRouter.tsx | 9 +- .../AssistSearchField/AssistSearchField.tsx | 65 +- .../Assist/RecordingsList/Recordings.tsx | 15 +- .../RequestingWindow/RequestingWindow.tsx | 11 +- .../AssistActions/AssistActions.tsx | 27 +- .../components/SessionList/SessionList.tsx | 30 +- .../Client/CustomFields/CustomFieldForm.js | 2 +- .../Client/CustomFields/CustomFieldForm.tsx | 92 +++ .../Client/CustomFields/CustomFields.js | 222 +++--- .../Client/CustomFields/CustomFields.tsx | 108 +++ .../Client/CustomFields/ListItem.js | 34 +- .../Integrations/AssistDoc/AssistDoc.js | 18 +- .../Integrations/BugsnagForm/BugsnagForm.js | 4 +- .../BugsnagForm/ProjectListDropdown.js | 19 +- .../CloudwatchForm/CloudwatchForm.js | 44 +- .../CloudwatchForm/LogGroupDropdown.js | 160 ++-- .../Client/Integrations/ElasticsearchForm.js | 145 ++-- .../Integrations/GraphQLDoc/GraphQLDoc.js | 18 +- .../Client/Integrations/IntegrationForm.js | 142 ---- .../Client/Integrations/IntegrationForm.tsx | 107 +++ .../Client/Integrations/Integrations.tsx | 304 ++++---- .../Client/Integrations/MobxDoc/MobxDoc.js | 18 +- .../Client/Integrations/NgRxDoc/NgRxDoc.js | 18 +- .../Client/Integrations/PiniaDoc/PiniaDoc.tsx | 60 +- .../Integrations/ProfilerDoc/ProfilerDoc.js | 21 +- .../Client/Integrations/ReduxDoc/ReduxDoc.js | 20 +- .../Integrations/SlackAddForm/SlackAddForm.js | 159 ++-- .../SlackChannelList/SlackChannelList.js | 19 +- .../Client/Integrations/SlackForm.tsx | 26 +- .../Integrations/Teams/TeamsAddForm.tsx | 174 +++-- .../Integrations/Teams/TeamsChannelList.tsx | 92 +-- .../Client/Integrations/Teams/index.tsx | 27 +- .../Client/Integrations/VueDoc/VueDoc.js | 20 +- .../Integrations/ZustandDoc/ZustandDoc.js | 18 +- .../app/components/Client/Modules/Modules.tsx | 26 +- .../components/Client/ProfileSettings/Api.js | 63 +- .../Client/ProfileSettings/ChangePassword.tsx | 28 +- .../Client/ProfileSettings/Licenses.js | 11 +- .../Client/ProfileSettings/OptOut.js | 21 +- .../Client/ProfileSettings/ProfileSettings.js | 192 +++-- .../Client/ProfileSettings/Settings.js | 120 ++- .../Client/ProfileSettings/TenantKey.js | 74 +- .../app/components/Client/Roles/Roles.tsx | 232 +++--- .../components/Permissions/Permissions.tsx | 15 - .../Roles/components/Permissions/index.ts | 1 - .../Roles/components/RoleForm/RoleForm.tsx | 370 ++++----- .../Client/SessionsListingSettings.tsx | 35 +- .../AddProjectButton/AddProjectButton.tsx | 19 +- .../app/components/Client/Sites/GDPRForm.js | 184 +++-- .../components/Client/Sites/NewSiteForm.tsx | 115 ++- .../app/components/Client/Sites/Sites.tsx | 36 +- .../app/components/Client/Users/UsersView.tsx | 16 +- .../Users/components/UserForm/UserForm.tsx | 309 ++++---- .../Dashboard/WidgetHolder/WidgetHolder.js | 33 - .../Dashboard/WidgetHolder/index.js | 1 - .../WidgetHolder/widgetHolder.module.css | 12 - .../ClickMapCard/ClickMapCard.tsx | 25 +- .../Dashboard/components/Alerts/NewAlert.tsx | 1 - .../DashboardList/DashboardList.tsx | 10 +- .../NewDashModal/NewDashboardModal.tsx | 19 +- .../DashboardOptions/DashboardOptions.tsx | 13 +- .../DashboardSideMenu/DashboardSideMenu.tsx | 8 +- .../Errors/ErrorDetails/ErrorDetails.tsx | 67 -- .../ErrorDetails/ErrorFrame/ErrorFrame.tsx | 53 -- .../ErrorFrame/errorFrame.module.css | 25 - .../Errors/ErrorDetails/ErrorFrame/index.js | 1 - .../components/Errors/ErrorDetails/index.js | 1 - .../Errors/ErrorListItem/ErrorListItem.tsx | 9 +- .../Errors/ErrorsList/ErrorsList.tsx | 21 - .../components/Errors/ErrorsList/index.ts | 1 - .../Errors/ErrorsWidget/ErrorsWidget.tsx | 12 - .../components/Errors/ErrorsWidget/index.ts | 1 - .../FilterSeries/ExcludeFilters.tsx | 1 - .../MetricTypeList/MetricTypeList.tsx | 12 +- .../MetricTypeDropdown/MetricTypeDropdown.tsx | 10 +- .../WidgetSessions/WidgetSessions.tsx | 56 +- .../app/components/Errors/Error/ErrorInfo.js | 123 +-- .../components/Errors/Error/MainSection.js | 254 +++---- .../components/Errors/Error/SideSection.js | 181 +++-- frontend/app/components/Errors/Errors.js | 154 ---- frontend/app/components/Errors/Header.js | 14 - frontend/app/components/Errors/List/List.js | 259 ------- .../Errors/List/ListItem/ListItem.js | 87 --- .../Errors/List/ListItem/listItem.module.css | 16 - .../Errors/SideMenu/SideMenuDividedItem.js | 20 - .../Errors/SideMenu/SideMenuHeader.js | 14 - .../Errors/SideMenu/SideMenuSection.js | 24 - .../Errors/SideMenu/sideMenuHeader.module.css | 4 - .../FFlags/NewFFlag/Description.tsx | 2 +- .../ForgotPassword/CreatePassword.tsx | 35 +- .../ForgotPassword/ForgotPassword.tsx | 14 +- .../ForgotPassword/ResetPasswordRequest.tsx | 21 +- .../Funnels/FunnelDetails/FunnelDetails.js__ | 159 ---- .../Funnels/FunnelGraph/FunnelGraph.js | 303 -------- .../components/Funnels/FunnelGraph/index.js | 1 - .../Funnels/FunnelHeader/FunnelDropdown.js | 37 - .../Funnels/FunnelHeader/FunnelHeader.js | 149 ---- .../FunnelHeader/funnelHeader.module.css | 30 - .../components/Funnels/FunnelHeader/index.js | 1 - .../FunnelIssueDetails/FunnelIssueDetails.js | 43 -- .../Funnels/FunnelIssueDetails/index.js | 2 +- .../Funnels/FunnelIssues/FunnelIssues.js | 89 --- .../FunnelIssues/SortDropdown/SortDropdown.js | 48 -- .../FunnelIssues/SortDropdown/index.js | 1 - .../SortDropdown/sortDropdown.module.css | 23 - .../components/Funnels/FunnelIssues/index.js | 1 - .../Funnels/FunnelIssuesHeader/DateRange.js | 27 - .../FunnelIssuesHeader/FunnelIssuesHeader.js | 19 - .../Funnels/FunnelIssuesHeader/index.js | 1 - .../Funnels/FunnelItem/FunnelItem.js__ | 32 - .../FunnelSaveModal/FunnelSaveModal.js | 112 --- .../funnelSaveModal.module.css | 15 - .../Funnels/FunnelSaveModal/index.js | 1 - .../FunnelSessionList/FunnelSessionList.js | 68 -- .../Funnels/FunnelSessionList/index.js | 1 - .../Funnels/FunnelSessionsHeader/DateRange.js | 28 - .../SortDropdown/SortDropdown.js | 32 - .../SortDropdown/index.js | 1 - .../SortDropdown/sortDropdown.module.css | 23 - .../Funnels/IssueFilter/IssueFilter.js | 55 -- .../components/Funnels/IssueFilter/index.js | 1 - .../IssueFilter/issueFilter.module.css | 7 - .../IssuesEmptyMessage/IssuesEmptyMessage.js | 26 - .../Funnels/IssuesEmptyMessage/index.js | 1 - .../NewProjectButton/NewProjectButton.tsx | 54 +- .../app/components/Header/SiteDropdown.js | 92 --- .../components/Header/UserMenu/UserMenu.tsx | 27 +- frontend/app/components/Header/VersionTag.tsx | 28 +- frontend/app/components/Login/Login.tsx | 74 +- .../components/MetadataList/MetadataList.tsx | 62 ++ .../OnboardingNavButton.js | 15 +- .../ProjectCodeSnippet/ProjectCodeSnippet.js | 48 +- .../ProjectFormButton/ProjectFormButton.js | 18 +- .../Onboarding/components/withOnboarding.tsx | 30 +- frontend/app/components/Overview/Overview.tsx | 9 +- .../app/components/ScopeForm/ScopeForm.tsx | 43 +- .../app/components/Session/LivePlayer.tsx | 40 +- .../app/components/Session/LiveSession.js | 48 +- .../app/components/Session/MobilePlayer.tsx | 29 +- .../Player/ClickMapRenderer/ThinPlayer.tsx | 12 +- .../AssistSessionsTabs/AssistSessionsTabs.tsx | 10 +- .../Player/LivePlayer/LiveControls.tsx | 49 +- .../LivePlayer/LivePlayerBlockHeader.tsx | 40 +- .../Player/LivePlayer/LivePlayerInst.tsx | 72 +- .../Session/Player/LivePlayer/Timeline.tsx | 31 +- .../Player/MobilePlayer/MobileControls.tsx | 111 +-- .../Player/MobilePlayer/MobileOverlay.tsx | 15 +- .../MobilePlayer/MobilePlayerHeader.tsx | 40 +- .../MobilePlayer/MobilePlayerSubheader.tsx | 19 +- .../Player/MobilePlayer/PerfWarnings.tsx | 17 +- .../Player/MobilePlayer/PlayerBlock.tsx | 23 +- .../Player/MobilePlayer/PlayerInst.tsx | 61 +- .../EventsBlock/Metadata/Metadata.js | 29 +- .../EventsBlock/Metadata/SessionList.js | 95 ++- .../EventsBlock/UserCard/UserCard.js | 9 +- .../Player/ReplayPlayer/PlayerBlock.tsx | 45 +- .../Player/ReplayPlayer/PlayerBlockHeader.tsx | 37 +- .../Player/ReplayPlayer/PlayerInst.tsx | 39 +- .../ReplayPlayer/SummaryBlock/index.tsx | 26 +- .../Player/ReplayPlayer/useShortcuts.ts | 2 +- .../Session/Player/TagWatch/SaveModal.tsx | 5 +- frontend/app/components/Session/Session.tsx | 54 +- frontend/app/components/Session/WebPlayer.tsx | 66 +- .../components/Session_/BottomBlock/Header.js | 25 +- .../Session_/EventsBlock/EventGroupWrapper.js | 284 +++---- .../Session_/EventsBlock/EventsBlock.tsx | 73 +- .../Session_/EventsBlock/Metadata/Metadata.js | 29 +- .../EventsBlock/Metadata/SessionList.js | 95 ++- .../Session_/EventsBlock/UserCard/UserCard.js | 17 +- .../Session_/Exceptions/Exceptions.tsx | 14 +- .../Session_/Issues/ActiveIssueClose.js | 21 - .../Session_/Issues/ActivityList.js | 13 - .../components/Session_/Issues/AuthoAvatar.js | 12 - .../Session_/Issues/ContentRender.js | 83 --- .../Session_/Issues/IssueComment.js | 26 - .../Session_/Issues/IssueCommentForm.js | 54 -- .../Session_/Issues/IssueDescription.js | 13 - .../Session_/Issues/IssueDetails.js | 57 -- .../components/Session_/Issues/IssueForm.js | 284 ++++--- .../components/Session_/Issues/IssueHeader.js | 33 - .../Session_/Issues/IssueListItem.js | 34 - .../app/components/Session_/Issues/Issues.js | 109 +-- .../components/Session_/Issues/IssuesModal.js | 8 +- .../Session_/Issues/contentRender.module.css | 32 - .../app/components/Session_/Issues/index.js | 3 +- .../Session_/Issues/issueDetails.module.css | 8 - .../Session_/Issues/issueHeader.module.css | 0 .../Session_/Issues/issueListItem.module.css | 21 - .../Session_/Issues/issuesModal.stories.js | 300 -------- .../Session_/Multiview/Multiview.tsx | 44 +- .../Session_/OverviewPanel/OverviewPanel.tsx | 82 +- .../TimelinePointer/TimelinePointer.tsx | 2 +- .../PageInsightsPanel/PageInsightsPanel.tsx | 57 +- .../Session_/Performance/Performance.tsx | 21 +- .../AssistSessionsModal.tsx | 126 ++-- .../AssistSessionsTabs/AssistSessionsTabs.tsx | 10 +- .../Session_/Player/Controls/Controls.tsx | 173 ++--- .../Session_/Player/Controls/Timeline.tsx | 41 +- .../Player/Controls/components/CreateNote.tsx | 44 +- .../Controls/components/TimeTooltip.tsx | 28 +- .../components/TimelineZoomButton.tsx | 24 +- .../Controls/components/TooltipContainer.tsx | 8 +- .../Controls/components/ZoomDragLayer.tsx | 24 +- .../components/Session_/Player/Overlay.tsx | 15 +- .../Session_/Player/Overlay/AutoplayTimer.tsx | 22 +- .../Session_/Player/Overlay/PlayIconLayer.tsx | 13 +- .../Session_/QueueControls/QueueControls.tsx | 59 +- .../ScreenRecorder/ScreenRecorder.tsx | 27 +- .../components/Session_/Storage/Storage.tsx | 25 +- frontend/app/components/Session_/Subheader.js | 72 +- .../Session_/components/NotePopup.tsx | 13 +- frontend/app/components/Signup/Signup.tsx | 26 +- .../Signup/SignupForm/SignupForm.tsx | 188 +++-- .../Spots/SpotPlayer/SpotPlayer.tsx | 18 +- .../SpotPlayer/components/CommentsSection.tsx | 20 +- .../components/SpotPlayerHeader.tsx | 19 +- .../UpdatePassword/UpdatePassword.js | 191 ++--- .../UsabilityTesting/TestOverview.tsx | 2 +- .../app/components/hocs/withPermissions.js | 48 +- frontend/app/components/hocs/withReport.tsx | 17 +- .../app/components/hocs/withSiteIdRouter.js | 39 +- .../app/components/hocs/withSiteIdUpdater.js | 61 +- .../components/shared/Bookmark/Bookmark.tsx | 27 +- .../shared/CustomMetrics/CustomMetrics.tsx | 17 - .../SessionListModal.module.css | 29 - .../SessionListModal/SessionListModal.tsx | 125 ---- .../CustomMetrics/SessionListModal/index.ts | 1 - .../components/shared/CustomMetrics/index.ts | 1 - .../shared/DevTools/BottomBlock/Header.tsx | 53 +- .../DevTools/ConsolePanel/ConsolePanel.tsx | 17 +- .../DevTools/NetworkPanel/NetworkPanel.tsx | 41 +- .../StackEventPanel/StackEventPanel.tsx | 41 +- .../EmailVerificationMessage.js | 15 +- .../shared/ErrorsBadge/ErrorsBadge.js | 46 +- .../FilterAutoComplete/FilterAutoComplete.tsx | 6 +- .../shared/Filters/FilterList/FilterList.tsx | 6 +- .../Filters/FilterModal/FilterModal.tsx | 64 +- .../FilterSelection/FilterSelection.tsx | 35 +- .../Filters/FilterValue/FilterValue.tsx | 2 +- .../LiveFilterModal/LiveFilterModal.tsx | 72 +- .../shared/FunnelSearch/FunnelSearch.tsx | 85 --- .../components/shared/FunnelSearch/index.ts | 1 - .../GettingStarted/GettingStartedProgress.tsx | 7 +- .../shared/GettingStarted/StepList.tsx | 12 +- .../IntegrateSlackButton.js | 8 +- .../shared/LiveSearchBar/LiveSearchBar.tsx | 18 +- .../LiveSessionList/LiveSessionList.tsx | 80 +- .../LiveSessionReloadButton.tsx | 30 +- .../LiveSessionSearch/LiveSessionSearch.tsx | 69 +- .../LiveSessionSearchField.tsx | 39 +- .../shared/MainSearchBar/MainSearchBar.tsx | 48 +- .../MainSearchBar/components/TagList.tsx | 33 +- .../NoSessionsMessage/NoSessionsMessage.js | 21 +- .../shared/OverviewMenu/OverviewMenu.tsx | 51 +- .../ProjectDropdown/ProjectDropdown.tsx | 84 +-- .../SaveFilterButton/SaveFilterButton.tsx | 38 +- .../SaveFunnelButton/SaveFunnelButton.tsx | 35 - .../shared/SaveFunnelButton/index.ts | 1 - .../SaveSearchModal/SaveSearchModal.tsx | 220 +++--- .../shared/SavedSearch/SavedSearch.tsx | 44 +- .../SavedSearchModal/SavedSearchModal.tsx | 182 ++--- .../shared/SessionItem/PlayLink/PlayLink.tsx | 10 +- .../shared/SessionItem/SessionItem.tsx | 13 +- .../shared/SessionSearch/SessionSearch.tsx | 76 +- .../AiSessionSearchField.tsx | 84 +-- .../SessionSearchField/SessionSearchField.tsx | 26 +- .../SessionSettings/SessionSettings.tsx | 11 +- .../components/CaptureRate.tsx | 21 +- .../SessionsTabOverview.tsx | 29 +- .../LatestSessionsMessage.tsx | 26 +- .../components/NoContentMessage.tsx | 25 +- .../components/Notes/NoteList.tsx | 2 +- .../SessionHeader/SessionHeader.tsx | 59 +- .../SessionList/SessionDateRange.tsx | 31 +- .../components/SessionList/SessionList.tsx | 185 ++--- .../components/SessionSort/SessionSort.tsx | 31 +- .../components/SessionTags/SessionTags.tsx | 139 ++-- .../shared/SharePopup/SharePopup.tsx | 57 +- .../shared/SiteDropdown/SiteDropdown.js | 13 +- .../TrackerUpdateMessage.js | 49 -- .../shared/TrackerUpdateMessage/index.js | 1 - .../ProjectCodeSnippet/ProjectCodeSnippet.js | 21 +- .../UpdateFunnelButton/UpdateFunnelButton.tsx | 26 - .../shared/UpdateFunnelButton/index.ts | 1 - .../ui/ErrorDetails/ErrorDetails.tsx | 30 +- frontend/app/components/ui/Link/Link.js | 25 +- .../NoSessionPermission.tsx | 125 ++-- .../ui/TimezoneDropdown/TimezoneDropdown.js | 13 +- frontend/app/date.ts | 2 +- frontend/app/duck/.eslintrc | 9 - frontend/app/duck/alerts.js | 0 frontend/app/duck/assignments.js | 116 --- frontend/app/duck/components/index.js | 9 - frontend/app/duck/components/player.ts | 140 ---- frontend/app/duck/components/targetDefiner.js | 72 -- frontend/app/duck/customField.js | 132 ---- frontend/app/duck/customMetrics.js | 203 ----- frontend/app/duck/dashboard.js | 41 - frontend/app/duck/errors.js | 239 ------ frontend/app/duck/filters.js | 394 ---------- frontend/app/duck/funcTools/crud/actions.js | 77 -- frontend/app/duck/funcTools/crud/index.js | 3 - frontend/app/duck/funcTools/crud/reducer.js | 66 -- frontend/app/duck/funcTools/crud/types.js | 9 - frontend/app/duck/funcTools/index.js | 0 frontend/app/duck/funcTools/list/actions.js | 13 - frontend/app/duck/funcTools/list/index.js | 3 - frontend/app/duck/funcTools/list/reducer.js | 29 - frontend/app/duck/funcTools/list/types.js | 4 - .../app/duck/funcTools/request/RequestType.js | 14 - frontend/app/duck/funcTools/request/index.js | 2 - .../app/duck/funcTools/request/reducer.js | 89 --- frontend/app/duck/funcTools/request/types.js | 1 - frontend/app/duck/funcTools/tools.js | 33 - frontend/app/duck/funcTools/types.js | 3 - frontend/app/duck/funnelFilters.js | 375 ---------- frontend/app/duck/funnels.js | 451 ----------- frontend/app/duck/index.ts | 51 -- frontend/app/duck/integrations/actions.js | 46 -- frontend/app/duck/integrations/index.js | 37 - .../app/duck/integrations/integrations.js | 40 - frontend/app/duck/integrations/reducer.js | 52 -- frontend/app/duck/integrations/slack.js | 102 --- frontend/app/duck/integrations/teams.js | 103 --- frontend/app/duck/issues.js | 122 --- frontend/app/duck/liveSearch.js | 157 ---- frontend/app/duck/member.js | 40 - frontend/app/duck/rehydrate.js | 54 -- frontend/app/duck/requestStateCreator.js | 80 -- frontend/app/duck/roles.js | 62 -- frontend/app/duck/search.js | 516 ------------- frontend/app/duck/sessions.ts | 704 ------------------ frontend/app/duck/site.js | 167 ----- frontend/app/duck/sources/index.js | 24 - .../app/duck/sources/listSourceCreator.js | 39 - frontend/app/duck/templates.js | 13 - frontend/app/duck/tools/crudDuck.js | 208 ------ frontend/app/duck/tools/index.js | 19 - frontend/app/duck/tools/requestDuck.js | 93 --- frontend/app/duck/tools/storageDuck.js | 25 - frontend/app/duck/user.js | 279 ------- frontend/app/initialize.tsx | 4 - frontend/app/layout/Layout.tsx | 36 +- frontend/app/layout/SideMenu.tsx | 70 +- .../app/layout/SpotToOpenReplayPrompt.tsx | 12 +- frontend/app/layout/TopHeader.tsx | 71 +- frontend/app/layout/TopRight.tsx | 25 +- frontend/app/mstore/customFieldStore.ts | 119 +++ frontend/app/mstore/errorStore.ts | 126 ++-- frontend/app/mstore/featureFlagsStore.ts | 4 +- frontend/app/mstore/index.tsx | 121 ++- frontend/app/mstore/integrationsStore.ts | 296 ++++++++ frontend/app/mstore/issueReportingStore.ts | 106 +++ frontend/app/mstore/loginStore.ts | 23 +- frontend/app/mstore/metricStore.ts | 4 +- frontend/app/mstore/projectsStore.ts | 189 +++++ frontend/app/mstore/roleStore.ts | 130 +++- frontend/app/mstore/searchStore.ts | 287 +++++++ frontend/app/mstore/searchStoreLive.ts | 199 +++++ frontend/app/mstore/sessionStore.ts | 379 +++++----- frontend/app/mstore/types/customField.ts | 55 ++ frontend/app/mstore/types/error.ts | 144 ++-- frontend/app/mstore/types/filter.ts | 320 +++++--- frontend/app/mstore/types/filterItem.ts | 14 +- frontend/app/mstore/types/gdpr.ts | 30 + .../app/mstore/types/integrations/consts.ts | 37 + .../mstore/types/integrations/messengers.ts | 41 + .../app/mstore/types/integrations/services.ts | 501 +++++++++++++ frontend/app/mstore/types/project.ts | 62 ++ frontend/app/mstore/types/role.ts | 69 +- frontend/app/mstore/types/savedSearch.ts | 77 ++ frontend/app/mstore/types/search.ts | 178 +++++ frontend/app/mstore/types/widget.ts | 4 +- frontend/app/mstore/uiPlayerStore.ts | 110 +++ frontend/app/mstore/userStore.ts | 550 ++++++++++++-- frontend/app/player/web/WebPlayer.ts | 2 - frontend/app/player/web/assist/Call.ts | 9 +- frontend/app/services/CustomFieldService.ts | 29 + frontend/app/services/DashboardService.ts | 30 +- frontend/app/services/ErrorService.ts | 44 +- frontend/app/services/IntegrationsService.ts | 63 ++ frontend/app/services/IssueReportsService.ts | 27 + frontend/app/services/ProjectsService.ts | 39 + frontend/app/services/SearchService.ts | 51 ++ frontend/app/services/SessionService.ts | 10 +- frontend/app/services/UserService.ts | 285 ++++--- frontend/app/services/index.ts | 19 +- frontend/app/store.js | 70 -- frontend/app/types/account/account.ts | 103 ++- frontend/app/types/account/limit.ts | 19 +- frontend/app/types/announcement.js | 20 - frontend/app/types/app/platform.js | 3 - frontend/app/types/client/client.js | 26 - frontend/app/types/client/client.ts | 15 + .../app/types/client/{index.js => index.ts} | 0 frontend/app/types/client/loggerOptions.js | 9 - frontend/app/types/customMetric.js | 4 +- frontend/app/types/environment.js | 16 - frontend/app/types/feedbackOptions.js | 20 - frontend/app/types/filter/customFilter.js | 5 +- frontend/app/types/filter/filter.js | 129 ++-- frontend/app/types/index.js | 4 - frontend/app/types/issue/issueUser.js | 8 - frontend/app/types/issue/issuesType.js | 17 - frontend/app/types/member.ts | 68 +- frontend/app/types/rehydrateJob.js | 33 - frontend/app/types/role.js | 34 - frontend/app/types/session/assignment.ts | 33 +- frontend/app/types/session/session.ts | 7 + frontend/app/types/site/gdpr.js | 19 - frontend/app/types/site/gdpr.ts | 18 + frontend/app/types/site/site.js | 53 -- frontend/app/types/site/site.ts | 44 ++ frontend/app/types/targetCustom.js | 18 - frontend/babel.config.js | 43 +- frontend/jest.config.mjs | 2 +- frontend/package.json | 9 +- frontend/path-alias.js | 2 - frontend/tests/featureFlagsStore.test.js | 130 ++-- frontend/tsconfig.json | 12 +- frontend/webpack.config.ts | 6 +- frontend/yarn.lock | 145 +--- 431 files changed, 9681 insertions(+), 17014 deletions(-) create mode 100644 frontend/.browserslistrc delete mode 100644 frontend/app/api_middleware.ts create mode 100644 frontend/app/components/Client/CustomFields/CustomFieldForm.tsx create mode 100644 frontend/app/components/Client/CustomFields/CustomFields.tsx delete mode 100644 frontend/app/components/Client/Integrations/IntegrationForm.js create mode 100644 frontend/app/components/Client/Integrations/IntegrationForm.tsx delete mode 100644 frontend/app/components/Client/Roles/components/Permissions/Permissions.tsx delete mode 100644 frontend/app/components/Client/Roles/components/Permissions/index.ts delete mode 100644 frontend/app/components/Dashboard/WidgetHolder/WidgetHolder.js delete mode 100644 frontend/app/components/Dashboard/WidgetHolder/index.js delete mode 100644 frontend/app/components/Dashboard/WidgetHolder/widgetHolder.module.css delete mode 100644 frontend/app/components/Dashboard/components/Errors/ErrorDetails/ErrorDetails.tsx delete mode 100644 frontend/app/components/Dashboard/components/Errors/ErrorDetails/ErrorFrame/ErrorFrame.tsx delete mode 100644 frontend/app/components/Dashboard/components/Errors/ErrorDetails/ErrorFrame/errorFrame.module.css delete mode 100644 frontend/app/components/Dashboard/components/Errors/ErrorDetails/ErrorFrame/index.js delete mode 100644 frontend/app/components/Dashboard/components/Errors/ErrorDetails/index.js delete mode 100644 frontend/app/components/Dashboard/components/Errors/ErrorsList/ErrorsList.tsx delete mode 100644 frontend/app/components/Dashboard/components/Errors/ErrorsList/index.ts delete mode 100644 frontend/app/components/Dashboard/components/Errors/ErrorsWidget/ErrorsWidget.tsx delete mode 100644 frontend/app/components/Dashboard/components/Errors/ErrorsWidget/index.ts delete mode 100644 frontend/app/components/Errors/Errors.js delete mode 100644 frontend/app/components/Errors/Header.js delete mode 100644 frontend/app/components/Errors/List/List.js delete mode 100644 frontend/app/components/Errors/List/ListItem/ListItem.js delete mode 100644 frontend/app/components/Errors/List/ListItem/listItem.module.css delete mode 100644 frontend/app/components/Errors/SideMenu/SideMenuDividedItem.js delete mode 100644 frontend/app/components/Errors/SideMenu/SideMenuHeader.js delete mode 100644 frontend/app/components/Errors/SideMenu/SideMenuSection.js delete mode 100644 frontend/app/components/Errors/SideMenu/sideMenuHeader.module.css delete mode 100644 frontend/app/components/Funnels/FunnelDetails/FunnelDetails.js__ delete mode 100644 frontend/app/components/Funnels/FunnelGraph/FunnelGraph.js delete mode 100644 frontend/app/components/Funnels/FunnelGraph/index.js delete mode 100644 frontend/app/components/Funnels/FunnelHeader/FunnelDropdown.js delete mode 100644 frontend/app/components/Funnels/FunnelHeader/FunnelHeader.js delete mode 100644 frontend/app/components/Funnels/FunnelHeader/funnelHeader.module.css delete mode 100644 frontend/app/components/Funnels/FunnelHeader/index.js delete mode 100644 frontend/app/components/Funnels/FunnelIssueDetails/FunnelIssueDetails.js delete mode 100644 frontend/app/components/Funnels/FunnelIssues/FunnelIssues.js delete mode 100644 frontend/app/components/Funnels/FunnelIssues/SortDropdown/SortDropdown.js delete mode 100644 frontend/app/components/Funnels/FunnelIssues/SortDropdown/index.js delete mode 100644 frontend/app/components/Funnels/FunnelIssues/SortDropdown/sortDropdown.module.css delete mode 100644 frontend/app/components/Funnels/FunnelIssues/index.js delete mode 100644 frontend/app/components/Funnels/FunnelIssuesHeader/DateRange.js delete mode 100644 frontend/app/components/Funnels/FunnelIssuesHeader/FunnelIssuesHeader.js delete mode 100644 frontend/app/components/Funnels/FunnelIssuesHeader/index.js delete mode 100644 frontend/app/components/Funnels/FunnelItem/FunnelItem.js__ delete mode 100644 frontend/app/components/Funnels/FunnelSaveModal/FunnelSaveModal.js delete mode 100644 frontend/app/components/Funnels/FunnelSaveModal/funnelSaveModal.module.css delete mode 100644 frontend/app/components/Funnels/FunnelSaveModal/index.js delete mode 100644 frontend/app/components/Funnels/FunnelSessionList/FunnelSessionList.js delete mode 100644 frontend/app/components/Funnels/FunnelSessionList/index.js delete mode 100644 frontend/app/components/Funnels/FunnelSessionsHeader/DateRange.js delete mode 100644 frontend/app/components/Funnels/FunnelSessionsHeader/SortDropdown/SortDropdown.js delete mode 100644 frontend/app/components/Funnels/FunnelSessionsHeader/SortDropdown/index.js delete mode 100644 frontend/app/components/Funnels/FunnelSessionsHeader/SortDropdown/sortDropdown.module.css delete mode 100644 frontend/app/components/Funnels/IssueFilter/IssueFilter.js delete mode 100644 frontend/app/components/Funnels/IssueFilter/index.js delete mode 100644 frontend/app/components/Funnels/IssueFilter/issueFilter.module.css delete mode 100644 frontend/app/components/Funnels/IssuesEmptyMessage/IssuesEmptyMessage.js delete mode 100644 frontend/app/components/Funnels/IssuesEmptyMessage/index.js delete mode 100644 frontend/app/components/Header/SiteDropdown.js create mode 100644 frontend/app/components/Onboarding/components/MetadataList/MetadataList.tsx delete mode 100644 frontend/app/components/Session_/Issues/ActiveIssueClose.js delete mode 100644 frontend/app/components/Session_/Issues/ActivityList.js delete mode 100644 frontend/app/components/Session_/Issues/AuthoAvatar.js delete mode 100644 frontend/app/components/Session_/Issues/ContentRender.js delete mode 100644 frontend/app/components/Session_/Issues/IssueComment.js delete mode 100644 frontend/app/components/Session_/Issues/IssueCommentForm.js delete mode 100644 frontend/app/components/Session_/Issues/IssueDescription.js delete mode 100644 frontend/app/components/Session_/Issues/IssueDetails.js delete mode 100644 frontend/app/components/Session_/Issues/IssueHeader.js delete mode 100644 frontend/app/components/Session_/Issues/IssueListItem.js delete mode 100644 frontend/app/components/Session_/Issues/contentRender.module.css delete mode 100644 frontend/app/components/Session_/Issues/issueDetails.module.css delete mode 100644 frontend/app/components/Session_/Issues/issueHeader.module.css delete mode 100644 frontend/app/components/Session_/Issues/issueListItem.module.css delete mode 100644 frontend/app/components/Session_/Issues/issuesModal.stories.js delete mode 100644 frontend/app/components/shared/CustomMetrics/CustomMetrics.tsx delete mode 100644 frontend/app/components/shared/CustomMetrics/SessionListModal/SessionListModal.module.css delete mode 100644 frontend/app/components/shared/CustomMetrics/SessionListModal/SessionListModal.tsx delete mode 100644 frontend/app/components/shared/CustomMetrics/SessionListModal/index.ts delete mode 100644 frontend/app/components/shared/CustomMetrics/index.ts delete mode 100644 frontend/app/components/shared/FunnelSearch/FunnelSearch.tsx delete mode 100644 frontend/app/components/shared/FunnelSearch/index.ts delete mode 100644 frontend/app/components/shared/SaveFunnelButton/SaveFunnelButton.tsx delete mode 100644 frontend/app/components/shared/SaveFunnelButton/index.ts delete mode 100644 frontend/app/components/shared/TrackerUpdateMessage/TrackerUpdateMessage.js delete mode 100644 frontend/app/components/shared/TrackerUpdateMessage/index.js delete mode 100644 frontend/app/components/shared/UpdateFunnelButton/UpdateFunnelButton.tsx delete mode 100644 frontend/app/components/shared/UpdateFunnelButton/index.ts delete mode 100644 frontend/app/duck/.eslintrc delete mode 100644 frontend/app/duck/alerts.js delete mode 100644 frontend/app/duck/assignments.js delete mode 100644 frontend/app/duck/components/index.js delete mode 100644 frontend/app/duck/components/player.ts delete mode 100644 frontend/app/duck/components/targetDefiner.js delete mode 100644 frontend/app/duck/customField.js delete mode 100644 frontend/app/duck/customMetrics.js delete mode 100644 frontend/app/duck/dashboard.js delete mode 100644 frontend/app/duck/errors.js delete mode 100644 frontend/app/duck/filters.js delete mode 100644 frontend/app/duck/funcTools/crud/actions.js delete mode 100644 frontend/app/duck/funcTools/crud/index.js delete mode 100644 frontend/app/duck/funcTools/crud/reducer.js delete mode 100644 frontend/app/duck/funcTools/crud/types.js delete mode 100644 frontend/app/duck/funcTools/index.js delete mode 100644 frontend/app/duck/funcTools/list/actions.js delete mode 100644 frontend/app/duck/funcTools/list/index.js delete mode 100644 frontend/app/duck/funcTools/list/reducer.js delete mode 100644 frontend/app/duck/funcTools/list/types.js delete mode 100644 frontend/app/duck/funcTools/request/RequestType.js delete mode 100644 frontend/app/duck/funcTools/request/index.js delete mode 100644 frontend/app/duck/funcTools/request/reducer.js delete mode 100644 frontend/app/duck/funcTools/request/types.js delete mode 100644 frontend/app/duck/funcTools/tools.js delete mode 100644 frontend/app/duck/funcTools/types.js delete mode 100644 frontend/app/duck/funnelFilters.js delete mode 100644 frontend/app/duck/funnels.js delete mode 100644 frontend/app/duck/index.ts delete mode 100644 frontend/app/duck/integrations/actions.js delete mode 100644 frontend/app/duck/integrations/index.js delete mode 100644 frontend/app/duck/integrations/integrations.js delete mode 100644 frontend/app/duck/integrations/reducer.js delete mode 100644 frontend/app/duck/integrations/slack.js delete mode 100644 frontend/app/duck/integrations/teams.js delete mode 100644 frontend/app/duck/issues.js delete mode 100644 frontend/app/duck/liveSearch.js delete mode 100644 frontend/app/duck/member.js delete mode 100644 frontend/app/duck/rehydrate.js delete mode 100644 frontend/app/duck/requestStateCreator.js delete mode 100644 frontend/app/duck/roles.js delete mode 100644 frontend/app/duck/search.js delete mode 100644 frontend/app/duck/sessions.ts delete mode 100644 frontend/app/duck/site.js delete mode 100644 frontend/app/duck/sources/index.js delete mode 100644 frontend/app/duck/sources/listSourceCreator.js delete mode 100644 frontend/app/duck/templates.js delete mode 100644 frontend/app/duck/tools/crudDuck.js delete mode 100644 frontend/app/duck/tools/index.js delete mode 100644 frontend/app/duck/tools/requestDuck.js delete mode 100644 frontend/app/duck/tools/storageDuck.js delete mode 100644 frontend/app/duck/user.js create mode 100644 frontend/app/mstore/customFieldStore.ts create mode 100644 frontend/app/mstore/integrationsStore.ts create mode 100644 frontend/app/mstore/issueReportingStore.ts create mode 100644 frontend/app/mstore/projectsStore.ts create mode 100644 frontend/app/mstore/searchStore.ts create mode 100644 frontend/app/mstore/searchStoreLive.ts create mode 100644 frontend/app/mstore/types/customField.ts create mode 100644 frontend/app/mstore/types/gdpr.ts create mode 100644 frontend/app/mstore/types/integrations/consts.ts create mode 100644 frontend/app/mstore/types/integrations/messengers.ts create mode 100644 frontend/app/mstore/types/integrations/services.ts create mode 100644 frontend/app/mstore/types/project.ts create mode 100644 frontend/app/mstore/types/savedSearch.ts create mode 100644 frontend/app/mstore/types/search.ts create mode 100644 frontend/app/mstore/uiPlayerStore.ts create mode 100644 frontend/app/services/CustomFieldService.ts create mode 100644 frontend/app/services/IntegrationsService.ts create mode 100644 frontend/app/services/IssueReportsService.ts create mode 100644 frontend/app/services/ProjectsService.ts create mode 100644 frontend/app/services/SearchService.ts delete mode 100644 frontend/app/store.js delete mode 100644 frontend/app/types/announcement.js delete mode 100644 frontend/app/types/app/platform.js delete mode 100644 frontend/app/types/client/client.js create mode 100644 frontend/app/types/client/client.ts rename frontend/app/types/client/{index.js => index.ts} (100%) delete mode 100644 frontend/app/types/client/loggerOptions.js delete mode 100644 frontend/app/types/environment.js delete mode 100644 frontend/app/types/feedbackOptions.js delete mode 100644 frontend/app/types/index.js delete mode 100644 frontend/app/types/issue/issueUser.js delete mode 100644 frontend/app/types/issue/issuesType.js delete mode 100644 frontend/app/types/rehydrateJob.js delete mode 100644 frontend/app/types/role.js delete mode 100644 frontend/app/types/site/gdpr.js create mode 100644 frontend/app/types/site/gdpr.ts delete mode 100644 frontend/app/types/site/site.js create mode 100644 frontend/app/types/site/site.ts delete mode 100644 frontend/app/types/targetCustom.js diff --git a/.github/workflows/tracker-tests.yaml b/.github/workflows/tracker-tests.yaml index 7cf1de010..084ea8a94 100644 --- a/.github/workflows/tracker-tests.yaml +++ b/.github/workflows/tracker-tests.yaml @@ -9,24 +9,16 @@ on: pull_request: branches: [ "dev", "main" ] paths: - - frontend/** - tracker/** jobs: build-and-test: runs-on: macos-latest name: Build and test Tracker - strategy: - matrix: - node-version: [ 18.x ] steps: - - uses: oven-sh/setup-bun@v1 + - uses: oven-sh/setup-bun@v2 with: bun-version: latest - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - name: Cache tracker modules uses: actions/cache@v3 with: diff --git a/frontend/.browserslistrc b/frontend/.browserslistrc new file mode 100644 index 000000000..f4f95cac0 --- /dev/null +++ b/frontend/.browserslistrc @@ -0,0 +1 @@ +> 0.25% and not dead \ No newline at end of file diff --git a/frontend/app/IFrameRoutes.tsx b/frontend/app/IFrameRoutes.tsx index 0973ccd59..8bfe3522b 100644 --- a/frontend/app/IFrameRoutes.tsx +++ b/frontend/app/IFrameRoutes.tsx @@ -1,15 +1,15 @@ import React, { lazy, Suspense } from 'react'; import { Switch, Route } from 'react-router-dom'; -import { connect } from 'react-redux'; import { Loader } from 'UI'; import withSiteIdUpdater from 'HOCs/withSiteIdUpdater'; import * as routes from './routes'; -import { Map } from 'immutable'; import NotFoundPage from 'Shared/NotFoundPage'; import { ModalProvider } from 'Components/Modal'; import Layout from 'App/layout/Layout'; import PublicRoutes from 'App/PublicRoutes'; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; const components: any = { SessionPure: lazy(() => import('Components/Session/Session')), @@ -29,20 +29,16 @@ const LIVE_SESSION_PATH = routes.liveSession(); interface Props { - isEnterprise: boolean; - tenantId: string; - siteId: string; - jwt: string; - sites: Map; - onboarding: boolean; isJwt?: boolean; isLoggedIn?: boolean; loading: boolean; } function IFrameRoutes(props: Props) { - const { isJwt = false, isLoggedIn = false, loading, onboarding, sites, siteId, jwt } = props; - const siteIdList: any = sites.map(({ id }) => id).toJS(); + const { projectsStore } = useStore(); + const sites = projectsStore.list; + const { isJwt = false, isLoggedIn = false, loading } = props; + const siteIdList: any = sites.map(({ id }) => id); if (isLoggedIn) { return ( @@ -72,14 +68,4 @@ function IFrameRoutes(props: Props) { } -export default connect((state: any) => ({ - changePassword: state.getIn(['user', 'account', 'changePassword']), - onboarding: state.getIn(['user', 'onboarding']), - sites: state.getIn(['site', 'list']), - siteId: state.getIn(['site', 'siteId']), - jwt: state.getIn(['user', 'jwt']), - tenantId: state.getIn(['user', 'account', 'tenantId']), - isEnterprise: - state.getIn(['user', 'account', 'edition']) === 'ee' || - state.getIn(['user', 'authDetails', 'edition']) === 'ee' -}))(IFrameRoutes); \ No newline at end of file +export default observer(IFrameRoutes); diff --git a/frontend/app/PrivateRoutes.tsx b/frontend/app/PrivateRoutes.tsx index 97f7fac83..ee5250c07 100644 --- a/frontend/app/PrivateRoutes.tsx +++ b/frontend/app/PrivateRoutes.tsx @@ -1,16 +1,13 @@ import withSiteIdUpdater from 'HOCs/withSiteIdUpdater'; -import { Map } from 'immutable'; import React, { Suspense, lazy } from 'react'; -import { connect } from 'react-redux'; import { Redirect, Route, Switch } from 'react-router-dom'; - -import AdditionalRoutes from 'App/AdditionalRoutes'; +import { observer } from 'mobx-react-lite' +import { useStore } from "./mstore"; import { GLOBAL_HAS_NO_RECORDINGS } from 'App/constants/storageKeys'; import { OB_DEFAULT_TAB } from 'App/routes'; import { Loader } from 'UI'; import APIClient from './api_client'; -import { getScope } from './duck/user'; import * as routes from './routes'; const components: any = { @@ -108,21 +105,18 @@ const SPOTS_LIST_PATH = routes.spotsList(); const SPOT_PATH = routes.spot(); const SCOPE_SETUP = routes.scopeSetup(); -interface Props { - tenantId: string; - siteId: string; - sites: Map; - onboarding: boolean; - scope: number; -} - -function PrivateRoutes(props: Props) { - const { onboarding, sites, siteId } = props; +function PrivateRoutes() { + const { projectsStore, userStore } = useStore(); + const onboarding = userStore.onboarding; + const scope = userStore.scopeState; + const tenantId = userStore.account.tenantId; + const sites = projectsStore.list; + const siteId = projectsStore.siteId; const hasRecordings = sites.some(s => s.recorded); - const redirectToSetup = props.scope === 0; + const redirectToSetup = scope === 0; const redirectToOnboarding = - !onboarding && (localStorage.getItem(GLOBAL_HAS_NO_RECORDINGS) === 'true' || !hasRecordings) && props.scope > 0; - const siteIdList: any = sites.map(({ id }) => id).toJS(); + !onboarding && (localStorage.getItem(GLOBAL_HAS_NO_RECORDINGS) === 'true' || (sites.length > 0 && !hasRecordings)) && scope > 0; + const siteIdList: any = sites.map(({ id }) => id); return ( }> @@ -151,7 +145,7 @@ function PrivateRoutes(props: Props) { path={SPOT_PATH} component={enhancedComponents.Spot} /> - {props.scope === 1 ? : null} + {scope === 1 ? : null} { @@ -160,13 +154,13 @@ function PrivateRoutes(props: Props) { case '/integrations/slack': client.post('integrations/slack/add', { code: location.search.split('=')[1], - state: props.tenantId, + state: tenantId, }); break; case '/integrations/msteams': client.post('integrations/msteams/add', { code: location.search.split('=')[1], - state: props.tenantId, + state: tenantId, }); break; } @@ -283,16 +277,12 @@ function PrivateRoutes(props: Props) { {Object.entries(routes.redirects).map(([fr, to]) => ( ))} - + + + ); } -export default connect((state: any) => ({ - onboarding: state.getIn(['user', 'onboarding']), - scope: getScope(state), - sites: state.getIn(['site', 'list']), - siteId: state.getIn(['site', 'siteId']), - tenantId: state.getIn(['user', 'account', 'tenantId']), -}))(PrivateRoutes); +export default observer(PrivateRoutes); diff --git a/frontend/app/PublicRoutes.tsx b/frontend/app/PublicRoutes.tsx index a1f8e72ed..8bb1294a0 100644 --- a/frontend/app/PublicRoutes.tsx +++ b/frontend/app/PublicRoutes.tsx @@ -3,7 +3,8 @@ import { Loader } from 'UI'; import { Redirect, Route, Switch } from 'react-router-dom'; import Signup from 'Components/Signup/Signup'; import SupportCallout from 'Shared/SupportCallout'; -import { connect } from 'react-redux'; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; import * as routes from 'App/routes'; @@ -14,16 +15,12 @@ const SPOT_PATH = routes.spot(); const Login = lazy(() => import('Components/Login/Login')); const ForgotPassword = lazy(() => import('Components/ForgotPassword/ForgotPassword')); -const UpdatePassword = lazy(() => import('Components/UpdatePassword/UpdatePassword')); const Spot = lazy(() => import('Components/Spots/SpotPlayer/SpotPlayer')); -interface Props { - isEnterprise: boolean; - changePassword: boolean; -} - -function PublicRoutes(props: Props) { - const hideSupport = props.isEnterprise || location.pathname.includes('spots') || location.pathname.includes('view-spot') +function PublicRoutes() { + const { userStore } = useStore(); + const isEnterprise = userStore.isEnterprise; + const hideSupport = isEnterprise || location.pathname.includes('spots') || location.pathname.includes('view-spot') return ( }> @@ -39,9 +36,4 @@ function PublicRoutes(props: Props) { } -export default connect((state: any) => ({ - changePassword: state.getIn(['user', 'account', 'changePassword']), - isEnterprise: - state.getIn(['user', 'account', 'edition']) === 'ee' || - state.getIn(['user', 'authDetails', 'edition']) === 'ee' -}))(PublicRoutes); \ No newline at end of file +export default observer(PublicRoutes) \ No newline at end of file diff --git a/frontend/app/Router.tsx b/frontend/app/Router.tsx index c050096b1..16710096a 100644 --- a/frontend/app/Router.tsx +++ b/frontend/app/Router.tsx @@ -1,6 +1,4 @@ -import { Map } from 'immutable'; import React, { useEffect, useRef } from 'react'; -import { ConnectedProps, connect } from 'react-redux'; import { RouteComponentProps, withRouter } from 'react-router-dom'; import IFrameRoutes from 'App/IFrameRoutes'; @@ -10,60 +8,49 @@ import { GLOBAL_DESTINATION_PATH, IFRAME, JWT_PARAM, - SPOT_ONBOARDING, + SPOT_ONBOARDING } from 'App/constants/storageKeys'; import Layout from 'App/layout/Layout'; -import { withStore } from 'App/mstore'; +import { useStore } from 'App/mstore'; import { checkParam, handleSpotJWT, isTokenExpired } from 'App/utils'; import { ModalProvider } from 'Components/Modal'; import { ModalProvider as NewModalProvider } from 'Components/ModalContext'; -import { fetchListActive as fetchMetadata } from 'Duck/customField'; -import { setSessionPath } from 'Duck/sessions'; -import { fetchList as fetchSiteList } from 'Duck/site'; -import { init as initSite } from 'Duck/site'; -import { fetchUserInfo, getScope, logout, setJwt } from 'Duck/user'; import { Loader } from 'UI'; import * as routes from './routes'; +import { observer } from 'mobx-react-lite' -interface RouterProps - extends RouteComponentProps, - ConnectedProps { - isLoggedIn: boolean; - sites: Map; - loading: boolean; - changePassword: boolean; - isEnterprise: boolean; - fetchUserInfo: () => any; - setSessionPath: (path: any) => any; - fetchSiteList: (siteId?: number) => any; +interface RouterProps extends RouteComponentProps { match: { params: { siteId: string; }; }; - mstore: any; - setJwt: (params: { jwt: string; spotJwt: string | null }) => any; - fetchMetadata: (siteId: string) => void; - initSite: (site: any) => void; - scopeSetup: boolean; - localSpotJwt: string | null; } const Router: React.FC = (props) => { const { - isLoggedIn, - siteId, - sites, - loading, location, - fetchUserInfo, - fetchSiteList, history, - setSessionPath, - scopeSetup, - localSpotJwt, - logout, } = props; + const mstore = useStore(); + const { customFieldStore, projectsStore, sessionStore, searchStore, userStore } = mstore; + const jwt = userStore.jwt; + const changePassword = userStore.account.changePassword; + const userInfoLoading = userStore.fetchInfoRequest.loading; + const scopeSetup = userStore.scopeState === 0; + const localSpotJwt = userStore.spotJwt; + const isLoggedIn = Boolean(jwt && !changePassword); + const fetchUserInfo = userStore.fetchUserInfo; + const setJwt = userStore.updateJwt; + const logout = userStore.logout; + + const setSessionPath = sessionStore.setSessionPath; + const siteId = projectsStore.siteId; + const sitesLoading = projectsStore.sitesLoading; + const sites = projectsStore.list; + const loading = Boolean(userInfoLoading || (!scopeSetup && !siteId) || sitesLoading); + const initSite = projectsStore.initProject; + const fetchSiteList = projectsStore.fetchList; const params = new URLSearchParams(location.search); const spotCb = params.get('spotCallback'); @@ -81,7 +68,7 @@ const Router: React.FC = (props) => { handleSpotLogin(spotJwt); } if (urlJWT) { - props.setJwt({ jwt: urlJWT, spotJwt: spotJwt ?? null }); + setJwt({ jwt: urlJWT, spotJwt: spotJwt ?? null }); } }; @@ -109,9 +96,9 @@ const Router: React.FC = (props) => { localStorage.setItem(SPOT_ONBOARDING, 'true'); } await fetchUserInfo(); - const siteIdFromPath = parseInt(location.pathname.split('/')[1]); + const siteIdFromPath = location.pathname.split('/')[1]; await fetchSiteList(siteIdFromPath); - props.mstore.initClient(); + mstore.initClient(); if (localSpotJwt && !isTokenExpired(localSpotJwt)) { handleSpotLogin(localSpotJwt); @@ -141,6 +128,7 @@ const Router: React.FC = (props) => { useEffect(() => { checkParams(); handleJwtFromUrl(); + mstore.initClient(); }, []); useEffect(() => { @@ -169,18 +157,23 @@ const Router: React.FC = (props) => { if (localSpotJwt && !isTokenExpired(localSpotJwt)) { handleSpotLogin(localSpotJwt); } else { - logout(); + void logout(); } } }, [isSpotCb, isLoggedIn, localSpotJwt, isSignup]); useEffect(() => { - if (siteId && siteId !== lastFetchedSiteIdRef.current) { - const activeSite = sites.find((s) => s.id == siteId); - props.initSite(activeSite); - props.fetchMetadata(siteId); - lastFetchedSiteIdRef.current = siteId; - } + const fetchData = async () => { + if (siteId && siteId !== lastFetchedSiteIdRef.current) { + const activeSite = sites.find((s) => s.id == siteId); + initSite(activeSite ?? {}); + lastFetchedSiteIdRef.current = activeSite?.id; + await customFieldStore.fetchListActive(siteId + ''); + await searchStore.fetchSavedSearchList() + } + }; + + void fetchData(); }, [siteId]); const lastFetchedSiteIdRef = useRef(null); @@ -225,51 +218,4 @@ const Router: React.FC = (props) => { ); }; -const mapStateToProps = (state: Map) => { - const siteId = state.getIn(['site', 'siteId']); - const jwt = state.getIn(['user', 'jwt']); - const changePassword = state.getIn(['user', 'account', 'changePassword']); - const userInfoLoading = state.getIn([ - 'user', - 'fetchUserInfoRequest', - 'loading', - ]); - const sitesLoading = state.getIn(['site', 'fetchListRequest', 'loading']); - const scopeSetup = getScope(state) === 0; - const loading = - Boolean(userInfoLoading) || - Boolean(sitesLoading) || - (!scopeSetup && !siteId); - return { - siteId, - changePassword, - sites: state.getIn(['site', 'list']), - jwt, - localSpotJwt: state.getIn(['user', 'spotJwt']), - isLoggedIn: jwt !== null && !changePassword, - scopeSetup, - loading, - email: state.getIn(['user', 'account', 'email']), - account: state.getIn(['user', 'account']), - organisation: state.getIn(['user', 'account', 'name']), - tenantId: state.getIn(['user', 'account', 'tenantId']), - tenants: state.getIn(['user', 'tenants']), - isEnterprise: - state.getIn(['user', 'account', 'edition']) === 'ee' || - state.getIn(['user', 'authDetails', 'edition']) === 'ee', - }; -}; - -const mapDispatchToProps = { - fetchUserInfo, - setSessionPath, - fetchSiteList, - setJwt, - fetchMetadata, - initSite, - logout, -}; - -const connector = connect(mapStateToProps, mapDispatchToProps); - -export default withStore(withRouter(connector(Router))); +export default withRouter(observer(Router)); diff --git a/frontend/app/api_client.ts b/frontend/app/api_client.ts index 2a9394687..9e5649070 100644 --- a/frontend/app/api_client.ts +++ b/frontend/app/api_client.ts @@ -1,6 +1,4 @@ -import store from 'App/store'; import { queried } from './routes'; -import { setJwt } from 'Duck/user'; const siteIdRequiredPaths: string[] = [ '/dashboard', @@ -54,27 +52,42 @@ export const clean = (obj: any, forbiddenValues: any[] = [undefined, '']): any = export default class APIClient { private init: RequestInit; - private readonly siteId: string | undefined; + private siteId: string | undefined; + private siteIdCheck: (() => { siteId: string | null }) | undefined; + private getJwt: () => string | null = () => null; + private onUpdateJwt: (data: { jwt?: string, spotJwt?: string }) => void; private refreshingTokenPromise: Promise | null = null; constructor() { - const jwt = store.getState().getIn(['user', 'jwt']); - const siteId = store.getState().getIn(['site', 'siteId']); this.init = { headers: new Headers({ Accept: 'application/json', 'Content-Type': 'application/json' }) }; + } + + setJwt(jwt: string | null): void { if (jwt !== null) { (this.init.headers as Headers).set('Authorization', `Bearer ${jwt}`); } - this.siteId = siteId; + } + + setOnUpdateJwt(onUpdateJwt: (data: { jwt?: string, spotJwt?: string }) => void): void { + this.onUpdateJwt = onUpdateJwt; + } + + setJwtChecker(checker: () => string | null): void { + this.getJwt = checker; + } + + setSiteIdCheck(checker: () => { siteId: string | null }): void { + this.siteIdCheck = checker } private getInit(method: string = 'GET', params?: any, reqHeaders?: Record): RequestInit { // Always fetch the latest JWT from the store - const jwt = store.getState().getIn(['user', 'jwt']); + const jwt = this.getJwt() const headers = new Headers({ 'Accept': 'application/json', 'Content-Type': 'application/json', @@ -101,6 +114,9 @@ export default class APIClient { delete init.body; // GET requests shouldn't have a body } + // /:id/path + // const idFromPath = window.location.pathname.split('/')[1]; + this.siteId = this.siteIdCheck?.().siteId ?? undefined; return init; } @@ -131,7 +147,7 @@ export default class APIClient { clean?: boolean } = { clean: true }, headers?: Record): Promise { let _path = path; - let jwt = store.getState().getIn(['user', 'jwt']); + let jwt = this.getJwt(); if (!path.includes('/refresh') && jwt && this.isTokenExpired(jwt)) { jwt = await this.handleTokenRefresh(); (this.init.headers as Headers).set('Authorization', `Bearer ${jwt}`); @@ -158,9 +174,9 @@ export default class APIClient { path !== '/targets_temp' && !path.includes('/metadata/session_search') && !path.includes('/assist/credentials') && - !!this.siteId && siteIdRequiredPaths.some(sidPath => path.startsWith(sidPath)) ) { + if (!this.siteId) console.trace('no id', path) edp = `${edp}/${this.siteId}`; } @@ -202,11 +218,11 @@ export default class APIClient { const data = await response.json(); const refreshedJwt = data.jwt; - store.dispatch(setJwt({ jwt: refreshedJwt, })); + this.onUpdateJwt({ jwt: refreshedJwt }); return refreshedJwt; } catch (error) { console.error('Error refreshing token:', error); - store.dispatch(setJwt({ jwt: null })); + this.onUpdateJwt({ jwt: undefined }); throw error; } } diff --git a/frontend/app/api_middleware.ts b/frontend/app/api_middleware.ts deleted file mode 100644 index fb6a84bc9..000000000 --- a/frontend/app/api_middleware.ts +++ /dev/null @@ -1,60 +0,0 @@ -import logger from 'App/logger'; -import APIClient from './api_client'; -import { FETCH_ACCOUNT, UPDATE_JWT } from 'Duck/user'; -import { handleSpotJWT } from "App/utils"; - -export default () => { - return (next: any) => async (action: any) => { - const { types, call, ...rest } = action; - - if (!call) { - return next(action); - } - - const [REQUEST, SUCCESS, FAILURE] = types; - next({ ...rest, type: REQUEST }); - - try { - const client = new APIClient(); - const response = await call(client); - - if (!response.ok) { - const text = await response.text(); - throw new Error(text); - } - - const json = await response.json() || {}; // TEMP TODO on server: no empty responses - const { jwt, spotJwt, errors, data } = json; - - if (errors) { - next({ type: FAILURE, errors, data }); - } else { - next({ type: SUCCESS, data, ...rest }); - } - - if (jwt) { - next({ type: UPDATE_JWT, data: { jwt } }); - } - if (spotJwt) { - handleSpotJWT(spotJwt); - } - - } catch (e) { - if (e.response?.status === 403) { - next({ type: FETCH_ACCOUNT.FAILURE }); - } - - const data = await e.response?.json(); - logger.error('Error during API request. ', e); - return next({ type: FAILURE, errors: data ? parseError(data.errors) : [] }); - } - }; -}; - -export function parseError(e: any) { - try { - return [...JSON.parse(e).errors] || []; - } catch { - return Array.isArray(e) ? e : [e]; - } -} \ No newline at end of file diff --git a/frontend/app/components/Assist/Assist.tsx b/frontend/app/components/Assist/Assist.tsx index 92ffa2c56..42b678f54 100644 --- a/frontend/app/components/Assist/Assist.tsx +++ b/frontend/app/components/Assist/Assist.tsx @@ -1,28 +1,15 @@ import React from 'react'; -import { withRouter, RouteComponentProps } from 'react-router-dom'; import withPageTitle from 'HOCs/withPageTitle'; import withPermissions from 'HOCs/withPermissions'; import AssistRouter from './AssistRouter'; -import { connect } from 'react-redux'; -interface Props extends RouteComponentProps { - siteId: string; - history: any; - isEnterprise: boolean; -} - -function Assist(props: Props) { +function Assist() { return ( ); } -const Cont = connect((state: any) => ({ - isEnterprise: - state.getIn(['user', 'account', 'edition']) === 'ee' || - state.getIn(['user', 'authDetails', 'edition']) === 'ee' -}))(Assist); export default withPageTitle('Assist - OpenReplay')( - withPermissions(['ASSIST_LIVE', 'SERVICE_ASSIST_LIVE'], '', false, false)(withRouter(Cont)) + withPermissions(['ASSIST_LIVE', 'SERVICE_ASSIST_LIVE'], '', false, false)(Assist) ); diff --git a/frontend/app/components/Assist/AssistRouter.tsx b/frontend/app/components/Assist/AssistRouter.tsx index c82d1cfb4..26b6fbaa2 100644 --- a/frontend/app/components/Assist/AssistRouter.tsx +++ b/frontend/app/components/Assist/AssistRouter.tsx @@ -1,12 +1,7 @@ import React from 'react'; -import { withRouter, RouteComponentProps } from 'react-router-dom'; import AssistView from './AssistView' -interface Props extends RouteComponentProps { - match: any; -} - -function AssistRouter(props: Props) { +function AssistRouter() { return (
@@ -14,4 +9,4 @@ function AssistRouter(props: Props) { ); } -export default withRouter(AssistRouter); +export default AssistRouter; diff --git a/frontend/app/components/Assist/AssistSearchField/AssistSearchField.tsx b/frontend/app/components/Assist/AssistSearchField/AssistSearchField.tsx index a4fc8997f..3380cae43 100644 --- a/frontend/app/components/Assist/AssistSearchField/AssistSearchField.tsx +++ b/frontend/app/components/Assist/AssistSearchField/AssistSearchField.tsx @@ -1,55 +1,46 @@ import React from 'react'; -import { connect } from 'react-redux'; - -import { - addFilterByKeyAndValue, - clearSearch, - edit as editFilter, - fetchFilterSearch, -} from 'Duck/liveSearch'; import { Button } from 'antd'; import { useModal } from 'App/components/Modal'; import SessionSearchField from 'Shared/SessionSearchField'; import { MODULES } from 'Components/Client/Modules'; import AssistStats from '../../AssistStats'; -import Recordings from '../RecordingsList/Recordings' +import Recordings from '../RecordingsList/Recordings'; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; -interface Props { - appliedFilter: any; - fetchFilterSearch: any; - addFilterByKeyAndValue: any; - clearSearch: any; - isEnterprise: boolean; - modules: string[] -} -function AssistSearchField(props: Props) { +function AssistSearchField() { + const { searchStoreLive, userStore } = useStore(); + const modules = userStore.account.settings?.modules ?? []; + const isEnterprise = userStore.isEnterprise const hasEvents = - props.appliedFilter.filters.filter((i: any) => i.isEvent).size > 0; + searchStoreLive.instance.filters.filter((i: any) => i.isEvent).length > 0; const hasFilters = - props.appliedFilter.filters.filter((i: any) => !i.isEvent).size > 0; - const { showModal, hideModal } = useModal(); + searchStoreLive.instance.filters.filter((i: any) => !i.isEvent).length > 0; + const { showModal } = useModal(); const showStats = () => { - showModal(, { right: true, width: 960 }) - } + showModal(, { right: true, width: 960 }); + }; const showRecords = () => { - showModal(, { right: true, width: 960 }) - } + showModal(, { right: true, width: 960 }); + }; return (
- {props.isEnterprise && props.modules.includes(MODULES.OFFLINE_RECORDINGS) - ? : null + {isEnterprise && modules.includes(MODULES.OFFLINE_RECORDINGS) + ? : null } - + @@ -57,18 +48,4 @@ function AssistSearchField(props: Props) { ); } -export default connect( - (state: any) => ({ - appliedFilter: state.getIn(['liveSearch', 'instance']), - modules: state.getIn(['user', 'account', 'settings', 'modules']) || [], - isEnterprise: - state.getIn(['user', 'account', 'edition']) === 'ee' || - state.getIn(['user', 'authDetails', 'edition']) === 'ee' - }), - { - fetchFilterSearch, - editFilter, - addFilterByKeyAndValue, - clearSearch, - } -)(AssistSearchField); +export default observer(AssistSearchField); diff --git a/frontend/app/components/Assist/RecordingsList/Recordings.tsx b/frontend/app/components/Assist/RecordingsList/Recordings.tsx index 00d99e6b5..394db4aac 100644 --- a/frontend/app/components/Assist/RecordingsList/Recordings.tsx +++ b/frontend/app/components/Assist/RecordingsList/Recordings.tsx @@ -4,17 +4,12 @@ import Select from 'Shared/Select'; import RecordingsSearch from './RecordingsSearch'; import RecordingsList from './RecordingsList'; import { useStore } from 'App/mstore'; -import { connect } from 'react-redux'; import SelectDateRange from 'Shared/SelectDateRange/SelectDateRange'; import { observer } from 'mobx-react-lite'; -interface Props { - userId: string; -} - -function Recordings(props: Props) { - const { userId } = props; - const { recordingsStore } = useStore(); +function Recordings() { + const { recordingsStore, userStore } = useStore(); + const userId = userStore.account.id; const recordingsOwner = [ { value: '0', label: 'All Videos' }, @@ -51,6 +46,4 @@ function Recordings(props: Props) { ); } -export default connect((state: any) => ({ - userId: state.getIn(['user', 'account', 'id']) -}))(observer(Recordings)); +export default observer(Recordings); diff --git a/frontend/app/components/Assist/RequestingWindow/RequestingWindow.tsx b/frontend/app/components/Assist/RequestingWindow/RequestingWindow.tsx index 212fea91c..16453e24c 100644 --- a/frontend/app/components/Assist/RequestingWindow/RequestingWindow.tsx +++ b/frontend/app/components/Assist/RequestingWindow/RequestingWindow.tsx @@ -1,8 +1,9 @@ import React from 'react'; import { INDEXES } from 'App/constants/zindex'; -import { connect } from 'react-redux'; import { Button, Loader, Icon } from 'UI'; import { PlayerContext } from 'App/components/Session/playerContext'; +import { useStore } from "App/mstore"; +import { observer } from 'mobx-react-lite'; interface Props { userDisplayName: string; @@ -42,7 +43,9 @@ const WIN_VARIANTS = { } }; -function RequestingWindow({ userDisplayName, getWindowType }: Props) { +function RequestingWindow({ getWindowType }: Props) { + const { sessionStore } = useStore(); + const userDisplayName = sessionStore.current.userDisplayName; const windowType = getWindowType() if (!windowType) return; const { player } = React.useContext(PlayerContext) @@ -81,6 +84,4 @@ function RequestingWindow({ userDisplayName, getWindowType }: Props) { ); } -export default connect((state: any) => ({ - userDisplayName: state.getIn(['sessions', 'current']).userDisplayName, -}))(RequestingWindow); +export default observer(RequestingWindow); diff --git a/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx b/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx index a80a9f5e1..80a49bedd 100644 --- a/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx +++ b/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx @@ -1,6 +1,5 @@ import React, { useState, useEffect } from 'react'; import { Button, Tooltip } from 'UI'; -import { connect } from 'react-redux'; import cn from 'classnames'; import ChatWindow from '../../ChatWindow'; import { CallingState, ConnectionStatus, RemoteControlStatus, RequestLocalStream } from 'Player'; @@ -12,6 +11,7 @@ import { confirm } from 'UI'; import stl from './AassistActions.module.css'; import ScreenRecorder from 'App/components/Session_/ScreenRecorder/ScreenRecorder'; import { audioContextManager } from 'App/utils/screenRecorder'; +import { useStore } from "App/mstore"; function onReject() { toast.info(`Call was rejected.`); @@ -31,12 +31,9 @@ function onError(e: any) { interface Props { userId: string; - hasPermission: boolean; - isEnterprise: boolean; isCallActive: boolean; agentIds: string[]; userDisplayName: string; - agentId: number, } const AssistActionsPing = { @@ -52,15 +49,17 @@ const AssistActionsPing = { function AssistActions({ userId, - hasPermission, - isEnterprise, isCallActive, agentIds, - userDisplayName, - agentId, }: Props) { // @ts-ignore ??? const { player, store } = React.useContext(PlayerContext); + const { sessionStore, userStore } = useStore(); + const permissions = userStore.account.permissions || []; + const hasPermission = permissions.includes('ASSIST_CALL') || permissions.includes('SERVICE_ASSIST_CALL'); + const isEnterprise = userStore.isEnterprise; + const agentId = userStore.account.id; + const userDisplayName = sessionStore.current.userDisplayName; const { assistManager: { @@ -289,14 +288,4 @@ function AssistActions({ ); } -const con = connect((state: any) => { - const permissions = state.getIn(['user', 'account', 'permissions']) || []; - return { - hasPermission: permissions.includes('ASSIST_CALL') || permissions.includes('SERVICE_ASSIST_CALL'), - isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee', - userDisplayName: state.getIn(['sessions', 'current']).userDisplayName, - agentId: state.getIn(['user', 'account', 'id']) - }; -}); - -export default con(observer(AssistActions)); +export default observer(AssistActions); diff --git a/frontend/app/components/Assist/components/SessionList/SessionList.tsx b/frontend/app/components/Assist/components/SessionList/SessionList.tsx index 6fd78da60..2bf5d2ecc 100644 --- a/frontend/app/components/Assist/components/SessionList/SessionList.tsx +++ b/frontend/app/components/Assist/components/SessionList/SessionList.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from 'react'; -import { connect } from 'react-redux'; -import { fetchLiveList } from 'Duck/sessions'; +import { observer } from 'mobx-react-lite' +import { useStore } from 'App/mstore'; import { Loader, NoContent, Label } from 'UI'; import SessionItem from 'Shared/SessionItem'; import { useModal } from 'App/components/Modal'; @@ -11,16 +11,20 @@ interface Props { list: any; session: any; userId: any; - fetchLiveList: (params: any) => void; } function SessionList(props: Props) { const { hideModal } = useModal(); + const { sessionStore } = useStore(); + const fetchLiveList = sessionStore.fetchLiveSessions; + const session = sessionStore.current; + const list = sessionStore.liveSessions.filter((i: any) => i.userId === session.userId && i.sessionId !== session.sessionId); + const loading = sessionStore.loadingLiveSessions; useEffect(() => { const params: any = {}; if (props.session.userId) { params.userId = props.session.userId; } - props.fetchLiveList(params); + void fetchLiveList(params); }, []); return ( @@ -33,9 +37,9 @@ function SessionList(props: Props) { {props.userId}'s Live Sessions{' '}
- + @@ -45,7 +49,7 @@ function SessionList(props: Props) { } >
- {props.list.map((session: any) => ( + {list.map((session: any) => (
{session.pageTitle && session.pageTitle !== '' && (
@@ -65,14 +69,4 @@ function SessionList(props: Props) { ); } -export default connect( - (state: any) => { - const session = state.getIn(['sessions', 'current']); - return { - session, - list: state.getIn(['sessions', 'liveSessions']).filter((i: any) => i.userId === session.userId && i.sessionId !== session.sessionId), - loading: state.getIn(['sessions', 'fetchLiveListRequest', 'loading']), - }; - }, - { fetchLiveList } -)(SessionList); +export default observer(SessionList); diff --git a/frontend/app/components/Client/CustomFields/CustomFieldForm.js b/frontend/app/components/Client/CustomFields/CustomFieldForm.js index 4f2d1e278..b988c89e2 100644 --- a/frontend/app/components/Client/CustomFields/CustomFieldForm.js +++ b/frontend/app/components/Client/CustomFields/CustomFieldForm.js @@ -55,7 +55,7 @@ const CustomFieldForm = ({ field, saving, errors, edit, save, onSave, onClose, o const mapStateToProps = (state) => ({ field: state.getIn(['customFields', 'instance']), saving: state.getIn(['customFields', 'saveRequest', 'loading']), - errors: state.getIn(['customFields', 'saveRequest', 'errors']), + errors: state.getIn(['customFields', 'saveRequest', 'errors']) }); export default connect(mapStateToProps, { edit, save })(CustomFieldForm); diff --git a/frontend/app/components/Client/CustomFields/CustomFieldForm.tsx b/frontend/app/components/Client/CustomFields/CustomFieldForm.tsx new file mode 100644 index 000000000..577153da2 --- /dev/null +++ b/frontend/app/components/Client/CustomFields/CustomFieldForm.tsx @@ -0,0 +1,92 @@ +import React, { useRef, useState } from 'react'; +import { Form, Input, confirm } from 'UI'; +import styles from './customFieldForm.module.css'; +import { useStore } from 'App/mstore'; +import { useModal } from 'Components/Modal'; +import { toast } from 'react-toastify'; +import { Button } from 'antd'; +import { Trash } from 'UI/Icons'; +import { observer } from 'mobx-react-lite'; + +interface CustomFieldFormProps { + siteId: string; +} + +const CustomFieldForm: React.FC = ({ siteId }) => { + console.log('siteId', siteId); + const focusElementRef = useRef(null); + const { customFieldStore: store } = useStore(); + const field = store.instance; + const { hideModal } = useModal(); + const [loading, setLoading] = useState(false); + + const write = ({ target: { value, name } }: any) => store.edit({ [name]: value }); + const exists = field?.exists(); + + const onDelete = async () => { + if ( + await confirm({ + header: 'Metadata', + confirmation: `Are you sure you want to remove?` + }) + ) { + store.remove(siteId, field?.index!).then(() => { + hideModal(); + }); + } + }; + + const onSave = (field: any) => { + setLoading(true); + store.save(siteId, field).then((response) => { + if (!response || !response.errors || response.errors.size === 0) { + hideModal(); + toast.success('Metadata added successfully!'); + } else { + toast.error(response.errors[0]); + } + }).finally(() => { + setLoading(false); + }); + }; + + return ( +
+

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

+
+ + + + + +
+
+ + +
+ + +
+
+
+ ); +}; + +export default observer(CustomFieldForm); diff --git a/frontend/app/components/Client/CustomFields/CustomFields.js b/frontend/app/components/Client/CustomFields/CustomFields.js index a525094f3..1b800095a 100644 --- a/frontend/app/components/Client/CustomFields/CustomFields.js +++ b/frontend/app/components/Client/CustomFields/CustomFields.js @@ -14,124 +14,124 @@ import { useModal } from 'App/components/Modal'; import { toast } from 'react-toastify'; function CustomFields(props) { - const [currentSite, setCurrentSite] = React.useState(props.sites.get(0)); - const [deletingItem, setDeletingItem] = React.useState(null); - const { showModal, hideModal } = useModal(); + const [currentSite, setCurrentSite] = React.useState(props.sites.get(0)); + const [deletingItem, setDeletingItem] = React.useState(null); + const { showModal, hideModal } = useModal(); - useEffect(() => { - const activeSite = props.sites.get(0); - if (!activeSite) return; + useEffect(() => { + const activeSite = props.sites.get(0); + if (!activeSite) return; - props.fetchList(activeSite.id); - }, []); + props.fetchList(activeSite.id); + }, []); - const save = (field) => { - props.save(currentSite.id, field).then((response) => { - if (!response || !response.errors || response.errors.size === 0) { - hideModal(); - toast.success('Metadata added successfully!'); - } else { - toast.error(response.errors[0]); - } + const save = (field) => { + props.save(currentSite.id, field).then((response) => { + if (!response || !response.errors || response.errors.size === 0) { + hideModal(); + toast.success('Metadata added successfully!'); + } else { + toast.error(response.errors[0]); + } + }); + }; + + const init = (field) => { + props.init(field); + showModal( removeMetadata(field)} />); + }; + + const onChangeSelect = ({ value }) => { + const site = props.sites.find((s) => s.id === value.value); + setCurrentSite(site); + props.fetchList(site.id); + }; + + const removeMetadata = async (field) => { + if ( + await confirm({ + header: 'Metadata', + confirmation: `Are you sure you want to remove?` + }) + ) { + setDeletingItem(field.index); + props + .remove(currentSite.id, field.index) + .then(() => { + hideModal(); + }) + .finally(() => { + setDeletingItem(null); }); - }; + } + }; - const init = (field) => { - props.init(field); - showModal( removeMetadata(field)} />); - }; - - const onChangeSelect = ({ value }) => { - const site = props.sites.find((s) => s.id === value.value); - setCurrentSite(site); - props.fetchList(site.id); - }; - - const removeMetadata = async (field) => { - if ( - await confirm({ - header: 'Metadata', - confirmation: `Are you sure you want to remove?`, - }) - ) { - setDeletingItem(field.index); - props - .remove(currentSite.id, field.index) - .then(() => { - hideModal(); - }) - .finally(() => { - setDeletingItem(null); - }); - } - }; - - const { fields, loading } = props; - return ( -
-
-

{'Metadata'}

-
- -
-
- - - -
-
-
- - See additonal user information in sessions. - Learn more -
- - - - - {/*
*/} -
None added yet
-
- } - size="small" - show={fields.size === 0} - > -
- {fields - .filter((i) => i.index) - .map((field) => ( - <> - removeMetadata(field) } - /> - - - ))} -
-
-
+ const { fields, loading } = props; + return ( +
+
+

{'Metadata'}

+
+
- ); +
+ + + +
+
+
+ + See additonal user information in sessions. + Learn more +
+ + + + + {/*
*/} +
None added yet
+
+ } + size="small" + show={fields.size === 0} + > +
+ {fields + .filter((i) => i.index) + .map((field) => ( + <> + removeMetadata(field) } + /> + + + ))} +
+
+
+
+ ); } export default connect( - (state) => ({ - fields: state.getIn(['customFields', 'list']).sortBy((i) => i.index), - field: state.getIn(['customFields', 'instance']), - loading: state.getIn(['customFields', 'fetchRequest', 'loading']), - sites: state.getIn(['site', 'list']), - errors: state.getIn(['customFields', 'saveRequest', 'errors']), - }), - { - init, - fetchList, - save, - remove, - } + (state) => ({ + fields: state.getIn(['customFields', 'list']).sortBy((i) => i.index), + field: state.getIn(['customFields', 'instance']), + loading: state.getIn(['customFields', 'fetchRequest', 'loading']), + sites: state.getIn(['site', 'list']), + errors: state.getIn(['customFields', 'saveRequest', 'errors']) + }), + { + init, + fetchList, + save, + remove + } )(withPageTitle('Metadata - OpenReplay Preferences')(CustomFields)); diff --git a/frontend/app/components/Client/CustomFields/CustomFields.tsx b/frontend/app/components/Client/CustomFields/CustomFields.tsx new file mode 100644 index 000000000..e4152911e --- /dev/null +++ b/frontend/app/components/Client/CustomFields/CustomFields.tsx @@ -0,0 +1,108 @@ +import React, { useEffect, useState } from 'react'; +import cn from 'classnames'; +import withPageTitle from 'HOCs/withPageTitle'; +import { Button, Loader, NoContent, Icon, Tooltip, Divider } from 'UI'; +import SiteDropdown from 'Shared/SiteDropdown'; +import styles from './customFields.module.css'; +import CustomFieldForm from './CustomFieldForm'; +import ListItem from './ListItem'; +import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; +import { useModal } from 'App/components/Modal'; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; + +const CustomFields = () => { + const { customFieldStore: store, projectsStore } = useStore(); + const sites = projectsStore.list; + const [currentSite, setCurrentSite] = useState(sites[0]); + const [deletingItem, setDeletingItem] = useState(null); + const { showModal, hideModal } = useModal(); + const fields = store.list; + const [loading, setLoading] = useState(false); + + useEffect(() => { + const activeSite = sites[0]; + if (!activeSite) return; + + setCurrentSite(activeSite); + + setLoading(true); + store.fetchList(activeSite.id).finally(() => { + setLoading(false); + }); + }, [sites]); + + const handleInit = (field?: any) => { + console.log('field', field); + store.init(field); + showModal(, { + title: field ? 'Edit Metadata' : 'Add Metadata', right: true + }); + }; + + const onChangeSelect = ({ value }: { value: { value: number } }) => { + const site = sites.find((s: any) => s.id === value.value); + setCurrentSite(site); + + setLoading(true); + store.fetchList(site.id).finally(() => { + setLoading(false); + }); + }; + + return ( +
+
+

{'Metadata'}

+
+ +
+
+ + + +
+
+
+ + See additional user information in sessions. + + Learn more + +
+ + + + +
None added yet
+
+ } + size="small" + show={fields.length === 0} + > +
+ {fields + .filter((i: any) => i.index) + .map((field: any) => ( + <> + + + + ))} +
+ + +
+ ); +}; + +export default withPageTitle('Metadata - OpenReplay Preferences')(observer(CustomFields)); diff --git a/frontend/app/components/Client/CustomFields/ListItem.js b/frontend/app/components/Client/CustomFields/ListItem.js index 326faa1b5..9c38e12e9 100644 --- a/frontend/app/components/Client/CustomFields/ListItem.js +++ b/frontend/app/components/Client/CustomFields/ListItem.js @@ -4,23 +4,23 @@ import { Button } from 'UI'; import styles from './listItem.module.css'; const ListItem = ({ field, onEdit, disabled }) => { - return ( -
field.index != 0 && onEdit(field)} - > - {field.key} -
-
-
- ); + return ( +
field.index !== 0 && onEdit(field)} + > + {field.key} +
+
+
+ ); }; export default ListItem; diff --git a/frontend/app/components/Client/Integrations/AssistDoc/AssistDoc.js b/frontend/app/components/Client/Integrations/AssistDoc/AssistDoc.js index dd94f90e6..1165af6ec 100644 --- a/frontend/app/components/Client/Integrations/AssistDoc/AssistDoc.js +++ b/frontend/app/components/Client/Integrations/AssistDoc/AssistDoc.js @@ -1,10 +1,11 @@ +import { useStore } from "App/mstore"; import React from 'react'; import DocLink from 'Shared/DocLink/DocLink'; import AssistScript from './AssistScript'; import AssistNpm from './AssistNpm'; import { Tabs, CodeBlock } from 'UI'; import { useState } from 'react'; -import { connect } from 'react-redux'; +import { observer } from 'mobx-react-lite' const NPM = 'NPM'; const SCRIPT = 'SCRIPT'; @@ -13,8 +14,11 @@ const TABS = [ { key: NPM, text: NPM }, ]; -const AssistDoc = (props) => { - const { projectKey } = props; +const AssistDoc = () => { + const { integrationsStore, projectsStore } = useStore(); + const sites = projectsStore.list; + const siteId = integrationsStore.integrations.siteId + const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey const [activeTab, setActiveTab] = useState(SCRIPT); const renderActiveTab = () => { @@ -53,10 +57,4 @@ const AssistDoc = (props) => { AssistDoc.displayName = 'AssistDoc'; -export default connect((state) => { - const siteId = state.getIn(['integrations', 'siteId']); - const sites = state.getIn(['site', 'list']); - return { - projectKey: sites.find((site) => site.get('id') === siteId).get('projectKey'), - }; -})(AssistDoc); +export default observer(AssistDoc); diff --git a/frontend/app/components/Client/Integrations/BugsnagForm/BugsnagForm.js b/frontend/app/components/Client/Integrations/BugsnagForm/BugsnagForm.js index f58154f91..780067755 100644 --- a/frontend/app/components/Client/Integrations/BugsnagForm/BugsnagForm.js +++ b/frontend/app/components/Client/Integrations/BugsnagForm/BugsnagForm.js @@ -1,7 +1,7 @@ import React from 'react'; import { tokenRE } from 'Types/integrations/bugsnagConfig'; import IntegrationForm from '../IntegrationForm'; -import ProjectListDropdown from './ProjectListDropdown'; +// import ProjectListDropdown from './ProjectListDropdown'; import DocLink from 'Shared/DocLink/DocLink'; import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModalCard'; @@ -31,7 +31,7 @@ const BugsnagForm = (props) => ( key: 'bugsnagProjectId', label: 'Project', checkIfDisplayed: (config) => tokenRE.test(config.authorizationToken), - component: ProjectListDropdown + // component: ProjectListDropdown } ]} /> diff --git a/frontend/app/components/Client/Integrations/BugsnagForm/ProjectListDropdown.js b/frontend/app/components/Client/Integrations/BugsnagForm/ProjectListDropdown.js index c30b57953..85dc8cf92 100644 --- a/frontend/app/components/Client/Integrations/BugsnagForm/ProjectListDropdown.js +++ b/frontend/app/components/Client/Integrations/BugsnagForm/ProjectListDropdown.js @@ -1,17 +1,20 @@ import React from 'react'; import { connect } from 'react-redux'; import { tokenRE } from 'Types/integrations/bugsnagConfig'; -import { edit } from 'Duck/integrations/actions'; import Select from 'Shared/Select'; import { withRequest } from 'HOCs'; +function ProjectListDropdown(props) { + +} + @connect(state => ({ token: state.getIn([ 'bugsnag', 'instance', 'authorizationToken' ]) -}), { edit }) +})) @withRequest({ dataName: "projects", initialData: [], - dataWrapper: (data = [], prevData) => { + dataWrapper: (data = []) => { if (!Array.isArray(data)) throw new Error('Wrong responce format.'); const withOrgName = data.length > 1; return data.reduce((accum, { name: orgName, projects }) => { @@ -35,15 +38,7 @@ export default class ProjectListDropdown extends React.PureComponent { if (!tokenRE.test(token)) return; this.props.fetchProjectList({ authorizationToken: token, - }).then(() => { - const { value, projects } = this.props; - const values = projects.map(p => p.id); - if (!values.includes(value) && values.length > 0) { - this.props.edit("bugsnag", { - projectId: values[0], - }); - } - }); + }) } componentDidUpdate(prevProps) { if (prevProps.token !== this.props.token) { diff --git a/frontend/app/components/Client/Integrations/CloudwatchForm/CloudwatchForm.js b/frontend/app/components/Client/Integrations/CloudwatchForm/CloudwatchForm.js index ca4e6ae3b..003545e23 100644 --- a/frontend/app/components/Client/Integrations/CloudwatchForm/CloudwatchForm.js +++ b/frontend/app/components/Client/Integrations/CloudwatchForm/CloudwatchForm.js @@ -1,41 +1,53 @@ +import { + ACCESS_KEY_ID_LENGTH, + SECRET_ACCESS_KEY_LENGTH, +} from 'Types/integrations/cloudwatchConfig'; import React from 'react'; -import { ACCESS_KEY_ID_LENGTH, SECRET_ACCESS_KEY_LENGTH } from 'Types/integrations/cloudwatchConfig'; + +import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModalCard'; + +import DocLink from 'Shared/DocLink/DocLink'; + import IntegrationForm from '../IntegrationForm'; import LogGroupDropdown from './LogGroupDropdown'; import RegionDropdown from './RegionDropdown'; -import DocLink from 'Shared/DocLink/DocLink'; -import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModalCard'; const CloudwatchForm = (props) => ( -
- -
-
How it works?
+
+ +
+
How it works?
  1. Create a Service Account
  2. Enter the details below
  3. Propagate openReplaySessionToken
- +
( checkIfDisplayed: (config) => config.awsSecretAccessKey.length === SECRET_ACCESS_KEY_LENGTH && config.region !== '' && - config.awsAccessKeyId.length === ACCESS_KEY_ID_LENGTH - } + config.awsAccessKeyId.length === ACCESS_KEY_ID_LENGTH, + }, ]} />
diff --git a/frontend/app/components/Client/Integrations/CloudwatchForm/LogGroupDropdown.js b/frontend/app/components/Client/Integrations/CloudwatchForm/LogGroupDropdown.js index d1d306244..ce2c85e3a 100644 --- a/frontend/app/components/Client/Integrations/CloudwatchForm/LogGroupDropdown.js +++ b/frontend/app/components/Client/Integrations/CloudwatchForm/LogGroupDropdown.js @@ -1,77 +1,93 @@ -import React from 'react'; -import { connect } from 'react-redux'; +import React, { useState, useEffect, useCallback } from 'react'; import { ACCESS_KEY_ID_LENGTH, SECRET_ACCESS_KEY_LENGTH } from 'Types/integrations/cloudwatchConfig'; -import { edit } from 'Duck/integrations/actions'; +import { observer } from 'mobx-react-lite'; +import { useStore } from 'App/mstore'; import Select from 'Shared/Select'; -import { withRequest } from 'HOCs'; +import { integrationsService } from "App/services"; -@connect(state => ({ - config: state.getIn([ 'cloudwatch', 'instance' ]) -}), { edit }) -@withRequest({ - dataName: "values", - initialData: [], - resetBeforeRequest: true, - requestName: "fetchLogGroups", - endpoint: '/integrations/cloudwatch/list_groups', - method: 'POST', -}) -export default class LogGroupDropdown extends React.PureComponent { - constructor(props) { - super(props); - this.fetchLogGroups() - } - fetchLogGroups() { - const { config } = this.props; - if (config.region === "" || - config.awsSecretAccessKey.length !== SECRET_ACCESS_KEY_LENGTH || - config.awsAccessKeyId.length !== ACCESS_KEY_ID_LENGTH - ) return; - this.props.fetchLogGroups({ - region: config.region, - awsSecretAccessKey: config.awsSecretAccessKey, - awsAccessKeyId: config.awsAccessKeyId, - }).then(() => { - const { value, values, name } = this.props; - if (!values.includes(value) && values.length > 0) { - this.props.edit("cloudwatch", { - [ name ]: values[0], - }); - } - }); - } - componentDidUpdate(prevProps) { - const { config } = this.props; - if (prevProps.config.region !== config.region || - prevProps.config.awsSecretAccessKey !== config.awsSecretAccessKey || - prevProps.config.awsAccessKeyId !== config.awsAccessKeyId) { - this.fetchLogGroups(); +const LogGroupDropdown = (props) => { + const { integrationsStore } = useStore(); + const config = integrationsStore.cloudwatch.instance; + const edit = integrationsStore.cloudwatch.edit; + const { + value, + name, + placeholder, + onChange, + } = props; + + const [values, setValues] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(false); + + const { region, awsSecretAccessKey, awsAccessKeyId } = config; + + const fetchLogGroups = useCallback(() => { + if ( + region === '' || + awsSecretAccessKey.length !== SECRET_ACCESS_KEY_LENGTH || + awsAccessKeyId.length !== ACCESS_KEY_ID_LENGTH + ) { + return; } - } - onChange = (target) => { - if (typeof this.props.onChange === 'function') { - this.props.onChange({ target }); + + setLoading(true); + setError(false); + setValues([]); // Reset values before request + + const params = { + region: region, + awsSecretAccessKey: awsSecretAccessKey, + awsAccessKeyId: awsAccessKeyId, + }; + + integrationsService.client + .post('/integrations/cloudwatch/list_groups', params) + .then((response) => response.json()) + .then(({ errors, data }) => { + if (errors) { + setError(true); + setLoading(false); + return; + } + setValues(data); + setLoading(false); + + // If current value is not in the new values list, update it + if (!data.includes(value) && data.length > 0) { + edit({ + [name]: data[0], + }); + } + }) + .catch(() => { + setError(true); + setLoading(false); + }); + }, [region, awsSecretAccessKey, awsAccessKeyId, value, name, edit]); + + // Fetch log groups on mount and when config changes + useEffect(() => { + fetchLogGroups(); + }, [fetchLogGroups]); + + const handleChange = (target) => { + if (typeof onChange === 'function') { + onChange({ target }); } - } - render() { - const { - values, - name, - value, - placeholder, - loading, - } = this.props; - const options = values.map(g => ({ text: g, value: g })); - return ( - o.value === value)} + placeholder={placeholder} + onChange={handleChange} + loading={loading} + /> + ); +}; + +export default observer(LogGroupDropdown); diff --git a/frontend/app/components/Client/Integrations/ElasticsearchForm.js b/frontend/app/components/Client/Integrations/ElasticsearchForm.js index 2c30cea47..8c5c3d7f6 100644 --- a/frontend/app/components/Client/Integrations/ElasticsearchForm.js +++ b/frontend/app/components/Client/Integrations/ElasticsearchForm.js @@ -1,97 +1,64 @@ import React from 'react'; -import { connect } from 'react-redux'; -import IntegrationForm from './IntegrationForm'; -import { withRequest } from 'HOCs'; -import { edit } from 'Duck/integrations/actions'; -import DocLink from 'Shared/DocLink/DocLink'; import IntegrationModalCard from 'Components/Client/Integrations/IntegrationModalCard'; -@connect( - (state) => ({ - config: state.getIn(['elasticsearch', 'instance']) - }), - { edit } -) -@withRequest({ - dataName: 'isValid', - initialData: false, - dataWrapper: (data) => data.state, - requestName: 'validateConfig', - endpoint: '/integrations/elasticsearch/test', - method: 'POST' -}) -export default class ElasticsearchForm extends React.PureComponent { - componentWillReceiveProps(newProps) { - const { - config: { host, port, apiKeyId, apiKey } - } = this.props; - const { loading, config } = newProps; - const valuesChanged = host !== config.host || port !== config.port || apiKeyId !== config.apiKeyId || apiKey !== config.apiKey; - if (!loading && valuesChanged && newProps.config.validateKeys() && newProps) { - this.validateConfig(newProps); - } - } +import DocLink from 'Shared/DocLink/DocLink'; - validateConfig = (newProps) => { - const { config } = newProps; - this.props - .validateConfig({ - host: config.host, - port: config.port, - apiKeyId: config.apiKeyId, - apiKey: config.apiKey - }) - .then((res) => { - const { isValid } = this.props; - this.props.edit('elasticsearch', { isValid: isValid }); - }); - }; +import IntegrationForm from './IntegrationForm'; - render() { - const props = this.props; - return ( -
- +const ElasticsearchForm = (props) => { + return ( +
+ -
-
How it works?
-
    -
  1. Create a new Elastic API key
  2. -
  3. Enter the API key below
  4. -
  5. Propagate openReplaySessionToken
  6. -
- -
- +
How it works?
+
    +
  1. Create a new Elastic API key
  2. +
  3. Enter the API key below
  4. +
  5. Propagate openReplaySessionToken
  6. +
+
- ); - } -} + +
+ ); +}; + +export default ElasticsearchForm; diff --git a/frontend/app/components/Client/Integrations/GraphQLDoc/GraphQLDoc.js b/frontend/app/components/Client/Integrations/GraphQLDoc/GraphQLDoc.js index 18c78d368..2b7bb916a 100644 --- a/frontend/app/components/Client/Integrations/GraphQLDoc/GraphQLDoc.js +++ b/frontend/app/components/Client/Integrations/GraphQLDoc/GraphQLDoc.js @@ -1,11 +1,15 @@ +import { useStore } from "App/mstore"; import React from 'react'; import { CodeBlock } from "UI"; import DocLink from 'Shared/DocLink/DocLink'; import ToggleContent from 'Shared/ToggleContent'; -import { connect } from 'react-redux'; +import { observer } from 'mobx-react-lite' -const GraphQLDoc = (props) => { - const { projectKey } = props; +const GraphQLDoc = () => { + const { integrationsStore, projectsStore } = useStore(); + const sites = projectsStore.list; + const siteId = integrationsStore.integrations.siteId + const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey const usage = `import OpenReplay from '@openreplay/tracker'; import trackerGraphQL from '@openreplay/tracker-graphql'; //... @@ -70,10 +74,4 @@ export const recordGraphQL = tracker.use(trackerGraphQL());` GraphQLDoc.displayName = 'GraphQLDoc'; -export default connect((state) => { - const siteId = state.getIn(['integrations', 'siteId']); - const sites = state.getIn(['site', 'list']); - return { - projectKey: sites.find((site) => site.get('id') === siteId).get('projectKey'), - }; -})(GraphQLDoc); +export default observer(GraphQLDoc); diff --git a/frontend/app/components/Client/Integrations/IntegrationForm.js b/frontend/app/components/Client/Integrations/IntegrationForm.js deleted file mode 100644 index c4d634562..000000000 --- a/frontend/app/components/Client/Integrations/IntegrationForm.js +++ /dev/null @@ -1,142 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { Input, Form, Button, Checkbox, Loader } from 'UI'; -import { save, init, edit, remove } from 'Duck/integrations/actions'; -import { fetchIntegrationList } from 'Duck/integrations/integrations'; - -@connect( - (state, { name, customPath }) => ({ - sites: state.getIn(['site', 'list']), - initialSiteId: state.getIn(['site', 'siteId']), - list: state.getIn([name, 'list']), - config: state.getIn([name, 'instance']), - loading: state.getIn([name, 'fetchRequest', 'loading']), - saving: state.getIn([customPath || name, 'saveRequest', 'loading']), - removing: state.getIn([name, 'removeRequest', 'loading']), - siteId: state.getIn(['integrations', 'siteId']), - }), - { - save, - init, - edit, - remove, - // fetchList, - fetchIntegrationList, - } -) -export default class IntegrationForm extends React.PureComponent { - constructor(props) { - super(props); - } - - fetchList = () => { - const { siteId, initialSiteId } = this.props; - if (!siteId) { - this.props.fetchIntegrationList(initialSiteId); - } else { - this.props.fetchIntegrationList(siteId); - } - } - - write = ({ target: { value, name: key, type, checked } }) => { - if (type === 'checkbox') this.props.edit(this.props.name, { [key]: checked }); - else this.props.edit(this.props.name, { [key]: value }); - }; - - // onChangeSelect = ({ value }) => { - // const { sites, list, name } = this.props; - // const site = sites.find((s) => s.id === value.value); - // this.setState({ currentSiteId: site.id }); - // this.init(value.value); - // }; - - // init = (siteId) => { - // const { list, name } = this.props; - // const config = parseInt(siteId) > 0 ? list.find((s) => s.projectId === siteId) : undefined; - // this.props.init(name, config ? config : list.first()); - // }; - - save = () => { - const { config, name, customPath, ignoreProject } = this.props; - const isExists = config.exists(); - // const { currentSiteId } = this.state; - this.props.save(customPath || name, !ignoreProject ? this.props.siteId : null, config).then(() => { - // this.props.fetchList(name); - this.fetchList(); - this.props.onClose(); - if (isExists) return; - }); - }; - - remove = () => { - const { name, config, ignoreProject } = this.props; - this.props.remove(name, !ignoreProject ? config.projectId : null).then(() => { - this.props.onClose(); - this.fetchList(); - }); - }; - - render() { - const { config, saving, removing, formFields, name, loading, integrated } = this.props; - return ( - -
-
- {formFields.map( - ({ - key, - label, - placeholder = label, - component: Component = 'input', - type = 'text', - checkIfDisplayed, - autoFocus = false, - }) => - (typeof checkIfDisplayed !== 'function' || checkIfDisplayed(config)) && - (type === 'checkbox' ? ( - - - - ) : ( - - - - - )) - )} - - - - {integrated && ( - - )} -
-
-
- ); - } -} diff --git a/frontend/app/components/Client/Integrations/IntegrationForm.tsx b/frontend/app/components/Client/Integrations/IntegrationForm.tsx new file mode 100644 index 000000000..1563b9358 --- /dev/null +++ b/frontend/app/components/Client/Integrations/IntegrationForm.tsx @@ -0,0 +1,107 @@ +import { observer } from 'mobx-react-lite'; +import React from 'react'; + +import { useStore } from 'App/mstore'; +import { namedStore } from 'App/mstore/integrationsStore'; +import { Button, Checkbox, Form, Input, Loader } from 'UI'; + +function IntegrationForm(props: any) { + const { formFields, name, integrated } = props; + const { integrationsStore, projectsStore } = useStore(); + const initialSiteId = projectsStore.siteId; + const integrationStore = integrationsStore[name as unknown as namedStore]; + const config = integrationStore.instance; + const loading = integrationStore.loading; + const onSave = integrationStore.saveIntegration; + const onRemove = integrationStore.deleteIntegration; + const edit = integrationStore.edit; + const fetchIntegrationList = integrationsStore.integrations.fetchIntegrations; + + const fetchList = () => { + void fetchIntegrationList(initialSiteId); + }; + + const write = ({ target: { value, name: key, type, checked } }) => { + if (type === 'checkbox') edit({ [key]: checked }); + else edit({ [key]: value }); + }; + + const save = () => { + const { name, customPath } = props; + onSave(customPath || name).then(() => { + fetchList(); + props.onClose(); + }); + }; + + const remove = () => { + onRemove().then(() => { + props.onClose(); + fetchList(); + }); + }; + + return ( + +
+
+ {formFields.map( + ({ + key, + label, + placeholder = label, + component: Component = 'input', + type = 'text', + checkIfDisplayed, + autoFocus = false, + }) => + (typeof checkIfDisplayed !== 'function' || + checkIfDisplayed(config)) && + (type === 'checkbox' ? ( + + + + ) : ( + + + + + )) + )} + + + + {integrated && ( + + )} +
+
+
+ ); +} + +export default observer(IntegrationForm); diff --git a/frontend/app/components/Client/Integrations/Integrations.tsx b/frontend/app/components/Client/Integrations/Integrations.tsx index e81a7038a..a39a533d5 100644 --- a/frontend/app/components/Client/Integrations/Integrations.tsx +++ b/frontend/app/components/Client/Integrations/Integrations.tsx @@ -1,88 +1,94 @@ -import React, { useEffect, useState } from 'react'; -import { connect } from 'react-redux'; -import { useModal } from 'App/components/Modal'; -import cn from 'classnames'; - -import { fetch, init } from 'Duck/integrations/actions'; -import { fetchIntegrationList, setSiteId } from 'Duck/integrations/integrations'; -import SiteDropdown from 'Shared/SiteDropdown'; -import ReduxDoc from './ReduxDoc'; -import VueDoc from './VueDoc'; -import GraphQLDoc from './GraphQLDoc'; -import NgRxDoc from './NgRxDoc'; -import MobxDoc from './MobxDoc'; -import ProfilerDoc from './ProfilerDoc'; -import AssistDoc from './AssistDoc'; -import PiniaDoc from './PiniaDoc'; -import ZustandDoc from './ZustandDoc'; -import MSTeams from './Teams'; -import DocCard from 'Shared/DocCard/DocCard'; -import { PageTitle, Tooltip } from 'UI'; import withPageTitle from 'HOCs/withPageTitle'; +import cn from 'classnames'; +import { observer } from 'mobx-react-lite'; +import React, { useEffect, useState } from 'react'; +import { useModal } from 'App/components/Modal'; +import { useStore } from 'App/mstore'; +import IntegrationFilters from 'Components/Client/Integrations/IntegrationFilters'; +import { PageTitle } from 'UI'; + +import DocCard from 'Shared/DocCard/DocCard'; + +import AssistDoc from './AssistDoc'; import BugsnagForm from './BugsnagForm'; import CloudwatchForm from './CloudwatchForm'; import DatadogForm from './DatadogForm'; import ElasticsearchForm from './ElasticsearchForm'; import GithubForm from './GithubForm'; +import GraphQLDoc from './GraphQLDoc'; import IntegrationItem from './IntegrationItem'; import JiraForm from './JiraForm'; +import MobxDoc from './MobxDoc'; import NewrelicForm from './NewrelicForm'; +import NgRxDoc from './NgRxDoc'; +import PiniaDoc from './PiniaDoc'; +import ProfilerDoc from './ProfilerDoc'; +import ReduxDoc from './ReduxDoc'; import RollbarForm from './RollbarForm'; import SentryForm from './SentryForm'; import SlackForm from './SlackForm'; import StackdriverForm from './StackdriverForm'; import SumoLogicForm from './SumoLogicForm'; -import IntegrationFilters from 'Components/Client/Integrations/IntegrationFilters'; +import MSTeams from './Teams'; +import VueDoc from './VueDoc'; +import ZustandDoc from './ZustandDoc'; interface Props { - fetch: (name: string, siteId: string) => void; - init: () => void; - fetchIntegrationList: (siteId: any) => void; - integratedList: any; - initialSiteId: string; - setSiteId: (siteId: string) => void; siteId: string; hideHeader?: boolean; - loading?: boolean; } function Integrations(props: Props) { - const { initialSiteId, hideHeader = false, loading = false } = props; + const { integrationsStore, projectsStore } = useStore(); + const siteId = projectsStore.siteId; + const fetchIntegrationList = integrationsStore.integrations.fetchIntegrations; + const storeIntegratedList = integrationsStore.integrations.list; + const { hideHeader = false } = props; const { showModal } = useModal(); const [integratedList, setIntegratedList] = useState([]); const [activeFilter, setActiveFilter] = useState('all'); useEffect(() => { - const list = props.integratedList + const list = storeIntegratedList .filter((item: any) => item.integrated) .map((item: any) => item.name); setIntegratedList(list); - }, [props.integratedList]); + }, [storeIntegratedList]); useEffect(() => { - props.fetchIntegrationList(initialSiteId); - props.setSiteId(initialSiteId); - }, []); + void fetchIntegrationList(siteId); + }, [siteId]); const onClick = (integration: any, width: number) => { - if (integration.slug && integration.slug !== 'slack' && integration.slug !== 'msteams') { - props.fetch(integration.slug, props.siteId); + if ( + integration.slug && + integration.slug !== 'slack' && + integration.slug !== 'msteams' + ) { + const intName = integration.slug as + | 'sentry' + | 'bugsnag' + | 'rollbar' + | 'elasticsearch' + | 'datadog' + | 'sumologic' + | 'stackdriver' + | 'cloudwatch' + | 'newrelic'; + if (integrationsStore[intName]) { + void integrationsStore[intName].fetchIntegration(siteId); + } } showModal( React.cloneElement(integration.component, { - integrated: integratedList.includes(integration.slug) + integrated: integratedList.includes(integration.slug), }), { right: true, width } ); }; - const onChangeSelect = ({ value }: any) => { - props.setSiteId(value.value); - props.fetchIntegrationList(value.value); - }; - const onChange = (key: string) => { setActiveFilter(key); }; @@ -99,83 +105,92 @@ function Integrations(props: Props) { key: cat.key, title: cat.title, label: cat.title, - icon: cat.icon - })) - - - const allIntegrations = filteredIntegrations.flatMap(cat => cat.integrations); + icon: cat.icon, + })); + const allIntegrations = filteredIntegrations.flatMap( + (cat) => cat.integrations + ); + console.log( + allIntegrations, + integratedList + ) return ( <> -
+
{!hideHeader && Integrations
} />} - +
-
+
-
+`)} + > {allIntegrations.map((integration: any) => ( - onClick(integration, filteredIntegrations.find(cat => cat.integrations.includes(integration)).title === 'Plugins' ? 500 : 350) + onClick( + integration, + filteredIntegrations.find((cat) => + cat.integrations.includes(integration) + ).title === 'Plugins' + ? 500 + : 350 + ) } hide={ (integration.slug === 'github' && integratedList.includes('jira')) || - (integration.slug === 'jira' && - integratedList.includes('github')) + (integration.slug === 'jira' && integratedList.includes('github')) } /> ))}
- ); } -export default connect( - (state: any) => ({ - initialSiteId: state.getIn(['site', 'siteId']), - integratedList: state.getIn(['integrations', 'list']) || [], - loading: state.getIn(['integrations', 'fetchRequest', 'loading']), - siteId: state.getIn(['integrations', 'siteId']) - }), - { fetch, init, fetchIntegrationList, setSiteId } -)(withPageTitle('Integrations - OpenReplay Preferences')(Integrations)); - +export default withPageTitle('Integrations - OpenReplay Preferences')(observer(Integrations)) const integrations = [ { title: 'Issue Reporting', key: 'issue-reporting', - description: 'Seamlessly report issues or share issues with your team right from OpenReplay.', + description: + 'Seamlessly report issues or share issues with your team right from OpenReplay.', isProject: false, icon: 'exclamation-triangle', integrations: [ { title: 'Jira', - subtitle: 'Integrate Jira with OpenReplay to enable the creation of a new ticket directly from a session.', + subtitle: + 'Integrate Jira with OpenReplay to enable the creation of a new ticket directly from a session.', slug: 'jira', category: 'Errors', icon: 'integrations/jira', - component: + component: , }, { title: 'Github', - subtitle: 'Integrate GitHub with OpenReplay to enable the direct creation of a new issue from a session.', + subtitle: + 'Integrate GitHub with OpenReplay to enable the direct creation of a new issue from a session.', slug: 'github', category: 'Errors', icon: 'integrations/github', - component: - } - ] + component: , + }, + ], }, { title: 'Backend Logging', @@ -186,106 +201,119 @@ const integrations = [ '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. + Sync your backend errors with sessions replays and see what happened + front-to-back. ), integrations: [ { title: 'Sentry', - subtitle: 'Integrate Sentry with session replays to seamlessly observe backend errors.', + subtitle: + 'Integrate Sentry with session replays to seamlessly observe backend errors.', slug: 'sentry', icon: 'integrations/sentry', - component: + component: , }, { title: 'Bugsnag', - subtitle: 'Integrate Bugsnag to access the OpenReplay session linked to the JS exception within its interface.', + subtitle: + 'Integrate Bugsnag to access the OpenReplay session linked to the JS exception within its interface.', slug: 'bugsnag', icon: 'integrations/bugsnag', - component: + component: , }, { title: 'Rollbar', - subtitle: 'Integrate Rollbar with session replays to seamlessly observe backend errors.', + subtitle: + 'Integrate Rollbar with session replays to seamlessly observe backend errors.', slug: 'rollbar', icon: 'integrations/rollbar', - component: + component: , }, { title: 'Elasticsearch', - subtitle: 'Integrate Elasticsearch with session replays to seamlessly observe backend errors.', + subtitle: + 'Integrate Elasticsearch with session replays to seamlessly observe backend errors.', slug: 'elasticsearch', icon: 'integrations/elasticsearch', - component: + component: , }, { title: 'Datadog', - subtitle: 'Incorporate DataDog to visualize backend errors alongside session replay, for easy troubleshooting.', + subtitle: + 'Incorporate DataDog to visualize backend errors alongside session replay, for easy troubleshooting.', slug: 'datadog', icon: 'integrations/datadog', - component: + component: , }, { title: 'Sumo Logic', - subtitle: 'Integrate Sumo Logic with session replays to seamlessly observe backend errors.', + subtitle: + 'Integrate Sumo Logic with session replays to seamlessly observe backend errors.', slug: 'sumologic', icon: 'integrations/sumologic', - component: + component: , }, { title: 'Google Cloud', - subtitle: 'Integrate Google Cloud to view backend logs and errors in conjunction with session replay', + subtitle: + 'Integrate Google Cloud to view backend logs and errors in conjunction with session replay', slug: 'stackdriver', icon: 'integrations/google-cloud', - component: + component: , }, { title: 'CloudWatch', - subtitle: 'Integrate CloudWatch to see backend logs and errors alongside session replay.', + subtitle: + 'Integrate CloudWatch to see backend logs and errors alongside session replay.', slug: 'cloudwatch', icon: 'integrations/aws', - component: + component: , }, { title: 'Newrelic', - subtitle: 'Integrate NewRelic with session replays to seamlessly observe backend errors.', + subtitle: + 'Integrate NewRelic with session replays to seamlessly observe backend errors.', slug: 'newrelic', icon: 'integrations/newrelic', - component: - } - ] + component: , + }, + ], }, { title: 'Collaboration', key: 'collaboration', isProject: false, icon: 'file-code', - description: 'Share your sessions with your team and collaborate on issues.', + description: + 'Share your sessions with your team and collaborate on issues.', integrations: [ { title: 'Slack', - subtitle: 'Integrate Slack to empower every user in your org with the ability to send sessions to any Slack channel.', + subtitle: + 'Integrate Slack to empower every user in your org with the ability to send sessions to any Slack channel.', slug: 'slack', category: 'Errors', icon: 'integrations/slack', component: , - shared: true + shared: true, }, { title: 'MS Teams', - subtitle: 'Integrate MS Teams to empower every user in your org with the ability to send sessions to any MS Teams channel.', + subtitle: + 'Integrate MS Teams to empower every user in your org with the ability to send sessions to any MS Teams channel.', slug: 'msteams', category: 'Errors', icon: 'integrations/teams', component: , - shared: true - } - ] + shared: true, + }, + ], }, // { // title: 'State Management', @@ -302,72 +330,82 @@ const integrations = [ 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. + Plugins capture your application’s store, monitor queries, track + performance issues and even assist your end user through live sessions. ), description: - 'Reproduce issues as if they happened in your own browser. Plugins help capture your application\'s store, HTTP requeets, GraphQL queries, and more.', + "Reproduce issues as if they happened in your own browser. Plugins help capture your application's store, HTTP requeets, GraphQL queries, and more.", integrations: [ { title: 'Redux', - subtitle: 'Capture Redux actions/state and inspect them later on while replaying session recordings.', - icon: 'integrations/redux', component: + subtitle: + 'Capture Redux actions/state and inspect them later on while replaying session recordings.', + icon: 'integrations/redux', + component: , }, { title: 'VueX', - subtitle: 'Capture VueX mutations/state and inspect them later on while replaying session recordings.', + subtitle: + 'Capture VueX mutations/state and inspect them later on while replaying session recordings.', icon: 'integrations/vuejs', - component: + component: , }, { title: 'Pinia', - subtitle: 'Capture Pinia mutations/state and inspect them later on while replaying session recordings.', + subtitle: + 'Capture Pinia mutations/state and inspect them later on while replaying session recordings.', icon: 'integrations/pinia', - component: + component: , }, { title: 'GraphQL', - subtitle: 'Capture GraphQL requests and inspect them later on while replaying session recordings. This plugin is compatible with Apollo and Relay implementations.', + subtitle: + 'Capture GraphQL requests and inspect them later on while replaying session recordings. This plugin is compatible with Apollo and Relay implementations.', icon: 'integrations/graphql', - component: + component: , }, { title: 'NgRx', - subtitle: 'Capture NgRx actions/state and inspect them later on while replaying session recordings.\n', + subtitle: + 'Capture NgRx actions/state and inspect them later on while replaying session recordings.\n', icon: 'integrations/ngrx', - component: + component: , }, { title: 'MobX', - subtitle: 'Capture MobX mutations and inspect them later on while replaying session recordings.', + subtitle: + 'Capture MobX mutations and inspect them later on while replaying session recordings.', icon: 'integrations/mobx', - component: + component: , }, { title: 'Profiler', - subtitle: 'Plugin allows you to measure your JS functions performance and capture both arguments and result for each call.', + subtitle: + 'Plugin allows you to measure your JS functions performance and capture both arguments and result for each call.', icon: 'integrations/openreplay', - component: + component: , }, { title: 'Assist', - subtitle: 'OpenReplay Assist allows you to support your users by seeing their live screen and instantly hopping on call (WebRTC) with them without requiring any 3rd-party screen sharing software.\n', + subtitle: + 'OpenReplay Assist allows you to support your users by seeing their live screen and instantly hopping on call (WebRTC) with them without requiring any 3rd-party screen sharing software.\n', icon: 'integrations/openreplay', - component: + component: , }, { title: 'Zustand', - subtitle: 'Capture Zustand mutations/state and inspect them later on while replaying session recordings.', + subtitle: + 'Capture Zustand mutations/state and inspect them later on while replaying session recordings.', icon: 'integrations/zustand', // header: '🐻', - component: - } - ] - } + component: , + }, + ], + }, ]; diff --git a/frontend/app/components/Client/Integrations/MobxDoc/MobxDoc.js b/frontend/app/components/Client/Integrations/MobxDoc/MobxDoc.js index 65705b1e0..9c917f876 100644 --- a/frontend/app/components/Client/Integrations/MobxDoc/MobxDoc.js +++ b/frontend/app/components/Client/Integrations/MobxDoc/MobxDoc.js @@ -1,11 +1,15 @@ import React from 'react'; import ToggleContent from 'Shared/ToggleContent'; import DocLink from 'Shared/DocLink/DocLink'; -import { connect } from 'react-redux'; import { CodeBlock } from "UI"; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; -const MobxDoc = (props) => { - const { projectKey } = props; +const MobxDoc = () => { + const { integrationsStore, projectsStore } = useStore(); + const sites = projectsStore.list; + const siteId = integrationsStore.integrations.siteId + const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey const mobxUsage = `import OpenReplay from '@openreplay/tracker'; import trackerMobX from '@openreplay/tracker-mobx'; @@ -67,10 +71,4 @@ function SomeFunctionalComponent() { MobxDoc.displayName = 'MobxDoc'; -export default connect((state) => { - const siteId = state.getIn(['integrations', 'siteId']); - const sites = state.getIn(['site', 'list']); - return { - projectKey: sites.find((site) => site.get('id') === siteId).get('projectKey'), - }; -})(MobxDoc); +export default observer(MobxDoc) diff --git a/frontend/app/components/Client/Integrations/NgRxDoc/NgRxDoc.js b/frontend/app/components/Client/Integrations/NgRxDoc/NgRxDoc.js index 8097a4618..e4d199f5b 100644 --- a/frontend/app/components/Client/Integrations/NgRxDoc/NgRxDoc.js +++ b/frontend/app/components/Client/Integrations/NgRxDoc/NgRxDoc.js @@ -1,11 +1,15 @@ +import { useStore } from "App/mstore"; import React from 'react'; import { CodeBlock } from "UI"; import ToggleContent from 'Shared/ToggleContent'; import DocLink from 'Shared/DocLink/DocLink'; -import { connect } from 'react-redux'; +import { observer } from 'mobx-react-lite' -const NgRxDoc = (props) => { - const { projectKey } = props; +const NgRxDoc = () => { + const { integrationsStore, projectsStore } = useStore(); + const sites = projectsStore.list; + const siteId = integrationsStore.integrations.siteId + const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey const usage = `import { StoreModule } from '@ngrx/store'; import { reducers } from './reducers'; import OpenReplay from '@openreplay/tracker'; @@ -80,10 +84,4 @@ const metaReducers = [tracker.use(trackerNgRx())]; // check list of ava NgRxDoc.displayName = 'NgRxDoc'; -export default connect((state) => { - const siteId = state.getIn(['integrations', 'siteId']); - const sites = state.getIn(['site', 'list']); - return { - projectKey: sites.find((site) => site.get('id') === siteId).get('projectKey'), - }; -})(NgRxDoc); +export default observer(NgRxDoc); diff --git a/frontend/app/components/Client/Integrations/PiniaDoc/PiniaDoc.tsx b/frontend/app/components/Client/Integrations/PiniaDoc/PiniaDoc.tsx index a0a9c157e..71a9cbccb 100644 --- a/frontend/app/components/Client/Integrations/PiniaDoc/PiniaDoc.tsx +++ b/frontend/app/components/Client/Integrations/PiniaDoc/PiniaDoc.tsx @@ -1,11 +1,19 @@ +import { observer } from 'mobx-react-lite'; import React from 'react'; -import { CodeBlock } from "UI"; -import ToggleContent from '../../../shared/ToggleContent'; -import DocLink from 'Shared/DocLink/DocLink'; -import { connect } from 'react-redux'; -const PiniaDoc = (props) => { - const { projectKey } = props; +import { useStore } from 'App/mstore'; +import ToggleContent from 'Components/shared/ToggleContent'; +import { CodeBlock } from 'UI'; + +import DocLink from 'Shared/DocLink/DocLink'; + +const PiniaDoc = () => { + const { integrationsStore, projectsStore } = useStore(); + const sites = projectsStore.list; + const siteId = integrationsStore.integrations.siteId; + const projectKey = siteId + ? sites.find((site) => site.id === siteId)?.projectKey + : sites[0]?.projectKey; const usage = `import Vuex from 'vuex' import OpenReplay from '@openreplay/tracker'; import trackerVuex from '@openreplay/tracker-vuex'; @@ -28,7 +36,7 @@ piniaStorePlugin(examplePiniaStore) // now you can use examplePiniaStore as // usual pinia store // (destructure values or return it as a whole etc) -` +`; const usageCjs = `import Vuex from 'vuex' import OpenReplay from '@openreplay/tracker/cjs'; import trackerVuex from '@openreplay/tracker-vuex/cjs'; @@ -55,34 +63,38 @@ piniaStorePlugin(examplePiniaStore) // now you can use examplePiniaStore as // usual pinia store // (destructure values or return it as a whole etc) -}` +}`; return ( -
+

VueX

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

- Initialize the @openreplay/tracker package as usual and load the plugin into it. Then put - the generated plugin into your plugins field of your store. + 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={ - - } + first={} + second={} /> { - const siteId = state.getIn(['integrations', 'siteId']); - const sites = state.getIn(['site', 'list']); - return { - projectKey: sites.find((site: any) => site.get('id') === siteId).get('projectKey'), - }; -})(PiniaDoc); +export default observer(PiniaDoc); diff --git a/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js b/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js index eb7ad3999..35abc1eb0 100644 --- a/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js +++ b/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js @@ -1,13 +1,16 @@ +import { useStore } from "App/mstore"; import React from 'react'; -import { connect } from 'react-redux'; - +import { observer } from 'mobx-react-lite'; import { CodeBlock } from 'UI'; import DocLink from 'Shared/DocLink/DocLink'; import ToggleContent from 'Shared/ToggleContent'; -const ProfilerDoc = (props) => { - const { projectKey } = props; +const ProfilerDoc = () => { + const { integrationsStore, projectsStore } = useStore(); + const sites = projectsStore.list; + const siteId = integrationsStore.integrations.siteId + const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey const usage = `import OpenReplay from '@openreplay/tracker'; import trackerProfiler from '@openreplay/tracker-profiler'; @@ -87,12 +90,4 @@ const fn = profiler('call_name')(() => { ProfilerDoc.displayName = 'ProfilerDoc'; -export default connect((state) => { - const siteId = state.getIn(['integrations', 'siteId']); - const sites = state.getIn(['site', 'list']); - return { - projectKey: sites - .find((site) => site.get('id') === siteId) - .get('projectKey'), - }; -})(ProfilerDoc); +export default observer(ProfilerDoc); diff --git a/frontend/app/components/Client/Integrations/ReduxDoc/ReduxDoc.js b/frontend/app/components/Client/Integrations/ReduxDoc/ReduxDoc.js index 3566bb82d..e782a2298 100644 --- a/frontend/app/components/Client/Integrations/ReduxDoc/ReduxDoc.js +++ b/frontend/app/components/Client/Integrations/ReduxDoc/ReduxDoc.js @@ -1,11 +1,15 @@ +import { useStore } from "App/mstore"; import React from 'react'; import { CodeBlock } from 'UI' -import ToggleContent from '../../../shared/ToggleContent'; +import ToggleContent from 'Components/shared/ToggleContent'; import DocLink from 'Shared/DocLink/DocLink'; -import { connect } from 'react-redux'; +import { observer } from 'mobx-react-lite' -const ReduxDoc = (props) => { - const { projectKey } = props; +const ReduxDoc = () => { + const { integrationsStore, projectsStore } = useStore(); + const sites = projectsStore.list; + const siteId = integrationsStore.integrations.siteId + const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey const usage = `import { applyMiddleware, createStore } from 'redux'; import OpenReplay from '@openreplay/tracker'; @@ -74,10 +78,4 @@ const store = createStore( ReduxDoc.displayName = 'ReduxDoc'; -export default connect((state) => { - const siteId = state.getIn(['integrations', 'siteId']); - const sites = state.getIn(['site', 'list']); - return { - projectKey: sites.find((site) => site.get('id') === siteId).get('projectKey'), - }; -})(ReduxDoc); +export default observer(ReduxDoc); diff --git a/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js b/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js index 26cfc9520..b8cdc1981 100644 --- a/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js +++ b/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js @@ -1,25 +1,35 @@ import React from 'react'; -import { connect } from 'react-redux'; -import { edit, save, init, update } from 'Duck/integrations/slack'; import { Form, Input, Button, Message } from 'UI'; import { confirm } from 'UI'; -import { remove } from 'Duck/integrations/slack'; +import { observer } from 'mobx-react-lite' +import { useStore } from 'App/mstore' -class SlackAddForm extends React.PureComponent { - componentWillUnmount() { - this.props.init({}); - } +function SlackAddForm(props) { + const { onClose } = props; + const { integrationsStore } = useStore(); + const instance = integrationsStore.slack.instance; + const saving = integrationsStore.slack.loading; + const errors = integrationsStore.slack.errors; + const edit = integrationsStore.slack.edit; + const onSave = integrationsStore.slack.saveIntegration; + const update = integrationsStore.slack.update; + const init = integrationsStore.slack.init; + const onRemove = integrationsStore.slack.removeInt; + + React.useEffect(() => { + return () => init({}) + }, []) - save = () => { - const instance = this.props.instance; + + const save = () => { if (instance.exists()) { - this.props.update(this.props.instance); + void update(instance); } else { - this.props.save(this.props.instance); + void onSave(instance); } }; - remove = async (id) => { + const remove = async (id) => { if ( await confirm({ header: 'Confirm', @@ -27,79 +37,68 @@ class SlackAddForm extends React.PureComponent { confirmation: `Are you sure you want to permanently delete this channel?`, }) ) { - this.props.remove(id); + await onRemove(id); + onClose(); } }; - write = ({ target: { name, value } }) => this.props.edit({ [name]: value }); - - render() { - const { instance, saving, errors, onClose } = this.props; - return ( -
-
- - - - - - - - -
-
- - - -
- - -
-
- {errors && ( -
- {errors.map((error) => ( - - {error} - - ))} +
- )} -
- ); - } + + +
+ + + {errors && ( +
+ {errors.map((error) => ( + + {error} + + ))} +
+ )} +
+ ); } -export default connect( - (state) => ({ - instance: state.getIn(['slack', 'instance']), - saving: - state.getIn(['slack', 'saveRequest', 'loading']) || - state.getIn(['slack', 'updateRequest', 'loading']), - errors: state.getIn(['slack', 'saveRequest', 'errors']), - }), - { edit, save, init, remove, update } -)(SlackAddForm); +export default observer(SlackAddForm); diff --git a/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js b/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js index 8d25b4454..db53d3100 100644 --- a/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js +++ b/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js @@ -1,14 +1,16 @@ import React from 'react'; -import { connect } from 'react-redux'; import { NoContent } from 'UI'; -import { remove, edit, init } from 'Duck/integrations/slack'; import DocLink from 'Shared/DocLink/DocLink'; +import { observer } from 'mobx-react-lite' +import { useStore } from 'App/mstore' function SlackChannelList(props) { - const { list } = props; + const { integrationsStore } = useStore(); + const list = integrationsStore.slack.list; + const edit = integrationsStore.slack.edit; const onEdit = (instance) => { - props.edit(instance); + edit(instance.toData()); props.onEdit(); }; @@ -24,7 +26,7 @@ function SlackChannelList(props) {
} size="small" - show={list.size === 0} + show={list.length === 0} > {list.map((c) => (
({ - list: state.getIn(['slack', 'list']), - }), - { remove, edit, init } -)(SlackChannelList); +export default observer(SlackChannelList); diff --git a/frontend/app/components/Client/Integrations/SlackForm.tsx b/frontend/app/components/Client/Integrations/SlackForm.tsx index 018dbe885..43a720da4 100644 --- a/frontend/app/components/Client/Integrations/SlackForm.tsx +++ b/frontend/app/components/Client/Integrations/SlackForm.tsx @@ -1,17 +1,14 @@ import React, { useEffect } from 'react'; import SlackChannelList from './SlackChannelList/SlackChannelList'; -import { fetchList, init } from 'Duck/integrations/slack'; -import { connect } from 'react-redux'; import SlackAddForm from './SlackAddForm'; import { Button } from 'UI'; +import { observer } from 'mobx-react-lite' +import { useStore } from 'App/mstore' -interface Props { - onEdit?: (integration: any) => void; - istance: any; - fetchList: any; - init: any; -} -const SlackForm = (props: Props) => { +const SlackForm = () => { + const { integrationsStore } = useStore(); + const init = integrationsStore.slack.init; + const fetchList = integrationsStore.slack.fetchIntegrations; const [active, setActive] = React.useState(false); const onEdit = () => { @@ -20,11 +17,11 @@ const SlackForm = (props: Props) => { const onNew = () => { setActive(true); - props.init({}); + init({}); } useEffect(() => { - props.fetchList(); + void fetchList(); }, []); return ( @@ -47,9 +44,4 @@ const SlackForm = (props: Props) => { SlackForm.displayName = 'SlackForm'; -export default connect( - (state: any) => ({ - istance: state.getIn(['slack', 'instance']), - }), - { fetchList, init } -)(SlackForm); +export default observer(SlackForm); \ No newline at end of file diff --git a/frontend/app/components/Client/Integrations/Teams/TeamsAddForm.tsx b/frontend/app/components/Client/Integrations/Teams/TeamsAddForm.tsx index f13efc535..e45d6d7b1 100644 --- a/frontend/app/components/Client/Integrations/Teams/TeamsAddForm.tsx +++ b/frontend/app/components/Client/Integrations/Teams/TeamsAddForm.tsx @@ -1,36 +1,38 @@ +import { observer } from 'mobx-react-lite'; import React from 'react'; -import { connect } from 'react-redux'; -import { edit, save, init, update, remove } from 'Duck/integrations/teams'; -import { Form, Input, Button, Message } from 'UI'; + +import { useStore } from 'App/mstore'; +import { Button, Form, Input, Message } from 'UI'; import { confirm } from 'UI'; interface Props { - edit: (inst: any) => void; - save: (inst: any) => void; - init: (inst: any) => void; - update: (inst: any) => void; - remove: (id: string) => void; onClose: () => void; - instance: any; - saving: boolean; - errors: any; } -class TeamsAddForm extends React.PureComponent { - componentWillUnmount() { - this.props.init({}); - } +function TeamsAddForm({ onClose }: Props) { + const { integrationsStore } = useStore(); + const instance = integrationsStore.msteams.instance; + const saving = integrationsStore.msteams.loading; + const errors = integrationsStore.msteams.errors; + const edit = integrationsStore.msteams.edit; + const onSave = integrationsStore.msteams.saveIntegration; + const init = integrationsStore.msteams.init; + const onRemove = integrationsStore.msteams.removeInt; + const update = integrationsStore.msteams.update; - save = () => { - const instance = this.props.instance; - if (instance.exists()) { - this.props.update(this.props.instance); + React.useEffect(() => { + return () => init({}); + }, []); + + const save = () => { + if (instance?.exists()) { + void update(); } else { - this.props.save(this.props.instance); + void onSave(); } }; - remove = async (id: string) => { + const remove = async (id: string) => { if ( await confirm({ header: 'Confirm', @@ -38,80 +40,74 @@ class TeamsAddForm extends React.PureComponent { confirmation: `Are you sure you want to permanently delete this channel?`, }) ) { - this.props.remove(id); + void onRemove(id); } }; - write = ({ target: { name, value } }: { target: { name: string; value: string } }) => - this.props.edit({ [name]: value }); + const write = ({ + target: { name, value }, + }: { + target: { name: string; value: string }; + }) => edit({ [name]: value }); - render() { - const { instance, saving, errors, onClose } = this.props; - return ( -
-
- - - - - - - - -
-
- - - -
- - -
-
- {errors && ( -
- {errors.map((error: any) => ( - - {error} - - ))} +
- )} -
- ); - } + + +
+ + + {errors && ( +
+ {errors.map((error: any) => ( + + {error} + + ))} +
+ )} +
+ ); } -export default connect( - (state: any) => ({ - instance: state.getIn(['teams', 'instance']), - saving: - state.getIn(['teams', 'saveRequest', 'loading']) || - state.getIn(['teams', 'updateRequest', 'loading']), - errors: state.getIn(['teams', 'saveRequest', 'errors']), - }), - { edit, save, init, remove, update } -)(TeamsAddForm); +export default observer(TeamsAddForm); diff --git a/frontend/app/components/Client/Integrations/Teams/TeamsChannelList.tsx b/frontend/app/components/Client/Integrations/Teams/TeamsChannelList.tsx index 942e1e32c..131a404c8 100644 --- a/frontend/app/components/Client/Integrations/Teams/TeamsChannelList.tsx +++ b/frontend/app/components/Client/Integrations/Teams/TeamsChannelList.tsx @@ -1,51 +1,57 @@ +import { observer } from 'mobx-react-lite'; import React from 'react'; -import { connect } from 'react-redux'; + +import { useStore } from 'App/mstore'; import { NoContent } from 'UI'; -import { remove, edit, init } from 'Duck/integrations/teams'; + import DocLink from 'Shared/DocLink/DocLink'; -function TeamsChannelList(props: { list: any, edit: (inst: any) => any, onEdit: () => void }) { - const { list } = props; +function TeamsChannelList(props: { onEdit: () => void }) { + const { integrationsStore } = useStore(); + const list = integrationsStore.msteams.list; + const edit = integrationsStore.msteams.edit; - const onEdit = (instance: Record) => { - props.edit(instance); - props.onEdit(); - }; + const onEdit = (instance: Record) => { + edit(instance); + props.onEdit(); + }; - return ( -
- -
- Integrate MS Teams with OpenReplay and share insights with the rest of the team, directly from the recording page. -
- -
- } - size="small" - show={list.size === 0} - > - {list.map((c: any) => ( -
onEdit(c)} - > -
-
{c.name}
-
{c.endpoint}
-
-
- ))} - -
- ); + return ( +
+ +
+ 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} + > + {list.map((c: any) => ( +
onEdit(c)} + > +
+
{c.name}
+
+ {c.endpoint} +
+
+
+ ))} + +
+ ); } -export default connect( - (state: any) => ({ - list: state.getIn(['teams', 'list']), - }), - { remove, edit, init } -)(TeamsChannelList); +export default observer(TeamsChannelList); diff --git a/frontend/app/components/Client/Integrations/Teams/index.tsx b/frontend/app/components/Client/Integrations/Teams/index.tsx index e51bd64b1..b85a4f010 100644 --- a/frontend/app/components/Client/Integrations/Teams/index.tsx +++ b/frontend/app/components/Client/Integrations/Teams/index.tsx @@ -1,17 +1,15 @@ import React, { useEffect } from 'react'; import TeamsChannelList from './TeamsChannelList'; -import { fetchList, init } from 'Duck/integrations/teams'; -import { connect } from 'react-redux'; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; + import TeamsAddForm from './TeamsAddForm'; import { Button } from 'UI'; -interface Props { - onEdit?: (integration: any) => void; - istance: any; - fetchList: any; - init: any; -} -const MSTeams = (props: Props) => { +const MSTeams = () => { + const { integrationsStore } = useStore(); + const fetchList = integrationsStore.msteams.fetchIntegrations; + const init = integrationsStore.msteams.init; const [active, setActive] = React.useState(false); const onEdit = () => { @@ -20,11 +18,11 @@ const MSTeams = (props: Props) => { const onNew = () => { setActive(true); - props.init({}); + init({}); } useEffect(() => { - props.fetchList(); + void fetchList(); }, []); return ( @@ -47,9 +45,4 @@ const MSTeams = (props: Props) => { MSTeams.displayName = 'MSTeams'; -export default connect( - (state: any) => ({ - istance: state.getIn(['teams', 'instance']), - }), - { fetchList, init } -)(MSTeams); +export default observer(MSTeams); diff --git a/frontend/app/components/Client/Integrations/VueDoc/VueDoc.js b/frontend/app/components/Client/Integrations/VueDoc/VueDoc.js index 2a9e2e2a6..1c2ff19c6 100644 --- a/frontend/app/components/Client/Integrations/VueDoc/VueDoc.js +++ b/frontend/app/components/Client/Integrations/VueDoc/VueDoc.js @@ -1,11 +1,15 @@ +import { useStore } from "App/mstore"; import React from 'react'; import { CodeBlock } from "UI"; -import ToggleContent from '../../../shared/ToggleContent'; +import ToggleContent from 'Components/shared/ToggleContent'; import DocLink from 'Shared/DocLink/DocLink'; -import { connect } from 'react-redux'; +import { observer } from 'mobx-react-lite'; -const VueDoc = (props) => { - const { projectKey, siteId } = props; +const VueDoc = () => { + const { integrationsStore, projectsStore } = useStore(); + const sites = projectsStore.list; + const siteId = integrationsStore.integrations.siteId + const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey const usage = `import Vuex from 'vuex' import OpenReplay from '@openreplay/tracker'; @@ -81,10 +85,4 @@ const store = new Vuex.Store({ VueDoc.displayName = 'VueDoc'; -export default connect((state) => { - const siteId = state.getIn(['integrations', 'siteId']); - const sites = state.getIn(['site', 'list']); - return { - projectKey: sites.find((site) => site.get('id') === siteId).get('projectKey'), - }; -})(VueDoc); +export default observer(VueDoc); diff --git a/frontend/app/components/Client/Integrations/ZustandDoc/ZustandDoc.js b/frontend/app/components/Client/Integrations/ZustandDoc/ZustandDoc.js index 9ed446cdc..eb7dcd091 100644 --- a/frontend/app/components/Client/Integrations/ZustandDoc/ZustandDoc.js +++ b/frontend/app/components/Client/Integrations/ZustandDoc/ZustandDoc.js @@ -1,11 +1,15 @@ +import { useStore } from "App/mstore"; import React from 'react'; import { CodeBlock } from "UI"; -import ToggleContent from '../../../shared/ToggleContent'; +import ToggleContent from 'Components//shared/ToggleContent'; import DocLink from 'Shared/DocLink/DocLink'; -import { connect } from 'react-redux'; +import { observer } from 'mobx-react-lite' const ZustandDoc = (props) => { - const { projectKey } = props; + const { integrationsStore, projectsStore } = useStore(); + const sites = projectsStore.list; + const siteId = integrationsStore.integrations.siteId + const projectKey = siteId ? sites.find((site) => site.id === siteId)?.projectKey : sites[0]?.projectKey const usage = `import create from "zustand"; import Tracker from '@openreplay/tracker'; @@ -97,10 +101,4 @@ const useBearStore = create( ZustandDoc.displayName = 'ZustandDoc'; -export default connect((state) => { - const siteId = state.getIn(['integrations', 'siteId']); - const sites = state.getIn(['site', 'list']); - return { - projectKey: sites.find((site) => site.get('id') === siteId).get('projectKey'), - }; -})(ZustandDoc); +export default observer(ZustandDoc); diff --git a/frontend/app/components/Client/Modules/Modules.tsx b/frontend/app/components/Client/Modules/Modules.tsx index 2ee7c1424..2767ff76f 100644 --- a/frontend/app/components/Client/Modules/Modules.tsx +++ b/frontend/app/components/Client/Modules/Modules.tsx @@ -2,20 +2,17 @@ import React, { useEffect } from 'react'; import ModuleCard from 'Components/Client/Modules/ModuleCard'; import { modules as list } from './'; import withPageTitle from 'HOCs/withPageTitle'; -import { connect } from 'react-redux'; import { userService } from 'App/services'; import { toast } from 'react-toastify'; -import { updateModule } from 'Duck/user'; +import { useStore } from "App/mstore"; +import { observer } from 'mobx-react-lite'; -interface Props { - modules: string[]; - updateModule: (moduleKey: string) => void; - isEnterprise: boolean; -} - -function Modules(props: Props) { - const { modules } = props; - const [modulesState, setModulesState, isEnterprise = false] = React.useState([]); +function Modules() { + const { userStore } = useStore(); + const updateModule = userStore.updateModule; + const modules = userStore.account.settings?.modules ?? []; + const isEnterprise = userStore.account.edition === 'ee'; + const [modulesState, setModulesState] = React.useState([]); const onToggle = async (module: any) => { try { @@ -26,7 +23,7 @@ function Modules(props: Props) { module: module.key, status: isEnabled, }); - props.updateModule(module.key); + updateModule(module.key); toast.success(`Module ${module.label} ${!isEnabled ? 'enabled' : 'disabled'}`); } catch (err) { console.error(err); @@ -66,7 +63,4 @@ function Modules(props: Props) { } -export default withPageTitle('Modules - OpenReplay Preferences')(connect((state: any) => ({ - modules: state.getIn(['user', 'account', 'settings', 'modules']) || [], - isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee' -}), { updateModule })(Modules)); +export default withPageTitle('Modules - OpenReplay Preferences')(observer(Modules)); diff --git a/frontend/app/components/Client/ProfileSettings/Api.js b/frontend/app/components/Client/ProfileSettings/Api.js index 4307895aa..8a2614a8b 100644 --- a/frontend/app/components/Client/ProfileSettings/Api.js +++ b/frontend/app/components/Client/ProfileSettings/Api.js @@ -1,46 +1,25 @@ import React from 'react'; -import copy from 'copy-to-clipboard'; -import { connect } from 'react-redux'; -import styles from './profileSettings.module.css'; -import { Form, Input, Button, CopyButton } from 'UI'; +import { observer } from 'mobx-react-lite' +import { useStore } from 'App/mstore'; +import { CopyButton, Form, Input } from 'UI'; -@connect(state => ({ - apiKey: state.getIn([ 'user', 'account', 'apiKey' ]), - loading: state.getIn([ 'user', 'updateAccountRequest', 'loading' ]) || - state.getIn([ 'user', 'putClientRequest', 'loading' ]), -})) -export default class Api extends React.PureComponent { - state = { copied: false } +function ApiKeySettings() { + const { userStore } = useStore(); - copyHandler = () => { - const { apiKey } = this.props; - this.setState({ copied: true }); - copy(apiKey); - setTimeout(() => { - this.setState({ copied: false }); - }, 1000); - }; - - render() { - const { apiKey } = this.props; - const { copied } = this.state; - - return ( -
- - - - } - /> - -
- ); - } + const apiKey = userStore.account.apiKey; + return ( + + + } + /> + + ); } + +export default observer(ApiKeySettings); \ No newline at end of file diff --git a/frontend/app/components/Client/ProfileSettings/ChangePassword.tsx b/frontend/app/components/Client/ProfileSettings/ChangePassword.tsx index ce385345a..3b997a431 100644 --- a/frontend/app/components/Client/ProfileSettings/ChangePassword.tsx +++ b/frontend/app/components/Client/ProfileSettings/ChangePassword.tsx @@ -1,18 +1,20 @@ import React, { useState, useCallback } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; import { Button, Message, Form, Input } from 'UI'; import styles from './profileSettings.module.css'; -import { updatePassword } from 'Duck/user'; import { toast } from 'react-toastify'; import { validatePassword } from 'App/validate'; import { PASSWORD_POLICY } from 'App/constants'; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; const ERROR_DOESNT_MATCH = "Passwords don't match"; const MIN_LENGTH = 8; -type PropsFromRedux = ConnectedProps; - -const ChangePassword: React.FC = ({ passwordErrors, loading, updatePassword }) => { +const ChangePassword = () => { + const { userStore } = useStore(); + const updatePassword = userStore.updatePassword; + const passwordErrors = userStore.updatePasswordRequest.errors; + const loading = userStore.updatePasswordRequest.loading; const [oldPassword, setOldPassword] = useState(''); const [newPassword, setNewPassword] = useState<{ value: string; error: boolean }>({ value: '', @@ -22,7 +24,6 @@ const ChangePassword: React.FC = ({ passwordErrors, loading, upd value: '', error: false, }); - const [success, setSuccess] = useState(false); const [show, setShow] = useState(false); const checkDoesntMatch = useCallback((newPassword: string, newPasswordRepeat: string) => { @@ -55,7 +56,6 @@ const ChangePassword: React.FC = ({ passwordErrors, loading, upd newPassword: newPassword.value, }).then((e: any) => { const success = !e || !e.errors || e.errors.length === 0; - setSuccess(success); setShow(!success); if (success) { toast.success(`Successfully changed password`); @@ -133,7 +133,6 @@ const ChangePassword: React.FC = ({ passwordErrors, loading, upd setOldPassword(''); setNewPassword({ value: '', error: false }); setNewPasswordRepeat({ value: '', error: false }); - setSuccess(false); setShow(false); }} > @@ -148,15 +147,4 @@ const ChangePassword: React.FC = ({ passwordErrors, loading, upd ); }; -const mapStateToProps = (state: any) => ({ - passwordErrors: state.getIn(['user', 'passwordErrors']), - loading: state.getIn(['user', 'updatePasswordRequest', 'loading']), -}); - -const mapDispatchToProps = { - updatePassword, -}; - -const connector = connect(mapStateToProps, mapDispatchToProps); - -export default connector(ChangePassword); +export default observer(ChangePassword); diff --git a/frontend/app/components/Client/ProfileSettings/Licenses.js b/frontend/app/components/Client/ProfileSettings/Licenses.js index 748d5bb80..a071b2b79 100644 --- a/frontend/app/components/Client/ProfileSettings/Licenses.js +++ b/frontend/app/components/Client/ProfileSettings/Licenses.js @@ -1,7 +1,10 @@ import React from 'react' -import { connect } from 'react-redux' +import { observer } from 'mobx-react-lite' +import { useStore } from 'App/mstore' -function Licenses({ account }) { +function Licenses() { + const { userStore } = useStore() + const account = userStore.account return (
{account.license}
@@ -14,6 +17,4 @@ function Licenses({ account }) { ) } -export default connect(state => ({ - account: state.getIn([ 'user', 'account' ]), -}))(Licenses) +export default observer(Licenses) diff --git a/frontend/app/components/Client/ProfileSettings/OptOut.js b/frontend/app/components/Client/ProfileSettings/OptOut.js index b435a22c5..7cc271394 100644 --- a/frontend/app/components/Client/ProfileSettings/OptOut.js +++ b/frontend/app/components/Client/ProfileSettings/OptOut.js @@ -1,28 +1,29 @@ import React from 'react' -import { connect } from 'react-redux'; import { Checkbox } from 'UI' -import { updateClient } from 'Duck/user' +import { observer } from 'mobx-react-lite' +import { useStore } from "App/mstore"; + +function OptOut() { + const { userStore } = useStore(); + const optOut = userStore.account.optOut; + const updateClient = userStore.updateClient; -function OptOut(props) { - const { optOut } = props; const onChange = () => { - props.updateClient({ optOut: !optOut }) + void updateClient({ optOut: !optOut }); } + return (
) } -export default connect(state => ({ - optOut: state.getIn([ 'user', 'account', 'optOut' ]), -}), { updateClient })(OptOut); +export default observer(OptOut); diff --git a/frontend/app/components/Client/ProfileSettings/ProfileSettings.js b/frontend/app/components/Client/ProfileSettings/ProfileSettings.js index 2b3ea3e8e..1da280a01 100644 --- a/frontend/app/components/Client/ProfileSettings/ProfileSettings.js +++ b/frontend/app/components/Client/ProfileSettings/ProfileSettings.js @@ -7,106 +7,104 @@ import Api from './Api'; import TenantKey from './TenantKey'; import OptOut from './OptOut'; import Licenses from './Licenses'; -import { connect } from 'react-redux'; import { PageTitle } from 'UI'; +import { observer } from 'mobx-react-lite'; +import { useStore } from 'App/mstore'; -@withPageTitle('Account - OpenReplay Preferences') -@connect((state) => ({ - account: state.getIn(['user', 'account']), - isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee', -})) -export default class ProfileSettings extends React.PureComponent { - render() { - const { account, isEnterprise } = this.props; - return ( -
- Account
} /> -
-
-

{'Profile'}

-
{'Your email address is your identity on OpenReplay and is used to login.'}
-
-
- -
-
+function ProfileSettings() { + const { userStore } = useStore(); + const account = userStore.account; + const isEnterprise = userStore.isEnterprise; + return ( +
+ Account
} /> +
+
+

{'Profile'}

+
{'Your email address is your identity on OpenReplay and is used to login.'}
+
+
+ +
+
-
+
- {account.hasPassword && ( - <> -
-
-

{'Change Password'}

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

{'Organization API Key'}

-
{'Your API key gives you access to an extra set of services.'}
-
-
- -
-
- - {isEnterprise && (account.admin || account.superAdmin) && ( - <> -
-
-
-

{'Tenant Key'}

-
{'For SSO (SAML) authentication.'}
-
-
- -
-
- - )} - - {!isEnterprise && ( - <> -
-
-
-

{'Data Collection'}

-
- {'Enables you to control how OpenReplay captures data on your organization’s usage to improve our product.'} -
-
-
- -
-
- - )} - - {account.license && ( - <> -
- -
-
-

{'License'}

-
{'License key and expiration date.'}
-
-
- -
-
- - )} + {account.hasPassword && ( + <> +
+
+

{'Change Password'}

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

{'Organization API Key'}

+
{'Your API key gives you access to an extra set of services.'}
+
+
+ +
+
+ + {isEnterprise && (account.admin || account.superAdmin) && ( + <> +
+
+
+

{'Tenant Key'}

+
{'For SSO (SAML) authentication.'}
+
+
+ +
+
+ + )} + + {!isEnterprise && ( + <> +
+
+
+

{'Data Collection'}

+
+ {'Enables you to control how OpenReplay captures data on your organization’s usage to improve our product.'} +
+
+
+ +
+
+ + )} + + {account.license && ( + <> +
+ +
+
+

{'License'}

+
{'License key and expiration date.'}
+
+
+ +
+
+ + )} +
+ ); } + +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.js index 12272763c..e6c5a6b96 100644 --- a/frontend/app/components/Client/ProfileSettings/Settings.js +++ b/frontend/app/components/Client/ProfileSettings/Settings.js @@ -1,74 +1,66 @@ import React from 'react'; -import { connect } from 'react-redux'; import { Button, Input, Form } from 'UI'; -import { updateAccount, updateClient } from 'Duck/user'; import styles from './profileSettings.module.css'; +import { observer } from 'mobx-react-lite'; +import { useStore } from 'App/mstore'; -@connect( - (state) => ({ - accountName: state.getIn(['user', 'account', 'name']), - organizationName: state.getIn(['user', 'account', 'tenantName']), - loading: - state.getIn(['user', 'updateAccountRequest', 'loading']) || - state.getIn(['user', 'putClientRequest', 'loading']), - }), - { - updateAccount, - updateClient, +function Settings() { + const { userStore } = useStore(); + const updateClient = userStore.updateClient; + const storeAccountName = userStore.account.name; + const storeOrganizationName = userStore.account.tenantName; + const loading = userStore.loading; + const [accountName, setAccountName] = React.useState(storeAccountName); + const [organizationName, setOrganizationName] = React.useState(storeOrganizationName); + const [changed, setChanged] = React.useState(false); + + const onAccNameChange = (e) => { + setAccountName(e.target.value); + setChanged(true); } -) -export default class Settings extends React.PureComponent { - state = { - accountName: this.props.accountName, - organizationName: this.props.organizationName, - }; - onChange = ({ target: { value, name } }) => { - this.setState({ changed: true, [name]: value }); - }; + const onOrgNameChange = (e) => { + setOrganizationName(e.target.value); + setChanged(true); + } - handleSubmit = (e) => { + const handleSubmit = async (e) => { e.preventDefault(); - const { accountName, organizationName } = this.state; - this.props - .updateClient({ name: accountName, tenantName: organizationName }) - .then(() => this.setState({ changed: false })); - }; - - render() { - const { loading } = this.props; - const { accountName, organizationName, changed, copied } = this.state; - - return ( -
- - - - - - - - - - - -
- ); + await updateClient({ name: accountName, tenantName: organizationName }); + setChanged(false); } + + return ( +
+ + + + + + + + + + + +
+ ); } + +export default observer(Settings); diff --git a/frontend/app/components/Client/ProfileSettings/TenantKey.js b/frontend/app/components/Client/ProfileSettings/TenantKey.js index 62bf48ba0..070d6f72a 100644 --- a/frontend/app/components/Client/ProfileSettings/TenantKey.js +++ b/frontend/app/components/Client/ProfileSettings/TenantKey.js @@ -1,51 +1,43 @@ -// TODO this can be deleted import React from 'react'; import copy from 'copy-to-clipboard'; -import { connect } from 'react-redux'; -import styles from './profileSettings.module.css'; import { Form, Input, Button } from "UI"; +import { observer } from 'mobx-react-lite'; +import { useStore } from 'App/mstore'; -@connect(state => ({ - tenantKey: state.getIn([ 'user', 'account', 'tenantKey' ]), -})) -export default class TenantKey extends React.PureComponent { - state = { copied: false } - copyHandler = () => { - const { tenantKey } = this.props; - this.setState({ copied: true }); +function TenantKey() { + const [ copied, setCopied ] = React.useState(false); + const { userStore } = useStore(); + const tenantKey = userStore.account.tenantKey; + + const copyHandler = () => { + setCopied(true); copy(tenantKey); setTimeout(() => { - this.setState({ copied: false }); + setCopied(false); }, 1000); - }; - - render() { - const { tenantKey } = this.props; - const { copied } = this.state; - - return ( -
- - - - { copied ? 'Copied' : 'Copy' } - - } - /> - -
- ); } + return ( + + + + { copied ? 'Copied' : 'Copy' } + + } + /> + + ); } + +export default observer(TenantKey); diff --git a/frontend/app/components/Client/Roles/Roles.tsx b/frontend/app/components/Client/Roles/Roles.tsx index 470cd611b..3cc97be7c 100644 --- a/frontend/app/components/Client/Roles/Roles.tsx +++ b/frontend/app/components/Client/Roles/Roles.tsx @@ -1,133 +1,117 @@ -import React, { useEffect } from 'react'; -import cn from 'classnames'; -import { Loader, NoContent, Button, Tooltip } from 'UI'; -import { connect } from 'react-redux'; -import stl from './roles.module.css'; -import RoleForm from './components/RoleForm'; -import { init, edit, fetchList, remove as deleteRole, resetErrors } from 'Duck/roles'; -import RoleItem from './components/RoleItem'; -import { confirm } from 'UI'; -import { toast } from 'react-toastify'; import withPageTitle from 'HOCs/withPageTitle'; +import cn from 'classnames'; +import { observer } from 'mobx-react-lite'; +import React, { useEffect } from 'react'; + import { useModal } from 'App/components/Modal'; +import { useStore } from 'App/mstore'; +import { Button, Loader, NoContent, Tooltip } from 'UI'; +import { confirm } from 'UI'; -interface Props { - loading: boolean; - init: (role?: any) => void; - edit: (role: any) => void; - instance: any; - roles: any[]; - deleteRole: (id: any) => Promise; - fetchList: () => Promise; - account: any; - permissionsMap: any; - removeErrors: any; - resetErrors: () => void; - projectsMap: any; -} +import RoleForm from './components/RoleForm'; +import RoleItem from './components/RoleItem'; +import stl from './roles.module.css'; -function Roles(props: Props) { - const { loading, roles, init, edit, deleteRole, account, permissionsMap, projectsMap, removeErrors } = props; - const { showModal, hideModal } = useModal(); - const isAdmin = account.admin || account.superAdmin; +function Roles() { + const { roleStore, projectsStore, userStore } = useStore(); + const account = userStore.account; + const projectsMap = projectsStore.list.reduce((acc: any, p: any) => { + acc[p.id] = p.name; + return acc; + }, {}); + const roles = roleStore.list; + const loading = roleStore.loading; + const init = roleStore.init; + const deleteRole = roleStore.deleteRole; + const permissionsMap: any = {}; + roleStore.permissions.forEach((p: any) => { + permissionsMap[p.value] = p.text; + }); + const { showModal, hideModal } = useModal(); + const isAdmin = account.admin || account.superAdmin; - useEffect(() => { - props.fetchList(); - }, []); + useEffect(() => { + void roleStore.fetchRoles(); + }, []); - useEffect(() => { - if (removeErrors && removeErrors.size > 0) { - removeErrors.forEach((e: any) => { - toast.error(e); - }); - } - return () => { - props.resetErrors(); - }; - }, [removeErrors]); - - const editHandler = (role: any) => { - init(role); - showModal(, { right: true }); - }; - - const deleteHandler = async (role: any) => { - if ( - await confirm({ - header: 'Roles', - confirmation: `Are you sure you want to remove this role?`, - }) - ) { - deleteRole(role.roleId).then(hideModal); - } - }; - - return ( - - -
-
-
-

Roles and Access

- - - -
-
- - -
-
-
- Title -
-
- Project Access -
-
- Feature Access -
-
-
- {roles.map((role) => ( - - ))} -
-
-
-
-
+ const editHandler = (role: any) => { + init(role); + showModal( + , + { right: true } ); + }; + + const deleteHandler = async (role: any) => { + if ( + await confirm({ + header: 'Roles', + confirmation: `Are you sure you want to remove this role?`, + }) + ) { + deleteRole(role.roleId).then(hideModal); + } + }; + + return ( + + +
+
+
+

Roles and Access

+ + + +
+
+ + +
+
+
+ Title +
+
+ Project Access +
+
+ Feature Access +
+
+
+ {roles.map((role) => ( + + ))} +
+
+
+
+
+ ); } -export default connect( - (state: any) => { - const permissions = state.getIn(['roles', 'permissions']); - const permissionsMap: any = {}; - permissions.forEach((p: any) => { - permissionsMap[p.value] = p.text; - }); - const projects = state.getIn(['site', 'list']); - return { - instance: state.getIn(['roles', 'instance']) || null, - permissionsMap: permissionsMap, - roles: state.getIn(['roles', 'list']), - removeErrors: state.getIn(['roles', 'removeRequest', 'errors']), - loading: state.getIn(['roles', 'fetchRequest', 'loading']), - account: state.getIn(['user', 'account']), - projectsMap: projects.reduce((acc: any, p: any) => { - acc[p.get('id')] = p.get('name'); - return acc; - }, {}), - }; - }, - { init, edit, fetchList, deleteRole, resetErrors } -)(withPageTitle('Roles & Access - OpenReplay Preferences')(Roles)); +export default withPageTitle('Roles & Access - OpenReplay Preferences')( + observer(Roles) +); diff --git a/frontend/app/components/Client/Roles/components/Permissions/Permissions.tsx b/frontend/app/components/Client/Roles/components/Permissions/Permissions.tsx deleted file mode 100644 index 0dd56dfd9..000000000 --- a/frontend/app/components/Client/Roles/components/Permissions/Permissions.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import Role from 'Types/role' - -interface Props { - role: Role -} -function Permissions(props: Props) { - return ( -
- -
- ); -} - -export default Permissions; \ No newline at end of file diff --git a/frontend/app/components/Client/Roles/components/Permissions/index.ts b/frontend/app/components/Client/Roles/components/Permissions/index.ts deleted file mode 100644 index 659544a53..000000000 --- a/frontend/app/components/Client/Roles/components/Permissions/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './Permissions'; \ No newline at end of file diff --git a/frontend/app/components/Client/Roles/components/RoleForm/RoleForm.tsx b/frontend/app/components/Client/Roles/components/RoleForm/RoleForm.tsx index b6b9efe92..b674a8b3d 100644 --- a/frontend/app/components/Client/Roles/components/RoleForm/RoleForm.tsx +++ b/frontend/app/components/Client/Roles/components/RoleForm/RoleForm.tsx @@ -1,196 +1,222 @@ -import React, { useRef, useEffect } from 'react'; -import { connect } from 'react-redux'; -import stl from './roleForm.module.css'; -import { save, edit } from 'Duck/roles'; -import { Form, Input, Button, Checkbox, Icon } from 'UI'; +import { observer } from 'mobx-react-lite'; +import React, { useEffect, useRef } from 'react'; + +import { useStore } from 'App/mstore'; +import { Button, Checkbox, Form, Icon, Input } from 'UI'; + import Select from 'Shared/Select'; -interface Permission { - name: string; - value: string; -} +import stl from './roleForm.module.css'; interface Props { - role: any; - edit: (role: any) => void; - save: (role: any) => Promise; - closeModal: (toastMessage?: string) => void; - saving: boolean; - permissions: Array[]; - projectOptions: Array[]; - permissionsMap: any; - projectsMap: any; - deleteHandler: (id: any) => Promise; + closeModal: (toastMessage?: string) => void; + permissionsMap: any; + deleteHandler: (id: any) => Promise; } const RoleForm = (props: Props) => { - const { role, edit, save, closeModal, saving, permissions, projectOptions, permissionsMap, projectsMap } = props; - let focusElement = useRef(null); - const _save = () => { - save(role).then(() => { - closeModal(role.exists() ? 'Role updated' : 'Role created'); - }); - }; + const { roleStore, projectsStore } = useStore(); + const projects = projectsStore.list; + const role = roleStore.instance; + const saving = roleStore.loading; + const { closeModal, permissionsMap } = props; + const projectOptions = projects + .filter(({ value }) => !role.projects.includes(value)) + .map((p: any) => ({ + key: p.id, + value: p.id, + label: p.name, + })) + .filter(({ value }: any) => !role.projects.includes(value)); + const projectsMap = projects.reduce((acc: any, p: any) => { + acc[p.id] = p.name; + return acc; + }, {}); - const write = ({ target: { value, name } }: any) => edit({ [name]: value }); + let focusElement = useRef(null); + const permissions: {}[] = roleStore.permissions + .filter(({ value }) => !role.permissions.includes(value)) + .map((p) => ({ + label: p.text, + value: p.value, + })); + const _save = () => { + roleStore.saveRole(role).then(() => { + closeModal(role.exists() ? 'Role updated' : 'Role created'); + }); + }; - const onChangePermissions = (e: any) => { - const { permissions } = role; - const index = permissions.indexOf(e); - const _perms = permissions.contains(e) ? permissions.remove(index) : permissions.push(e); - edit({ permissions: _perms }); - }; + const write = ({ target: { value, name } }: any) => + roleStore.editRole({ [name]: value }); - const onChangeProjects = (e: any) => { - const { projects } = role; - const index = projects.indexOf(e); - const _projects = index === -1 ? projects.push(e) : projects.remove(index); - edit({ projects: _projects }); - }; + const onChangePermissions = (e: any) => { + const { permissions } = role; + const index = permissions.indexOf(e); + let _perms; + if (permissions.includes(e)) { + permissions.splice(index, 1); + _perms = permissions; + } else { + _perms = permissions.concat(e); + } + roleStore.editRole({ permissions: _perms }); + }; - const writeOption = ({ name, value }: any) => { - if (name === 'permissions') { - onChangePermissions(value); - } else if (name === 'projects') { - onChangeProjects(value); - } - }; + const onChangeProjects = (e: any) => { + const { projects } = role; + const index = projects.indexOf(e); + let _projects; + if (index === -1) { + _projects = projects.concat(e); + } else { + projects.splice(index, 1); + _projects = projects; + } + roleStore.editRole({ projects: _projects }); + }; - const toggleAllProjects = () => { - const { allProjects } = role; - edit({ allProjects: !allProjects }); - }; + const writeOption = ({ name, value }: any) => { + if (name === 'permissions') { + onChangePermissions(value); + } else if (name === 'projects') { + onChangeProjects(value); + } + }; - useEffect(() => { - focusElement && focusElement.current && focusElement.current.focus(); - }, []); + const toggleAllProjects = () => { + const { allProjects } = role; + roleStore.editRole({ allProjects: !allProjects }); + }; - return ( -
-

{role.exists() ? 'Edit Role' : 'Create Role'}

-
-
- - - - + useEffect(() => { + focusElement && focusElement.current && focusElement.current.focus(); + }, []); - - + return ( +
+

+ {role.exists() ? 'Edit Role' : 'Create Role'} +

+
+ + + + + -
- -
-
All Projects
- (Uncheck to select specific projects) -
-
- {!role.allProjects && ( - <> - writeOption({ name: 'permissions', value: value.value })} - value={null} - /> - {role.permissions.size > 0 && ( -
- {role.permissions.map((p: any) => OptionLabel(permissionsMap, p, onChangePermissions))} -
- )} - - - -
-
- - {role.exists() && } -
- {role.exists() && ( - - )} -
+
+ +
+
All Projects
+ + (Uncheck to select specific projects) + +
+ {!role.allProjects && ( + <> + + writeOption({ name: 'permissions', value: value.value }) + } + value={null} + /> + {role.permissions.length > 0 && ( +
+ {role.permissions.map((p: any) => + OptionLabel(permissionsMap, p, onChangePermissions) + )} +
+ )} + + + +
+
+ + {role.exists() && } +
+ {role.exists() && ( + + )}
- ); +
+
+ ); }; -export default connect( - (state: any) => { - const role = state.getIn(['roles', 'instance']); - const projects = state.getIn(['site', 'list']); - return { - role, - projectOptions: projects - .map((p: any) => ({ - key: p.get('id'), - value: p.get('id'), - label: p.get('name'), - // isDisabled: role.projects.includes(p.get('id')), - })) - .filter(({ value }: any) => !role.projects.includes(value)) - .toJS(), - permissions: state - .getIn(['roles', 'permissions']) - .filter(({ value }: any) => !role.permissions.includes(value)) - .map(({ text, value }: any) => ({ label: text, value })) - .toJS(), - saving: state.getIn(['roles', 'saveRequest', 'loading']), - projectsMap: projects.reduce((acc: any, p: any) => { - acc[p.get('id')] = p.get('name'); - return acc; - }, {}), - }; - }, - { edit, save } -)(RoleForm); +export default observer(RoleForm); function OptionLabel(nameMap: any, p: any, onChangeOption: (e: any) => void) { - return ( -
-
{nameMap[p]}
-
onChangeOption(p)}> - -
-
- ); + return ( +
+
{nameMap[p]}
+
onChangeOption(p)}> + +
+
+ ); } diff --git a/frontend/app/components/Client/SessionsListingSettings.tsx b/frontend/app/components/Client/SessionsListingSettings.tsx index 79a78061a..8dee24bf5 100644 --- a/frontend/app/components/Client/SessionsListingSettings.tsx +++ b/frontend/app/components/Client/SessionsListingSettings.tsx @@ -1,41 +1,29 @@ +import withPageTitle from 'HOCs/withPageTitle'; import React from 'react'; -import { connect } from 'react-redux'; -import { PageTitle, Divider } from 'UI'; -import ListingVisibility from 'Shared/SessionSettings/components/ListingVisibility'; +import { Divider, PageTitle } from 'UI'; + import DefaultPlaying from 'Shared/SessionSettings/components/DefaultPlaying'; import DefaultTimezone from 'Shared/SessionSettings/components/DefaultTimezone'; -import withPageTitle from 'HOCs/withPageTitle'; +import ListingVisibility from 'Shared/SessionSettings/components/ListingVisibility'; import MouseTrailSettings from 'Shared/SessionSettings/components/MouseTrailSettings'; - -type Props = {} - -const mapStateToProps = (state: any) => ({ - isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee', - account: state.getIn(['user', 'account']) -}); - -const connector = connect(mapStateToProps); - -function SessionsListingSettings(props: Props) { +function SessionsListingSettings() { return ( -
+
Sessions Listing
} /> -
-
+
+
-
-
@@ -44,12 +32,11 @@ function SessionsListingSettings(props: Props) {
-
); } -export default connector( - withPageTitle('Sessions Listings - OpenReplay Preferences')(SessionsListingSettings) -); \ No newline at end of file +export default withPageTitle('Sessions Listings - OpenReplay Preferences')( + SessionsListingSettings +); diff --git a/frontend/app/components/Client/Sites/AddProjectButton/AddProjectButton.tsx b/frontend/app/components/Client/Sites/AddProjectButton/AddProjectButton.tsx index 0a2712462..ea8ca522a 100644 --- a/frontend/app/components/Client/Sites/AddProjectButton/AddProjectButton.tsx +++ b/frontend/app/components/Client/Sites/AddProjectButton/AddProjectButton.tsx @@ -1,25 +1,22 @@ import React from 'react'; import { Tooltip, Button } from 'UI'; import { useStore } from 'App/mstore'; -import { useObserver } from 'mobx-react-lite'; -import { init, remove, fetchGDPR } from 'Duck/site'; -import { connect } from 'react-redux'; +import { observer } from 'mobx-react-lite'; import { useModal } from 'App/components/Modal'; import NewSiteForm from '../NewSiteForm'; const PERMISSION_WARNING = 'You don’t have the permissions to perform this action.'; const LIMIT_WARNING = 'You have reached site limit.'; -function AddProjectButton({ isAdmin = false, init = () => {} }: any) { - const { userStore } = useStore(); +function AddProjectButton({ isAdmin = false }: any) { + const { userStore, projectsStore } = useStore(); + const init = projectsStore.initProject; const { showModal, hideModal } = useModal(); - const limtis = useObserver(() => userStore.limits); - const canAddProject = useObserver( - () => isAdmin && (limtis.projects === -1 || limtis.projects > 0) - ); + const limits = userStore.limits; + const canAddProject = isAdmin && (limits.projects === -1 || limits.projects > 0) const onClick = () => { - init(); + init({}); showModal(, { right: true }); }; return ( @@ -34,4 +31,4 @@ function AddProjectButton({ isAdmin = false, init = () => {} }: any) { ); } -export default connect(null, { init, remove, fetchGDPR })(AddProjectButton); +export default observer(AddProjectButton); diff --git a/frontend/app/components/Client/Sites/GDPRForm.js b/frontend/app/components/Client/Sites/GDPRForm.js index c65559cd3..fa54f73be 100644 --- a/frontend/app/components/Client/Sites/GDPRForm.js +++ b/frontend/app/components/Client/Sites/GDPRForm.js @@ -1,7 +1,7 @@ import React from 'react'; -import { connect } from 'react-redux'; +import { observer } from 'mobx-react-lite'; +import { useStore } from "App/mstore"; import { Form, Button, Input, Icon } from 'UI'; -import { editGDPR, saveGDPR } from 'Duck/site'; import { validateNumber } from 'App/validate'; import styles from './siteForm.module.css'; import Select from 'Shared/Select'; @@ -12,124 +12,118 @@ const inputModeOptions = [ { label: 'Obscure all inputs', value: 'hidden' }, ]; -@connect(state => ({ - site: state.getIn([ 'site', 'instance' ]), - gdpr: state.getIn([ 'site', 'instance', 'gdpr' ]), - saving: state.getIn([ 'site', 'saveGDPR', 'loading' ]), -}), { - editGDPR, - saveGDPR, -}) -export default class GDPRForm extends React.PureComponent { - onChange = ({ target: { name, value } }) => { +function GDPRForm(props) { + const { projectsStore } = useStore(); + const site = projectsStore.instance; + const gdpr = site.gdpr; + const saving = false //projectsStore.; + const editGDPR = projectsStore.editGDPR; + const saveGDPR = projectsStore.saveGDPR; + + + const onChange = ({ target: { name, value } }) => { if (name === "sampleRate") { if (!validateNumber(value, { min: 0, max: 100 })) return; if (value.length > 1 && value[0] === "0") { value = value.slice(1); } } - this.props.editGDPR({ [ name ]: value }); + editGDPR({ [ name ]: value }); } - onSampleRateBlur = ({ target: { name, value } }) => { //TODO: editState hoc + const onSampleRateBlur = ({ target: { name, value } }) => { //TODO: editState hoc if (value === ''){ - this.props.editGDPR({ sampleRate: 100 }); + editGDPR({ sampleRate: 100 }); } } - onChangeSelect = ({ name, value }) => { - this.props.editGDPR({ [ name ]: value }); + const onChangeSelect = ({ name, value }) => { + props.editGDPR({ [ name ]: value }); }; - onChangeOption = ({ target: { checked, name } }) => { - this.props.editGDPR({ [ name ]: checked }); + const onChangeOption = ({ target: { checked, name } }) => { + editGDPR({ [ name ]: checked }); } - onSubmit = (e) => { + const onSubmit = (e) => { e.preventDefault(); - const { site, gdpr } = this.props; - this.props.saveGDPR(site.id, gdpr); + void saveGDPR(site.id); } + + return ( +
+
+ + +
{ site.host }
+
+ + + + - render() { - const { - site, onClose, saving, gdpr, - } = this.props; + + + + + { 'Do not record any numeric text' } +
{ 'If enabled, OpenReplay will not record or store any numeric text for all sessions.' }
+ + - - - - + { 'Do not record email addresses ' } +
{ 'If enabled, OpenReplay will not record or store any email address for all sessions.' }
+ + - - - - - - - - -
-
- { 'Block IP' } -
+
+
+ { 'Block IP' }
+
-
-
- - ); - } +
+
+ + ) } + +export default observer(GDPRForm); \ No newline at end of file diff --git a/frontend/app/components/Client/Sites/NewSiteForm.tsx b/frontend/app/components/Client/Sites/NewSiteForm.tsx index 66a31948e..7efa83678 100644 --- a/frontend/app/components/Client/Sites/NewSiteForm.tsx +++ b/frontend/app/components/Client/Sites/NewSiteForm.tsx @@ -1,61 +1,48 @@ import { Segmented } from 'antd'; import React, { ChangeEvent, FormEvent, useEffect, useState } from 'react'; -import { ConnectedProps, connect } from 'react-redux'; import { RouteComponentProps, withRouter } from 'react-router-dom'; import { toast } from 'react-toastify'; - -import { withStore } from 'App/mstore'; -import { clearSearch as clearSearchLive } from 'Duck/liveSearch'; -import { clearSearch } from 'Duck/search'; -import { edit, fetchList, remove, save, update } from 'Duck/site'; -import { setSiteId } from 'Duck/site'; -import { pushNewSite } from 'Duck/user'; -import { Button, Form, Icon, Input, SegmentSelection } from 'UI'; +import { useStore } from 'App/mstore'; +import { Button, Form, Icon, Input } from 'UI'; import { confirm } from 'UI'; +import { observer } from 'mobx-react-lite'; import styles from './siteForm.module.css'; type OwnProps = { onClose: (arg: any) => void; - mstore: any; - canDelete: boolean; }; -type PropsFromRedux = ConnectedProps; - -type Props = PropsFromRedux & RouteComponentProps & OwnProps; +type Props = RouteComponentProps & OwnProps; const NewSiteForm = ({ - site, - loading, - save, - remove, - edit, - update, - pushNewSite, - fetchList, - setSiteId, - clearSearch, - clearSearchLive, - location: { pathname }, - onClose, - mstore, - activeSiteId, - canDelete, -}: Props) => { + location: { pathname }, + onClose + }: Props) => { + const mstore = useStore(); + const { projectsStore } = mstore; + const activeSiteId = projectsStore.active?.id; + const site = projectsStore.instance; + const siteList = projectsStore.list; + const loading = projectsStore.loading; + const canDelete = siteList.length > 1; + const setSiteId = projectsStore.setSiteId; + const saveProject = projectsStore.save; + const fetchList = projectsStore.fetchList; const [existsError, setExistsError] = useState(false); + const { searchStore } = useStore(); useEffect(() => { - if (pathname.includes('onboarding')) { + if (pathname.includes('onboarding') && site?.id) { setSiteId(site.id); } + if (!site) projectsStore.initProject({}); }, []); const onSubmit = (e: FormEvent) => { e.preventDefault(); - - if (site.exists()) { - update(site, site.id).then((response: any) => { + if (site?.id && site.exists()) { + projectsStore.updateProject(site.id, site.toData()).then((response: any) => { if (!response || !response.errors || response.errors.size === 0) { onClose(null); if (!pathname.includes('onboarding')) { @@ -67,11 +54,11 @@ const NewSiteForm = ({ } }); } else { - save(site).then((response: any) => { + saveProject(site!).then((response: any) => { if (!response || !response.errors || response.errors.size === 0) { onClose(null); - clearSearch(); - clearSearchLive(); + searchStore.clearSearch(); + mstore.searchStoreLive.clearSearch(); mstore.initClient(); toast.success('Project added successfully'); } else { @@ -87,10 +74,11 @@ const NewSiteForm = ({ header: 'Project Deletion Alert', confirmation: `Are you sure you want to delete this project? Deleting it will permanently remove the project, along with all associated sessions and data.`, confirmButton: 'Yes, delete', - cancelButton: 'Cancel', + cancelButton: 'Cancel' }) + && site?.id ) { - remove(site.id).then(() => { + projectsStore.removeProject(site.id).then(() => { onClose(null); if (site.id === activeSiteId) { setSiteId(null); @@ -100,12 +88,15 @@ const NewSiteForm = ({ }; const handleEdit = ({ - target: { name, value }, - }: ChangeEvent) => { + target: { name, value } + }: ChangeEvent) => { setExistsError(false); - edit({ [name]: value }); + projectsStore.editInstance({ [name]: value }); }; + if (!site) { + return null; + } return (
@@ -137,16 +128,16 @@ const NewSiteForm = ({ options={[ { value: 'web', - label: 'Web', + label: 'Web' }, { value: 'ios', - label: 'Mobile', - }, + label: 'Mobile' + } ]} value={site.platform} onChange={(value) => { - edit({ platform: value }); + projectsStore.editInstance({ platform: value }); }} />
@@ -157,9 +148,9 @@ const NewSiteForm = ({ type="submit" className="float-left mr-2" loading={loading} - disabled={!site.validate()} + disabled={!site.validate} > - {site.exists() ? 'Update' : 'Add'} + {site?.exists() ? 'Update' : 'Add'} {site.exists() && (
} size="small" - show={!loading && filteredSites.size === 0} + show={!loading && filteredSites.length === 0} >
Project Name
@@ -160,7 +163,7 @@ const Sites = ({ loading, sites, user, init }: PropsFromRedux) => {
updatePage(page)} limit={pageSize} /> @@ -180,19 +183,4 @@ const Sites = ({ loading, sites, user, init }: PropsFromRedux) => { ); }; -const mapStateToProps = (state: any) => ({ - site: state.getIn(['site', 'instance']), - sites: state.getIn(['site', 'list']), - loading: state.getIn(['site', 'loading']), - user: state.getIn(['user', 'account']), - account: state.getIn(['user', 'account']), -}); - -const connector = connect(mapStateToProps, { - init, - remove, - fetchGDPR, - setSiteId, -}); - -export default connector(withPageTitle('Projects - OpenReplay Preferences')(Sites)); +export default withPageTitle('Projects - OpenReplay Preferences')(observer(Sites)); diff --git a/frontend/app/components/Client/Users/UsersView.tsx b/frontend/app/components/Client/Users/UsersView.tsx index 6478c7b60..faaa78ab4 100644 --- a/frontend/app/components/Client/Users/UsersView.tsx +++ b/frontend/app/components/Client/Users/UsersView.tsx @@ -6,18 +6,17 @@ import { useObserver } from 'mobx-react-lite'; import UserSearch from './components/UserSearch'; import { useModal } from 'App/components/Modal'; import UserForm from './components/UserForm'; -import { connect } from 'react-redux'; +import { observer } from 'mobx-react-lite'; import AddUserButton from './components/AddUserButton'; import withPageTitle from 'HOCs/withPageTitle'; interface Props { isOnboarding?: boolean; - account: any; - isEnterprise: boolean; } -function UsersView(props: Props) { - const { account, isEnterprise, isOnboarding = false } = props; +function UsersView({ isOnboarding = false }: Props) { const { userStore, roleStore } = useStore(); + const account = userStore.account; + const isEnterprise = userStore.isEnterprise; const userCount = useObserver(() => userStore.list.length); const roles = useObserver(() => roleStore.list); const { showModal } = useModal(); @@ -31,7 +30,7 @@ function UsersView(props: Props) { useEffect(() => { if (roles.length === 0 && isEnterprise) { - roleStore.fetchRoles(); + void roleStore.fetchRoles(); } }, []); @@ -60,7 +59,4 @@ function UsersView(props: Props) { ); } -export default connect((state: any) => ({ - account: state.getIn(['user', 'account']), - isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee', -}))(withPageTitle('Team - OpenReplay Preferences')(UsersView)); +export default withPageTitle('Team - OpenReplay Preferences')(observer(UsersView)); diff --git a/frontend/app/components/Client/Users/components/UserForm/UserForm.tsx b/frontend/app/components/Client/Users/components/UserForm/UserForm.tsx index c0e050a7c..9b5b9bd03 100644 --- a/frontend/app/components/Client/Users/components/UserForm/UserForm.tsx +++ b/frontend/app/components/Client/Users/components/UserForm/UserForm.tsx @@ -1,162 +1,171 @@ -import React from 'react'; -import { Form, Input, CopyButton, Button, Icon } from 'UI' import cn from 'classnames'; -import { useStore } from 'App/mstore'; import { useObserver } from 'mobx-react-lite'; +import { observer } from 'mobx-react-lite'; +import React from 'react'; + import { useModal } from 'App/components/Modal'; -import Select from 'Shared/Select'; +import { useStore } from 'App/mstore'; +import { Button, CopyButton, Form, Icon, Input } from 'UI'; import { confirm } from 'UI'; -import { connect } from 'react-redux'; -interface Props { - isSmtp?: boolean; - isEnterprise?: boolean; -} -function UserForm(props: Props) { - const { isSmtp = false, isEnterprise = false } = props; - const { hideModal } = useModal(); - const { userStore, roleStore } = useStore(); - const isSaving = useObserver(() => userStore.saving); - const user: any = useObserver(() => userStore.instance || userStore.initUser()); - const roles = useObserver(() => roleStore.list.filter(r => r.isProtected ? user.isSuperAdmin : true).map(r => ({ label: r.name, value: r.roleId }))); +import Select from 'Shared/Select'; - const onChangeCheckbox = (e: any) => { - user.updateKey('isAdmin', !user.isAdmin); +function UserForm() { + const { hideModal } = useModal(); + const { userStore, roleStore } = useStore(); + const isEnterprise = userStore.isEnterprise; + const isSmtp = userStore.account.smtp; + const isSaving = userStore.saving; + const user: any = userStore.instance || userStore.initUser(); + const roles = roleStore.list + .filter((r) => (r.isProtected ? user.isSuperAdmin : true)) + .map((r) => ({ label: r.name, value: r.roleId })); + + const onChangeCheckbox = (e: any) => { + user.updateKey('isAdmin', !user.isAdmin); + }; + + const onSave = () => { + userStore.saveUser(user).then(() => { + hideModal(); + userStore.fetchLimits(); + }); + }; + + const write = ({ target: { name, value } }) => { + user.updateKey(name, value); + }; + + const deleteHandler = async () => { + if ( + await confirm({ + header: 'Confirm', + confirmButton: 'Yes, delete', + confirmation: `Are you sure you want to permanently delete this user?`, + }) + ) { + userStore.deleteUser(user.userId).then(() => { + hideModal(); + userStore.fetchLimits(); + }); } + }; - const onSave = () => { - userStore.saveUser(user).then(() => { - hideModal(); - userStore.fetchLimits(); - }); - } + return useObserver(() => ( +
+
+

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

+
+ + + + + - const write = ({ target: { name, value } }) => { - user.updateKey(name, value); - } - - const deleteHandler = async () => { - if (await confirm({ - header: 'Confirm', - confirmButton: 'Yes, delete', - confirmation: `Are you sure you want to permanently delete this user?` - })) { - userStore.deleteUser(user.userId).then(() => { - hideModal(); - userStore.fetchLimits(); - }); - } - } - - return useObserver(() => ( -
-
-

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

-
- - - - - - -
- - -
- { !isSmtp && -
- 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. -
- } - - - - - { isEnterprise && ( - - -
- )); + {!isSmtp && ( +
+ 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. +
+ )} + + + + + {isEnterprise && ( + + +
)}
-
+
+
-
-
+
+
No relevant sessions found for the selected time period
@@ -159,22 +159,22 @@ function WidgetSessions(props: Props) { {filteredSessions.sessions.map((session: any) => ( -
+
))} -
+
Showing{' '} - + {(metricStore.sessionsPage - 1) * metricStore.sessionsPageSize + 1} {' '} to{' '} - + {(metricStore.sessionsPage - 1) * metricStore.sessionsPageSize + filteredSessions.sessions.length} {' '} - of {numberWithCommas(filteredSessions.total)}{' '} + of {numberWithCommas(filteredSessions.total)}{' '} sessions.
{ return arr; }; -const mapStateToProps = (state: any) => ({ - metaList: state.getIn(['customFields', 'list']).map((i: any) => i.key), -}); - -export default connect(mapStateToProps)(observer(WidgetSessions)); +export default observer(WidgetSessions); diff --git a/frontend/app/components/Errors/Error/ErrorInfo.js b/frontend/app/components/Errors/Error/ErrorInfo.js index ff15d4589..5c70faff6 100644 --- a/frontend/app/components/Errors/Error/ErrorInfo.js +++ b/frontend/app/components/Errors/Error/ErrorInfo.js @@ -1,89 +1,48 @@ +import { observer } from 'mobx-react-lite'; import React from 'react'; -import { connect } from 'react-redux'; -import withSiteIdRouter from 'HOCs/withSiteIdRouter'; -import { error as errorRoute } from 'App/routes'; -import { NoContent, Loader } from 'UI'; -import { fetch, fetchTrace } from 'Duck/errors'; -import MainSection from './MainSection'; -import SideSection from './SideSection'; + +import { useStore } from 'App/mstore'; +import { Loader, NoContent } from 'UI'; + import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; -@connect( - (state) => ({ - errorIdInStore: state.getIn(['errors', 'instance']).errorId, - list: state.getIn(['errors', 'instanceTrace']), - loading: - state.getIn(['errors', 'fetch', 'loading']) || - state.getIn(['errors', 'fetchTrace', 'loading']), - errorOnFetch: - state.getIn(['errors', 'fetch', 'errors']) || state.getIn(['errors', 'fetchTrace', 'errors']), - }), - { - fetch, - fetchTrace, - } -) -@withSiteIdRouter -export default class ErrorInfo extends React.PureComponent { - ensureInstance() { - const { errorId, loading, errorOnFetch } = this.props; - if (!loading && this.props.errorIdInStore !== errorId && errorId != null) { - this.props.fetch(errorId); - this.props.fetchTrace(errorId); - } - } - componentDidMount() { - this.ensureInstance(); - } - componentDidUpdate(prevProps) { - if (prevProps.errorId !== this.props.errorId || prevProps.errorIdInStore !== this.props.errorIdInStore) { - this.ensureInstance(); - } - } - next = () => { - const { list, errorId } = this.props; - const curIndex = list.findIndex((e) => e.errorId === errorId); - const next = list.get(curIndex + 1); - if (next != null) { - this.props.history.push(errorRoute(next.errorId)); - } - }; - prev = () => { - const { list, errorId } = this.props; - const curIndex = list.findIndex((e) => e.errorId === errorId); - const prev = list.get(curIndex - 1); - if (prev != null) { - this.props.history.push(errorRoute(prev.errorId)); - } - }; - render() { - const { loading, errorIdInStore, list, errorId } = this.props; +import MainSection from './MainSection'; +import SideSection from './SideSection'; - let nextDisabled = true, - prevDisabled = true; - if (list.size > 0) { - nextDisabled = loading || list.last().errorId === errorId; - prevDisabled = loading || list.first().errorId === errorId; - } +function ErrorInfo(props) { + const { errorStore } = useStore(); + const instance = errorStore.instance; + const ensureInstance = () => { + if (errorStore.isLoading) return; + errorStore.fetchError(props.errorId); + errorStore.fetchErrorTrace(props.errorId); + }; - return ( - - -
No Error Found!
-
- } - subtext="Please try to find existing one." - show={!loading && errorIdInStore == null} - > -
- - - - + React.useEffect(() => { + ensureInstance(); + }, [props.errorId]); + + const errorIdInStore = errorStore.instance?.errorId; + const loading = errorStore.isLoading; + return ( + + +
No Error Found!
- - ); - } + } + subtext="Please try to find existing one." + show={!loading && errorIdInStore == null} + > +
+ + + + +
+ + ); } + +export default observer(ErrorInfo); diff --git a/frontend/app/components/Errors/Error/MainSection.js b/frontend/app/components/Errors/Error/MainSection.js index a74755408..730e11088 100644 --- a/frontend/app/components/Errors/Error/MainSection.js +++ b/frontend/app/components/Errors/Error/MainSection.js @@ -1,154 +1,132 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import cn from 'classnames'; -import withSiteIdRouter from 'HOCs/withSiteIdRouter'; -import { ErrorDetails, Icon, Loader, Button } from 'UI'; -import { sessions as sessionsRoute } from 'App/routes'; import { RESOLVED } from 'Types/errorInfo'; -import { addFilterByKeyAndValue } from 'Duck/search'; -import { resolve, unresolve, ignore, toggleFavorite } from 'Duck/errors'; +import { FilterKey } from 'Types/filter/filterType'; +import cn from 'classnames'; +import { observer } from 'mobx-react-lite'; +import React from 'react'; +import { withRouter } from 'react-router-dom'; + import { resentOrDate } from 'App/date'; +import { useStore } from 'App/mstore'; +import { sessions as sessionsRoute } from 'App/routes'; import Divider from 'Components/Errors/ui/Divider'; import ErrorName from 'Components/Errors/ui/ErrorName'; import Label from 'Components/Errors/ui/Label'; -import { FilterKey } from 'Types/filter/filterType'; +import { Button, ErrorDetails, Icon, Loader } from 'UI'; import SessionBar from './SessionBar'; -@withSiteIdRouter -@connect( - (state) => ({ - error: state.getIn(['errors', 'instance']), - trace: state.getIn(['errors', 'instanceTrace']), - sourcemapUploaded: state.getIn(['errors', 'sourcemapUploaded']), - resolveToggleLoading: - state.getIn(['errors', 'resolve', 'loading']) || - state.getIn(['errors', 'unresolve', 'loading']), - ignoreLoading: state.getIn(['errors', 'ignore', 'loading']), - toggleFavoriteLoading: state.getIn(['errors', 'toggleFavorite', 'loading']), - traceLoading: state.getIn(['errors', 'fetchTrace', 'loading']), - }), - { - resolve, - unresolve, - ignore, - toggleFavorite, - addFilterByKeyAndValue, - } -) -export default class MainSection extends React.PureComponent { - resolve = () => { - const { error } = this.props; - this.props.resolve(error.errorId); - }; +function MainSection(props) { + const { errorStore, searchStore } = useStore(); + const error = errorStore.instance; + const trace = errorStore.instanceTrace; + const sourcemapUploaded = errorStore.sourcemapUploaded; + const loading = errorStore.isLoading; + const className = props.className; - unresolve = () => { - const { error } = this.props; - this.props.unresolve(error.errorId); + const findSessions = () => { + searchStore.addFilterByKeyAndValue(FilterKey.ERROR, error.message); + props.history.push(sessionsRoute()); }; - - ignore = () => { - const { error } = this.props; - this.props.ignore(error.errorId); - }; - bookmark = () => { - const { error } = this.props; - this.props.toggleFavorite(error.errorId); - }; - - findSessions = () => { - this.props.addFilterByKeyAndValue(FilterKey.ERROR, this.props.error.message); - this.props.history.push(sessionsRoute()); - }; - - render() { - const { - error, - trace, - sourcemapUploaded, - ignoreLoading, - resolveToggleLoading, - toggleFavoriteLoading, - className, - traceLoading, - } = this.props; - const isPlayer = window.location.pathname.includes('/session/'); - - return ( -
-
- -
-
- {error.message} + return ( +
+
+ +
+
+ {error.message} +
+
+
+
-
-
-
-
Over the past 30 days
+
+ Over the past 30 days
- - -
-
-

Last session with this error

- {resentOrDate(error.lastOccurrence)} - -
- - {error.customTags.length > 0 ? ( -
-
- More Info (most recent call) -
-
- {error.customTags.map((tag) => ( -
-
{Object.entries(tag)[0][0]}
{Object.entries(tag)[0][1]}
-
- ))} -
-
- ) : null} -
- -
- - - -
- ); - } + + +
+
+

+ Last session with this error +

+ + {resentOrDate(error.lastOccurrence)} + + +
+ + {error.customTags.length > 0 ? ( +
+
+ More Info{' '} + (most recent call) +
+
+ {error.customTags.map((tag) => ( +
+
+ {Object.entries(tag)[0][0]} +
+ {' '} +
+ {Object.entries(tag)[0][1]} +
+
+ ))} +
+
+ ) : null} +
+ +
+ + + +
+
+ ); } + +export default withRouter( + (observer(MainSection)) +); diff --git a/frontend/app/components/Errors/Error/SideSection.js b/frontend/app/components/Errors/Error/SideSection.js index 017387f26..8be6d61d8 100644 --- a/frontend/app/components/Errors/Error/SideSection.js +++ b/frontend/app/components/Errors/Error/SideSection.js @@ -1,123 +1,120 @@ -import React from 'react'; -import { connect } from 'react-redux'; import cn from 'classnames'; -import withRequest from 'HOCs/withRequest'; -import { Loader } from 'UI'; +import { observer } from 'mobx-react-lite'; +import React from 'react'; + import { countries } from 'App/constants'; -import Trend from './Trend'; +import { useStore } from 'App/mstore'; +import { Loader } from 'UI'; + import DateAgo from './DateAgo'; import DistributionBar from './DistributionBar'; +import Trend from './Trend'; +import { errorService } from 'App/services'; const MAX_PERCENTAGE = 3; const MIN_COUNT = 4; const MAX_COUNT = 10; function hidePredicate(percentage, index) { - if (index < MIN_COUNT) return false; - if (index < MAX_COUNT && percentage < MAX_PERCENTAGE) return false; - return true; + if (index < MIN_COUNT) return false; + if (index < MAX_COUNT && percentage < MAX_PERCENTAGE) return false; + return true; } function partitionsWrapper(partitions = [], mapCountry = false) { const counts = partitions.map(({ count }) => count); - const sum = counts.reduce((a,b)=>parseInt(a)+parseInt(b),0); + const sum = counts.reduce((a, b) => parseInt(a) + parseInt(b), 0); if (sum === 0) { - return []; + return []; } - const otherPrcs = counts - .map(c => c/sum * 100) - .filter(hidePredicate); - const otherPrcsSum = otherPrcs.reduce((a,b)=>a+b,0); - const showLength = partitions.length - otherPrcs.length; - const show = partitions - .sort((a, b) => b.count - a.count) - .slice(0, showLength) - .map(p => ({ - label: mapCountry - ? (countries[p.name] || "Unknown") - : p.name, - prc: p.count/sum * 100, - })) + const otherPrcs = counts.map((c) => (c / sum) * 100).filter(hidePredicate); + const otherPrcsSum = otherPrcs.reduce((a, b) => a + b, 0); + const showLength = partitions.length - otherPrcs.length; + const show = partitions + .sort((a, b) => b.count - a.count) + .slice(0, showLength) + .map((p) => ({ + label: mapCountry ? countries[p.name] || 'Unknown' : p.name, + prc: (p.count / sum) * 100, + })); if (otherPrcsSum > 0) { show.push({ - label: "Other", + label: 'Other', prc: otherPrcsSum, other: true, - }) + }); } return show; } function tagsWrapper(tags = []) { - return tags.map(({ name, partitions }) => ({ - name, - partitions: partitionsWrapper(partitions, name === "country") - })) + return tags.map(({ name, partitions }) => ({ + name, + partitions: partitionsWrapper(partitions, name === 'country'), + })); } function dataWrapper(data = {}) { - return { - chart24: data.chart24 || [], - chart30: data.chart30 || [], - tags: tagsWrapper(data.tags), - }; + return { + chart24: data.chart24 || [], + chart30: data.chart30 || [], + tags: tagsWrapper(data.tags), + }; } -@connect(state => ({ - error: state.getIn([ "errors", "instance" ]) -})) -@withRequest({ - initialData: props => dataWrapper(props.error), - endpoint: props => `/errors/${ props.error.errorId }/stats`, - dataWrapper, -}) -export default class SideSection extends React.PureComponent { - onDateChange = ({ startDate, endDate }) => { - this.props.request({ startDate, endDate }); - } +function SideSection(props) { + const [data, setData] = React.useState({ + chart24: [], + chart30: [], + tags: [], + }); + const [loading, setLoading] = React.useState(false); + const { className } = props; + const { errorStore } = useStore(); + const error = errorStore.instance; - render() { - const { - className, - error, - data, - loading, - } = this.props; - return ( -
-

Overview

- -
- -
- - - { data.tags.length > 0 &&

Summary

} - - { data.tags.map(({ name, partitions }) => - - )} - -
- ); - } + const grabData = async () => { + setLoading(true); + errorService.fetchErrorStats(error.errorId) + .then(data => { + setData(dataWrapper(data)) + }) + .finally(() => setLoading(false)); + } + + React.useEffect(() => { + setData(dataWrapper(error)) + }, [error.errorId]) + + return ( +
+

Overview

+ +
+ +
+ + + {data.tags.length > 0 &&

Summary

} + + {data.tags.map(({ name, partitions }) => ( + + ))} + +
+ ); } + +export default observer(SideSection); diff --git a/frontend/app/components/Errors/Errors.js b/frontend/app/components/Errors/Errors.js deleted file mode 100644 index b9ec178b5..000000000 --- a/frontend/app/components/Errors/Errors.js +++ /dev/null @@ -1,154 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import withSiteIdRouter from 'HOCs/withSiteIdRouter'; -import withPermissions from 'HOCs/withPermissions' -import { UNRESOLVED, RESOLVED, IGNORED, BOOKMARK } from "Types/errorInfo"; -import { fetchBookmarks, editOptions } from "Duck/errors"; -import { applyFilter } from 'Duck/search'; -import { errors as errorsRoute, isRoute } from "App/routes"; -import withPageTitle from 'HOCs/withPageTitle'; -import cn from 'classnames'; -import SelectDateRange from 'Shared/SelectDateRange'; -import Period from 'Types/app/period'; - -import List from './List/List'; -import ErrorInfo from './Error/ErrorInfo'; -import Header from './Header'; -import SideMenuSection from './SideMenu/SideMenuSection'; -import SideMenuDividedItem from './SideMenu/SideMenuDividedItem'; - -const ERRORS_ROUTE = errorsRoute(); - -function getStatusLabel(status) { - switch(status) { - case UNRESOLVED: - return "Unresolved"; - case RESOLVED: - return "Resolved"; - case IGNORED: - return "Ignored"; - default: - return ""; - } -} - -@withPermissions(['ERRORS'], 'page-margin container-90') -@withSiteIdRouter -@connect(state => ({ - list: state.getIn([ "errors", "list" ]), - status: state.getIn([ "errors", "options", "status" ]), - filter: state.getIn([ 'search', 'instance' ]), -}), { - fetchBookmarks, - applyFilter, - editOptions, -}) -@withPageTitle("Errors - OpenReplay") -export default class Errors extends React.PureComponent { - constructor(props) { - super(props) - this.state = { - filter: '', - } - } - - ensureErrorsPage() { - const { history } = this.props; - if (!isRoute(ERRORS_ROUTE, history.location.pathname)) { - history.push(ERRORS_ROUTE); - } - } - - onStatusItemClick = ({ key }) => { - this.props.editOptions({ status: key }); - } - - onBookmarksClick = () => { - this.props.editOptions({ status: BOOKMARK }); - } - - onDateChange = (e) => { - const dateValues = e.toJSON(); - this.props.applyFilter(dateValues); - }; - - render() { - const { - count, - match: { - params: { errorId } - }, - status, - list, - history, - filter, - } = this.props; - - const { startDate, endDate, rangeValue } = filter; - const period = new Period({ start: startDate, end: endDate, rangeName: rangeValue }); - - return ( -
-
- - -
- -
- { errorId == null ? - <> -
-
-
- Seen in - -
-
- - - : - - } -
-
- ); - } -} \ No newline at end of file diff --git a/frontend/app/components/Errors/Header.js b/frontend/app/components/Errors/Header.js deleted file mode 100644 index 283eb599b..000000000 --- a/frontend/app/components/Errors/Header.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -function Header({ text, count }) { - return ( -

- { text } - { count != null && { count } } -

- ); -} - -Header.displayName = "Header"; - -export default Header; - \ No newline at end of file diff --git a/frontend/app/components/Errors/List/List.js b/frontend/app/components/Errors/List/List.js deleted file mode 100644 index a51a3c2b0..000000000 --- a/frontend/app/components/Errors/List/List.js +++ /dev/null @@ -1,259 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { Set } from "immutable"; -import { NoContent, Loader, Checkbox, IconButton, Input, Pagination } from 'UI'; -import { merge, resolve, unresolve, ignore, updateCurrentPage, editOptions } from "Duck/errors"; -import { applyFilter } from 'Duck/filters'; -import { IGNORED, UNRESOLVED } from 'Types/errorInfo'; -import Divider from 'Components/Errors/ui/Divider'; -import ListItem from './ListItem/ListItem'; -import { debounce } from 'App/utils'; -import Select from 'Shared/Select'; -import EmptyStateSvg from '../../../svg/no-results.svg'; - -const sortOptionsMap = { - 'occurrence-desc': 'Last Occurrence', - 'occurrence-desc': 'First Occurrence', - 'sessions-asc': 'Sessions Ascending', - 'sessions-desc': 'Sessions Descending', - 'users-asc': 'Users Ascending', - 'users-desc': 'Users Descending', -}; -const sortOptions = Object.entries(sortOptionsMap) - .map(([ value, label ]) => ({ value, label })); - -@connect(state => ({ - loading: state.getIn([ "errors", "loading" ]), - resolveToggleLoading: state.getIn(["errors", "resolve", "loading"]) || - state.getIn(["errors", "unresolve", "loading"]), - ignoreLoading: state.getIn([ "errors", "ignore", "loading" ]), - mergeLoading: state.getIn([ "errors", "merge", "loading" ]), - currentPage: state.getIn(["errors", "currentPage"]), - limit: state.getIn(["errors", "limit"]), - total: state.getIn([ 'errors', 'totalCount' ]), - sort: state.getIn([ 'errors', 'options', 'sort' ]), - order: state.getIn([ 'errors', 'options', 'order' ]), - query: state.getIn([ "errors", "options", "query" ]), -}), { - merge, - resolve, - unresolve, - ignore, - applyFilter, - updateCurrentPage, - editOptions, -}) -export default class List extends React.PureComponent { - constructor(props) { - super(props) - this.state = { - checkedAll: false, - checkedIds: Set(), - query: props.query, - } - this.debounceFetch = debounce(this.props.editOptions, 1000); - } - - componentDidMount() { - this.props.applyFilter({ }); - } - - check = ({ errorId }) => { - const { checkedIds } = this.state; - const newCheckedIds = checkedIds.contains(errorId) - ? checkedIds.remove(errorId) - : checkedIds.add(errorId); - this.setState({ - checkedAll: newCheckedIds.size === this.props.list.size, - checkedIds: newCheckedIds - }); - } - - checkAll = () => { - if (this.state.checkedAll) { - this.setState({ - checkedAll: false, - checkedIds: Set(), - }); - } else { - this.setState({ - checkedAll: true, - checkedIds: this.props.list.map(({ errorId }) => errorId).toSet(), - }); - } - } - - resetChecked = () => { - this.setState({ - checkedAll: false, - checkedIds: Set(), - }); - } - - currentCheckedIds() { - return this.state.checkedIds - .intersect(this.props.list.map(({ errorId }) => errorId).toSet()); - } - - merge = () => { - this.props.merge(currentCheckedIds().toJS()).then(this.resetChecked); - } - - applyToAllChecked(f) { - return Promise.all(this.currentCheckedIds().map(f).toJS()).then(this.resetChecked); - } - - resolve = () => { - this.applyToAllChecked(this.props.resolve); - } - - unresolve = () => { - this.applyToAllChecked(this.props.unresolve); - } - - ignore = () => { - this.applyToAllChecked(this.props.ignore); - } - - addPage = () => this.props.updateCurrentPage(this.props.currentPage + 1) - - writeOption = ({ name, value }) => { - const [ sort, order ] = value.split('-'); - if (name === 'sort') { - this.props.editOptions({ sort, order }); - } - } - - // onQueryChange = ({ target: { value, name } }) => props.edit({ [ name ]: value }) - - onQueryChange = ({ target: { value, name } }) => { - this.setState({ query: value }); - this.debounceFetch({ query: value }); - } - - render() { - const { - list, - status, - loading, - ignoreLoading, - resolveToggleLoading, - mergeLoading, - currentPage, - total, - sort, - order, - limit, - } = this.props; - const { - checkedAll, - checkedIds, - query, - } = this.state; - const someLoading = loading || ignoreLoading || resolveToggleLoading || mergeLoading; - const currentCheckedIds = this.currentCheckedIds(); - - return ( -
-
-
- - { status === UNRESOLVED - ? - : - } - { status !== IGNORED && - - } -
-
- Sort By - -
-
- - - - No Errors Found! - - } - subtext="Please try to change your search parameters." - // animatedIcon="empty-state" - show={ !loading && list.size === 0} - > - - { list.map(e => -
- - -
- )} -
- this.props.updateCurrentPage(page)} - limit={limit} - debounceRequest={500} - /> -
-
- - - ); - } -} diff --git a/frontend/app/components/Errors/List/ListItem/ListItem.js b/frontend/app/components/Errors/List/ListItem/ListItem.js deleted file mode 100644 index 0e4474567..000000000 --- a/frontend/app/components/Errors/List/ListItem/ListItem.js +++ /dev/null @@ -1,87 +0,0 @@ -import React from 'react'; -import { BarChart, Bar, YAxis, Tooltip, XAxis } from 'recharts'; -import cn from 'classnames'; -import { DateTime } from 'luxon' -import { diffFromNowString } from 'App/date'; -import { error as errorRoute } from 'App/routes'; -import { IGNORED, RESOLVED } from 'Types/errorInfo'; -import { Checkbox, Link } from 'UI'; -import ErrorName from 'Components/Errors/ui/ErrorName'; -import Label from 'Components/Errors/ui/Label'; -import stl from './listItem.module.css'; -import { Styles } from '../../../Dashboard/Widgets/common'; - -const CustomTooltip = ({ active, payload, label }) => { - if (active) { - const p = payload[0].payload; - const dateStr = p.timestamp ? DateTime.fromMillis(p.timestamp).toFormat('l') : '' - return ( -
-

{dateStr}

-

Sessions: {p.count}

-
- ); - } - - return null; -}; - -function ListItem({ className, onCheck, checked, error, disabled }) { - - const getDateFormat = val => { - const d = new Date(val); - return (d.getMonth()+ 1) + '/' + d.getDate() - } - - return ( -
- onCheck(error) } - /> - -
- - -
- { error.message } -
- -
- - - - } /> - - -
- ); -} - - -ListItem.displayName = "ListItem"; -export default ListItem; \ No newline at end of file diff --git a/frontend/app/components/Errors/List/ListItem/listItem.module.css b/frontend/app/components/Errors/List/ListItem/listItem.module.css deleted file mode 100644 index c9f7589b9..000000000 --- a/frontend/app/components/Errors/List/ListItem/listItem.module.css +++ /dev/null @@ -1,16 +0,0 @@ -.name { - min-width: 55%; -} - -.sessions { - width: 6%; -} - -.users { - width: 5%; -} - -.occurrence { - width: 15%; - min-width: 152px; -} diff --git a/frontend/app/components/Errors/SideMenu/SideMenuDividedItem.js b/frontend/app/components/Errors/SideMenu/SideMenuDividedItem.js deleted file mode 100644 index 9efa63ed4..000000000 --- a/frontend/app/components/Errors/SideMenu/SideMenuDividedItem.js +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import { SideMenuitem } from "UI"; -import Divider from 'Components/Errors/ui/Divider'; -function SideMenuDividedItem({ className, noTopDivider = false, noBottomDivider = false, ...props }) { - return ( -
- { !noTopDivider && } - - { !noBottomDivider && } -
- ); -} - -SideMenuDividedItem.displayName = "SideMenuDividedItem"; - -export default SideMenuDividedItem; - diff --git a/frontend/app/components/Errors/SideMenu/SideMenuHeader.js b/frontend/app/components/Errors/SideMenu/SideMenuHeader.js deleted file mode 100644 index 32f1f7fc6..000000000 --- a/frontend/app/components/Errors/SideMenu/SideMenuHeader.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import cn from 'classnames'; -import stl from './sideMenuHeader.module.css'; - -function SideMenuHeader({ text, className }) { - return ( -
- { text } -
- ) -} - -SideMenuHeader.displayName = "SideMenuHeader"; -export default SideMenuHeader; diff --git a/frontend/app/components/Errors/SideMenu/SideMenuSection.js b/frontend/app/components/Errors/SideMenu/SideMenuSection.js deleted file mode 100644 index f2d6732f2..000000000 --- a/frontend/app/components/Errors/SideMenu/SideMenuSection.js +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import { SideMenuitem } from 'UI'; -import SideMenuHeader from './SideMenuHeader'; - -function SideMenuSection({ title, items, onItemClick }) { - return ( - <> - - { items.map(item => - onItemClick(item)} - /> - )} - - ); -} - -SideMenuSection.displayName = "SideMenuSection"; - -export default SideMenuSection; \ No newline at end of file diff --git a/frontend/app/components/Errors/SideMenu/sideMenuHeader.module.css b/frontend/app/components/Errors/SideMenu/sideMenuHeader.module.css deleted file mode 100644 index 5dce4e250..000000000 --- a/frontend/app/components/Errors/SideMenu/sideMenuHeader.module.css +++ /dev/null @@ -1,4 +0,0 @@ -.label { - letter-spacing: 0.2em; - color: gray; -} \ No newline at end of file diff --git a/frontend/app/components/FFlags/NewFFlag/Description.tsx b/frontend/app/components/FFlags/NewFFlag/Description.tsx index cfcbab77d..65a4ed882 100644 --- a/frontend/app/components/FFlags/NewFFlag/Description.tsx +++ b/frontend/app/components/FFlags/NewFFlag/Description.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { observer } from 'mobx-react-lite'; import { Button } from 'UI'; import cn from 'classnames'; -import FeatureFlag from 'MOBX/types/FeatureFlag'; +import FeatureFlag from 'App/mstore/types/FeatureFlag'; function Description({ isDescrEditing, diff --git a/frontend/app/components/ForgotPassword/CreatePassword.tsx b/frontend/app/components/ForgotPassword/CreatePassword.tsx index 00f93542f..b276e6d4f 100644 --- a/frontend/app/components/ForgotPassword/CreatePassword.tsx +++ b/frontend/app/components/ForgotPassword/CreatePassword.tsx @@ -1,8 +1,8 @@ import React, { useEffect } from 'react'; -import { connect } from 'react-redux'; +import { observer } from 'mobx-react-lite'; +import { useStore } from 'App/mstore'; import ReCAPTCHA from 'react-google-recaptcha'; import { Form, Input, Loader, Button, Icon, Message } from 'UI'; -import { requestResetPassword, resetPassword, resetErrors } from 'Duck/user'; import stl from './forgotPassword.module.css'; import { validatePassword } from 'App/validate'; import { PASSWORD_POLICY } from 'App/constants'; @@ -13,29 +13,26 @@ const CAPTCHA_ENABLED = window.env.CAPTCHA_ENABLED === 'true'; const CAPTCHA_SITE_KEY = window.env.CAPTCHA_SITE_KEY; interface Props { - errors: any; - resetErrors: any; - loading: boolean; params: any; - resetPassword: Function; } function CreatePassword(props: Props) { - const { loading, params } = props; + const { params } = props; + const { userStore } = useStore(); + const loading = userStore.loading; + const resetPassword = userStore.resetPassword; const [error, setError] = React.useState(null); const [validationError, setValidationError] = React.useState(null); const [updated, setUpdated] = React.useState(false); - const [requested, setRequested] = React.useState(false); const [passwordRepeat, setPasswordRepeat] = React.useState(''); const [password, setPassword] = React.useState(''); - const [doesntMatch, setDoesntMatch] = React.useState(false); const pass = params.get('pass'); const invitation = params.get('invitation'); - const handleSubmit = (token?: any) => { + const handleSubmit = () => { if (!validatePassword(password)) { return; } - props.resetPassword({ invitation, pass, password }).then((response: any) => { + resetPassword({ invitation, pass, password }).then((response: any) => { if (response && response.errors && response.errors.length > 0) { setError(response.errors[0]); } else { @@ -84,7 +81,6 @@ function CreatePassword(props: Props) { handleSubmit(token)} /> @@ -150,17 +146,4 @@ function CreatePassword(props: Props) { ); } -export default connect( - (state: any) => ({ - errors: state.getIn(['user', 'requestResetPassowrd', 'errors']), - resetErrors: state.getIn(['user', 'resetPassword', 'errors']), - loading: - state.getIn(['user', 'requestResetPassowrd', 'loading']) || - state.getIn(['user', 'resetPassword', 'loading']), - }), - { - requestResetPassword, - resetPassword, - resetErrors, - } -)(CreatePassword); +export default observer(CreatePassword); diff --git a/frontend/app/components/ForgotPassword/ForgotPassword.tsx b/frontend/app/components/ForgotPassword/ForgotPassword.tsx index 253c7283d..d9a98f798 100644 --- a/frontend/app/components/ForgotPassword/ForgotPassword.tsx +++ b/frontend/app/components/ForgotPassword/ForgotPassword.tsx @@ -1,19 +1,15 @@ import Copyright from 'Shared/Copyright'; import React from 'react'; -import { Form, Input, Loader, Link, Icon, Message } from 'UI'; +import { Link } from 'UI'; import {Button} from 'antd'; import { login as loginRoute } from 'App/routes'; -import { connect } from 'react-redux'; import ResetPassword from './ResetPasswordRequest'; import CreatePassword from './CreatePassword'; const LOGIN = loginRoute(); -interface Props { - params: any; -} -function ForgotPassword(props: Props) { - const { params } = props; +function ForgotPassword(props) { + const params = new URLSearchParams(props.location.search); const pass = params.get('pass'); const invitation = params.get('invitation'); const creatingNewPassword = pass && invitation; @@ -54,6 +50,4 @@ function ForgotPassword(props: Props) { ); } -export default connect((state: any, props: any) => ({ - params: new URLSearchParams(props.location.search), -}))(ForgotPassword); +export default ForgotPassword; diff --git a/frontend/app/components/ForgotPassword/ResetPasswordRequest.tsx b/frontend/app/components/ForgotPassword/ResetPasswordRequest.tsx index 2be473d3b..bdc170b33 100644 --- a/frontend/app/components/ForgotPassword/ResetPasswordRequest.tsx +++ b/frontend/app/components/ForgotPassword/ResetPasswordRequest.tsx @@ -1,15 +1,13 @@ import React from 'react'; import { Form, Input, Loader, Button, Icon } from 'UI'; import ReCAPTCHA from 'react-google-recaptcha'; -import { connect } from 'react-redux'; -import { requestResetPassword } from 'Duck/user'; +import { observer } from 'mobx-react-lite'; +import { useStore } from 'App/mstore'; -interface Props { - requestResetPassword: Function; - loading?: boolean; -} -function ResetPasswordRequest(props: Props) { - const { loading = false } = props; +function ResetPasswordRequest() { + const { userStore } = useStore(); + const loading = userStore.loading; + const requestResetPassword = userStore.requestResetPassword; const recaptchaRef = React.createRef(); const [requested, setRequested] = React.useState(false); const [email, setEmail] = React.useState(''); @@ -35,8 +33,7 @@ function ResetPasswordRequest(props: Props) { if (CAPTCHA_ENABLED && recaptchaRef.current && (token === null || token === undefined)) return; setError(null); - props - .requestResetPassword({ email: email.trim(), 'g-recaptcha-response': token }) + requestResetPassword({ email: email.trim(), 'g-recaptcha-response': token }) .then((response: any) => { setRequested(true); if (response && response.errors && response.errors.length > 0) { @@ -106,6 +103,4 @@ function ResetPasswordRequest(props: Props) { ); } -export default connect((state: any) => ({ - loading: state.getIn(['user', 'requestResetPassowrd', 'loading']), -}), { requestResetPassword })(ResetPasswordRequest); +export default observer(ResetPasswordRequest); diff --git a/frontend/app/components/Funnels/FunnelDetails/FunnelDetails.js__ b/frontend/app/components/Funnels/FunnelDetails/FunnelDetails.js__ deleted file mode 100644 index 59e172c54..000000000 --- a/frontend/app/components/Funnels/FunnelDetails/FunnelDetails.js__ +++ /dev/null @@ -1,159 +0,0 @@ -import React, { useState, useEffect } from 'react' -import { Tabs, Loader } from 'UI' -import FunnelHeader from 'Components/Funnels/FunnelHeader' -import FunnelGraph from 'Components/Funnels/FunnelGraph' -import FunnelSessionList from 'Components/Funnels/FunnelSessionList' -import FunnelOverview from 'Components/Funnels/FunnelOverview' -import FunnelIssues from 'Components/Funnels/FunnelIssues' -import { connect } from 'react-redux'; -import { - fetch, fetchInsights, fetchList, fetchFiltered, fetchIssuesFiltered, fetchSessionsFiltered, fetchIssueTypes, resetFunnel, refresh -} from 'Duck/funnels'; -import { applyFilter, setFilterOptions, resetFunnelFilters, setInitialFilters } from 'Duck/funnelFilters'; -import { withRouter } from 'react-router'; -import { sessions as sessionsRoute, funnel as funnelRoute, withSiteId } from 'App/routes'; -import FunnelSearch from 'Shared/FunnelSearch'; -import cn from 'classnames'; -import IssuesEmptyMessage from 'Components/Funnels/IssuesEmptyMessage' - -const TAB_ISSUES = 'ANALYSIS'; -const TAB_SESSIONS = 'SESSIONS'; - -const TABS = [ TAB_ISSUES, TAB_SESSIONS ].map(tab => ({ - text: tab, - disabled: false, - key: tab, -})); - -const FunnelDetails = (props) => { - const { insights, funnels, funnel, funnelId, loading, liveFilters, issuesLoading, sessionsLoading, refresh } = props; - const [activeTab, setActiveTab] = useState(TAB_ISSUES) - const [showFilters, setShowFilters] = useState(false) - const [mounted, setMounted] = useState(false); - const onTabClick = activeTab => setActiveTab(activeTab) - - useEffect(() => { - if (funnels.size === 0) { - props.fetchList(); - } - props.fetchIssueTypes() - - props.fetch(funnelId).then(() => { - setMounted(true); - }).then(() => { - props.refresh(funnelId); - }) - - }, []); - - // useEffect(() => { - // if (funnel && funnel.filter && liveFilters.events.size === 0) { - // props.setInitialFilters(); - // } - // }, [funnel]) - - const onBack = () => { - props.history.push(sessionsRoute()); - } - - const redirect = funnelId => { - const { siteId, history } = props; - props.resetFunnel(); - props.resetFunnelFilters(); - - history.push(withSiteId(funnelRoute(parseInt(funnelId)), siteId)); - } - - const renderActiveTab = (tab, hasNoStages) => { - switch(tab) { - case TAB_ISSUES: - return !hasNoStages && - case TAB_SESSIONS: - return - } - } - - const hasNoStages = !loading && insights.stages.length <= 1; - const showEmptyMessage = hasNoStages && activeTab === TAB_ISSUES && !loading; - - return ( -
- setShowFilters(!showFilters)} - showFilters={showFilters} - /> -
- {showFilters && ( - - ) - } -
- -
- - setShowFilters(true)} show={showEmptyMessage}> -
-
-
- -
-
- -
-
-
- - { renderActiveTab(activeTab, hasNoStages) } - -
- - - -
- ) -} - -export default connect((state, props) => { - const insightsLoading = state.getIn(['funnels', 'fetchInsights', 'loading']); - const issuesLoading = state.getIn(['funnels', 'fetchIssuesRequest', 'loading']); - const funnelLoading = state.getIn(['funnels', 'fetchRequest', 'loading']); - const sessionsLoading = state.getIn(['funnels', 'fetchSessionsRequest', 'loading']); - return { - funnels: state.getIn(['funnels', 'list']), - funnel: state.getIn(['funnels', 'instance']), - insights: state.getIn(['funnels', 'insights']), - loading: funnelLoading || (insightsLoading && (issuesLoading || sessionsLoading)), - issuesLoading, - sessionsLoading, - funnelId: props.match.params.funnelId, - activeStages: state.getIn(['funnels', 'activeStages']), - funnelFilters: state.getIn(['funnels', 'funnelFilters']), - siteId: state.getIn([ 'site', 'siteId' ]), - liveFilters: state.getIn(['funnelFilters', 'appliedFilter']), - } -}, { - fetch, - fetchInsights, - fetchFiltered, - fetchIssuesFiltered, - fetchList, - applyFilter, - setFilterOptions, - fetchIssuesFiltered, - fetchSessionsFiltered, - fetchIssueTypes, - resetFunnel, - resetFunnelFilters, - setInitialFilters, - refresh, -})(withRouter((FunnelDetails))) diff --git a/frontend/app/components/Funnels/FunnelGraph/FunnelGraph.js b/frontend/app/components/Funnels/FunnelGraph/FunnelGraph.js deleted file mode 100644 index 221a59e8f..000000000 --- a/frontend/app/components/Funnels/FunnelGraph/FunnelGraph.js +++ /dev/null @@ -1,303 +0,0 @@ -import React, { useState } from 'react'; -import { Icon, Tooltip as AppTooltip } from 'UI'; -import { numberCompact } from 'App/utils'; -import { - BarChart, - Bar, - Cell, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - LabelList, - -} from 'recharts'; -import { connect } from 'react-redux'; -import { setActiveStages } from 'Duck/funnels'; -import { Styles } from '../../Dashboard/Widgets/common'; -import { numberWithCommas } from 'App/utils'; -import { truncate } from 'App/utils'; - -const MIN_BAR_HEIGHT = 20; - -function CustomTick(props) { - const { x, y, payload } = props; - return ( - - - {payload.value} - - - ); -} - -function FunnelGraph(props) { - const { data, activeStages, funnelId, liveFilters } = props; - const [activeIndex, setActiveIndex] = useState(activeStages); - - const renderPercentage = (props) => { - const { x, y, width, height, value } = props; - const radius = 10; - const _x = x + width / 2 + 45; - - return ( - - - - - - - - {numberCompact(value)} - - - ); - }; - - const renderCustomizedLabel = (props) => { - const { x, y, width, height, value, textColor = '#fff' } = props; - const radius = 10; - - if (value === 0) return; - - return ( - - - {numberCompact(value)} - - - ); - }; - - const handleClick = (data, index) => { - if (activeStages.length === 1 && activeStages.includes(index)) { - // selecting the same bar - props.setActiveStages([], null); - return; - } - - if (activeStages.length === 2) { - // already having two bars - return; - } - - // new selection - const arr = activeStages.concat([index]); - props.setActiveStages(arr.sort(), arr.length === 2 && liveFilters, funnelId); - }; - - const resetActiveSatges = () => { - props.setActiveStages([], liveFilters, funnelId, true); - }; - - const renderDropLabel = ({ x, y, width, value }) => { - if (value === 0) return; - return ( - - {value} - - ); - }; - - const renderMainLabel = ({ x, y, width, value }) => { - return ( - - {numberWithCommas(value)} - - ); - }; - - const CustomBar = (props) => { - const { fill, x, y, width, height, sessionsCount, index, dropDueToIssues } = props; - const yp = sessionsCount < MIN_BAR_HEIGHT ? MIN_BAR_HEIGHT - 1 : dropDueToIssues; - const tmp = (height <= 20 ? 20 : height) - (TEMP[index].height > 20 ? 0 : TEMP[index].height); - return ( - - - - ); - }; - const MainBar = (props) => { - const { - fill, - x, - y, - width, - height, - sessionsCount, - index, - dropDueToIssues, - hasSelection = false, - } = props; - const yp = sessionsCount < MIN_BAR_HEIGHT ? MIN_BAR_HEIGHT - 1 : dropDueToIssues; - - TEMP[index] = { height, y }; - - return ( - - - - ); - }; - - const renderDropPct = (props) => { - // TODO - const { fill, x, y, width, height, value, totalBars } = props; - const barW = x + 730 / totalBars / 2; - - return ( - - - - ); - }; - - const CustomTooltip = (props) => { - const { payload } = props; - if (payload.length === 0) return null; - const { value, headerText } = payload[0].payload; - - // const value = payload[0].payload.value; - if (!value) return null; - return ( -
-
{headerText}
- {value.map((i) => ( -
{truncate(i, 30)}
- ))} -
- ); - }; - // const CustomTooltip = ({ active, payload, msg = '' }) => { - // return ( - //
- //

{msg}

- //
- // ); - // }; - - const TEMP = {}; - - return ( -
- {activeStages.length === 2 && ( -
- - - -
- )} - - - {/* {activeStages.length < 2 && 0 ? 'Select one more event.' : 'Select any two events to analyze in depth.'} />} />} */} - - - } - cursor="pointer" - minPointSize={MIN_BAR_HEIGHT} - background={false} - > - - {data.map((entry, index) => { - const selected = - activeStages.includes(index) || (index > activeStages[0] && index < activeStages[1]); - const opacity = activeStages.length > 0 && !selected ? 0.4 : 1; - return ( - - ); - })} - - - } - minPointSize={MIN_BAR_HEIGHT} - > - - {data.map((entry, index) => { - const selected = - activeStages.includes(index) || (index > activeStages[0] && index < activeStages[1]); - const opacity = activeStages.length > 0 && !selected ? 0.4 : 1; - return ( - - ); - })} - - - } - xAxisId={0} - /> - {/* '"' + val + '"'} - /> */} - Styles.tickFormatter(val)} - /> - -
- ); -} - -export default connect( - (state) => ({ - activeStages: state.getIn(['funnels', 'activeStages']).toJS(), - liveFilters: state.getIn(['funnelFilters', 'appliedFilter']), - }), - { setActiveStages } -)(FunnelGraph); diff --git a/frontend/app/components/Funnels/FunnelGraph/index.js b/frontend/app/components/Funnels/FunnelGraph/index.js deleted file mode 100644 index 990d34099..000000000 --- a/frontend/app/components/Funnels/FunnelGraph/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './FunnelGraph' \ No newline at end of file diff --git a/frontend/app/components/Funnels/FunnelHeader/FunnelDropdown.js b/frontend/app/components/Funnels/FunnelHeader/FunnelDropdown.js deleted file mode 100644 index a5b3bf445..000000000 --- a/frontend/app/components/Funnels/FunnelHeader/FunnelDropdown.js +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react' -import { connect } from 'react-redux' -import { withRouter } from 'react-router' -import { Dropdown } from 'UI' -import { funnel as funnelRoute, withSiteId } from 'App/routes'; - -function FunnelDropdown(props) { - const { options, funnel } = props; - - const writeOption = (e, { name, value }) => { - const { siteId, history } = props; - history.push(withSiteId(funnelRoute(parseInt(value)), siteId)); - } - - return ( -
- -
- ) -} - -export default connect((state, props) => ({ - funnels: state.getIn(['funnels', 'list']), - funnel: state.getIn(['funnels', 'instance']), - siteId: state.getIn([ 'site', 'siteId' ]), -}), { })(withRouter(FunnelDropdown)) diff --git a/frontend/app/components/Funnels/FunnelHeader/FunnelHeader.js b/frontend/app/components/Funnels/FunnelHeader/FunnelHeader.js deleted file mode 100644 index 58e64d295..000000000 --- a/frontend/app/components/Funnels/FunnelHeader/FunnelHeader.js +++ /dev/null @@ -1,149 +0,0 @@ -import React, { useState } from 'react'; -import { Icon, BackLink, IconButton, Dropdown, Tooltip, TextEllipsis, Button } from 'UI'; -import { - remove as deleteFunnel, - fetch, - fetchInsights, - fetchIssuesFiltered, - fetchSessionsFiltered, -} from 'Duck/funnels'; -import { editFilter, editFunnelFilter, refresh } from 'Duck/funnels'; -import DateRange from 'Shared/DateRange'; -import { connect } from 'react-redux'; -import { confirm } from 'UI'; -import FunnelSaveModal from 'Components/Funnels/FunnelSaveModal'; -import stl from './funnelHeader.module.css'; - -const Info = ({ label = '', value = '', className = 'mx-4' }) => { - return ( -
- {label} - {value} -
- ); -}; - -const FunnelHeader = (props) => { - const { - funnel, - insights, - funnels, - onBack, - funnelId, - showFilters = false, - funnelFilters, - renameHandler, - } = props; - const [showSaveModal, setShowSaveModal] = useState(false); - - const writeOption = (e, { name, value }) => { - props.redirect(value); - props.fetch(value).then(() => props.refresh(value)); - }; - - const deleteFunnel = async (e, funnel) => { - e.preventDefault(); - e.stopPropagation(); - - if ( - await confirm({ - header: 'Delete Funnel', - confirmButton: 'Delete', - confirmation: `Are you sure you want to permanently delete "${funnel.name}"?`, - }) - ) { - props.deleteFunnel(funnel.funnelId).then(props.onBack); - } else { - } - }; - - const onDateChange = (e) => { - props.editFunnelFilter(e, funnelId); - }; - - const options = funnels.map(({ funnelId, name }) => ({ text: name, value: funnelId })).toJS(); - const selectedFunnel = funnels.filter((i) => i.funnelId === parseInt(funnelId)).first() || {}; - const eventsCount = funnel.filter.filters.filter((i) => i.isEvent).size; - - return ( -
-
- - setShowSaveModal(false)} /> -
- - -
- } - options={options} - className={stl.dropdown} - name="funnel" - value={parseInt(funnelId)} - // icon={null} - onChange={writeOption} - selectOnBlur={false} - icon={ - - } - /> - - - - - - -
-
-
- - setShowSaveModal(true)} /> - - - deleteFunnel(e, funnel)} - className="ml-2 mr-2" - /> - -
- -
-
-
- ); -}; - -export default connect( - (state) => ({ - funnelFilters: state.getIn(['funnels', 'funnelFilters']).toJS(), - funnel: state.getIn(['funnels', 'instance']), - }), - { - editFilter, - editFunnelFilter, - deleteFunnel, - fetch, - fetchInsights, - fetchIssuesFiltered, - fetchSessionsFiltered, - refresh, - } -)(FunnelHeader); diff --git a/frontend/app/components/Funnels/FunnelHeader/funnelHeader.module.css b/frontend/app/components/Funnels/FunnelHeader/funnelHeader.module.css deleted file mode 100644 index 7ab834b76..000000000 --- a/frontend/app/components/Funnels/FunnelHeader/funnelHeader.module.css +++ /dev/null @@ -1,30 +0,0 @@ -.dropdown { - display: flex !important; - align-items: center; - padding: 0 20px; - border-radius: 0; - border-radius: 0; - color: $gray-darkest; - font-weight: 500; - height: 54px; - padding-right: 20px; - border-right: solid thin #eee; - border-bottom-left-radius: 3px; - border-top-left-radius: 3px; - &:hover { - background-color: $gray-lightest; - } -} - -.dropdownTrigger { - padding: 4px 8px; - border-radius: 3px; - &:hover { - background-color: $gray-light; - } -} - -.dropdownIcon { - margin-top: 4px; - margin-left: 6px; -} \ No newline at end of file diff --git a/frontend/app/components/Funnels/FunnelHeader/index.js b/frontend/app/components/Funnels/FunnelHeader/index.js deleted file mode 100644 index a5efa97af..000000000 --- a/frontend/app/components/Funnels/FunnelHeader/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './FunnelHeader'; \ No newline at end of file diff --git a/frontend/app/components/Funnels/FunnelIssueDetails/FunnelIssueDetails.js b/frontend/app/components/Funnels/FunnelIssueDetails/FunnelIssueDetails.js deleted file mode 100644 index 77017e4ac..000000000 --- a/frontend/app/components/Funnels/FunnelIssueDetails/FunnelIssueDetails.js +++ /dev/null @@ -1,43 +0,0 @@ -import React, { useEffect } from 'react' -import IssueItem from 'Components/Funnels/IssueItem' -import FunnelSessionList from 'Components/Funnels/FunnelSessionList' -import { connect } from 'react-redux' -import { withRouter } from 'react-router' -import { fetchIssue, setNavRef, resetIssue } from 'Duck/funnels' -import { funnel as funnelRoute, withSiteId } from 'App/routes' -import { Loader } from 'UI' - -function FunnelIssueDetails(props) { - const { issue, issueId, funnelId, loading = false } = props; - - useEffect(() => { - props.fetchIssue(funnelId, issueId) - - return () => { - props.resetIssue(); - } - }, [issueId]) - - const onBack = () => { - const { siteId, history } = props; - history.push(withSiteId(funnelRoute(parseInt(funnelId)), siteId)); - } - - return ( -
- - -
- - -
- ) -} - -export default connect((state, props) => ({ - loading: state.getIn(['funnels', 'fetchIssueRequest', 'loading']), - issue: state.getIn(['funnels', 'issue']), - issueId: props.match.params.issueId, - funnelId: props.match.params.funnelId, - siteId: state.getIn([ 'site', 'siteId' ]), -}), { fetchIssue, setNavRef, resetIssue })(withRouter(FunnelIssueDetails)) diff --git a/frontend/app/components/Funnels/FunnelIssueDetails/index.js b/frontend/app/components/Funnels/FunnelIssueDetails/index.js index 3ac32a034..0c66aec5b 100644 --- a/frontend/app/components/Funnels/FunnelIssueDetails/index.js +++ b/frontend/app/components/Funnels/FunnelIssueDetails/index.js @@ -1 +1 @@ -export { default } from './FunnelIssueDetails' \ No newline at end of file +//export { default } from './FunnelIssueDetails' diff --git a/frontend/app/components/Funnels/FunnelIssues/FunnelIssues.js b/frontend/app/components/Funnels/FunnelIssues/FunnelIssues.js deleted file mode 100644 index 3a6070afe..000000000 --- a/frontend/app/components/Funnels/FunnelIssues/FunnelIssues.js +++ /dev/null @@ -1,89 +0,0 @@ -import React, { useState } from 'react' -import { connect } from 'react-redux' -import { fetchIssues, fetchIssuesFiltered } from 'Duck/funnels' -import { LoadMoreButton, NoContent } from 'UI' -import FunnelIssuesHeader from '../FunnelIssuesHeader' -import IssueItem from '../IssueItem'; -import { funnelIssue as funnelIssueRoute, withSiteId } from 'App/routes' -import { withRouter } from 'react-router' -import IssueFilter from '../IssueFilter'; -import SortDropdown from './SortDropdown'; -import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; - -const PER_PAGE = 10; - -function FunnelIssues(props) { - const { - funnel, list, loading = false, - criticalIssuesCount, issueFilters, sort - } = props; - - const [showPages, setShowPages] = useState(1) - - const addPage = () => setShowPages(showPages + 1); - - const onClick = ({ issueId }) => { - const { siteId, history } = props; - history.push(withSiteId(funnelIssueRoute(funnel.funnelId, issueId), siteId)); - } - - let filteredList = issueFilters.size > 0 ? list.filter(item => issueFilters.includes(item.type)) : list; - filteredList = sort.sort ? filteredList.sortBy(i => i[sort.sort]) : filteredList; - filteredList = sort.order === 'desc' ? filteredList.reverse() : filteredList; - const displayedCount = Math.min(showPages * PER_PAGE, filteredList.size); - - return ( -
- -
- -
- Sort By - -
-
- - -
No Issues Found!
-
- } - subtext="Please try changing your search parameters." - // animatedIcon="no-results" - show={ !loading && filteredList.size === 0} - > - { filteredList.take(displayedCount).map(issue => ( -
- onClick(issue)} - /> -
- ))} - - - -
- ) -} - -export default connect(state => ({ - list: state.getIn(['funnels', 'issues']), - criticalIssuesCount: state.getIn(['funnels', 'criticalIssuesCount']), - loading: state.getIn(['funnels', 'fetchIssuesRequest', 'loading']), - siteId: state.getIn([ 'site', 'siteId' ]), - funnel: state.getIn(['funnels', 'instance']), - activeStages: state.getIn(['funnels', 'activeStages']), - funnelFilters: state.getIn(['funnels', 'funnelFilters']), - liveFilters: state.getIn(['funnelFilters', 'appliedFilter']), - issueFilters: state.getIn(['funnels', 'issueFilters', 'filters']), - sort: state.getIn(['funnels', 'issueFilters', 'sort']), -}), { fetchIssues, fetchIssuesFiltered })(withRouter(FunnelIssues)) diff --git a/frontend/app/components/Funnels/FunnelIssues/SortDropdown/SortDropdown.js b/frontend/app/components/Funnels/FunnelIssues/SortDropdown/SortDropdown.js deleted file mode 100644 index a9d22f2f0..000000000 --- a/frontend/app/components/Funnels/FunnelIssues/SortDropdown/SortDropdown.js +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import Select from 'Shared/Select' -import { sort } from 'Duck/sessions'; -import { applyIssueFilter } from 'Duck/funnels'; - -const sortOptionsMap = { - 'afectedUsers-desc': 'Affected Users (High)', - 'afectedUsers-asc': 'Affected Users (Low)', - 'conversionImpact-desc': 'Conversion Impact (High)', - 'conversionImpact-asc': 'Conversion Impact (Low)', - 'lostConversions-desc': 'Lost Conversions (High)', - 'lostConversions-asc': 'Lost Conversions (Low)', -}; - -const sortOptions = Object.entries(sortOptionsMap) - .map(([ value, label ]) => ({ value, label })); - -@connect(state => ({ - sorts: state.getIn(['funnels', 'issueFilters', 'sort']) -}), { sort, applyIssueFilter }) -export default class SortDropdown extends React.PureComponent { - state = { value: null } - sort = ({ value }) => { - this.setState({ value: value }) - const [ sort, order ] = value.split('-'); - const sign = order === 'desc' ? -1 : 1; - this.props.applyIssueFilter({ sort: { order, sort } }); - - this.props.sort(sort, sign) - setTimeout(() => this.props.sort(sort, sign), 3000); //AAA - } - - render() { - const { sorts } = this.props; - - return ( - - - - -
- -
this.props.edit({ isPublic: !funnel.isPublic })} - > - - Team Visible -
-
-
- - - - - - - - ); - } -} diff --git a/frontend/app/components/Funnels/FunnelSaveModal/funnelSaveModal.module.css b/frontend/app/components/Funnels/FunnelSaveModal/funnelSaveModal.module.css deleted file mode 100644 index ed2600745..000000000 --- a/frontend/app/components/Funnels/FunnelSaveModal/funnelSaveModal.module.css +++ /dev/null @@ -1,15 +0,0 @@ -@import 'mixins.css'; - -.modalHeader { - display: flex !important; - align-items: center; - justify-content: space-between; -} - -.cancelButton { - @mixin plainButton; -} - -.applyButton { - @mixin basicButton; -} \ No newline at end of file diff --git a/frontend/app/components/Funnels/FunnelSaveModal/index.js b/frontend/app/components/Funnels/FunnelSaveModal/index.js deleted file mode 100644 index 1265b5b10..000000000 --- a/frontend/app/components/Funnels/FunnelSaveModal/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './FunnelSaveModal' \ No newline at end of file diff --git a/frontend/app/components/Funnels/FunnelSessionList/FunnelSessionList.js b/frontend/app/components/Funnels/FunnelSessionList/FunnelSessionList.js deleted file mode 100644 index c7aca3149..000000000 --- a/frontend/app/components/Funnels/FunnelSessionList/FunnelSessionList.js +++ /dev/null @@ -1,68 +0,0 @@ -import React, { useState, useEffect } from 'react' -import { connect } from 'react-redux' -import SessionItem from 'Shared/SessionItem' -import { fetchSessions, fetchSessionsFiltered } from 'Duck/funnels' -import { setFunnelPage } from 'Duck/sessions' -import { LoadMoreButton, NoContent } from 'UI' -import FunnelSessionsHeader from '../FunnelSessionsHeader' -import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; - -const PER_PAGE = 10; - -function FunnelSessionList(props) { - const { funnelId, issueId, list, sessionsTotal, sessionsSort, inDetails = false } = props; - - const [showPages, setShowPages] = useState(1) - const displayedCount = Math.min(showPages * PER_PAGE, list.size); - - const addPage = () => setShowPages(showPages + 1); - - useEffect(() => { - props.setFunnelPage({ - funnelId, - issueId - }) - }, []) - - return ( -
- -
- - -
No recordings found!
-
- } - subtext="Please try changing your search parameters." - // animatedIcon="no-results" - show={ list.size === 0} - > - { list.take(displayedCount).map(session => ( - - ))} - - - -
- ) -} - -export default connect(state => ({ - list: state.getIn(['funnels', 'sessions']), - sessionsTotal: state.getIn(['funnels', 'sessionsTotal']), - funnel: state.getIn(['funnels', 'instance']), - activeStages: state.getIn(['funnels', 'activeStages']).toJS(), - liveFilters: state.getIn(['funnelFilters', 'appliedFilter']), - funnelFilters: state.getIn(['funnels', 'funnelFilters']), - sessionsSort: state.getIn(['funnels', 'sessionsSort']), -}), { fetchSessions, fetchSessionsFiltered, setFunnelPage })(FunnelSessionList) diff --git a/frontend/app/components/Funnels/FunnelSessionList/index.js b/frontend/app/components/Funnels/FunnelSessionList/index.js deleted file mode 100644 index 45c8e472e..000000000 --- a/frontend/app/components/Funnels/FunnelSessionList/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './FunnelSessionList' \ No newline at end of file diff --git a/frontend/app/components/Funnels/FunnelSessionsHeader/DateRange.js b/frontend/app/components/Funnels/FunnelSessionsHeader/DateRange.js deleted file mode 100644 index c532f5af6..000000000 --- a/frontend/app/components/Funnels/FunnelSessionsHeader/DateRange.js +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { applyFilter, fetchList } from 'Duck/filters'; -import { fetchList as fetchFunnelsList } from 'Duck/funnels'; -import DateRangeDropdown from 'Shared/DateRangeDropdown'; - -@connect(state => ({ - rangeValue: state.getIn([ 'filters', 'appliedFilter', 'rangeValue' ]), - startDate: state.getIn([ 'filters', 'appliedFilter', 'startDate' ]), - endDate: state.getIn([ 'filters', 'appliedFilter', 'endDate' ]), -}), { - applyFilter, fetchList, fetchFunnelsList -}) -export default class DateRange extends React.PureComponent { - render() { - const { startDate, endDate, rangeValue, className } = this.props; - return ( - - ); - } -} \ No newline at end of file diff --git a/frontend/app/components/Funnels/FunnelSessionsHeader/SortDropdown/SortDropdown.js b/frontend/app/components/Funnels/FunnelSessionsHeader/SortDropdown/SortDropdown.js deleted file mode 100644 index b865a6b57..000000000 --- a/frontend/app/components/Funnels/FunnelSessionsHeader/SortDropdown/SortDropdown.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import Select from 'Shared/Select'; -import { setSessionsSort as sort } from 'Duck/funnels'; -import { setSessionsSort } from 'Duck/funnels'; - -@connect(state => ({ - sessionsSort: state.getIn(['funnels','sessionsSort']) -}), { sort, setSessionsSort }) -export default class SortDropdown extends React.PureComponent { - state = { value: null } - sort = ({ value }) => { - this.setState({ value: value }) - const [ sort, order ] = value.split('-'); - const sign = order === 'desc' ? -1 : 1; - setTimeout(() => this.props.sort(sort, sign), 100); - } - - render() { - const { options, issuesSort } = this.props; - return ( - - -
- -
- ); - } -}; - -export default connect(state => ({ - loading: state.getIn(['assignments', 'addMessage', 'loading']) -}), { addMessage })(IssueCommentForm) diff --git a/frontend/app/components/Session_/Issues/IssueDescription.js b/frontend/app/components/Session_/Issues/IssueDescription.js deleted file mode 100644 index 9087df233..000000000 --- a/frontend/app/components/Session_/Issues/IssueDescription.js +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; -import ContentRender from './ContentRender'; - -const IssueDescription = ({ className, description, provider }) => { - return ( -
-
Description
- -
- ); -}; - -export default IssueDescription; diff --git a/frontend/app/components/Session_/Issues/IssueDetails.js b/frontend/app/components/Session_/Issues/IssueDetails.js deleted file mode 100644 index f2f8f5d0c..000000000 --- a/frontend/app/components/Session_/Issues/IssueDetails.js +++ /dev/null @@ -1,57 +0,0 @@ -import { connect } from 'react-redux'; -import React from 'react'; -import cn from 'classnames'; -import { Loader } from 'UI'; -import IssueHeader from './IssueHeader'; -import IssueCommentForm from './IssueCommentForm'; -import IssueComment from './IssueComment'; -import stl from './issueDetails.module.css'; -import IssueDescription from './IssueDescription'; - -class IssueDetails extends React.PureComponent { - state = { searchQuery: ''} - - write = (e, { name, value }) => this.setState({ [ name ]: value }); - - render() { - const { sessionId, issue, loading, users, issueTypeIcons, issuesIntegration } = this.props; - const activities = issue.activities; - const provider = issuesIntegration.provider; - const assignee = users.filter(({id}) => issue.assignee === id).first(); - - return ( -
- -
- -
- - - { activities.size > 0 &&
Comments
} - { activities.map(activity => ( - - ))} -
- -
-
-
- ); - } -} - -export default connect(state => ({ - users: state.getIn(['assignments', 'users']), - loading: state.getIn(['assignments', 'fetchAssignment', 'loading']), - issueTypeIcons: state.getIn(['assignments', 'issueTypeIcons']), - issuesIntegration: state.getIn([ 'issues', 'list'])[0] || {}, -}))(IssueDetails); diff --git a/frontend/app/components/Session_/Issues/IssueForm.js b/frontend/app/components/Session_/Issues/IssueForm.js index 248fd478c..f83e6b79a 100644 --- a/frontend/app/components/Session_/Issues/IssueForm.js +++ b/frontend/app/components/Session_/Issues/IssueForm.js @@ -1,182 +1,178 @@ +import { observer } from 'mobx-react-lite'; import React from 'react'; -import { connect } from 'react-redux'; -import { Form, Input, Button, CircularLoader, Loader } from 'UI'; -import { addActivity, init, edit, fetchAssignments, fetchMeta } from 'Duck/assignments'; + +import { useStore } from 'App/mstore'; +import { Button, CircularLoader, Form, Input, Loader } from 'UI'; + import Select from 'Shared/Select'; const SelectedValue = ({ icon, text }) => { return (
- {/* */} {icon} {text}
); }; -class IssueForm extends React.PureComponent { - componentDidMount() { - const { projects, issueTypes } = this.props; - this.props.init({ +function IssueForm(props) { + const { closeHandler } = props; + const { issueReportingStore } = useStore(); + const creating = issueReportingStore.createLoading; + const projects = issueReportingStore.projects; + const projectsLoading = issueReportingStore.projectsLoading; + const users = issueReportingStore.users; + const instance = issueReportingStore.instance; + const metaLoading = issueReportingStore.metaLoading; + const issueTypes = issueReportingStore.issueTypes; + const addActivity = issueReportingStore.saveIssue; + const init = issueReportingStore.init; + const edit = issueReportingStore.editInstance; + const fetchMeta = issueReportingStore.fetchMeta; + + React.useEffect(() => { + init({ projectId: projects[0] ? projects[0].id : '', issueType: issueTypes[0] ? issueTypes[0].id : '', }); - } + }, []); - componentWillReceiveProps(newProps) { - const { instance } = this.props; - if (newProps.instance.projectId && newProps.instance.projectId != instance.projectId) { - this.props.fetchMeta(newProps.instance.projectId).then(() => { - this.props.edit({ issueType: '', assignee: '', projectId: newProps.instance.projectId }); + React.useEffect(() => { + if (instance?.projectId) { + fetchMeta(instance?.projectId).then(() => { + edit({ + issueType: '', + assignee: '', + projectId: instance?.projectId, + }); }); } - } + }, [instance?.projectId]); - onSubmit = () => { - const { sessionId, addActivity } = this.props; - const { instance } = this.props; - - addActivity(sessionId, instance.toJS()).then(() => { - const { errors } = this.props; + const onSubmit = () => { + const { sessionId } = props; + addActivity(sessionId, instance).then(() => { + const { errors } = props; if (!errors || errors.length === 0) { - this.props.init({ projectId: instance.projectId }); - this.props.fetchAssignments(sessionId); - this.props.closeHandler(); + init({ projectId: instance?.projectId }); + void issueReportingStore.fetchList(sessionId); + closeHandler(); } }); }; - write = (e) => { + const write = (e) => { const { target: { name, value }, } = e; - this.props.edit({ [name]: value }); + edit({ [name]: value }); }; - writeOption = ({ name, value }) => this.props.edit({ [name]: value.value }); - render() { - const { - creating, - projects, - users, - issueTypes, - instance, - closeHandler, - metaLoading, - projectsLoading, - } = this.props; - const projectOptions = projects.map(({ name, id }) => ({ label: name, value: id })).toArray(); - const userOptions = users.map(({ name, id }) => ({ label: name, value: id })).toArray(); + const writeOption = ({ name, value }) => edit({ [name]: value.value }); + const projectOptions = projects.map(({ name, id }) => ({ + label: name, + value: id, + })); + const userOptions = users.map(({ name, id }) => ({ label: name, value: id })); - const issueTypeOptions = issueTypes.map(({ name, id, iconUrl, color }) => { - return { label: name, value: id, iconUrl, color }; - }); + const issueTypeOptions = issueTypes.map(({ name, id, iconUrl, color }) => { + return { label: name, value: id, iconUrl, color }; + }); - const selectedIssueType = issueTypes.filter((issue) => issue.id == instance.issueType)[0]; + const selectedIssueType = issueTypes.filter( + (issue) => issue.id == instance?.issueType + )[0]; - return ( - -
- - - - ) : ( - '' - ) - } - /> - + return ( + + + + + + ) : ( + '' + ) + } + /> + - - - + - - - - + + + + - - -