Merge branch 'dev' into implement_loguru
This commit is contained in:
commit
e8846fc568
13 changed files with 68 additions and 53 deletions
2
Makefile
2
Makefile
|
|
@ -27,7 +27,7 @@ format:
|
||||||
cd src/frontend && npm run format
|
cd src/frontend && npm run format
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
poetry run mypy --exclude .venv .
|
poetry run mypy src/backend/langflow
|
||||||
poetry run black . --check
|
poetry run black . --check
|
||||||
poetry run ruff . --fix
|
poetry run ruff . --fix
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ class ValidatePromptRequest(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
template: str
|
template: str
|
||||||
# optional for tweak call
|
# optional for tweak call
|
||||||
frontend_node: Optional[FrontendNodeRequest]
|
frontend_node: Optional[FrontendNodeRequest] = None
|
||||||
|
|
||||||
|
|
||||||
# Build ValidationResponse class for {"imports": {"errors": []}, "function": {"errors": []}}
|
# Build ValidationResponse class for {"imports": {"errors": []}, "function": {"errors": []}}
|
||||||
|
|
@ -42,7 +42,7 @@ class CodeValidationResponse(BaseModel):
|
||||||
class PromptValidationResponse(BaseModel):
|
class PromptValidationResponse(BaseModel):
|
||||||
input_variables: list
|
input_variables: list
|
||||||
# object return for tweak call
|
# object return for tweak call
|
||||||
frontend_node: FrontendNodeRequest | object
|
frontend_node: Optional[FrontendNodeRequest] = None
|
||||||
|
|
||||||
|
|
||||||
INVALID_CHARACTERS = {
|
INVALID_CHARACTERS = {
|
||||||
|
|
|
||||||
|
|
@ -11,17 +11,15 @@ from fastapi.responses import StreamingResponse
|
||||||
from langflow.api.utils import build_input_keys_response
|
from langflow.api.utils import build_input_keys_response
|
||||||
from langflow.api.v1.schemas import BuildStatus, BuiltResponse, InitResponse, StreamData
|
from langflow.api.v1.schemas import BuildStatus, BuiltResponse, InitResponse, StreamData
|
||||||
|
|
||||||
from langflow.services import service_manager, ServiceType
|
|
||||||
from langflow.graph.graph.base import Graph
|
from langflow.graph.graph.base import Graph
|
||||||
from langflow.services.auth.utils import get_current_active_user, get_current_user
|
from langflow.services.auth.utils import get_current_active_user, get_current_user
|
||||||
from langflow.services.utils import get_session
|
from langflow.services.utils import get_session
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
from langflow.services.utils import get_chat_manager, get_session
|
||||||
from cachetools import LRUCache
|
from cachetools import LRUCache
|
||||||
from sqlmodel import Session
|
from sqlmodel import Session
|
||||||
from typing import TYPE_CHECKING
|
from langflow.services.chat.manager import ChatManager
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from langflow.services.chat.manager import ChatManager
|
|
||||||
|
|
||||||
router = APIRouter(tags=["Chat"])
|
router = APIRouter(tags=["Chat"])
|
||||||
|
|
||||||
|
|
@ -34,6 +32,7 @@ async def chat(
|
||||||
websocket: WebSocket,
|
websocket: WebSocket,
|
||||||
token: str = Query(...),
|
token: str = Query(...),
|
||||||
db: Session = Depends(get_session),
|
db: Session = Depends(get_session),
|
||||||
|
chat_manager: "ChatManager" = Depends(get_chat_manager),
|
||||||
):
|
):
|
||||||
"""Websocket endpoint for chat."""
|
"""Websocket endpoint for chat."""
|
||||||
try:
|
try:
|
||||||
|
|
@ -48,7 +47,6 @@ async def chat(
|
||||||
code=status.WS_1008_POLICY_VIOLATION, reason="Unauthorized"
|
code=status.WS_1008_POLICY_VIOLATION, reason="Unauthorized"
|
||||||
)
|
)
|
||||||
|
|
||||||
chat_manager: "ChatManager" = service_manager.get(ServiceType.CHAT_MANAGER)
|
|
||||||
if client_id in chat_manager.in_memory_cache:
|
if client_id in chat_manager.in_memory_cache:
|
||||||
await chat_manager.handle_websocket(client_id, websocket)
|
await chat_manager.handle_websocket(client_id, websocket)
|
||||||
else:
|
else:
|
||||||
|
|
@ -72,7 +70,10 @@ async def chat(
|
||||||
|
|
||||||
@router.post("/build/init/{flow_id}", response_model=InitResponse, status_code=201)
|
@router.post("/build/init/{flow_id}", response_model=InitResponse, status_code=201)
|
||||||
async def init_build(
|
async def init_build(
|
||||||
graph_data: dict, flow_id: str, current_user=Depends(get_current_active_user)
|
graph_data: dict,
|
||||||
|
flow_id: str,
|
||||||
|
current_user=Depends(get_current_active_user),
|
||||||
|
chat_manager: "ChatManager" = Depends(get_chat_manager),
|
||||||
):
|
):
|
||||||
"""Initialize the build by storing graph data and returning a unique session ID."""
|
"""Initialize the build by storing graph data and returning a unique session ID."""
|
||||||
|
|
||||||
|
|
@ -87,7 +88,6 @@ async def init_build(
|
||||||
return InitResponse(flowId=flow_id)
|
return InitResponse(flowId=flow_id)
|
||||||
|
|
||||||
# Delete from cache if already exists
|
# Delete from cache if already exists
|
||||||
chat_manager = service_manager.get(ServiceType.CHAT_MANAGER)
|
|
||||||
if flow_id in chat_manager.in_memory_cache:
|
if flow_id in chat_manager.in_memory_cache:
|
||||||
with chat_manager.in_memory_cache._lock:
|
with chat_manager.in_memory_cache._lock:
|
||||||
chat_manager.in_memory_cache.delete(flow_id)
|
chat_manager.in_memory_cache.delete(flow_id)
|
||||||
|
|
@ -123,7 +123,9 @@ async def build_status(flow_id: str):
|
||||||
|
|
||||||
|
|
||||||
@router.get("/build/stream/{flow_id}", response_class=StreamingResponse)
|
@router.get("/build/stream/{flow_id}", response_class=StreamingResponse)
|
||||||
async def stream_build(flow_id: str):
|
async def stream_build(
|
||||||
|
flow_id: str, chat_manager: "ChatManager" = Depends(get_chat_manager)
|
||||||
|
):
|
||||||
"""Stream the build process based on stored flow data."""
|
"""Stream the build process based on stored flow data."""
|
||||||
|
|
||||||
async def event_stream(flow_id):
|
async def event_stream(flow_id):
|
||||||
|
|
@ -202,7 +204,6 @@ async def stream_build(flow_id: str):
|
||||||
"handle_keys": [],
|
"handle_keys": [],
|
||||||
}
|
}
|
||||||
yield str(StreamData(event="message", data=input_keys_response))
|
yield str(StreamData(event="message", data=input_keys_response))
|
||||||
chat_manager = service_manager.get(ServiceType.CHAT_MANAGER)
|
|
||||||
chat_manager.set_cache(flow_id, langchain_object)
|
chat_manager.set_cache(flow_id, langchain_object)
|
||||||
# We need to reset the chat history
|
# We need to reset the chat history
|
||||||
chat_manager.chat_history.empty_history(flow_id)
|
chat_manager.chat_history.empty_history(flow_id)
|
||||||
|
|
|
||||||
|
|
@ -34,14 +34,15 @@ from sqlmodel import Session
|
||||||
router = APIRouter(tags=["Base"])
|
router = APIRouter(tags=["Base"])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/all")
|
@router.get("/all", dependencies=[Depends(get_current_active_user)])
|
||||||
def get_all(current_user: User = Depends(get_current_active_user)):
|
def get_all(
|
||||||
|
settings_manager=Depends(get_settings_manager),
|
||||||
|
):
|
||||||
logger.debug("Building langchain types dict")
|
logger.debug("Building langchain types dict")
|
||||||
native_components = build_langchain_types_dict()
|
native_components = build_langchain_types_dict()
|
||||||
# custom_components is a list of dicts
|
# custom_components is a list of dicts
|
||||||
# need to merge all the keys into one dict
|
# need to merge all the keys into one dict
|
||||||
custom_components_from_file: dict[str, Any] = {}
|
custom_components_from_file: dict[str, Any] = {}
|
||||||
settings_manager = get_settings_manager()
|
|
||||||
if settings_manager.settings.COMPONENTS_PATH:
|
if settings_manager.settings.COMPONENTS_PATH:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Building custom components from {settings_manager.settings.COMPONENTS_PATH}"
|
f"Building custom components from {settings_manager.settings.COMPONENTS_PATH}"
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ def update_flow(
|
||||||
flow_id: UUID,
|
flow_id: UUID,
|
||||||
flow: FlowUpdate,
|
flow: FlowUpdate,
|
||||||
current_user: User = Depends(get_current_active_user),
|
current_user: User = Depends(get_current_active_user),
|
||||||
|
settings_manager=Depends(get_settings_manager),
|
||||||
):
|
):
|
||||||
"""Update a flow."""
|
"""Update a flow."""
|
||||||
|
|
||||||
|
|
@ -90,7 +91,6 @@ def update_flow(
|
||||||
if not db_flow:
|
if not db_flow:
|
||||||
raise HTTPException(status_code=404, detail="Flow not found")
|
raise HTTPException(status_code=404, detail="Flow not found")
|
||||||
flow_data = flow.dict(exclude_unset=True)
|
flow_data = flow.dict(exclude_unset=True)
|
||||||
settings_manager = get_settings_manager()
|
|
||||||
if settings_manager.settings.REMOVE_API_KEYS:
|
if settings_manager.settings.REMOVE_API_KEYS:
|
||||||
flow_data = remove_api_keys(flow_data)
|
flow_data = remove_api_keys(flow_data)
|
||||||
for key, value in flow_data.items():
|
for key, value in flow_data.items():
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,9 @@ async def login_to_get_access_token(
|
||||||
|
|
||||||
|
|
||||||
@router.get("/auto_login")
|
@router.get("/auto_login")
|
||||||
async def auto_login(db: Session = Depends(get_session)):
|
async def auto_login(
|
||||||
settings_manager = get_settings_manager()
|
db: Session = Depends(get_session), settings_manager=Depends(get_settings_manager)
|
||||||
|
):
|
||||||
if settings_manager.auth_settings.AUTO_LOGIN:
|
if settings_manager.auth_settings.AUTO_LOGIN:
|
||||||
return create_user_longterm_token(db)
|
return create_user_longterm_token(db)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ router = APIRouter(tags=["Users"])
|
||||||
@router.post("/user", response_model=UserRead, status_code=201)
|
@router.post("/user", response_model=UserRead, status_code=201)
|
||||||
def add_user(
|
def add_user(
|
||||||
user: UserCreate,
|
user: UserCreate,
|
||||||
db: Session = Depends(get_session),
|
session: Session = Depends(get_session),
|
||||||
) -> User:
|
) -> User:
|
||||||
"""
|
"""
|
||||||
Add a new user to the database.
|
Add a new user to the database.
|
||||||
|
|
@ -38,11 +38,11 @@ def add_user(
|
||||||
try:
|
try:
|
||||||
new_user.password = get_password_hash(user.password)
|
new_user.password = get_password_hash(user.password)
|
||||||
|
|
||||||
db.add(new_user)
|
session.add(new_user)
|
||||||
db.commit()
|
session.commit()
|
||||||
db.refresh(new_user)
|
session.refresh(new_user)
|
||||||
except IntegrityError as e:
|
except IntegrityError as e:
|
||||||
db.rollback()
|
session.rollback()
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400, detail="This username is unavailable."
|
status_code=400, detail="This username is unavailable."
|
||||||
) from e
|
) from e
|
||||||
|
|
@ -65,16 +65,16 @@ def read_all_users(
|
||||||
skip: int = 0,
|
skip: int = 0,
|
||||||
limit: int = 10,
|
limit: int = 10,
|
||||||
current_user: Session = Depends(get_current_active_superuser),
|
current_user: Session = Depends(get_current_active_superuser),
|
||||||
db: Session = Depends(get_session),
|
session: Session = Depends(get_session),
|
||||||
) -> UsersResponse:
|
) -> UsersResponse:
|
||||||
"""
|
"""
|
||||||
Retrieve a list of users from the database with pagination.
|
Retrieve a list of users from the database with pagination.
|
||||||
"""
|
"""
|
||||||
query = select(User).offset(skip).limit(limit)
|
query = select(User).offset(skip).limit(limit)
|
||||||
users = db.execute(query).fetchall()
|
users = session.execute(query).fetchall()
|
||||||
|
|
||||||
count_query = select(func.count()).select_from(User) # type: ignore
|
count_query = select(func.count()).select_from(User) # type: ignore
|
||||||
total_count = db.execute(count_query).scalar()
|
total_count = session.execute(count_query).scalar()
|
||||||
|
|
||||||
return UsersResponse(
|
return UsersResponse(
|
||||||
total_count=total_count, # type: ignore
|
total_count=total_count, # type: ignore
|
||||||
|
|
@ -87,19 +87,19 @@ def patch_user(
|
||||||
user_id: UUID,
|
user_id: UUID,
|
||||||
user: UserUpdate,
|
user: UserUpdate,
|
||||||
_: Session = Depends(get_current_active_user),
|
_: Session = Depends(get_current_active_user),
|
||||||
db: Session = Depends(get_session),
|
session: Session = Depends(get_session),
|
||||||
) -> User:
|
) -> User:
|
||||||
"""
|
"""
|
||||||
Update an existing user's data.
|
Update an existing user's data.
|
||||||
"""
|
"""
|
||||||
return update_user(user_id, user, db)
|
return update_user(user_id, user, session)
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/user/{user_id}")
|
@router.delete("/user/{user_id}")
|
||||||
def delete_user(
|
def delete_user(
|
||||||
user_id: UUID,
|
user_id: UUID,
|
||||||
current_user: User = Depends(get_current_active_superuser),
|
current_user: User = Depends(get_current_active_superuser),
|
||||||
db: Session = Depends(get_session),
|
session: Session = Depends(get_session),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
Delete a user from the database.
|
Delete a user from the database.
|
||||||
|
|
@ -113,12 +113,12 @@ def delete_user(
|
||||||
status_code=403, detail="You don't have the permission to delete this user"
|
status_code=403, detail="You don't have the permission to delete this user"
|
||||||
)
|
)
|
||||||
|
|
||||||
user_db = db.query(User).filter(User.id == user_id).first()
|
user_db = session.query(User).filter(User.id == user_id).first()
|
||||||
if not user_db:
|
if not user_db:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
|
||||||
db.delete(user_db)
|
session.delete(user_db)
|
||||||
db.commit()
|
session.commit()
|
||||||
|
|
||||||
return {"detail": "User deleted"}
|
return {"detail": "User deleted"}
|
||||||
|
|
||||||
|
|
@ -126,7 +126,7 @@ def delete_user(
|
||||||
# TODO: REMOVE - Just for testing purposes
|
# TODO: REMOVE - Just for testing purposes
|
||||||
@router.post("/super_user", response_model=User)
|
@router.post("/super_user", response_model=User)
|
||||||
def add_super_user_for_testing_purposes_delete_me_before_merge_into_dev(
|
def add_super_user_for_testing_purposes_delete_me_before_merge_into_dev(
|
||||||
db: Session = Depends(get_session),
|
session: Session = Depends(get_session),
|
||||||
) -> User:
|
) -> User:
|
||||||
"""
|
"""
|
||||||
Add a superuser for testing purposes.
|
Add a superuser for testing purposes.
|
||||||
|
|
@ -141,11 +141,11 @@ def add_super_user_for_testing_purposes_delete_me_before_merge_into_dev(
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db.add(new_user)
|
session.add(new_user)
|
||||||
db.commit()
|
session.commit()
|
||||||
db.refresh(new_user)
|
session.refresh(new_user)
|
||||||
except IntegrityError as e:
|
except IntegrityError as e:
|
||||||
db.rollback()
|
session.rollback()
|
||||||
raise HTTPException(status_code=400, detail="User exists") from e
|
raise HTTPException(status_code=400, detail="User exists") from e
|
||||||
|
|
||||||
return new_user
|
return new_user
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ def post_validate_prompt(prompt_request: ValidatePromptRequest):
|
||||||
if prompt_request.frontend_node is None:
|
if prompt_request.frontend_node is None:
|
||||||
return PromptValidationResponse(
|
return PromptValidationResponse(
|
||||||
input_variables=input_variables,
|
input_variables=input_variables,
|
||||||
frontend_node={},
|
frontend_node=None,
|
||||||
)
|
)
|
||||||
old_custom_fields = get_old_custom_fields(prompt_request)
|
old_custom_fields = get_old_custom_fields(prompt_request)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
from langflow.services import ServiceType, service_manager
|
from langflow.services import ServiceType, service_manager
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Generator
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from langflow.services.database.manager import DatabaseManager
|
from langflow.services.database.manager import DatabaseManager
|
||||||
from langflow.services.settings.manager import SettingsManager
|
from langflow.services.settings.manager import SettingsManager
|
||||||
|
from langflow.services.chat.manager import ChatManager
|
||||||
|
from sqlmodel import Session
|
||||||
|
|
||||||
|
|
||||||
def get_settings_manager() -> "SettingsManager":
|
def get_settings_manager() -> "SettingsManager":
|
||||||
|
|
@ -15,6 +17,10 @@ def get_db_manager() -> "DatabaseManager":
|
||||||
return service_manager.get(ServiceType.DATABASE_MANAGER)
|
return service_manager.get(ServiceType.DATABASE_MANAGER)
|
||||||
|
|
||||||
|
|
||||||
def get_session():
|
def get_session() -> Generator["Session", None, None]:
|
||||||
db_manager = service_manager.get(ServiceType.DATABASE_MANAGER)
|
db_manager = service_manager.get(ServiceType.DATABASE_MANAGER)
|
||||||
yield from db_manager.get_session()
|
yield from db_manager.get_session()
|
||||||
|
|
||||||
|
|
||||||
|
def get_chat_manager() -> "ChatManager":
|
||||||
|
return service_manager.get(ServiceType.CHAT_MANAGER)
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,7 @@ export default function DropdownButton({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!showOptions ? (
|
{!showOptions ? (
|
||||||
<IconComponent
|
<IconComponent name="ChevronDown" />
|
||||||
name="ChevronDown"
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<IconComponent name="ChevronUp" />
|
<IconComponent name="ChevronUp" />
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -315,9 +315,11 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
input.type = "file";
|
input.type = "file";
|
||||||
input.accept = ".json";
|
input.accept = ".json";
|
||||||
// add a change event listener to the file input
|
// add a change event listener to the file input
|
||||||
id = await new Promise(resolve => {
|
id = await new Promise((resolve) => {
|
||||||
input.onchange = async (e: Event) => {
|
input.onchange = async (e: Event) => {
|
||||||
if ((e.target as HTMLInputElement).files![0].type === "application/json") {
|
if (
|
||||||
|
(e.target as HTMLInputElement).files![0].type === "application/json"
|
||||||
|
) {
|
||||||
const currentfile = (e.target as HTMLInputElement).files![0];
|
const currentfile = (e.target as HTMLInputElement).files![0];
|
||||||
let text = await currentfile.text();
|
let text = await currentfile.text();
|
||||||
let flow: FlowType = JSON.parse(text);
|
let flow: FlowType = JSON.parse(text);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useContext, useEffect } from "react";
|
import { useContext, useEffect } from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
|
import DropdownButton from "../../components/DropdownButtonComponent";
|
||||||
import { CardComponent } from "../../components/cardComponent";
|
import { CardComponent } from "../../components/cardComponent";
|
||||||
import IconComponent from "../../components/genericIconComponent";
|
import IconComponent from "../../components/genericIconComponent";
|
||||||
import Header from "../../components/headerComponent";
|
import Header from "../../components/headerComponent";
|
||||||
|
|
@ -7,7 +8,6 @@ import { SkeletonCardComponent } from "../../components/skeletonCardComponent";
|
||||||
import { Button } from "../../components/ui/button";
|
import { Button } from "../../components/ui/button";
|
||||||
import { USER_PROJECTS_HEADER } from "../../constants/constants";
|
import { USER_PROJECTS_HEADER } from "../../constants/constants";
|
||||||
import { TabsContext } from "../../contexts/tabsContext";
|
import { TabsContext } from "../../contexts/tabsContext";
|
||||||
import DropdownButton from "../../components/DropdownButtonComponent";
|
|
||||||
export default function HomePage(): JSX.Element {
|
export default function HomePage(): JSX.Element {
|
||||||
const {
|
const {
|
||||||
flows,
|
flows,
|
||||||
|
|
@ -15,12 +15,19 @@ export default function HomePage(): JSX.Element {
|
||||||
downloadFlows,
|
downloadFlows,
|
||||||
uploadFlows,
|
uploadFlows,
|
||||||
addFlow,
|
addFlow,
|
||||||
removeFlow, uploadFlow,
|
removeFlow,
|
||||||
|
uploadFlow,
|
||||||
isLoading,
|
isLoading,
|
||||||
} = useContext(TabsContext);
|
} = useContext(TabsContext);
|
||||||
const dropdownOptions = [{name: "Import from JSON", onBtnClick: () => uploadFlow(true).then((id) => {
|
const dropdownOptions = [
|
||||||
navigate("/flow/" + id);
|
{
|
||||||
})}]
|
name: "Import from JSON",
|
||||||
|
onBtnClick: () =>
|
||||||
|
uploadFlow(true).then((id) => {
|
||||||
|
navigate("/flow/" + id);
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
// Set a null id
|
// Set a null id
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -73,7 +80,7 @@ export default function HomePage(): JSX.Element {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="main-page-description-text">
|
<span className="main-page-description-text">
|
||||||
Manage your personal projects. Download or upload your collection.
|
Manage your personal projects. Download or upload your collection.
|
||||||
</span>
|
</span>
|
||||||
<div className="main-page-flows-display">
|
<div className="main-page-flows-display">
|
||||||
{isLoading && flows.length == 0 ? (
|
{isLoading && flows.length == 0 ? (
|
||||||
|
|
|
||||||
|
|
@ -548,5 +548,5 @@ export type fetchErrorComponentType = {
|
||||||
export type dropdownButtonPropsType = {
|
export type dropdownButtonPropsType = {
|
||||||
firstButtonName: string;
|
firstButtonName: string;
|
||||||
onFirstBtnClick: () => void;
|
onFirstBtnClick: () => void;
|
||||||
options: Array<{ name: string; onBtnClick: () => void; }>;
|
options: Array<{ name: string; onBtnClick: () => void }>;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue