Skip to content

Commit

Permalink
✨ Add api key auth in magic link and embedded catalog component
Browse files Browse the repository at this point in the history
  • Loading branch information
mit-27 committed Jun 25, 2024
1 parent e8f6655 commit b3c868c
Show file tree
Hide file tree
Showing 18 changed files with 1,292 additions and 51 deletions.
5 changes: 5 additions & 0 deletions apps/embedded-catalog/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,15 @@
"dependencies": {
"@panora/shared": "workspace:^",
"lucide-react": "^0.344.0",
"@radix-ui/react-label": "^2.0.2",
"react-hook-form": "^7.51.2",
"@hookform/resolvers": "^3.3.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-slot": "^1.0.2",
"@tanstack/react-query": "^5.12.2",
"class-variance-authority": "^0.7.0",
"zod": "^3.22.4",
"clsx": "^2.1.0",
"react-loader-spinner": "^5.4.5",
"tailwind-merge": "^2.2.1"
Expand Down
7 changes: 4 additions & 3 deletions apps/embedded-catalog/react/src/components/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react'
import {X} from 'lucide-react'

const Modal = ({open,setOpen,children} : {open:boolean,setOpen: React.Dispatch<React.SetStateAction<boolean>>,children: React.ReactNode}) => {
return (
Expand All @@ -17,12 +18,12 @@ const Modal = ({open,setOpen,children} : {open:boolean,setOpen: React.Dispatch<R
${open ? "scale-100 opacity-100" : "scale-125 opacity-0"}
`}
>
{/* <button
<button
onClick={() => setOpen(false)}
className="absolute top-2 right-2 p-1 rounded-lg text-gray-400 bg-[#1d1d1d] hover:text-gray-600"
className="absolute top-2 right-2 p-1 rounded-lg text-gray-400 bg-[#1d1d1d]"
>
<X color='gray' />
</button> */}
</button>
{children}
</div>
</div>
Expand Down
203 changes: 180 additions & 23 deletions apps/embedded-catalog/react/src/components/PanoraDynamicCatalog.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {useState,useEffect} from 'react'
import {providersArray, ConnectorCategory, categoryFromSlug, Provider,CONNECTORS_METADATA} from '@panora/shared';
import {providersArray, ConnectorCategory, categoryFromSlug, Provider,CONNECTORS_METADATA, AuthStrategy} from '@panora/shared';
import useOAuth from '@/hooks/useOAuth';
import useProjectConnectors from '@/hooks/queries/useProjectConnectors';
import { Card } from './ui/card';
Expand All @@ -8,13 +8,45 @@ import { ArrowRightIcon } from '@radix-ui/react-icons';
import {ArrowLeftRight} from 'lucide-react'
import Modal from './Modal';
import config from '@/helpers/config';
import { useForm } from "react-hook-form";
import * as z from "zod";
import { Input } from "@/components/ui/input";
import { zodResolver } from "@hookform/resolvers/zod";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import useCreateApiKeyConnection from '@/hooks/queries/useCreateApiKeyConnection';
import { LoadingSpinner } from './ui/loading-spinner';


export interface DynamicCardProp {
projectId: string;
linkedUserId: string;
category?: ConnectorCategory;
optionalApiUrl?: string,
}


const formSchema = z.object({
apiKey: z.string().min(2, {
message: "Api Key must be at least 2 characters.",
})
})

const DynamicCatalog = ({projectId,linkedUserId, category, optionalApiUrl} : DynamicCardProp) => {

// by default we render all integrations but if category is provided we filter by category
Expand All @@ -24,19 +56,29 @@ const DynamicCatalog = ({projectId,linkedUserId, category, optionalApiUrl} : Dyn
category: string;
}>();

const [loading, setLoading] = useState<{
status: boolean; provider: string
}>({status: false, provider: ''});
const [loading, setLoading] = useState<boolean>(false);

const [error,setError] = useState(false);
const [startFlow, setStartFlow] = useState(false);
const [errorResponse,setErrorResponse] = useState<{
errorPresent: boolean; errorMessage : string
}>({errorPresent:false,errorMessage:''});
const [startFlow, setStartFlow] = useState<boolean>(false);
const [openSuccessDialog,setOpenSuccessDialog] = useState<boolean>(false);
const [openApiKeyDialog,setOpenApiKeyDialog] = useState<boolean>(false);
const [currentProviderLogoURL,setCurrentProviderLogoURL] = useState<string>('')
const [currentProvider,setCurrentProvider] = useState<string>('')
const returnUrlWithWindow = (typeof window !== 'undefined')
? window.location.href
: '';

const {mutate : createApiKeyConnection} = useCreateApiKeyConnection();


const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
apiKey: "",
},
})

const [data, setData] = useState<Provider[]>([]);

Expand All @@ -56,25 +98,26 @@ const DynamicCatalog = ({projectId,linkedUserId, category, optionalApiUrl} : Dyn
const {data: connectorsForProject} = useProjectConnectors(projectId,optionalApiUrl ? optionalApiUrl : config.API_URL!);

const onWindowClose = () => {
setSelectedProvider({
provider: '',
category: ''
});
setLoading({
status: false,
provider: ''
})
// setSelectedProvider({
// provider: '',
// category: ''
// });
setLoading(false)
setStartFlow(false);
}

useEffect(() => {
if (startFlow && isReady) {
open(onWindowClose);
} else if (startFlow && !isReady) {
setLoading({
status: false,
provider: ''
setErrorResponse({errorPresent:false,errorMessage:''});

open(onWindowClose)
.catch((error : Error) => {
setLoading(false);
setStartFlow(false);
setErrorResponse({errorPresent:true,errorMessage:error.message})
});
} else if (startFlow && !isReady) {
setLoading(false);
}
}, [startFlow, isReady]);

Expand Down Expand Up @@ -102,8 +145,17 @@ const DynamicCatalog = ({projectId,linkedUserId, category, optionalApiUrl} : Dyn
const logoPath = CONNECTORS_METADATA[category.toLowerCase()][walletName.toLowerCase()].logoPath;
setCurrentProviderLogoURL(logoPath)
setCurrentProvider(walletName.toLowerCase())
setLoading({status: true, provider: selectedProvider?.provider!});
setStartFlow(true);
if(CONNECTORS_METADATA[category.toLowerCase()][walletName.toLowerCase()].authStrategy.strategy===AuthStrategy.api_key)
{
setOpenApiKeyDialog(true);
}
else
{
setLoading(true);
setStartFlow(true);
}


}

function transformConnectorsStatus(connectors : {[key: string]: boolean}): { connector_name: string;category: string; status: string }[] {
Expand All @@ -121,14 +173,54 @@ const DynamicCatalog = ({projectId,linkedUserId, category, optionalApiUrl} : Dyn
});
}

const onApiKeySubmit = (values: z.infer<typeof formSchema>) => {
setErrorResponse({errorPresent:false,errorMessage:''});
setOpenApiKeyDialog(false);
setLoading(true);

// Creating API Key Connection
createApiKeyConnection({
query : {
linkedUserId: linkedUserId,
projectId: projectId,
providerName: selectedProvider?.provider!,
vertical: selectedProvider?.category!
},
data: {
apikey: values.apiKey
},
api_url: optionalApiUrl ?? config.API_URL!
},
{
onSuccess: () => {
// setSelectedProvider({
// provider: '',
// category: ''
// });

setLoading(false);
setOpenSuccessDialog(true);
},
onError: (error) => {
setErrorResponse({errorPresent:true,errorMessage: error.message});
setLoading(false);
// setSelectedProvider({
// provider: '',
// category: ''
// });
}
})
}


return (
<div className="flex flex-col gap-2 pt-0">
{data && data.map((item) => {
return (
<Card
key={`${item.name}-${item.vertical}`}
className= "flex flex-col border w-1/2 items-start gap-2 rounded-lg p-3 text-left text-sm transition-all hover:border-stone-100"
className={`flex flex-col border w-1/2 items-start gap-2 rounded-lg p-3 text-left text-sm transition-all
${ (item.name.toLowerCase()===selectedProvider?.provider && item.vertical?.toLowerCase()===selectedProvider?.category) && errorResponse.errorPresent ? 'border-red-600' : 'hover:border-stone-400'}`}
>
<div className="flex w-full items-start justify-between">
<div className="flex flex-col gap-1">
Expand All @@ -140,10 +232,36 @@ const DynamicCatalog = ({projectId,linkedUserId, category, optionalApiUrl} : Dyn
{item.description!.substring(0, 300)}
</div>
<div className="line-clamp-2 mt-2 text-xs text-muted-foreground">
<Button className='h-7 gap-1' size="sm" variant="expandIcon" Icon={ArrowRightIcon} iconPlacement="right" onClick={() => handleStartFlow(item.name, item.vertical!)} >
{ (item.name.toLowerCase()===selectedProvider?.provider && item.vertical?.toLowerCase()===selectedProvider?.category) && loading ? (
<Button size="sm" className='h-7 gap-1' variant="default" disabled>
<LoadingSpinner/>
Connecting
</Button>
)
:
(
<Button className='h-7 gap-1' size="sm" variant="expandIcon" Icon={ArrowRightIcon} iconPlacement="right" onClick={() => handleStartFlow(item.name, item.vertical!)} >
Connect
</Button>
)}
{/* {loading ? (
<Button size="sm" variant="default" disabled>
<LoadingSpinner className='mr-2'/>
Connecting
</Button>
)
:
(
<Button size="sm" variant="ringHover" onClick={() => handleStartFlow(item.name, item.vertical!)} >
Connect
</Button>
)} */}
{/* <Button className='h-7 gap-1' size="sm" variant="expandIcon" Icon={ArrowRightIcon} iconPlacement="right" onClick={() => handleStartFlow(item.name, item.vertical!)} >
Connect
</Button> */}
</div>

{item.name.toLowerCase()===selectedProvider?.provider && item.vertical?.toLowerCase()===selectedProvider?.category && errorResponse.errorPresent ? <p className='mt-2 text-xs text-red-700'>{errorResponse.errorMessage}</p> : (<></>)}

</div>
<div>
Expand All @@ -153,6 +271,45 @@ const DynamicCatalog = ({projectId,linkedUserId, category, optionalApiUrl} : Dyn
)})
}

{/* Dialog for apikey input */}
<Dialog open={openApiKeyDialog} onOpenChange={setOpenApiKeyDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Enter a API key</DialogTitle>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onApiKeySubmit)}>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<FormField
control={form.control}
name="apiKey"
render={({ field }) => (
<FormItem>
<FormLabel>Enter your API key for {selectedProvider?.provider}</FormLabel>
<FormControl>
<Input
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
placeholder="Your awesome key name" {...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
<DialogFooter>
<Button variant='outline' type="reset" size="sm" className="h-7 gap-1" onClick={() => setOpenApiKeyDialog(false)}>Cancel</Button>
<Button type='submit' size="sm" className="h-7 gap-1">
Continue
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>


{/* OAuth Successful Modal */}
<Modal open={openSuccessDialog} setOpen={setOpenSuccessDialog} >
Expand Down
Loading

0 comments on commit b3c868c

Please sign in to comment.