From 5a011692f86e5fb3a4d41273c567b3071b8fd113 Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Tue, 17 Sep 2024 16:52:56 +0200 Subject: [PATCH 01/12] refactoring integrations reducers etc WIP --- .../BugsnagForm/ProjectListDropdown.js | 15 +- .../CloudwatchForm/CloudwatchForm.js | 44 ++- .../Client/Integrations/ElasticsearchForm.js | 145 ++++----- .../Client/Integrations/IntegrationForm.js | 142 --------- .../Client/Integrations/IntegrationForm.tsx | 110 +++++++ .../Client/Integrations/Integrations.tsx | 298 ++++++++++-------- .../Client/Integrations/MobxDoc/MobxDoc.js | 12 +- .../Integrations/SlackAddForm/SlackAddForm.js | 158 +++++----- .../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 +- .../app/components/Session/LiveSession.js | 8 +- .../app/components/Session/MobilePlayer.tsx | 5 +- frontend/app/components/Session/Session.tsx | 2 - frontend/app/components/Session/WebPlayer.tsx | 10 +- .../Player/Controls/components/CreateNote.tsx | 33 +- .../shared/SharePopup/SharePopup.tsx | 46 +-- frontend/app/duck/integrations/teams.js | 2 - frontend/app/mstore/index.tsx | 3 + frontend/app/mstore/integrationsStore.ts | 256 ++++++++++++--- .../app/mstore/types/integrations/consts.ts | 37 +++ .../mstore/types/integrations/messengers.ts | 41 +++ .../services.ts} | 155 ++++++--- frontend/app/services/IntegrationsService.ts | 41 ++- 26 files changed, 1079 insertions(+), 822 deletions(-) delete mode 100644 frontend/app/components/Client/Integrations/IntegrationForm.js create mode 100644 frontend/app/components/Client/Integrations/IntegrationForm.tsx create mode 100644 frontend/app/mstore/types/integrations/consts.ts create mode 100644 frontend/app/mstore/types/integrations/messengers.ts rename frontend/app/mstore/types/{integrations.ts => integrations/services.ts} (73%) diff --git a/frontend/app/components/Client/Integrations/BugsnagForm/ProjectListDropdown.js b/frontend/app/components/Client/Integrations/BugsnagForm/ProjectListDropdown.js index c30b57953..8fb9d5df2 100644 --- a/frontend/app/components/Client/Integrations/BugsnagForm/ProjectListDropdown.js +++ b/frontend/app/components/Client/Integrations/BugsnagForm/ProjectListDropdown.js @@ -1,17 +1,16 @@ 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'; @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 +34,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/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/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..905b41d70 --- /dev/null +++ b/frontend/app/components/Client/Integrations/IntegrationForm.tsx @@ -0,0 +1,110 @@ +import { observer } from 'mobx-react-lite'; +import React from 'react'; +import { connect } from 'react-redux'; + +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 } = useStore(); + 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(props.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 connect((state: any) => ({ + sites: state.getIn(['site', 'list']), + initialSiteId: state.getIn(['site', 'siteId']), +}))(observer(IntegrationForm)); diff --git a/frontend/app/components/Client/Integrations/Integrations.tsx b/frontend/app/components/Client/Integrations/Integrations.tsx index e81a7038a..31cd2e46d 100644 --- a/frontend/app/components/Client/Integrations/Integrations.tsx +++ b/frontend/app/components/Client/Integrations/Integrations.tsx @@ -1,88 +1,95 @@ +import withPageTitle from 'HOCs/withPageTitle'; +import cn from 'classnames'; +import { observer } from 'mobx-react-lite'; import React, { useEffect, useState } from 'react'; import { connect } from 'react-redux'; + import { useModal } from 'App/components/Modal'; -import cn from 'classnames'; +import { useStore } from 'App/mstore'; +import IntegrationFilters from 'Components/Client/Integrations/IntegrationFilters'; +import { PageTitle } from 'UI'; -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 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 } = useStore(); + + const fetchIntegrationList = integrationsStore.integrations.fetchIntegrations; + const storeIntegratedList = integrationsStore.integrations.list; + const { siteId, 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 +106,91 @@ 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 + ); 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 connect((state: any) => ({ + siteId: state.getIn(['site', 'siteId']), +}))( + 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..fb1c1ec3a 100644 --- a/frontend/app/components/Client/Integrations/MobxDoc/MobxDoc.js +++ b/frontend/app/components/Client/Integrations/MobxDoc/MobxDoc.js @@ -3,9 +3,14 @@ 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 { integrationsStore } = useStore(); + const sites = props.sites ? props.sites.toJS() : [] + 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'; @@ -68,9 +73,8 @@ 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'), + sites, }; -})(MobxDoc); +})(observer(MobxDoc)) diff --git a/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js b/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js index 26cfc9520..60edc363d 100644 --- a/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js +++ b/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js @@ -1,25 +1,36 @@ 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 +38,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/Session/LiveSession.js b/frontend/app/components/Session/LiveSession.js index a9bba11b9..eec861e04 100644 --- a/frontend/app/components/Session/LiveSession.js +++ b/frontend/app/components/Session/LiveSession.js @@ -3,28 +3,29 @@ import { useEffect } from 'react'; import { connect } from 'react-redux'; import usePageTitle from 'App/hooks/usePageTitle'; import { fetch as fetchSession, clearCurrentSession } from 'Duck/sessions'; -import { fetchList as fetchSlackList } from 'Duck/integrations/slack'; import { Loader } from 'UI'; import withPermissions from 'HOCs/withPermissions'; import LivePlayer from './LivePlayer'; import { clearLogs } from 'App/dev/console'; import { toast } from 'react-toastify'; +import { useStore } from 'App/mstore' function LiveSession({ sessionId, fetchSession, - fetchSlackList, hasSessionsPath, session, fetchFailed, clearCurrentSession, }) { + const { integrationsStore } = useStore(); + const fetchSlackList = integrationsStore.slack.fetchIntegrations; const [initialLoading, setInitialLoading] = React.useState(true); usePageTitle('OpenReplay Assist'); useEffect(() => { clearLogs(); - fetchSlackList(); + void fetchSlackList(); return () => { clearCurrentSession() @@ -77,7 +78,6 @@ export default withPermissions(['ASSIST_LIVE', 'SERVICE_ASSIST_LIVE'], '', true, }, { fetchSession, - fetchSlackList, clearCurrentSession, } )(LiveSession) diff --git a/frontend/app/components/Session/MobilePlayer.tsx b/frontend/app/components/Session/MobilePlayer.tsx index 16f4a9178..c03efefa9 100644 --- a/frontend/app/components/Session/MobilePlayer.tsx +++ b/frontend/app/components/Session/MobilePlayer.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useState } from 'react'; import { connect } from 'react-redux'; import { Modal, Loader } from 'UI'; -import { fetchList } from 'Duck/integrations'; import { createIOSPlayer } from 'Player'; import { makeAutoObservable } from 'mobx'; import withLocationHandlers from 'HOCs/withLocationHandlers'; @@ -24,7 +23,7 @@ let playerInst: IOSPlayerContext['player'] | undefined; function MobilePlayer(props: any) { const { session, fetchList } = props; - const { notesStore, sessionStore, uiPlayerStore } = useStore(); + const { notesStore, sessionStore, uiPlayerStore, integrationsStore } = useStore(); const [activeTab, setActiveTab] = useState(''); const [noteItem, setNoteItem] = useState(undefined); // @ts-ignore @@ -37,7 +36,7 @@ function MobilePlayer(props: any) { useEffect(() => { playerInst = undefined; if (!session.sessionId || contextValue.player !== undefined) return; - fetchList('issues'); + void integrationsStore.issues.fetchIntegrations(); sessionStore.setUserTimezone(session.timezone); const [IOSPlayerInst, PlayerStore] = createIOSPlayer( session, diff --git a/frontend/app/components/Session/Session.tsx b/frontend/app/components/Session/Session.tsx index 026ae0c7b..02ab206a5 100644 --- a/frontend/app/components/Session/Session.tsx +++ b/frontend/app/components/Session/Session.tsx @@ -8,7 +8,6 @@ import usePageTitle from 'App/hooks/usePageTitle'; import { useStore } from 'App/mstore'; import { sessions as sessionsRoute } from 'App/routes'; import MobilePlayer from 'Components/Session/MobilePlayer'; -import { fetchList as fetchSlackList } from 'Duck/integrations/slack'; import { clearCurrentSession, fetchV2 } from 'Duck/sessions'; import { Link, Loader, NoContent } from 'UI'; @@ -89,7 +88,6 @@ export default withPermissions( }; }, { - fetchSlackList, fetchV2, clearCurrentSession, } diff --git a/frontend/app/components/Session/WebPlayer.tsx b/frontend/app/components/Session/WebPlayer.tsx index 93970d515..8ccd9f49e 100644 --- a/frontend/app/components/Session/WebPlayer.tsx +++ b/frontend/app/components/Session/WebPlayer.tsx @@ -9,7 +9,6 @@ import { toast } from 'react-toastify'; import { useStore } from 'App/mstore'; import { Note } from 'App/services/NotesService'; -import { fetchList } from 'Duck/integrations'; import { Loader, Modal } from 'UI'; import ReadNote from '../Session_/Player/Controls/components/ReadNote'; @@ -36,10 +35,9 @@ let playerInst: IPlayerContext['player'] | undefined; function WebPlayer(props: any) { const { session, - fetchList, startedAt, } = props; - const { notesStore, sessionStore, uxtestingStore, uiPlayerStore } = useStore(); + const { notesStore, sessionStore, uxtestingStore, uiPlayerStore, integrationsStore } = useStore(); const fullscreen = uiPlayerStore.fullscreen; const toggleFullscreen = uiPlayerStore.toggleFullscreen; const closeBottomBlock = uiPlayerStore.closeBottomBlock; @@ -72,7 +70,7 @@ function WebPlayer(props: any) { | Record | undefined; const usePrefetched = props.prefetched && mobData?.data; - fetchList('issues'); + void integrationsStore.issues.fetchIntegrations(); sessionStore.setUserTimezone(session.timezone); const [WebPlayerInst, PlayerStore] = createWebPlayer( session, @@ -256,7 +254,5 @@ export default connect( jwt: state.getIn(['user', 'jwt']), startedAt: state.getIn(['sessions', 'current']).startedAt || 0, }), - { - fetchList, - } + )(withLocationHandlers()(observer(WebPlayer))); diff --git a/frontend/app/components/Session_/Player/Controls/components/CreateNote.tsx b/frontend/app/components/Session_/Player/Controls/components/CreateNote.tsx index 3da4a31d7..4d4eb49b6 100644 --- a/frontend/app/components/Session_/Player/Controls/components/CreateNote.tsx +++ b/frontend/app/components/Session_/Player/Controls/components/CreateNote.tsx @@ -1,10 +1,9 @@ import { Tag } from 'antd'; -import { List } from 'immutable'; import { Duration } from 'luxon'; import React from 'react'; import { connect } from 'react-redux'; import { toast } from 'react-toastify'; - +import { observer } from 'mobx-react-lite'; import { useStore } from 'App/mstore'; import { Note, @@ -13,8 +12,6 @@ import { iTag, tagProps, } from 'App/services/NotesService'; -import { fetchList as fetchSlack } from 'Duck/integrations/slack'; -import { fetchList as fetchTeams } from 'Duck/integrations/teams'; import { addNote, updateNote } from 'Duck/sessions'; import { Button, Checkbox, Icon } from 'UI'; @@ -27,10 +24,6 @@ interface Props { sessionId: string; isEdit?: boolean; editNote?: WriteNote; - slackChannels: List>; - teamsChannels: List>; - fetchSlack: () => void; - fetchTeams: () => void; hideModal: () => void; } @@ -40,12 +33,13 @@ function CreateNote({ isEdit, editNote, updateNote, - slackChannels, - fetchSlack, - teamsChannels, - fetchTeams, hideModal, }: Props) { + const { notesStore, integrationsStore } = useStore(); + const slackChannels = integrationsStore.slack.list; + const fetchSlack = integrationsStore.slack.fetchIntegrations; + const teamsChannels = integrationsStore.msteams.list; + const fetchTeams = integrationsStore.msteams.fetchIntegrations; const [text, setText] = React.useState(''); const [slackChannel, setSlackChannel] = React.useState(''); const [teamsChannel, setTeamsChannel] = React.useState(''); @@ -56,7 +50,6 @@ function CreateNote({ const [useTeams, setTeams] = React.useState(false); const inputRef = React.createRef(); - const { notesStore } = useStore(); React.useEffect(() => { if (isEdit && editNote) { @@ -151,14 +144,12 @@ function CreateNote({ .map(({ webhookId, name }) => ({ value: webhookId, label: name, - })) - .toJS() as unknown as { value: string; label: string }[]; + })) as unknown as { value: string; label: string }[]; const teamsChannelsOptions = teamsChannels .map(({ webhookId, name }) => ({ value: webhookId, label: name, - })) - .toJS() as unknown as { value: string; label: string }[]; + })) as unknown as { value: string; label: string }[]; slackChannelsOptions.unshift({ // @ts-ignore @@ -334,10 +325,8 @@ function CreateNote({ export default connect( (state: any) => { - const slackChannels = state.getIn(['slack', 'list']); - const teamsChannels = state.getIn(['teams', 'list']); const sessionId = state.getIn(['sessions', 'current']).sessionId; - return { sessionId, slackChannels, teamsChannels }; + return { sessionId }; }, - { addNote, updateNote, fetchSlack, fetchTeams } -)(CreateNote); + { addNote, updateNote,} +)(observer(CreateNote)); diff --git a/frontend/app/components/shared/SharePopup/SharePopup.tsx b/frontend/app/components/shared/SharePopup/SharePopup.tsx index ce7955924..66b61ef81 100644 --- a/frontend/app/components/shared/SharePopup/SharePopup.tsx +++ b/frontend/app/components/shared/SharePopup/SharePopup.tsx @@ -7,11 +7,10 @@ import styles from './sharePopup.module.css'; import IntegrateSlackButton from '../IntegrateSlackButton/IntegrateSlackButton'; import SessionCopyLink from './SessionCopyLink'; import Select from 'Shared/Select'; -import { fetchList as fetchSlack, sendSlackMsg } from 'Duck/integrations/slack'; -import { fetchList as fetchTeams, sendMsTeamsMsg } from 'Duck/integrations/teams'; import { Button, Segmented } from 'antd'; import { PlayerContext } from 'App/components/Session/playerContext'; import { observer } from 'mobx-react-lite'; +import { useStore } from 'App/mstore'; interface Msg { integrationId: string; @@ -51,15 +50,7 @@ const SharePopup = ({ interface Props { sessionId: string; - channels: { webhookId: string; name: string }[]; - slackLoaded: boolean; - msTeamsChannels: { webhookId: string; name: string }[]; - msTeamsLoaded: boolean; tenantId: string; - fetchSlack: () => void; - fetchTeams: () => void; - sendSlackMsg: (msg: Msg) => any; - sendMsTeamsMsg: (msg: Msg) => any; showCopyLink?: boolean; hideModal: () => void; time: number; @@ -67,18 +58,20 @@ interface Props { function ShareModalComp({ sessionId, - sendSlackMsg, - sendMsTeamsMsg, showCopyLink, - channels, - slackLoaded, - msTeamsChannels, - msTeamsLoaded, - fetchSlack, - fetchTeams, hideModal, time, }: Props) { + const { integrationsStore } = useStore(); + const channels = integrationsStore.slack.list; + const slackLoaded = integrationsStore.slack.loaded; + const msTeamsChannels = integrationsStore.msteams.list; + const msTeamsLoaded = integrationsStore.msteams.loaded; + const fetchSlack = integrationsStore.slack.fetchIntegrations; + const fetchTeams = integrationsStore.msteams.fetchIntegrations; + const sendSlackMsg = integrationsStore.slack.sendMessage; + const sendMsTeamsMsg = integrationsStore.msteams.sendMessage; + const [shareTo, setShareTo] = useState('slack'); const [comment, setComment] = useState(''); // @ts-ignore @@ -104,7 +97,7 @@ function ShareModalComp({ const editMessage = (e: React.ChangeEvent) => setComment(e.target.value); const shareToSlack = () => { setLoadingSlack(true); - sendSlackMsg({ + void sendSlackMsg({ integrationId: channelId, entity: 'sessions', entityId: sessionId, @@ -140,16 +133,12 @@ function ShareModalComp({ value: webhookId, label: name, })) - // @ts-ignore - .toJS(); const msTeamsOptions = msTeamsChannels .map(({ webhookId, name }) => ({ value: webhookId, label: name, })) - // @ts-ignore - .toJS(); const sendMsg = () => { if (shareTo === 'slack') { @@ -279,18 +268,9 @@ function ShareModalComp({ const mapStateToProps = (state: Record) => ({ sessionId: state.getIn(['sessions', 'current']).sessionId, - channels: state.getIn(['slack', 'list']), - slackLoaded: state.getIn(['slack', 'loaded']), - msTeamsChannels: state.getIn(['teams', 'list']), - msTeamsLoaded: state.getIn(['teams', 'loaded']), tenantId: state.getIn(['user', 'account', 'tenantId']), }); -const ShareModal = connect(mapStateToProps, { - fetchSlack, - fetchTeams, - sendSlackMsg, - sendMsTeamsMsg, -})(ShareModalComp); +const ShareModal = connect(mapStateToProps)(ShareModalComp); export default observer(SharePopup); diff --git a/frontend/app/duck/integrations/teams.js b/frontend/app/duck/integrations/teams.js index e77835a37..29fac4710 100644 --- a/frontend/app/duck/integrations/teams.js +++ b/frontend/app/duck/integrations/teams.js @@ -93,8 +93,6 @@ export function remove(id) { }; } -// https://api.openreplay.com/5587/integrations/msteams/notify/315/sessions/7856803626558104 -// export function sendMsTeamsMsg({ integrationId, entity, entityId, data }) { return { types: SEND_MSG.toArray(), diff --git a/frontend/app/mstore/index.tsx b/frontend/app/mstore/index.tsx index 723655adb..b127fa585 100644 --- a/frontend/app/mstore/index.tsx +++ b/frontend/app/mstore/index.tsx @@ -27,6 +27,7 @@ import FilterStore from './filterStore'; import UiPlayerStore from './uiPlayerStore'; import IssueReportingStore from './issueReportingStore'; import CustomFieldStore from './customFieldStore'; +import { IntegrationsStore } from "./integrationsStore"; export class RootStore { dashboardStore: DashboardStore; @@ -55,6 +56,7 @@ export class RootStore { uiPlayerStore: UiPlayerStore; issueReportingStore: IssueReportingStore; customFieldStore: CustomFieldStore; + integrationsStore: IntegrationsStore constructor() { this.dashboardStore = new DashboardStore(); @@ -83,6 +85,7 @@ export class RootStore { this.uiPlayerStore = new UiPlayerStore(); this.issueReportingStore = new IssueReportingStore(); this.customFieldStore = new CustomFieldStore(); + this.integrationsStore = new IntegrationsStore(); } initClient() { diff --git a/frontend/app/mstore/integrationsStore.ts b/frontend/app/mstore/integrationsStore.ts index e10e23158..fd3b7fc8c 100644 --- a/frontend/app/mstore/integrationsStore.ts +++ b/frontend/app/mstore/integrationsStore.ts @@ -1,7 +1,9 @@ import { makeAutoObservable } from 'mobx'; import { integrationsService } from 'App/services'; +import ElasticsearchForm from "../components/Client/Integrations/ElasticsearchForm"; +import { MessengerConfig } from './types/integrations/messengers'; import { Bugsnag, Cloudwatch, @@ -16,11 +18,12 @@ import { SentryInt, StackDriverInt, SumoLogic, -} from './types/integrations'; +} from './types/integrations/services'; class GenericIntegrationsStore { list: any[] = []; - siteId: string | null = null; + isLoading: boolean = false; + siteId: string = ''; constructor() { makeAutoObservable(this); } @@ -33,9 +36,20 @@ class GenericIntegrationsStore { this.list = list; } - fetchIntegrations = async () => { - //client.get(`/${siteID}/integrations`) - // this.setList() + setLoading(loading: boolean) { + this.isLoading = loading; + } + + fetchIntegrations = async (siteId?: string) => { + this.setLoading(true); + try { + const { data } = await integrationsService.fetchList(siteId); + this.setList(data); + } catch (e) { + console.log(e); + } finally { + this.setLoading(false); + } }; } @@ -44,83 +58,235 @@ class NamedIntegrationStore { list: T[] = []; fetched: boolean = false; issuesFetched: boolean = false; + loading = false; constructor( private readonly name: string, - private readonly NamedType: new (config: Record) => T + private readonly namedTypeCreator: (config: Record) => T ) { + this.instance = namedTypeCreator({}); makeAutoObservable(this); } + setLoading(loading: boolean): void { + this.loading = loading; + } + setInstance(instance: T): void { this.instance = instance; } - setList(list: T[]): void { + setList = (list: T[]): void => { this.list = list; } - setFetched(fetched: boolean): void { + setFetched = (fetched: boolean): void => { this.fetched = fetched; } - setIssuesFetched(issuesFetched: boolean): void { + setIssuesFetched = (issuesFetched: boolean): void => { this.issuesFetched = issuesFetched; } fetchIntegrations = async (): Promise => { - const { data } = await integrationsService.fetchList(this.name); - this.setList( - data.map((config: Record) => new this.NamedType(config)) - ); + this.setLoading(true); + try { + const { data } = await integrationsService.fetchList(this.name); + this.setList( + data.map((config: Record) => this.namedTypeCreator(config)) + ); + } catch (e) { + console.log(e); + } finally { + this.setFetched(true); + this.setLoading(false); + } }; - fetchIntegration = async (siteId: string): void => { - const { data } = await integrationsService.fetchIntegration( - this.name, - siteId - ); - this.setInstance(new this.NamedType(data)); + fetchIntegration = async (siteId: string): Promise => { + this.setLoading(true); + try { + const { data } = await integrationsService.fetchIntegration( + this.name, + siteId + ); + this.setInstance(this.namedTypeCreator(data)); + } catch (e) { + console.log(e); + } finally { + this.setLoading(false); + } }; - saveIntegration(name: string, siteId: string): void { + saveIntegration = async (name: string, siteId?: string): Promise => { if (!this.instance) return; - const response = integrationsService.saveIntegration( - name, - siteId, - this.instance.toData() + await integrationsService.saveIntegration( + this.name ?? name, + this.instance.toData(), + siteId ); return; } - edit(data: T): void { - this.setInstance(data); + edit = (data: T): void => { + if (!this.instance) { + this.instance = this.namedTypeCreator({}); + } + this.instance.edit(data); } - deleteIntegration(siteId: string) { + deleteIntegration = async (siteId?: string) => { if (!this.instance) return; return integrationsService.removeIntegration(this.name, siteId); } - init(config: Record): void { - this.instance = new this.NamedType(config); + init = (config: Record): void => { + this.instance = this.namedTypeCreator(config); } } -export class IntegrationsStore { - sentry = new NamedIntegrationStore('sentry', SentryInt); - datadog = new NamedIntegrationStore('datadog', DatadogInt); - stackdriver = new NamedIntegrationStore('stackdriver', StackDriverInt); - rollbar = new NamedIntegrationStore('rollbar', RollbarInt); - newrelic = new NamedIntegrationStore('newrelic', NewRelicInt); - bugsnag = new NamedIntegrationStore('bugsnag', Bugsnag); - cloudwatch = new NamedIntegrationStore('cloudwatch', Cloudwatch); - elasticsearch = new NamedIntegrationStore('elasticsearch', ElasticSearchInt); - sumologic = new NamedIntegrationStore('sumologic', SumoLogic); - jira = new NamedIntegrationStore('jira', JiraInt); - github = new NamedIntegrationStore('github', GithubInt); - issues = new NamedIntegrationStore('issues', IssueTracker); - integrations = new GenericIntegrationsStore(); - // + slack - // + teams +class MessengerIntegrationStore { + list: MessengerConfig[] = []; + instance: MessengerConfig | null = null; + loaded: boolean = false; + loading: boolean = false; + errors: any[] = []; + + constructor(private readonly mName: 'slack' | 'msteams') { + makeAutoObservable(this); + } + + setList(list: MessengerConfig[]): void { + this.list = list; + } + + setLoading(loading: boolean): void { + this.loading = loading; + } + + setInstance(instance: MessengerConfig): void { + this.instance = instance; + } + + setLoaded(loaded: boolean): void { + this.loaded = loaded; + } + + setErrors = (errors: any[]) => { + this.errors = errors; + }; + + saveIntegration = async (): Promise => { + // redux todo: errors + if (!this.instance) return; + this.setLoading(true); + try { + await integrationsService.saveIntegration( + this.mName, + this.instance.toData(), + undefined + ); + this.setList([...this.list, this.instance]); + } catch (e) { + console.log(e); + this.setErrors(["Couldn't process the request: check your data."]); + } finally { + this.setLoading(false); + } + }; + + fetchIntegrations = async (): Promise => { + const { data } = await integrationsService.fetchMessengerChannels( + this.mName + ); + this.setList( + data.map((config: Record) => new MessengerConfig(config)) + ); + this.setLoaded(true); + }; + + sendMessage = ({ + integrationId, + entity, + entityId, + data, + }: { + integrationId: string; + entity: string; + entityId: string; + data: any; + }) => { + return integrationsService.sendMsg( + integrationId, + entity, + entityId, + this.mName, + data + ); + }; + + init = (config: Record): void => { + this.instance = new MessengerConfig(config); + }; + + removeInt = async (intId: string) => { + await integrationsService.removeMessengerInt(this.mName, intId); + this.setList(this.list.filter((int) => int.webhookId !== intId)); + }; + + edit = (data: Record): void => { + if (!this.instance) { + this.instance = new MessengerConfig({}); + } + this.instance.edit(data); + }; + + update = async () => { + // redux todo: errors + if (!this.instance) return; + this.setLoading(true); + await integrationsService.updateMessengerInt( + this.mName, + this.instance.toData() + ); + this.setList( + this.list.map((int) => + int.webhookId === this.instance?.webhookId ? this.instance : int + ) + ); + this.setLoading(false); + }; +} +export type namedStore = 'sentry' + | 'datadog' + | 'stackdriver' + | 'rollbar' + | 'newrelic' + | 'bugsnag' + | 'cloudwatch' + | 'elasticsearch' + | 'sumologic' + | 'jira' + | 'github' + | 'issues' +export class IntegrationsStore { + sentry = new NamedIntegrationStore('sentry', (d) => new SentryInt(d)); + datadog = new NamedIntegrationStore('datadog', (d) => new DatadogInt(d)); + stackdriver = new NamedIntegrationStore('stackdriver', (d) => new StackDriverInt(d)); + rollbar = new NamedIntegrationStore('rollbar', (d) => new RollbarInt(d)); + newrelic = new NamedIntegrationStore('newrelic', (d) => new NewRelicInt(d)); + bugsnag = new NamedIntegrationStore('bugsnag', (d) => new Bugsnag(d)); + cloudwatch = new NamedIntegrationStore('cloudwatch', (d) => new Cloudwatch(d)); + elasticsearch = new NamedIntegrationStore('elasticsearch', (d) => new ElasticSearchInt(d)); + sumologic = new NamedIntegrationStore('sumologic', (d) => new SumoLogic(d)); + jira = new NamedIntegrationStore('jira', (d) => new JiraInt(d)); + github = new NamedIntegrationStore('github', (d) => new GithubInt(d)); + issues = new NamedIntegrationStore('issues', (d) => new IssueTracker(d)); + integrations = new GenericIntegrationsStore(); + slack = new MessengerIntegrationStore('slack'); + msteams = new MessengerIntegrationStore('msteams'); + + constructor() { + makeAutoObservable(this); + } } diff --git a/frontend/app/mstore/types/integrations/consts.ts b/frontend/app/mstore/types/integrations/consts.ts new file mode 100644 index 000000000..6e9561657 --- /dev/null +++ b/frontend/app/mstore/types/integrations/consts.ts @@ -0,0 +1,37 @@ +export const sumoRegionLabels = { + au: 'Asia Pacific (Sydney)', + ca: 'Canada (Central)', + de: 'EU (Frankfurt)', + eu: 'EU (Ireland)', + fed: 'US East (N. Virginia)', + in: 'Asia Pacific (Mumbai)', + jp: 'Asia Pacific (Tokyo)', + us1: 'US East (N. Virginia)', + us2: 'US West (Oregon)', +}; +export const API_KEY_ID_LENGTH = 5; +export const API_KEY_LENGTH = 5; +export const SECRET_ACCESS_KEY_LENGTH = 40; +export const ACCESS_KEY_ID_LENGTH = 20; +export const tokenRE = + /^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$/i; +export const awsRegionLabels = { + 'us-east-1': 'US East (N. Virginia)', + 'us-east-2': 'US East (Ohio)', + 'us-west-1': 'US West (N. California)', + 'us-west-2': 'US West (Oregon)', + 'ap-east-1': 'Asia Pacific (Hong Kong)', + 'ap-south-1': 'Asia Pacific (Mumbai)', + 'ap-northeast-2': 'Asia Pacific (Seoul)', + 'ap-southeast-1': 'Asia Pacific (Singapore)', + 'ap-southeast-2': 'Asia Pacific (Sydney)', + 'ap-northeast-1': 'Asia Pacific (Tokyo)', + 'ca-central-1': 'Canada (Central)', + 'eu-central-1': 'EU (Frankfurt)', + 'eu-west-1': 'EU (Ireland)', + 'eu-west-2': 'EU (London)', + 'eu-west-3': 'EU (Paris)', + 'eu-north-1': 'EU (Stockholm)', + 'me-south-1': 'Middle East (Bahrain)', + 'sa-east-1': 'South America (São Paulo)', +}; diff --git a/frontend/app/mstore/types/integrations/messengers.ts b/frontend/app/mstore/types/integrations/messengers.ts new file mode 100644 index 000000000..1e6ae9300 --- /dev/null +++ b/frontend/app/mstore/types/integrations/messengers.ts @@ -0,0 +1,41 @@ +import { validateURL } from "App/validate"; +import { makeAutoObservable } from "mobx"; + +export class MessengerConfig { + endpoint: string = ""; + name: string = ""; + webhookId: string = ""; + + constructor(config: any) { + Object.assign(this, { + endpoint: config.endpoint, + name: config.name, + webhookId: config.webhookId + }); + makeAutoObservable(this); + } + + edit = (data: any): void => { + Object.keys(data).forEach((key) => { + // @ts-ignore + this[key] = data[key]; + }) + } + + validate(): boolean { + return this.endpoint !== '' && this.name != '' && validateURL(this.endpoint); + } + + exists(): boolean { + return !!this.webhookId; + } + + toData(): { endpoint: string, url: string, name: string, webhookId: string } { + return { + endpoint: this.endpoint, + url: this.endpoint, + name: this.name, + webhookId: this.webhookId + }; + } +} diff --git a/frontend/app/mstore/types/integrations.ts b/frontend/app/mstore/types/integrations/services.ts similarity index 73% rename from frontend/app/mstore/types/integrations.ts rename to frontend/app/mstore/types/integrations/services.ts index 3b3eec493..0acbdc687 100644 --- a/frontend/app/mstore/types/integrations.ts +++ b/frontend/app/mstore/types/integrations/services.ts @@ -2,10 +2,21 @@ import { makeAutoObservable } from 'mobx'; import { validateURL } from 'App/validate'; +import { + ACCESS_KEY_ID_LENGTH, + API_KEY_ID_LENGTH, + API_KEY_LENGTH, + SECRET_ACCESS_KEY_LENGTH, + awsRegionLabels, + sumoRegionLabels, + tokenRE, +} from './consts'; + export interface Integration { validate(): boolean; exists(): boolean; toData(): Record; + edit(data: Record): void; } export class SentryInt implements Integration { @@ -17,10 +28,17 @@ export class SentryInt implements Integration { constructor(config: any) { Object.assign(this, { ...config, - projectId: config.projectId || -1, + projectId: config?.projectId ?? -1, }); + makeAutoObservable(this); } + edit = (data: Record) => { + Object.keys(data).forEach((key) => { + this[key] = data[key]; + }); + }; + validate() { return Boolean(this.organizationSlug && this.projectSlug && this.token); } @@ -47,10 +65,17 @@ export class DatadogInt implements Integration { constructor(config: any) { Object.assign(this, { ...config, - projectId: config.projectId || -1, + projectId: config?.projectId ?? -1, }); + makeAutoObservable(this); } + edit = (data: Record) => { + Object.keys(data).forEach((key) => { + this[key] = data[key]; + }); + }; + validate() { return Boolean(this.apiKey && this.applicationKey); } @@ -76,10 +101,17 @@ export class StackDriverInt implements Integration { constructor(config: any) { Object.assign(this, { ...config, - projectId: config.projectId || -1, + projectId: config?.projectId ?? -1, }); + makeAutoObservable(this); } + edit = (data: Record) => { + Object.keys(data).forEach((key) => { + this[key] = data[key]; + }); + }; + validate() { return Boolean( this.serviceAccountCredentials !== '' && this.logName !== '' @@ -106,10 +138,17 @@ export class RollbarInt implements Integration { constructor(config: any) { Object.assign(this, { ...config, - projectId: config.projectId || -1, + projectId: config?.projectId ?? -1, }); + makeAutoObservable(this); } + edit = (data: Record) => { + Object.keys(data).forEach((key) => { + this[key] = data[key]; + }); + }; + validate() { return Boolean(this.accessToken); } @@ -135,10 +174,17 @@ export class NewRelicInt implements Integration { constructor(config: any) { Object.assign(this, { ...config, - projectId: config.projectId || -1, + projectId: config?.projectId ?? -1, }); + makeAutoObservable(this); } + edit = (data: Record) => { + Object.keys(data).forEach((key) => { + this[key] = data[key]; + }); + }; + validate() { return Boolean(this.applicationId && this.xQueryKey); } @@ -165,10 +211,17 @@ export class Bugsnag implements Integration { constructor(config: any) { Object.assign(this, { ...config, - projectId: config.projectId || -1, + projectId: config?.projectId ?? -1, }); + makeAutoObservable(this); } + edit = (data: Record) => { + Object.keys(data).forEach((key) => { + this[key] = data[key]; + }); + }; + validate() { return Boolean( this.bugsnagProjectId !== '' && tokenRE.test(this.authorizationToken) @@ -198,10 +251,17 @@ export class Cloudwatch implements Integration { constructor(config: any) { Object.assign(this, { ...config, - projectId: config.projectId || -1, + projectId: config?.projectId ?? -1, }); + makeAutoObservable(this); } + edit = (data: Record) => { + Object.keys(data).forEach((key) => { + this[key] = data[key]; + }); + }; + validate() { return Boolean( this.awsAccessKeyId !== '' && @@ -237,10 +297,17 @@ export class ElasticSearchInt implements Integration { constructor(config: any) { Object.assign(this, { ...config, - projectId: config.projectId || -1, + projectId: config?.projectId ?? -1, }); + makeAutoObservable(this); } + edit = (data: Record) => { + Object.keys(data).forEach((key) => { + this[key] = data[key]; + }); + }; + private validateKeys() { return Boolean( this.apiKeyId.length > API_KEY_ID_LENGTH && @@ -285,10 +352,17 @@ export class SumoLogic implements Integration { constructor(config: any) { Object.assign(this, { ...config, - projectId: config.projectId || -1, + projectId: config?.projectId ?? -1, }); + makeAutoObservable(this); } + edit = (data: Record) => { + Object.keys(data).forEach((key) => { + this[key] = data[key]; + }); + }; + validate() { return Boolean(this.accessKey && this.accessId); } @@ -316,10 +390,17 @@ export class JiraInt implements Integration { constructor(config: any) { Object.assign(this, { ...config, - projectId: config.projectId || -1, + projectId: config?.projectId ?? -1, }); + makeAutoObservable(this); } + edit = (data: Record) => { + Object.keys(data).forEach((key) => { + this[key] = data[key]; + }); + }; + validateFetchProjects() { return this.username !== '' && this.token !== '' && validateURL(this.url); } @@ -350,10 +431,17 @@ export class GithubInt implements Integration { constructor(config: any) { Object.assign(this, { ...config, - projectId: config.projectId || -1, + projectId: config?.projectId ?? -1, }); + makeAutoObservable(this); } + edit = (data: Record) => { + Object.keys(data).forEach((key) => { + this[key] = data[key]; + }); + }; + validate() { return this.token !== ''; } @@ -381,8 +469,15 @@ export class IssueTracker implements Integration { Object.assign(this, { ...config, }); + makeAutoObservable(this); } + edit = (data: Record) => { + Object.keys(data).forEach((key) => { + this[key] = data[key]; + }); + }; + validateFetchProjects() { return this.username !== '' && this.token !== '' && validateURL(this.url); } @@ -404,41 +499,3 @@ export class IssueTracker implements Integration { }; } } - -export const sumoRegionLabels = { - au: 'Asia Pacific (Sydney)', - ca: 'Canada (Central)', - de: 'EU (Frankfurt)', - eu: 'EU (Ireland)', - fed: 'US East (N. Virginia)', - in: 'Asia Pacific (Mumbai)', - jp: 'Asia Pacific (Tokyo)', - us1: 'US East (N. Virginia)', - us2: 'US West (Oregon)', -}; -export const API_KEY_ID_LENGTH = 5; -export const API_KEY_LENGTH = 5; -export const SECRET_ACCESS_KEY_LENGTH = 40; -export const ACCESS_KEY_ID_LENGTH = 20; -export const tokenRE = - /^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$/i; -export const awsRegionLabels = { - 'us-east-1': 'US East (N. Virginia)', - 'us-east-2': 'US East (Ohio)', - 'us-west-1': 'US West (N. California)', - 'us-west-2': 'US West (Oregon)', - 'ap-east-1': 'Asia Pacific (Hong Kong)', - 'ap-south-1': 'Asia Pacific (Mumbai)', - 'ap-northeast-2': 'Asia Pacific (Seoul)', - 'ap-southeast-1': 'Asia Pacific (Singapore)', - 'ap-southeast-2': 'Asia Pacific (Sydney)', - 'ap-northeast-1': 'Asia Pacific (Tokyo)', - 'ca-central-1': 'Canada (Central)', - 'eu-central-1': 'EU (Frankfurt)', - 'eu-west-1': 'EU (Ireland)', - 'eu-west-2': 'EU (London)', - 'eu-west-3': 'EU (Paris)', - 'eu-north-1': 'EU (Stockholm)', - 'me-south-1': 'Middle East (Bahrain)', - 'sa-east-1': 'South America (São Paulo)', -}; diff --git a/frontend/app/services/IntegrationsService.ts b/frontend/app/services/IntegrationsService.ts index d54f90aad..c39595a2a 100644 --- a/frontend/app/services/IntegrationsService.ts +++ b/frontend/app/services/IntegrationsService.ts @@ -1,8 +1,8 @@ import BaseService from "./BaseService"; export default class IntegrationsService extends BaseService { - fetchList = async (name: string) => { - const r = await this.client.get(`/integrations/${name}`) + fetchList = async (name?: string, siteId?: string) => { + const r = await this.client.get(`${siteId ? `/${siteId}` : ''}/integrations/${name}`) const data = await r.json() return data @@ -16,7 +16,7 @@ export default class IntegrationsService extends BaseService { return data } - saveIntegration = async (name: string, siteId: string, data: any) => { + saveIntegration = async (name: string, data: any, siteId?: string) => { const url = (siteId ? `/${siteId}` : '') + `/integrations/${name}` const r = await this.client.post(url, data) const res = await r.json() @@ -24,11 +24,40 @@ export default class IntegrationsService extends BaseService { return res } - removeIntegration = async (name: string, siteId: string) => { + removeIntegration = async (name: string, siteId?: string) => { const url = (siteId ? `/${siteId}` : '') + `/integrations/${name}` const r = await this.client.delete(url) - const res = await r.json() - return res + return await r.json() + } + + fetchMessengerChannels = async (name: string) => { + const r = await this.client.get(`/integrations/${name}/channels`) + + return await r.json() + } + + updateMessengerInt = async (name: string, data: any) => { + const r = await this.client.put(`/integrations/${name}/${data.webhookId}`, data) + + return await r.json() + } + + removeMessengerInt = async (name: string, webhookId: string) => { + const r = await this.client.delete(`/integrations/${name}/${webhookId}`) + + return await r.json() + } + + sendMsg = async (integrationId, entity, entityId, name, data) => { + const r = await this.client.post(`/integrations/${name}/notify/${integrationId}/${entity}/${entityId}`, data) + + return await r.json() + } + + testElastic = async (data: any) => { + const r = await this.client.post('/integrations/elasticsearch/test', data) + + return r.json(); } } From c94ca6fb88f13f34463693b54fb52e65a7f1bdac Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Wed, 18 Sep 2024 10:57:15 +0200 Subject: [PATCH 02/12] finish removing integrations state --- .../Integrations/AssistDoc/AssistDoc.js | 12 +- .../CloudwatchForm/LogGroupDropdown.js | 160 ++++++++++-------- .../Integrations/GraphQLDoc/GraphQLDoc.js | 12 +- .../Client/Integrations/NgRxDoc/NgRxDoc.js | 12 +- .../Client/Integrations/PiniaDoc/PiniaDoc.tsx | 54 +++--- .../Integrations/ProfilerDoc/ProfilerDoc.js | 15 +- .../Client/Integrations/ReduxDoc/ReduxDoc.js | 14 +- .../Client/Integrations/VueDoc/VueDoc.js | 14 +- .../Integrations/ZustandDoc/ZustandDoc.js | 14 +- frontend/app/duck/index.ts | 2 - 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 | 101 ----------- 16 files changed, 180 insertions(+), 507 deletions(-) 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 diff --git a/frontend/app/components/Client/Integrations/AssistDoc/AssistDoc.js b/frontend/app/components/Client/Integrations/AssistDoc/AssistDoc.js index dd94f90e6..7e75ed186 100644 --- a/frontend/app/components/Client/Integrations/AssistDoc/AssistDoc.js +++ b/frontend/app/components/Client/Integrations/AssistDoc/AssistDoc.js @@ -1,3 +1,4 @@ +import { useStore } from "App/mstore"; import React from 'react'; import DocLink from 'Shared/DocLink/DocLink'; import AssistScript from './AssistScript'; @@ -5,6 +6,7 @@ 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'; @@ -14,7 +16,10 @@ const TABS = [ ]; const AssistDoc = (props) => { - const { projectKey } = props; + const { integrationsStore } = useStore(); + const sites = props.sites ? props.sites.toJS() : [] + 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 = () => { @@ -54,9 +59,8 @@ 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'), + sites, }; -})(AssistDoc); +})(observer(AssistDoc)); 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/GraphQLDoc/GraphQLDoc.js b/frontend/app/components/Client/Integrations/GraphQLDoc/GraphQLDoc.js index 18c78d368..da06db940 100644 --- a/frontend/app/components/Client/Integrations/GraphQLDoc/GraphQLDoc.js +++ b/frontend/app/components/Client/Integrations/GraphQLDoc/GraphQLDoc.js @@ -1,11 +1,16 @@ +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 { integrationsStore } = useStore(); + const sites = props.sites ? props.sites.toJS() : [] + 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'; //... @@ -71,9 +76,8 @@ 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'), + sites, }; -})(GraphQLDoc); +})(observer(GraphQLDoc)); diff --git a/frontend/app/components/Client/Integrations/NgRxDoc/NgRxDoc.js b/frontend/app/components/Client/Integrations/NgRxDoc/NgRxDoc.js index 8097a4618..a06981550 100644 --- a/frontend/app/components/Client/Integrations/NgRxDoc/NgRxDoc.js +++ b/frontend/app/components/Client/Integrations/NgRxDoc/NgRxDoc.js @@ -1,11 +1,16 @@ +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 { integrationsStore } = useStore(); + const sites = props.sites ? props.sites.toJS() : [] + 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'; @@ -81,9 +86,8 @@ 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'), + sites, }; -})(NgRxDoc); +})(observer(NgRxDoc)); diff --git a/frontend/app/components/Client/Integrations/PiniaDoc/PiniaDoc.tsx b/frontend/app/components/Client/Integrations/PiniaDoc/PiniaDoc.tsx index a0a9c157e..4184b4293 100644 --- a/frontend/app/components/Client/Integrations/PiniaDoc/PiniaDoc.tsx +++ b/frontend/app/components/Client/Integrations/PiniaDoc/PiniaDoc.tsx @@ -1,11 +1,20 @@ +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'; +import { useStore } from 'App/mstore'; +import ToggleContent from 'Components/shared/ToggleContent'; +import { CodeBlock } from 'UI'; + +import DocLink from 'Shared/DocLink/DocLink'; + const PiniaDoc = (props) => { - const { projectKey } = props; + const { integrationsStore } = useStore(); + const sites = props.sites ? props.sites.toJS() : []; + 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 +37,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 +64,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'), + sites, }; -})(PiniaDoc); +})(observer(PiniaDoc)); diff --git a/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js b/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js index eb7ad3999..4788768d0 100644 --- a/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js +++ b/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js @@ -1,13 +1,17 @@ +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 { integrationsStore } = useStore(); + const sites = props.sites ? props.sites.toJS() : [] + 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'; @@ -88,11 +92,8 @@ 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'), + sites }; -})(ProfilerDoc); +})(observer(ProfilerDoc)); diff --git a/frontend/app/components/Client/Integrations/ReduxDoc/ReduxDoc.js b/frontend/app/components/Client/Integrations/ReduxDoc/ReduxDoc.js index 3566bb82d..b6b3a4b98 100644 --- a/frontend/app/components/Client/Integrations/ReduxDoc/ReduxDoc.js +++ b/frontend/app/components/Client/Integrations/ReduxDoc/ReduxDoc.js @@ -1,11 +1,16 @@ +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 { integrationsStore } = useStore(); + const sites = props.sites ? props.sites.toJS() : [] + 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'; @@ -75,9 +80,8 @@ 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'), + sites }; -})(ReduxDoc); +})(observer(ReduxDoc)); diff --git a/frontend/app/components/Client/Integrations/VueDoc/VueDoc.js b/frontend/app/components/Client/Integrations/VueDoc/VueDoc.js index 2a9e2e2a6..cf058537a 100644 --- a/frontend/app/components/Client/Integrations/VueDoc/VueDoc.js +++ b/frontend/app/components/Client/Integrations/VueDoc/VueDoc.js @@ -1,11 +1,16 @@ +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 { integrationsStore } = useStore(); + const sites = props.sites ? props.sites.toJS() : [] + 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'; @@ -82,9 +87,8 @@ 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'), + sites, }; -})(VueDoc); +})(observer(VueDoc)); diff --git a/frontend/app/components/Client/Integrations/ZustandDoc/ZustandDoc.js b/frontend/app/components/Client/Integrations/ZustandDoc/ZustandDoc.js index 9ed446cdc..ae11e06d9 100644 --- a/frontend/app/components/Client/Integrations/ZustandDoc/ZustandDoc.js +++ b/frontend/app/components/Client/Integrations/ZustandDoc/ZustandDoc.js @@ -1,11 +1,16 @@ +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 } = useStore(); + const sites = props.sites ? props.sites.toJS() : [] + 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'; @@ -98,9 +103,8 @@ 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'), + sites, }; -})(ZustandDoc); +})(observer(ZustandDoc)); diff --git a/frontend/app/duck/index.ts b/frontend/app/duck/index.ts index 763420479..941c9b7ea 100644 --- a/frontend/app/duck/index.ts +++ b/frontend/app/duck/index.ts @@ -6,7 +6,6 @@ import sessions from './sessions'; import sources from './sources'; import site from './site'; import customFields from './customField'; -import integrations from './integrations'; import search from './search'; import liveSearch from './liveSearch'; @@ -17,7 +16,6 @@ const rootReducer = combineReducers({ customFields, search, liveSearch, - ...integrations, ...sources }); diff --git a/frontend/app/duck/integrations/actions.js b/frontend/app/duck/integrations/actions.js deleted file mode 100644 index 1f926014f..000000000 --- a/frontend/app/duck/integrations/actions.js +++ /dev/null @@ -1,46 +0,0 @@ -import { fetchListType, fetchType, saveType, editType, initType, removeType } from '../funcTools/types'; - -export function fetchList(name) { - return { - types: fetchListType(name).array, - call: (client) => client.get(`/integrations/${name}`), - name, - }; -} - -export function fetch(name, siteId) { - return { - types: fetchType(name).array, - call: (client) => client.get(siteId && name !== 'github' && name !== 'jira' ? `/${siteId}/integrations/${name}` : `/integrations/${name}`), - name, - }; -} - -export function save(name, siteId, instance) { - return { - types: saveType(name).array, - call: (client) => client.post((siteId ? `/${siteId}` : '') + `/integrations/${name}`, instance.toData()), - }; -} - -export function edit(name, instance) { - return { - type: editType(name), - instance, - }; -} - -export function init(name, instance) { - return { - type: initType(name), - instance, - }; -} - -export function remove(name, siteId) { - return { - types: removeType(name).array, - call: (client) => client.delete((siteId ? `/${siteId}` : '') + `/integrations/${name}`), - siteId, - }; -} diff --git a/frontend/app/duck/integrations/index.js b/frontend/app/duck/integrations/index.js deleted file mode 100644 index dec847235..000000000 --- a/frontend/app/duck/integrations/index.js +++ /dev/null @@ -1,37 +0,0 @@ -import SentryConfig from 'Types/integrations/sentryConfig'; -import DatadogConfig from 'Types/integrations/datadogConfig'; -import StackdriverConfig from 'Types/integrations/stackdriverConfig'; -import RollbarConfig from 'Types/integrations/rollbarConfig'; -import NewrelicConfig from 'Types/integrations/newrelicConfig'; -import BugsnagConfig from 'Types/integrations/bugsnagConfig'; -import CloudWatch from 'Types/integrations/cloudwatchConfig'; -import ElasticsearchConfig from 'Types/integrations/elasticsearchConfig'; -import SumoLogicConfig from 'Types/integrations/sumoLogicConfig'; -import JiraConfig from 'Types/integrations/jiraConfig'; -import GithubConfig from 'Types/integrations/githubConfig'; -import IssueTracker from 'Types/integrations/issueTracker'; -import slack from './slack'; -import integrations from './integrations'; -import teams from './teams' - -import { createIntegrationReducer } from './reducer'; - -export default { - sentry: createIntegrationReducer('sentry', SentryConfig), - datadog: createIntegrationReducer('datadog', DatadogConfig), - stackdriver: createIntegrationReducer('stackdriver', StackdriverConfig), - rollbar: createIntegrationReducer('rollbar', RollbarConfig), - newrelic: createIntegrationReducer('newrelic', NewrelicConfig), - bugsnag: createIntegrationReducer('bugsnag', BugsnagConfig), - cloudwatch: createIntegrationReducer('cloudwatch', CloudWatch), - elasticsearch: createIntegrationReducer('elasticsearch', ElasticsearchConfig), - sumologic: createIntegrationReducer('sumologic', SumoLogicConfig), - jira: createIntegrationReducer('jira', JiraConfig), - github: createIntegrationReducer('github', GithubConfig), - issues: createIntegrationReducer('issues', IssueTracker), - slack, - teams, - integrations, -}; - -export * from './actions'; diff --git a/frontend/app/duck/integrations/integrations.js b/frontend/app/duck/integrations/integrations.js deleted file mode 100644 index c21952b9b..000000000 --- a/frontend/app/duck/integrations/integrations.js +++ /dev/null @@ -1,40 +0,0 @@ -import { Map } from 'immutable'; -import { fetchListType } from '../funcTools/types'; -import { createRequestReducer } from '../funcTools/request'; - -const FETCH_LIST = fetchListType('integrations/FETCH_LIST'); -const SET_SITE_ID = 'integrations/SET_SITE_ID'; -const initialState = Map({ - list: [], - siteId: null, -}); -const reducer = (state = initialState, action = {}) => { - switch (action.type) { - case FETCH_LIST.success: - return state.set('list', action.data); - case SET_SITE_ID: - return state.set('siteId', action.siteId); - } - return state; -}; - -export default createRequestReducer( - { - fetchRequest: FETCH_LIST, - }, - reducer -); - -export function fetchIntegrationList(siteID) { - return { - types: FETCH_LIST.array, - call: (client) => client.get(`/${siteID}/integrations`), - }; -} - -export function setSiteId(siteId) { - return { - type: SET_SITE_ID, - siteId, - }; -} diff --git a/frontend/app/duck/integrations/reducer.js b/frontend/app/duck/integrations/reducer.js deleted file mode 100644 index 425b1cc50..000000000 --- a/frontend/app/duck/integrations/reducer.js +++ /dev/null @@ -1,52 +0,0 @@ -import { List, Map } from 'immutable'; -import { createRequestReducer } from '../funcTools/request'; -import { fetchListType, saveType, removeType, editType, initType, fetchType } from '../funcTools/types'; -import { createItemInListUpdater } from '../funcTools/tools'; - -const idKey = 'siteId'; -const itemInListUpdater = createItemInListUpdater(idKey); - -export const createIntegrationReducer = (name, Config) => { - const FETCH_LIST = fetchListType(name); - const SAVE = saveType(name); - const REMOVE = removeType(name); - const EDIT = editType(name); - const INIT = initType(name); - const FETCH = fetchType(name); - - const initialState = Map({ - instance: Config(), - list: List(), - fetched: false, - issuesFetched: false, - }); - const reducer = (state = initialState, action = {}) => { - switch (action.type) { - case FETCH_LIST.success: - return state - .set('list', Array.isArray(action.data) ? List(action.data).map(Config) : List([new Config(action.data)])) - .set(action.name + 'Fetched', true); - case FETCH.success: - return state.set('instance', Config(action.data || {})); - case SAVE.success: - const config = Config(action.data); - return state.update('list', itemInListUpdater(config)).set('instance', config); - case REMOVE.success: - return state.update('list', (list) => list.filter((site) => site.siteId !== action.siteId)).set('instance', Config()); - case EDIT: - return state.mergeIn(['instance'], action.instance); - case INIT: - return state.set('instance', Config(action.instance)); - } - return state; - }; - return createRequestReducer( - { - // fetchRequest: FETCH_LIST, - fetchRequest: FETCH, - saveRequest: SAVE, - removeRequest: REMOVE, - }, - reducer - ); -}; diff --git a/frontend/app/duck/integrations/slack.js b/frontend/app/duck/integrations/slack.js deleted file mode 100644 index e17180df3..000000000 --- a/frontend/app/duck/integrations/slack.js +++ /dev/null @@ -1,102 +0,0 @@ -import { Map, List } from 'immutable'; -import withRequestState, { RequestTypes } from 'Duck/requestStateCreator'; -import Config from 'Types/integrations/slackConfig'; -import { createItemInListUpdater } from '../funcTools/tools'; - -const SAVE = new RequestTypes('slack/SAVE'); -const UPDATE = new RequestTypes('slack/UPDATE'); -const REMOVE = new RequestTypes('slack/REMOVE'); -const FETCH_LIST = new RequestTypes('slack/FETCH_LIST'); -const SEND_MSG = new RequestTypes('slack/SEND_MSG'); -const EDIT = 'slack/EDIT'; -const INIT = 'slack/INIT'; -const idKey = 'webhookId'; -const itemInListUpdater = createItemInListUpdater(idKey); - -const initialState = Map({ - instance: Config(), - loaded: false, - list: List(), -}); - -const reducer = (state = initialState, action = {}) => { - switch (action.type) { - case FETCH_LIST.REQUEST: - return state.set('loaded', true); - case FETCH_LIST.SUCCESS: - return state.set('list', List(action.data).map(Config)).set('loaded', true) - case UPDATE.SUCCESS: - case SAVE.SUCCESS: - const config = Config(action.data); - return state.update('list', itemInListUpdater(config)).set('instance', config); - case REMOVE.SUCCESS: - return state.update('list', (list) => list.filter((item) => item.webhookId !== action.id)).set('instance', Config()); - case EDIT: - return state.mergeIn(['instance'], action.instance); - case INIT: - return state.set('instance', Config(action.instance)); - } - return state; -}; - -export default withRequestState( - { - fetchRequest: FETCH_LIST, - saveRequest: SAVE, - updateRequest: UPDATE, - removeRequest: REMOVE, - }, - reducer -); - -export function fetchList() { - return { - types: FETCH_LIST.toArray(), - call: (client) => client.get('/integrations/slack/channels'), - }; -} - -export function save(instance) { - return { - types: SAVE.toArray(), - call: (client) => client.post(`/integrations/slack`, instance.toData()), - }; -} - -export function update(instance) { - return { - types: UPDATE.toArray(), - call: (client) => client.post(`/integrations/slack/${instance.webhookId}`, instance.toData()), - }; -} - -export function edit(instance) { - return { - type: EDIT, - instance, - }; -} - -export function init(instance) { - return { - type: INIT, - instance, - }; -} - -export function remove(id) { - return { - types: REMOVE.toArray(), - call: (client) => client.delete(`/integrations/slack/${id}`), - id, - }; -} - -// https://api.openreplay.com/5587/integrations/slack/notify/315/sessions/7856803626558104 -// -export function sendSlackMsg({ integrationId, entity, entityId, data }) { - return { - types: SEND_MSG.toArray(), - call: (client) => client.post(`/integrations/slack/notify/${integrationId}/${entity}/${entityId}`, data) - } -} diff --git a/frontend/app/duck/integrations/teams.js b/frontend/app/duck/integrations/teams.js deleted file mode 100644 index 29fac4710..000000000 --- a/frontend/app/duck/integrations/teams.js +++ /dev/null @@ -1,101 +0,0 @@ -import { Map, List } from 'immutable'; -import withRequestState, { RequestTypes } from 'Duck/requestStateCreator'; -import Config from 'Types/integrations/slackConfig'; -import { createItemInListUpdater } from '../funcTools/tools'; - -const SAVE = new RequestTypes('msteams/SAVE'); -const UPDATE = new RequestTypes('msteams/UPDATE'); -const REMOVE = new RequestTypes('msteams/REMOVE'); -const FETCH_LIST = new RequestTypes('msteams/FETCH_LIST'); -const SEND_MSG = new RequestTypes('msteams/SEND_MSG'); - -const EDIT = 'msteams/EDIT'; -const INIT = 'msteams/INIT'; -const idKey = 'webhookId'; -const itemInListUpdater = createItemInListUpdater(idKey); - -const initialState = Map({ - instance: Config(), - list: List(), - loaded: false, -}); - -const reducer = (state = initialState, action = {}) => { - switch (action.type) { - case FETCH_LIST.REQUEST: - return state.set('loaded', true); - case FETCH_LIST.SUCCESS: - return state.set('list', List(action.data).map(Config)).set('loaded', true); - case UPDATE.SUCCESS: - case SAVE.SUCCESS: - const config = Config(action.data); - return state.update('list', itemInListUpdater(config)).set('instance', config); - case REMOVE.SUCCESS: - return state.update('list', (list) => list.filter((item) => item.webhookId !== action.id)).set('instance', Config()); - case EDIT: - return state.mergeIn(['instance'], action.instance); - case INIT: - return state.set('instance', Config(action.instance)); - } - return state; -}; - -export default withRequestState( - { - fetchRequest: FETCH_LIST, - saveRequest: SAVE, - updateRequest: UPDATE, - removeRequest: REMOVE, - }, - reducer -); - -export function fetchList() { - return { - types: FETCH_LIST.toArray(), - call: (client) => client.get('/integrations/msteams/channels'), - }; -} - -export function save(instance) { - return { - types: SAVE.toArray(), - call: (client) => client.post(`/integrations/msteams`, instance.toData()), - }; -} - -export function update(instance) { - return { - types: UPDATE.toArray(), - call: (client) => client.post(`/integrations/msteams/${instance.webhookId}`, instance.toData()), - }; -} - -export function edit(instance) { - return { - type: EDIT, - instance, - }; -} - -export function init(instance) { - return { - type: INIT, - instance, - }; -} - -export function remove(id) { - return { - types: REMOVE.toArray(), - call: (client) => client.delete(`/integrations/msteams/${id}`), - id, - }; -} - -export function sendMsTeamsMsg({ integrationId, entity, entityId, data }) { - return { - types: SEND_MSG.toArray(), - call: (client) => client.post(`/integrations/msteams/notify/${integrationId}/${entity}/${entityId}`, data) - } -} From a7a1eca9c39bb0bd8bdbf65e6703ea2537ad88dd Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Wed, 18 Sep 2024 11:06:27 +0200 Subject: [PATCH 03/12] some fixes for integrated check --- frontend/app/components/Client/Integrations/Integrations.tsx | 5 +++++ frontend/app/mstore/integrationsStore.ts | 2 +- frontend/app/services/IntegrationsService.ts | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/app/components/Client/Integrations/Integrations.tsx b/frontend/app/components/Client/Integrations/Integrations.tsx index 31cd2e46d..b3cf4050f 100644 --- a/frontend/app/components/Client/Integrations/Integrations.tsx +++ b/frontend/app/components/Client/Integrations/Integrations.tsx @@ -112,6 +112,11 @@ function Integrations(props: Props) { const allIntegrations = filteredIntegrations.flatMap( (cat) => cat.integrations ); + + console.log( + allIntegrations, + integratedList + ) return ( <>
diff --git a/frontend/app/mstore/integrationsStore.ts b/frontend/app/mstore/integrationsStore.ts index fd3b7fc8c..ef0d22cde 100644 --- a/frontend/app/mstore/integrationsStore.ts +++ b/frontend/app/mstore/integrationsStore.ts @@ -43,7 +43,7 @@ class GenericIntegrationsStore { fetchIntegrations = async (siteId?: string) => { this.setLoading(true); try { - const { data } = await integrationsService.fetchList(siteId); + const { data } = await integrationsService.fetchList(undefined, siteId); this.setList(data); } catch (e) { console.log(e); diff --git a/frontend/app/services/IntegrationsService.ts b/frontend/app/services/IntegrationsService.ts index c39595a2a..b07e50df9 100644 --- a/frontend/app/services/IntegrationsService.ts +++ b/frontend/app/services/IntegrationsService.ts @@ -2,7 +2,7 @@ import BaseService from "./BaseService"; export default class IntegrationsService extends BaseService { fetchList = async (name?: string, siteId?: string) => { - const r = await this.client.get(`${siteId ? `/${siteId}` : ''}/integrations/${name}`) + const r = await this.client.get(`${siteId ? `/${siteId}` : ''}/integrations${name ? `/${name}` : ''}`) const data = await r.json() return data From 82586d23b2f5ea65b564adf429fad8d3b11e494d Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Wed, 18 Sep 2024 15:14:22 +0200 Subject: [PATCH 04/12] start of projects refactoring --- frontend/app/api_client.ts | 5 +- frontend/app/mstore/index.tsx | 5 + frontend/app/mstore/projectsStore.ts | 143 +++++++++++++++++++++++ frontend/app/mstore/types/gdpr.ts | 30 +++++ frontend/app/mstore/types/project.ts | 54 +++++++++ frontend/app/services/ProjectsService.ts | 27 +++++ frontend/app/services/index.ts | 3 + 7 files changed, 265 insertions(+), 2 deletions(-) create mode 100644 frontend/app/mstore/projectsStore.ts create mode 100644 frontend/app/mstore/types/gdpr.ts create mode 100644 frontend/app/mstore/types/project.ts create mode 100644 frontend/app/services/ProjectsService.ts diff --git a/frontend/app/api_client.ts b/frontend/app/api_client.ts index 2a9394687..c967e9c0f 100644 --- a/frontend/app/api_client.ts +++ b/frontend/app/api_client.ts @@ -1,6 +1,7 @@ import store from 'App/store'; import { queried } from './routes'; import { setJwt } from 'Duck/user'; +import { projectStore } from 'App/mstore'; const siteIdRequiredPaths: string[] = [ '/dashboard', @@ -59,7 +60,7 @@ export default class APIClient { constructor() { const jwt = store.getState().getIn(['user', 'jwt']); - const siteId = store.getState().getIn(['site', 'siteId']); + const { siteId } = projectStore.getSiteId(); this.init = { headers: new Headers({ Accept: 'application/json', @@ -69,7 +70,7 @@ export default class APIClient { if (jwt !== null) { (this.init.headers as Headers).set('Authorization', `Bearer ${jwt}`); } - this.siteId = siteId; + this.siteId = siteId || undefined; } private getInit(method: string = 'GET', params?: any, reqHeaders?: Record): RequestInit { diff --git a/frontend/app/mstore/index.tsx b/frontend/app/mstore/index.tsx index b127fa585..3101f8a36 100644 --- a/frontend/app/mstore/index.tsx +++ b/frontend/app/mstore/index.tsx @@ -28,6 +28,9 @@ import UiPlayerStore from './uiPlayerStore'; import IssueReportingStore from './issueReportingStore'; import CustomFieldStore from './customFieldStore'; import { IntegrationsStore } from "./integrationsStore"; +import ProjectsStore from './projectsStore'; + +export const projectStore = new ProjectsStore(); export class RootStore { dashboardStore: DashboardStore; @@ -57,6 +60,7 @@ export class RootStore { issueReportingStore: IssueReportingStore; customFieldStore: CustomFieldStore; integrationsStore: IntegrationsStore + projectsStore: ProjectsStore; constructor() { this.dashboardStore = new DashboardStore(); @@ -86,6 +90,7 @@ export class RootStore { this.issueReportingStore = new IssueReportingStore(); this.customFieldStore = new CustomFieldStore(); this.integrationsStore = new IntegrationsStore(); + this.projectsStore = projectStore; } initClient() { diff --git a/frontend/app/mstore/projectsStore.ts b/frontend/app/mstore/projectsStore.ts new file mode 100644 index 000000000..cdd87fd61 --- /dev/null +++ b/frontend/app/mstore/projectsStore.ts @@ -0,0 +1,143 @@ +import { makeAutoObservable, runInAction } from 'mobx'; +import Project from './types/project'; +import GDPR from './types/gdpr'; +import { GLOBAL_HAS_NO_RECORDINGS, SITE_ID_STORAGE_KEY } from 'App/constants/storageKeys'; +import { projectsService } from "App/services"; + +export default class ProjectsStore { + list: Project[] = []; + instance: Project | null = null; + siteId: string | null = null; + active: Project | null = null; + sitesLoading = false; + + constructor() { + const storedSiteId = localStorage.getItem(SITE_ID_STORAGE_KEY); + this.siteId = storedSiteId ?? null; + makeAutoObservable(this); + } + + getSiteId = () => { + return { + siteId: this.siteId, + active: this.active, + }; + } + + initProject = (project: Partial) => { + this.instance = new Project(project); + } + + setSitesLoading = (loading: boolean) => { + this.sitesLoading = loading; + } + + setSiteId(siteId: string) { + this.siteId = siteId; + localStorage.setItem(SITE_ID_STORAGE_KEY, siteId.toString()); + this.active = this.list.find((site) => site.id! === siteId) ?? null; + } + + editGDPR(gdprData: Partial) { + if (this.instance) { + this.instance.gdpr.edit(gdprData); + } + } + + editInstance = (instance: Partial) => { + if (!this.instance) return; + this.instance.edit(instance); + } + + async fetchGDPR(siteId: string) { + try { + const response = await projectsService.fetchGDPR(siteId) + runInAction(() => { + if (this.instance) { + Object.assign(this.instance.gdpr, response.data); + } + }); + } catch (error) { + console.error('Failed to fetch GDPR:', error); + } + } + + saveGDPR = async (siteId: string) => { + if (!this.instance) return; + try { + const gdprData = this.instance.gdpr.toData(); + const response = await projectsService.saveGDPR(siteId, gdprData); + this.editGDPR(response.data); + } catch (error) { + console.error('Failed to save GDPR:', error); + } + } + + fetchList = async (siteIdFromPath: string) =>{ + this.setSitesLoading(true); + try { + const response = await projectsService.fetchList(); + runInAction(() => { + this.list = response.data.map((data) => new Project(data)); + const siteIds = this.list.map(site => site.id); + let siteId = this.siteId; + const siteExists = siteId ? siteIds.includes(siteId) : false; + + if (siteIdFromPath && siteIds.includes(siteIdFromPath)) { + siteId = siteIdFromPath; + } else if (!siteId || !siteExists) { + siteId = siteIds.includes(this.siteId) + ? this.siteId + : response.data[0].projectId; + } + + const hasRecordings = this.list.some(site => site.recorded); + if (!hasRecordings) { + localStorage.setItem(GLOBAL_HAS_NO_RECORDINGS, 'true'); + } else { + localStorage.removeItem(GLOBAL_HAS_NO_RECORDINGS); + } + if (siteId) { + this.setSiteId(siteId); + } + }); + } catch (error) { + console.error('Failed to fetch site list:', error); + } finally { + this.setSitesLoading(false); + } + } + + save = async (projectData: Partial) => { + try { + const response = await projectsService.saveProject(projectData); + runInAction(() => { + const newSite = new Project(response.data); + const index = this.list.findIndex(site => site.id === newSite.id); + if (index !== -1) { + this.list[index] = newSite; + } else { + this.list.push(newSite); + } + this.setSiteId(newSite.id); + this.active = newSite; + }); + } catch (error) { + console.error('Failed to save site:', error); + } + } + + updateProjectRecordingStatus = (siteId: string, status: boolean) => { + const site = this.list.find(site => site.id === siteId); + if (site) { + site.recorded = status; + const hasRecordings = this.list.some(site => site.recorded); + if (!hasRecordings) { + localStorage.setItem(GLOBAL_HAS_NO_RECORDINGS, 'true'); + } else { + localStorage.removeItem(GLOBAL_HAS_NO_RECORDINGS); + } + } + } +} + diff --git a/frontend/app/mstore/types/gdpr.ts b/frontend/app/mstore/types/gdpr.ts new file mode 100644 index 000000000..2871d763c --- /dev/null +++ b/frontend/app/mstore/types/gdpr.ts @@ -0,0 +1,30 @@ +import { makeAutoObservable } from 'mobx'; + +export default class GDPR { + id = undefined; + maskEmails = false; + maskNumbers = false; + defaultInputMode = 'plain'; + sampleRate = 0; + + constructor(data = {}) { + Object.assign(this, data); + makeAutoObservable(this); + } + + edit = (data: Partial) => { + Object.keys(data).forEach((key) => { + this[key] = data[key]; + }) + } + + toData = () => { + return { + id: this.id, + maskEmails: this.maskEmails, + maskNumbers: this.maskNumbers, + defaultInputMode: this.defaultInputMode, + sampleRate: this.sampleRate, + }; + } +} diff --git a/frontend/app/mstore/types/project.ts b/frontend/app/mstore/types/project.ts new file mode 100644 index 000000000..35f0da5a8 --- /dev/null +++ b/frontend/app/mstore/types/project.ts @@ -0,0 +1,54 @@ +import { makeAutoObservable } from 'mobx'; +import GDPR from './gdpr'; + +export default class Project { + id?: string; + name: string = ''; + host: string = ''; + platform: string = 'web'; + lastRecordedSessionAt?: any; + gdpr: GDPR; + recorded: boolean = false; + stackIntegrations: boolean = false; + projectKey?: string; + projectId?: number; + trackerVersion?: string; + saveRequestPayloads: boolean = false; + sampleRate: number = 0; + conditionsCount: number = 0; + + constructor(data: Partial = {}) { + Object.assign(this, data); + this.gdpr = data.gdpr ? new GDPR(data.gdpr) : new GDPR(); + this.id = data.projectId?.toString(); + this.host = data.name || ''; + makeAutoObservable(this); + } + + edit = (data: Partial) => { + Object.keys(data).forEach((key) => { + if (key in this) { + this[key] = data[key]; + } + }) + } + + toData = () => { + return { + id: this.id, + name: this.name, + host: this.host, + platform: this.platform, + lastRecordedSessionAt: this.lastRecordedSessionAt, + gdpr: this.gdpr.toData(), + recorded: this.recorded, + stackIntegrations: this.stackIntegrations, + projectKey: this.projectKey, + projectId: this.projectId, + trackerVersion: this.trackerVersion, + saveRequestPayloads: this.saveRequestPayloads, + sampleRate: this.sampleRate, + conditionsCount: this.conditionsCount, + }; + } +} diff --git a/frontend/app/services/ProjectsService.ts b/frontend/app/services/ProjectsService.ts new file mode 100644 index 000000000..848457096 --- /dev/null +++ b/frontend/app/services/ProjectsService.ts @@ -0,0 +1,27 @@ +import BaseService from "./BaseService"; + +export default class ProjectsService extends BaseService { + fetchGDPR = async (siteId: string) => { + const r = await this.client.get(`/${siteId}/gdpr`); + + return await r.json(); + } + + saveGDPR = async (siteId: string, gdprData: any) => { + const r = await this.client.post(`/${siteId}/gdpr`, gdprData); + + return await r.json(); + } + + fetchList = async () => { + const r = await this.client.get('/projects'); + + return await r.json(); + } + + saveProject = async (projectData: any) => { + const r = await this.client.post('/projects', projectData); + + return await r.json(); + } +} \ No newline at end of file diff --git a/frontend/app/services/index.ts b/frontend/app/services/index.ts index 6afacb791..93a2aee0a 100644 --- a/frontend/app/services/index.ts +++ b/frontend/app/services/index.ts @@ -23,6 +23,7 @@ import FilterService from "./FilterService"; import IssueReportsService from "./IssueReportsService"; import CustomFieldService from './CustomFieldService'; import IntegrationsService from './IntegrationsService'; +import ProjectsService from './ProjectsService'; export const dashboardService = new DashboardService(); export const metricService = new MetricService(); @@ -48,6 +49,7 @@ export const filterService = new FilterService(); export const issueReportsService = new IssueReportsService(); export const customFieldService = new CustomFieldService(); export const integrationsService = new IntegrationsService(); +export const projectsService = new ProjectsService(); export const services = [ dashboardService, @@ -74,4 +76,5 @@ export const services = [ issueReportsService, customFieldService, integrationsService, + projectsService, ]; From df20cd5333e95b487b1ede59f1087c127b75c997 Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Wed, 18 Sep 2024 15:39:33 +0200 Subject: [PATCH 05/12] move api and "few" files to new project store --- frontend/app/PrivateRoutes.tsx | 20 +- frontend/app/Router.tsx | 59 +++--- frontend/app/api_client.ts | 11 +- .../Client/CustomFields/CustomFields.tsx | 22 +-- .../Client/Integrations/IntegrationForm.tsx | 11 +- .../Client/Integrations/Integrations.tsx | 12 +- .../Client/Integrations/PiniaDoc/PiniaDoc.tsx | 14 +- .../AddProjectButton/AddProjectButton.tsx | 18 +- .../app/components/Client/Sites/GDPRForm.js | 184 +++++++++--------- .../app/components/Client/Sites/Sites.tsx | 27 ++- .../app/components/hocs/withSiteIdRouter.js | 39 ++-- .../app/components/hocs/withSiteIdUpdater.js | 61 +++--- .../ProjectDropdown/ProjectDropdown.tsx | 31 ++- frontend/app/layout/Layout.tsx | 22 +-- frontend/app/layout/TopHeader.tsx | 14 +- frontend/app/layout/TopRight.tsx | 10 +- frontend/app/mstore/index.tsx | 1 + frontend/app/services/DashboardService.ts | 30 +-- 18 files changed, 255 insertions(+), 331 deletions(-) diff --git a/frontend/app/PrivateRoutes.tsx b/frontend/app/PrivateRoutes.tsx index 97f7fac83..0d562bff9 100644 --- a/frontend/app/PrivateRoutes.tsx +++ b/frontend/app/PrivateRoutes.tsx @@ -3,8 +3,8 @@ 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'; @@ -110,20 +110,20 @@ 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; + const { projectsStore } = useStore(); + const sites = projectsStore.list; + const siteId = projectsStore.siteId; + const { onboarding } = props; const hasRecordings = sites.some(s => s.recorded); const redirectToSetup = props.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)) && props.scope > 0; + const siteIdList: any = sites.map(({ id }) => id); return ( }> @@ -292,7 +292,5 @@ function PrivateRoutes(props: Props) { 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); +}))(observer(PrivateRoutes)); diff --git a/frontend/app/Router.tsx b/frontend/app/Router.tsx index 8c5801dc9..b3986e35b 100644 --- a/frontend/app/Router.tsx +++ b/frontend/app/Router.tsx @@ -13,56 +13,55 @@ import { SPOT_ONBOARDING } from 'App/constants/storageKeys'; import Layout from 'App/layout/Layout'; -import { useStore, 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 { 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; match: { params: { siteId: string; }; }; - mstore: any; setJwt: (params: { jwt: string; spotJwt: string | null }) => any; - initSite: (site: any) => void; - scopeSetup: boolean; localSpotJwt: string | null; } const Router: React.FC = (props) => { const { isLoggedIn, - siteId, - sites, - loading, + userInfoLoading, location, fetchUserInfo, - fetchSiteList, history, setSessionPath, - scopeSetup, localSpotJwt, - logout + logout, + scopeSetup, + setJwt, } = props; - const { customFieldStore } = useStore(); + const mstore = useStore(); + const { customFieldStore, projectsStore } = mstore; + + 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'); @@ -80,7 +79,7 @@ const Router: React.FC = (props) => { handleSpotLogin(spotJwt); } if (urlJWT) { - props.setJwt({ jwt: urlJWT, spotJwt: spotJwt ?? null }); + setJwt({ jwt: urlJWT, spotJwt: spotJwt ?? null }); } }; @@ -108,9 +107,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); @@ -177,13 +176,13 @@ const Router: React.FC = (props) => { const fetchData = async () => { if (siteId && siteId !== lastFetchedSiteIdRef.current) { const activeSite = sites.find((s) => s.id == siteId); - props.initSite(activeSite); - lastFetchedSiteIdRef.current = activeSite.id; + initSite(activeSite ?? {}); + lastFetchedSiteIdRef.current = activeSite?.id; await customFieldStore.fetchListActive(siteId + ''); } }; - fetchData(); + void fetchData(); }, [siteId]); const lastFetchedSiteIdRef = useRef(null); @@ -229,7 +228,6 @@ 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([ @@ -237,21 +235,14 @@ const mapStateToProps = (state: Map) => { '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, + scopeSetup, localSpotJwt: state.getIn(['user', 'spotJwt']), isLoggedIn: jwt !== null && !changePassword, - scopeSetup, - loading, + userInfoLoading, email: state.getIn(['user', 'account', 'email']), account: state.getIn(['user', 'account']), organisation: state.getIn(['user', 'account', 'name']), @@ -266,12 +257,10 @@ const mapStateToProps = (state: Map) => { const mapDispatchToProps = { fetchUserInfo, setSessionPath, - fetchSiteList, setJwt, - initSite, logout }; const connector = connect(mapStateToProps, mapDispatchToProps); -export default withStore(withRouter(connector(Router))); +export default withRouter(connector(observer(Router))); diff --git a/frontend/app/api_client.ts b/frontend/app/api_client.ts index c967e9c0f..2ea7f80d9 100644 --- a/frontend/app/api_client.ts +++ b/frontend/app/api_client.ts @@ -1,7 +1,6 @@ import store from 'App/store'; import { queried } from './routes'; import { setJwt } from 'Duck/user'; -import { projectStore } from 'App/mstore'; const siteIdRequiredPaths: string[] = [ '/dashboard', @@ -55,12 +54,12 @@ 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 refreshingTokenPromise: Promise | null = null; constructor() { const jwt = store.getState().getIn(['user', 'jwt']); - const { siteId } = projectStore.getSiteId(); this.init = { headers: new Headers({ Accept: 'application/json', @@ -70,7 +69,10 @@ export default class APIClient { if (jwt !== null) { (this.init.headers as Headers).set('Authorization', `Bearer ${jwt}`); } - this.siteId = siteId || undefined; + } + + setSiteIdCheck(checker: () => { siteId: string | null }): void { + this.siteIdCheck = checker } private getInit(method: string = 'GET', params?: any, reqHeaders?: Record): RequestInit { @@ -102,6 +104,7 @@ export default class APIClient { delete init.body; // GET requests shouldn't have a body } + this.siteId = this.siteIdCheck?.().siteId ?? undefined; return init; } diff --git a/frontend/app/components/Client/CustomFields/CustomFields.tsx b/frontend/app/components/Client/CustomFields/CustomFields.tsx index dd42911e4..e4152911e 100644 --- a/frontend/app/components/Client/CustomFields/CustomFields.tsx +++ b/frontend/app/components/Client/CustomFields/CustomFields.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from 'react'; import cn from 'classnames'; -import { connect } from 'react-redux'; import withPageTitle from 'HOCs/withPageTitle'; import { Button, Loader, NoContent, Icon, Tooltip, Divider } from 'UI'; import SiteDropdown from 'Shared/SiteDropdown'; @@ -12,20 +11,17 @@ import { useModal } from 'App/components/Modal'; import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; -interface CustomFieldsProps { - sites: any; -} - -const CustomFields: React.FC = (props) => { - const [currentSite, setCurrentSite] = useState(props.sites.get(0)); +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 { customFieldStore: store } = useStore(); const fields = store.list; const [loading, setLoading] = useState(false); useEffect(() => { - const activeSite = props.sites.get(0); + const activeSite = sites[0]; if (!activeSite) return; setCurrentSite(activeSite); @@ -34,7 +30,7 @@ const CustomFields: React.FC = (props) => { store.fetchList(activeSite.id).finally(() => { setLoading(false); }); - }, [props.sites]); + }, [sites]); const handleInit = (field?: any) => { console.log('field', field); @@ -45,7 +41,7 @@ const CustomFields: React.FC = (props) => { }; const onChangeSelect = ({ value }: { value: { value: number } }) => { - const site = props.sites.find((s: any) => s.id === value.value); + const site = sites.find((s: any) => s.id === value.value); setCurrentSite(site); setLoading(true); @@ -109,6 +105,4 @@ const CustomFields: React.FC = (props) => { ); }; -export default connect((state: any) => ({ - sites: state.getIn(['site', 'list']) -}))(withPageTitle('Metadata - OpenReplay Preferences')(observer(CustomFields))); +export default withPageTitle('Metadata - OpenReplay Preferences')(observer(CustomFields)); diff --git a/frontend/app/components/Client/Integrations/IntegrationForm.tsx b/frontend/app/components/Client/Integrations/IntegrationForm.tsx index 905b41d70..a290e91f1 100644 --- a/frontend/app/components/Client/Integrations/IntegrationForm.tsx +++ b/frontend/app/components/Client/Integrations/IntegrationForm.tsx @@ -8,7 +8,9 @@ import { Button, Checkbox, Form, Input, Loader } from 'UI'; function IntegrationForm(props: any) { const { formFields, name, integrated } = props; - const { integrationsStore } = useStore(); + const { integrationsStore, projectsStore } = useStore(); + const sites = projectsStore.list; + const initialSiteId = projectsStore.siteId; const integrationStore = integrationsStore[name as unknown as namedStore]; const config = integrationStore.instance; const loading = integrationStore.loading; @@ -18,7 +20,7 @@ function IntegrationForm(props: any) { const fetchIntegrationList = integrationsStore.integrations.fetchIntegrations; const fetchList = () => { - void fetchIntegrationList(props.initialSiteId); + void fetchIntegrationList(initialSiteId); }; const write = ({ target: { value, name: key, type, checked } }) => { @@ -104,7 +106,4 @@ function IntegrationForm(props: any) { ); } -export default connect((state: any) => ({ - sites: state.getIn(['site', 'list']), - initialSiteId: state.getIn(['site', 'siteId']), -}))(observer(IntegrationForm)); +export default observer(IntegrationForm); diff --git a/frontend/app/components/Client/Integrations/Integrations.tsx b/frontend/app/components/Client/Integrations/Integrations.tsx index b3cf4050f..2509e883b 100644 --- a/frontend/app/components/Client/Integrations/Integrations.tsx +++ b/frontend/app/components/Client/Integrations/Integrations.tsx @@ -41,11 +41,11 @@ interface Props { } function Integrations(props: Props) { - const { integrationsStore } = useStore(); - + const { integrationsStore, projectsStore } = useStore(); + const siteId = projectsStore.siteId; const fetchIntegrationList = integrationsStore.integrations.fetchIntegrations; const storeIntegratedList = integrationsStore.integrations.list; - const { siteId, hideHeader = false } = props; + const { hideHeader = false } = props; const { showModal } = useModal(); const [integratedList, setIntegratedList] = useState([]); const [activeFilter, setActiveFilter] = useState('all'); @@ -162,11 +162,7 @@ function Integrations(props: Props) { ); } -export default connect((state: any) => ({ - siteId: state.getIn(['site', 'siteId']), -}))( - withPageTitle('Integrations - OpenReplay Preferences')(observer(Integrations)) -); +export default withPageTitle('Integrations - OpenReplay Preferences')(observer(Integrations)) const integrations = [ { diff --git a/frontend/app/components/Client/Integrations/PiniaDoc/PiniaDoc.tsx b/frontend/app/components/Client/Integrations/PiniaDoc/PiniaDoc.tsx index 4184b4293..71a9cbccb 100644 --- a/frontend/app/components/Client/Integrations/PiniaDoc/PiniaDoc.tsx +++ b/frontend/app/components/Client/Integrations/PiniaDoc/PiniaDoc.tsx @@ -1,6 +1,5 @@ import { observer } from 'mobx-react-lite'; import React from 'react'; -import { connect } from 'react-redux'; import { useStore } from 'App/mstore'; import ToggleContent from 'Components/shared/ToggleContent'; @@ -8,9 +7,9 @@ import { CodeBlock } from 'UI'; import DocLink from 'Shared/DocLink/DocLink'; -const PiniaDoc = (props) => { - const { integrationsStore } = useStore(); - const sites = props.sites ? props.sites.toJS() : []; +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 @@ -110,9 +109,4 @@ piniaStorePlugin(examplePiniaStore) PiniaDoc.displayName = 'PiniaDoc'; -export default connect((state: any) => { - const sites = state.getIn(['site', 'list']); - return { - sites, - }; -})(observer(PiniaDoc)); +export default observer(PiniaDoc); diff --git a/frontend/app/components/Client/Sites/AddProjectButton/AddProjectButton.tsx b/frontend/app/components/Client/Sites/AddProjectButton/AddProjectButton.tsx index 0a2712462..5f0df0bce 100644 --- a/frontend/app/components/Client/Sites/AddProjectButton/AddProjectButton.tsx +++ b/frontend/app/components/Client/Sites/AddProjectButton/AddProjectButton.tsx @@ -1,8 +1,7 @@ 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 { observer } from 'mobx-react-lite'; import { connect } from 'react-redux'; import { useModal } from 'App/components/Modal'; import NewSiteForm from '../NewSiteForm'; @@ -10,16 +9,15 @@ 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 +32,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/Sites.tsx b/frontend/app/components/Client/Sites/Sites.tsx index 0b716fb4c..f804bede4 100644 --- a/frontend/app/components/Client/Sites/Sites.tsx +++ b/frontend/app/components/Client/Sites/Sites.tsx @@ -3,7 +3,6 @@ import { connect, ConnectedProps } from 'react-redux'; import { Tag } from 'antd'; import cn from 'classnames'; import { Loader, Button, TextLink, NoContent, Pagination, PageTitle, Divider, Icon } from 'UI'; -import { init, remove, fetchGDPR, setSiteId } from 'Duck/site'; import withPageTitle from 'HOCs/withPageTitle'; import stl from './sites.module.css'; import NewSiteForm from './NewSiteForm'; @@ -16,9 +15,11 @@ import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; import { useModal } from 'App/components/Modal'; import CaptureRate from 'Shared/SessionSettings/components/CaptureRate'; import { BranchesOutlined } from '@ant-design/icons'; +import { observer } from 'mobx-react-lite'; +import { useStore } from 'App/mstore' type Project = { - id: number; + id: string; name: string; conditionsCount: number; platform: 'web' | 'mobile'; @@ -29,7 +30,11 @@ type Project = { type PropsFromRedux = ConnectedProps; -const Sites = ({ loading, sites, user, init }: PropsFromRedux) => { +const Sites = ({ user }: PropsFromRedux) => { + const { projectsStore } = useStore(); + const sites = projectsStore.list; + const loading = projectsStore.sitesLoading; + const init = projectsStore.initProject const [searchQuery, setSearchQuery] = useState(''); const [showCaptureRate, setShowCaptureRate] = useState(true); const [activeProject, setActiveProject] = useState(null); @@ -140,7 +145,7 @@ const Sites = ({ loading, sites, user, init }: PropsFromRedux) => {
} size="small" - show={!loading && filteredSites.size === 0} + show={!loading && filteredSites.length === 0} >
Project Name
@@ -160,7 +165,7 @@ const Sites = ({ loading, sites, user, init }: PropsFromRedux) => {
updatePage(page)} limit={pageSize} /> @@ -181,18 +186,10 @@ 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, -}); +const connector = connect(mapStateToProps, null); -export default connector(withPageTitle('Projects - OpenReplay Preferences')(Sites)); +export default connector(withPageTitle('Projects - OpenReplay Preferences')(observer(Sites))); diff --git a/frontend/app/components/hocs/withSiteIdRouter.js b/frontend/app/components/hocs/withSiteIdRouter.js index ee41610ce..756a5d280 100644 --- a/frontend/app/components/hocs/withSiteIdRouter.js +++ b/frontend/app/components/hocs/withSiteIdRouter.js @@ -1,32 +1,21 @@ import React from 'react'; import { withRouter } from 'react-router-dom'; -import { connect } from 'react-redux'; import { withSiteId } from 'App/routes'; -import { setSiteId } from 'Duck/site'; +import { observer } from 'mobx-react-lite' +import { useStore } from "App/mstore"; -export default BaseComponent => { - @withRouter - @connect((state, props) => ({ - urlSiteId: props.match.params.siteId, - siteId: state.getIn(['site', 'siteId']), - }), { - setSiteId, - }) - class WrappedClass extends React.PureComponent { - push = (location) => { - const { history, siteId } = this.props; - if (typeof location === 'string') { - history.push(withSiteId(location, siteId)); - } else if (typeof location === 'object') { - history.push({ ...location, pathname: withSiteId(location.pathname, siteId) }); - } - } +export default BaseComponent => withRouter(observer((props) => { + const { history, ...other } = props + const { projectsStore } = useStore(); + const siteId = projectsStore.siteId - render() { - const { history, ...other } = this.props - - return + const push = (location) => { + if (typeof location === 'string') { + history.push(withSiteId(location, siteId)); + } else if (typeof location === 'object') { + history.push({ ...location, pathname: withSiteId(location.pathname, siteId) }); } } - return WrappedClass -} \ No newline at end of file + + return +})) \ No newline at end of file diff --git a/frontend/app/components/hocs/withSiteIdUpdater.js b/frontend/app/components/hocs/withSiteIdUpdater.js index 0849bbd84..2bf0acb44 100644 --- a/frontend/app/components/hocs/withSiteIdUpdater.js +++ b/frontend/app/components/hocs/withSiteIdUpdater.js @@ -1,40 +1,39 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { setSiteId } from 'Duck/site'; +import React, { useEffect, useRef } from 'react'; +import { useStore } from "App/mstore"; +import { observer } from 'mobx-react-lite' -export default (BaseComponent) => { - @connect((state, props) => ({ - urlSiteId: props.match.params.siteId, - siteId: state.getIn(['site', 'siteId']), - }), { - setSiteId, - }) - class WrapperClass extends React.PureComponent { - state = { load: false } - constructor(props) { - super(props); - if (props.urlSiteId && props.urlSiteId !== props.siteId) { - props.setSiteId(props.urlSiteId); +const withSiteIdUpdater = (BaseComponent) => { + const WrapperComponent = (props) => { + const { projectsStore } = useStore(); + const siteId = projectsStore.siteId; + const setSiteId = projectsStore.setSiteId; + const urlSiteId = props.match.params.siteId + const prevSiteIdRef = useRef(props.siteId); + + useEffect(() => { + if (urlSiteId && urlSiteId !== siteId) { + props.setSiteId(urlSiteId); } - } - componentDidUpdate(prevProps) { - const { urlSiteId, siteId, location: { pathname }, history } = this.props; + }, []); + + useEffect(() => { + const { location: { pathname }, history } = props; + const shouldUrlUpdate = urlSiteId && parseInt(urlSiteId, 10) !== parseInt(siteId, 10); if (shouldUrlUpdate) { const path = ['', siteId].concat(pathname.split('/').slice(2)).join('/'); history.push(path); } - const shouldBaseComponentReload = shouldUrlUpdate || siteId !== prevProps.siteId; - if (shouldBaseComponentReload) { - this.setState({ load: true }); - setTimeout(() => this.setState({ load: false }), 0); - } - } + prevSiteIdRef.current = siteId; + }, [urlSiteId, siteId, props.location.pathname, props.history]); - render() { - return this.state.load ? null : ; - } - } + const key = props.siteId; - return WrapperClass -} + const passedProps = { ...props, siteId, setSiteId, urlSiteId }; + return ; + }; + + return observer(WrapperComponent); +}; + +export default withSiteIdUpdater; diff --git a/frontend/app/components/shared/ProjectDropdown/ProjectDropdown.tsx b/frontend/app/components/shared/ProjectDropdown/ProjectDropdown.tsx index de96ad67e..f15493212 100644 --- a/frontend/app/components/shared/ProjectDropdown/ProjectDropdown.tsx +++ b/frontend/app/components/shared/ProjectDropdown/ProjectDropdown.tsx @@ -8,7 +8,8 @@ import React from 'react'; import { connect } from 'react-redux'; import { RouteComponentProps, withRouter } from 'react-router-dom'; -import { useStore, withStore } from 'App/mstore'; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; import { hasSiteId, siteChangeAvailable } from 'App/routes'; import NewSiteForm from 'Components/Client/Sites/NewSiteForm'; import { useModal } from 'Components/Modal'; @@ -27,18 +28,19 @@ interface Site { } interface Props extends RouteComponentProps { - sites: Site[]; - siteId: string; - setSiteId: (siteId: string) => void; clearSearch: (isSession: boolean) => void; clearSearchLive: () => void; - initProject: (data: any) => void; - mstore: any; account: any; } function ProjectDropdown(props: Props) { - const { sites, siteId, location, account } = props; + const mstore = useStore(); + const { projectsStore } = mstore; + const sites = projectsStore.list; + const siteId = projectsStore.siteId; + const setSiteId = projectsStore.setSiteId; + const initProject = projectsStore.initProject; + const { location, account } = props; const isAdmin = account.admin || account.superAdmin; const activeSite = sites.find((s) => s.id === siteId); const showCurrent = @@ -47,21 +49,20 @@ function ProjectDropdown(props: Props) { const { customFieldStore } = useStore(); const handleSiteChange = async (newSiteId: string) => { - props.setSiteId(newSiteId); // Fixed: should set the new siteId, not the existing one + setSiteId(newSiteId); // Fixed: should set the new siteId, not the existing one await customFieldStore.fetchList(newSiteId) props.clearSearch(location.pathname.includes('/sessions')); props.clearSearchLive(); - props.mstore.initClient(); + mstore.initClient(); }; const addProjectClickHandler = () => { - props.initProject({}); + initProject({}); showModal(, { right: true }); }; - // @ts-ignore immutable - const menuItems = sites.toJS().map((site) => ({ + const menuItems = sites.map((site) => ({ key: site.id, label: (
({ - sites: state.getIn(['site', 'list']), - siteId: state.getIn(['site', 'siteId']), account: state.getIn(['user', 'account']), }); export default withRouter( connect(mapStateToProps, { - setSiteId, clearSearch, clearSearchLive, - initProject, - })(withStore(ProjectDropdown)) + })(observer(ProjectDropdown)) ); diff --git a/frontend/app/layout/Layout.tsx b/frontend/app/layout/Layout.tsx index 7b9370072..49a96df2c 100644 --- a/frontend/app/layout/Layout.tsx +++ b/frontend/app/layout/Layout.tsx @@ -4,24 +4,19 @@ import SideMenu from 'App/layout/SideMenu'; import TopHeader from 'App/layout/TopHeader'; import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; -import { init as initSite } from 'Duck/site'; -import { connect } from 'react-redux'; - const { Sider, Content } = AntLayout; interface Props { children: React.ReactNode; hideHeader?: boolean; - siteId?: string; - initSite: (site: any) => void; - sites: any[]; } function Layout(props: Props) { - const { hideHeader, siteId } = props; + const { hideHeader } = props; const isPlayer = /\/(session|assist|view-spot)\//.test(window.location.pathname); - const { settingsStore } = useStore(); + const { settingsStore, projectsStore } = useStore(); + const siteId = projectsStore.siteId; return ( @@ -29,7 +24,7 @@ function Layout(props: Props) { )} - {!hideHeader && !window.location.pathname.includes('/onboarding/') && ( + {!hideHeader && !window.location.pathname.includes('/onboarding/') ? ( - + - )} + ) : null} {props.children} @@ -52,7 +47,4 @@ function Layout(props: Props) { ); } -export default connect((state: any) => ({ - siteId: state.getIn(['site', 'siteId']), - sites: state.getIn(['site', 'list']) -}), { initSite })(observer(Layout)); +export default observer(Layout); diff --git a/frontend/app/layout/TopHeader.tsx b/frontend/app/layout/TopHeader.tsx index e82a4df33..3f2e38b3c 100644 --- a/frontend/app/layout/TopHeader.tsx +++ b/frontend/app/layout/TopHeader.tsx @@ -1,28 +1,26 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import Logo from 'App/layout/Logo'; import TopRight from 'App/layout/TopRight'; import { Layout, Space, Tooltip } from 'antd'; import { useStore } from 'App/mstore'; import { Icon } from 'UI'; -import { observer, useObserver } from 'mobx-react-lite'; +import { observer } from 'mobx-react-lite'; import { INDEXES } from 'App/constants/zindex'; import { connect } from 'react-redux'; import { logout } from 'Duck/user'; -import { init as initSite } from 'Duck/site'; const { Header } = Layout; interface Props { account: any; - siteId: string; - initSite: (site: any) => void; } function TopHeader(props: Props) { const { settingsStore } = useStore(); - const { account, siteId } = props; - const { userStore, notificationStore } = useStore(); + const { account } = props; + const { userStore, notificationStore, projectsStore } = useStore(); + const siteId = projectsStore.siteId; const initialDataFetched = userStore.initialDataFetched; useEffect(() => { @@ -74,12 +72,10 @@ function TopHeader(props: Props) { const mapStateToProps = (state: any) => ({ account: state.getIn(['user', 'account']), - siteId: state.getIn(['site', 'siteId']) }); const mapDispatchToProps = { onLogoutClick: logout, - initSite }; export default connect( diff --git a/frontend/app/layout/TopRight.tsx b/frontend/app/layout/TopRight.tsx index 9beab1bb5..c47902e03 100644 --- a/frontend/app/layout/TopRight.tsx +++ b/frontend/app/layout/TopRight.tsx @@ -10,17 +10,17 @@ import UserMenu from 'Components/Header/UserMenu/UserMenu'; import GettingStartedProgress from 'Shared/GettingStarted/GettingStartedProgress'; import ProjectDropdown from 'Shared/ProjectDropdown'; import { getScope } from "../duck/user"; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; interface Props { account: any; - siteId: any; - sites: any; spotOnly?: boolean; } function TopRight(props: Props) { + const { projectsStore } = useStore(); const { account } = props; - // @ts-ignore return ( {props.spotOnly ? null : ( @@ -52,9 +52,7 @@ function mapStateToProps(state: any) { return { account: state.getIn(['user', 'account']), spotOnly: getScope(state) === 1, - siteId: state.getIn(['site', 'siteId']), - sites: state.getIn(['site', 'list']), }; } -export default connect(mapStateToProps)(TopRight); +export default connect(mapStateToProps)(observer(TopRight)); diff --git a/frontend/app/mstore/index.tsx b/frontend/app/mstore/index.tsx index 3101f8a36..70e009e6c 100644 --- a/frontend/app/mstore/index.tsx +++ b/frontend/app/mstore/index.tsx @@ -98,6 +98,7 @@ export class RootStore { services.forEach((service) => { service.initClient(client); }); + client.setSiteIdCheck(this.projectsStore.getSiteId) } } diff --git a/frontend/app/services/DashboardService.ts b/frontend/app/services/DashboardService.ts index 4c16c4e76..0b8e2a07d 100644 --- a/frontend/app/services/DashboardService.ts +++ b/frontend/app/services/DashboardService.ts @@ -1,22 +1,12 @@ import Dashboard from "App/mstore/types/dashboard"; -import APIClient from 'App/api_client'; +import BaseService from "./BaseService"; import Widget from "App/mstore/types/widget"; -export default class DashboardService { - private client: APIClient; - - constructor(client?: APIClient) { - this.client = client ? client : new APIClient(); - } - - initClient(client?: APIClient) { - this.client = client || new APIClient(); - } - +export default class DashboardService extends BaseService { /** * Get all widgets from a dashboard. * @param dashboardId Required - * @returns + * @returns */ getWidgets(dashboardId: string): Promise { return this.client.get(`/dashboards/${dashboardId}/widgets`) @@ -34,10 +24,10 @@ export default class DashboardService { .then(response => response.json()) .then(response => response.data || []); } - + /** * Get a dashboard by dashboardId. - * @param dashboardId + * @param dashboardId * @returns {Promise} */ getDashboard(dashboardId: string): Promise { @@ -66,9 +56,9 @@ export default class DashboardService { /** * Add a widget to a dashboard. - * @param dashboard - * @param metricIds - * @returns + * @param dashboard + * @param metricIds + * @returns */ addWidget(dashboard: Dashboard, metricIds: any): Promise { const data = dashboard.toJson() @@ -80,7 +70,7 @@ export default class DashboardService { /** * Delete a dashboard. - * @param dashboardId + * @param dashboardId * @returns {Promise} */ deleteDashboard(dashboardId: string): Promise { @@ -89,7 +79,7 @@ export default class DashboardService { /** - * Create a new Meitrc, if the dashboardId is not provided, + * Create a new Meitrc, if the dashboardId is not provided, * it will add the metric to the dashboard. * @param metric Required * @param dashboardId Optional From 81ecbac892485483273ec782b5564400e00c43eb Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Wed, 18 Sep 2024 16:49:59 +0200 Subject: [PATCH 06/12] new batch for site -> projects --- frontend/app/IFrameRoutes.tsx | 13 +- .../app/components/Client/Roles/Roles.tsx | 14 +- .../Roles/components/RoleForm/RoleForm.tsx | 13 +- .../components/Client/Sites/NewSiteForm.tsx | 74 ++++------ .../DashboardList/DashboardList.tsx | 9 +- .../NewDashModal/NewDashboardModal.tsx | 10 +- .../components/MetadataList/MetadataList.tsx | 28 ++-- .../AssistSessionsTabs/AssistSessionsTabs.tsx | 10 +- frontend/app/components/hocs/withReport.tsx | 17 +-- .../components/SessionList/SessionList.tsx | 19 ++- .../NoSessionPermission.tsx | 132 +++++++++--------- frontend/app/mstore/projectsStore.ts | 54 ++++++- frontend/app/mstore/types/project.ts | 8 ++ frontend/app/services/ProjectsService.ts | 14 +- 14 files changed, 223 insertions(+), 192 deletions(-) diff --git a/frontend/app/IFrameRoutes.tsx b/frontend/app/IFrameRoutes.tsx index 0973ccd59..5ba0b0b09 100644 --- a/frontend/app/IFrameRoutes.tsx +++ b/frontend/app/IFrameRoutes.tsx @@ -10,6 +10,8 @@ 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')), @@ -41,8 +43,11 @@ interface Props { } 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 siteId = projectsStore.siteId; + const { isJwt = false, isLoggedIn = false, loading, onboarding, jwt } = props; + const siteIdList: any = sites.map(({ id }) => id); if (isLoggedIn) { return ( @@ -75,11 +80,9 @@ 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 +}))(observer(IFrameRoutes)); diff --git a/frontend/app/components/Client/Roles/Roles.tsx b/frontend/app/components/Client/Roles/Roles.tsx index a5ef62264..1ea6e9685 100644 --- a/frontend/app/components/Client/Roles/Roles.tsx +++ b/frontend/app/components/Client/Roles/Roles.tsx @@ -23,11 +23,14 @@ interface Props { permissionsMap: any; removeErrors: any; resetErrors: () => void; - projectsMap: any; } function Roles(props: Props) { - const { roleStore } = useStore(); + const { roleStore, projectsStore } = useStore(); + 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; @@ -36,7 +39,7 @@ function Roles(props: Props) { roleStore.permissions.forEach((p: any) => { permissionsMap[p.value] = p.text; }); - const { account, projectsMap } = props; + const { account } = props; const { showModal, hideModal } = useModal(); const isAdmin = account.admin || account.superAdmin; @@ -108,13 +111,8 @@ function Roles(props: Props) { export default connect( (state: any) => { - const projects = state.getIn(['site', 'list']); return { account: state.getIn(['user', 'account']), - projectsMap: projects.reduce((acc: any, p: any) => { - acc[p.id] = p.name; - return acc; - }, {}), }; } )(withPageTitle('Roles & Access - OpenReplay Preferences')(observer(Roles))); diff --git a/frontend/app/components/Client/Roles/components/RoleForm/RoleForm.tsx b/frontend/app/components/Client/Roles/components/RoleForm/RoleForm.tsx index b8287dd42..e83bf2993 100644 --- a/frontend/app/components/Client/Roles/components/RoleForm/RoleForm.tsx +++ b/frontend/app/components/Client/Roles/components/RoleForm/RoleForm.tsx @@ -23,16 +23,16 @@ interface Permission { interface Props { closeModal: (toastMessage?: string) => void; - projects: any[]; permissionsMap: any; deleteHandler: (id: any) => Promise; } const RoleForm = (props: Props) => { - const { roleStore } = useStore(); + const { roleStore, projectsStore } = useStore(); + const projects = projectsStore.list; const role = roleStore.instance; const saving = roleStore.loading; - const { closeModal, permissionsMap, projects } = props; + const { closeModal, permissionsMap } = props; const projectOptions = projects .filter(({ value }) => !role.projects.includes(value)) .map((p: any) => ({ @@ -217,12 +217,7 @@ const RoleForm = (props: Props) => { ); }; -export default connect((state: any) => { - const projects = state.getIn(['site', 'list']); - return { - projects, - }; -})(observer(RoleForm)); +export default observer(RoleForm); function OptionLabel(nameMap: any, p: any, onChangeOption: (e: any) => void) { return ( diff --git a/frontend/app/components/Client/Sites/NewSiteForm.tsx b/frontend/app/components/Client/Sites/NewSiteForm.tsx index 66a31948e..95d0f69ed 100644 --- a/frontend/app/components/Client/Sites/NewSiteForm.tsx +++ b/frontend/app/components/Client/Sites/NewSiteForm.tsx @@ -3,22 +3,18 @@ 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 { confirm } from 'UI'; +import { useStore } from 'App/mstore'; +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; @@ -26,36 +22,35 @@ type PropsFromRedux = ConnectedProps; type Props = PropsFromRedux & RouteComponentProps & OwnProps; const NewSiteForm = ({ - site, - loading, - save, - remove, - edit, - update, pushNewSite, - fetchList, - setSiteId, clearSearch, clearSearchLive, location: { pathname }, onClose, - mstore, - activeSiteId, - canDelete, }: 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); 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,7 +62,7 @@ const NewSiteForm = ({ } }); } else { - save(site).then((response: any) => { + saveProject(site!).then((response: any) => { if (!response || !response.errors || response.errors.size === 0) { onClose(null); clearSearch(); @@ -89,8 +84,9 @@ const NewSiteForm = ({ confirmButton: 'Yes, delete', cancelButton: 'Cancel', }) + && site?.id ) { - remove(site.id).then(() => { + projectsStore.removeProject(site.id).then(() => { onClose(null); if (site.id === activeSiteId) { setSiteId(null); @@ -103,9 +99,12 @@ const NewSiteForm = ({ target: { name, value }, }: ChangeEvent) => { setExistsError(false); - edit({ [name]: value }); + projectsStore.editInstance({ [name]: value }); }; + if (!site) { + return null + } return (
@@ -146,7 +145,7 @@ const NewSiteForm = ({ ]} value={site.platform} onChange={(value) => { - edit({ platform: value }); + projectsStore.editInstance({ platform: value }); }} />
@@ -157,9 +156,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() && ( - {/* */} -
- ); + return ( +
+ +
Not allowed
+ {session.isLive ? ( + + This session is still live, and you don’t have the necessary + permissions to access this feature. Please check with your admin. + + ) : ( + + You don’t have the necessary permissions to access this feature. + Please check with your admin. + + )} + {/* */} + + {/* */} +
+ ); } export default withRouter( - connect((state: any) => { - const isAssist = window.location.pathname.includes("/assist/"); - return { - isAssist, - session: state.getIn(["sessions", "current"]), - siteId: state.getIn(["site", "siteId"]), - sessionPath: state.getIn(["sessions", "sessionPath"]), - }; - })(NoSessionPermission) + connect((state: any) => { + const isAssist = window.location.pathname.includes('/assist/'); + return { + isAssist, + session: state.getIn(['sessions', 'current']), + sessionPath: state.getIn(['sessions', 'sessionPath']), + }; + })(observer(NoSessionPermission)) ); diff --git a/frontend/app/mstore/projectsStore.ts b/frontend/app/mstore/projectsStore.ts index cdd87fd61..eee73e38f 100644 --- a/frontend/app/mstore/projectsStore.ts +++ b/frontend/app/mstore/projectsStore.ts @@ -10,6 +10,7 @@ export default class ProjectsStore { siteId: string | null = null; active: Project | null = null; sitesLoading = false; + loading = false; constructor() { const storedSiteId = localStorage.getItem(SITE_ID_STORAGE_KEY); @@ -17,6 +18,10 @@ export default class ProjectsStore { makeAutoObservable(this); } + get isMobile() { + return this.instance ? ['ios', 'android'].includes(this.instance.platform) : false; + } + getSiteId = () => { return { siteId: this.siteId, @@ -32,6 +37,10 @@ export default class ProjectsStore { this.sitesLoading = loading; } + setLoading = (loading: boolean) => { + this.loading = loading; + } + setSiteId(siteId: string) { this.siteId = siteId; localStorage.setItem(SITE_ID_STORAGE_KEY, siteId.toString()); @@ -73,7 +82,7 @@ export default class ProjectsStore { } } - fetchList = async (siteIdFromPath: string) =>{ + fetchList = async (siteIdFromPath?: string) => { this.setSitesLoading(true); try { const response = await projectsService.fetchList(); @@ -87,8 +96,8 @@ export default class ProjectsStore { siteId = siteIdFromPath; } else if (!siteId || !siteExists) { siteId = siteIds.includes(this.siteId) - ? this.siteId - : response.data[0].projectId; + ? this.siteId + : response.data[0].projectId; } const hasRecordings = this.list.some(site => site.recorded); @@ -109,6 +118,7 @@ export default class ProjectsStore { } save = async (projectData: Partial) => { + this.setLoading(true); try { const response = await projectsService.saveProject(projectData); runInAction(() => { @@ -124,6 +134,8 @@ export default class ProjectsStore { }); } catch (error) { console.error('Failed to save site:', error); + } finally { + this.setLoading(false); } } @@ -139,5 +151,39 @@ export default class ProjectsStore { } } } -} + removeProject = async (projectId: string) => { + this.setLoading(true); + try { + await projectsService.removeProject(projectId); + runInAction(() => { + this.list = this.list.filter(site => site.id !== projectId); + if (this.siteId === projectId) { + this.setSiteId(this.list[0].id!); + } + }) + } catch (e) { + console.error('Failed to remove project:', e); + } finally { + this.setLoading(false); + } + } + + updateProject = async (projectId: string, projectData: Partial) => { + this.setLoading(true); + try { + const response = await projectsService.updateProject(projectId, projectData); + runInAction(() => { + const updatedSite = new Project(response.data); + const index = this.list.findIndex(site => site.id === updatedSite.id); + if (index !== -1) { + this.list[index] = updatedSite; + } + }); + } catch (error) { + console.error('Failed to update site:', error); + } finally { + this.setLoading + } + } +} diff --git a/frontend/app/mstore/types/project.ts b/frontend/app/mstore/types/project.ts index 35f0da5a8..6fe11e27f 100644 --- a/frontend/app/mstore/types/project.ts +++ b/frontend/app/mstore/types/project.ts @@ -25,6 +25,14 @@ export default class Project { makeAutoObservable(this); } + exists = () => { + return !!this.id; + } + + get validate() { + return this.name.length > 0; + } + edit = (data: Partial) => { Object.keys(data).forEach((key) => { if (key in this) { diff --git a/frontend/app/services/ProjectsService.ts b/frontend/app/services/ProjectsService.ts index 848457096..dec448903 100644 --- a/frontend/app/services/ProjectsService.ts +++ b/frontend/app/services/ProjectsService.ts @@ -24,4 +24,16 @@ export default class ProjectsService extends BaseService { return await r.json(); } -} \ No newline at end of file + + removeProject = async (projectId: string) => { + const r = await this.client.delete(`/projects/${projectId}`) + + return await r.json(); + } + + updateProject = async (projectId: string, projectData: any) => { + const r = await this.client.put(`/projects/${projectId}`, projectData); + + return await r.json(); + } +} From c2bc023c5e663efd0da115dc660f8de3cd9f39e4 Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Wed, 18 Sep 2024 16:53:23 +0200 Subject: [PATCH 07/12] fix setid context --- .../Onboarding/components/withOnboarding.tsx | 14 +++++++------- frontend/app/mstore/projectsStore.ts | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/frontend/app/components/Onboarding/components/withOnboarding.tsx b/frontend/app/components/Onboarding/components/withOnboarding.tsx index 931609428..ad87a358e 100644 --- a/frontend/app/components/Onboarding/components/withOnboarding.tsx +++ b/frontend/app/components/Onboarding/components/withOnboarding.tsx @@ -3,6 +3,8 @@ import { withRouter, RouteComponentProps } from 'react-router-dom'; import { connect, ConnectedProps } from 'react-redux'; import { setOnboarding } from 'Duck/user'; import { sessions, withSiteId, onboarding as onboardingRoute } from 'App/routes'; +import { observer } from 'mobx-react-lite'; +import { useStore } from 'App/mstore'; export interface WithOnboardingProps { history: RouteComponentProps['history']; @@ -18,10 +20,7 @@ export interface WithOnboardingProps { } const connector = connect( - (state: any) => ({ - siteId: state.getIn(['site', 'siteId']), - sites: state.getIn(['site', 'list']), - }), + null, { setOnboarding } ); @@ -31,8 +30,9 @@ const withOnboarding =

( Component: React.ComponentType

) => { const WithOnboarding: React.FC

= (props) => { + const { projectsStore } = useStore(); + const sites = projectsStore.list; const { - sites, match: { params: { siteId }, }, @@ -43,7 +43,7 @@ const withOnboarding =

( props.setOnboarding(true); props.history.push(withSiteId(sessions(), siteId)); }; - + const navTo = (tab: string) => { props.history.push(withSiteId(onboardingRoute(tab), siteId)); }; @@ -51,7 +51,7 @@ const withOnboarding =

( return ; }; - return withRouter(connector(WithOnboarding as React.ComponentType)); + return withRouter(connector(observer(WithOnboarding as React.ComponentType))); }; export default withOnboarding; diff --git a/frontend/app/mstore/projectsStore.ts b/frontend/app/mstore/projectsStore.ts index eee73e38f..296bd49c9 100644 --- a/frontend/app/mstore/projectsStore.ts +++ b/frontend/app/mstore/projectsStore.ts @@ -41,13 +41,13 @@ export default class ProjectsStore { this.loading = loading; } - setSiteId(siteId: string) { + setSiteId = (siteId: string) => { this.siteId = siteId; localStorage.setItem(SITE_ID_STORAGE_KEY, siteId.toString()); this.active = this.list.find((site) => site.id! === siteId) ?? null; } - editGDPR(gdprData: Partial) { + editGDPR = (gdprData: Partial) => { if (this.instance) { this.instance.gdpr.edit(gdprData); } @@ -58,7 +58,7 @@ export default class ProjectsStore { this.instance.edit(instance); } - async fetchGDPR(siteId: string) { + fetchGDPR = async (siteId: string) => { try { const response = await projectsService.fetchGDPR(siteId) runInAction(() => { From 997d69c3890cb3fd3e93785d00ec03599beb552d Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Thu, 19 Sep 2024 10:38:07 +0200 Subject: [PATCH 08/12] move all critical components, drop site duck --- .../app/components/Session/LivePlayer.tsx | 10 +- .../LivePlayer/LivePlayerBlockHeader.tsx | 5 +- .../Player/MobilePlayer/MobileControls.tsx | 5 +- .../MobilePlayer/MobilePlayerHeader.tsx | 6 +- .../MobilePlayer/MobilePlayerSubheader.tsx | 4 +- .../Player/ReplayPlayer/PlayerBlockHeader.tsx | 5 +- .../Session/Player/TagWatch/SaveModal.tsx | 5 +- .../Session_/Multiview/Multiview.tsx | 6 +- .../AssistSessionsTabs/AssistSessionsTabs.tsx | 9 +- .../Session_/Player/Controls/Controls.tsx | 5 +- frontend/app/components/Session_/Subheader.js | 63 ++++--- .../NoSessionsMessage/NoSessionsMessage.js | 21 +-- .../shared/SessionItem/PlayLink/PlayLink.tsx | 10 +- .../shared/SessionSearch/SessionSearch.tsx | 6 +- .../SessionSettings/SessionSettings.tsx | 11 +- .../components/CaptureRate.tsx | 2 +- .../shared/SiteDropdown/SiteDropdown.js | 13 +- frontend/app/components/ui/Link/Link.js | 25 +-- frontend/app/duck/site.js | 164 ------------------ 19 files changed, 108 insertions(+), 267 deletions(-) delete mode 100644 frontend/app/duck/site.js diff --git a/frontend/app/components/Session/LivePlayer.tsx b/frontend/app/components/Session/LivePlayer.tsx index 516427d11..2e1eda943 100644 --- a/frontend/app/components/Session/LivePlayer.tsx +++ b/frontend/app/components/Session/LivePlayer.tsx @@ -14,6 +14,7 @@ import withLocationHandlers from 'HOCs/withLocationHandlers'; import APIClient from 'App/api_client'; import { useLocation } from 'react-router-dom'; import { toast } from 'react-toastify'; +import { useStore } from 'App/mstore'; interface Props { session: Session; @@ -26,7 +27,6 @@ interface Props { query?: Record any>; request: () => void; userId: number; - siteId: number; } let playerInst: ILivePlayerContext['player'] | undefined; @@ -40,7 +40,6 @@ function LivePlayer({ query, isEnterprise, userId, - siteId, }: Props) { // @ts-ignore const [contextValue, setContextValue] = useState(defaultContextValue); @@ -48,8 +47,10 @@ function LivePlayer({ const openedFromMultiview = query?.get('multi') === 'true'; const usedSession = isMultiview ? customSession! : session; const location = useLocation(); + const { projectsStore } = useStore(); useEffect(() => { + const projectId = projectsStore.getSiteId(); playerInst = undefined; if (!usedSession.sessionId || contextValue.player !== undefined) return; console.debug('creating live player for', usedSession.sessionId); @@ -69,7 +70,7 @@ function LivePlayer({ sessionWithAgentData, data, userId, - siteId, + projectId, (state) => makeAutoObservable(state), toast ); @@ -81,7 +82,7 @@ function LivePlayer({ sessionWithAgentData, null, userId, - siteId, + projectId, (state) => makeAutoObservable(state), toast ); @@ -140,7 +141,6 @@ function LivePlayer({ export default withPermissions(['ASSIST_LIVE', 'SERVICE_ASSIST_LIVE'], '', true, false)( connect((state: any) => { return { - siteId: state.getIn([ 'site', 'siteId' ]), session: state.getIn(['sessions', 'current']), showAssist: state.getIn(['sessions', 'showChatWindow']), isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee', diff --git a/frontend/app/components/Session/Player/LivePlayer/LivePlayerBlockHeader.tsx b/frontend/app/components/Session/Player/LivePlayer/LivePlayerBlockHeader.tsx index df37e1684..96b7751b4 100644 --- a/frontend/app/components/Session/Player/LivePlayer/LivePlayerBlockHeader.tsx +++ b/frontend/app/components/Session/Player/LivePlayer/LivePlayerBlockHeader.tsx @@ -22,7 +22,8 @@ const ASSIST_ROUTE = assistRoute(); function LivePlayerBlockHeader(props: any) { const [hideBack, setHideBack] = React.useState(false); const { store } = React.useContext(PlayerContext); - const { assistMultiviewStore } = useStore(); + const { assistMultiviewStore, projectsStore } = useStore(); + const siteId = projectsStore.siteId; const history = useHistory(); const { width, height } = store.get(); @@ -30,7 +31,6 @@ function LivePlayerBlockHeader(props: any) { session, metaList, closedLive = false, - siteId, isMultiview, } = props; @@ -109,7 +109,6 @@ const PlayerHeaderCont = connect( isAssist, session, sessionPath: state.getIn(['sessions', 'sessionPath']), - siteId: state.getIn(['site', 'siteId']), metaList: state.getIn(['customFields', 'list']).map((i: any) => i.key), closedLive: !!state.getIn(['sessions', 'errors']) || (isAssist && !session.live), }; diff --git a/frontend/app/components/Session/Player/MobilePlayer/MobileControls.tsx b/frontend/app/components/Session/Player/MobilePlayer/MobileControls.tsx index 5bc1ab3b5..386532833 100644 --- a/frontend/app/components/Session/Player/MobilePlayer/MobileControls.tsx +++ b/frontend/app/components/Session/Player/MobilePlayer/MobileControls.tsx @@ -46,7 +46,7 @@ function Controls(props: any) { const { player, store } = React.useContext(MobilePlayerContext); const history = useHistory(); const { playing, completed, skip, speed, messagesLoading } = store.get(); - const { uiPlayerStore } = useStore(); + const { uiPlayerStore, projectsStore } = useStore(); const fullscreen = uiPlayerStore.fullscreen; const bottomBlock = uiPlayerStore.bottomBlock; const toggleBottomBlock = uiPlayerStore.toggleBottomBlock @@ -54,12 +54,12 @@ function Controls(props: any) { const fullscreenOff = uiPlayerStore.fullscreenOff; const changeSkipInterval = uiPlayerStore.changeSkipInterval; const skipInterval = uiPlayerStore.skipInterval; + const siteId = projectsStore.siteId; const { session, setActiveTab, previousSessionId, nextSessionId, - siteId, disableDevtools, } = props; @@ -289,7 +289,6 @@ export default connect( totalAssistSessions: state.getIn(['liveSearch', 'total']), previousSessionId: state.getIn(['sessions', 'previousId']), nextSessionId: state.getIn(['sessions', 'nextId']), - siteId: state.getIn(['site', 'siteId']), }; }, { diff --git a/frontend/app/components/Session/Player/MobilePlayer/MobilePlayerHeader.tsx b/frontend/app/components/Session/Player/MobilePlayer/MobilePlayerHeader.tsx index 183c419f1..5a4a97773 100644 --- a/frontend/app/components/Session/Player/MobilePlayer/MobilePlayerHeader.tsx +++ b/frontend/app/components/Session/Player/MobilePlayer/MobilePlayerHeader.tsx @@ -23,13 +23,12 @@ function PlayerBlockHeader(props: any) { const playerState = store?.get?.() || { width: 0, height: 0, showEvents: false }; const { width = 0, height = 0, showEvents = false } = playerState; - const { customFieldStore } = useStore(); - + const { customFieldStore, projectsStore } = useStore(); + const siteId = projectsStore.siteId!; const { session, fullscreen, metaList, - siteId, setActiveTab, activeTab, history, @@ -108,7 +107,6 @@ const PlayerHeaderCont = connect( sessionPath: state.getIn(['sessions', 'sessionPath']), local: state.getIn(['sessions', 'timezone']), funnelRef: state.getIn(['funnels', 'navRef']), - siteId: state.getIn(['site', 'siteId']), metaList: state.getIn(['customFields', 'list']).map((i: any) => i.key), }; }, diff --git a/frontend/app/components/Session/Player/MobilePlayer/MobilePlayerSubheader.tsx b/frontend/app/components/Session/Player/MobilePlayer/MobilePlayerSubheader.tsx index c07e903fd..df068843f 100644 --- a/frontend/app/components/Session/Player/MobilePlayer/MobilePlayerSubheader.tsx +++ b/frontend/app/components/Session/Player/MobilePlayer/MobilePlayerSubheader.tsx @@ -3,7 +3,6 @@ import QueueControls from 'Components/Session_/QueueControls'; import Bookmark from 'Shared/Bookmark'; import Issues from 'Components/Session_/Issues/Issues'; import NotePopup from 'Components/Session_/components/NotePopup'; -import { observer } from 'mobx-react-lite'; import { connect } from 'react-redux'; import { Tag } from 'antd' import { ShareAltOutlined } from '@ant-design/icons'; @@ -56,8 +55,7 @@ function SubHeader(props: any) { } export default connect((state: any) => ({ - siteId: state.getIn(['site', 'siteId']), modules: state.getIn(['user', 'account', 'modules']) || [], integrations: state.getIn(['issues', 'list']), isIOS: state.getIn(['sessions', 'current']).platform === 'ios', -}))(observer(SubHeader)); +}))(SubHeader); diff --git a/frontend/app/components/Session/Player/ReplayPlayer/PlayerBlockHeader.tsx b/frontend/app/components/Session/Player/ReplayPlayer/PlayerBlockHeader.tsx index c4ffefc89..7a8b05629 100644 --- a/frontend/app/components/Session/Player/ReplayPlayer/PlayerBlockHeader.tsx +++ b/frontend/app/components/Session/Player/ReplayPlayer/PlayerBlockHeader.tsx @@ -24,7 +24,8 @@ const SESSIONS_ROUTE = sessionsRoute(); function PlayerBlockHeader(props: any) { const [hideBack, setHideBack] = React.useState(false); const { player, store } = React.useContext(PlayerContext); - const { uxtestingStore, customFieldStore } = useStore() + const { uxtestingStore, customFieldStore, projectsStore } = useStore() + const siteId = projectsStore.siteId!; const playerState = store?.get?.() || { width: 0, height: 0, showEvents: false } const { width = 0, height = 0, showEvents = false } = playerState @@ -33,7 +34,6 @@ function PlayerBlockHeader(props: any) { fullscreen, metaList, closedLive = false, - siteId, setActiveTab, activeTab, history, @@ -137,7 +137,6 @@ const PlayerHeaderCont = connect( sessionPath: state.getIn(['sessions', 'sessionPath']), local: state.getIn(['sessions', 'timezone']), funnelRef: state.getIn(['funnels', 'navRef']), - siteId: state.getIn(['site', 'siteId']), metaList: state.getIn(['customFields', 'list']).map((i: any) => i.key), }; }, diff --git a/frontend/app/components/Session/Player/TagWatch/SaveModal.tsx b/frontend/app/components/Session/Player/TagWatch/SaveModal.tsx index 3fa29e4ef..d612d94d7 100644 --- a/frontend/app/components/Session/Player/TagWatch/SaveModal.tsx +++ b/frontend/app/components/Session/Player/TagWatch/SaveModal.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Button, Checkbox, Input } from 'antd'; import { useHistory } from 'react-router-dom'; import { withSiteId, sessions } from 'App/routes'; -import store from 'App/store'; +import { useStore } from 'App/mstore'; interface Props { onSave: (name: string, ignoreClRage: boolean, ignoreDeadCl: boolean) => Promise; @@ -11,6 +11,7 @@ interface Props { function SaveModal({ onSave, hideModal }: Props) { const history = useHistory(); + const { projectsStore } = useStore(); const [name, setName] = React.useState(''); const [ignoreClRage, setIgnoreClRage] = React.useState(false); const [ignoreDeadCl, setIgnoreDeadCl] = React.useState(false); @@ -22,7 +23,7 @@ function SaveModal({ onSave, hideModal }: Props) { const saveAndOpen = () => { onSave(name, ignoreClRage, ignoreDeadCl).then((tagId) => { hideModal(); - const siteId = store.getState().getIn(['site', 'siteId']); + const siteId = projectsStore.getSiteId() as unknown as string; history.push(withSiteId(sessions({ tnw: `is|${tagId}`, range: 'LAST_24_HOURS' }), siteId)); }); }; diff --git a/frontend/app/components/Session_/Multiview/Multiview.tsx b/frontend/app/components/Session_/Multiview/Multiview.tsx index de5ab0865..583e0d421 100644 --- a/frontend/app/components/Session_/Multiview/Multiview.tsx +++ b/frontend/app/components/Session_/Multiview/Multiview.tsx @@ -15,20 +15,19 @@ import SessionTileFooter from './SessionTileFooter' function Multiview({ total, fetchSessions, - siteId, assistCredentials, customSetSessions, }: { total: number; customSetSessions: (data: any) => void; fetchSessions: (filter: any) => void; - siteId: string; assistCredentials: any; list: Record[]; }) { const { showModal, hideModal } = useModal(); - const { assistMultiviewStore } = useStore(); + const { assistMultiviewStore, projectsStore } = useStore(); + const siteId = projectsStore.siteId!; const history = useHistory(); // @ts-ignore const { sessionsquery } = useParams(); @@ -128,7 +127,6 @@ function Multiview({ export default connect( (state: any) => ({ total: state.getIn(['liveSearch', 'total']), - siteId: state.getIn(['site', 'siteId']), }), { fetchSessions, diff --git a/frontend/app/components/Session_/Player/Controls/AssistSessionsTabs/AssistSessionsTabs.tsx b/frontend/app/components/Session_/Player/Controls/AssistSessionsTabs/AssistSessionsTabs.tsx index 07311665a..d6e249998 100644 --- a/frontend/app/components/Session_/Player/Controls/AssistSessionsTabs/AssistSessionsTabs.tsx +++ b/frontend/app/components/Session_/Player/Controls/AssistSessionsTabs/AssistSessionsTabs.tsx @@ -42,9 +42,10 @@ const CurrentTab = React.memo(() => ( )); -function AssistTabs({ session, siteId }: { session: Record; siteId: string }) { +function AssistTabs({ session }: { session: Record }) { const history = useHistory(); - const { assistMultiviewStore } = useStore(); + const { assistMultiviewStore, projectsStore } = useStore(); + const siteId = projectsStore.siteId!; const placeholder = new Array(4 - assistMultiviewStore.sessions.length).fill(0); @@ -83,6 +84,4 @@ function AssistTabs({ session, siteId }: { session: Record; siteId: ); } -export default connect((state: any) => ({ siteId: state.getIn(['site', 'siteId']) }))( - observer(AssistTabs) -); +export default observer(AssistTabs) diff --git a/frontend/app/components/Session_/Player/Controls/Controls.tsx b/frontend/app/components/Session_/Player/Controls/Controls.tsx index b8bf33365..32de45b0a 100644 --- a/frontend/app/components/Session_/Player/Controls/Controls.tsx +++ b/frontend/app/components/Session_/Player/Controls/Controls.tsx @@ -70,7 +70,7 @@ function getStorageName(type: any) { function Controls(props: any) { const { player, store } = React.useContext(PlayerContext); - const { uxtestingStore, uiPlayerStore } = useStore(); + const { uxtestingStore, uiPlayerStore, projectsStore } = useStore(); const fullscreen = uiPlayerStore.fullscreen; const bottomBlock = uiPlayerStore.bottomBlock; const toggleBottomBlock = uiPlayerStore.toggleBottomBlock; @@ -80,6 +80,7 @@ function Controls(props: any) { const skipInterval = uiPlayerStore.skipInterval; const showStorageRedux = !uiPlayerStore.hiddenHints.storage; const history = useHistory(); + const siteId = projectsStore.siteId; const { playing, completed, @@ -95,7 +96,6 @@ function Controls(props: any) { session, previousSessionId, nextSessionId, - siteId, setActiveTab, } = props; @@ -440,7 +440,6 @@ export default connect( totalAssistSessions: state.getIn(['liveSearch', 'total']), previousSessionId: state.getIn(['sessions', 'previousId']), nextSessionId: state.getIn(['sessions', 'nextId']), - siteId: state.getIn(['site', 'siteId']), }; }, { diff --git a/frontend/app/components/Session_/Subheader.js b/frontend/app/components/Session_/Subheader.js index 970963140..65164ee94 100644 --- a/frontend/app/components/Session_/Subheader.js +++ b/frontend/app/components/Session_/Subheader.js @@ -1,34 +1,40 @@ +import { ShareAltOutlined } from '@ant-design/icons'; +import { Button as AntButton, Popover, Switch, Tooltip } from 'antd'; +import cn from 'classnames'; +import { Link2 } from 'lucide-react'; +import { observer } from 'mobx-react-lite'; import React, { useMemo } from 'react'; +import { connect } from 'react-redux'; + +import { PlayerContext } from 'App/components/Session/playerContext'; +import { IFRAME } from 'App/constants/storageKeys'; import { useStore } from 'App/mstore'; +import { checkParam, truncateStringToFit } from 'App/utils'; +import SessionTabs from 'Components/Session/Player/SharedComponents/SessionTabs'; import KeyboardHelp from 'Components/Session_/Player/Controls/components/KeyboardHelp'; import { Icon } from 'UI'; -import {Link2} from 'lucide-react'; -import QueueControls from './QueueControls'; + import Bookmark from 'Shared/Bookmark'; + import SharePopup from '../shared/SharePopup/SharePopup'; import Issues from './Issues/Issues'; +import QueueControls from './QueueControls'; import NotePopup from './components/NotePopup'; -import { PlayerContext } from 'App/components/Session/playerContext'; -import { observer } from 'mobx-react-lite'; -import { connect } from 'react-redux'; -import SessionTabs from 'Components/Session/Player/SharedComponents/SessionTabs'; -import { IFRAME } from 'App/constants/storageKeys'; -import cn from 'classnames'; -import { Switch, Button as AntButton, Popover, Tooltip } from 'antd'; -import { ShareAltOutlined } from '@ant-design/icons'; -import { checkParam, truncateStringToFit } from 'App/utils'; const localhostWarn = (project) => project + '_localhost_warn'; const disableDevtools = 'or_devtools_uxt_toggle'; function SubHeader(props) { - const localhostWarnKey = localhostWarn(props.siteId); - const defaultLocalhostWarn = localStorage.getItem(localhostWarnKey) !== '1'; + const { uxtestingStore, projectsStore } = useStore(); + const defaultLocalhostWarn = React.useMemo(() => { + const siteId = projectsStore.siteId; + const localhostWarnKey = localhostWarn(siteId); + return localStorage.getItem(localhostWarnKey) !== '1'; + }, [projectsStore.siteId]); const [showWarningModal, setWarning] = React.useState(defaultLocalhostWarn); const { store } = React.useContext(PlayerContext); const { location: currentLocation = 'loading...' } = store.get(); const hasIframe = localStorage.getItem(IFRAME) === 'true'; - const { uxtestingStore } = useStore(); const [hideTools, setHideTools] = React.useState(false); React.useEffect(() => { const hideDevtools = checkParam('hideTools'); @@ -46,10 +52,15 @@ function SubHeader(props) { return integrations.some((i) => i.token); }, [props.integrations]); - const locationTruncated = truncateStringToFit(currentLocation, window.innerWidth - 200); + const locationTruncated = truncateStringToFit( + currentLocation, + window.innerWidth - 200 + ); const showWarning = - currentLocation && /(localhost)|(127.0.0.1)|(0.0.0.0)/.test(currentLocation) && showWarningModal; + currentLocation && + /(localhost)|(127.0.0.1)|(0.0.0.0)/.test(currentLocation) && + showWarningModal; const closeWarning = () => { localStorage.setItem(localhostWarnKey, '1'); setWarning(false); @@ -65,7 +76,11 @@ function SubHeader(props) {

{showWarning ? ( @@ -77,7 +92,7 @@ function SubHeader(props) { left: '50%', bottom: '-24px', transform: 'translate(-50%, 0)', - fontWeight: 500 + fontWeight: 500, }} > Some assets may load incorrectly on localhost. @@ -114,7 +129,10 @@ function SubHeader(props) { trigger={
- + @@ -141,8 +159,8 @@ function SubHeader(props) { {locationTruncated && (
- - + + {locationTruncated} @@ -155,7 +173,6 @@ function SubHeader(props) { } export default connect((state) => ({ - siteId: state.getIn(['site', 'siteId']), integrations: state.getIn(['issues', 'list']), - modules: state.getIn(['user', 'account', 'modules']) || [] + modules: state.getIn(['user', 'account', 'modules']) || [], }))(observer(SubHeader)); diff --git a/frontend/app/components/shared/NoSessionsMessage/NoSessionsMessage.js b/frontend/app/components/shared/NoSessionsMessage/NoSessionsMessage.js index 1a8e271be..cd8b36203 100644 --- a/frontend/app/components/shared/NoSessionsMessage/NoSessionsMessage.js +++ b/frontend/app/components/shared/NoSessionsMessage/NoSessionsMessage.js @@ -1,23 +1,19 @@ import React from 'react'; import { Alert, Space, Button } from 'antd'; -import { connect } from 'react-redux'; +import { observer } from 'mobx-react-lite' +import { useStore } from "App/mstore"; import { onboarding as onboardingRoute } from 'App/routes'; import { withRouter } from 'react-router-dom'; import * as routes from '../../../routes'; -import { indigo } from 'tailwindcss/colors'; import { SquareArrowOutUpRight } from 'lucide-react'; import { useHistory } from 'react-router'; - const withSiteId = routes.withSiteId; -const indigoWithOpacity = `rgba(${parseInt(indigo[500].slice(1, 3), 16)}, ${parseInt(indigo[500].slice(3, 5), 16)}, ${parseInt(indigo[500].slice(5, 7), 16)}, 0.1)`; // 0.5 is the opacity level - -const NoSessionsMessage = (props) => { - const { - sites, - siteId - } = props; +const NoSessionsMessage = () => { + const { projectsStore } = useStore(); + const sites = projectsStore.list; + const siteId = projectsStore.siteId; const history = useHistory(); const activeSite = sites.find((s) => s.id === siteId); const showNoSessions = !!activeSite && !activeSite.recorded; @@ -60,7 +56,4 @@ const NoSessionsMessage = (props) => { ); }; -export default connect((state) => ({ - site: state.getIn(['site', 'siteId']), - sites: state.getIn(['site', 'list']) -}))(withRouter(NoSessionsMessage)); +export default withRouter(observer(NoSessionsMessage)); diff --git a/frontend/app/components/shared/SessionItem/PlayLink/PlayLink.tsx b/frontend/app/components/shared/SessionItem/PlayLink/PlayLink.tsx index 8c0543c5f..deab8d8a4 100644 --- a/frontend/app/components/shared/SessionItem/PlayLink/PlayLink.tsx +++ b/frontend/app/components/shared/SessionItem/PlayLink/PlayLink.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useState } from 'react'; -import { connect } from 'react-redux'; import { useHistory } from 'react-router'; import { @@ -8,6 +7,7 @@ import { withSiteId, } from 'App/routes'; import { Icon, Link } from 'UI'; +import { useStore } from 'App/mstore'; const PLAY_ICON_NAMES = { notPlayed: 'play-fill', @@ -30,6 +30,7 @@ interface Props { siteId?: string; } function PlayLink(props: Props) { + const { projectsStore } = useStore(); const { isAssist, viewed, sessionId, onClick = null, queryParams } = props; const history = useHistory(); const defaultIconName = getDefaultIconName(viewed); @@ -47,9 +48,10 @@ function PlayLink(props: Props) { : sessionRoute(sessionId); const handleBeforeOpen = (e: any) => { + const projectId = props.siteId ?? projectsStore.getSiteId().siteId!; const replayLink = withSiteId( link + (props.query ? props.query : ''), - props.siteId + projectId ); if (props.beforeOpen) { // check for ctrl or shift @@ -86,6 +88,4 @@ function PlayLink(props: Props) { ); } -export default connect((state: any, props: Props) => ({ - siteId: props.siteId || state.getIn(['site', 'siteId']), -}))(PlayLink); +export default PlayLink diff --git a/frontend/app/components/shared/SessionSearch/SessionSearch.tsx b/frontend/app/components/shared/SessionSearch/SessionSearch.tsx index f4038f396..19eda3727 100644 --- a/frontend/app/components/shared/SessionSearch/SessionSearch.tsx +++ b/frontend/app/components/shared/SessionSearch/SessionSearch.tsx @@ -28,8 +28,9 @@ interface Props { } function SessionSearch(props: Props) { - const { tagWatchStore, aiFiltersStore } = useStore(); - const { appliedFilter, saveRequestPayloads = false, metaLoading = false } = props; + const { tagWatchStore, aiFiltersStore, projectsStore } = useStore(); + const saveRequestPayloads = projectsStore.instance?.saveRequestPayloads ?? false + const { appliedFilter, metaLoading = false } = props; const hasEvents = appliedFilter.filters.filter((i: any) => i.isEvent).size > 0; const hasFilters = appliedFilter.filters.filter((i: any) => !i.isEvent).size > 0; @@ -154,7 +155,6 @@ function SessionSearch(props: Props) { export default connect( (state: any) => ({ - saveRequestPayloads: state.getIn(['site', 'instance', 'saveRequestPayloads']), appliedFilter: state.getIn(['search', 'instance']), metaLoading: state.getIn(['customFields', 'fetchRequestActive', 'loading']), }), diff --git a/frontend/app/components/shared/SessionSettings/SessionSettings.tsx b/frontend/app/components/shared/SessionSettings/SessionSettings.tsx index 787ced443..c0298e9ce 100644 --- a/frontend/app/components/shared/SessionSettings/SessionSettings.tsx +++ b/frontend/app/components/shared/SessionSettings/SessionSettings.tsx @@ -3,9 +3,12 @@ import ListingVisibility from './components/ListingVisibility'; import DefaultPlaying from './components/DefaultPlaying'; import DefaultTimezone from './components/DefaultTimezone'; import CaptureRate from './components/CaptureRate'; -import { connect } from 'react-redux'; +import { observer } from 'mobx-react-lite'; +import { useStore } from 'App/mstore'; -function SessionSettings({ projectId }: { projectId: number }) { +function SessionSettings() { + const { projectsStore } = useStore(); + const projectId = projectsStore.siteId; return (
@@ -32,6 +35,4 @@ function SessionSettings({ projectId }: { projectId: number }) { ); } -export default connect((state: any) => ({ - projectId: state.getIn(['site', 'siteId']) -}))(SessionSettings); +export default observer(SessionSettings) diff --git a/frontend/app/components/shared/SessionSettings/components/CaptureRate.tsx b/frontend/app/components/shared/SessionSettings/components/CaptureRate.tsx index 215a2d26b..232733a91 100644 --- a/frontend/app/components/shared/SessionSettings/components/CaptureRate.tsx +++ b/frontend/app/components/shared/SessionSettings/components/CaptureRate.tsx @@ -11,7 +11,7 @@ import ConditionalRecordingSettings from 'Shared/SessionSettings/components/Cond type Props = { isAdmin: boolean; isEnterprise?: boolean; - projectId?: number; + projectId?: string; setShowCaptureRate: (show: boolean) => void; open: boolean; showCaptureRate: boolean; diff --git a/frontend/app/components/shared/SiteDropdown/SiteDropdown.js b/frontend/app/components/shared/SiteDropdown/SiteDropdown.js index 44ebebd57..0b3916c32 100644 --- a/frontend/app/components/shared/SiteDropdown/SiteDropdown.js +++ b/frontend/app/components/shared/SiteDropdown/SiteDropdown.js @@ -1,9 +1,12 @@ import React from 'react'; -import { connect } from 'react-redux'; +import { observer } from 'mobx-react-lite' +import { useStore } from 'App/mstore' import Select from 'Shared/Select'; -const SiteDropdown = ({ contextName = '', sites, onChange, value }) => { - const options = sites.map(site => ({ value: site.id, label: site.host })).toJS(); +const SiteDropdown = ({ contextName = '', onChange, value }) => { + const { projectsStore } = useStore(); + const sites = projectsStore.list; + const options = sites.map(site => ({ value: site.id, label: site.host })); return (