fix(store.py): flatten the "tags" field in the list_components response to improve data structure and readability

feat(store.py): add new endpoint to get list of components liked by a user
fix(store.py): add "downloads_count" field to the list_components response to display the number of downloads for each component
fix(store.py): add "metadata" field to the list_components response to include additional metadata for each component
fix(store.py): change the "user_created.name" field to "user_created.first_name" in the list_components response to display the first name of the user who created the component
fix(store.py): change the "liked_by_count" field in the component response to be an integer instead of a list of UUIDs
fix(store.py): change the "downloads_count" field in the component response to be an integer instead of a list of UUIDs
fix(store.py): add "metadata" field to the component response to include additional metadata for each component
fix(store.py): add "metadata" field to the store_component_create request schema to allow passing additional metadata for a component
fix(store.py): add "metadata" field to the download_component response schema to include additional metadata for a component
fix(store.py): add "metadata" field to the download_component response schema to include additional metadata for a component
fix(store.py): add "metadata" field to the download_component response schema to include additional metadata for a component
fix(store.py): add "metadata" field to the download_component response schema to include additional metadata for a component
fix(store.py): add "metadata" field to the download_component response schema to include additional metadata for a component
fix(store.py): add "metadata" field to the download_component response schema to include additional metadata for a component
fix(store.py): add "metadata" field to the download_component response schema to include additional metadata for a component
fix(store.py): add "metadata" field to the download_component response schema to include additional metadata for a component
fix(store.py): add "metadata" field to the download_component response schema to include additional metadata for a component
fix(store.py): add "metadata" field to the download_component response schema to include additional metadata for a component
fix(store.py): add "metadata" field to the download_component response schema to include additional metadata for a component
fix(store.py): add "metadata" field to the download_component response schema to include additional metadata for a component
This commit is contained in:
cristhianzl 2023-10-26 19:11:16 -03:00
commit 665dae87e5
9 changed files with 141 additions and 67 deletions

View file

@ -12,6 +12,7 @@ from langflow.services.store.schema import (
ListComponentResponse, ListComponentResponse,
StoreComponentCreate, StoreComponentCreate,
TagResponse, TagResponse,
UsersLikesResponse,
) )
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
@ -77,11 +78,14 @@ def list_components(
"id", "id",
"name", "name",
"description", "description",
"user_created.name", "user_created.first_name",
"user_created.id",
"is_component", "is_component",
"tags.tags_id.name", "tags.tags_id.name",
"tags.tags_id.id", "tags.tags_id.id",
"count(liked_by)", "count(liked_by)",
"count(downloads)",
"metadata",
] ]
result = store_service.query_components( result = store_service.query_components(
store_api_Key, store_api_Key,
@ -90,6 +94,9 @@ def list_components(
fields=fields, fields=fields,
filter_by_user=filter_by_user, filter_by_user=filter_by_user,
) )
# tags comes as "tags" : [{"tags_id": {"name": "tag1", "id": 1}}]
# so we need to flatten it
return result return result
except Exception as exc: except Exception as exc:
raise HTTPException(status_code=400, detail=str(exc)) raise HTTPException(status_code=400, detail=str(exc))
@ -171,3 +178,14 @@ def get_tags(
return store_service.get_tags(store_api_Key) return store_service.get_tags(store_api_Key)
except Exception as exc: except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) raise HTTPException(status_code=500, detail=str(exc))
@router.get("/users/likes", response_model=List[UsersLikesResponse])
def get_list_of_components_liked_by_user(
store_service: StoreService = Depends(get_store_service),
store_api_Key: str = Depends(get_user_store_api_key),
):
try:
return store_service.get_user_likes(store_api_Key)
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc))

View file

