Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FastAPI hooks and cleanup #155

Merged
merged 5 commits into from
Aug 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions frontend/utils/hooks/useCreateAPI.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useCallback, useState } from "react";
import axios from "axios";

const useCreateAPI = (url) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);

const create = useCallback(
async (data) => {
setIsLoading(true);
try {
const response = await axios.post(url, data);
setIsLoading(false);
return response.data;
} catch (err) {
setIsLoading(false);
setError(err);
throw err;
}
},
[url]
);

return {
create,
isLoading,
error,
};
};

export default useCreateAPI;
26 changes: 26 additions & 0 deletions frontend/utils/hooks/useCreateUpdateAPI.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import useCreateAPI from "utils/hooks/useCreateAPI";
import useUpdateAPI from "utils/hooks/useUpdateAPI";

/**
* Hook that encapsulates both create and update APIs into a single save function.
* @param createURL
* @param updateURL
* @returns {{isLoading: boolean, save: ((function(*): Promise<any|undefined>)|*)}}
*/
export const useCreateUpdateAPI = (createURL, updateURL) => {
const { create, isLoading: isCreateLoading } = useCreateAPI(createURL);
const { update, isLoading: isUpdateLoading } = useUpdateAPI(updateURL);

const save = async (data) => {
if (data.id) {
return await update(data);
} else {
return await create(data);
}
};

return {
save,
isLoading: isCreateLoading || isUpdateLoading,
};
};
24 changes: 24 additions & 0 deletions frontend/utils/hooks/useDeleteAPI.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useCallback, useState } from "react";
import axios from "axios";

export const useDeleteAPI = (endpoint) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);

const deleteData = useCallback(async () => {
setIsLoading(true);
try {
await axios.delete(endpoint);
} catch (err) {
setError(err);
} finally {
setIsLoading(false);
}
}, [endpoint]);

return {
call: deleteData,
isLoading,
error,
};
};
33 changes: 33 additions & 0 deletions frontend/utils/hooks/useDetailAPI.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useState, useEffect, useCallback } from "react";
import axios from "axios";

export const useDetailAPI = (endpoint, { load = true } = {}) => {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);

const loadData = useCallback(async () => {
setIsLoading(true);
try {
const response = await axios.get(endpoint);
setData(response.data);
} catch (err) {
setError(err);
} finally {
setIsLoading(false);
}
}, [endpoint]);

useEffect(() => {
if (load) {
loadData();
}
}, [endpoint, load]);

return {
data,
load: loadData,
isLoading,
error,
};
};
60 changes: 60 additions & 0 deletions frontend/utils/hooks/useObjectEditorView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useEffect, useState } from "react";
import { v4 as uuid4 } from "uuid";

/**
* Hook for handling the state of an object editor view.
* state for handling whether how to load the data (new vs existing)
* and when to reset the editor when opened. The cached state does not
* reset when the url changes as protection against reloading when
* creating new chains. This state tracks when to reset the cache.
*
* @param id - object id, or undefined/null if new
* @param load - function to load the object when needed
*/
export const useObjectEditorView = (id, load) => {
const [idRef, setIdRef] = useState(null);
const [isNew, setIsNew] = useState(null);
const [wasCreated, setWasCreated] = useState(null);
useEffect(() => {
const firstRender = isNew === null;
if (firstRender) {
// first render caches whether this started as a new chain
setIsNew(id === undefined);
} else {
// switch from existing to new
if (id === undefined && !isNew) {
setIsNew(true);
setWasCreated(false);
}
// a new chain was created
if (id !== undefined && isNew) {
setWasCreated(true);
}
// switch from created to new
if (id === undefined && wasCreated) {
setIsNew(true);
setWasCreated(false);
setIdRef(uuid4());
}
}
}, [id]);

useEffect(() => {
// load chain if id is provided on view load
// otherwise state will be handled internally by the editor
if (isNew === false) {
load();
setIdRef(id);
} else {
// create a uuid here to force a new editor. This helps detect
// creating a new object after a new object was just created.
setIdRef(uuid4());
}
}, [isNew]);

return {
isNew,
idRef,
wasCreated,
};
};
43 changes: 43 additions & 0 deletions frontend/utils/hooks/usePaginatedAPI.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useCallback, useEffect, useState } from "react";
import axios from "axios";

export function usePaginatedAPI(
endpoint,
{ offset = 0, limit = 10, load = true, loadDependencies = [] } = {}
) {
const [page, setPage] = useState(null);
const [isLoading, setIsLoading] = useState(true);

const _load = useCallback(
async ({ search } = {}) => {
setIsLoading(true);
const params = { limit, offset };
if (search) {
params.search = search;
}
try {
const response = await axios.get(endpoint, {
params,
});
setPage(response.data);
} catch (error) {
console.error("Failed to fetch data:", error);
} finally {
setIsLoading(false);
}
},
[endpoint]
);

useEffect(() => {
if (load) {
_load();
}
}, [_load, ...loadDependencies]);

return {
page,
isLoading,
load: _load,
};
}
31 changes: 31 additions & 0 deletions frontend/utils/hooks/useUpdateAPI.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useCallback, useState } from "react";
import axios from "axios";

const useUpdateAPI = (url, { onSuccess, onError } = {}) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);

const update = useCallback(
async (data) => {
setIsLoading(true);
try {
const response = await axios.put(url, data);
setIsLoading(false);
return response.data;
} catch (err) {
setIsLoading(false);
setError(err);
throw err;
}
},
[url]
);

return {
update,
isLoading,
error,
};
};

export default useUpdateAPI;
15 changes: 10 additions & 5 deletions ix/api/agents/endpoints.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from asgiref.sync import sync_to_async
from django.db.models import Q
from fastapi import HTTPException, APIRouter
from typing import List, Optional
from typing import Optional
from pydantic import BaseModel
from uuid import UUID
from ix.agents.models import Agent
from ix.api.chains.endpoints import DeletedItem
from ix.api.agents.types import Agent as AgentPydantic
from ix.api.agents.types import Agent as AgentPydantic, AgentPage

__all__ = ["router", "AgentCreateUpdate"]

Expand Down Expand Up @@ -38,14 +39,18 @@ async def get_agent(agent_id: str):
return AgentPydantic.from_orm(agent)


@router.get("/agents/", response_model=List[AgentPydantic], tags=["Agents"])
async def get_agents(search: Optional[str] = None):
@router.get("/agents/", response_model=AgentPage, tags=["Agents"])
async def get_agents(search: Optional[str] = None, limit: int = 10, offset: int = 0):
query = (
Agent.objects.filter(Q(name__icontains=search) | Q(alias__icontains=search))
if search
else Agent.objects.all()
)
return [AgentPydantic.from_orm(agent) async for agent in query]

# punting on async implementation of pagination until later
return await sync_to_async(AgentPage.paginate)(
output_model=AgentPydantic, queryset=query, limit=limit, offset=offset
)


@router.put("/agents/{agent_id}", response_model=AgentPydantic, tags=["Agents"])
Expand Down
8 changes: 8 additions & 0 deletions ix/api/agents/types.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from datetime import datetime
from typing import List
from uuid import UUID

from pydantic import BaseModel, Field

import logging

from ix.utils.graphene.pagination import QueryPage

logger = logging.getLogger(__name__)


Expand All @@ -20,3 +23,8 @@ class Agent(BaseModel):

class Config:
orm_mode = True


class AgentPage(QueryPage[Agent]):
# override objects, FastAPI isn't detecting QueryPage type
objects: List[Agent]
Loading
Loading