diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index e651c9538..75728c21f 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,8 +1,8 @@ import "@/styles/globals.css" import { Metadata } from "next" -import SupabaseProvider from "@/providers/supabase" -import SupabaseListener from "@/providers/supabase-listener" +import { SessionContextProvider } from "@/providers/session" +import { WorkflowProvider } from "@/providers/workflow" import { createClient } from "@/utils/supabase/server" import { siteConfig } from "@/config/site" @@ -39,10 +39,9 @@ export default async function RootLayout({ children }: RootLayoutProps) { fontSans.className )} > - - - {children} - + + {children} + diff --git a/frontend/src/app/workflows/[id]/page.tsx b/frontend/src/app/workflows/[id]/page.tsx index ba72406ee..2aa5a60e8 100644 --- a/frontend/src/app/workflows/[id]/page.tsx +++ b/frontend/src/app/workflows/[id]/page.tsx @@ -1,7 +1,6 @@ import { Metadata } from "next" import { cookies } from "next/headers" import { DefaultQueryClientProvider } from "@/providers/query" -import { WorkflowProvider } from "@/providers/workflow" import { Navbar } from "@/components/navbar" import { Workspace } from "@/components/workspace" @@ -31,16 +30,14 @@ export default function DashboardPage() { return ( <> - -
- - -
-
+
+ + +
) diff --git a/frontend/src/app/workflows/page.tsx b/frontend/src/app/workflows/page.tsx index b0ca68ec0..867226c49 100644 --- a/frontend/src/app/workflows/page.tsx +++ b/frontend/src/app/workflows/page.tsx @@ -1,21 +1,26 @@ "use client" -import React, { useEffect } from "react" +import React, { useEffect, useState } from "react" import Link from "next/link" import { useRouter } from "next/navigation" -import { useSupabase } from "@/providers/supabase" -import { User } from "@supabase/supabase-js" +import { useSessionContext } from "@/providers/session" import { Loader2 } from "lucide-react" -import { fetchWorkflows, WorkflowMetadata } from "@/lib/flow" +import { WorkflowMetadata } from "@/types/schemas" +import { fetchWorkflows } from "@/lib/flow" import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" +import NoSSR from "@/components/no-ssr" export default function Page() { - return + return ( + + + + ) } function WorkflowsPage(props: React.HTMLAttributes) { - const { supabase, session } = useSupabase() + const { supabaseClient, session, isLoading } = useSessionContext() if (!session) { return (
) {
) } + const { user } = session const router = useRouter() - const [userWorkflows, setUserWorkflows] = React.useState( - [] - ) - const [user, setUser] = React.useState(null) - const [isLoading, setIsLoading] = React.useState(true) - const [error, setError] = React.useState(null) + const [userWorkflows, setUserWorkflows] = useState([]) const signOut = async () => { - await supabase.auth.signOut() + await supabaseClient.auth.signOut() router.push("/login") router.refresh() - setUser(null) } useEffect(() => { @@ -49,17 +49,6 @@ function WorkflowsPage(props: React.HTMLAttributes) { } }, [user]) - useEffect(() => { - async function getUser() { - const { - data: { user }, - } = await supabase.auth.getUser() - setUser(user) - setIsLoading(false) - } - getUser() - }, []) - if (isLoading || !user) { return (
) { ) } - if (error) { - return ( -
-
Error: {error.message}
-
- ) - } - return ( userWorkflows.length > 0 && (
{} +export function Navbar(props: NavbarProps) { const params = useParams() - const workflowId = params["id"] + const [workflowId, setWorkflowId] = useState(params["id"] as string) const pathname = usePathname() + const [enableWorkflow, setEnableWorkflow] = useState(false) useEffect(() => { const updateWorkflowStatus = async () => { @@ -42,16 +44,19 @@ export function Navbar() { } updateWorkflowStatus() - }, [enableWorkflow, workflowId]) + }, [enableWorkflow]) return ( -
+
-
+
{/* TODO: Ensure that workflow switcher doesn't make an API call to update workflows when page is switched between workflow view and cases view */} - + + + + + + CN + + ) +} diff --git a/frontend/src/components/user-nav.tsx b/frontend/src/components/user-nav.tsx index abb316e07..36f9906fe 100644 --- a/frontend/src/components/user-nav.tsx +++ b/frontend/src/components/user-nav.tsx @@ -1,6 +1,6 @@ -import { KeyRound, LogOut, Settings, UserRound, UsersRound } from "lucide-react" +import { useSessionContext, useUser } from "@/providers/session" +import { KeyRound, LogOut, Settings, UsersRound } from "lucide-react" -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Button } from "@/components/ui/button" import { DropdownMenu, @@ -11,46 +11,51 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" +import UserAvatar from "@/components/user-avatar" +const userDefaults = { + name: "Test User", + email: "name@example.com", +} export function UserNav() { + const { signOut } = useSessionContext() + const user = useUser() + console.log("user", user) return (
-

shadcn

+

+ {user?.user_metadata.name ?? userDefaults.name} +

- m@example.com + {user?.email ?? userDefaults.email}

- + - Settings + Settings - + Credentials - + Manage users - + Logout diff --git a/frontend/src/components/workflow-switcher.tsx b/frontend/src/components/workflow-switcher.tsx index 116951b83..37434b3af 100644 --- a/frontend/src/components/workflow-switcher.tsx +++ b/frontend/src/components/workflow-switcher.tsx @@ -1,7 +1,7 @@ "use client" -import React from "react" -import { useParams } from "next/navigation" +import React, { useEffect } from "react" +import { useParams, useRouter } from "next/navigation" import { useWorkflowMetadata } from "@/providers/workflow" import { zodResolver } from "@hookform/resolvers/zod" import { @@ -9,16 +9,12 @@ import { CheckIcon, PlusCircledIcon, } from "@radix-ui/react-icons" -import { useQuery } from "@tanstack/react-query" +import { useQuery, useQueryClient } from "@tanstack/react-query" import { useForm } from "react-hook-form" import { z } from "zod" -import { - createWorkflow, - fetchWorkflow, - fetchWorkflows, - WorkflowMetadata, -} from "@/lib/flow" +import { WorkflowMetadata, workflowMetadataSchema } from "@/types/schemas" +import { createWorkflow, fetchWorkflow, fetchWorkflows } from "@/lib/flow" import { cn } from "@/lib/utils" import { Avatar, AvatarImage } from "@/components/ui/avatar" import { Button } from "@/components/ui/button" @@ -31,6 +27,7 @@ import { } from "@/components/ui/command" import { Dialog, + DialogClose, DialogContent, DialogDescription, DialogFooter, @@ -65,8 +62,8 @@ const newWorkflowFormSchema = z.object({ type WorkflowFormInputs = z.infer export default function WorkflowSwitcher({ className }: WorkflowSwitcherProps) { - const { setWorkflowMetadata } = useWorkflowMetadata() const [open, setOpen] = React.useState(false) + const { setWorkflowMetadata } = useWorkflowMetadata() const [showNewWorkflowDialog, setShowNewWorkflowDialog] = React.useState(false) const params = useParams<{ id: string }>() @@ -81,13 +78,16 @@ export default function WorkflowSwitcher({ className }: WorkflowSwitcherProps) { queryKey: ["workflow", workflowId], queryFn: async ({ queryKey }) => { const [_, workflowId] = queryKey as [string, string] - console.log(workflowId) const data = await fetchWorkflow(workflowId) - setWorkflowMetadata(data) + const validatedData = workflowMetadataSchema.parse(data) + setWorkflowMetadata(validatedData) return data }, }) + const queryClient = useQueryClient() + const router = useRouter() + const form = useForm({ resolver: zodResolver(newWorkflowFormSchema), }) @@ -99,13 +99,16 @@ export default function WorkflowSwitcher({ className }: WorkflowSwitcherProps) { // like showing a notification, refreshing a list of workflows, or resetting the form form.reset() // Add here any action like closing modal or refreshing the workflow list + queryClient.invalidateQueries({ + queryKey: ["workflows"], + }) } catch (error) { console.error("Failed to create workflow", error) // Handle error, maybe set an error state or show a toast notification } } - if (!workflowMetadata || !workflows) { + if (!workflows) { return } @@ -123,18 +126,20 @@ export default function WorkflowSwitcher({ className }: WorkflowSwitcherProps) { aria-label="Select a team" className={cn("w-96 justify-between", className)} > - {workflowMetadata.title} + {workflowMetadata?.title} - + {workflows.map((workflow) => ( { + setWorkflowMetadata(workflow) + router.push(`/workflows/${workflow.id}`) setOpen(false) }} className="text-xs" @@ -151,7 +156,9 @@ export default function WorkflowSwitcher({ className }: WorkflowSwitcherProps) { @@ -195,14 +202,20 @@ export default function WorkflowSwitcher({ className }: WorkflowSwitcherProps) { Workflow Name - + )} /> - + + + diff --git a/frontend/src/providers/session.tsx b/frontend/src/providers/session.tsx new file mode 100644 index 000000000..795589944 --- /dev/null +++ b/frontend/src/providers/session.tsx @@ -0,0 +1,211 @@ +"use client" + +import React, { + createContext, + PropsWithChildren, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from "react" +import { useRouter } from "next/navigation" +import { createClient } from "@/utils/supabase/client" +import { AuthError, Session, SupabaseClient } from "@supabase/supabase-js" + +export type SessionContext = + | { + isLoading: true + session: null + error: null + supabaseClient: SupabaseClient + signOut: () => void + } + | { + isLoading: false + session: Session + error: null + supabaseClient: SupabaseClient + signOut: () => void + } + | { + isLoading: false + session: null + error: AuthError + supabaseClient: SupabaseClient + signOut: () => void + } + | { + isLoading: false + session: null + error: null + supabaseClient: SupabaseClient + signOut: () => void + } + +const SessionContext = createContext({ + isLoading: true, + session: null, + error: null, + supabaseClient: {} as any, + signOut: () => {}, +}) + +export interface SessionContextProviderProps { + initialSession?: Session | null +} + +export const SessionContextProvider = ({ + initialSession = null, + children, +}: PropsWithChildren) => { + const [supabaseClient] = useState(() => createClient()) + const [session, setSession] = useState(initialSession) + const [isLoading, setIsLoading] = useState(!initialSession) + const [error, setError] = useState() + const router = useRouter() + + useEffect(() => { + if (!session && initialSession) { + setSession(initialSession) + } + }, [session, initialSession]) + + useEffect(() => { + let mounted = true + + async function getSession() { + const { + data: { session }, + error, + } = await supabaseClient.auth.getSession() + + // only update the react state if the component is still mounted + if (mounted) { + if (error) { + setError(error) + setIsLoading(false) + return + } + + setSession(session) + setIsLoading(false) + } + } + + getSession() + + return () => { + mounted = false + } + }, []) + + useEffect(() => { + const { + data: { subscription }, + } = supabaseClient.auth.onAuthStateChange((event, session) => { + if ( + session && + (event === "SIGNED_IN" || + event === "TOKEN_REFRESHED" || + event === "USER_UPDATED") + ) { + setSession(session) + } + + if (event === "SIGNED_OUT") { + setSession(null) + } + }) + + return () => { + subscription.unsubscribe() + } + }, []) + + const signOut = useCallback(async () => { + await supabaseClient.auth.signOut() + router.push("/login") + router.refresh() + }, [supabaseClient]) + + const value: SessionContext = useMemo(() => { + const constant = { + supabaseClient, + signOut, + } + if (isLoading) { + return { + isLoading: true, + session: null, + error: null, + ...constant, + } + } + + if (error) { + return { + isLoading: false, + session: null, + error, + ...constant, + } + } + + return { + isLoading: false, + session, + error: null, + ...constant, + } + }, [isLoading, session, error]) + + return ( + {children} + ) +} + +export const useSessionContext = () => { + const context = useContext(SessionContext) + if (context === undefined) { + throw new Error( + `useSessionContext must be used within a SessionContextProvider.` + ) + } + + return context +} + +export function useSupabaseClient< + Database = any, + SchemaName extends string & keyof Database = "public" extends keyof Database + ? "public" + : string & keyof Database, +>() { + const context = useContext(SessionContext) + if (context === undefined) { + throw new Error( + `useSupabaseClient must be used within a SessionContextProvider.` + ) + } + + return context.supabaseClient as SupabaseClient +} + +export const useSession = () => { + const context = useContext(SessionContext) + if (context === undefined) { + throw new Error(`useSession must be used within a SessionContextProvider.`) + } + + return context.session +} + +export const useUser = () => { + const context = useContext(SessionContext) + if (context === undefined) { + throw new Error(`useUser must be used within a SessionContextProvider.`) + } + + return context.session?.user ?? null +} diff --git a/frontend/src/providers/workflow.tsx b/frontend/src/providers/workflow.tsx index 8aa677f4c..0d09af48e 100644 --- a/frontend/src/providers/workflow.tsx +++ b/frontend/src/providers/workflow.tsx @@ -1,17 +1,22 @@ "use client" -import React, { createContext, ReactNode, useContext, useState } from "react" +import React, { + createContext, + ReactNode, + useContext, + useEffect, + useState, +} from "react" +import axios from "axios" -export type WorkflowMetadata = { - id?: string - title?: string - description?: string - status?: string -} +import { WorkflowMetadata } from "@/types/schemas" +type MaybeWorkflowMetadata = WorkflowMetadata | null type WorkflowContextType = { - workflowMetadata: WorkflowMetadata - setWorkflowMetadata: (workflow: WorkflowMetadata) => void + workflowMetadata: MaybeWorkflowMetadata + setWorkflowMetadata: (workflow: MaybeWorkflowMetadata) => void + workflowId: string | null + setWorkflowId: (id: string) => void } const WorkflowContext = createContext( @@ -21,15 +26,30 @@ const WorkflowContext = createContext( export const WorkflowProvider: React.FC<{ children: ReactNode }> = ({ children, }) => { - const [workflowMetadata, setWorkflowMetadata] = useState({ - id: undefined, - title: undefined, - description: undefined, - status: undefined, - }) - + const [workflowMetadata, setWorkflowMetadata] = + useState(null) + const [workflowId, setWorkflowId] = useState(null) + useEffect(() => { + async function fetchWorkflowId(id: string) { + const response = await axios.get( + `http://localhost:8000/workflows/${id}` + ) + setWorkflowMetadata(response.data) + } + if (workflowId) { + console.log("fetching workflow id", workflowId) + fetchWorkflowId(workflowId) + } + }, [workflowId]) return ( - + {children} )