Add Panel For Cache Key Value Selection And Deletion (#3266)
This commit is contained in:
@@ -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,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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[];
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user