Add Panel For Cache Key Value Selection And Deletion (#3266)

This commit is contained in:
Jonathan Dobson
2025-08-21 19:13:36 -04:00
committed by GitHub
parent 00db70cdc8
commit c54ba42bb9
9 changed files with 627 additions and 40 deletions

View File

@@ -28,7 +28,7 @@ function OrgWalled({
<TooltipTrigger asChild>
<div
className={cn(
"relative rounded-md border-2 border-dashed border-yellow-400 p-2",
"relative rounded-md border-2 border-dashed border-yellow-400 p-2 transition-all duration-100 ease-linear hover:border-transparent hover:p-0",
className,
)}
>

View File

@@ -15,9 +15,9 @@ function BrowserIcon({ className }: Props) {
<path
stroke="currentColor"
d="M3 10V18C3 19.1046 3.89543 20 5 20H19C20.1046 20 21 19.1046 21 18V10M3 10V6C3 4.89543 3.89543 4 5 4H19C20.1046 4 21 4.89543 21 6V10M3 10H21"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<circle cx="6" cy="7" r="1" fill="currentColor" />
<circle cx="9" cy="7" r="1" fill="currentColor" />

View File

@@ -1,18 +1,16 @@
import { ExitIcon } from "@radix-ui/react-icons";
import { useParams } from "react-router-dom";
import { useSearchParams } from "react-router-dom";
import { Handle } from "@xyflow/react";
import { Position } from "@xyflow/react";
import { KeyIcon } from "@/components/icons/KeyIcon";
import { WorkflowBlockType } from "@/routes/workflows/types/workflowTypes";
import { useToggleScriptForNodeCallback } from "@/routes/workflows/hooks/useToggleScriptForNodeCallback";
import { useWorkflowQuery } from "@/routes/workflows/hooks/useWorkflowQuery";
import { cn } from "@/util/utils";
import { CodeEditor } from "./CodeEditor";
import { workflowBlockTitle } from "../editor/nodes/types";
import { WorkflowBlockIcon } from "../editor/nodes/WorkflowBlockIcon";
import { constructCacheKeyValue } from "../editor/utils";
function BlockCodeEditor({
blockLabel,
@@ -25,19 +23,11 @@ function BlockCodeEditor({
script: string | undefined;
onClick?: (e: React.MouseEvent) => void;
}) {
const { workflowPermanentId } = useParams();
const [searchParams] = useSearchParams();
const blockTitle = workflowBlockTitle[blockType];
const toggleScriptForNodeCallback = useToggleScriptForNodeCallback();
const { data: workflow } = useWorkflowQuery({
workflowPermanentId,
});
const cacheKey = workflow?.cache_key ?? "";
const cacheKeyValue = workflow
? cacheKey === ""
? ""
: constructCacheKeyValue(cacheKey, workflow)
: "";
const cacheKeyValue = searchParams.get("cache-key-value");
return (
<div className="h-full">
@@ -81,7 +71,9 @@ function BlockCodeEditor({
<KeyIcon />
</div>
<span className="text-xs text-slate-400">
{cacheKeyValue === "" ? "(none)" : cacheKeyValue}
{cacheKeyValue === "" || !cacheKeyValue
? "(none)"
: cacheKeyValue}
</span>
</div>
</div>

View File

@@ -1,3 +1,13 @@
import {
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
CopyIcon,
PlayIcon,
ReloadIcon,
} from "@radix-ui/react-icons";
import { useEffect, useRef, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { SaveIcon } from "@/components/icons/SaveIcon";
import { BrowserIcon } from "@/components/icons/BrowserIcon";
import { Button } from "@/components/ui/button";
@@ -7,14 +17,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
ChevronDownIcon,
ChevronUpIcon,
CopyIcon,
PlayIcon,
ReloadIcon,
} from "@radix-ui/react-icons";
import { useNavigate, useParams } from "react-router-dom";
import { Input } from "@/components/ui/input";
import { useUser } from "@/hooks/useUser";
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
import { useGlobalWorkflowsQuery } from "../hooks/useGlobalWorkflowsQuery";
@@ -26,21 +29,65 @@ import { useDebugStore } from "@/store/useDebugStore";
import { useWorkflowTitleStore } from "@/store/WorkflowTitleStore";
import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore";
import { cn } from "@/util/utils";
import { WorkflowApiResponse } from "../types/workflowTypes";
import { CacheKeyValuesResponse } from "@/routes/workflows/types/scriptTypes";
import { OrgWalled } from "@/components/Orgwalled";
function CopyButton({ value }: { value: string }) {
const [copied, setCopied] = useState(false);
const handleCopy = () => {
if (copied) {
return;
}
window.navigator.clipboard.writeText(value);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
};
return (
<Button size="icon" variant="ghost" onClick={handleCopy}>
{copied ? <CheckIcon /> : <CopyIcon />}
</Button>
);
}
interface Dom {
input: React.MutableRefObject<HTMLInputElement | null>;
}
type Props = {
cacheKeyValue: string | null;
cacheKeyValues: CacheKeyValuesResponse | undefined;
cacheKeyValuesPanelOpen: boolean;
parametersPanelOpen: boolean;
saving: boolean;
workflow: WorkflowApiResponse;
onCacheKeyValueAccept: (cacheKeyValue: string | null) => void;
onCacheKeyValuesBlurred: (cacheKeyValue: string | null) => void;
onCacheKeyValuesFilter: (cacheKeyValue: string) => void;
onCacheKeyValuesKeydown: (e: React.KeyboardEvent<HTMLInputElement>) => void;
onParametersClick: () => void;
onCacheKeyValuesClick: () => void;
onSave: () => void;
onRun?: () => void;
saving: boolean;
};
function WorkflowHeader({
cacheKeyValue,
cacheKeyValues,
cacheKeyValuesPanelOpen,
parametersPanelOpen,
saving,
workflow,
onCacheKeyValueAccept,
onCacheKeyValuesBlurred,
onCacheKeyValuesFilter,
onCacheKeyValuesKeydown,
onParametersClick,
onCacheKeyValuesClick,
onSave,
onRun,
saving,
}: Props) {
const { title, setTitle } = useWorkflowTitleStore();
const workflowChangesStore = useWorkflowHasChangesStore();
@@ -53,6 +100,22 @@ function WorkflowHeader({
const workflowRunIsRunningOrQueued =
workflowRun && statusIsRunningOrQueued(workflowRun);
const user = useUser().get();
const [chosenCacheKeyValue, setChosenCacheKeyValue] = useState<string | null>(
cacheKeyValue ?? null,
);
const dom: Dom = {
input: useRef<HTMLInputElement>(null),
};
useEffect(() => {
if (cacheKeyValue === chosenCacheKeyValue) {
return;
}
setChosenCacheKeyValue(cacheKeyValue ?? null);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [cacheKeyValue]);
if (!globalWorkflows) {
return null; // this should be loaded already by some other components
@@ -81,6 +144,68 @@ function WorkflowHeader({
/>
</div>
<div className="flex h-full items-center justify-end gap-4">
{user && workflow.generate_script && (
// (cacheKeyValues?.total_count ?? 0) > 0 && (
<OrgWalled className="p-0">
<div
tabIndex={1}
className="flex max-w-[15rem] items-center justify-center gap-1 rounded-md border border-input pr-1 focus-within:ring-1 focus-within:ring-ring"
>
<Input
ref={dom.input}
className="focus-visible:transparent focus-visible:none h-[2.75rem] text-ellipsis whitespace-nowrap border-none focus-visible:outline-none focus-visible:ring-0"
onChange={(e) => {
setChosenCacheKeyValue(e.target.value);
onCacheKeyValuesFilter(e.target.value);
}}
onMouseDown={() => {
if (!cacheKeyValuesPanelOpen) {
onCacheKeyValuesClick();
}
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
const numFiltered = cacheKeyValues?.values?.length ?? 0;
if (numFiltered === 1) {
const first = cacheKeyValues?.values?.[0];
if (first) {
setChosenCacheKeyValue(first);
onCacheKeyValueAccept(first);
}
return;
}
setChosenCacheKeyValue(chosenCacheKeyValue);
onCacheKeyValueAccept(chosenCacheKeyValue);
}
onCacheKeyValuesKeydown(e);
}}
placeholder="Script Key"
value={chosenCacheKeyValue ?? undefined}
onBlur={(e) => {
onCacheKeyValuesBlurred(e.target.value);
setChosenCacheKeyValue(e.target.value);
}}
/>
{cacheKeyValuesPanelOpen ? (
<ChevronUpIcon
className="h-6 w-6 cursor-pointer"
onClick={onCacheKeyValuesClick}
/>
) : (
<ChevronDownIcon
className="h-6 w-6 cursor-pointer"
onClick={() => {
dom.input.current?.focus();
onCacheKeyValuesClick();
}}
/>
)}
<CopyButton value={chosenCacheKeyValue ?? ""} />
</div>
</OrgWalled>
)}
{isGlobalWorkflow ? (
<Button
size="lg"
@@ -112,7 +237,7 @@ function WorkflowHeader({
<Button
size="icon"
variant={debugStore.isDebugMode ? "default" : "tertiary"}
className="size-10"
className="size-10 min-w-[2.5rem]"
disabled={workflowRunIsRunningOrQueued}
onClick={() => {
if (debugStore.isDebugMode) {
@@ -143,7 +268,7 @@ function WorkflowHeader({
<Button
size="icon"
variant="tertiary"
className="size-10"
className="size-10 min-w-[2.5rem]"
disabled={isGlobalWorkflow}
onClick={() => {
onSave();

View File

@@ -2,7 +2,7 @@ import { AxiosError } from "axios";
import { useEffect, useRef, useState } from "react";
import { nanoid } from "nanoid";
import { ReloadIcon } from "@radix-ui/react-icons";
import { useParams } from "react-router-dom";
import { useParams, useSearchParams } from "react-router-dom";
import { useEdgesState, useNodesState, Edge } from "@xyflow/react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
@@ -13,6 +13,7 @@ import { useMountEffect } from "@/hooks/useMountEffect";
import { useRanker } from "../hooks/useRanker";
import { useDebugSessionQuery } from "../hooks/useDebugSessionQuery";
import { useBlockScriptsQuery } from "@/routes/workflows/hooks/useBlockScriptsQuery";
import { useCacheKeyValuesQuery } from "../hooks/useCacheKeyValuesQuery";
import { useBlockScriptStore } from "@/store/BlockScriptStore";
import { useSidebarStore } from "@/store/SidebarStore";
@@ -46,6 +47,7 @@ import { FlowRenderer, type FlowRendererProps } from "./FlowRenderer";
import { AppNode, isWorkflowBlockNode, WorkflowBlockNode } from "./nodes";
import { WorkflowNodeLibraryPanel } from "./panels/WorkflowNodeLibraryPanel";
import { WorkflowParametersPanel } from "./panels/WorkflowParametersPanel";
import { WorkflowCacheKeyValuesPanel } from "./panels/WorkflowCacheKeyValuesPanel";
import { getWorkflowErrors } from "./workflowEditorUtils";
import { WorkflowHeader } from "./WorkflowHeader";
import {
@@ -84,7 +86,13 @@ function Workspace({
workflow,
}: Props) {
const { blockLabel, workflowPermanentId, workflowRunId } = useParams();
const [searchParams, setSearchParams] = useSearchParams();
const cacheKeyValueParam = searchParams.get("cache-key-value");
const [content, setContent] = useState("actions");
const [cacheKeyValueFilter, setCacheKeyValueFilter] = useState<string | null>(
null,
);
const [page, setPage] = useState(1);
const { workflowPanelState, setWorkflowPanelState, closeWorkflowPanel } =
useWorkflowPanelStore();
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
@@ -96,7 +104,15 @@ function Workspace({
const interactor = workflowRun && isFinalized === false ? "agent" : "human";
const browserTitle = interactor === "agent" ? `Browser [🤖]` : `Browser [👤]`;
const [openDialogue, setOpenDialogue] = useState(false);
const [openCycleBrowserDialogue, setOpenCycleBrowserDialogue] =
useState(false);
const [toDeleteCacheKeyValue, setToDeleteCacheKeyValue] = useState<
string | null
>(null);
const [
openConfirmCacheKeyValueDeleteDialogue,
setOpenConfirmCacheKeyValueDeleteDialogue,
] = useState(false);
const [activeDebugSession, setActiveDebugSession] =
useState<DebugSessionApiResponse | null>(null);
const [showPowerButton, setShowPowerButton] = useState(true);
@@ -130,8 +146,28 @@ function Workspace({
// ---end fya
const cacheKey = workflow?.cache_key ?? "";
const cacheKeyValue =
cacheKey === "" ? "" : constructCacheKeyValue(cacheKey, workflow);
const [cacheKeyValue, setCacheKeyValue] = useState(
cacheKey === ""
? ""
: cacheKeyValueParam
? cacheKeyValueParam
: constructCacheKeyValue(cacheKey, workflow),
);
useEffect(() => {
if (cacheKeyValue === "") {
setSearchParams((prev) => {
const newParams = new URLSearchParams(prev);
newParams.delete("cache-key-value");
return newParams;
});
} else {
setSearchParams({
"cache-key-value": `${cacheKeyValue}`,
});
}
}, [cacheKeyValue, setSearchParams]);
const { data: blockScripts } = useBlockScriptsQuery({
cacheKey,
@@ -139,6 +175,15 @@ function Workspace({
workflowPermanentId,
});
const { data: cacheKeyValues, isLoading: cacheKeyValuesLoading } =
useCacheKeyValuesQuery({
cacheKey,
debounceMs: 100,
filter: cacheKeyValueFilter || undefined,
page,
workflowPermanentId,
});
const { data: debugSession } = useDebugSessionQuery({
workflowPermanentId,
enabled: shouldFetchDebugSession && !!workflowPermanentId,
@@ -163,7 +208,7 @@ function Workspace({
};
const handleOnCycle = () => {
setOpenDialogue(true);
setOpenCycleBrowserDialogue(true);
};
useMountEffect(() => {
@@ -175,7 +220,27 @@ function Workspace({
queryKey: ["debugSession", workflowPermanentId],
});
setShouldFetchDebugSession(true);
queryClient.invalidateQueries({
queryKey: ["cache-key-values", workflowPermanentId, cacheKey],
});
}
closeWorkflowPanel();
});
useMountEffect(() => {
const closePanelsWhenEscapeIsPressed = (e: KeyboardEvent) => {
if (e.key === "Escape") {
closeWorkflowPanel();
}
};
document.addEventListener("keydown", closePanelsWhenEscapeIsPressed);
return () => {
document.removeEventListener("keydown", closePanelsWhenEscapeIsPressed);
};
});
useEffect(() => {
@@ -184,7 +249,7 @@ function Workspace({
}, [blockScripts]);
const afterCycleBrowser = () => {
setOpenDialogue(false);
setOpenCycleBrowserDialogue(false);
setShowPowerButton(false);
if (powerButtonTimeoutRef.current) {
@@ -228,6 +293,38 @@ function Workspace({
},
});
const deleteCacheKeyValue = useMutation({
mutationFn: async ({
workflowPermanentId,
cacheKeyValue,
}: {
workflowPermanentId: string;
cacheKeyValue: string;
}) => {
const client = await getClient(credentialGetter, "sans-api-v1");
const encodedCacheKeyValue = encodeURIComponent(cacheKeyValue);
return client.delete(
`/scripts/${workflowPermanentId}/value/${encodedCacheKeyValue}`,
);
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["cache-key-values", workflowPermanentId, cacheKey],
});
setToDeleteCacheKeyValue(null);
setOpenConfirmCacheKeyValueDeleteDialogue(false);
},
onError: (error: AxiosError) => {
toast({
variant: "destructive",
title: "Failed to delete cache key value",
description: error.message,
});
setToDeleteCacheKeyValue(null);
setOpenConfirmCacheKeyValueDeleteDialogue(false);
},
});
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const powerButtonTimeoutRef = useRef<NodeJS.Timeout | null>(null);
@@ -348,15 +445,35 @@ function Workspace({
doLayout(newNodesAfter, [...editedEdges, ...newEdges]);
}
function openCacheKeyValuesPanel() {
setWorkflowPanelState({
active: true,
content: "cacheKeyValues",
});
promote("dropdown");
}
function toggleCacheKeyValuesPanel() {
if (
workflowPanelState.active &&
workflowPanelState.content === "cacheKeyValues"
) {
closeWorkflowPanel();
promote("header");
} else {
openCacheKeyValuesPanel();
}
}
return (
<div className="relative h-full w-full">
<Dialog
open={openDialogue}
open={openCycleBrowserDialogue}
onOpenChange={(open) => {
if (!open && cycleBrowser.isPending) {
return;
}
setOpenDialogue(open);
setOpenCycleBrowserDialogue(open);
}}
>
<DialogContent>
@@ -397,6 +514,63 @@ function Workspace({
</DialogContent>
</Dialog>
<Dialog
open={openConfirmCacheKeyValueDeleteDialogue}
onOpenChange={(open) => {
if (!open && deleteCacheKeyValue.isPending) {
return;
}
setOpenConfirmCacheKeyValueDeleteDialogue(open);
}}
>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete A Script Key Value</DialogTitle>
<DialogDescription>
<div className="w-full pb-2 pt-4 text-sm text-slate-400">
{deleteCacheKeyValue.isPending ? (
"Deleting script key value..."
) : (
<div className="flex w-full flex-col gap-2">
<div className="w-full">
Are you sure you want to delete this script key value?
</div>
<div
className="max-w-[29rem] overflow-hidden text-ellipsis whitespace-nowrap text-sm font-bold text-slate-400"
title={toDeleteCacheKeyValue ?? undefined}
>
{toDeleteCacheKeyValue}
</div>
</div>
)}
</div>
</DialogDescription>
</DialogHeader>
<DialogFooter>
{!deleteCacheKeyValue.isPending && (
<DialogClose asChild>
<Button variant="secondary">Cancel</Button>
</DialogClose>
)}
<Button
variant="default"
onClick={() => {
deleteCacheKeyValue.mutate({
workflowPermanentId: workflowPermanentId!,
cacheKeyValue: toDeleteCacheKeyValue!,
});
}}
disabled={deleteCacheKeyValue.isPending}
>
Yes, Continue{" "}
{deleteCacheKeyValue.isPending && (
<ReloadIcon className="ml-2 size-4 animate-spin" />
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* header panel */}
<div
className="absolute left-6 right-6 top-8 h-20"
@@ -406,11 +580,42 @@ function Workspace({
}}
>
<WorkflowHeader
cacheKeyValue={cacheKeyValue}
cacheKeyValues={cacheKeyValues}
saving={workflowChangesStore.saveIsPending}
cacheKeyValuesPanelOpen={
workflowPanelState.active &&
workflowPanelState.content === "cacheKeyValues"
}
parametersPanelOpen={
workflowPanelState.active &&
workflowPanelState.content === "parameters"
}
workflow={workflow}
onCacheKeyValueAccept={(v) => {
setCacheKeyValue(v ?? "");
setCacheKeyValueFilter("");
closeWorkflowPanel();
}}
onCacheKeyValuesBlurred={(v) => {
setCacheKeyValue(v ?? "");
}}
onCacheKeyValuesKeydown={(e) => {
if (e.key === "Enter") {
toggleCacheKeyValuesPanel();
return;
}
if (e.key !== "Tab") {
openCacheKeyValuesPanel();
}
}}
onCacheKeyValuesFilter={(v) => {
setCacheKeyValueFilter(v);
}}
onCacheKeyValuesClick={() => {
toggleCacheKeyValuesPanel();
}}
onParametersClick={() => {
if (
workflowPanelState.active &&
@@ -443,6 +648,12 @@ function Workspace({
return;
}
await saveWorkflow.mutateAsync();
queryClient.invalidateQueries({
queryKey: ["cache-key-values", workflowPermanentId, cacheKey],
});
setCacheKeyValueFilter("");
}}
onRun={() => {
closeWorkflowPanel();
@@ -466,6 +677,28 @@ function Workspace({
promote("dropdown");
}}
>
{workflowPanelState.content === "cacheKeyValues" && (
<WorkflowCacheKeyValuesPanel
cacheKeyValues={cacheKeyValues}
pending={cacheKeyValuesLoading}
scriptKey={workflow.cache_key ?? "<none>"}
onDelete={(cacheKeyValue) => {
setToDeleteCacheKeyValue(cacheKeyValue);
setOpenConfirmCacheKeyValueDeleteDialogue(true);
}}
onMouseDownCapture={() => {
promote("dropdown");
}}
onPaginate={(page) => {
setPage(page);
}}
onSelect={(cacheKeyValue) => {
setCacheKeyValue(cacheKeyValue);
setCacheKeyValueFilter("");
closeWorkflowPanel();
}}
/>
)}
{workflowPanelState.content === "parameters" && (
<WorkflowParametersPanel
onMouseDownCapture={() => {
@@ -541,7 +774,6 @@ function Workspace({
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
initialTitle={initialTitle}
// initialParameters={initialParameters}
workflow={workflow}
onMouseDownCapture={() => promote("infiniteCanvas")}
zIndex={rankedItems.infiniteCanvas}

View File

@@ -0,0 +1,165 @@
import { CrossCircledIcon, ReloadIcon } from "@radix-ui/react-icons";
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
import { Button } from "@/components/ui/button";
import { CacheKeyValuesResponse } from "@/routes/workflows/types/scriptTypes";
import { cn } from "@/util/utils";
interface Props {
cacheKeyValues: CacheKeyValuesResponse | undefined;
pending: boolean;
scriptKey: string;
onDelete: (cacheKeyValue: string) => void;
onMouseDownCapture?: () => void;
onPaginate: (page: number) => void;
onSelect: (cacheKeyValue: string) => void;
}
function WorkflowCacheKeyValuesPanel({
cacheKeyValues,
pending,
scriptKey,
onDelete,
onMouseDownCapture,
onPaginate,
onSelect,
}: Props) {
const values = cacheKeyValues?.values ?? [];
const page = cacheKeyValues?.page ?? 0;
const pageSize = cacheKeyValues?.page_size ?? 0;
const filteredCount = cacheKeyValues?.filtered_count ?? 0;
const totalCount = cacheKeyValues?.total_count ?? 0;
const totalPages = Math.ceil(filteredCount / pageSize);
const displayPage = totalPages === 0 ? 0 : page;
return (
<div
className="relative z-10 w-[44.26rem] rounded-xl border border-slate-700 bg-slate-950 p-5 shadow-xl"
onMouseDownCapture={() => onMouseDownCapture?.()}
>
<div className="space-y-4">
<header>
<h1 className="text-lg">Cached Scripts</h1>
<span className="text-sm text-slate-400">
Given your script key,{" "}
<code className="font-mono text-xs text-slate-200">
{scriptKey}
</code>
, search for a cached value to see scripts for. For this script key
there {totalCount === 1 ? "is" : "are"}{" "}
<span className="font-bold text-slate-200">{totalCount}</span>{" "}
cached {totalCount === 1 ? "value" : "values"}
{filteredCount !== totalCount && (
<>
{" "}
(
<span className="font-bold text-slate-200">
{filteredCount}
</span>{" "}
filtered)
</>
)}
.
</span>
</header>
<div className="h-[10rem] w-full overflow-hidden overflow-y-auto border-b border-slate-700 p-1">
{values.length ? (
<div className="grid w-full grid-cols-[3rem_1fr_3rem] text-sm">
{values.map((cacheKeyValue, i) => (
<div
key={cacheKeyValue}
className={cn(
"col-span-3 grid w-full cursor-pointer grid-cols-subgrid rounded-md hover:bg-slate-800",
{
"bg-slate-900 hover:bg-slate-800": i % 2 === 0,
},
)}
onClick={() => {
onSelect(cacheKeyValue);
}}
>
<div
className={cn(
"flex items-center justify-end p-1 text-slate-500",
)}
>
{i + 1 + (page - 1) * pageSize}
</div>
<div
className={cn(
"flex min-w-0 flex-1 items-center justify-start p-1 text-slate-300",
)}
title={cacheKeyValue}
>
<div className="overflow-hidden text-ellipsis whitespace-nowrap">
{cacheKeyValue}
</div>
</div>
<Button
variant="ghost"
size="icon"
className="ml-auto"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onDelete(cacheKeyValue);
}}
>
<CrossCircledIcon />
</Button>
</div>
))}
</div>
) : (
<div className="flex h-full w-full items-center justify-center">
No cached scripts found
</div>
)}
</div>
<div className="flex items-center justify-between p-1 text-slate-500">
{pending && <ReloadIcon className="size-6 animate-spin" />}
<Pagination className="justify-end pt-2">
<PaginationContent>
<PaginationItem>
<PaginationPrevious
className={cn({
"pointer-events-none opacity-50": displayPage <= 1,
})}
onClick={() => {
if (page <= 1) {
return;
}
onPaginate(page - 1);
}}
/>
</PaginationItem>
<PaginationItem>
<div className="text-sm font-bold">
{displayPage} of {totalPages}
</div>
</PaginationItem>
<PaginationItem>
<PaginationNext
className={cn({
"pointer-events-none opacity-50":
displayPage === totalPages,
})}
onClick={() => {
onPaginate(page + 1);
}}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
</div>
</div>
);
}
export { WorkflowCacheKeyValuesPanel };

View File

@@ -0,0 +1,65 @@
import { getClient } from "@/api/AxiosClient";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { useQuery } from "@tanstack/react-query";
import { keepPreviousData } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { CacheKeyValuesResponse } from "../types/scriptTypes";
type Props = {
cacheKey?: string;
filter?: string;
page: number;
workflowPermanentId?: string;
debounceMs?: number;
};
function useCacheKeyValuesQuery({
cacheKey,
filter,
page,
workflowPermanentId,
debounceMs = 300,
}: Props) {
const credentialGetter = useCredentialGetter();
// Debounce the filter parameter
const [debouncedFilter, setDebouncedFilter] = useState(filter);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedFilter(filter);
}, debounceMs);
return () => clearTimeout(timer);
}, [filter, debounceMs]);
return useQuery<CacheKeyValuesResponse>({
queryKey: [
"cache-key-values",
workflowPermanentId,
cacheKey,
page,
debouncedFilter,
],
queryFn: async () => {
const client = await getClient(credentialGetter, "sans-api-v1");
const cacheKeyEncoded = encodeURIComponent(cacheKey ?? "");
let url = `/scripts/${workflowPermanentId}/${cacheKeyEncoded}/values?page=${page}&page_size=25`;
if (debouncedFilter) {
url += `&filter=${encodeURIComponent(debouncedFilter)}`;
}
const result = await client
.get<CacheKeyValuesResponse>(url)
.then((response) => response.data);
return result;
},
enabled: !!workflowPermanentId,
placeholderData: keepPreviousData,
staleTime: 5 * 60 * 1000,
});
}
export { useCacheKeyValuesQuery };

View File

@@ -1,3 +1,11 @@
export type ScriptBlocksResponse = {
blocks: { [blockName: string]: string };
};
export type CacheKeyValuesResponse = {
filtered_count: number;
page: number;
page_size: number;
total_count: number;
values: string[];
};

View File

@@ -2,7 +2,7 @@ import { create } from "zustand";
type WorkflowPanelState = {
active: boolean;
content: "parameters" | "nodeLibrary";
content: "cacheKeyValues" | "parameters" | "nodeLibrary";
data?: {
previous?: string | null;
next?: string | null;