From 81ecbac892485483273ec782b5564400e00c43eb Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Wed, 18 Sep 2024 16:49:59 +0200 Subject: [PATCH] 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(); + } +}