Sync with Langflow Store

This commit is contained in:
Lucas Oliveira 2023-11-18 18:06:33 -03:00
commit a81abb4c2d
4 changed files with 353 additions and 250 deletions

View file

@ -3,6 +3,7 @@ import { alertContext } from "../../contexts/alertContext";
import { FlowsContext } from "../../contexts/flowsContext"; import { FlowsContext } from "../../contexts/flowsContext";
import { StoreContext } from "../../contexts/storeContext"; import { StoreContext } from "../../contexts/storeContext";
import { getComponent, postLikeComponent } from "../../controllers/API"; import { getComponent, postLikeComponent } from "../../controllers/API";
import DeleteConfirmationModal from "../../modals/DeleteConfirmationModal";
import { storeComponent } from "../../types/store"; import { storeComponent } from "../../types/store";
import cloneFLowWithParent from "../../utils/storeUtils"; import cloneFLowWithParent from "../../utils/storeUtils";
import { cn } from "../../utils/utils"; import { cn } from "../../utils/utils";
@ -166,13 +167,17 @@ export default function CollectionCardComponent({
</div> </div>
)} )}
{onDelete && ( {onDelete && !data.metadata && (
<button onClick={onDelete}> <DeleteConfirmationModal
onConfirm={() => {
onDelete();
}}
>
<IconComponent <IconComponent
name="Trash2" name="Trash2"
className="h-5 w-5 text-primary opacity-0 transition-all hover:text-destructive group-hover:opacity-100" className="h-5 w-5 text-primary opacity-0 transition-all hover:text-destructive group-hover:opacity-100"
/> />
</button> </DeleteConfirmationModal>
)} )}
</CardTitle> </CardTitle>
</div> </div>
@ -207,36 +212,69 @@ export default function CollectionCardComponent({
</div> </div>
{data.liked_by_count != undefined && ( {data.liked_by_count != undefined && (
<div className="flex gap-0.5"> <div className="flex gap-0.5">
<ShadTooltip {onDelete && data.metadata ? (
content={authorized ? "Like" : "Please review your API key."} <ShadTooltip
> content={
<Button authorized ? "Delete" : "Please review your API key."
disabled={loadingLike}
variant="ghost"
size="xs"
className={
"whitespace-nowrap" +
(!authorized ? " cursor-not-allowed" : "")
} }
onClick={() => {
if (!authorized) {
return;
}
handleLike();
}}
> >
<IconComponent <DeleteConfirmationModal
name="Heart" onConfirm={() => {
className={cn( onDelete();
"h-5 w-5", }}
liked_by_user >
? "fill-destructive stroke-destructive" <Button
: "", variant="ghost"
!authorized ? " text-ring" : "" size="xs"
)} className={
/> "whitespace-nowrap" +
</Button> (!authorized ? " cursor-not-allowed" : "")
</ShadTooltip> }
>
<IconComponent
name="Trash2"
className={cn(
"h-5 w-5",
!authorized ? " text-ring" : ""
)}
/>
</Button>
</DeleteConfirmationModal>
</ShadTooltip>
) : (
<ShadTooltip
content={
authorized ? "Like" : "Please review your API key."
}
>
<Button
disabled={loadingLike}
variant="ghost"
size="xs"
className={
"whitespace-nowrap" +
(!authorized ? " cursor-not-allowed" : "")
}
onClick={() => {
if (!authorized) {
return;
}
handleLike();
}}
>
<IconComponent
name="Heart"
className={cn(
"h-5 w-5",
liked_by_user
? "fill-destructive stroke-destructive"
: "",
!authorized ? " text-ring" : ""
)}
/>
</Button>
</ShadTooltip>
)}
<ShadTooltip <ShadTooltip
content={ content={
authorized authorized

View file

@ -0,0 +1,26 @@
import Header from "../headerComponent";
import { Separator } from "../ui/separator";
export default function PageLayout({
title,
description,
children,
}: {
title: string;
description: string;
children: React.ReactNode;
}) {
return (
<div className="flex h-screen w-full flex-col">
<Header />
<div className="flex h-full w-full flex-col justify-between overflow-auto bg-background px-16">
<div className="flex w-full flex-col justify-between space-y-0.5 py-8 pb-2">
<h2 className="text-2xl font-bold tracking-tight">{title}</h2>
<p className="text-muted-foreground">{description}</p>
</div>
<Separator className="my-6 flex" />
{children}
</div>
</div>
);
}

View file

@ -0,0 +1,56 @@
import { DialogClose } from "@radix-ui/react-dialog";
import { Trash2 } from "lucide-react";
import { Button } from "../../components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../../components/ui/dialog";
export default function DeleteConfirmationModal({
children,
onConfirm,
description,
}: {
children: JSX.Element;
onConfirm: () => void;
description?: string;
}) {
return (
<Dialog>
<DialogTrigger>{children}</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
<div className="flex">
Delete <Trash2 className="ml-3 h-4 w-4" strokeWidth={1.5} />
</div>
</DialogTitle>
<DialogDescription>
Are you sure you want to delete this {description ?? "component"}?
<br></br>
This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose>
<Button className="mr-3">Cancel</Button>
<Button
type="submit"
onClick={() => {
onConfirm();
}}
>
Delete
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View file

@ -4,10 +4,9 @@ import PaginatorComponent from "../../components/PaginatorComponent";
import ShadTooltip from "../../components/ShadTooltipComponent"; import ShadTooltip from "../../components/ShadTooltipComponent";
import CollectionCardComponent from "../../components/cardComponent"; import CollectionCardComponent from "../../components/cardComponent";
import IconComponent from "../../components/genericIconComponent"; import IconComponent from "../../components/genericIconComponent";
import Header from "../../components/headerComponent"; import PageLayout from "../../components/pageLayout";
import { SkeletonCardComponent } from "../../components/skeletonCardComponent"; import { SkeletonCardComponent } from "../../components/skeletonCardComponent";
import { Badge } from "../../components/ui/badge"; import { Badge } from "../../components/ui/badge";
import { Button } from "../../components/ui/button";
import { Input } from "../../components/ui/input"; import { Input } from "../../components/ui/input";
import { import {
Select, Select,
@ -21,7 +20,6 @@ import { alertContext } from "../../contexts/alertContext";
import { AuthContext } from "../../contexts/authContext"; import { AuthContext } from "../../contexts/authContext";
import { StoreContext } from "../../contexts/storeContext"; import { StoreContext } from "../../contexts/storeContext";
import { getStoreComponents, getStoreTags } from "../../controllers/API"; import { getStoreComponents, getStoreTags } from "../../controllers/API";
import StoreApiKeyModal from "../../modals/StoreApiKeyModal";
import { storeComponent } from "../../types/store"; import { storeComponent } from "../../types/store";
import { cn } from "../../utils/utils"; import { cn } from "../../utils/utils";
export default function StorePage(): JSX.Element { export default function StorePage(): JSX.Element {
@ -197,237 +195,222 @@ export default function StorePage(): JSX.Element {
}; };
return ( return (
<> <PageLayout
<Header /> title="Langflow Store"
description="Search flows and components from the community."
<div className="flex h-full w-full flex-col justify-between overflow-auto bg-background px-16"> /* button={{StoreApiKeyModal && (
<div> <StoreApiKeyModal disabled={loading}>
<div className="flex w-full justify-between py-8 pb-2"> <Button
<span className="flex items-center justify-center gap-2 text-2xl font-semibold"> disabled={loading}
<IconComponent name="Store" className="w-6" /> className={cn(
Langflow Store `${!validApiKey ? "animate-pulse border-error" : ""}`,
</span> loading ? "cursor-not-allowed" : ""
<div className="flex gap-2"> )}
{StoreApiKeyModal && ( variant="primary"
<StoreApiKeyModal disabled={loading}> >
<Button <IconComponent name="Key" className="mr-2 w-4" />
disabled={loading} API Key
className={cn( </Button>
`${!validApiKey ? "animate-pulse border-error" : ""}`, </StoreApiKeyModal>
loading ? "cursor-not-allowed" : "" )}} */
)} >
variant="primary" <div className="flex h-full w-full flex-col justify-between">
> <div className="flex w-full flex-col gap-4 p-0">
<IconComponent name="Key" className="mr-2 w-4" /> <div className="flex items-end gap-4">
API Key <div className="relative h-12 w-[40%]">
</Button> <Input
</StoreApiKeyModal> disabled={loading}
)} placeholder="Search Flows and Components"
</div> className="absolute h-12 pl-5 pr-12"
</div> onChange={(e) => {
<span className="flex w-[70%] pb-8 text-muted-foreground"> setInputText(e.target.value);
Search flows and components from the community. }}
</span> onKeyDown={(e) => {
<div className="flex w-full flex-col gap-4 p-0"> if (e.key === "Enter") {
<div className="flex items-end gap-4">
<div className="relative h-12 w-[40%]">
<Input
disabled={loading}
placeholder="Search Flows and Components"
className="absolute h-12 pl-5 pr-12"
onChange={(e) => {
setInputText(e.target.value);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
setSearchNow(uniqueId());
}
}}
value={inputText}
/>
<button
disabled={loading}
className="absolute bottom-0 right-4 top-0 my-auto h-6 cursor-pointer stroke-1 text-muted-foreground"
onClick={() => {
setSearchNow(uniqueId()); setSearchNow(uniqueId());
}}
>
<IconComponent
name={loading ? "Loader2" : "Search"}
className={
loading ? " animate-spin cursor-not-allowed" : ""
}
/>
</button>
</div>
<div className="ml-4 flex w-full gap-2 border-b border-border">
<button
disabled={loading}
onClick={() => {
setTabActive("All");
}}
className={
(tabActive === "All"
? "border-b-2 border-primary p-3"
: " border-b-2 border-transparent p-3 text-muted-foreground hover:text-primary") +
(loading ? " cursor-not-allowed " : "")
} }
> }}
All value={inputText}
</button> />
<button <button
disabled={loading}
onClick={() => {
setTabActive("Flows");
}}
className={
(tabActive === "Flows"
? "border-b-2 border-primary p-3"
: " border-b-2 border-transparent p-3 text-muted-foreground hover:text-primary") +
(loading ? " cursor-not-allowed " : "")
}
>
Flows
</button>
<button
disabled={loading}
onClick={() => {
setTabActive("Components");
}}
className={
(tabActive === "Components"
? "border-b-2 border-primary p-3"
: " border-b-2 border-transparent p-3 text-muted-foreground hover:text-primary") +
(loading ? " cursor-not-allowed " : "")
}
>
Components
</button>
<ShadTooltip content="Coming Soon">
<button className="cursor-not-allowed p-3 text-muted-foreground">
Bundles
</button>
</ShadTooltip>
</div>
</div>
<div className="flex items-center gap-2">
<Select
disabled={loading} disabled={loading}
onValueChange={setSelectFilter} className="absolute bottom-0 right-4 top-0 my-auto h-6 cursor-pointer stroke-1 text-muted-foreground"
value={selectFilter} onClick={() => {
> setSearchNow(uniqueId());
<SelectTrigger className="mr-4 w-[160px] flex-shrink-0">
<SelectValue placeholder="Filter Values" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="all">All</SelectItem>
<SelectItem value="createdbyme">Created By Me</SelectItem>
<SelectItem value="likedbyme">Liked By Me</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<div ref={fadeContainerRef} className="fade-container">
<div
ref={scrollContainerRef}
className="scroll-container flex gap-2"
>
{!loadingTags &&
tags.map((tag, idx) => (
<button
disabled={loading}
className={loading ? "cursor-not-allowed" : ""}
onClick={() => {
updateTags(tag.name);
}}
>
<Badge
key={idx}
variant="outline"
size="sq"
className={cn(
filteredCategories.some(
(category) => category === tag.name
)
? "bg-beta-foreground text-background hover:bg-beta-foreground"
: ""
)}
>
{tag.name}
</Badge>
</button>
))}
</div>
</div>
</div>
<div className="flex items-end justify-between">
<span className="px-0.5 text-sm text-muted-foreground">
{(!loading || searchData.length !== 0) && (
<>
{totalRowsCount}{" "}
{totalRowsCount !== 1 ? "results" : "result"}
</>
)}
</span>
<Select
disabled={loading}
onValueChange={(e) => {
setPageOrder(e);
}} }}
> >
<SelectTrigger> <IconComponent
<SelectValue placeholder="Popular" /> name={loading ? "Loader2" : "Search"}
</SelectTrigger> className={loading ? " animate-spin cursor-not-allowed" : ""}
<SelectContent> />
<SelectItem value="Popular">Popular</SelectItem> </button>
{/* <SelectItem value="Recent">Most Recent</SelectItem> */}
<SelectItem value="Alphabetical">Alphabetical</SelectItem>
</SelectContent>
</Select>
</div> </div>
<div className="ml-4 flex w-full gap-2 border-b border-border">
<button
disabled={loading}
onClick={() => {
setTabActive("All");
}}
className={
(tabActive === "All"
? "border-b-2 border-primary p-3"
: " border-b-2 border-transparent p-3 text-muted-foreground hover:text-primary") +
(loading ? " cursor-not-allowed " : "")
}
>
All
</button>
<button
disabled={loading}
onClick={() => {
setTabActive("Flows");
}}
className={
(tabActive === "Flows"
? "border-b-2 border-primary p-3"
: " border-b-2 border-transparent p-3 text-muted-foreground hover:text-primary") +
(loading ? " cursor-not-allowed " : "")
}
>
Flows
</button>
<button
disabled={loading}
onClick={() => {
setTabActive("Components");
}}
className={
(tabActive === "Components"
? "border-b-2 border-primary p-3"
: " border-b-2 border-transparent p-3 text-muted-foreground hover:text-primary") +
(loading ? " cursor-not-allowed " : "")
}
>
Components
</button>
<ShadTooltip content="Coming Soon">
<button className="cursor-not-allowed p-3 text-muted-foreground">
Bundles
</button>
</ShadTooltip>
</div>
</div>
<div className="grid w-full gap-4 md:grid-cols-2 lg:grid-cols-3"> <div className="flex items-center gap-2">
{!loading || searchData.length !== 0 ? ( <Select
searchData.map((item) => { disabled={loading}
return ( onValueChange={setSelectFilter}
<> value={selectFilter}
<CollectionCardComponent >
key={item.id} <SelectTrigger className="mr-4 w-[160px] flex-shrink-0">
data={item} <SelectValue placeholder="Filter Values" />
authorized={validApiKey} </SelectTrigger>
disabled={loading} <SelectContent>
/> <SelectGroup>
</> <SelectItem value="all">All</SelectItem>
); <SelectItem value="createdbyme">Created By Me</SelectItem>
}) <SelectItem value="likedbyme">Liked By Me</SelectItem>
) : ( </SelectGroup>
</SelectContent>
</Select>
<div ref={fadeContainerRef} className="fade-container">
<div
ref={scrollContainerRef}
className="scroll-container flex gap-2"
>
{!loadingTags &&
tags.map((tag, idx) => (
<button
disabled={loading}
className={loading ? "cursor-not-allowed" : ""}
onClick={() => {
updateTags(tag.name);
}}
>
<Badge
key={idx}
variant="outline"
size="sq"
className={cn(
filteredCategories.some(
(category) => category === tag.name
)
? "bg-beta-foreground text-background hover:bg-beta-foreground"
: ""
)}
>
{tag.name}
</Badge>
</button>
))}
</div>
</div>
</div>
<div className="flex items-end justify-between">
<span className="px-0.5 text-sm text-muted-foreground">
{(!loading || searchData.length !== 0) && (
<> <>
<SkeletonCardComponent /> {totalRowsCount} {totalRowsCount !== 1 ? "results" : "result"}
<SkeletonCardComponent />
<SkeletonCardComponent />
</> </>
)} )}
</div> </span>
{!loading && searchData?.length === 0 && ( <Select
<div className="mt-6 flex w-full items-center justify-center text-center"> disabled={loading}
<div className="flex h-full w-full flex-col"> onValueChange={(e) => {
<div className="flex w-full flex-col gap-4"> setPageOrder(e);
<div className="grid w-full gap-4"> }}
You haven't{" "} >
{selectFilter === "createdbyme" ? "created" : "liked"}{" "} <SelectTrigger>
anything yet. <SelectValue placeholder="Popular" />
</div> </SelectTrigger>
<SelectContent>
<SelectItem value="Popular">Popular</SelectItem>
{/* <SelectItem value="Recent">Most Recent</SelectItem> */}
<SelectItem value="Alphabetical">Alphabetical</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid w-full gap-4 md:grid-cols-2 lg:grid-cols-3">
{!loading || searchData.length !== 0 ? (
searchData.map((item) => {
return (
<>
<CollectionCardComponent
key={item.id}
data={item}
authorized={validApiKey}
disabled={loading}
/>
</>
);
})
) : (
<>
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
</>
)}
</div>
{!loading && searchData?.length === 0 && (
<div className="mt-6 flex w-full items-center justify-center text-center">
<div className="flex h-full w-full flex-col">
<div className="flex w-full flex-col gap-4">
<div className="grid w-full gap-4">
You haven't{" "}
{selectFilter === "createdbyme" ? "created" : "liked"}{" "}
anything yet.
</div> </div>
</div> </div>
</div> </div>
)} </div>
</div> )}
</div> </div>
{!loading && searchData.length > 0 && ( {!loading && searchData.length > 0 && (
<div className="relative my-6"> <div className="relative py-6">
<PaginatorComponent <PaginatorComponent
storeComponent={true} storeComponent={true}
pageIndex={pageIndex} pageIndex={pageIndex}
@ -441,6 +424,6 @@ export default function StorePage(): JSX.Element {
</div> </div>
)} )}
</div> </div>
</> </PageLayout>
); );
} }