Skip to content

Commit

Permalink
fix: implement polling while asynchronous tool refresh is unresolved
Browse files Browse the repository at this point in the history
- force refresh happens asynchronously from response. Must implement polling to ensure data displayed is up to date
- removes the redundant `ToolReferenceService.getToolReferencesCategoryMap` method by extracting unique code and applying it as needed in components
  • Loading branch information
ryanhopperlowe committed Jan 14, 2025
1 parent 79c8cd6 commit c18c017
Show file tree
Hide file tree
Showing 8 changed files with 134 additions and 80 deletions.
19 changes: 13 additions & 6 deletions ui/admin/app/components/tools/ToolCatalog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import useSWR from "swr";
import { OAuthProvider } from "~/lib/model/oauthApps/oauth-helpers";
import {
ToolCategory,
ToolReferenceService,
} from "~/lib/service/api/toolreferenceService";
convertToolReferencesToCategoryMap,
} from "~/lib/model/toolReferences";
import { ToolReferenceService } from "~/lib/service/api/toolreferenceService";
import { cn } from "~/lib/utils";

import { ToolCatalogGroup } from "~/components/tools/ToolCatalogGroup";
Expand Down Expand Up @@ -41,11 +42,17 @@ export function ToolCatalog({
onUpdateTools,
classNames,
}: ToolCatalogProps) {
const { data: toolCategories, isLoading } = useSWR(
ToolReferenceService.getToolReferencesCategoryMap.key("tool"),
() => ToolReferenceService.getToolReferencesCategoryMap("tool"),
{ fallbackData: {} }
const { data: toolList, isLoading } = useSWR(
ToolReferenceService.getToolReferences.key("tool"),
() => ToolReferenceService.getToolReferences("tool"),
{ fallbackData: [] }
);

const toolCategories = useMemo(
() => convertToolReferencesToCategoryMap(toolList),
[toolList]
);

const [search, setSearch] = useState("");

const oauthApps = useOAuthAppList();
Expand Down
2 changes: 1 addition & 1 deletion ui/admin/app/components/tools/ToolCatalogGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";

import { ToolCategory } from "~/lib/service/api/toolreferenceService";
import { ToolCategory } from "~/lib/model/toolReferences";
import { cn } from "~/lib/utils";

import { ToolItem } from "~/components/tools/ToolItem";
Expand Down
27 changes: 14 additions & 13 deletions ui/admin/app/components/tools/toolGrid/ToolCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,22 @@ import {
TooltipTrigger,
} from "~/components/ui/tooltip";
import { useAsync } from "~/hooks/useAsync";
import { usePollSingleTool } from "~/hooks/usePollSingleTool";

interface ToolCardProps {
tool: ToolReference;
onDelete: (id: string) => void;
}

export function ToolCard({ tool, onDelete }: ToolCardProps) {
const { startPolling, isPolling } = usePollSingleTool(tool.id);

const forceRefresh = useAsync(
ToolReferenceService.forceRefreshToolReference,
{
onSuccess: () => {
onSuccess: ({ resolved }) => {
toast.success("Tool reference force refreshed");
ToolReferenceService.getToolReferences.revalidate("tool");
if (!resolved) startPolling();
},
}
);
Expand Down Expand Up @@ -80,25 +83,23 @@ export function ToolCard({ tool, onDelete }: ToolCardProps) {
)}
</h4>

{!tool.builtin && (
<DropdownMenu>
<div className="flex items-center gap-2">
{forceRefresh.isLoading && <LoadingSpinner />}
<div className="flex items-center gap-2">
{(forceRefresh.isLoading || isPolling) && <LoadingSpinner />}

<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="m-0">
<EllipsisVerticalIcon />
</Button>
</DropdownMenuTrigger>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="m-0">
<EllipsisVerticalIcon />
</Button>
</DropdownMenuTrigger>

<DropdownMenuContent side="top" align="start">
<DropdownMenuItem onClick={() => forceRefresh.execute(tool.id)}>
Refresh Tool
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</CardHeader>
<CardContent className="flex-grow">
{!tool.builtin && (
Expand Down
4 changes: 2 additions & 2 deletions ui/admin/app/components/tools/toolGrid/ToolGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { useCallback, useEffect, useState } from "react";

import { ToolReference } from "~/lib/model/toolReferences";
import {
CustomToolsToolCategory,
ToolCategoryMap,
} from "~/lib/service/api/toolreferenceService";
ToolReference,
} from "~/lib/model/toolReferences";

import { CategoryHeader } from "~/components/tools/toolGrid/CategoryHeader";
import { CategoryTools } from "~/components/tools/toolGrid/CategoryTools";
Expand Down
49 changes: 49 additions & 0 deletions ui/admin/app/hooks/usePollSingleTool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useEffect, useState } from "react";
import useSWR from "swr";

import { ToolReferenceService } from "~/lib/service/api/toolreferenceService";

export function usePollSingleTool(toolId: string) {
const [isPolling, setIsPolling] = useState(false);

const { mutate: updateTools } = useSWR(
isPolling ? ToolReferenceService.getToolReferences.key("tool") : null,
({ type }) => ToolReferenceService.getToolReferences(type),
{ fallbackData: [], revalidateIfStale: false }
);

const getTool = useSWR(
isPolling ? ToolReferenceService.getToolReferenceById.key(toolId) : null,
({ toolReferenceId }) =>
ToolReferenceService.getToolReferenceById(toolReferenceId),
{ refreshInterval: 1000 }
);

useEffect(() => {
if (!getTool.data) return;

setIsPolling(!getTool.data.resolved);

// resolved means async update is complete
if (getTool.data.resolved) {
updateTools(
(tools) => {
if (!getTool.data) return tools;
if (!tools) return [getTool.data];

const index = tools.findIndex((tool) => tool.id === toolId);

const copy = [...tools];
copy[index] = getTool.data;
return copy;
},
{ revalidate: false }
);
}
}, [getTool.data, updateTools, toolId]);

return {
startPolling: () => setIsPolling(true),
isPolling,
};
}
39 changes: 39 additions & 0 deletions ui/admin/app/lib/model/toolReferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,42 @@ export const toolReferenceToTemplate = (toolReference: ToolReference) => {
args: toolReference.params,
} as Template;
};

export type ToolCategory = {
bundleTool?: ToolReference;
tools: ToolReference[];
};
export const UncategorizedToolCategory = "Uncategorized";
export const CustomToolsToolCategory = "Custom Tools";
export type ToolCategoryMap = Record<string, ToolCategory>;

export function convertToolReferencesToCategoryMap(
toolReferences: ToolReference[]
) {
const result: ToolCategoryMap = {};

for (const toolReference of toolReferences) {
if (toolReference.deleted) {
// skip tools if marked with deleted
continue;
}

const category = !toolReference.builtin
? CustomToolsToolCategory
: toolReference.metadata?.category || UncategorizedToolCategory;

if (!result[category]) {
result[category] = {
tools: [],
};
}

if (toolReference.metadata?.bundle === "true") {
result[category].bundleTool = toolReference;
} else {
result[category].tools.push(toolReference);
}
}

return result;
}
48 changes: 0 additions & 48 deletions ui/admin/app/lib/service/api/toolreferenceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,53 +26,6 @@ getToolReferences.revalidate = (type?: ToolReferenceType) => {
);
};

export type ToolCategory = {
bundleTool?: ToolReference;
tools: ToolReference[];
};
export const UncategorizedToolCategory = "Uncategorized";
export const CustomToolsToolCategory = "Custom Tools";
export type ToolCategoryMap = Record<string, ToolCategory>;
async function getToolReferencesCategoryMap(type?: ToolReferenceType) {
const res = await request<{ items: ToolReference[] }>({
url: ApiRoutes.toolReferences.base({ type }).url,
errorMessage: "Failed to fetch tool references category map",
});

const toolReferences = res.data.items;
const result: ToolCategoryMap = {};

for (const toolReference of toolReferences) {
if (toolReference.deleted) {
// skip tools if marked with deleted
continue;
}

const category = !toolReference.builtin
? CustomToolsToolCategory
: toolReference.metadata?.category || UncategorizedToolCategory;

if (!result[category]) {
result[category] = {
tools: [],
};
}

if (toolReference.metadata?.bundle === "true") {
result[category].bundleTool = toolReference;
} else {
result[category].tools.push(toolReference);
}
}

return result;
}
getToolReferencesCategoryMap.key = (type?: ToolReferenceType) =>
({
url: ApiRoutes.toolReferences.base({ type }).path,
responseType: "map",
}) as const;

const getToolReferenceById = async (toolReferenceId: string) => {
const res = await request<ToolReference>({
url: ApiRoutes.toolReferences.getById(toolReferenceId).url,
Expand Down Expand Up @@ -142,7 +95,6 @@ async function deleteToolReference(id: string) {

export const ToolReferenceService = {
getToolReferences,
getToolReferencesCategoryMap,
getToolReferenceById,
createToolReference,
updateToolReference,
Expand Down
26 changes: 16 additions & 10 deletions ui/admin/app/routes/_auth.tools._index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { PlusIcon, SearchIcon } from "lucide-react";
import { useState } from "react";
import { useMemo, useState } from "react";
import { MetaFunction } from "react-router";
import useSWR, { preload } from "swr";

import { convertToolReferencesToCategoryMap } from "~/lib/model/toolReferences";
import { ToolReferenceService } from "~/lib/service/api/toolreferenceService";
import { RouteHandle } from "~/lib/service/routeHandles";

Expand All @@ -23,36 +24,41 @@ import { ScrollArea } from "~/components/ui/scroll-area";

export async function clientLoader() {
await Promise.all([
preload(ToolReferenceService.getToolReferencesCategoryMap.key("tool"), () =>
ToolReferenceService.getToolReferencesCategoryMap("tool")
preload(ToolReferenceService.getToolReferences.key("tool"), () =>
ToolReferenceService.getToolReferences("tool")
),
]);
return null;
}

export default function Tools() {
const { data: toolCategories, mutate } = useSWR(
ToolReferenceService.getToolReferencesCategoryMap.key("tool"),
() => ToolReferenceService.getToolReferencesCategoryMap("tool"),
{ fallbackData: {} }
const getTools = useSWR(
ToolReferenceService.getToolReferences.key("tool"),
() => ToolReferenceService.getToolReferences("tool"),
{ fallbackData: [] }
);

const toolCategories = useMemo(
() => convertToolReferencesToCategoryMap(getTools.data),
[getTools.data]
);

const [isDialogOpen, setIsDialogOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [errorDialogError, setErrorDialogError] = useState("");

const handleCreateSuccess = () => {
mutate();
getTools.mutate();
setIsDialogOpen(false);
};

const handleDelete = async (id: string) => {
await ToolReferenceService.deleteToolReference(id);
mutate();
getTools.mutate();
};

const handleErrorDialogError = (error: string) => {
mutate();
getTools.mutate();
setErrorDialogError(error);
setIsDialogOpen(false);
};
Expand Down

0 comments on commit c18c017

Please sign in to comment.