Marketplace and API key modal inserted
This commit is contained in:
parent
c5ef201755
commit
439c331939
8 changed files with 382 additions and 0 deletions
|
|
@ -64,6 +64,16 @@ export default function Header(): JSX.Element {
|
|||
<div className="flex-1">Community Examples</div>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/store">
|
||||
<Button
|
||||
className="gap-2"
|
||||
variant={location.pathname === "/store" ? "primary" : "secondary"}
|
||||
size="sm"
|
||||
>
|
||||
<IconComponent name="Store" className="h-4 w-4" />
|
||||
<div className="flex-1">Store</div>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="header-end-division">
|
||||
<div className="header-end-display">
|
||||
|
|
|
|||
92
src/frontend/src/modals/StoreApiKeyModal/index.tsx
Normal file
92
src/frontend/src/modals/StoreApiKeyModal/index.tsx
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import * as Form from "@radix-ui/react-form";
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Input } from "../../components/ui/input";
|
||||
import { CONTROL_NEW_API_KEY } from "../../constants/constants";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import {
|
||||
ApiKeyInputType,
|
||||
StoreApiKeyType,
|
||||
inputHandlerEventType,
|
||||
} from "../../types/components";
|
||||
import BaseModal from "../baseModal";
|
||||
|
||||
export default function StoreApiKeyModal({
|
||||
children,
|
||||
onCloseModal,
|
||||
}: StoreApiKeyType) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [apiKeyValue, setApiKeyValue] = useState("");
|
||||
const [inputState, setInputState] =
|
||||
useState<ApiKeyInputType>(CONTROL_NEW_API_KEY);
|
||||
const { setSuccessData } = useContext(alertContext);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
function handleInput({
|
||||
target: { name, value },
|
||||
}: inputHandlerEventType): void {
|
||||
setInputState((prev) => ({ ...prev, [name]: value }));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
resetForm();
|
||||
} else {
|
||||
onCloseModal();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
function resetForm() {
|
||||
setApiKeyValue("");
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseModal size="small-h-full" open={open} setOpen={setOpen}>
|
||||
<BaseModal.Trigger>{children}</BaseModal.Trigger>
|
||||
<BaseModal.Header description={"Insert your Langflow API key."}>
|
||||
<span className="pr-2">API Key</span>
|
||||
<IconComponent
|
||||
name="Key"
|
||||
className="h-6 w-6 pl-1 text-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content>
|
||||
<Form.Root
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<div className="grid gap-5">
|
||||
<Form.Field name="username">
|
||||
<Form.Control asChild>
|
||||
<Input
|
||||
onChange={({ target: { value } }) => {
|
||||
handleInput({ target: { name: "apikey", value } });
|
||||
}}
|
||||
placeholder="Insert your API Key"
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Field>
|
||||
</div>
|
||||
<div className="float-right">
|
||||
<Button
|
||||
className="mr-3"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Form.Submit asChild>
|
||||
<Button className="mt-8">Save</Button>
|
||||
</Form.Submit>
|
||||
</div>
|
||||
</Form.Root>
|
||||
</BaseModal.Content>
|
||||
</BaseModal>
|
||||
);
|
||||
}
|
||||
94
src/frontend/src/pages/StorePage/components/market-card.tsx
Normal file
94
src/frontend/src/pages/StorePage/components/market-card.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import { Link, ToyBrick } from "lucide-react";
|
||||
import { Badge } from "../../../components/ui/badge";
|
||||
import {
|
||||
Card,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "../../../components/ui/card";
|
||||
import { FlowComponent } from "../../../types/store";
|
||||
|
||||
export const MarketCardComponent = ({
|
||||
data,
|
||||
onAdd,
|
||||
}: {
|
||||
data: FlowComponent;
|
||||
onAdd: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<Card className="group relative flex cursor-pointer flex-col justify-between overflow-hidden transition-all hover:shadow-md">
|
||||
<div>
|
||||
<CardHeader>
|
||||
{/*
|
||||
<div className="mb-2 flex flex-wrap gap-1">
|
||||
{data.tags.map((tag) => (
|
||||
<Badge
|
||||
size="sm"
|
||||
className={
|
||||
tagGradients[parseInt(tag, 35) % tagGradients.length] +
|
||||
" " +
|
||||
tagText[parseInt(tag, 35) % tagText.length]
|
||||
}
|
||||
>
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div> */}
|
||||
<div>
|
||||
<CardTitle className="flex w-full items-center justify-between gap-3 text-xl">
|
||||
<span className="flex w-full items-center gap-2 word-break-break-word">
|
||||
{data.name}
|
||||
</span>
|
||||
<Badge size="sm" variant="gray">
|
||||
Free
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
{/* <span className="inline-flex items-center text-sm">
|
||||
<img
|
||||
className="mr-2 h-4 w-4 rounded-full"
|
||||
src={data.image}
|
||||
/>
|
||||
{data.creator.name}
|
||||
</span>
|
||||
<span className="flex text-xs items-center gap-2 text-ring">
|
||||
<Download className="h-3 w-3" />
|
||||
{nFormatter(data.downloads, 2)}
|
||||
</span> */}
|
||||
</div>
|
||||
<CardDescription className="pb-2 pt-2">
|
||||
<div className="truncate-doubleline">{data.description}</div>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</div>
|
||||
|
||||
<CardFooter>
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<div className="flex w-full flex-wrap items-end justify-between gap-2">
|
||||
<div className=" flex items-center gap-3">
|
||||
<Badge size="md" variant="outline">
|
||||
chain
|
||||
<Link className="ml-1.5 w-3 text-green-700" />
|
||||
</Badge>
|
||||
<span className="flex items-center gap-1.5 text-xs text-foreground">
|
||||
<ToyBrick className="h-4 w-4" />
|
||||
123
|
||||
</span>
|
||||
</div>
|
||||
{/* {data.isChat ? (
|
||||
<Button size="sm" variant="outline">
|
||||
<Plus className="h-4 mr-2" />
|
||||
Add
|
||||
</Button>
|
||||
) : (
|
||||
<Button size="sm" variant="success">
|
||||
<Check className="h-4 mr-2" />
|
||||
Added
|
||||
</Button>
|
||||
)} */}
|
||||
</div>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
157
src/frontend/src/pages/StorePage/index.tsx
Normal file
157
src/frontend/src/pages/StorePage/index.tsx
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import { cloneDeep } from "lodash";
|
||||
import { Link, Search } from "lucide-react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import Header from "../../components/headerComponent";
|
||||
import { Badge } from "../../components/ui/badge";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Input } from "../../components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../../components/ui/select";
|
||||
import { Switch } from "../../components/ui/switch";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import StoreApiKeyModal from "../../modals/StoreApiKeyModal";
|
||||
import { FlowComponent } from "../../types/store";
|
||||
import { cn } from "../../utils/utils";
|
||||
import { MarketCardComponent } from "./components/market-card";
|
||||
export default function StorePage(): JSX.Element {
|
||||
const { flows, setTabId, downloadFlows, uploadFlows, addFlow } =
|
||||
useContext(TabsContext);
|
||||
|
||||
// set null id
|
||||
useEffect(() => {
|
||||
setTabId("");
|
||||
}, []);
|
||||
const [data, setData] = useState<FlowComponent[]>([]);
|
||||
const [dataSelect, setDataSelect] = useState<FlowComponent[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [filteredCategories, setFilteredCategories] = useState(new Set());
|
||||
const [inputText, setInputText] = useState<string>("");
|
||||
const [searchData, setSearchData] = useState(data);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(false);
|
||||
/* getComponents()
|
||||
.then((res) => {
|
||||
setData(res);
|
||||
setSearchData(res);
|
||||
setDataSelect(res);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
}); */
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
|
||||
<div className="community-page-arrangement">
|
||||
<div className="community-page-nav-arrangement">
|
||||
<span className="community-page-nav-title">
|
||||
<IconComponent name="Users2" className="w-6" />
|
||||
Langflow Store
|
||||
</span>
|
||||
<div className="community-page-nav-button">
|
||||
<StoreApiKeyModal onCloseModal={() => {}}>
|
||||
<Button variant="primary">
|
||||
<IconComponent name="Key" className="main-page-nav-button" />
|
||||
API Key
|
||||
</Button>
|
||||
</StoreApiKeyModal>
|
||||
</div>
|
||||
</div>
|
||||
<span className="community-page-description-text">
|
||||
Search flows and components from the community.
|
||||
</span>
|
||||
{!loading && (
|
||||
<div className="flex w-full flex-col gap-4 p-4">
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<div className="flex w-[13%] items-center justify-center gap-3 text-sm">
|
||||
Installed Only <Switch />
|
||||
</div>
|
||||
<div className="relative h-12 w-[35%]">
|
||||
<Input
|
||||
placeholder="Search Flows and Components"
|
||||
className="absolute h-12 px-5"
|
||||
onChange={(e) => {}}
|
||||
value={inputText}
|
||||
/>
|
||||
<Search className="absolute bottom-0 right-4 top-0 my-auto h-6 stroke-1 text-muted-foreground " />
|
||||
</div>
|
||||
<div className="flex w-[13%] items-center justify-center gap-3 text-sm">
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
const filter = value === "Flow" ? false : true;
|
||||
const search = data.filter(
|
||||
(f) => f.is_component === filter
|
||||
);
|
||||
value === "" ? setSearchData(data) : setSearchData(search);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Flows" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Flow">Flows</SelectItem>
|
||||
<SelectItem value="Component">Components</SelectItem>
|
||||
<SelectItem value="">Both</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
{Array.from(new Set(searchData.map((i) => i.is_component))).map(
|
||||
(i, idx) => (
|
||||
<Badge
|
||||
onClick={() => {
|
||||
filteredCategories.has(i)
|
||||
? setFilteredCategories((old) => {
|
||||
let newFilteredCategories = cloneDeep(old);
|
||||
newFilteredCategories.delete(i);
|
||||
return newFilteredCategories;
|
||||
})
|
||||
: setFilteredCategories((old) => {
|
||||
let newFilteredCategories = cloneDeep(old);
|
||||
newFilteredCategories.add(i);
|
||||
return newFilteredCategories;
|
||||
});
|
||||
}}
|
||||
variant="gray"
|
||||
size="md"
|
||||
className={cn(
|
||||
"cursor-pointer border-none",
|
||||
filteredCategories.has(i)
|
||||
? "bg-beta-foreground text-background hover:bg-beta-foreground"
|
||||
: ""
|
||||
)}
|
||||
>
|
||||
<Link className="mr-1.5 w-4" />
|
||||
{i}
|
||||
</Badge>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-6 grid w-full gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{searchData
|
||||
.filter(
|
||||
(f) =>
|
||||
Array.from(filteredCategories).length === 0 ||
|
||||
filteredCategories.has(f.is_component)
|
||||
)
|
||||
.map((item, idx) => (
|
||||
<MarketCardComponent key={idx} data={item} onAdd={() => {}} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import CommunityPage from "./pages/CommunityPage";
|
|||
import FlowPage from "./pages/FlowPage";
|
||||
import HomePage from "./pages/MainPage";
|
||||
import ProfileSettingsPage from "./pages/ProfileSettingsPage";
|
||||
import StorePage from "./pages/StorePage";
|
||||
import ViewPage from "./pages/ViewPage";
|
||||
import DeleteAccountPage from "./pages/deleteAccountPage";
|
||||
import LoginPage from "./pages/loginPage";
|
||||
|
|
@ -34,6 +35,14 @@ const Router = () => {
|
|||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/store"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<StorePage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route path="/flow/:id/">
|
||||
<Route
|
||||
path=""
|
||||
|
|
|
|||
|
|
@ -326,6 +326,11 @@ export type ApiKeyType = {
|
|||
onCloseModal: () => void;
|
||||
};
|
||||
|
||||
export type StoreApiKeyType = {
|
||||
children: ReactElement;
|
||||
onCloseModal: () => void;
|
||||
};
|
||||
|
||||
export type ApiKeyInputType = {
|
||||
apikeyname: string;
|
||||
};
|
||||
|
|
|
|||
13
src/frontend/src/types/store/index.ts
Normal file
13
src/frontend/src/types/store/index.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
export type FlowComponent = {
|
||||
id: string;
|
||||
status: string;
|
||||
sort: null | any;
|
||||
user_created: string;
|
||||
date_created: string;
|
||||
user_updated: string;
|
||||
date_updated: string;
|
||||
is_component: boolean;
|
||||
name: string;
|
||||
description: string;
|
||||
data: Object;
|
||||
};
|
||||
|
|
@ -69,6 +69,7 @@ import {
|
|||
Shield,
|
||||
Sparkles,
|
||||
Square,
|
||||
Store,
|
||||
SunIcon,
|
||||
TerminalSquare,
|
||||
Trash2,
|
||||
|
|
@ -285,6 +286,7 @@ export const nodeIconsLucide: iconsType = {
|
|||
Clipboard,
|
||||
Code2,
|
||||
Variable,
|
||||
Store,
|
||||
Download,
|
||||
Eraser,
|
||||
Lock,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue