Skip to content

Commit

Permalink
Merge pull request #2413 from Agenta-AI/AGE-1486/-illegal-react-opera…
Browse files Browse the repository at this point in the history
…tion-due-to-changed-order-of-rendered-hooks

(frontend)[AGE-1486]: Illegal react operation due to changed order of rendered hooks
  • Loading branch information
mmabrouk authored Jan 24, 2025
2 parents 3af3b04 + 879c876 commit c6772f8
Show file tree
Hide file tree
Showing 11 changed files with 231 additions and 69 deletions.
15 changes: 4 additions & 11 deletions agenta-web/src/components/Layout/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useEffect, useMemo, useState} from "react"
import React, {useEffect, useMemo} from "react"
import {
Breadcrumb,
Button,
Expand Down Expand Up @@ -27,7 +27,8 @@ import {JSSTheme, StyleProps as MainStyleProps} from "@/lib/Types"
import {Lightning} from "@phosphor-icons/react"
import packageJsonData from "../../../package.json"
import {useProjectData} from "@/contexts/project.context"
import {dynamicComponent, dynamicContext} from "@/lib/helpers/dynamic"
import {useOrgData} from "@/contexts/org.context"
import {dynamicComponent} from "@/lib/helpers/dynamic"

const Sidebar: any = dynamicComponent("Sidebar/Sidebar", () => <Skeleton className="w-[236px]" />)

Expand Down Expand Up @@ -139,16 +140,8 @@ const App: React.FC<LayoutProps> = ({children}) => {
const isDarkTheme = appTheme === "dark"
const {token} = theme.useToken()
const [modal, contextHolder] = Modal.useModal()

const [useOrgData, setUseOrgData] = useState<Function>(() => () => "")
const {changeSelectedOrg} = useOrgData()

useEffect(() => {
dynamicContext("org.context", {useOrgData}).then((context) => {
setUseOrgData(() => context.useOrgData)
})
}, [])

useEffect(() => {
if (user && isDemo()) {
;(window as any).intercomSettings = {
Expand Down Expand Up @@ -235,7 +228,7 @@ const App: React.FC<LayoutProps> = ({children}) => {

const handleBackToWorkspaceSwitch = () => {
const project = projects.find((p) => p.user_role === "owner")
if (project && !project.is_demo) {
if (project && !project.is_demo && project.organization_id) {
changeSelectedOrg(project.organization_id)
}
}
Expand Down
11 changes: 3 additions & 8 deletions agenta-web/src/components/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ import {useProfileData} from "@/contexts/profile.context"
import {useSession} from "@/hooks/useSession"
import {CaretDown, Gear, SidebarSimple, SignOut} from "@phosphor-icons/react"
import AlertPopup from "../AlertPopup/AlertPopup"
import {dynamicContext} from "@/lib/helpers/dynamic"
import Avatar from "@/components/Avatar/Avatar"
import {useProjectData} from "@/contexts/project.context"
import {useOrgData} from "@/contexts/org.context"
import clsx from "clsx"
import {ItemType} from "antd/es/menu/interface"

const {Sider} = Layout
const {Text} = Typography
Expand Down Expand Up @@ -189,16 +190,9 @@ const Sidebar: React.FC = () => {
const {user} = useProfileData()
const {logout} = useSession()
const {project} = useProjectData()
const [useOrgData, setUseOrgData] = useState<Function>(() => () => "")
const {selectedOrg, orgs, changeSelectedOrg} = useOrgData()
const [isHovered, setIsHovered] = useState(false)

useEffect(() => {
dynamicContext("org.context", {useOrgData}).then((context) => {
setUseOrgData(() => context.useOrgData)
})
}, [])

const {topItems, bottomItems} = useMemo(() => {
const topItems: SidebarConfig[] = []
const bottomItems: SidebarConfig[] = []
Expand Down Expand Up @@ -329,6 +323,7 @@ const Sidebar: React.FC = () => {
<Dropdown
trigger={["hover"]}
menu={{
// @ts-ignore
items: dropdownItems,
selectedKeys: [selectedOrg.id],
onClick: ({key}) => {
Expand Down
10 changes: 1 addition & 9 deletions agenta-web/src/components/Sidebar/config.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {useAppId} from "@/hooks/useAppId"
import {useSession} from "@/hooks/useSession"
import {dynamicContext} from "@/lib/helpers/dynamic"
import {isDemo} from "@/lib/helpers/utils"
import {AppstoreOutlined, DatabaseOutlined, RocketOutlined, GithubFilled} from "@ant-design/icons"
import {useEffect, useState} from "react"
Expand All @@ -18,6 +17,7 @@ import {
TreeView,
} from "@phosphor-icons/react"
import {useAppsData} from "@/contexts/app.context"
import {useOrgData} from "@/contexts/org.context"

export type SidebarConfig = {
key: string
Expand All @@ -41,14 +41,6 @@ export const useSidebarConfig = () => {
const {doesSessionExist} = useSession()
const {currentApp, recentlyVisitedAppId} = useAppsData()
const isOss = !isDemo()
const [useOrgData, setUseOrgData] = useState<Function>(() => () => "")

useEffect(() => {
dynamicContext("org.context", {useOrgData}).then((context) => {
setUseOrgData(() => context.useOrgData)
})
}, [])

const {selectedOrg} = useOrgData()

const sidebarConfig: SidebarConfig[] = [
Expand Down
11 changes: 2 additions & 9 deletions agenta-web/src/components/pages/app-management/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {useAppsData} from "@/contexts/app.context"
import {useProfileData} from "@/contexts/profile.context"
import {usePostHogAg} from "@/lib/helpers/analytics/hooks/usePostHogAg"
import {type LlmProvider} from "@/lib/helpers/llmProviders"
import {dynamicComponent, dynamicContext} from "@/lib/helpers/dynamic"
import {dynamicComponent} from "@/lib/helpers/dynamic"
import dayjs from "dayjs"
import {useAppTheme} from "@/components/Layout/ThemeContextProvider"
import HelpAndSupportSection from "./components/HelpAndSupportSection"
Expand All @@ -18,6 +18,7 @@ import ApplicationManagementSection from "./components/ApplicationManagementSect
import ResultComponent from "@/components/ResultComponent/ResultComponent"
import {useProjectData} from "@/contexts/project.context"
import {useVaultSecret} from "@/hooks/useVaultSecret"
import {useOrgData} from "@/contexts/org.context"

const CreateAppStatusModal: any = dynamicComponent(
"pages/app-management/modals/CreateAppStatusModal",
Expand Down Expand Up @@ -85,17 +86,9 @@ const AppManagement: React.FC = () => {
appId: undefined,
})
const {secrets} = useVaultSecret()

const {project} = useProjectData()
const [useOrgData, setUseOrgData] = useState<Function>(() => () => "")
const {selectedOrg} = useOrgData()

useEffect(() => {
dynamicContext("org.context", {useOrgData}).then((context) => {
setUseOrgData(() => context.useOrgData)
})
}, [])

useEffect(() => {
if (!isLoading) mutate()
const fetchTemplates = async () => {
Expand Down
9 changes: 1 addition & 8 deletions agenta-web/src/contexts/app.context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import {axiosFetcher} from "@/services/api"
import {useRouter} from "next/router"
import {PropsWithChildren, createContext, useContext, useEffect, useMemo, useState} from "react"
import useSWR from "swr"
import {dynamicContext} from "@/lib/helpers/dynamic"
import {HookAPI} from "antd/es/modal/useModal"
import {useLocalStorage} from "usehooks-ts"
import {useProfileData} from "./profile.context"
import {useProjectData, DEFAULT_UUID} from "./project.context"
import {useOrgData} from "./org.context"

type AppContextType = {
currentApp: ListAppsItem | null
Expand All @@ -32,16 +32,9 @@ const initialValues: AppContextType = {
}

const useApps = () => {
const [useOrgData, setUseOrgData] = useState<Function>(() => () => "")
const {projectId} = useProjectData()
const {user} = useProfileData()

useEffect(() => {
dynamicContext("org.context", {useOrgData}).then((context) => {
setUseOrgData(() => context.useOrgData)
})
}, [])

const isMockProjectId = projectId === DEFAULT_UUID

const {selectedOrg, loading} = useOrgData()
Expand Down
146 changes: 146 additions & 0 deletions agenta-web/src/contexts/org.context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import {useSession} from "@/hooks/useSession"
import useStateCallback from "@/hooks/useStateCallback"
import {isDemo} from "@/lib/helpers/utils"
import {fetchSingleOrg, fetchAllOrgsList} from "@/services/organization/api"
import {Org, OrgDetails} from "@/lib/Types"
import {useRouter} from "next/router"
import React, {
PropsWithChildren,
createContext,
useState,
useContext,
useEffect,
useCallback,
} from "react"
import {useUpdateEffect} from "usehooks-ts"
import {useProfileData} from "@/contexts/profile.context"

const LS_ORG_KEY = "selectedOrg"

type OrgContextType = {
orgs: Org[]
selectedOrg: OrgDetails | null
loading: boolean
changeSelectedOrg: (orgId: string, onSuccess?: () => void) => void
setSelectedOrg: React.Dispatch<React.SetStateAction<OrgDetails | null>>
reset: () => void
refetch: (onSuccess?: () => void) => void
}

const initialValues: OrgContextType = {
orgs: [],
selectedOrg: null,
loading: false,
changeSelectedOrg: () => {},
setSelectedOrg: () => {},
reset: () => {},
refetch: () => {},
}

export const OrgContext = createContext<OrgContextType>(initialValues)

export const useOrgData = () => useContext(OrgContext)

const orgContextValues = {...initialValues}

export const getOrgValues = () => orgContextValues

const OrgContextProvider: React.FC<PropsWithChildren> = ({children}) => {
const [orgs, setOrgs] = useStateCallback<Org[]>([])
const [selectedOrg, setSelectedOrg] = useStateCallback<OrgDetails | null>(null)
const [loadingOrgs, setLoadingOrgs] = useState(false)
const [loadingOrgDetails, setLoadingOrgDetails] = useState(false)
const {logout, doesSessionExist} = useSession()
const {user} = useProfileData()
const router = useRouter()

const fetchAllOrgs = useCallback((onSuccess?: () => void) => {
setLoadingOrgs(true)
fetchAllOrgsList()
.then((orgs) => {
setOrgs(orgs, onSuccess)
})
.catch((error) => {
console.error(error)
if (isDemo()) logout()
})
.finally(() => setLoadingOrgs(false))
}, [])

useUpdateEffect(() => {
if (user?.id && orgs.length > 0) {
setLoadingOrgDetails(true)
const org =
orgs.find((org: Org) => org.id === localStorage.getItem(LS_ORG_KEY)) ||
orgs.find((org: Org) => org.owner === user.id) ||
orgs[0]
if (org) {
fetchSingleOrg({orgId: org.id})
.then(setSelectedOrg)
.catch(console.error)
.finally(() => setLoadingOrgDetails(false))
} else {
setSelectedOrg(null)
setLoadingOrgDetails(false)
}
}
}, [user?.id, orgs])

useUpdateEffect(() => {
localStorage.setItem(LS_ORG_KEY, selectedOrg?.id || "")
}, [selectedOrg?.id])

useEffect(() => {
// fetch profile and orgs list only if user is logged in
if (doesSessionExist && isDemo()) {
fetchAllOrgs()
}
}, [doesSessionExist])

if (!isDemo()) {
return <OrgContext.Provider value={initialValues}>{children}</OrgContext.Provider>
}

const changeSelectedOrg: OrgContextType["changeSelectedOrg"] = (orgId, onSuccess) => {
setLoadingOrgDetails(true)
const org = orgs.find((org) => org.id === orgId) || selectedOrg
fetchSingleOrg({orgId: org?.id!})
.then((data) => {
setSelectedOrg(data)
if (onSuccess) {
onSuccess()
}
router.push("/apps")
})
.finally(() => setLoadingOrgDetails(false))
.catch(console.error)
}

const reset = () => {
setOrgs(initialValues.orgs)
setSelectedOrg(initialValues.selectedOrg)
}

orgContextValues.orgs = orgs
orgContextValues.selectedOrg = selectedOrg
orgContextValues.changeSelectedOrg = changeSelectedOrg
orgContextValues.setSelectedOrg = setSelectedOrg

return (
<OrgContext.Provider
value={{
orgs,
selectedOrg,
loading: loadingOrgs || loadingOrgDetails,
changeSelectedOrg,
setSelectedOrg,
reset,
refetch: fetchAllOrgs,
}}
>
{children}
</OrgContext.Provider>
)
}

export default OrgContextProvider
9 changes: 1 addition & 8 deletions agenta-web/src/contexts/project.context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import {useSession} from "@/hooks/useSession"
import {PropsWithChildren, createContext, useState, useContext, useEffect} from "react"
import {fetchAllProjects} from "@/services/project"
import useStateCallback from "@/hooks/useStateCallback"
import {dynamicContext} from "@/lib/helpers/dynamic"
import {isDemo} from "@/lib/helpers/utils"
import {ProjectsResponse} from "@/services/project/types"
import {useOrgData} from "./org.context"

export const DEFAULT_UUID = "00000000-0000-0000-0000-000000000000"

Expand Down Expand Up @@ -39,16 +39,9 @@ export const getCurrentProject = () => projectContextValues
const ProjectContextProvider: React.FC<PropsWithChildren> = ({children}) => {
const [project, setProject] = useStateCallback<ProjectsResponse | null>(null)
const [projects, setProjects] = useState<ProjectsResponse[]>([])
const [useOrgData, setUseOrgData] = useState<Function>(() => () => "")
const [isLoading, setIsLoading] = useState(false)
const {doesSessionExist} = useSession()

useEffect(() => {
dynamicContext("org.context", {useOrgData}).then((context) => {
setUseOrgData(() => context.useOrgData)
})
}, [])

const {selectedOrg} = useOrgData()

const workspaceId: string = selectedOrg?.default_workspace.id || DEFAULT_UUID
Expand Down
43 changes: 43 additions & 0 deletions agenta-web/src/lib/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -705,3 +705,46 @@ export type FilterConditions =
| "is_not"
| "btwn"
| ""

export interface WorkspaceRole {
role_description: string
role_name: string
}

export interface WorkspaceUser {
id: string
email: string
username: string
status: "member" | "pending" | "expired"
created_at: string
}

export interface WorkspaceMember {
user: WorkspaceUser
roles: (WorkspaceRole & {permissions: string[]})[]
}

export interface Workspace {
id: string
name: string
description: string
created_at: string
updated_at: string
organization: string
type: "default"
members: WorkspaceMember[]
}

export interface Org {
id: string
name: string
description: string
owner: string
is_paying: boolean
}

export type OrgDetails = Org & {
type: "default"
default_workspace: Workspace
workspaces: string[]
}
Loading

0 comments on commit c6772f8

Please sign in to comment.