@ -9,6 +9,11 @@ class TagResponse(BaseModel):
name: Optional[str] name: Optional[str]
class UsersLikesResponse(BaseModel):
id: UUID
likes: Optional[List[UUID]]
class ComponentResponse(BaseModel): class ComponentResponse(BaseModel):
id: UUID id: UUID
status: Optional[str] status: Optional[str]
@ -22,8 +27,14 @@ class ComponentResponse(BaseModel):
description: Optional[str] description: Optional[str]
data: Optional[dict] data: Optional[dict]
tags: Optional[List[int]] tags: Optional[List[int]]
liked_by_count: Optional[List[UUID]] liked_by_count: Optional[int]
downloads_count: Optional[int]
parent: Optional[UUID] parent: Optional[UUID]
metadata: Optional[dict]
class TagsIdResponse(BaseModel):
tags_id: Optional[TagResponse]
class ListComponentResponse(BaseModel): class ListComponentResponse(BaseModel):
@ -32,6 +43,10 @@ class ListComponentResponse(BaseModel):
description: Optional[str] description: Optional[str]
liked_by_count: Optional[int] liked_by_count: Optional[int]
is_component: Optional[bool] is_component: Optional[bool]
metadata: Optional[dict]
user_created: Optional[dict]
tags: Optional[List[TagsIdResponse]] = None
downloads_count: Optional[int]
class DownloadComponentResponse(BaseModel): class DownloadComponentResponse(BaseModel):
@ -40,6 +55,8 @@ class DownloadComponentResponse(BaseModel):
description: Optional[str] description: Optional[str]
data: Optional[dict] data: Optional[dict]
is_component: Optional[bool] is_component: Optional[bool]
metadata: Optional[dict]
downloads_count: Optional[int]
class StoreComponentCreate(BaseModel): class StoreComponentCreate(BaseModel):
@ -49,3 +66,4 @@ class StoreComponentCreate(BaseModel):
tags: Optional[List[str]] tags: Optional[List[str]]
parent: Optional[UUID] parent: Optional[UUID]
is_component: Optional[bool] is_component: Optional[bool]
metadata: Optional[dict]

View file

@ -152,27 +152,46 @@ class StoreService(Service):
",".join(fields) ",".join(fields)
if fields if fields
else ",".join( else ",".join(
["id", "name", "description", "count(liked_by)", "is_component"] [
"id",
"name",
"description",
"user_created.first_name",
"user_created.id",
"is_component",
"tags.tags_id.name",
"tags.tags_id.id",
"count(liked_by)",
"count(downloads)",
"metadata",
]
) )
) )
# Only public components or the ones created by the user # Only public components or the ones created by the user
# check for "public" or "Public" # check for "public" or "Public"
if filter_by_user: if filter_by_user:
params["deep"] = json.dumps( user_data = self._get(
{ f"{self.base_url}/users/me", api_key, params={"fields": "id"}
"components": {
"_filter": {"user_created": {"token": {"_eq": api_key}}}
}
}
) )
params["filter"] = json.dumps({"user_created": {"_eq": user_data["id"]}})
# Get the
params.pop("page", None)
params.pop("limit", None)
params["fields"] = ["id"]
else: else:
params["filter"] = params["filter"] = json.dumps( params["filter"] = params["filter"] = json.dumps(
{"status": {"_in": ["public", "Public"]}} {"status": {"_in": ["public", "Public"]}}
) )
results = self._get(self.components_url, api_key, params) results = self._get(self.components_url, api_key, params)
return [ListComponentResponse(**component) for component in results] results_objects = [ListComponentResponse(**component) for component in results]
# Flatten the tags
for component in results_objects:
if component.tags:
component.tags = [tags_id.tags_id for tags_id in component.tags]
return results_objects
def download(self, api_key: str, component_id: str) -> DownloadComponentResponse: def download(self, api_key: str, component_id: str) -> DownloadComponentResponse:
url = f"{self.components_url}/{component_id}" url = f"{self.components_url}/{component_id}"
@ -216,3 +235,11 @@ class StoreService(Service):
params = {"fields": ",".join(["id", "name"])} params = {"fields": ",".join(["id", "name"])}
tags = self._get(url, api_key, params) tags = self._get(url, api_key, params)
return tags return tags
def get_user_likes(self, api_key: str) -> List[Dict[str, Any]]:
url = f"{self.base_url}/users/me"
params = {
"fields": ",".join(["id", "likes"]),
}
likes = self._get(url, api_key, params)
return likes

View file

