Merge branch 'login' into authentication
This commit is contained in:
commit
16695c8241
97 changed files with 3022 additions and 1077 deletions
|
|
@ -106,14 +106,9 @@ async def stream_build(flow_id: str):
|
|||
return
|
||||
|
||||
logger.debug("Building langchain object")
|
||||
try:
|
||||
# Some error could happen when building the graph
|
||||
graph = Graph.from_payload(graph_data)
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
error_message = str(exc)
|
||||
yield str(StreamData(event="error", data={"error": error_message}))
|
||||
return
|
||||
|
||||
# Some error could happen when building the graph
|
||||
graph = Graph.from_payload(graph_data)
|
||||
|
||||
number_of_nodes = len(graph.nodes)
|
||||
flow_data_store[flow_id]["status"] = BuildStatus.IN_PROGRESS
|
||||
|
|
@ -128,7 +123,9 @@ async def stream_build(flow_id: str):
|
|||
params = vertex._built_object_repr()
|
||||
valid = True
|
||||
logger.debug(f"Building node {str(vertex.vertex_type)}")
|
||||
logger.debug(f"Output: {params}")
|
||||
logger.debug(
|
||||
f"Output: {params[:100]}{'...' if len(params) > 100 else ''}"
|
||||
)
|
||||
if vertex.artifacts:
|
||||
# The artifacts will be prompt variables
|
||||
# passed to build_input_keys_response
|
||||
|
|
|
|||
|
|
@ -65,7 +65,6 @@ def get_all():
|
|||
logger.info(
|
||||
f"Loading {len(custom_component_dict[category])} component(s) from category {category}"
|
||||
)
|
||||
logger.debug(custom_component_dict)
|
||||
custom_components_from_file = merge_nested_dicts_with_renaming(
|
||||
custom_components_from_file, custom_component_dict
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
from typing import List
|
||||
from uuid import UUID
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
|
||||
from langflow.api.utils import remove_api_keys
|
||||
from langflow.api.v1.schemas import FlowListCreate, FlowListRead
|
||||
from langflow.services.database.models.flow import (
|
||||
|
|
@ -10,12 +12,11 @@ from langflow.services.database.models.flow import (
|
|||
)
|
||||
from langflow.services.utils import get_session
|
||||
from langflow.services.utils import get_settings_manager
|
||||
import orjson
|
||||
from sqlmodel import Session, select
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
|
||||
from fastapi import File, UploadFile
|
||||
import json
|
||||
|
||||
# build router
|
||||
router = APIRouter(prefix="/flows", tags=["Flows"])
|
||||
|
|
@ -105,7 +106,7 @@ async def upload_file(
|
|||
):
|
||||
"""Upload flows from a file."""
|
||||
contents = await file.read()
|
||||
data = json.loads(contents)
|
||||
data = orjson.loads(contents)
|
||||
if "flows" in data:
|
||||
flow_list = FlowListCreate(**data)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@ from uuid import UUID
|
|||
from langflow.services.database.models.api_key.api_key import ApiKeyRead
|
||||
from langflow.services.database.models.flow import FlowCreate, FlowRead
|
||||
from langflow.services.database.models.user import UserRead
|
||||
from langflow.services.database.models.base import orjson_dumps
|
||||
|
||||
from pydantic import BaseModel, Field, validator
|
||||
import json
|
||||
|
||||
|
||||
class BuildStatus(Enum):
|
||||
|
|
@ -119,7 +120,9 @@ class StreamData(BaseModel):
|
|||
data: dict
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"event: {self.event}\ndata: {json.dumps(self.data)}\n\n"
|
||||
return (
|
||||
f"event: {self.event}\ndata: {orjson_dumps(self.data, indent_2=False)}\n\n"
|
||||
)
|
||||
|
||||
|
||||
class CustomComponentCode(BaseModel):
|
||||
|
|
|
|||
76
src/backend/langflow/components/utilities/GetRequest.py
Normal file
76
src/backend/langflow/components/utilities/GetRequest.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
from langflow import CustomComponent
|
||||
from langchain.schema import Document
|
||||
from langflow.services.database.models.base import orjson_dumps
|
||||
import requests
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class GetRequest(CustomComponent):
|
||||
display_name: str = "GET Request"
|
||||
description: str = "Make a GET request to the given URL."
|
||||
output_types: list[str] = ["Document"]
|
||||
documentation: str = "https://docs.langflow.org/components/utilities#get-request"
|
||||
beta = True
|
||||
field_config = {
|
||||
"url": {
|
||||
"display_name": "URL",
|
||||
"info": "The URL to make the request to",
|
||||
"is_list": True,
|
||||
},
|
||||
"headers": {
|
||||
"display_name": "Headers",
|
||||
"field_type": "code",
|
||||
"info": "The headers to send with the request.",
|
||||
},
|
||||
"code": {"show": False},
|
||||
"timeout": {
|
||||
"display_name": "Timeout",
|
||||
"field_type": "int",
|
||||
"info": "The timeout to use for the request.",
|
||||
"value": 5,
|
||||
},
|
||||
}
|
||||
|
||||
def get_document(
|
||||
self, session: requests.Session, url: str, headers: Optional[dict], timeout: int
|
||||
) -> Document:
|
||||
try:
|
||||
response = session.get(url, headers=headers, timeout=int(timeout))
|
||||
try:
|
||||
response_json = response.json()
|
||||
result = orjson_dumps(response_json, indent_2=False)
|
||||
except Exception:
|
||||
result = response.text
|
||||
self.repr_value = result
|
||||
return Document(
|
||||
page_content=result,
|
||||
metadata={
|
||||
"source": url,
|
||||
"headers": headers,
|
||||
"status_code": response.status_code,
|
||||
},
|
||||
)
|
||||
except requests.Timeout:
|
||||
return Document(
|
||||
page_content="Request Timed Out",
|
||||
metadata={"source": url, "headers": headers, "status_code": 408},
|
||||
)
|
||||
except Exception as exc:
|
||||
return Document(
|
||||
page_content=str(exc),
|
||||
metadata={"source": url, "headers": headers, "status_code": 500},
|
||||
)
|
||||
|
||||
def build(
|
||||
self,
|
||||
url: str,
|
||||
headers: Optional[dict] = None,
|
||||
timeout: int = 5,
|
||||
) -> list[Document]:
|
||||
if headers is None:
|
||||
headers = {}
|
||||
urls = url if isinstance(url, list) else [url]
|
||||
with requests.Session() as session:
|
||||
documents = [self.get_document(session, u, headers, timeout) for u in urls]
|
||||
self.repr_value = documents
|
||||
return documents
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
### JSON Document Builder
|
||||
|
||||
# Build a Document containing a JSON object using a key and another Document page content.
|
||||
|
||||
# **Params**
|
||||
|
||||
# - **Key:** The key to use for the JSON object.
|
||||
# - **Document:** The Document page to use for the JSON object.
|
||||
|
||||
# **Output**
|
||||
|
||||
# - **Document:** The Document containing the JSON object.
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langchain.schema import Document
|
||||
from langflow.services.database.models.base import orjson_dumps
|
||||
|
||||
|
||||
class JSONDocumentBuilder(CustomComponent):
|
||||
display_name: str = "JSON Document Builder"
|
||||
description: str = "Build a Document containing a JSON object using a key and another Document page content."
|
||||
output_types: list[str] = ["Document"]
|
||||
beta = True
|
||||
documentation: str = (
|
||||
"https://docs.langflow.org/components/utilities#json-document-builder"
|
||||
)
|
||||
|
||||
field_config = {
|
||||
"key": {"display_name": "Key"},
|
||||
"document": {"display_name": "Document"},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
key: str,
|
||||
document: Document,
|
||||
) -> Document:
|
||||
documents = None
|
||||
if isinstance(document, list):
|
||||
documents = [
|
||||
Document(
|
||||
page_content=orjson_dumps({key: doc.page_content}, indent_2=False)
|
||||
)
|
||||
for doc in document
|
||||
]
|
||||
elif isinstance(document, Document):
|
||||
documents = Document(
|
||||
page_content=orjson_dumps({key: document.page_content}, indent_2=False)
|
||||
)
|
||||
else:
|
||||
raise TypeError(
|
||||
f"Expected Document or list of Documents, got {type(document)}"
|
||||
)
|
||||
self.repr_value = documents
|
||||
return documents
|
||||
81
src/backend/langflow/components/utilities/PostRequest.py
Normal file
81
src/backend/langflow/components/utilities/PostRequest.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
from langflow import CustomComponent
|
||||
from langchain.schema import Document
|
||||
from langflow.services.database.models.base import orjson_dumps
|
||||
import requests
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class PostRequest(CustomComponent):
|
||||
display_name: str = "POST Request"
|
||||
description: str = "Make a POST request to the given URL."
|
||||
output_types: list[str] = ["Document"]
|
||||
documentation: str = "https://docs.langflow.org/components/utilities#post-request"
|
||||
beta = True
|
||||
field_config = {
|
||||
"url": {"display_name": "URL", "info": "The URL to make the request to."},
|
||||
"headers": {
|
||||
"display_name": "Headers",
|
||||
"field_type": "code",
|
||||
"info": "The headers to send with the request.",
|
||||
},
|
||||
"code": {"show": False},
|
||||
"document": {"display_name": "Document"},
|
||||
}
|
||||
|
||||
def post_document(
|
||||
self,
|
||||
session: requests.Session,
|
||||
document: Document,
|
||||
url: str,
|
||||
headers: Optional[dict] = None,
|
||||
) -> Document:
|
||||
try:
|
||||
response = session.post(url, headers=headers, data=document.page_content)
|
||||
try:
|
||||
response_json = response.json()
|
||||
result = orjson_dumps(response_json, indent_2=False)
|
||||
except Exception:
|
||||
result = response.text
|
||||
self.repr_value = result
|
||||
return Document(
|
||||
page_content=result,
|
||||
metadata={
|
||||
"source": url,
|
||||
"headers": headers,
|
||||
"status_code": response,
|
||||
},
|
||||
)
|
||||
except Exception as exc:
|
||||
return Document(
|
||||
page_content=str(exc),
|
||||
metadata={
|
||||
"source": url,
|
||||
"headers": headers,
|
||||
"status_code": 500,
|
||||
},
|
||||
)
|
||||
|
||||
def build(
|
||||
self,
|
||||
document: Document,
|
||||
url: str,
|
||||
headers: Optional[dict] = None,
|
||||
) -> list[Document]:
|
||||
if headers is None:
|
||||
headers = {}
|
||||
|
||||
if not isinstance(document, list) and isinstance(document, Document):
|
||||
documents: list[Document] = [document]
|
||||
elif isinstance(document, list) and all(
|
||||
isinstance(doc, Document) for doc in document
|
||||
):
|
||||
documents = document
|
||||
else:
|
||||
raise ValueError("document must be a Document or a list of Documents")
|
||||
|
||||
with requests.Session() as session:
|
||||
documents = [
|
||||
self.post_document(session, doc, url, headers) for doc in documents
|
||||
]
|
||||
self.repr_value = documents
|
||||
return documents
|
||||
94
src/backend/langflow/components/utilities/UpdateRequest.py
Normal file
94
src/backend/langflow/components/utilities/UpdateRequest.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
from typing import List, Optional
|
||||
import requests
|
||||
from langflow import CustomComponent
|
||||
from langchain.schema import Document
|
||||
from langflow.services.database.models.base import orjson_dumps
|
||||
|
||||
|
||||
class UpdateRequest(CustomComponent):
|
||||
display_name: str = "Update Request"
|
||||
description: str = "Make a PATCH request to the given URL."
|
||||
output_types: list[str] = ["Document"]
|
||||
documentation: str = "https://docs.langflow.org/components/utilities#update-request"
|
||||
beta = True
|
||||
field_config = {
|
||||
"url": {"display_name": "URL", "info": "The URL to make the request to."},
|
||||
"headers": {
|
||||
"display_name": "Headers",
|
||||
"field_type": "code",
|
||||
"info": "The headers to send with the request.",
|
||||
},
|
||||
"code": {"show": False},
|
||||
"document": {"display_name": "Document"},
|
||||
"method": {
|
||||
"display_name": "Method",
|
||||
"field_type": "str",
|
||||
"info": "The HTTP method to use.",
|
||||
"options": ["PATCH", "PUT"],
|
||||
"value": "PATCH",
|
||||
},
|
||||
}
|
||||
|
||||
def update_document(
|
||||
self,
|
||||
session: requests.Session,
|
||||
document: Document,
|
||||
url: str,
|
||||
headers: Optional[dict] = None,
|
||||
method: str = "PATCH",
|
||||
) -> Document:
|
||||
try:
|
||||
if method == "PATCH":
|
||||
response = session.patch(
|
||||
url, headers=headers, data=document.page_content
|
||||
)
|
||||
elif method == "PUT":
|
||||
response = session.put(url, headers=headers, data=document.page_content)
|
||||
else:
|
||||
raise ValueError(f"Unsupported method: {method}")
|
||||
try:
|
||||
response_json = response.json()
|
||||
result = orjson_dumps(response_json, indent_2=False)
|
||||
except Exception:
|
||||
result = response.text
|
||||
self.repr_value = result
|
||||
return Document(
|
||||
page_content=result,
|
||||
metadata={
|
||||
"source": url,
|
||||
"headers": headers,
|
||||
"status_code": response.status_code,
|
||||
},
|
||||
)
|
||||
except Exception as exc:
|
||||
return Document(
|
||||
page_content=str(exc),
|
||||
metadata={"source": url, "headers": headers, "status_code": 500},
|
||||
)
|
||||
|
||||
def build(
|
||||
self,
|
||||
method: str,
|
||||
document: Document,
|
||||
url: str,
|
||||
headers: Optional[dict] = None,
|
||||
) -> List[Document]:
|
||||
if headers is None:
|
||||
headers = {}
|
||||
|
||||
if not isinstance(document, list) and isinstance(document, Document):
|
||||
documents: list[Document] = [document]
|
||||
elif isinstance(document, list) and all(
|
||||
isinstance(doc, Document) for doc in document
|
||||
):
|
||||
documents = document
|
||||
else:
|
||||
raise ValueError("document must be a Document or a list of Documents")
|
||||
|
||||
with requests.Session() as session:
|
||||
documents = [
|
||||
self.update_document(session, doc, url, headers, method)
|
||||
for doc in documents
|
||||
]
|
||||
self.repr_value = documents
|
||||
return documents
|
||||
109
src/backend/langflow/components/vectorstores/Chroma.py
Normal file
109
src/backend/langflow/components/vectorstores/Chroma.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
from typing import Optional, Union
|
||||
from langflow import CustomComponent
|
||||
|
||||
from langchain.vectorstores import Chroma
|
||||
from langchain.schema import Document
|
||||
from langchain.vectorstores.base import VectorStore
|
||||
from langchain.schema import BaseRetriever
|
||||
from langchain.embeddings.base import Embeddings
|
||||
import chromadb
|
||||
|
||||
|
||||
class ChromaComponent(CustomComponent):
|
||||
"""
|
||||
A custom component for implementing a Vector Store using Chroma.
|
||||
"""
|
||||
|
||||
display_name: str = "Chroma (Custom Component)"
|
||||
description: str = "Implementation of Vector Store using Chroma"
|
||||
documentation = "https://python.langchain.com/docs/integrations/vectorstores/chroma"
|
||||
beta = True
|
||||
|
||||
def build_config(self):
|
||||
"""
|
||||
Builds the configuration for the component.
|
||||
|
||||
Returns:
|
||||
- dict: A dictionary containing the configuration options for the component.
|
||||
"""
|
||||
return {
|
||||
"collection_name": {"display_name": "Collection Name", "value": "langflow"},
|
||||
"persist": {"display_name": "Persist"},
|
||||
"persist_directory": {"display_name": "Persist Directory"},
|
||||
"code": {"show": False, "display_name": "Code"},
|
||||
"documents": {"display_name": "Documents", "is_list": True},
|
||||
"embedding": {"display_name": "Embedding"},
|
||||
"chroma_server_cors_allow_origins": {
|
||||
"display_name": "Server CORS Allow Origins",
|
||||
"advanced": True,
|
||||
},
|
||||
"chroma_server_host": {"display_name": "Server Host", "advanced": True},
|
||||
"chroma_server_port": {"display_name": "Server Port", "advanced": True},
|
||||
"chroma_server_grpc_port": {
|
||||
"display_name": "Server gRPC Port",
|
||||
"advanced": True,
|
||||
},
|
||||
"chroma_server_ssl_enabled": {
|
||||
"display_name": "Server SSL Enabled",
|
||||
"advanced": True,
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
collection_name: str,
|
||||
persist: bool,
|
||||
chroma_server_ssl_enabled: bool,
|
||||
persist_directory: Optional[str] = None,
|
||||
embedding: Optional[Embeddings] = None,
|
||||
documents: Optional[Document] = None,
|
||||
chroma_server_cors_allow_origins: Optional[str] = None,
|
||||
chroma_server_host: Optional[str] = None,
|
||||
chroma_server_port: Optional[int] = None,
|
||||
chroma_server_grpc_port: Optional[int] = None,
|
||||
) -> Union[VectorStore, BaseRetriever]:
|
||||
"""
|
||||
Builds the Vector Store or BaseRetriever object.
|
||||
|
||||
Args:
|
||||
- collection_name (str): The name of the collection.
|
||||
- persist_directory (Optional[str]): The directory to persist the Vector Store to.
|
||||
- chroma_server_ssl_enabled (bool): Whether to enable SSL for the Chroma server.
|
||||
- persist (bool): Whether to persist the Vector Store or not.
|
||||
- embedding (Optional[Embeddings]): The embeddings to use for the Vector Store.
|
||||
- documents (Optional[Document]): The documents to use for the Vector Store.
|
||||
- chroma_server_cors_allow_origins (Optional[str]): The CORS allow origins for the Chroma server.
|
||||
- chroma_server_host (Optional[str]): The host for the Chroma server.
|
||||
- chroma_server_port (Optional[int]): The port for the Chroma server.
|
||||
- chroma_server_grpc_port (Optional[int]): The gRPC port for the Chroma server.
|
||||
|
||||
Returns:
|
||||
- Union[VectorStore, BaseRetriever]: The Vector Store or BaseRetriever object.
|
||||
"""
|
||||
|
||||
# Chroma settings
|
||||
chroma_settings = None
|
||||
|
||||
if chroma_server_host is not None:
|
||||
chroma_settings = chromadb.config.Settings(
|
||||
chroma_server_cors_allow_origins=chroma_server_cors_allow_origins
|
||||
or None,
|
||||
chroma_server_host=chroma_server_host,
|
||||
chroma_server_port=chroma_server_port or None,
|
||||
chroma_server_grpc_port=chroma_server_grpc_port or None,
|
||||
chroma_server_ssl_enabled=chroma_server_ssl_enabled,
|
||||
)
|
||||
|
||||
# If documents, then we need to create a Chroma instance using .from_documents
|
||||
if documents is not None and embedding is not None:
|
||||
return Chroma.from_documents(
|
||||
documents=documents, # type: ignore
|
||||
persist_directory=persist_directory if persist else None,
|
||||
collection_name=collection_name,
|
||||
embedding=embedding,
|
||||
client_settings=chroma_settings,
|
||||
)
|
||||
|
||||
return Chroma(
|
||||
persist_directory=persist_directory, client_settings=chroma_settings
|
||||
)
|
||||
|
|
@ -40,7 +40,6 @@ class Edge:
|
|||
if no_matched_type:
|
||||
logger.debug(self.source_types)
|
||||
logger.debug(self.target_reqs)
|
||||
if no_matched_type:
|
||||
raise ValueError(
|
||||
f"Edge between {self.source.vertex_type} and {self.target.vertex_type} "
|
||||
f"has no matched type"
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from langflow.interface.custom.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES
|
|||
from langflow.interface.custom.component import Component
|
||||
from langflow.interface.custom.directory_reader import DirectoryReader
|
||||
from langflow.services.utils import get_db_manager
|
||||
from langflow.interface.custom.utils import extract_inner_type
|
||||
|
||||
from langflow.utils import validate
|
||||
|
||||
|
|
@ -20,7 +21,7 @@ class CustomComponent(Component, extra=Extra.allow):
|
|||
function_entrypoint_name = "build"
|
||||
function: Optional[Callable] = None
|
||||
return_type_valid_list = list(CUSTOM_COMPONENT_SUPPORTED_TYPES.keys())
|
||||
repr_value: Optional[str] = ""
|
||||
repr_value: Optional[Any] = ""
|
||||
|
||||
def __init__(self, **data):
|
||||
super().__init__(**data)
|
||||
|
|
@ -123,6 +124,10 @@ class CustomComponent(Component, extra=Extra.allow):
|
|||
return_type = build_method["return_type"]
|
||||
if not return_type:
|
||||
return []
|
||||
# If list or List is in the return type, then we remove it and return the inner type
|
||||
if return_type.startswith("list") or return_type.startswith("List"):
|
||||
return_type = extract_inner_type(return_type)
|
||||
|
||||
# If the return type is not a Union, then we just return it as a list
|
||||
if "Union" not in return_type:
|
||||
return [return_type] if return_type in self.return_type_valid_list else []
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ class DirectoryReader:
|
|||
]
|
||||
filtered = [menu for menu in items if menu["components"]]
|
||||
logger.debug(
|
||||
f'Filtered components {"with errors" if with_errors else ""}: {filtered}'
|
||||
f'Filtered components {"with errors" if with_errors else ""}: {len(filtered)}'
|
||||
)
|
||||
return {"menu": filtered}
|
||||
|
||||
|
|
|
|||
10
src/backend/langflow/interface/custom/utils.py
Normal file
10
src/backend/langflow/interface/custom/utils.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import re
|
||||
|
||||
|
||||
def extract_inner_type(return_type: str) -> str:
|
||||
"""
|
||||
Extracts the inner type from a type hint that is a list.
|
||||
"""
|
||||
if match := re.match(r"list\[(.*)\]", return_type, re.IGNORECASE):
|
||||
return match[1]
|
||||
return return_type
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import json
|
||||
import orjson
|
||||
from typing import Any, Callable, Dict, Sequence, Type
|
||||
|
||||
from langchain.agents import agent as agent_module
|
||||
|
|
@ -66,7 +67,7 @@ def convert_kwargs(params):
|
|||
for key in kwargs_keys:
|
||||
if isinstance(params[key], str):
|
||||
try:
|
||||
params[key] = json.loads(params[key])
|
||||
params[key] = orjson.loads(params[key])
|
||||
except json.JSONDecodeError:
|
||||
# if the string is not a valid json string, we will
|
||||
# remove the key from the params
|
||||
|
|
@ -310,7 +311,7 @@ def instantiate_documentloader(class_object: Type[BaseLoader], params: Dict):
|
|||
metadata = params.pop("metadata", None)
|
||||
if metadata and isinstance(metadata, str):
|
||||
try:
|
||||
metadata = json.loads(metadata)
|
||||
metadata = orjson.loads(metadata)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(
|
||||
"The metadata you provided is not a valid JSON string."
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import contextlib
|
||||
import json
|
||||
from langflow.services.database.models.base import orjson_dumps
|
||||
import orjson
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from langchain.agents import ZeroShotAgent
|
||||
|
|
@ -95,9 +97,11 @@ def format_content(variable):
|
|||
|
||||
def try_to_load_json(content):
|
||||
with contextlib.suppress(json.JSONDecodeError):
|
||||
content = json.loads(content)
|
||||
content = orjson.loads(content)
|
||||
if isinstance(content, list):
|
||||
content = ",".join([str(item) for item in content])
|
||||
else:
|
||||
content = orjson_dumps(content)
|
||||
return content
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import json
|
||||
from typing import Any, Callable, Dict, Type
|
||||
from langchain.vectorstores import (
|
||||
Pinecone,
|
||||
|
|
@ -12,6 +11,8 @@ from langchain.vectorstores import (
|
|||
|
||||
import os
|
||||
|
||||
import orjson
|
||||
|
||||
|
||||
def docs_in_params(params: dict) -> bool:
|
||||
"""Check if params has documents OR texts and one of them is not an empty list,
|
||||
|
|
@ -92,7 +93,7 @@ def initialize_weaviate(class_object: Type[Weaviate], params: dict):
|
|||
import weaviate # type: ignore
|
||||
|
||||
client_kwargs_json = params.get("client_kwargs", "{}")
|
||||
client_kwargs = json.loads(client_kwargs_json)
|
||||
client_kwargs = orjson.loads(client_kwargs_json)
|
||||
client_params = {
|
||||
"url": params.get("weaviate_url"),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -190,17 +190,16 @@ def build_frontend_node(custom_component: CustomComponent):
|
|||
|
||||
def update_attributes(frontend_node, template_config):
|
||||
"""Update the display name and description of a frontend node"""
|
||||
if "display_name" in template_config:
|
||||
frontend_node["display_name"] = template_config["display_name"]
|
||||
|
||||
if "description" in template_config:
|
||||
frontend_node["description"] = template_config["description"]
|
||||
|
||||
if "beta" in template_config:
|
||||
frontend_node["beta"] = template_config["beta"]
|
||||
|
||||
if "documentation" in template_config:
|
||||
frontend_node["documentation"] = template_config["documentation"]
|
||||
attributes = [
|
||||
"display_name",
|
||||
"description",
|
||||
"beta",
|
||||
"documentation",
|
||||
"output_types",
|
||||
]
|
||||
for attribute in attributes:
|
||||
if attribute in template_config:
|
||||
frontend_node[attribute] = template_config[attribute]
|
||||
|
||||
|
||||
def build_field_config(custom_component: CustomComponent):
|
||||
|
|
@ -338,7 +337,9 @@ def build_valid_menu(valid_components):
|
|||
valid_menu[menu_name] = {}
|
||||
|
||||
for component in menu_item["components"]:
|
||||
logger.debug(f"Building component: {component}")
|
||||
logger.debug(
|
||||
f"Building component: {component.get('name'), component.get('output_types')}"
|
||||
)
|
||||
try:
|
||||
component_name = component["name"]
|
||||
component_code = component["code"]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import json
|
||||
from pathlib import Path
|
||||
from langchain.schema import AgentAction
|
||||
import json
|
||||
from langflow.interface.run import (
|
||||
build_sorted_vertices_with_caching,
|
||||
get_memory_key,
|
||||
|
|
|
|||
5
src/backend/langflow/services/cache/utils.py
vendored
5
src/backend/langflow/services/cache/utils.py
vendored
|
|
@ -2,13 +2,13 @@ import base64
|
|||
import contextlib
|
||||
import functools
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from appdirs import user_cache_dir
|
||||
from langflow.services.database.models.base import orjson_dumps
|
||||
|
||||
CACHE: Dict[str, Any] = {}
|
||||
|
||||
|
|
@ -90,7 +90,8 @@ def clear_old_cache_files(max_cache_size: int = 3):
|
|||
def compute_dict_hash(graph_data):
|
||||
graph_data = filter_json(graph_data)
|
||||
|
||||
cleaned_graph_json = json.dumps(graph_data, sort_keys=True)
|
||||
cleaned_graph_json = orjson_dumps(graph_data, sort_keys=True)
|
||||
|
||||
return hashlib.sha256(cleaned_graph_json.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ from langflow.utils.logger import logger
|
|||
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from langflow.services.cache.flow import InMemoryCache
|
||||
import orjson
|
||||
|
||||
|
||||
class ChatHistory(Subject):
|
||||
|
|
@ -190,7 +190,7 @@ class ChatManager(Service):
|
|||
while True:
|
||||
json_payload = await websocket.receive_json()
|
||||
try:
|
||||
payload = json.loads(json_payload)
|
||||
payload = orjson.loads(json_payload)
|
||||
except TypeError:
|
||||
payload = json_payload
|
||||
if "clear_history" in payload:
|
||||
|
|
|
|||
|
|
@ -2,9 +2,20 @@ from sqlmodel import SQLModel
|
|||
import orjson
|
||||
|
||||
|
||||
def orjson_dumps(v, *, default):
|
||||
# orjson.dumps returns bytes, to match standard json.dumps we need to decode
|
||||
return orjson.dumps(v, default=default).decode()
|
||||
def orjson_dumps(v, *, default=None, sort_keys=False, indent_2=True):
|
||||
option = orjson.OPT_SORT_KEYS if sort_keys else None
|
||||
if indent_2:
|
||||
# orjson.dumps returns bytes, to match standard json.dumps we need to decode
|
||||
# option
|
||||
# To modify how data is serialized, specify option. Each option is an integer constant in orjson.
|
||||
# To specify multiple options, mask them together, e.g., option=orjson.OPT_STRICT_INTEGER | orjson.OPT_NAIVE_UTC
|
||||
if option is None:
|
||||
option = orjson.OPT_INDENT_2
|
||||
else:
|
||||
option |= orjson.OPT_INDENT_2
|
||||
if default is None:
|
||||
return orjson.dumps(v, option=option).decode()
|
||||
return orjson.dumps(v, default=default, option=option).decode()
|
||||
|
||||
|
||||
class SQLModelSerializable(SQLModel):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import contextlib
|
||||
import json
|
||||
import orjson
|
||||
import os
|
||||
from shutil import copy2
|
||||
from typing import Optional, List
|
||||
|
|
@ -175,7 +176,7 @@ class Settings(BaseSettings):
|
|||
if isinstance(getattr(self, key), list):
|
||||
# value might be a '[something]' string
|
||||
with contextlib.suppress(json.decoder.JSONDecodeError):
|
||||
value = json.loads(str(value))
|
||||
value = orjson.loads(str(value))
|
||||
if isinstance(value, list):
|
||||
for item in value:
|
||||
if isinstance(item, Path):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import json
|
||||
from typing import Optional
|
||||
from langflow.services.database.models.base import orjson_dumps
|
||||
|
||||
from langflow.template.field.base import TemplateField
|
||||
from langflow.template.frontend_node.base import FrontendNode
|
||||
|
|
@ -89,7 +89,7 @@ class LLMFrontendNode(FrontendNode):
|
|||
if field.name == "config":
|
||||
field.show = True
|
||||
field.advanced = True
|
||||
field.value = json.dumps(CTRANSFORMERS_DEFAULT_CONFIG, indent=2)
|
||||
field.value = orjson_dumps(CTRANSFORMERS_DEFAULT_CONFIG, indent_2=True)
|
||||
|
||||
@staticmethod
|
||||
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import ast
|
||||
import json
|
||||
from typing import Optional
|
||||
from langflow.services.database.models.base import orjson_dumps
|
||||
|
||||
from langflow.template.field.base import TemplateField
|
||||
from langflow.template.frontend_node.base import FrontendNode
|
||||
|
|
@ -22,4 +22,4 @@ class UtilitiesFrontendNode(FrontendNode):
|
|||
|
||||
if isinstance(field.value, dict):
|
||||
field.field_type = "code"
|
||||
field.value = json.dumps(field.value, indent=4)
|
||||
field.value = orjson_dumps(field.value)
|
||||
|
|
|
|||
14
src/frontend/package-lock.json
generated
14
src/frontend/package-lock.json
generated
|
|
@ -45,6 +45,7 @@
|
|||
"esbuild": "^0.17.18",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.233.0",
|
||||
"moment": "^2.29.4",
|
||||
"react": "^18.2.0",
|
||||
"react-ace": "^10.1.0",
|
||||
"react-cookie": "^4.1.1",
|
||||
|
|
@ -7730,6 +7731,14 @@
|
|||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/mri": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||
|
|
@ -16181,6 +16190,11 @@
|
|||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz",
|
||||
"integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A=="
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
|
||||
},
|
||||
"mri": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@
|
|||
"esbuild": "^0.17.18",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.233.0",
|
||||
"moment": "^2.29.4",
|
||||
"react": "^18.2.0",
|
||||
"react-ace": "^10.1.0",
|
||||
"react-cookie": "^4.1.1",
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
0
src/frontend/set_proxy.sh
Executable file → Normal file
0
src/frontend/set_proxy.sh
Executable file → Normal file
|
|
@ -1,6 +1,6 @@
|
|||
import _ from "lodash";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import "reactflow/dist/style.css";
|
||||
import "./App.css";
|
||||
|
||||
|
|
@ -9,10 +9,18 @@ import ErrorAlert from "./alerts/error";
|
|||
import NoticeAlert from "./alerts/notice";
|
||||
import SuccessAlert from "./alerts/success";
|
||||
import CrashErrorComponent from "./components/CrashErrorComponent";
|
||||
import FetchErrorComponent from "./components/fetchErrorComponent";
|
||||
import LoadingComponent from "./components/loadingComponent";
|
||||
import {
|
||||
FETCH_ERROR_DESCRIPION,
|
||||
FETCH_ERROR_MESSAGE,
|
||||
} from "./constants/constants";
|
||||
import { alertContext } from "./contexts/alertContext";
|
||||
import { AuthContext } from "./contexts/authContext";
|
||||
import { locationContext } from "./contexts/locationContext";
|
||||
import { TabsContext } from "./contexts/tabsContext";
|
||||
import { autoLogin, getLoggedUser } from "./controllers/API";
|
||||
import { typesContext } from "./contexts/typesContext";
|
||||
import Router from "./routes";
|
||||
|
||||
export default function App() {
|
||||
|
|
@ -36,8 +44,12 @@ export default function App() {
|
|||
successData,
|
||||
successOpen,
|
||||
setSuccessOpen,
|
||||
setErrorData,
|
||||
loading,
|
||||
setLoading
|
||||
} = useContext(alertContext);
|
||||
const navigate = useNavigate();
|
||||
const { fetchError } = useContext(typesContext);
|
||||
|
||||
// Initialize state variable for the list of alerts
|
||||
const [alertsList, setAlertsList] = useState<
|
||||
|
|
@ -48,6 +60,11 @@ export default function App() {
|
|||
}>
|
||||
>([]);
|
||||
|
||||
const isLoginPage = location.pathname.includes("login");
|
||||
const isAdminPage = location.pathname.includes("admin");
|
||||
const isSignUpPage = location.pathname.includes("signup");
|
||||
const isLocalHost = window.location.href.includes("localhost");
|
||||
|
||||
// Use effect hook to update alertsList when a new alert is added
|
||||
useEffect(() => {
|
||||
// If there is an error alert open with data, add it to the alertsList
|
||||
|
|
@ -123,6 +140,37 @@ export default function App() {
|
|||
);
|
||||
};
|
||||
|
||||
//this function is to get the user logged in when the page is refreshed
|
||||
const { setUserData, getAuthentication, login, setAutoLogin, logout } =
|
||||
useContext(AuthContext);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
autoLogin().then((user) => {
|
||||
if(user && user['access_token']){
|
||||
user['refresh_token'] = "auto";
|
||||
login(user['access_token'], user['refresh_token']);
|
||||
setUserData(user);
|
||||
setAutoLogin(true);
|
||||
setLoading(false);
|
||||
}
|
||||
}).catch((error) => {
|
||||
setAutoLogin(false);
|
||||
if (getAuthentication() && !isLoginPage) {
|
||||
getLoggedUser()
|
||||
.then((user) => {
|
||||
setUserData(user);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((error) => {});
|
||||
}
|
||||
else{
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
//need parent component with width and height
|
||||
<div className="flex h-full flex-col">
|
||||
|
|
@ -137,7 +185,14 @@ export default function App() {
|
|||
>
|
||||
{loading ? (
|
||||
<div className="loading-page-panel">
|
||||
<LoadingComponent remSize={50} />
|
||||
{fetchError ? (
|
||||
<FetchErrorComponent
|
||||
description={FETCH_ERROR_DESCRIPION}
|
||||
message={FETCH_ERROR_MESSAGE}
|
||||
></FetchErrorComponent>
|
||||
) : (
|
||||
<LoadingComponent remSize={50} />
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -32,13 +32,6 @@ export default function AccordionComponent({
|
|||
value === "" ? setValue(keyValue!) : setValue("");
|
||||
}
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.key === "Backspace") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Accordion
|
||||
|
|
@ -46,7 +39,6 @@ export default function AccordionComponent({
|
|||
className="w-full"
|
||||
value={value}
|
||||
onValueChange={setValue}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<AccordionItem value={keyValue!} className="border-b">
|
||||
<AccordionTrigger
|
||||
|
|
|
|||
|
|
@ -12,17 +12,17 @@ import { Button } from "../ui/button";
|
|||
|
||||
export default function PaginatorComponent({
|
||||
pageSize = 10,
|
||||
pageIndex = 1,
|
||||
rowsCount = [10, 20, 30],
|
||||
pageIndex = 0,
|
||||
rowsCount = [10, 20, 50, 100],
|
||||
totalRowsCount = 0,
|
||||
paginate,
|
||||
}: PaginatorComponentType) {
|
||||
const [size, setPageSize] = useState(pageSize);
|
||||
const [index, setPageIndex] = useState(pageIndex);
|
||||
|
||||
const [maxIndex, setMaxPageIndex] = useState(
|
||||
Math.ceil(totalRowsCount / pageSize)
|
||||
);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -35,7 +35,7 @@ export default function PaginatorComponent({
|
|||
onValueChange={(pageSize: string) => {
|
||||
setPageSize(Number(pageSize));
|
||||
setMaxPageIndex(Math.ceil(totalRowsCount / Number(pageSize)));
|
||||
paginate(Number(pageSize), index);
|
||||
paginate(Number(pageSize), 0);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[100px]">
|
||||
|
|
@ -51,30 +51,30 @@ export default function PaginatorComponent({
|
|||
</Select>
|
||||
</div>
|
||||
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
|
||||
Page {index} of {maxIndex}
|
||||
Page {currentPage} of {maxIndex}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
disabled={index <= 0}
|
||||
variant="outline"
|
||||
className="hidden h-8 w-8 p-0 lg:flex"
|
||||
onClick={() => {
|
||||
setPageIndex(1);
|
||||
paginate(size, 1);
|
||||
setPageIndex(0);
|
||||
setCurrentPage(1);
|
||||
paginate(size, 0);
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Go to first page</span>
|
||||
<IconComponent name="ChevronsLeft" className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
disabled={index <= 0}
|
||||
onClick={() => {
|
||||
if (index <= 1) {
|
||||
setPageIndex(1);
|
||||
paginate(size, 1);
|
||||
} else {
|
||||
{
|
||||
setPageIndex(index - 1);
|
||||
paginate(size, index - 1);
|
||||
}
|
||||
if (index > 0) {
|
||||
const pgIndex = size - index;
|
||||
setCurrentPage(currentPage - 1);
|
||||
setPageIndex(pgIndex);
|
||||
paginate(size, pgIndex);
|
||||
}
|
||||
}}
|
||||
variant="outline"
|
||||
|
|
@ -84,14 +84,12 @@ export default function PaginatorComponent({
|
|||
<IconComponent name="ChevronLeft" className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
disabled={currentPage === maxIndex}
|
||||
onClick={() => {
|
||||
if (index >= maxIndex) {
|
||||
setPageIndex(maxIndex);
|
||||
paginate(size, maxIndex);
|
||||
} else {
|
||||
setPageIndex(index + 1);
|
||||
paginate(size, index + 1);
|
||||
}
|
||||
const pgIndex = size + index;
|
||||
setPageIndex(pgIndex);
|
||||
setCurrentPage(currentPage + 1);
|
||||
paginate(size, pgIndex);
|
||||
}}
|
||||
variant="outline"
|
||||
className="h-8 w-8 p-0"
|
||||
|
|
@ -100,11 +98,13 @@ export default function PaginatorComponent({
|
|||
<IconComponent name="ChevronRight" className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
disabled={currentPage === maxIndex}
|
||||
variant="outline"
|
||||
className="hidden h-8 w-8 p-0 lg:flex"
|
||||
onClick={() => {
|
||||
setPageIndex(maxIndex);
|
||||
paginate(size, maxIndex);
|
||||
setPageIndex(maxIndex - 1);
|
||||
setCurrentPage(maxIndex);
|
||||
paginate(size, size);
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Go to last page</span>
|
||||
|
|
|
|||
24
src/frontend/src/components/authAdminGuard/index.tsx
Normal file
24
src/frontend/src/components/authAdminGuard/index.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { useContext, useEffect } from "react";
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
|
||||
export const ProtectedAdminRoute = ({ children }) => {
|
||||
const { isAdmin, isAuthenticated, logout, getAuthentication, userData, autoLogin } =
|
||||
useContext(AuthContext);
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated && !getAuthentication()) {
|
||||
window.location.replace("/login");
|
||||
logout();
|
||||
}
|
||||
}, [isAuthenticated, getAuthentication, logout, userData]);
|
||||
|
||||
if (!isAuthenticated && !getAuthentication()) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
if (userData && !isAdmin || autoLogin) {
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
14
src/frontend/src/components/authGuard/index.tsx
Normal file
14
src/frontend/src/components/authGuard/index.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { useContext } from "react";
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
|
||||
export const ProtectedRoute = ({ children }) => {
|
||||
const { isAuthenticated, logout, getAuthentication } =
|
||||
useContext(AuthContext);
|
||||
if (!isAuthenticated && !getAuthentication()) {
|
||||
logout();
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
19
src/frontend/src/components/authLoginGuard/index.tsx
Normal file
19
src/frontend/src/components/authLoginGuard/index.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { useContext } from "react";
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
|
||||
export const ProtectedLoginRoute = ({ children }) => {
|
||||
const { getAuthentication, autoLogin } = useContext(AuthContext);
|
||||
|
||||
if (autoLogin === true) {
|
||||
window.location.replace("/");
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
if (getAuthentication()) {
|
||||
window.location.replace("/");
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
13
src/frontend/src/components/catchAllRoutes/index.tsx
Normal file
13
src/frontend/src/components/catchAllRoutes/index.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export const CatchAllRoute = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Redirect to the root ("/") when the catch-all route is matched
|
||||
useEffect(() => {
|
||||
navigate("/");
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
@ -60,6 +60,11 @@ export default function BuildTrigger({
|
|||
],
|
||||
});
|
||||
}
|
||||
if (errors.length === 0 && allNodesValid) {
|
||||
setSuccessData({
|
||||
title: "Flow is ready to run",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -28,8 +28,11 @@ import {
|
|||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "../../components/ui/tabs";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { darkContext } from "../../contexts/darkContext";
|
||||
import { typesContext } from "../../contexts/typesContext";
|
||||
import { codeTabsPropsType } from "../../types/components";
|
||||
import { unselectAllNodes } from "../../utils/reactflowUtils";
|
||||
import { classNames } from "../../utils/utils";
|
||||
import IconComponent from "../genericIconComponent";
|
||||
|
||||
|
|
@ -45,6 +48,8 @@ export default function CodeTabsComponent({
|
|||
const [data, setData] = useState(flow ? flow["data"]!["nodes"] : null);
|
||||
const [openAccordion, setOpenAccordion] = useState<string[]>([]);
|
||||
const { dark } = useContext(darkContext);
|
||||
const { reactFlowInstance } = useContext(typesContext);
|
||||
const { isTweakPage, setIsTweakPage } = useContext(alertContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (flow && flow["data"]!["nodes"]) {
|
||||
|
|
@ -52,6 +57,19 @@ export default function CodeTabsComponent({
|
|||
}
|
||||
}, [flow]);
|
||||
|
||||
useEffect(() => {
|
||||
unselectAllNodes({
|
||||
data,
|
||||
updateNodes: (nodes) => {
|
||||
reactFlowInstance?.setNodes(nodes);
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (isTweakPage) setIsTweakPage(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const copyToClipboard = () => {
|
||||
if (!navigator.clipboard || !navigator.clipboard.writeText) {
|
||||
return;
|
||||
|
|
@ -159,13 +177,13 @@ export default function CodeTabsComponent({
|
|||
)}
|
||||
</div>
|
||||
|
||||
{tabs.map((tab, index) => (
|
||||
{tabs.map((tab, idx) => (
|
||||
<TabsContent
|
||||
value={index.toString()}
|
||||
value={idx.toString()}
|
||||
className="api-modal-tabs-content"
|
||||
key={index} // Remember to add a unique key prop
|
||||
key={idx} // Remember to add a unique key prop
|
||||
>
|
||||
{index < 4 ? (
|
||||
{idx < 4 ? (
|
||||
<>
|
||||
{tab.description && (
|
||||
<div
|
||||
|
|
@ -181,7 +199,7 @@ export default function CodeTabsComponent({
|
|||
{tab.code}
|
||||
</SyntaxHighlighter>
|
||||
</>
|
||||
) : index === 4 ? (
|
||||
) : idx === 4 ? (
|
||||
<>
|
||||
<div className="api-modal-according-display">
|
||||
<div
|
||||
|
|
@ -192,8 +210,8 @@ export default function CodeTabsComponent({
|
|||
: "overflow-hidden"
|
||||
)}
|
||||
>
|
||||
{data?.map((node: any, index) => (
|
||||
<div className="px-3" key={index}>
|
||||
{data?.map((node: any, i) => (
|
||||
<div className="px-3" key={i}>
|
||||
{tweaks?.tweaksList!.current.includes(
|
||||
node["data"]["id"]
|
||||
) && (
|
||||
|
|
@ -236,10 +254,10 @@ export default function CodeTabsComponent({
|
|||
node.data.node.template[templateField]
|
||||
.type === "int")
|
||||
)
|
||||
.map((templateField, index) => {
|
||||
.map((templateField, indx) => {
|
||||
return (
|
||||
<TableRow
|
||||
key={index}
|
||||
key={indx}
|
||||
className="h-10 dark:border-b-muted"
|
||||
>
|
||||
<TableCell className="p-0 text-center text-sm text-foreground">
|
||||
|
|
@ -278,7 +296,7 @@ export default function CodeTabsComponent({
|
|||
let newInputList =
|
||||
cloneDeep(old);
|
||||
newInputList![
|
||||
index
|
||||
i
|
||||
].data.node.template[
|
||||
templateField
|
||||
].value = target;
|
||||
|
|
@ -327,7 +345,7 @@ export default function CodeTabsComponent({
|
|||
let newInputList =
|
||||
cloneDeep(old);
|
||||
newInputList![
|
||||
index
|
||||
i
|
||||
].data.node.template[
|
||||
templateField
|
||||
].value = target;
|
||||
|
|
@ -372,7 +390,7 @@ export default function CodeTabsComponent({
|
|||
let newInputList =
|
||||
cloneDeep(old);
|
||||
newInputList![
|
||||
index
|
||||
i
|
||||
].data.node.template[
|
||||
templateField
|
||||
].value = target;
|
||||
|
|
@ -405,7 +423,7 @@ export default function CodeTabsComponent({
|
|||
let newInputList =
|
||||
cloneDeep(old);
|
||||
newInputList![
|
||||
index
|
||||
i
|
||||
].data.node.template[
|
||||
templateField
|
||||
].value = e;
|
||||
|
|
@ -496,7 +514,7 @@ export default function CodeTabsComponent({
|
|||
let newInputList =
|
||||
cloneDeep(old);
|
||||
newInputList![
|
||||
index
|
||||
i
|
||||
].data.node.template[
|
||||
templateField
|
||||
].value = target;
|
||||
|
|
@ -532,7 +550,7 @@ export default function CodeTabsComponent({
|
|||
let newInputList =
|
||||
cloneDeep(old);
|
||||
newInputList![
|
||||
index
|
||||
i
|
||||
].data.node.template[
|
||||
templateField
|
||||
].value = target;
|
||||
|
|
@ -584,7 +602,7 @@ export default function CodeTabsComponent({
|
|||
let newInputList =
|
||||
cloneDeep(old);
|
||||
newInputList![
|
||||
index
|
||||
i
|
||||
].data.node.template[
|
||||
templateField
|
||||
].value = target;
|
||||
|
|
@ -639,7 +657,7 @@ export default function CodeTabsComponent({
|
|||
let newInputList =
|
||||
cloneDeep(old);
|
||||
newInputList![
|
||||
index
|
||||
i
|
||||
].data.node.template[
|
||||
templateField
|
||||
].value = target;
|
||||
|
|
@ -694,7 +712,7 @@ export default function CodeTabsComponent({
|
|||
let newInputList =
|
||||
cloneDeep(old);
|
||||
newInputList![
|
||||
index
|
||||
i
|
||||
].data.node.template[
|
||||
templateField
|
||||
].value = target;
|
||||
|
|
|
|||
16
src/frontend/src/components/fetchErrorComponent/index.tsx
Normal file
16
src/frontend/src/components/fetchErrorComponent/index.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { fetchErrorComponentType } from "../../types/components";
|
||||
import IconComponent from "../genericIconComponent";
|
||||
|
||||
export default function FetchErrorComponent({
|
||||
message,
|
||||
description,
|
||||
}: fetchErrorComponentType) {
|
||||
return (
|
||||
<div role="status" className="m-auto flex flex-col items-center">
|
||||
<IconComponent className={`h-16 w-16`} name="Unplug"></IconComponent>
|
||||
<br></br>
|
||||
<span className="text-lg text-almost-medium-blue">{message}</span>
|
||||
<span className="text-lg text-almost-medium-blue">{description}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,17 +1,19 @@
|
|||
import { forwardRef } from "react";
|
||||
import { IconComponentProps } from "../../types/components";
|
||||
import { nodeIconsLucide } from "../../utils/styleUtils";
|
||||
|
||||
export default function IconComponent({
|
||||
name,
|
||||
className,
|
||||
iconColor,
|
||||
}: IconComponentProps): JSX.Element {
|
||||
const TargetIcon = nodeIconsLucide[name] ?? nodeIconsLucide["unknown"];
|
||||
return (
|
||||
<TargetIcon
|
||||
strokeWidth={1.5}
|
||||
className={className}
|
||||
style={{ color: iconColor }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const ForwardedIconComponent = forwardRef(
|
||||
({ name, className, iconColor }: IconComponentProps, ref) => {
|
||||
const TargetIcon = nodeIconsLucide[name] ?? nodeIconsLucide["unknown"];
|
||||
return (
|
||||
<TargetIcon
|
||||
strokeWidth={1.5}
|
||||
className={className}
|
||||
style={iconColor ? { color: iconColor } : {}}
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default ForwardedIconComponent;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { useContext, useEffect, useState } from "react";
|
||||
import { FaDiscord, FaGithub, FaTwitter } from "react-icons/fa";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import AlertDropdown from "../../alerts/alertDropDown";
|
||||
import { USER_PROJECTS_HEADER } from "../../constants/constants";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
import { darkContext } from "../../contexts/darkContext";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { getRepoStars } from "../../controllers/API";
|
||||
|
|
@ -17,29 +18,48 @@ export default function Header(): JSX.Element {
|
|||
const { dark, setDark } = useContext(darkContext);
|
||||
const { notificationCenter } = useContext(alertContext);
|
||||
const location = useLocation();
|
||||
const { logout, autoLogin, isAdmin, stars } = useContext(AuthContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [stars, setStars] = useState(null);
|
||||
|
||||
// Get and set numbers of stars on header
|
||||
useEffect(() => {
|
||||
async function fetchStars() {
|
||||
const starsCount = await getRepoStars("logspace-ai", "langflow");
|
||||
setStars(starsCount);
|
||||
}
|
||||
fetchStars();
|
||||
}, []);
|
||||
return (
|
||||
<div className="header-arrangement">
|
||||
<div className="header-start-display">
|
||||
<Link to="/">
|
||||
<span className="ml-4 text-2xl">⛓️</span>
|
||||
</Link>
|
||||
<Button variant="outline" className="">
|
||||
Sign out
|
||||
</Button>
|
||||
|
||||
{flows.findIndex((f) => tabId === f.id) !== -1 && tabId !== "" && (
|
||||
<MenuBar flows={flows} tabId={tabId} />
|
||||
)}
|
||||
{location.pathname === "/admin" && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigate("/");
|
||||
}}
|
||||
variant="outline"
|
||||
className=""
|
||||
>
|
||||
Main page
|
||||
</Button>
|
||||
)}
|
||||
{autoLogin === false && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
logout();
|
||||
navigate("/login");
|
||||
}}
|
||||
variant="outline"
|
||||
className=""
|
||||
>
|
||||
Sign out
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isAdmin && !autoLogin && location.pathname !== "/admin" && (
|
||||
<Button variant="outline" onClick={() => navigate("/admin")}>
|
||||
Admin page
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="round-button-div">
|
||||
<Link to="/">
|
||||
|
|
@ -119,6 +139,18 @@ export default function Header(): JSX.Element {
|
|||
/>
|
||||
</div>
|
||||
</AlertDropdown>
|
||||
{!autoLogin && (
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate("/account/api-keys");
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
name="Key"
|
||||
className="side-bar-button-size text-muted-foreground hover:text-accent-foreground"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -81,7 +81,8 @@ export default function InputComponent({
|
|||
? "input-component-true-button"
|
||||
: "input-component-false-button"
|
||||
)}
|
||||
onClick={() => {
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
setPwdVisible(!pwdVisible);
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -508,6 +508,7 @@ export const URL_EXCLUDED_FROM_ERROR_RETRIES = [
|
|||
"/api/v1/validate/code",
|
||||
"/api/v1/custom_component",
|
||||
"/api/v1/validate/prompt",
|
||||
"http://localhost:7860/login",
|
||||
];
|
||||
|
||||
export const skipNodeUpdate = ["CustomComponent"];
|
||||
|
|
@ -522,6 +523,18 @@ export const CONTROL_LOGIN_STATE = {
|
|||
username: "",
|
||||
password: "",
|
||||
};
|
||||
|
||||
export const CONTROL_NEW_USER = {
|
||||
username: "",
|
||||
password: "",
|
||||
is_active: false,
|
||||
is_superuser: false,
|
||||
};
|
||||
|
||||
export const CONTROL_NEW_API_KEY = {
|
||||
apikeyname: "",
|
||||
};
|
||||
|
||||
export const tabsCode = [];
|
||||
|
||||
export function tabsArray(codes: string[], method: number) {
|
||||
|
|
@ -602,3 +615,11 @@ export function tabsArray(codes: string[], method: number) {
|
|||
},
|
||||
];
|
||||
}
|
||||
export const FETCH_ERROR_MESSAGE = "Couldn't establish a connection.";
|
||||
export const FETCH_ERROR_DESCRIPION =
|
||||
"Check if everything is working properly and try again.";
|
||||
|
||||
export const BASE_URL_API = "http://localhost:7860/";
|
||||
|
||||
export const SIGN_UP_SUCCESS =
|
||||
"Account created! Await admin activation. ";
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ const initialValue: alertContextType = {
|
|||
pushNotificationList: () => {},
|
||||
clearNotificationList: () => {},
|
||||
removeFromNotificationList: () => {},
|
||||
isTweakPage: false,
|
||||
setIsTweakPage: () => {},
|
||||
};
|
||||
|
||||
export const alertContext = createContext<alertContextType>(initialValue);
|
||||
|
|
@ -48,6 +50,7 @@ export function AlertProvider({ children }: { children: ReactNode }) {
|
|||
const [successOpen, setSuccessOpen] = useState(false);
|
||||
const [notificationCenter, setNotificationCenter] = useState(false);
|
||||
const [notificationList, setNotificationList] = useState<AlertItemType[]>([]);
|
||||
const [isTweakPage, setIsTweakPage] = useState<boolean>(false);
|
||||
const pushNotificationList = (notification: AlertItemType) => {
|
||||
setNotificationList((old) => {
|
||||
let newNotificationList = _.cloneDeep(old);
|
||||
|
|
@ -120,6 +123,8 @@ export function AlertProvider({ children }: { children: ReactNode }) {
|
|||
return (
|
||||
<alertContext.Provider
|
||||
value={{
|
||||
isTweakPage,
|
||||
setIsTweakPage,
|
||||
removeFromNotificationList,
|
||||
clearNotificationList,
|
||||
notificationList,
|
||||
|
|
|
|||
|
|
@ -1,44 +1,87 @@
|
|||
import { createContext, useEffect, useState } from "react";
|
||||
import { AuthContextType, userData } from "../types/contexts/auth";
|
||||
import Cookies from "universal-cookie";
|
||||
import { getLoggedUser, getRepoStars } from "../controllers/API";
|
||||
import { Users } from "../types/api";
|
||||
import { AuthContextType } from "../types/contexts/auth";
|
||||
|
||||
const initialValue: AuthContextType = {
|
||||
isAdmin: false,
|
||||
setIsAdmin: () => false,
|
||||
isAuthenticated: false,
|
||||
accessToken: null,
|
||||
refreshToken: null,
|
||||
login: () => {},
|
||||
logout: () => {},
|
||||
refreshAccessToken: () => Promise.resolve(),
|
||||
userData: null,
|
||||
setUserData: () => {},
|
||||
getAuthentication: () => false,
|
||||
authenticationErrorCount: 0,
|
||||
autoLogin: false,
|
||||
setAutoLogin: () => {},
|
||||
stars: 0,
|
||||
setStars: (stars) => 0,
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextType>(initialValue);
|
||||
export const AuthContext = createContext<AuthContextType>(initialValue);
|
||||
|
||||
export function AuthProvider({ children }): React.ReactElement {
|
||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||
const [userData, setUserData] = useState<userData | null>(null);
|
||||
const [refreshToken, setRefreshToken] = useState<string | null>(null);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
||||
const [isAdmin, setIsAdmin] = useState<boolean>(false);
|
||||
const [userData, setUserData] = useState<Users | null>(null);
|
||||
const [autoLogin, setAutoLogin] = useState<boolean>(false);
|
||||
const [stars, setStars] = useState<number>(0);
|
||||
const cookies = new Cookies();
|
||||
|
||||
useEffect(() => {
|
||||
const storedAccessToken = localStorage.getItem("access_token");
|
||||
const storedAccessToken = cookies.get("access_token");
|
||||
if (storedAccessToken) {
|
||||
setAccessToken(storedAccessToken);
|
||||
}
|
||||
async function fetchStars() {
|
||||
const starsCount = await getRepoStars("logspace-ai", "langflow");
|
||||
setStars(starsCount);
|
||||
}
|
||||
fetchStars();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (accessToken) {
|
||||
getLoggedUser().then((user) => {
|
||||
const isSuperUser = user.is_superuser;
|
||||
setIsAdmin(isSuperUser);
|
||||
});
|
||||
}
|
||||
}, [accessToken, isAdmin]);
|
||||
|
||||
function getAuthentication() {
|
||||
const storedRefreshToken = cookies.get("refresh_token");
|
||||
const storedAccess = cookies.get("access_token");
|
||||
const auth = storedAccess && storedRefreshToken ? true : false;
|
||||
return auth;
|
||||
}
|
||||
|
||||
function login(newAccessToken: string, refreshToken: string) {
|
||||
localStorage.setItem("access_token", newAccessToken);
|
||||
cookies.set("access_token", newAccessToken, { path: "/" });
|
||||
cookies.set("refresh_token", refreshToken, { path: "/" });
|
||||
setAccessToken(newAccessToken);
|
||||
// Store refreshToken if needed
|
||||
setRefreshToken(refreshToken);
|
||||
setIsAuthenticated(true);
|
||||
}
|
||||
|
||||
function logout() {
|
||||
localStorage.removeItem("access_token");
|
||||
// Clear refreshToken if used
|
||||
cookies.remove("access_token", { path: "/" });
|
||||
cookies.remove("refresh_token", { path: "/" });
|
||||
setUserData(null);
|
||||
setAccessToken(null);
|
||||
setRefreshToken(null);
|
||||
setIsAuthenticated(false);
|
||||
}
|
||||
|
||||
async function refreshAccessToken(refreshToken: string) {
|
||||
try {
|
||||
// Call your API to refresh the access token using the refresh token
|
||||
const response = await fetch("/api/refresh-token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
|
@ -50,6 +93,9 @@ export function AuthProvider({ children }): React.ReactElement {
|
|||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
login(data.accessToken, refreshToken);
|
||||
getLoggedUser().then((user) => {
|
||||
console.log("oi");
|
||||
});
|
||||
} else {
|
||||
logout();
|
||||
}
|
||||
|
|
@ -62,13 +108,22 @@ export function AuthProvider({ children }): React.ReactElement {
|
|||
// !! to convert string to boolean
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
stars,
|
||||
setStars,
|
||||
isAdmin,
|
||||
setIsAdmin,
|
||||
isAuthenticated: !!accessToken,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
login,
|
||||
logout,
|
||||
refreshAccessToken,
|
||||
setUserData,
|
||||
userData,
|
||||
getAuthentication,
|
||||
authenticationErrorCount: 0,
|
||||
setAutoLogin,
|
||||
autoLogin,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { ReactFlowProvider } from "reactflow";
|
|||
import { TooltipProvider } from "../components/ui/tooltip";
|
||||
import { SSEProvider } from "./SSEContext";
|
||||
import { AlertProvider } from "./alertContext";
|
||||
import { AuthProvider } from "./authContext";
|
||||
import { DarkProvider } from "./darkContext";
|
||||
import { LocationProvider } from "./locationContext";
|
||||
import { TabsProvider } from "./tabsContext";
|
||||
|
|
@ -13,23 +14,25 @@ export default function ContextWrapper({ children }: { children: ReactNode }) {
|
|||
//element to wrap all context
|
||||
return (
|
||||
<>
|
||||
<TooltipProvider>
|
||||
<ReactFlowProvider>
|
||||
<DarkProvider>
|
||||
<AlertProvider>
|
||||
<AuthProvider>
|
||||
<TooltipProvider>
|
||||
<ReactFlowProvider>
|
||||
<DarkProvider>
|
||||
<TypesProvider>
|
||||
<LocationProvider>
|
||||
<SSEProvider>
|
||||
<TabsProvider>
|
||||
<UndoRedoProvider>{children}</UndoRedoProvider>
|
||||
</TabsProvider>
|
||||
</SSEProvider>
|
||||
<AlertProvider>
|
||||
<SSEProvider>
|
||||
<TabsProvider>
|
||||
<UndoRedoProvider>{children}</UndoRedoProvider>
|
||||
</TabsProvider>
|
||||
</SSEProvider>
|
||||
</AlertProvider>
|
||||
</LocationProvider>
|
||||
</TypesProvider>
|
||||
</AlertProvider>
|
||||
</DarkProvider>
|
||||
</ReactFlowProvider>
|
||||
</TooltipProvider>
|
||||
</DarkProvider>
|
||||
</ReactFlowProvider>
|
||||
</TooltipProvider>
|
||||
</AuthProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import {
|
|||
import { getRandomDescription, getRandomName } from "../utils/utils";
|
||||
import { alertContext } from "./alertContext";
|
||||
import { typesContext } from "./typesContext";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
const uid = new ShortUniqueId({ length: 5 });
|
||||
|
||||
|
|
@ -68,7 +69,7 @@ export const TabsContext = createContext<TabsContextType>(
|
|||
);
|
||||
|
||||
export function TabsProvider({ children }: { children: ReactNode }) {
|
||||
const { setErrorData, setNoticeData } = useContext(alertContext);
|
||||
const { setErrorData, setNoticeData, setSuccessData } = useContext(alertContext);
|
||||
|
||||
const [tabId, setTabId] = useState("");
|
||||
|
||||
|
|
@ -579,6 +580,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
const updatedFlow = await updateFlowInDatabase(newFlow);
|
||||
if (updatedFlow) {
|
||||
// updates flow in state
|
||||
setSuccessData({ title: "Changes saved successfully" });
|
||||
setFlows((prevState) => {
|
||||
const newFlows = [...prevState];
|
||||
const index = newFlows.findIndex((flow) => flow.id === newFlow.id);
|
||||
|
|
@ -601,7 +603,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
});
|
||||
}
|
||||
} catch (err) {
|
||||
setErrorData(err as errorsVarType);
|
||||
setErrorData({title: "Error while saving changes",list:[(err as AxiosError).message]});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
useState,
|
||||
} from "react";
|
||||
import { Node, ReactFlowInstance } from "reactflow";
|
||||
import { getAll } from "../controllers/API";
|
||||
import { getAll, getHealth } from "../controllers/API";
|
||||
import { APIKindType } from "../types/api";
|
||||
import { typesContextType } from "../types/typesContext";
|
||||
import { alertContext } from "./alertContext";
|
||||
|
|
@ -23,6 +23,8 @@ const initialValue: typesContextType = {
|
|||
setTemplates: () => {},
|
||||
data: {},
|
||||
setData: () => {},
|
||||
setFetchError: () => {},
|
||||
fetchError: false,
|
||||
};
|
||||
|
||||
export const typesContext = createContext<typesContextType>(initialValue);
|
||||
|
|
@ -33,14 +35,10 @@ export function TypesProvider({ children }: { children: ReactNode }) {
|
|||
useState<ReactFlowInstance | null>(null);
|
||||
const [templates, setTemplates] = useState({});
|
||||
const [data, setData] = useState({});
|
||||
const [fetchError, setFetchError] = useState(false);
|
||||
const { setLoading } = useContext(alertContext);
|
||||
|
||||
useEffect(() => {
|
||||
let delay = 1000; // Start delay of 1 second
|
||||
let intervalId: NodeJS.Timer;
|
||||
let retryCount = 0; // Count of retry attempts
|
||||
const maxRetryCount = 5; // Max retry attempts
|
||||
|
||||
// We will keep a flag to handle the case where the component is unmounted before the API call resolves.
|
||||
let isMounted = true;
|
||||
|
||||
|
|
@ -48,7 +46,7 @@ export function TypesProvider({ children }: { children: ReactNode }) {
|
|||
try {
|
||||
const result = await getAll();
|
||||
// Make sure to only update the state if the component is still mounted.
|
||||
if (isMounted) {
|
||||
if (isMounted && result?.status === 200) {
|
||||
setLoading(false);
|
||||
setData(result.data);
|
||||
setTemplates(
|
||||
|
|
@ -78,21 +76,15 @@ export function TypesProvider({ children }: { children: ReactNode }) {
|
|||
}, {})
|
||||
);
|
||||
}
|
||||
// Clear the interval if successful.
|
||||
clearInterval(intervalId!);
|
||||
} catch (error) {
|
||||
console.error("An error has occurred while fetching types.");
|
||||
await getHealth().catch((e) => {
|
||||
setFetchError(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Start the initial interval.
|
||||
intervalId = setInterval(getTypes, delay);
|
||||
return () => {
|
||||
// This will clear the interval when the component unmounts, or when the dependencies of the useEffect hook change.
|
||||
clearInterval(intervalId!);
|
||||
// Indicate that the component has been unmounted.
|
||||
isMounted = false;
|
||||
};
|
||||
getTypes();
|
||||
}, []);
|
||||
|
||||
function deleteNode(idx: string) {
|
||||
|
|
@ -117,6 +109,8 @@ export function TypesProvider({ children }: { children: ReactNode }) {
|
|||
templates,
|
||||
data,
|
||||
setData,
|
||||
fetchError,
|
||||
setFetchError,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -1,53 +1,104 @@
|
|||
import axios, { AxiosError, AxiosInstance } from "axios";
|
||||
import { useContext, useEffect, useRef } from "react";
|
||||
import { useContext, useEffect } from "react";
|
||||
import { Cookies } from "react-cookie";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { renewAccessToken } from ".";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
|
||||
// Create a new Axios instance
|
||||
const api: AxiosInstance = axios.create({
|
||||
baseURL: "",
|
||||
});
|
||||
|
||||
function ApiInterceptor(): null {
|
||||
const retryCounts = useRef([]);
|
||||
function ApiInterceptor() {
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
let { accessToken, login, logout, authenticationErrorCount } =
|
||||
useContext(AuthContext);
|
||||
const navigate = useNavigate();
|
||||
const cookies = new Cookies();
|
||||
|
||||
useEffect(() => {
|
||||
const interceptor = api.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error: AxiosError) => {
|
||||
// if (URL_EXCLUDED_FROM_ERROR_RETRIES.includes(error.config?.url)) {
|
||||
// return Promise.reject(error);
|
||||
// }
|
||||
// let retryCount = 0;
|
||||
// while (retryCount < 4) {
|
||||
// await sleep(5000); // Sleep for 5 seconds
|
||||
// retryCount++;
|
||||
// try {
|
||||
// const response = await axios.request(error.config);
|
||||
// return response;
|
||||
// } catch (error) {
|
||||
// if (retryCount === 3) {
|
||||
// setErrorData({
|
||||
// title: "There was an error on web connection, please: ",
|
||||
// list: [
|
||||
// "Refresh the page",
|
||||
// "Use a new flow tab",
|
||||
// "Check if the backend is up",
|
||||
// "Endpoint: " + error.config?.url,
|
||||
// ],
|
||||
// });
|
||||
// return Promise.reject(error);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
if (error.response?.status === 401) {
|
||||
const refreshToken = cookies.get("refresh_token");
|
||||
|
||||
if (refreshToken) {
|
||||
authenticationErrorCount = authenticationErrorCount + 1;
|
||||
if (authenticationErrorCount > 3) {
|
||||
authenticationErrorCount = 0;
|
||||
logout();
|
||||
navigate("/login");
|
||||
}
|
||||
|
||||
const res = await renewAccessToken(refreshToken);
|
||||
login(res.data.access_token, res.data.refresh_token);
|
||||
try {
|
||||
if (error?.config?.headers) {
|
||||
delete error.config.headers["Authorization"];
|
||||
error.config.headers["Authorization"] = `Bearer ${accessToken}`;
|
||||
const response = await axios.request(error.config);
|
||||
return response;
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 401) {
|
||||
logout();
|
||||
navigate("/login");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!refreshToken && error?.config?.url?.includes("login")) {
|
||||
return Promise.reject(error);
|
||||
} else {
|
||||
logout();
|
||||
navigate("/login");
|
||||
}
|
||||
} else {
|
||||
// if (URL_EXCLUDED_FROM_ERROR_RETRIES.includes(error.config?.url)) {
|
||||
return Promise.reject(error);
|
||||
// }
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Request interceptor to add access token to every request
|
||||
const requestInterceptor = api.interceptors.request.use(
|
||||
(config) => {
|
||||
if (accessToken) {
|
||||
config.headers["Authorization"] = `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
if (
|
||||
config?.url?.includes(
|
||||
"https://raw.githubusercontent.com/logspace-ai/langflow_examples/main/examples"
|
||||
) ||
|
||||
config?.url?.includes(
|
||||
"https://api.github.com/repos/logspace-ai/langflow_examples/contents/examples"
|
||||
) ||
|
||||
config?.url?.includes(
|
||||
"https://api.github.com/repos/logspace-ai/langflow"
|
||||
) ||
|
||||
config?.url?.includes("auto_login")
|
||||
) {
|
||||
delete config.headers["Authorization"];
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
// Clean up the interceptor when the component unmounts
|
||||
// Clean up the interceptors when the component unmounts
|
||||
api.interceptors.response.eject(interceptor);
|
||||
api.interceptors.request.eject(requestInterceptor);
|
||||
};
|
||||
}, [retryCounts]);
|
||||
}, [accessToken, setErrorData]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
import { AxiosResponse } from "axios";
|
||||
import { ReactFlowJsonObject } from "reactflow";
|
||||
import { BASE_URL_API } from "../../constants/constants";
|
||||
import { api } from "../../controllers/API/api";
|
||||
import { APIObjectType, sendAllProps } from "../../types/api/index";
|
||||
import {
|
||||
APIObjectType,
|
||||
LoginType,
|
||||
Users,
|
||||
sendAllProps,
|
||||
} from "../../types/api/index";
|
||||
import { UserInputType } from "../../types/components";
|
||||
import { FlowStyleType, FlowType } from "../../types/flow";
|
||||
import {
|
||||
APIClassType,
|
||||
|
|
@ -346,3 +353,153 @@ export async function postCustomComponent(
|
|||
): Promise<AxiosResponse<APIClassType>> {
|
||||
return await api.post(`/api/v1/custom_component`, { code });
|
||||
}
|
||||
|
||||
export async function onLogin(user: LoginType) {
|
||||
try {
|
||||
const response = await api.post(
|
||||
`${BASE_URL_API}login`,
|
||||
new URLSearchParams({
|
||||
username: user.username,
|
||||
password: user.password,
|
||||
}).toString(),
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status === 200) {
|
||||
const data = response.data;
|
||||
return data;
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function autoLogin() {
|
||||
try {
|
||||
const response = await api.get(`${BASE_URL_API}auto_login`);
|
||||
|
||||
if (response.status === 200) {
|
||||
const data = response.data;
|
||||
return data;
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function renewAccessToken(token: string) {
|
||||
try {
|
||||
return await api.post(`${BASE_URL_API}refresh?token=${token}`);
|
||||
} catch (error) {
|
||||
console.log("Error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLoggedUser(): Promise<Users> {
|
||||
try {
|
||||
const res = await api.get(`${BASE_URL_API}user`);
|
||||
|
||||
if (res.status === 200) {
|
||||
return res.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function addUser(user: UserInputType): Promise<Users> {
|
||||
try {
|
||||
const res = await api.post(`${BASE_URL_API}user`, user);
|
||||
if (res.status === 200) {
|
||||
return res.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUsersPage(
|
||||
skip: number,
|
||||
limit: number
|
||||
): Promise<[Users]> {
|
||||
try {
|
||||
const res = await api.get(
|
||||
`${BASE_URL_API}users?skip=${skip}&limit=${limit}`
|
||||
);
|
||||
if (res.status === 200) {
|
||||
return res.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteUser(user_id: string) {
|
||||
try {
|
||||
const res = await api.delete(`${BASE_URL_API}user/${user_id}`);
|
||||
if (res.status === 200) {
|
||||
return res.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateUser(user_id: string, user: Users) {
|
||||
try {
|
||||
const res = await api.patch(`${BASE_URL_API}user/${user_id}`, user);
|
||||
if (res.status === 200) {
|
||||
return res.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getApiKey(user_id: String) {
|
||||
try {
|
||||
const res = await api.get(`${BASE_URL_API}api_key/${user_id}`);
|
||||
if (res.status === 200) {
|
||||
return res.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createApiKey(user_id: string) {
|
||||
try {
|
||||
const res = await api.post(`${BASE_URL_API}api_key/${user_id}`);
|
||||
if (res.status === 200) {
|
||||
return res.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function deleteApiKey(user_id: string) {
|
||||
try {
|
||||
const res = await api.delete(`${BASE_URL_API}api_key/${user_id}`);
|
||||
if (res.status === 200) {
|
||||
return res.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.0" id="katman_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 841.89 595.28" style="enable-background:new 0 0 841.89 595.28;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#505AA5;}
|
||||
</style>
|
||||
<path class="st0" d="M349.6,124.45c48.94-54.99,129.98-71.12,196.61-39.38c88.52,42.17,120.82,149.6,72.62,232.48L510.41,503.76
|
||||
c-6.06,10.41-16.03,18-27.72,21.11c-11.69,3.11-24.15,1.49-34.64-4.51l131.26-225.49c34.97-60.15,11.58-138.11-52.6-168.8
|
||||
c-48.16-23.03-107.02-11.53-142.6,28.08c-19.62,21.74-30.64,49.82-31.01,79.01c-0.37,29.2,9.94,57.53,29.01,79.76
|
||||
c3.43,3.99,7.12,7.75,11.04,11.25l-76.63,131.88c-3,5.16-6.99,9.67-11.74,13.3c-4.76,3.62-10.18,6.28-15.97,7.82
|
||||
c-5.79,1.54-11.83,1.93-17.77,1.16c-5.94-0.78-11.67-2.71-16.87-5.68l83.19-143.17c-11.95-17.11-20.53-36.31-25.29-56.58
|
||||
L261.1,360.8c-6.06,10.41-16.03,18-27.72,21.11c-11.69,3.11-24.15,1.49-34.64-4.51l131.83-226.76
|
||||
C336.06,141.32,342.43,132.55,349.6,124.45z M501.76,196.63c31.75,18.21,42.71,58.7,24.34,90.22L399.69,503.74
|
||||
c-6.06,10.41-16.03,18-27.72,21.11c-11.69,3.11-24.15,1.49-34.64-4.51l117.38-201.93c-9.42-1.97-18.29-5.94-26.01-11.65
|
||||
c-7.72-5.71-14.1-13.01-18.7-21.4c-4.6-8.4-7.31-17.68-7.95-27.22c-0.64-9.54,0.82-19.1,4.27-28.02c3.45-8.92,8.8-17,15.7-23.67
|
||||
c6.9-6.67,15.17-11.77,24.24-14.95c9.08-3.18,18.74-4.37,28.32-3.49S493.43,191.83,501.76,196.63z M455.78,237.39
|
||||
c-2.17,1.66-4,3.72-5.36,6.08h-0.01c-2.06,3.55-3.02,7.62-2.75,11.71c0.27,4.09,1.76,8,4.27,11.25c2.51,3.25,5.94,5.69,9.84,7
|
||||
c3.91,1.32,8.12,1.45,12.1,0.39c3.99-1.06,7.56-3.27,10.28-6.35c2.72-3.08,4.46-6.89,5-10.95c0.54-4.06-0.15-8.19-1.97-11.87
|
||||
c-1.82-3.67-4.71-6.73-8.28-8.78c-2.37-1.36-4.99-2.24-7.71-2.6c-2.72-0.35-5.48-0.18-8.12,0.53
|
||||
C460.43,234.52,457.95,235.73,455.78,237.39z"/>
|
||||
</svg>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.0" id="katman_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 841.89 595.28" style="enable-background:new 0 0 841.89 595.28;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#505AA5;}
|
||||
</style>
|
||||
<path class="st0" d="M349.6,124.45c48.94-54.99,129.98-71.12,196.61-39.38c88.52,42.17,120.82,149.6,72.62,232.48L510.41,503.76
|
||||
c-6.06,10.41-16.03,18-27.72,21.11c-11.69,3.11-24.15,1.49-34.64-4.51l131.26-225.49c34.97-60.15,11.58-138.11-52.6-168.8
|
||||
c-48.16-23.03-107.02-11.53-142.6,28.08c-19.62,21.74-30.64,49.82-31.01,79.01c-0.37,29.2,9.94,57.53,29.01,79.76
|
||||
c3.43,3.99,7.12,7.75,11.04,11.25l-76.63,131.88c-3,5.16-6.99,9.67-11.74,13.3c-4.76,3.62-10.18,6.28-15.97,7.82
|
||||
c-5.79,1.54-11.83,1.93-17.77,1.16c-5.94-0.78-11.67-2.71-16.87-5.68l83.19-143.17c-11.95-17.11-20.53-36.31-25.29-56.58
|
||||
L261.1,360.8c-6.06,10.41-16.03,18-27.72,21.11c-11.69,3.11-24.15,1.49-34.64-4.51l131.83-226.76
|
||||
C336.06,141.32,342.43,132.55,349.6,124.45z M501.76,196.63c31.75,18.21,42.71,58.7,24.34,90.22L399.69,503.74
|
||||
c-6.06,10.41-16.03,18-27.72,21.11c-11.69,3.11-24.15,1.49-34.64-4.51l117.38-201.93c-9.42-1.97-18.29-5.94-26.01-11.65
|
||||
c-7.72-5.71-14.1-13.01-18.7-21.4c-4.6-8.4-7.31-17.68-7.95-27.22c-0.64-9.54,0.82-19.1,4.27-28.02c3.45-8.92,8.8-17,15.7-23.67
|
||||
c6.9-6.67,15.17-11.77,24.24-14.95c9.08-3.18,18.74-4.37,28.32-3.49S493.43,191.83,501.76,196.63z M455.78,237.39
|
||||
c-2.17,1.66-4,3.72-5.36,6.08h-0.01c-2.06,3.55-3.02,7.62-2.75,11.71c0.27,4.09,1.76,8,4.27,11.25c2.51,3.25,5.94,5.69,9.84,7
|
||||
c3.91,1.32,8.12,1.45,12.1,0.39c3.99-1.06,7.56-3.27,10.28-6.35c2.72-3.08,4.46-6.89,5-10.95c0.54-4.06-0.15-8.19-1.97-11.87
|
||||
c-1.82-3.67-4.71-6.73-8.28-8.78c-2.37-1.36-4.99-2.24-7.71-2.6c-2.72-0.35-5.48-0.18-8.12,0.53
|
||||
C460.43,234.52,457.95,235.73,455.78,237.39z"/>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 1.9 KiB |
203
src/frontend/src/modals/SecretKeyModal/index.tsx
Normal file
203
src/frontend/src/modals/SecretKeyModal/index.tsx
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
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 { createApiKey } from "../../controllers/API";
|
||||
import {
|
||||
ApiKeyInputType,
|
||||
ApiKeyType,
|
||||
inputHandlerEventType,
|
||||
} from "../../types/components";
|
||||
import { nodeIconsLucide } from "../../utils/styleUtils";
|
||||
import BaseModal from "../baseModal";
|
||||
|
||||
export default function SecretKeyModal({
|
||||
title,
|
||||
cancelText,
|
||||
confirmationText,
|
||||
children,
|
||||
icon,
|
||||
data,
|
||||
onCloseModal
|
||||
}: ApiKeyType) {
|
||||
const Icon: any = nodeIconsLucide[icon];
|
||||
const [open, setOpen] = useState(false);
|
||||
const [apiKeyName, setApiKeyName] = useState(data?.apikeyname ?? "");
|
||||
const [apiKeyValue, setApiKeyValue] = useState("");
|
||||
const [inputState, setInputState] =
|
||||
useState<ApiKeyInputType>(CONTROL_NEW_API_KEY);
|
||||
const [renderKey, setRenderKey] = useState(false);
|
||||
const [textCopied, setTextCopied] = useState(true);
|
||||
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) {
|
||||
setRenderKey(false);
|
||||
resetForm();
|
||||
}
|
||||
else{
|
||||
onCloseModal();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
function resetForm() {
|
||||
setApiKeyName("");
|
||||
setApiKeyValue("");
|
||||
}
|
||||
|
||||
const handleCopyClick = async () => {
|
||||
if (apiKeyValue) {
|
||||
await navigator.clipboard.writeText(apiKeyValue);
|
||||
inputRef?.current?.focus();
|
||||
inputRef?.current?.select();
|
||||
setSuccessData({
|
||||
title: "API Key copied!",
|
||||
});
|
||||
setTextCopied(false);
|
||||
|
||||
setTimeout(() => {
|
||||
setTextCopied(true);
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
function handleAddNewKey() {
|
||||
createApiKey(data)
|
||||
.then((res) => {
|
||||
setApiKeyValue(res["api_key"]);
|
||||
})
|
||||
.catch((err) => {});
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseModal size="small-h-full" open={open} setOpen={setOpen}>
|
||||
<BaseModal.Trigger>{children}</BaseModal.Trigger>
|
||||
<BaseModal.Header description={""}>
|
||||
<span className="pr-2">{title}</span>
|
||||
<Icon
|
||||
name="icon"
|
||||
className="h-6 w-6 pl-1 text-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content>
|
||||
{renderKey === true && (
|
||||
<>
|
||||
<span className="text-xs">
|
||||
Please save this secret key somewhere safe and accessible. For
|
||||
security reasons,{" "}
|
||||
<strong>you won't be able to view it again</strong> through your
|
||||
account. If you lose this secret key, you'll need to generate a
|
||||
new one.
|
||||
</span>
|
||||
<div className="flex pt-3">
|
||||
<div className="w-full">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
onChange={(event) => {
|
||||
setApiKeyValue(event.target.value);
|
||||
}}
|
||||
readOnly={true}
|
||||
value={apiKeyValue}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
className="ml-3"
|
||||
onClick={() => {
|
||||
handleCopyClick();
|
||||
}}
|
||||
>
|
||||
{textCopied ? (
|
||||
<IconComponent name="Copy" className="h-4 w-4" />
|
||||
) : (
|
||||
<IconComponent name="Check" className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Form.Root
|
||||
onSubmit={(event) => {
|
||||
setRenderKey(true);
|
||||
handleAddNewKey();
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
{renderKey === false && (
|
||||
<div className="grid gap-5">
|
||||
<Form.Field name="username">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "baseline",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Form.Label className="data-[invalid]:label-invalid">
|
||||
Name (optional){" "}
|
||||
</Form.Label>
|
||||
</div>
|
||||
<Form.Control asChild>
|
||||
<input
|
||||
onChange={({ target: { value } }) => {
|
||||
handleInput({ target: { name: "apikeyname", value } });
|
||||
setApiKeyName(value);
|
||||
}}
|
||||
value={apiKeyName}
|
||||
className="primary-input"
|
||||
placeholder="My key name"
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Field>
|
||||
</div>
|
||||
)}
|
||||
{renderKey === false && (
|
||||
<div className="float-right">
|
||||
<Button
|
||||
className="mr-3"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{cancelText}
|
||||
</Button>
|
||||
|
||||
<Form.Submit asChild>
|
||||
<Button className="mt-8">{confirmationText}</Button>
|
||||
</Form.Submit>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{renderKey === true && (
|
||||
<div className="float-right">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
setRenderKey(false);
|
||||
}}
|
||||
className="mt-8"
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Form.Root>
|
||||
</BaseModal.Content>
|
||||
</BaseModal>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,8 +1,15 @@
|
|||
import * as Form from "@radix-ui/react-form";
|
||||
import { useEffect, useState } from "react";
|
||||
import InputComponent from "../../components/inputComponent";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { UserManagementType } from "../../types/components";
|
||||
import { Checkbox } from "../../components/ui/checkbox";
|
||||
import { CONTROL_NEW_USER } from "../../constants/constants";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
import {
|
||||
UserInputType,
|
||||
UserManagementType,
|
||||
inputHandlerEventType,
|
||||
} from "../../types/components";
|
||||
import { nodeIconsLucide } from "../../utils/styleUtils";
|
||||
import BaseModal from "../baseModal";
|
||||
|
||||
|
|
@ -18,18 +25,32 @@ export default function UserManagementModal({
|
|||
onConfirm,
|
||||
}: UserManagementType) {
|
||||
const Icon: any = nodeIconsLucide[icon];
|
||||
|
||||
const [pwdVisible, setPwdVisible] = useState(false);
|
||||
const [confirmPwdVisible, setConfirmPwdVisible] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const [password, setPassword] = useState(data?.password ?? "");
|
||||
const [username, setUserName] = useState(data?.user ?? "");
|
||||
const [username, setUserName] = useState(data?.username ?? "");
|
||||
const [confirmPassword, setConfirmPassword] = useState(data?.password ?? "");
|
||||
const [isActive, setIsActive] = useState(data?.is_active ?? false);
|
||||
const [isSuperUser, setIsSuperUser] = useState(data?.is_superuser ?? false);
|
||||
const [inputState, setInputState] = useState<UserInputType>(CONTROL_NEW_USER);
|
||||
const { userData } = useContext(AuthContext);
|
||||
|
||||
function handleInput({
|
||||
target: { name, value },
|
||||
}: inputHandlerEventType): void {
|
||||
setInputState((prev) => ({ ...prev, [name]: value }));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) {
|
||||
resetForm();
|
||||
} else {
|
||||
handleInput({ target: { name: "username", value: username } });
|
||||
handleInput({ target: { name: "is_active", value: isActive } });
|
||||
handleInput({ target: { name: "is_superuser", value: isSuperUser } });
|
||||
}
|
||||
}, [data, open]);
|
||||
}, [open]);
|
||||
|
||||
function resetForm() {
|
||||
setPassword("");
|
||||
|
|
@ -55,10 +76,8 @@ export default function UserManagementModal({
|
|||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
const data = Object.fromEntries(new FormData(event.currentTarget));
|
||||
resetForm();
|
||||
onConfirm(index ?? -1, data);
|
||||
onConfirm(1, inputState);
|
||||
setOpen(false);
|
||||
event.preventDefault();
|
||||
}}
|
||||
|
|
@ -79,8 +98,9 @@ export default function UserManagementModal({
|
|||
</div>
|
||||
<Form.Control asChild>
|
||||
<input
|
||||
onChange={(input) => {
|
||||
setUserName(input.target.value);
|
||||
onChange={({ target: { value } }) => {
|
||||
handleInput({ target: { name: "username", value } });
|
||||
setUserName(value);
|
||||
}}
|
||||
value={username}
|
||||
className="primary-input"
|
||||
|
|
@ -106,22 +126,40 @@ export default function UserManagementModal({
|
|||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Form.Label className="data-[invalid]:label-invalid">
|
||||
<Form.Label className="data-[invalid]:label-invalid flex">
|
||||
Password{" "}
|
||||
<span className="font-medium text-destructive">*</span>
|
||||
<span className="ml-1 mr-1 font-medium text-destructive">
|
||||
*
|
||||
</span>
|
||||
{pwdVisible && (
|
||||
<Eye
|
||||
onClick={() => setPwdVisible(!pwdVisible)}
|
||||
className="h-5 cursor-pointer"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
)}
|
||||
{!pwdVisible && (
|
||||
<EyeOff
|
||||
onClick={() => setPwdVisible(!pwdVisible)}
|
||||
className="h-5 cursor-pointer"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
)}
|
||||
</Form.Label>
|
||||
</div>
|
||||
<InputComponent
|
||||
onChange={(input) => {
|
||||
setPassword(input);
|
||||
}}
|
||||
value={password}
|
||||
password={true}
|
||||
isForm
|
||||
className="primary-input"
|
||||
required
|
||||
placeholder="Password"
|
||||
/>
|
||||
<Form.Control asChild>
|
||||
<input
|
||||
onChange={({ target: { value } }) => {
|
||||
handleInput({ target: { name: "password", value } });
|
||||
setPassword(value);
|
||||
}}
|
||||
value={password}
|
||||
className="primary-input"
|
||||
required={data ? false : true}
|
||||
type={pwdVisible ? "text" : "password"}
|
||||
/>
|
||||
</Form.Control>
|
||||
|
||||
<Form.Message className="field-invalid" match="valueMissing">
|
||||
Please enter a password
|
||||
</Form.Message>
|
||||
|
|
@ -146,93 +184,109 @@ export default function UserManagementModal({
|
|||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Form.Label className="data-[invalid]:label-invalid">
|
||||
<Form.Label className="data-[invalid]:label-invalid flex">
|
||||
Confirm password{" "}
|
||||
<span className="font-medium text-destructive">*</span>
|
||||
<span className="ml-1 mr-1 font-medium text-destructive">
|
||||
*
|
||||
</span>
|
||||
{confirmPwdVisible && (
|
||||
<Eye
|
||||
onClick={() =>
|
||||
setConfirmPwdVisible(!confirmPwdVisible)
|
||||
}
|
||||
className="h-5 cursor-pointer"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
)}
|
||||
{!confirmPwdVisible && (
|
||||
<EyeOff
|
||||
onClick={() =>
|
||||
setConfirmPwdVisible(!confirmPwdVisible)
|
||||
}
|
||||
className="h-5 cursor-pointer"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
)}
|
||||
</Form.Label>
|
||||
</div>
|
||||
<InputComponent
|
||||
onChange={(input) => {
|
||||
setConfirmPassword(input);
|
||||
}}
|
||||
value={confirmPassword}
|
||||
password={true}
|
||||
isForm
|
||||
className="primary-input"
|
||||
required
|
||||
placeholder="Confirm your password"
|
||||
/>
|
||||
<Form.Control asChild>
|
||||
<input
|
||||
onChange={(input) => {
|
||||
setConfirmPassword(input.target.value);
|
||||
}}
|
||||
value={confirmPassword}
|
||||
className="primary-input"
|
||||
required={data ? false : true}
|
||||
type={confirmPwdVisible ? "text" : "password"}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.Message className="field-invalid" match="valueMissing">
|
||||
Please confirm your password
|
||||
</Form.Message>
|
||||
</Form.Field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/*
|
||||
<Form.Field name="email">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "baseline",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Form.Label className="data-[invalid]:label-invalid">
|
||||
Email <span className="font-medium text-destructive">*</span>
|
||||
</Form.Label>
|
||||
<Form.Message className="field-invalid" match="valueMissing">
|
||||
Please enter your email
|
||||
</Form.Message>
|
||||
<Form.Message className="field-invalid" match="typeMismatch">
|
||||
Please provide a valid email
|
||||
</Form.Message>
|
||||
</div>
|
||||
<Form.Control asChild>
|
||||
<input className="primary-input" type="email" required />
|
||||
</Form.Control>
|
||||
</Form.Field> */}
|
||||
|
||||
{/*
|
||||
<Form.Field name="birth">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "baseline",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Form.Label className="data-[invalid]:label-invalid">
|
||||
Date of birth{" "}
|
||||
<span className="font-medium text-destructive">*</span>
|
||||
</Form.Label>
|
||||
<Form.Message className="field-invalid" match="valueMissing">
|
||||
Please enter your date of birth
|
||||
</Form.Message>
|
||||
</div>
|
||||
<Form.Control asChild>
|
||||
<input
|
||||
type="date"
|
||||
className="primary-input"
|
||||
required
|
||||
max={new Date().toISOString().split("T")[0]}
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Field> */}
|
||||
<div className="flex gap-8">
|
||||
<Form.Field name="is_active">
|
||||
<div>
|
||||
<Form.Label className="data-[invalid]:label-invalid mr-3">
|
||||
Active
|
||||
</Form.Label>
|
||||
<Form.Control asChild>
|
||||
<Checkbox
|
||||
value={isActive}
|
||||
checked={isActive}
|
||||
id="is_active"
|
||||
className="relative top-0.5"
|
||||
onCheckedChange={(value) => {
|
||||
handleInput({ target: { name: "is_active", value } });
|
||||
setIsActive(value);
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
</div>
|
||||
</Form.Field>
|
||||
{userData?.is_superuser && (
|
||||
<Form.Field name="is_superuser">
|
||||
<div>
|
||||
<Form.Label className="data-[invalid]:label-invalid mr-3">
|
||||
Superuser
|
||||
</Form.Label>
|
||||
<Form.Control asChild>
|
||||
<Checkbox
|
||||
checked={isSuperUser}
|
||||
value={isSuperUser}
|
||||
id="is_superuser"
|
||||
className="relative top-0.5"
|
||||
onCheckedChange={(value) => {
|
||||
handleInput({
|
||||
target: { name: "is_superuser", value },
|
||||
});
|
||||
setIsSuperUser(value);
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
</div>
|
||||
</Form.Field>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="float-right">
|
||||
<Form.Submit asChild>
|
||||
<Button className="mr-3 mt-8">{confirmationText}</Button>
|
||||
</Form.Submit>
|
||||
<Button
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
className="mr-3"
|
||||
>
|
||||
{cancelText}
|
||||
</Button>
|
||||
|
||||
<Form.Submit asChild>
|
||||
<Button className="mt-8">{confirmationText}</Button>
|
||||
</Form.Submit>
|
||||
|
||||
</div>
|
||||
</Form.Root>
|
||||
</BaseModal.Content>
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ export default function CodeAreaModal({
|
|||
const { dark } = useContext(darkContext);
|
||||
const { reactFlowInstance } = useContext(typesContext);
|
||||
const [height, setHeight] = useState<string | null>(null);
|
||||
const { setErrorData, setSuccessData } = useContext(alertContext);
|
||||
const { setErrorData, setSuccessData, isTweakPage } =
|
||||
useContext(alertContext);
|
||||
const [error, setError] = useState<{
|
||||
detail: { error: string | undefined; traceback: string | undefined };
|
||||
} | null>(null);
|
||||
|
|
@ -39,7 +40,7 @@ export default function CodeAreaModal({
|
|||
if (dynamic && Object.keys(nodeClass!.template).length > 2) {
|
||||
return;
|
||||
}
|
||||
processCode();
|
||||
if (!isTweakPage) processCode();
|
||||
}, []);
|
||||
|
||||
function processNonDynamicField() {
|
||||
|
|
|
|||
|
|
@ -12,15 +12,14 @@ export default function FlowSettingsModal({
|
|||
open,
|
||||
setOpen,
|
||||
}: FlowSettingsPropsType): JSX.Element {
|
||||
const { setSuccessData } = useContext(alertContext);
|
||||
const { flows, tabId, updateFlow, saveFlow } = useContext(TabsContext);
|
||||
const flow = flows.find((f) => f.id === tabId);
|
||||
useEffect(() => {
|
||||
setName(flow.name);
|
||||
setDescription(flow.description);
|
||||
}, [flow.name, flow.description]);
|
||||
const [name, setName] = useState(flow.name);
|
||||
const [description, setDescription] = useState(flow.description);
|
||||
setName(flow!.name);
|
||||
setDescription(flow!.description);
|
||||
}, [flow!.name, flow!.description]);
|
||||
const [name, setName] = useState(flow!.name);
|
||||
const [description, setDescription] = useState(flow!.description);
|
||||
const [invalidName, setInvalidName] = useState(false);
|
||||
|
||||
function handleClick(): void {
|
||||
|
|
@ -28,7 +27,6 @@ export default function FlowSettingsModal({
|
|||
savedFlow!.name = name;
|
||||
savedFlow!.description = description;
|
||||
saveFlow(savedFlow!);
|
||||
setSuccessData({ title: "Changes saved successfully" });
|
||||
setOpen(false);
|
||||
}
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,12 +1,62 @@
|
|||
import { useContext, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
import { Input } from "../../../components/ui/input";
|
||||
import { CONTROL_LOGIN_STATE } from "../../../constants/constants";
|
||||
import { alertContext } from "../../../contexts/alertContext";
|
||||
import { AuthContext } from "../../../contexts/authContext";
|
||||
import { getLoggedUser, onLogin } from "../../../controllers/API";
|
||||
import { LoginType } from "../../../types/api";
|
||||
import {
|
||||
inputHandlerEventType,
|
||||
loginInputStateType,
|
||||
} from "../../../types/components";
|
||||
|
||||
export default function LoginAdminPage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
function loginAdmin() {
|
||||
navigate("/admin/");
|
||||
const [inputState, setInputState] =
|
||||
useState<loginInputStateType>(CONTROL_LOGIN_STATE);
|
||||
const { login, getAuthentication, setUserData } = useContext(AuthContext);
|
||||
|
||||
const { password, username } = inputState;
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
|
||||
function handleInput({
|
||||
target: { name, value },
|
||||
}: inputHandlerEventType): void {
|
||||
setInputState((prev) => ({ ...prev, [name]: value }));
|
||||
}
|
||||
|
||||
function signIn() {
|
||||
const user: LoginType = {
|
||||
username: username,
|
||||
password: password,
|
||||
};
|
||||
onLogin(user)
|
||||
.then((user) => {
|
||||
login(user.access_token, user.refresh_token);
|
||||
getUser();
|
||||
navigate("/admin/");
|
||||
})
|
||||
.catch((error) => {
|
||||
setErrorData({
|
||||
title: "Error signing in",
|
||||
list: [error["response"]["data"]["detail"]],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getUser() {
|
||||
if (getAuthentication) {
|
||||
setTimeout(() => {
|
||||
getLoggedUser()
|
||||
.then((user) => {
|
||||
setUserData(user);
|
||||
})
|
||||
.catch((error) => {});
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -14,11 +64,24 @@ export default function LoginAdminPage() {
|
|||
<div className="flex w-72 flex-col items-center justify-center gap-2">
|
||||
<span className="mb-4 text-5xl">⛓️</span>
|
||||
<span className="mb-6 text-2xl font-semibold text-primary">Admin</span>
|
||||
<Input className="bg-background" placeholder="Email address" />
|
||||
<Input className="bg-background" placeholder="Password" />
|
||||
<Input
|
||||
onChange={({ target: { value } }) => {
|
||||
handleInput({ target: { name: "username", value } });
|
||||
}}
|
||||
className="bg-background"
|
||||
placeholder="Username"
|
||||
/>
|
||||
<Input
|
||||
type="password"
|
||||
onChange={({ target: { value } }) => {
|
||||
handleInput({ target: { name: "password", value } });
|
||||
}}
|
||||
className="bg-background"
|
||||
placeholder="Password"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
loginAdmin();
|
||||
signIn();
|
||||
}}
|
||||
variant="default"
|
||||
className="w-full"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import _ from "lodash";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { X } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import PaginatorComponent from "../../components/PaginatorComponent";
|
||||
import ShadTooltip from "../../components/ShadTooltipComponent";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Checkbox } from "../../components/ui/checkbox";
|
||||
import { Input } from "../../components/ui/input";
|
||||
import {
|
||||
Table,
|
||||
|
|
@ -14,265 +15,198 @@ import {
|
|||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../components/ui/table";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
import {
|
||||
addUser,
|
||||
deleteUser,
|
||||
getUsersPage,
|
||||
updateUser,
|
||||
} from "../../controllers/API";
|
||||
import ConfirmationModal from "../../modals/ConfirmationModal";
|
||||
import UserManagementModal from "../../modals/UserManagementModal";
|
||||
import { UserInputType } from "../../types/components";
|
||||
import Header from "../../components/headerComponent";
|
||||
|
||||
export default function AdminPage() {
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
|
||||
const [size, setPageSize] = useState(10);
|
||||
const [index, setPageIndex] = useState(1);
|
||||
const [index, setPageIndex] = useState(0);
|
||||
const [loadingUsers, setLoadingUsers] = useState(true);
|
||||
const { setErrorData, setSuccessData } = useContext(alertContext);
|
||||
const { userData } = useContext(AuthContext);
|
||||
const [totalRowsCount, setTotalRowsCount] = useState(0);
|
||||
|
||||
const userList = useRef([
|
||||
{
|
||||
user: generateRandomString(50),
|
||||
email: generateRandomString(50) + "@example.com",
|
||||
password: generateRandomString(50),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
]);
|
||||
const userList = useRef([]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
getUsers();
|
||||
}, 500);
|
||||
}, []);
|
||||
|
||||
const [filterUserList, setFilterUserList] = useState(userList.current);
|
||||
|
||||
function generateRandomString(length) {
|
||||
const characters =
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
let result = "";
|
||||
for (let i = 0; i < length; i++) {
|
||||
const randomIndex = Math.floor(Math.random() * characters.length);
|
||||
result += characters.charAt(randomIndex);
|
||||
}
|
||||
return result;
|
||||
function getUsers() {
|
||||
setLoadingUsers(true);
|
||||
getUsersPage(index, size)
|
||||
.then((users) => {
|
||||
setTotalRowsCount(users["total_count"]);
|
||||
userList.current = users["users"];
|
||||
setFilterUserList(users["users"]);
|
||||
setLoadingUsers(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
setLoadingUsers(false);
|
||||
});
|
||||
}
|
||||
|
||||
function generateRandomDate() {
|
||||
const start = new Date(2010, 0, 1);
|
||||
const end = new Date();
|
||||
const randomTimestamp =
|
||||
start.getTime() + Math.random() * (end.getTime() - start.getTime());
|
||||
const randomDate = new Date(randomTimestamp);
|
||||
|
||||
const options = { year: "numeric", month: "short", day: "numeric" };
|
||||
return randomDate.toLocaleDateString("en-US");
|
||||
}
|
||||
|
||||
const [editUser, setEditUser] = useState(-1);
|
||||
const [editedUser, setEditedUser] = useState("");
|
||||
const [modalEditOpen, setModalEditOpen] = useState(false);
|
||||
const [modalDeleteOpen, setModalDeleteOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
resetFilter();
|
||||
}, []);
|
||||
|
||||
const handleInputChange = (event, index) => {
|
||||
const user = _.cloneDeepWith(userList.current);
|
||||
user[index].password = event.target.value;
|
||||
userList.current = user;
|
||||
|
||||
const userFilter = _.cloneDeepWith(filterUserList);
|
||||
userFilter[index].password = event.target.value;
|
||||
setFilterUserList(userFilter);
|
||||
|
||||
setEditedUser(event.target.value);
|
||||
};
|
||||
|
||||
function handleChangePagination(pageIndex: number, pageSize: number) {
|
||||
setPageIndex(pageIndex);
|
||||
setPageSize(pageSize);
|
||||
|
||||
const startIndex = (pageIndex - 1) * pageSize;
|
||||
const endIndex = startIndex + pageSize;
|
||||
const newList = userList.current.slice(startIndex, endIndex);
|
||||
|
||||
setFilterUserList(newList);
|
||||
setLoadingUsers(true);
|
||||
getUsersPage(pageIndex, pageSize)
|
||||
.then((users) => {
|
||||
setTotalRowsCount(users["total_count"]);
|
||||
userList.current = users["users"];
|
||||
setFilterUserList(users["users"]);
|
||||
setLoadingUsers(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
setLoadingUsers(false);
|
||||
});
|
||||
}
|
||||
|
||||
function resetFilter() {
|
||||
setFilterUserList(userList.current);
|
||||
setPageIndex(1);
|
||||
setPageIndex(0);
|
||||
setPageSize(10);
|
||||
|
||||
const startIndex = (index - 1) * size;
|
||||
const endIndex = index + size - 1;
|
||||
const newList = userList.current.slice(startIndex, endIndex);
|
||||
|
||||
console.log(userList.current);
|
||||
|
||||
setFilterUserList(newList);
|
||||
getUsers();
|
||||
}
|
||||
|
||||
function handleFilterUsers(input: string) {
|
||||
setInputValue(input);
|
||||
|
||||
if (input === "") {
|
||||
resetFilter();
|
||||
setFilterUserList(userList.current);
|
||||
} else {
|
||||
const filteredList = userList.current.filter(
|
||||
(user) =>
|
||||
user.user.toLowerCase().includes(input.toLowerCase()) ||
|
||||
user.email.toLowerCase().includes(input.toLowerCase())
|
||||
const filteredList = userList.current.filter((user) =>
|
||||
user.username.toLowerCase().includes(input.toLowerCase())
|
||||
);
|
||||
setFilterUserList(filteredList);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeleteUser(index) {
|
||||
const user = _.cloneDeepWith(userList.current);
|
||||
user.splice(index, 1);
|
||||
userList.current = user;
|
||||
|
||||
const userFilter = _.cloneDeepWith(filterUserList);
|
||||
userFilter.splice(index, 1);
|
||||
setFilterUserList(userFilter);
|
||||
|
||||
resetFilter();
|
||||
function handleDeleteUser(user) {
|
||||
deleteUser(user.id)
|
||||
.then((res) => {
|
||||
resetFilter();
|
||||
setSuccessData({
|
||||
title: "Success! User deleted!",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
setErrorData({
|
||||
title: "Error on delete user",
|
||||
list: [error["response"]["data"]["detail"]],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function handleEditUser(index, user) {
|
||||
const newUser = _.cloneDeepWith(userList.current);
|
||||
newUser[index].password = user.password;
|
||||
newUser[index].user = user.username;
|
||||
userList.current = newUser;
|
||||
resetFilter();
|
||||
function handleEditUser(userId, user) {
|
||||
updateUser(userId, user)
|
||||
.then((res) => {
|
||||
resetFilter();
|
||||
setSuccessData({
|
||||
title: "Success! User edited!",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
setErrorData({
|
||||
title: "Error on edit user",
|
||||
list: [error["response"]["data"]["detail"]],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function handleNewUser(user) {
|
||||
const newUser = {
|
||||
user: user.username,
|
||||
email: generateRandomString(50) + "@example.com",
|
||||
password: user.password,
|
||||
register_date: generateRandomDate(),
|
||||
};
|
||||
function handleDisableUser(check, userId, user) {
|
||||
const userEdit = cloneDeep(user);
|
||||
userEdit.is_active = !check;
|
||||
|
||||
userList.current.unshift(newUser);
|
||||
console.log(userList.current);
|
||||
updateUser(userId, userEdit)
|
||||
.then((res) => {
|
||||
resetFilter();
|
||||
setSuccessData({
|
||||
title: "Success! User edited!",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
setErrorData({
|
||||
title: "Error on edit user",
|
||||
list: [error["response"]["data"]["detail"]],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
resetFilter();
|
||||
function handleSuperUserEdit(check, userId, user) {
|
||||
const userEdit = cloneDeep(user);
|
||||
userEdit.is_superuser = !check;
|
||||
updateUser(userId, userEdit)
|
||||
.then((res) => {
|
||||
resetFilter();
|
||||
setSuccessData({
|
||||
title: "Success! User edited!",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
setErrorData({
|
||||
title: "Error on edit user",
|
||||
list: [error["response"]["data"]["detail"]],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function handleNewUser(user: UserInputType) {
|
||||
addUser(user)
|
||||
.then((res) => {
|
||||
resetFilter();
|
||||
setSuccessData({
|
||||
title: "Success! New user added!",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
setErrorData({
|
||||
title: "Error on add new user",
|
||||
list: [error["response"]["data"]["detail"]],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="main-page-panel">
|
||||
<div className="m-auto flex h-full flex-row justify-center">
|
||||
<div className="basis-5/6">
|
||||
<div className="m-auto flex h-full flex-col space-y-8 p-8 ">
|
||||
<div className="flex items-center justify-between space-y-2">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold tracking-tight">
|
||||
Welcome back!
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Here's a list of all users!
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2"></div>
|
||||
</div>
|
||||
|
||||
{userList.current.length === 0 && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h2>There's no users left :)</h2>
|
||||
<Header />
|
||||
{userData && (
|
||||
<div className="main-page-panel">
|
||||
<div className="m-auto flex h-full flex-row justify-center">
|
||||
<div className="basis-5/6">
|
||||
<div className="m-auto flex h-full flex-col space-y-8 p-8 ">
|
||||
<div className="flex items-center justify-between space-y-2">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold tracking-tight">
|
||||
Welcome back!
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Here's a list of all users!
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{userList.current.length > 0 && (
|
||||
<div className="flex items-center space-x-2"></div>
|
||||
</div>
|
||||
|
||||
{userList.current.length === 0 && !loadingUsers && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h2>There's no users registered :)</h2>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-1 items-center space-x-2">
|
||||
|
|
@ -285,8 +219,8 @@ export default function AdminPage() {
|
|||
{inputValue.length > 0 && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
resetFilter();
|
||||
setInputValue("");
|
||||
setFilterUserList(userList.current);
|
||||
}}
|
||||
variant="ghost"
|
||||
className="h-8 px-2 lg:px-3"
|
||||
|
|
@ -311,91 +245,183 @@ export default function AdminPage() {
|
|||
</UserManagementModal>
|
||||
</div>
|
||||
</div>
|
||||
{loadingUsers && (
|
||||
<div>
|
||||
<strong>Loading...</strong>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="overflow-scroll overflow-x-hidden rounded-md border-2 bg-muted
|
||||
custom-scroll"
|
||||
className={
|
||||
"max-h-[26rem] min-h-[26rem] overflow-scroll overflow-x-hidden rounded-md border-2 bg-muted custom-scroll" +
|
||||
(loadingUsers ? " border-0" : "")
|
||||
}
|
||||
>
|
||||
<Table className="table-fixed bg-muted outline-1 ">
|
||||
<TableHeader>
|
||||
<Table className={"table-fixed bg-muted outline-1"}>
|
||||
<TableHeader
|
||||
className={
|
||||
loadingUsers
|
||||
? "hidden"
|
||||
: "table-fixed bg-muted outline-1"
|
||||
}
|
||||
>
|
||||
<TableRow>
|
||||
<TableHead className="h-10">User</TableHead>
|
||||
<TableHead className="h-10">Password</TableHead>
|
||||
<TableHead className="h-10">Id</TableHead>
|
||||
<TableHead className="h-10">Username</TableHead>
|
||||
<TableHead className="h-10">Active</TableHead>
|
||||
<TableHead className="h-10">Superuser</TableHead>
|
||||
<TableHead className="h-10">Created At</TableHead>
|
||||
<TableHead className="h-10">Updated At</TableHead>
|
||||
<TableHead className="h-10 w-[100px] text-right"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filterUserList.map((user, index) => (
|
||||
<TableRow key={user.user}>
|
||||
<TableCell className="truncate py-2 font-medium">
|
||||
{user.user}
|
||||
</TableCell>
|
||||
<TableCell className="truncate py-2">
|
||||
{user.password}
|
||||
</TableCell>
|
||||
<TableCell className="flex w-[100px] py-2 text-right">
|
||||
<div className="flex">
|
||||
<UserManagementModal
|
||||
title="Edit"
|
||||
titleHeader={`${user.user}`}
|
||||
cancelText="Cancel"
|
||||
confirmationText="Edit"
|
||||
icon={"UserPlus2"}
|
||||
data={user}
|
||||
index={index}
|
||||
onConfirm={(index, user) => {
|
||||
handleEditUser(index, user);
|
||||
}}
|
||||
>
|
||||
<ShadTooltip content="Edit" side="top">
|
||||
<IconComponent
|
||||
name="Pencil"
|
||||
className="h-4 w-4 cursor-pointer"
|
||||
/>
|
||||
</ShadTooltip>
|
||||
</UserManagementModal>
|
||||
|
||||
{!loadingUsers && (
|
||||
<TableBody>
|
||||
{filterUserList.map((user, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell className="truncate py-2 font-medium">
|
||||
<ShadTooltip content={user.id}>
|
||||
<span className="cursor-default">
|
||||
{user.id}
|
||||
</span>
|
||||
</ShadTooltip>
|
||||
</TableCell>
|
||||
<TableCell className="truncate py-2">
|
||||
<ShadTooltip content={user.username}>
|
||||
<span className="cursor-default">
|
||||
{user.username}
|
||||
</span>
|
||||
</ShadTooltip>
|
||||
</TableCell>
|
||||
<TableCell className="relative left-5 truncate py-2 text-align-last-left">
|
||||
<ConfirmationModal
|
||||
title="Delete"
|
||||
titleHeader="Delete User"
|
||||
title="Edit"
|
||||
titleHeader={`${user.username}`}
|
||||
modalContentTitle="Attention!"
|
||||
modalContent="Are you sure you want to delete this user? This action cannot be undone."
|
||||
modalContent="Are you completely confident about the changes you are making to this user?"
|
||||
cancelText="Cancel"
|
||||
confirmationText="Delete"
|
||||
icon={"UserMinus2"}
|
||||
confirmationText="Confirm"
|
||||
icon={"UserCog2"}
|
||||
data={user}
|
||||
index={index}
|
||||
onConfirm={(index, user) => {
|
||||
handleDeleteUser(index);
|
||||
handleDisableUser(
|
||||
user.is_active,
|
||||
user.id,
|
||||
user
|
||||
);
|
||||
}}
|
||||
>
|
||||
<ShadTooltip content="Delete" side="top">
|
||||
<IconComponent
|
||||
name="Trash2"
|
||||
className="ml-2 h-4 w-4 cursor-pointer"
|
||||
/>
|
||||
</ShadTooltip>
|
||||
<Checkbox
|
||||
id="is_active"
|
||||
checked={user.is_active}
|
||||
/>
|
||||
</ConfirmationModal>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</TableCell>
|
||||
<TableCell className="relative left-5 truncate py-2 text-align-last-left">
|
||||
<ConfirmationModal
|
||||
title="Edit"
|
||||
titleHeader={`${user.username}`}
|
||||
modalContentTitle="Attention!"
|
||||
modalContent="Are you completely confident about the changes you are making to this user?"
|
||||
cancelText="Cancel"
|
||||
confirmationText="Confirm"
|
||||
icon={"UserCog2"}
|
||||
data={user}
|
||||
index={index}
|
||||
onConfirm={(index, user) => {
|
||||
handleSuperUserEdit(
|
||||
user.is_superuser,
|
||||
user.id,
|
||||
user
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
id="is_superuser"
|
||||
checked={user.is_superuser}
|
||||
/>
|
||||
</ConfirmationModal>
|
||||
</TableCell>
|
||||
<TableCell className="truncate py-2 ">
|
||||
{
|
||||
new Date(user.create_at)
|
||||
.toISOString()
|
||||
.split("T")[0]
|
||||
}
|
||||
</TableCell>
|
||||
<TableCell className="truncate py-2">
|
||||
{
|
||||
new Date(user.updated_at)
|
||||
.toISOString()
|
||||
.split("T")[0]
|
||||
}
|
||||
</TableCell>
|
||||
<TableCell className="flex w-[100px] py-2 text-right">
|
||||
<div className="flex">
|
||||
<UserManagementModal
|
||||
title="Edit"
|
||||
titleHeader={`${user.id}`}
|
||||
cancelText="Cancel"
|
||||
confirmationText="Save"
|
||||
icon={"UserPlus2"}
|
||||
data={user}
|
||||
index={index}
|
||||
onConfirm={(index, editUser) => {
|
||||
handleEditUser(user.id, editUser);
|
||||
}}
|
||||
>
|
||||
<ShadTooltip content="Edit" side="top">
|
||||
<IconComponent
|
||||
name="Pencil"
|
||||
className="h-4 w-4 cursor-pointer"
|
||||
/>
|
||||
</ShadTooltip>
|
||||
</UserManagementModal>
|
||||
|
||||
<ConfirmationModal
|
||||
title="Delete"
|
||||
titleHeader="Delete User"
|
||||
modalContentTitle="Attention!"
|
||||
modalContent="Are you sure you want to delete this user? This action cannot be undone."
|
||||
cancelText="Cancel"
|
||||
confirmationText="Delete"
|
||||
icon={"UserMinus2"}
|
||||
data={user}
|
||||
index={index}
|
||||
onConfirm={(index, user) => {
|
||||
handleDeleteUser(user);
|
||||
}}
|
||||
>
|
||||
<ShadTooltip content="Delete" side="top">
|
||||
<IconComponent
|
||||
name="Trash2"
|
||||
className="ml-2 h-4 w-4 cursor-pointer"
|
||||
/>
|
||||
</ShadTooltip>
|
||||
</ConfirmationModal>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
)}
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<PaginatorComponent
|
||||
pageIndex={index}
|
||||
pageSize={size}
|
||||
totalRowsCount={filterUserList.length}
|
||||
totalRowsCount={totalRowsCount}
|
||||
paginate={(pageIndex, pageSize) => {
|
||||
handleChangePagination(pageSize, pageIndex);
|
||||
}}
|
||||
></PaginatorComponent>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
246
src/frontend/src/pages/ApiKeysPage/index.tsx
Normal file
246
src/frontend/src/pages/ApiKeysPage/index.tsx
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import ShadTooltip from "../../components/ShadTooltipComponent";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../components/ui/table";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
import { deleteApiKey, getApiKey } from "../../controllers/API";
|
||||
import ConfirmationModal from "../../modals/ConfirmationModal";
|
||||
import SecretKeyModal from "../../modals/SecretKeyModal";
|
||||
|
||||
import moment from "moment";
|
||||
import { ApiKey } from "../../types/components";
|
||||
import Header from "../../components/headerComponent";
|
||||
|
||||
export default function ApiKeysPage() {
|
||||
const [loadingKeys, setLoadingKeys] = useState(true);
|
||||
const { setErrorData, setSuccessData } = useContext(alertContext);
|
||||
const { userData } = useContext(AuthContext);
|
||||
const [userId, setUserId] = useState("");
|
||||
const keysList = useRef([]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
getKeys();
|
||||
}, 500);
|
||||
}, [userData]);
|
||||
|
||||
function getKeys() {
|
||||
setLoadingKeys(true);
|
||||
if (userData) {
|
||||
getApiKey(userData.id)
|
||||
.then((keys: [ApiKey]) => {
|
||||
keysList.current = keys["api_keys"];
|
||||
setUserId(keys["user_id"]);
|
||||
setLoadingKeys(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
setLoadingKeys(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function resetFilter() {
|
||||
getKeys();
|
||||
}
|
||||
|
||||
function handleDeleteKey(keys) {
|
||||
deleteApiKey(keys)
|
||||
.then((res) => {
|
||||
resetFilter();
|
||||
setSuccessData({
|
||||
title: "Success! Key deleted!",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
setErrorData({
|
||||
title: "Error on delete key",
|
||||
list: [error["response"]["data"]["detail"]],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function lastUsedMessage() {
|
||||
return (
|
||||
<div className="text-xs">
|
||||
<span>
|
||||
The last time this key was used.<br></br> Accurate to within the hour
|
||||
from the most recent usage.
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header></Header>
|
||||
{userData && (
|
||||
<div className="main-page-panel">
|
||||
<div className="m-auto flex h-full flex-row justify-center">
|
||||
<div className="basis-5/6">
|
||||
<div className="m-auto flex h-full flex-col space-y-8 p-8 ">
|
||||
<div className="flex items-center justify-between space-y-2">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold tracking-tight">
|
||||
API keys
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Your secret API keys are listed below. Please note that we
|
||||
do not display your secret API keys again after you
|
||||
generate them.<br></br>
|
||||
Do not share your API key with others, or expose it in the
|
||||
browser or other client-side code.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2"></div>
|
||||
</div>
|
||||
|
||||
{keysList.current.length === 0 && !loadingKeys && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h2>There's no users registered :)</h2>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
{loadingKeys && (
|
||||
<div>
|
||||
<strong>Loading...</strong>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={
|
||||
"max-h-[15rem] overflow-scroll overflow-x-hidden rounded-md border-2 bg-muted custom-scroll" +
|
||||
(loadingKeys ? " border-0" : "")
|
||||
}
|
||||
>
|
||||
<Table className={"table-fixed bg-muted outline-1"}>
|
||||
<TableHeader
|
||||
className={
|
||||
loadingKeys
|
||||
? "hidden"
|
||||
: "table-fixed bg-muted outline-1"
|
||||
}
|
||||
>
|
||||
<TableRow>
|
||||
<TableHead className="h-10">Id</TableHead>
|
||||
<TableHead className="h-10">Name</TableHead>
|
||||
<TableHead className="h-10">Key</TableHead>
|
||||
<TableHead className="h-10">Created</TableHead>
|
||||
<TableHead className="flex h-10 items-center">
|
||||
Last Used
|
||||
<ShadTooltip side="top" content={lastUsedMessage()}>
|
||||
<div>
|
||||
<IconComponent
|
||||
name="Info"
|
||||
className="ml-1 h-3 w-3"
|
||||
/>
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
</TableHead>
|
||||
<TableHead className="h-10 w-[100px] text-right"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
{!loadingKeys && (
|
||||
<TableBody>
|
||||
{keysList.current.map(
|
||||
(api_keys: ApiKey, index: number) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell className="truncate py-2">
|
||||
<ShadTooltip content={api_keys.id}>
|
||||
<span className="cursor-default">
|
||||
{api_keys.id}
|
||||
</span>
|
||||
</ShadTooltip>
|
||||
</TableCell>
|
||||
<TableCell className="truncate py-2">
|
||||
<ShadTooltip content={api_keys.name}>
|
||||
<span className="cursor-default">
|
||||
{api_keys.name}
|
||||
</span>
|
||||
</ShadTooltip>
|
||||
</TableCell>
|
||||
<TableCell className="truncate py-2">
|
||||
<span className="cursor-default">
|
||||
{api_keys.api_key}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="truncate py-2 ">
|
||||
{moment(api_keys.created_at).format(
|
||||
"YYYY-MM-DD HH:mm"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="truncate py-2">
|
||||
{moment(api_keys.last_used_at).format(
|
||||
"YYYY-MM-DD HH:mm"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="flex w-[100px] py-2 text-right">
|
||||
<div className="flex">
|
||||
<ConfirmationModal
|
||||
title="Delete"
|
||||
titleHeader="Delete User"
|
||||
modalContentTitle="Attention!"
|
||||
modalContent="Are you sure you want to delete this key? This action cannot be undone."
|
||||
cancelText="Cancel"
|
||||
confirmationText="Delete"
|
||||
icon={"UserMinus2"}
|
||||
data={api_keys.id}
|
||||
index={index}
|
||||
onConfirm={(index, keys) => {
|
||||
handleDeleteKey(keys);
|
||||
}}
|
||||
>
|
||||
<ShadTooltip content="Delete" side="top">
|
||||
<IconComponent
|
||||
name="Trash2"
|
||||
className="ml-2 h-4 w-4 cursor-pointer"
|
||||
/>
|
||||
</ShadTooltip>
|
||||
</ConfirmationModal>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
)}
|
||||
</TableBody>
|
||||
)}
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<SecretKeyModal
|
||||
title="Create new secret key"
|
||||
cancelText="Cancel"
|
||||
confirmationText="Create secret key"
|
||||
icon={"Key"}
|
||||
data={userId}
|
||||
onCloseModal={() => {
|
||||
getKeys();
|
||||
}}
|
||||
>
|
||||
<Button>
|
||||
<IconComponent name="Plus" className="mr-1 h-5 w-5" />
|
||||
Create new secret key
|
||||
</Button>
|
||||
</SecretKeyModal>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -21,7 +21,8 @@ export default function ExtraSidebar(): JSX.Element {
|
|||
const { data, templates } = useContext(typesContext);
|
||||
const { flows, tabId, uploadFlow, tabsState, saveFlow, isBuilt } =
|
||||
useContext(TabsContext);
|
||||
const { setSuccessData, setErrorData } = useContext(alertContext);
|
||||
const { setSuccessData, setErrorData, setIsTweakPage } =
|
||||
useContext(alertContext);
|
||||
const [dataFilter, setFilterData] = useState(data);
|
||||
const [search, setSearch] = useState("");
|
||||
const isPending = tabsState[tabId]?.isPending;
|
||||
|
|
@ -100,7 +101,10 @@ export default function ExtraSidebar(): JSX.Element {
|
|||
<div className="side-bar-button">
|
||||
{flow && flow.data && (
|
||||
<ApiModal flow={flow} disable={!isBuilt}>
|
||||
<div className={classNames("extra-side-bar-buttons")}>
|
||||
<div
|
||||
className={classNames("extra-side-bar-buttons")}
|
||||
onClick={() => setIsTweakPage(true)}
|
||||
>
|
||||
<IconComponent
|
||||
name="Code2"
|
||||
className={
|
||||
|
|
@ -121,7 +125,6 @@ export default function ExtraSidebar(): JSX.Element {
|
|||
}
|
||||
onClick={(event) => {
|
||||
saveFlow(flow!);
|
||||
setSuccessData({ title: "Changes saved successfully" });
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
import * as Form from "@radix-ui/react-form";
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useContext, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import InputComponent from "../../components/inputComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Input } from "../../components/ui/input";
|
||||
import { CONTROL_LOGIN_STATE } from "../../constants/constants";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
import { getLoggedUser, onLogin } from "../../controllers/API";
|
||||
import { LoginType } from "../../types/api";
|
||||
import {
|
||||
inputHandlerEventType,
|
||||
loginInputStateType,
|
||||
|
|
@ -15,12 +19,47 @@ export default function LoginPage(): JSX.Element {
|
|||
useState<loginInputStateType>(CONTROL_LOGIN_STATE);
|
||||
|
||||
const { password, username } = inputState;
|
||||
const { login, getAuthentication, setUserData } = useContext(AuthContext);
|
||||
const navigate = useNavigate();
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
|
||||
function handleInput({
|
||||
target: { name, value },
|
||||
}: inputHandlerEventType): void {
|
||||
setInputState((prev) => ({ ...prev, [name]: value }));
|
||||
}
|
||||
|
||||
function signIn() {
|
||||
const user: LoginType = {
|
||||
username: username,
|
||||
password: password,
|
||||
};
|
||||
onLogin(user)
|
||||
.then((user) => {
|
||||
login(user.access_token, user.refresh_token);
|
||||
getUser();
|
||||
navigate("/");
|
||||
})
|
||||
.catch((error) => {
|
||||
setErrorData({
|
||||
title: "Error signing in",
|
||||
list: [error["response"]["data"]["detail"]],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getUser() {
|
||||
if (getAuthentication()) {
|
||||
setTimeout(() => {
|
||||
getLoggedUser()
|
||||
.then((user) => {
|
||||
setUserData(user);
|
||||
})
|
||||
.catch((error) => {});
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Root
|
||||
onSubmit={(event) => {
|
||||
|
|
@ -28,7 +67,7 @@ export default function LoginPage(): JSX.Element {
|
|||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
signIn();
|
||||
const data = Object.fromEntries(new FormData(event.currentTarget));
|
||||
event.preventDefault();
|
||||
}}
|
||||
|
|
@ -92,7 +131,7 @@ export default function LoginPage(): JSX.Element {
|
|||
</Form.Submit>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Link to="">
|
||||
<Link to="/signup">
|
||||
<Button className="w-full" variant="outline">
|
||||
Don't have an account? <b>Sign Up</b>
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
import * as Form from "@radix-ui/react-form";
|
||||
import { FormEvent, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { FormEvent, useContext, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import InputComponent from "../../components/inputComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Input } from "../../components/ui/input";
|
||||
import { CONTROL_INPUT_STATE } from "../../constants/constants";
|
||||
import {
|
||||
CONTROL_INPUT_STATE,
|
||||
SIGN_UP_SUCCESS,
|
||||
} from "../../constants/constants";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { addUser } from "../../controllers/API";
|
||||
import {
|
||||
UserInputType,
|
||||
inputHandlerEventType,
|
||||
signUpInputStateType,
|
||||
} from "../../types/components";
|
||||
|
|
@ -15,12 +21,42 @@ export default function SignUp(): JSX.Element {
|
|||
useState<signUpInputStateType>(CONTROL_INPUT_STATE);
|
||||
|
||||
const { password, cnfPassword, username } = inputState;
|
||||
const { setErrorData, setSuccessData } = useContext(alertContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
function handleInput({
|
||||
target: { name, value },
|
||||
}: inputHandlerEventType): void {
|
||||
setInputState((prev) => ({ ...prev, [name]: value }));
|
||||
}
|
||||
|
||||
function handleSignup(): void {
|
||||
const { username, password } = inputState;
|
||||
const newUser: UserInputType = {
|
||||
username,
|
||||
password,
|
||||
};
|
||||
addUser(newUser)
|
||||
.then((user) => {
|
||||
setSuccessData({
|
||||
title: SIGN_UP_SUCCESS,
|
||||
});
|
||||
navigate("/login");
|
||||
})
|
||||
.catch((error) => {
|
||||
const {
|
||||
response: {
|
||||
data: { detail },
|
||||
},
|
||||
} = error;
|
||||
setErrorData({
|
||||
title: "Error signing up",
|
||||
list: [detail[0].msg],
|
||||
});
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Root
|
||||
onSubmit={(event: FormEvent<HTMLFormElement>) => {
|
||||
|
|
@ -120,7 +156,14 @@ export default function SignUp(): JSX.Element {
|
|||
</div>
|
||||
<div className="w-full">
|
||||
<Form.Submit asChild>
|
||||
<Button className="mr-3 mt-6 w-full">Sign up</Button>
|
||||
<Button
|
||||
className="mr-3 mt-6 w-full"
|
||||
onClick={() => {
|
||||
handleSignup();
|
||||
}}
|
||||
>
|
||||
Sign up
|
||||
</Button>
|
||||
</Form.Submit>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
|
|
|
|||
|
|
@ -1,32 +1,116 @@
|
|||
import { Route, Routes } from "react-router-dom";
|
||||
import { ProtectedAdminRoute } from "./components/authAdminGuard";
|
||||
import { ProtectedRoute } from "./components/authGuard";
|
||||
import { ProtectedLoginRoute } from "./components/authLoginGuard";
|
||||
import { CatchAllRoute } from "./components/catchAllRoutes";
|
||||
import AdminPage from "./pages/AdminPage";
|
||||
import LoginAdminPage from "./pages/AdminPage/LoginPage";
|
||||
import ApiKeysPage from "./pages/ApiKeysPage";
|
||||
import CommunityPage from "./pages/CommunityPage";
|
||||
import FlowPage from "./pages/FlowPage";
|
||||
import HomePage from "./pages/MainPage";
|
||||
import ViewPage from "./pages/ViewPage";
|
||||
import DeleteAccountPage from "./pages/deleteAccountPage";
|
||||
import LoginPage from "./pages/loginPage";
|
||||
import SignUp from "./pages/signUpPage";
|
||||
|
||||
const Router = () => {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/community" element={<CommunityPage />} />
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<HomePage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/community"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<CommunityPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route path="/flow/:id/">
|
||||
<Route path="" element={<FlowPage />} />
|
||||
<Route path="view" element={<ViewPage />} />
|
||||
<Route
|
||||
path=""
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<FlowPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="view"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<ViewPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="*" element={<HomePage />} />
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<CatchAllRoute />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
{/* <Route path="/signup" element={<SignUp />} /> */}
|
||||
<Route path="/login/admin" element={<LoginAdminPage />} />
|
||||
<Route
|
||||
path="/login"
|
||||
element={
|
||||
<ProtectedLoginRoute>
|
||||
<LoginPage />
|
||||
</ProtectedLoginRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/signup"
|
||||
element={
|
||||
<ProtectedLoginRoute>
|
||||
<SignUp />
|
||||
</ProtectedLoginRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/login/admin"
|
||||
element={
|
||||
<ProtectedLoginRoute>
|
||||
<LoginAdminPage />
|
||||
</ProtectedLoginRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path="/admin" element={<AdminPage />} />
|
||||
<Route
|
||||
path="/admin"
|
||||
element={
|
||||
|
||||
<AdminPage />
|
||||
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path="/account">
|
||||
<Route path="delete" element={<DeleteAccountPage />}></Route>
|
||||
<Route
|
||||
path="delete"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<DeleteAccountPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
></Route>
|
||||
<Route
|
||||
path="api-keys"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<ApiKeysPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
></Route>
|
||||
</Route>
|
||||
</Routes>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -62,3 +62,27 @@ export type UploadFileTypeAPI = {
|
|||
file_path: string;
|
||||
flowId: string;
|
||||
};
|
||||
|
||||
export type LoginType = {
|
||||
grant_type?: string;
|
||||
username: string;
|
||||
password: string;
|
||||
scrope?: string;
|
||||
client_id?: string;
|
||||
client_secret?: string;
|
||||
};
|
||||
|
||||
export type LoginAuthType = {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
token_type?: string;
|
||||
};
|
||||
|
||||
export type Users = {
|
||||
id: string;
|
||||
username: string;
|
||||
is_active: boolean;
|
||||
is_superuser: boolean;
|
||||
create_at: Date;
|
||||
updated_at: Date;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ export type signUpInputStateType = {
|
|||
|
||||
export type inputHandlerEventType = {
|
||||
target: {
|
||||
value: string;
|
||||
value: string | boolean;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
|
|
@ -261,6 +261,27 @@ export type loginInputStateType = {
|
|||
password: string;
|
||||
};
|
||||
|
||||
export type UserInputType = {
|
||||
username: string;
|
||||
password: string;
|
||||
is_active?: boolean;
|
||||
is_superuser?: boolean;
|
||||
};
|
||||
|
||||
export type ApiKeyType = {
|
||||
title: string;
|
||||
cancelText: string;
|
||||
confirmationText: string;
|
||||
children: ReactElement;
|
||||
icon: string;
|
||||
data?: any;
|
||||
onCloseModal: () => void;
|
||||
|
||||
};
|
||||
|
||||
export type ApiKeyInputType = {
|
||||
apikeyname: string;
|
||||
};
|
||||
export type groupedObjType = {
|
||||
family: string;
|
||||
type: string;
|
||||
|
|
@ -508,3 +529,15 @@ export type validationStatusType = {
|
|||
progress: number;
|
||||
valid: boolean;
|
||||
};
|
||||
|
||||
export type ApiKey = {
|
||||
id: string;
|
||||
api_key: string;
|
||||
name: string;
|
||||
created_at: string;
|
||||
last_used_at: string;
|
||||
};
|
||||
export type fetchErrorComponentType = {
|
||||
message: string;
|
||||
description: string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,16 +1,20 @@
|
|||
import { Users } from "../api";
|
||||
|
||||
export type AuthContextType = {
|
||||
isAdmin: boolean;
|
||||
setIsAdmin: (isAdmin: boolean) => void;
|
||||
isAuthenticated: boolean;
|
||||
accessToken: string | null;
|
||||
refreshToken: string | null;
|
||||
login: (accessToken: string, refreshToken: string) => void;
|
||||
logout: () => void;
|
||||
refreshAccessToken: (refreshToken: string) => Promise<void>;
|
||||
userData: userData | null;
|
||||
setUserData: (userData: userData | null) => void;
|
||||
};
|
||||
|
||||
export type userData = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
role: string;
|
||||
userData: Users | null;
|
||||
setUserData: (userData: Users | null) => void;
|
||||
getAuthentication: () => boolean;
|
||||
authenticationErrorCount: number;
|
||||
autoLogin: boolean;
|
||||
setAutoLogin: (autoLogin: boolean) => void;
|
||||
stars: number;
|
||||
setStars: (stars: number) => void;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ export type typesContextType = {
|
|||
setTemplates: (newState: {}) => void;
|
||||
data: APIDataType;
|
||||
setData: (newState: {}) => void;
|
||||
fetchError: boolean;
|
||||
setFetchError: (newState: boolean) => void;
|
||||
};
|
||||
|
||||
export type alertContextType = {
|
||||
|
|
@ -39,6 +41,8 @@ export type alertContextType = {
|
|||
removeFromNotificationList: (index: string) => void;
|
||||
loading: boolean;
|
||||
setLoading: (newState: boolean) => void;
|
||||
isTweakPage: boolean;
|
||||
setIsTweakPage: (newState: boolean) => void;
|
||||
};
|
||||
|
||||
export type darkContextType = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Edge } from "reactflow";
|
||||
import { Edge, Node } from "reactflow";
|
||||
import { NodeType } from "../flow";
|
||||
|
||||
export type cleanEdgesType = {
|
||||
|
|
@ -8,3 +8,8 @@ export type cleanEdgesType = {
|
|||
};
|
||||
updateEdge: (edge: Edge[]) => void;
|
||||
};
|
||||
|
||||
export type unselectAllNodesType = {
|
||||
updateNodes: (nodes: Node[]) => void;
|
||||
data: Node[] | null;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,13 +2,17 @@ import _ from "lodash";
|
|||
import {
|
||||
Connection,
|
||||
Edge,
|
||||
Node,
|
||||
ReactFlowInstance,
|
||||
ReactFlowJsonObject,
|
||||
} from "reactflow";
|
||||
import { specialCharsRegex } from "../constants/constants";
|
||||
import { APITemplateType } from "../types/api";
|
||||
import { FlowType, NodeType } from "../types/flow";
|
||||
import { cleanEdgesType } from "../types/utils/reactflowUtils";
|
||||
import {
|
||||
cleanEdgesType,
|
||||
unselectAllNodesType,
|
||||
} from "../types/utils/reactflowUtils";
|
||||
import { toNormalCase } from "./utils";
|
||||
|
||||
export function cleanEdges({
|
||||
|
|
@ -55,6 +59,14 @@ export function cleanEdges({
|
|||
updateEdge(newEdges);
|
||||
}
|
||||
|
||||
export function unselectAllNodes({ updateNodes, data }: unselectAllNodesType) {
|
||||
let newNodes = _.cloneDeep(data);
|
||||
newNodes!.forEach((node: Node) => {
|
||||
node.selected = false;
|
||||
});
|
||||
updateNodes(newNodes!);
|
||||
}
|
||||
|
||||
export function isValidConnection(
|
||||
{ source, target, sourceHandle, targetHandle }: Connection,
|
||||
reactFlowInstance: ReactFlowInstance
|
||||
|
|
@ -247,7 +259,6 @@ export function handleKeyDown(
|
|||
inputValue: string | string[] | null,
|
||||
block: string
|
||||
) {
|
||||
console.log(e, inputValue, block);
|
||||
//condition to fix bug control+backspace on Windows/Linux
|
||||
if (
|
||||
(typeof inputValue === "string" &&
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import {
|
|||
Edit,
|
||||
Eraser,
|
||||
ExternalLink,
|
||||
Eye,
|
||||
EyeOff,
|
||||
File,
|
||||
FileDown,
|
||||
FileSearch,
|
||||
|
|
@ -33,6 +35,7 @@ import {
|
|||
HelpCircle,
|
||||
Home,
|
||||
Info,
|
||||
Key,
|
||||
Laptop2,
|
||||
Layers,
|
||||
Lightbulb,
|
||||
|
|
@ -59,7 +62,9 @@ import {
|
|||
TerminalSquare,
|
||||
Trash2,
|
||||
Undo,
|
||||
Unplug,
|
||||
Upload,
|
||||
UserCog2,
|
||||
UserMinus2,
|
||||
UserPlus2,
|
||||
Users2,
|
||||
|
|
@ -290,4 +295,9 @@ export const nodeIconsLucide: iconsType = {
|
|||
ChevronsLeft,
|
||||
FaGithub,
|
||||
FaApple,
|
||||
EyeOff,
|
||||
Eye,
|
||||
UserCog2,
|
||||
Key,
|
||||
Unplug,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -201,6 +201,12 @@ module.exports = {
|
|||
".dark .theme-attribution .react-flow__attribution a": {
|
||||
color: "black",
|
||||
},
|
||||
".text-align-last-left": {
|
||||
"text-align-last": "left",
|
||||
},
|
||||
".text-align-last-right": {
|
||||
"text-align-last": "right",
|
||||
},
|
||||
});
|
||||
}),
|
||||
require("@tailwindcss/typography"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue