diff --git a/src/backend/langflow/api/v1/store.py b/src/backend/langflow/api/v1/store.py index 2a2c1118f..e63b20aab 100644 --- a/src/backend/langflow/api/v1/store.py +++ b/src/backend/langflow/api/v1/store.py @@ -22,49 +22,53 @@ from langflow.services.store.service import StoreService router = APIRouter(prefix="/store", tags=["Components Store"]) -def get_user_store_api_key(user: User = Depends(auth_utils.get_current_active_user)): +def get_user_store_api_key( + user: User = Depends(auth_utils.get_current_active_user), + settings_service=Depends(get_settings_service), +): if not user.store_api_key: raise HTTPException( status_code=400, detail="You must have a store API key set." ) - return user.store_api_key + decrypted = auth_utils.decrypt_api_key(user.store_api_key, settings_service) + return decrypted def get_optional_user_store_api_key( user: User = Depends(auth_utils.get_current_active_user), + settings_service=Depends(get_settings_service), ): - return user.store_api_key + if not user.store_api_key: + return None + decrypted = auth_utils.decrypt_api_key(user.store_api_key, settings_service) + return decrypted @router.post("/components/", response_model=ComponentResponse, status_code=201) def create_component( component: StoreComponentCreate, store_service: StoreService = Depends(get_store_service), - settings_service=Depends(get_settings_service), store_api_Key: str = Depends(get_user_store_api_key), ): try: - decrypted = auth_utils.decrypt_api_key(store_api_Key, settings_service) - return store_service.upload(decrypted, component) + return store_service.upload(store_api_Key, component) except Exception as exc: raise HTTPException(status_code=400, detail=str(exc)) @router.get("/components/", response_model=List[ListComponentResponse]) def list_components( + filter_by_user: bool = Query(False), page: int = 1, limit: int = 10, store_service: StoreService = Depends(get_store_service), store_api_Key: str = Depends(get_optional_user_store_api_key), - settings_service=Depends(get_settings_service), ): try: fields = ["id", "name", "description", "user_created.name", "is_component"] - if store_api_Key: - decrypted = auth_utils.decrypt_api_key(store_api_Key, settings_service) - else: - decrypted = None - result = store_service.list_components(decrypted, page, limit, fields=fields) + result = store_service.list_components( + store_api_Key, page, limit, fields=fields, filter_by_user=filter_by_user + ) return result except Exception as exc: raise HTTPException(status_code=400, detail=str(exc)) @@ -73,16 +77,13 @@ def list_components( @router.get("/components/{component_id}", response_model=DownloadComponentResponse) def read_component( component_id: UUID, - filter_by_user: bool = Query(False), store_service: StoreService = Depends(get_store_service), store_api_Key: str = Depends(get_user_store_api_key), - settings_service=Depends(get_settings_service), ): # If the component is from the store, we need to get it from the store try: - decrypted = auth_utils.decrypt_api_key(store_api_Key, settings_service) - component = store_service.download(decrypted, component_id, filter_by_user) + component = store_service.download(store_api_Key, component_id) except Exception as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc @@ -102,6 +103,7 @@ async def search_endpoint( date_from: Optional[datetime] = Query(None), date_to: Optional[datetime] = Query(None), sort: Optional[List[str]] = Query(None), + filter_by_user: bool = Query(False), fields: Optional[List[str]] = Query(None), store_service: "StoreService" = Depends(get_store_service), store_api_Key: str = Depends(get_optional_user_store_api_key), @@ -118,6 +120,7 @@ async def search_endpoint( date_to=date_to, sort=sort, fields=fields, + filter_by_user=filter_by_user, ) except Exception as exc: raise HTTPException(status_code=500, detail=str(exc)) diff --git a/src/backend/langflow/services/store/service.py b/src/backend/langflow/services/store/service.py index e827ad632..39d31996e 100644 --- a/src/backend/langflow/services/store/service.py +++ b/src/backend/langflow/services/store/service.py @@ -72,6 +72,7 @@ class StoreService(Service): date_to: Optional[datetime] = None, sort: Optional[List[str]] = ["-likes"], fields: Optional[List[str]] = None, + filter_by_user: bool = False, ) -> List[ComponentResponse]: # ?sort=sort,-date_created,author.name @@ -104,34 +105,6 @@ class StoreService(Service): if fields: params["fields"] = ",".join(fields) - results = self._get(self.components_url, api_key, params) - return [ComponentResponse(**component) for component in results] - - def list_components( - self, - api_key: str, - page: int = 1, - limit: int = 10, - fields: Optional[List[str]] = None, - ) -> List[ListComponentResponse]: - params = {"page": page, "limit": limit} - # ?aggregate[count]=likes - params["fields"] = ( - ",".join(fields) - if fields - else ",".join(["id", "name", "description", "count(likes)", "is_component"]) - ) - - results = self._get(self.components_url, api_key, params) - return [ListComponentResponse(**component) for component in results] - - def download( - self, api_key: str, component_id: str, filter_by_user: bool - ) -> DownloadComponentResponse: - url = f"{self.components_url}/{component_id}" - params = { - "fields": ",".join(["id", "name", "description", "data", "is_component"]) - } if filter_by_user: params["deep"] = json.dumps( { @@ -140,6 +113,52 @@ class StoreService(Service): } } ) + else: + params["filter"] = json.dumps({"status": {"_eq": "public"}}) + + results = self._get(self.components_url, api_key, params) + return [ComponentResponse(**component) for component in results] + + def list_components( + self, + api_key: str, + page: int = 1, + limit: int = 15, + fields: Optional[List[str]] = None, + filter_by_user: bool = False, + ) -> List[ListComponentResponse]: + params = {"page": page, "limit": limit} + # ?aggregate[count]=likes + params["fields"] = ( + ",".join(fields) + if fields + else ",".join(["id", "name", "description", "count(likes)", "is_component"]) + ) + # Only public components or the ones created by the user + # check for "public" or "Public" + + if filter_by_user: + params["deep"] = json.dumps( + { + "components": { + "_filter": {"user_created": {"token": {"_eq": api_key}}} + } + } + ) + else: + params["filter"] = params["filter"] = json.dumps( + {"status": {"_in": ["public", "Public"]}} + ) + + results = self._get(self.components_url, api_key, params) + return [ListComponentResponse(**component) for component in results] + + def download(self, api_key: str, component_id: str) -> DownloadComponentResponse: + url = f"{self.components_url}/{component_id}" + params = { + "fields": ",".join(["id", "name", "description", "data", "is_component"]) + } + component = self._get(url, api_key, params) self.call_webhook(api_key, self.webhook_url, component_id) diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index 496d6c4f3..0bf66bdca 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -3796,9 +3796,9 @@ "integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==" }, "node_modules/@types/react": { - "version": "18.2.31", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.31.tgz", - "integrity": "sha512-c2UnPv548q+5DFh03y8lEDeMfDwBn9G3dRwfkrxQMo/dOtRHUUO57k6pHvBIfH/VF4Nh+98mZ5aaSe+2echD5g==", + "version": "18.2.32", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.32.tgz", + "integrity": "sha512-F0FVIZQ1x5Gxy/VYJb7XcWvCcHR28Sjwt1dXLspdIatfPq1MVACfnBDwKe6ANLxQ64riIJooXClpUR6oxTiepg==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -4517,9 +4517,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001553", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001553.tgz", - "integrity": "sha512-N0ttd6TrFfuqKNi+pMgWJTb9qrdJu4JSpgPFLe/lrD19ugC6fZgF0pUewRowDwzdDnb9V41mFcdlYgl/PyKf4A==", + "version": "1.0.30001554", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001554.tgz", + "integrity": "sha512-A2E3U//MBwbJVzebddm1YfNp7Nud5Ip+IPn4BozBmn4KqVX7AvluoIDFWjsv5OkGnKUXQVmMSoMKLa3ScCblcQ==", "funding": [ { "type": "opencollective", @@ -10220,9 +10220,9 @@ } }, "node_modules/tailwindcss": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.4.tgz", - "integrity": "sha512-JXZNOkggUAc9T5E7nCrimoXHcSf9h3NWFe5sh36CGD/3M5TRLuQeFnQoDsit2uVTqgoOZHLx5rTykLUu16vsMQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz", + "integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -13755,9 +13755,9 @@ "integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==" }, "@types/react": { - "version": "18.2.31", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.31.tgz", - "integrity": "sha512-c2UnPv548q+5DFh03y8lEDeMfDwBn9G3dRwfkrxQMo/dOtRHUUO57k6pHvBIfH/VF4Nh+98mZ5aaSe+2echD5g==", + "version": "18.2.32", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.32.tgz", + "integrity": "sha512-F0FVIZQ1x5Gxy/VYJb7XcWvCcHR28Sjwt1dXLspdIatfPq1MVACfnBDwKe6ANLxQ64riIJooXClpUR6oxTiepg==", "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -14248,9 +14248,9 @@ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==" }, "caniuse-lite": { - "version": "1.0.30001553", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001553.tgz", - "integrity": "sha512-N0ttd6TrFfuqKNi+pMgWJTb9qrdJu4JSpgPFLe/lrD19ugC6fZgF0pUewRowDwzdDnb9V41mFcdlYgl/PyKf4A==" + "version": "1.0.30001554", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001554.tgz", + "integrity": "sha512-A2E3U//MBwbJVzebddm1YfNp7Nud5Ip+IPn4BozBmn4KqVX7AvluoIDFWjsv5OkGnKUXQVmMSoMKLa3ScCblcQ==" }, "ccount": { "version": "2.0.1", @@ -18070,9 +18070,9 @@ "integrity": "sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==" }, "tailwindcss": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.4.tgz", - "integrity": "sha512-JXZNOkggUAc9T5E7nCrimoXHcSf9h3NWFe5sh36CGD/3M5TRLuQeFnQoDsit2uVTqgoOZHLx5rTykLUu16vsMQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz", + "integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==", "requires": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", diff --git a/src/frontend/src/components/PaginatorComponent/index.tsx b/src/frontend/src/components/PaginatorComponent/index.tsx index fa759a8bb..1840c4d0e 100644 --- a/src/frontend/src/components/PaginatorComponent/index.tsx +++ b/src/frontend/src/components/PaginatorComponent/index.tsx @@ -16,6 +16,7 @@ export default function PaginatorComponent({ rowsCount = [10, 20, 50, 100], totalRowsCount = 0, paginate, + storeComponent = false, }: PaginatorComponentType) { const [size, setPageSize] = useState(pageSize); const [maxIndex, setMaxPageIndex] = useState( @@ -30,7 +31,13 @@ export default function PaginatorComponent({ <>
-
+

Rows per page

-
- {Array.from(new Set(searchData.map((i) => i.is_component))).map( - (i, idx) => ( - { - 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" - : "" - )} - > - - {i} - - ) +
+ {renderComponents && + Array.from(new Set(searchData.map((i) => i.is_component))).map( + (i, idx) => ( + { + 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" + : "" + )} + > + + {i} + + ) + )} +
+
+ {loadingWithApiKey ? ( + <> + + + + + ) : ( + searchData + .filter( + (f) => + Array.from(filteredCategories).length === 0 || + filteredCategories.has(f.is_component) + ) + .map((item, idx) => ( + + )) )}
- - {renderComponents && ( - <> -
- {searchData - .filter( - (f) => - Array.from(filteredCategories).length === 0 || - filteredCategories.has(f.is_component) - ) - .map((item, idx) => ( - - ))} -
- - )}
- {totalRowsCount > 0 && !loading && ( + + {renderPagination && (
)} - {loadingWithApiKey && ( -
Loading...
- )}
); diff --git a/src/frontend/src/style/applies.css b/src/frontend/src/style/applies.css index f0b98ebd6..aedb23c28 100644 --- a/src/frontend/src/style/applies.css +++ b/src/frontend/src/style/applies.css @@ -123,7 +123,7 @@ @apply flex gap-2; } .primary-input { - @apply form-input block w-full truncate rounded-md border-border bg-background px-3 text-left shadow-sm placeholder:text-muted-foreground focus:border-ring focus:placeholder-transparent focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 sm:text-sm; + @apply form-input block w-full truncate rounded-md border-border bg-background px-3 text-left shadow-sm placeholder:text-muted-foreground focus:border-ring focus:placeholder-transparent focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 text-sm; } .skeleton-card { @@ -237,7 +237,7 @@ } .main-page-flows-display { - @apply grid w-full gap-4 md:grid-cols-2 lg:grid-cols-4; + @apply grid w-full gap-4 md:grid-cols-2 lg:grid-cols-3; } .community-page-arrangement { @@ -495,7 +495,7 @@ @apply flex items-center gap-0.5 rounded-md px-1.5 py-1 text-sm font-medium; } .header-menu-bar-display { - @apply flex max-w-[200px] cursor-pointer items-center gap-2; + @apply flex max-w-[120px] lg:max-w-[200px] cursor-pointer items-center gap-2; } .header-menu-flow-name { @apply flex-1 truncate; @@ -505,13 +505,13 @@ } .header-arrangement { - @apply flex-max-width h-12 items-center justify-between border-border bg-background; + @apply flex-max-width h-12 items-center justify-between border-b border-border bg-muted; } .header-start-display { - @apply flex w-[30%] items-center justify-start gap-2; + @apply flex items-center justify-start gap-2; } .header-end-division { - @apply flex w-[30%] justify-end px-2; + @apply flex justify-end px-2; } .header-end-display { @apply ml-auto mr-2 flex items-center gap-5; @@ -535,7 +535,7 @@ @apply hover:bg-accent hover:text-accent-foreground; } .header-github-display { - @apply -mr-px ml-2 flex h-9 items-center justify-center rounded-md rounded-l-none border bg-background px-2 text-sm; + @apply -mr-px ml-1 flex h-9 items-center justify-center rounded-md rounded-l-none border bg-background px-2 text-sm; } .header-notifications-box { @apply fixed left-0 top-0 h-screen w-screen; diff --git a/src/frontend/src/types/contexts/store.ts b/src/frontend/src/types/contexts/store.ts index 82012c13a..26185748b 100644 --- a/src/frontend/src/types/contexts/store.ts +++ b/src/frontend/src/types/contexts/store.ts @@ -1,8 +1,6 @@ -import { FlowType } from "../flow"; - export type storeContextType = { - savedFlows: { [key: string]: FlowType }; - setSavedFlows: (newState: { [key: string]: FlowType }) => void; + savedFlows: Set; + setSavedFlows: (newState: Set) => void; setHasStore: (store: boolean) => void; hasStore: boolean; }; diff --git a/src/frontend/src/types/tabs/index.ts b/src/frontend/src/types/tabs/index.ts index c3ecc2681..b8b4a24e9 100644 --- a/src/frontend/src/types/tabs/index.ts +++ b/src/frontend/src/types/tabs/index.ts @@ -36,7 +36,7 @@ export type TabsContextType = { setLastCopiedSelection: (selection: { nodes: any; edges: any }) => void; setTweak: (tweak: tweakType) => tweakType | void; getTweak: tweakType; - saveComponent: (component: NodeDataType) => void; + saveComponent: (component: NodeDataType) => Promise; deleteComponent: (id: string, key: string) => void; }; diff --git a/src/frontend/src/utils/reactflowUtils.ts b/src/frontend/src/utils/reactflowUtils.ts index cf62f2694..4f9c7ba0b 100644 --- a/src/frontend/src/utils/reactflowUtils.ts +++ b/src/frontend/src/utils/reactflowUtils.ts @@ -1009,7 +1009,8 @@ export function expandGroupNode( gNodes[nodeIndex].data.node!.template[field].show = show; gNodes[nodeIndex].data.node!.template[field].advanced = advanced; gNodes[nodeIndex].data.node!.template[field].display_name = display_name; - gNodes[nodeIndex].selected = false; + // keep the nodes selected after ungrouping + // gNodes[nodeIndex].selected = false; if (proxy) { gNodes[nodeIndex].data.node!.template[field].proxy = proxy; } else { @@ -1096,3 +1097,8 @@ export function downloadNode(NodeFLow: FlowType) { element.download = `${NodeFLow.name}.json`; element.click(); } + +export function updateComponentNameAndType( + data: any, + component: NodeDataType +) {} diff --git a/src/frontend/src/utils/utils.ts b/src/frontend/src/utils/utils.ts index 567a18247..551dcc8d0 100644 --- a/src/frontend/src/utils/utils.ts +++ b/src/frontend/src/utils/utils.ts @@ -571,7 +571,7 @@ export function IncrementObjectKey( export function removeCountFromString(input: string): string { // Define a regex pattern to match the count in parentheses - const pattern = /\s*\(\d+\)\s*$/; + const pattern = /\s*\(\w+\)\s*$/; // Use the `replace` method to remove the matched pattern const result = input.replace(pattern, ""); @@ -579,9 +579,13 @@ export function removeCountFromString(input: string): string { return result.trim(); // Trim any leading/trailing spaces } +export function createRandomKey(key: string, uid: string): string { + return removeCountFromString(key) + ` (${uid})`; +} + export function sensitiveSort(a: string, b: string): number { // Extract the name and number from each string using regular expressions - const regex = /(.+) \((\d+)\)/; + const regex = /(.+) \((\w+)\)/; const matchA = a.match(regex); const matchB = b.match(regex);