@ -3796,9 +3796,9 @@
"integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==" "integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g=="
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "18.2.32", "version": "18.2.33",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.32.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.33.tgz",
"integrity": "sha512-F0FVIZQ1x5Gxy/VYJb7XcWvCcHR28Sjwt1dXLspdIatfPq1MVACfnBDwKe6ANLxQ64riIJooXClpUR6oxTiepg==", "integrity": "sha512-v+I7S+hu3PIBoVkKGpSYYpiBT1ijqEzWpzQD62/jm4K74hPpSP7FF9BnKG6+fg2+62weJYkkBWDJlZt5JO/9hg==",
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"@types/scheduler": "*", "@types/scheduler": "*",
@ -5237,9 +5237,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.4.566", "version": "1.4.567",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.566.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.567.tgz",
"integrity": "sha512-mv+fAy27uOmTVlUULy15U3DVJ+jg+8iyKH1bpwboCRhtDC69GKf1PPTZvEIhCyDr81RFqfxZJYrbgp933a1vtg==" "integrity": "sha512-8KR114CAYQ4/r5EIEsOmOMqQ9j0MRbJZR3aXD/KFA8RuKzyoUB4XrUCg+l8RUGqTVQgKNIgTpjaG8YHRPAbX2w=="
}, },
"node_modules/emoji-regex": { "node_modules/emoji-regex": {
"version": "8.0.0", "version": "8.0.0",
@ -13755,9 +13755,9 @@
"integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==" "integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g=="
}, },
"@types/react": { "@types/react": {
"version": "18.2.32", "version": "18.2.33",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.32.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.33.tgz",
"integrity": "sha512-F0FVIZQ1x5Gxy/VYJb7XcWvCcHR28Sjwt1dXLspdIatfPq1MVACfnBDwKe6ANLxQ64riIJooXClpUR6oxTiepg==", "integrity": "sha512-v+I7S+hu3PIBoVkKGpSYYpiBT1ijqEzWpzQD62/jm4K74hPpSP7FF9BnKG6+fg2+62weJYkkBWDJlZt5JO/9hg==",
"requires": { "requires": {
"@types/prop-types": "*", "@types/prop-types": "*",
"@types/scheduler": "*", "@types/scheduler": "*",
@ -14768,9 +14768,9 @@
} }
}, },
"electron-to-chromium": { "electron-to-chromium": {
"version": "1.4.566", "version": "1.4.567",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.566.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.567.tgz",
"integrity": "sha512-mv+fAy27uOmTVlUULy15U3DVJ+jg+8iyKH1bpwboCRhtDC69GKf1PPTZvEIhCyDr81RFqfxZJYrbgp933a1vtg==" "integrity": "sha512-8KR114CAYQ4/r5EIEsOmOMqQ9j0MRbJZR3aXD/KFA8RuKzyoUB4XrUCg+l8RUGqTVQgKNIgTpjaG8YHRPAbX2w=="
}, },
"emoji-regex": { "emoji-regex": {
"version": "8.0.0", "version": "8.0.0",

View file

@ -6,7 +6,7 @@ import { alertContext } from "../../../../contexts/alertContext";
import { AuthContext } from "../../../../contexts/authContext"; import { AuthContext } from "../../../../contexts/authContext";
import { TabsContext } from "../../../../contexts/tabsContext"; import { TabsContext } from "../../../../contexts/tabsContext";
import { getStoreComponents } from "../../../../controllers/API"; import { getStoreComponents } from "../../../../controllers/API";
import { FlowComponent } from "../../../../types/store"; import { storeComponent } from "../../../../types/store";
import { MarketCardComponent } from "../../../StorePage/components/market-card"; import { MarketCardComponent } from "../../../StorePage/components/market-card";
export default function SavedComponents(): JSX.Element { export default function SavedComponents(): JSX.Element {
@ -18,7 +18,7 @@ export default function SavedComponents(): JSX.Element {
useEffect(() => { useEffect(() => {
setTabId(""); setTabId("");
}, []); }, []);
const [data, setData] = useState<FlowComponent[]>([]); const [data, setData] = useState<storeComponent[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [filteredCategories, setFilteredCategories] = useState(new Set()); const [filteredCategories, setFilteredCategories] = useState(new Set());
const { setErrorData } = useContext(alertContext); const { setErrorData } = useContext(alertContext);

View file

@ -1,5 +1,5 @@
import { Link, ToyBrick } from "lucide-react";
import { useContext, useEffect, useRef, useState } from "react"; import { useContext, useEffect, useRef, useState } from "react";
import ShadTooltip from "../../../components/ShadTooltipComponent";
import IconComponent from "../../../components/genericIconComponent"; import IconComponent from "../../../components/genericIconComponent";
import { Badge } from "../../../components/ui/badge"; import { Badge } from "../../../components/ui/badge";
import { Button } from "../../../components/ui/button"; import { Button } from "../../../components/ui/button";
@ -15,15 +15,15 @@ import { StoreContext } from "../../../contexts/storeContext";
import { TabsContext } from "../../../contexts/tabsContext"; import { TabsContext } from "../../../contexts/tabsContext";
import { getComponent, saveFlowStore } from "../../../controllers/API"; import { getComponent, saveFlowStore } from "../../../controllers/API";
import { FlowType } from "../../../types/flow"; import { FlowType } from "../../../types/flow";
import { FlowComponent } from "../../../types/store"; import { storeComponent } from "../../../types/store";
import cloneFLowWithParent from "../../../utils/storeUtils"; import cloneFLowWithParent from "../../../utils/storeUtils";
export const MarketCardComponent = ({ data }: { data: FlowComponent }) => { export const MarketCardComponent = ({ data }: { data: storeComponent }) => {
const { savedFlows } = useContext(StoreContext); const { savedFlows } = useContext(StoreContext);
const [added, setAdded] = useState(savedFlows.has(data.id) ? true : false); const [added, setAdded] = useState(savedFlows.has(data.id) ? true : false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { addFlow } = useContext(TabsContext); const { addFlow } = useContext(TabsContext);
const { setSuccessData } = useContext(alertContext); const { setSuccessData, setErrorData } = useContext(alertContext);
const flowData = useRef<FlowType>(); const flowData = useRef<FlowType>();
useEffect(() => { useEffect(() => {
@ -42,9 +42,14 @@ export const MarketCardComponent = ({ data }: { data: FlowComponent }) => {
.then(() => { .then(() => {
setAdded(true); setAdded(true);
setLoading(false); setLoading(false);
setSuccessData({ title: "Component Added to account" });
}) })
.catch((error) => { .catch((error) => {
console.error(error); console.error(error);
setErrorData({
title: "Error on adding Component",
list: [error["response"]["data"]["detail"]],
});
}); });
}, },
(error) => { (error) => {
@ -69,28 +74,6 @@ export const MarketCardComponent = ({ data }: { data: FlowComponent }) => {
} }
} }
function handleFork(flowId: string, is_component: boolean) {
getComponent(flowId).then(
(res) => {
console.log(res);
const newFLow = cloneFLowWithParent(res.data, res.id, is_component);
console.log(newFLow);
saveFlowStore(newFLow).then(
(res) => {
console.log(JSON.parse(JSON.stringify(res)));
addFlow(true, newFLow);
},
(error) => {
console.error(error);
}
);
},
(error) => {
console.log(error);
}
);
}
return ( return (
<Card className="group relative flex cursor-pointer flex-col justify-between overflow-hidden transition-all hover:shadow-md"> <Card className="group relative flex cursor-pointer flex-col justify-between overflow-hidden transition-all hover:shadow-md">
<div> <div>
@ -143,12 +126,29 @@ export const MarketCardComponent = ({ data }: { data: FlowComponent }) => {
<div className=" flex items-center gap-3"> <div className=" flex items-center gap-3">
<Badge size="md" variant="outline"> <Badge size="md" variant="outline">
chain chain
<Link className="ml-1.5 w-3 text-green-700" /> <IconComponent
name="Link"
className="ml-1.5 w-3 text-green-700"
/>
</Badge> </Badge>
<span className="flex items-center gap-1.5 text-xs text-foreground"> <ShadTooltip content="Components">
<ToyBrick className="h-4 w-4" /> <span className="flex items-center gap-1.5 text-xs text-foreground">
123 <IconComponent name="ToyBrick" className="h-4 w-4" />
</span> 123
</span>
</ShadTooltip>
<ShadTooltip content="Favorites">
<span className="flex items-center gap-1.5 text-xs text-foreground">
<IconComponent name="Heart" className="h-4 w-4" />
{data.liked_by_count ?? 0}
</span>
</ShadTooltip>
<ShadTooltip content="Downloads">
<span className="flex items-center gap-1.5 text-xs text-foreground">
<IconComponent name="DownloadCloud" className="h-4 w-4" />
{data.downloads_count}
</span>
</ShadTooltip>
</div> </div>
{/* {data.isChat ? ( {/* {data.isChat ? (
<Button size="sm" variant="outline"> <Button size="sm" variant="outline">

View file

@ -26,7 +26,7 @@ import {
searchComponent, searchComponent,
} from "../../controllers/API"; } from "../../controllers/API";
import StoreApiKeyModal from "../../modals/StoreApiKeyModal"; import StoreApiKeyModal from "../../modals/StoreApiKeyModal";
import { FlowComponent } from "../../types/store"; import { storeComponent } from "../../types/store";
import { cn } from "../../utils/utils"; import { cn } from "../../utils/utils";
import { MarketCardComponent } from "./components/market-card"; import { MarketCardComponent } from "./components/market-card";
export default function StorePage(): JSX.Element { export default function StorePage(): JSX.Element {
@ -35,7 +35,7 @@ export default function StorePage(): JSX.Element {
useEffect(() => { useEffect(() => {
setTabId(""); setTabId("");
}, []); }, []);
const [data, setData] = useState<FlowComponent[]>([]); const [data, setData] = useState<storeComponent[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [search, setSearch] = useState(false); const [search, setSearch] = useState(false);
const [filteredCategories, setFilteredCategories] = useState(new Set()); const [filteredCategories, setFilteredCategories] = useState(new Set());
@ -45,6 +45,7 @@ export default function StorePage(): JSX.Element {
const [totalRowsCount, setTotalRowsCount] = useState(0); const [totalRowsCount, setTotalRowsCount] = useState(0);
const [size, setPageSize] = useState(10); const [size, setPageSize] = useState(10);
const [index, setPageIndex] = useState(1); const [index, setPageIndex] = useState(1);
const [errorApiKey, setErrorApiKey] = useState(false);
const { setSavedFlows } = useContext(StoreContext); const { setSavedFlows } = useContext(StoreContext);
async function getSavedComponents() { async function getSavedComponents() {
@ -58,10 +59,15 @@ export default function StorePage(): JSX.Element {
} }
useEffect(() => { useEffect(() => {
getSavedComponents().then((_) => handleGetComponents());
getNumberOfComponents().then((res) => { getNumberOfComponents().then((res) => {
setTotalRowsCount(Number(res["count"])); setTotalRowsCount(Number(res["count"]));
}); });
getSavedComponents()
.finally(() => handleGetComponents())
.catch((err) => {
setErrorApiKey(true);
console.error(err);
});
}, []); }, []);
const handleGetComponents = () => { const handleGetComponents = () => {
@ -138,7 +144,10 @@ export default function StorePage(): JSX.Element {
handleGetComponents(); handleGetComponents();
}} }}
> >
<Button variant="primary"> <Button
className={`${errorApiKey ? "animate-pulse border-error" : ""}`}
variant="primary"
>
<IconComponent name="Key" className="main-page-nav-button" /> <IconComponent name="Key" className="main-page-nav-button" />
API Key API Key
</Button> </Button>

View file

@ -1,13 +1,10 @@
export type FlowComponent = { export type storeComponent = {
id: string; id: string;
status: string;
sort: null | any;
user_created: string;
date_created: string;
user_updated: string;
date_updated: string;
is_component: boolean; is_component: boolean;
tags: string[];
metadata?: {};
downloads_count: number;
name: string; name: string;
description: string; description: string;
data: Object; liked_by_count: number;
}; };

View file

@ -38,6 +38,7 @@ import {
GithubIcon, GithubIcon,
Group, Group,
Hammer, Hammer,
Heart,
HelpCircle, HelpCircle,
Home, Home,
Info, Info,
@ -75,6 +76,7 @@ import {
Store, Store,
SunIcon, SunIcon,
TerminalSquare, TerminalSquare,
ToyBrick,
Trash2, Trash2,
Undo, Undo,
Ungroup, Ungroup,
@ -334,4 +336,7 @@ export const nodeIconsLucide: iconsType = {
GitBranchPlus, GitBranchPlus,
Loader2, Loader2,
BookmarkPlus, BookmarkPlus,
Heart,
Link,
ToyBrick,
}; };