feat: Generic Callback Dialog Input for Custom Component (#6236)
* force dialog * Reimplement backend dialog * Update astradb.py * Clean up dropdown options * Remove unused import * [autofix.ci] apply automated fixes * Update astradb.py * Ruff fixes * Update Vector Store RAG.json * [autofix.ci] apply automated fixes * fix: Conditionally render custom option dialog in dropdown * ✨ (NodeDialogComponent/index.tsx): Add support for passing 'name' prop to NodeDialog component to improve customization and flexibility 📝 (NodeDialogComponent/index.tsx): Update comments and remove unused import to improve code readability and maintainability 🔧 (dropdownComponent/index.tsx): Pass 'name' prop to Dropdown component to enhance customization and flexibility * ✨ Refactor NodeDialog component to improve state management and payload handling * Update astradb.py * [autofix.ci] apply automated fixes * ✨ Enhance NodeDialog and Dropdown components with improved payload handling and type safety * Add DB creation functionality * First version of create * Update astradb.py * Fix ruff errors * Update Vector Store RAG.json * [autofix.ci] apply automated fixes * Update astradb.py * [autofix.ci] apply automated fixes * Update astradb.py * [autofix.ci] apply automated fixes * Update astradb.py * Update astradb.py * Update astradb.py * Update Vector Store RAG.json * [autofix.ci] apply automated fixes * Update astradb.py * [autofix.ci] apply automated fixes * feat: Enhance dropdown and node dialog with loading states and improved UX * refactor: Improve error handling in NodeDialog component * refactor: Update default excluded keys in dropdown metadata filter * [autofix.ci] apply automated fixes * refactor: Update Vector Store RAG starter project JSON with formatting and connection ID corrections * Hide fields that aren't relevant yet * [autofix.ci] apply automated fixes * Update Vector Store RAG.json * [autofix.ci] apply automated fixes * Update astradb.py * feat: Improve dropdown component with loading states and enhanced UX * Update astradb.py * [autofix.ci] apply automated fixes * Update astradb.py * Simon feedback * [autofix.ci] apply automated fixes * feat: Enhance dropdown and UI components with status indicators and loading states * refactor: Update dropdown metadata filtering to exclude 'icon' key * fix: Conditionally render dropdown icon when available * fix: Improve dropdown icon rendering with null checks * chore: Remove debug console log in dropdown component * Add support for icons in the dropdowns * Update astradb.py * Update Vector Store RAG.json * [autofix.ci] apply automated fixes * feat: Enhance dropdown status display and color handling * feat: Add auto-close functionality to node dialog and expand status color handling * feat: Add real-time template refresh for node dialog fields * refactor: Improve node dialog component state management and naming * Async for create collection * [autofix.ci] apply automated fixes * Dynamic provider list generation * Update astradb.py * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * Update astradb.py * [autofix.ci] apply automated fixes --------- Co-authored-by: Eric Hare <ericrhare@gmail.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: cristhianzl <cristhian.lousa@gmail.com>
This commit is contained in:
parent
b8d2e63221
commit
c902fb9e11
11 changed files with 1314 additions and 538 deletions
|
|
@ -8,105 +8,184 @@ import {
|
|||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { usePostTemplateValue } from "@/controllers/API/queries/nodes/use-post-template-value";
|
||||
import { getCustomParameterTitle } from "@/customization/components/custom-parameter";
|
||||
import useHandleOnNewValue from "@/CustomNodes/hooks/use-handle-new-value";
|
||||
import { mutateTemplate } from "@/CustomNodes/helpers/mutate-template";
|
||||
import useAlertStore from "@/stores/alertStore";
|
||||
import useFlowStore from "@/stores/flowStore";
|
||||
import { InputFieldType } from "@/types/api";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { APIClassType, InputFieldType } from "@/types/api";
|
||||
import { useState } from "react";
|
||||
|
||||
export const NodeDialog = ({
|
||||
open,
|
||||
onClose,
|
||||
dialogInputs,
|
||||
nodeId,
|
||||
}: {
|
||||
interface NodeDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
dialogInputs: any;
|
||||
nodeId: string;
|
||||
name: string;
|
||||
nodeClass: APIClassType;
|
||||
}
|
||||
|
||||
interface ValueObject {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const NodeDialog: React.FC<NodeDialogProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
dialogInputs,
|
||||
nodeId,
|
||||
name,
|
||||
nodeClass,
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [fieldValues, setFieldValues] = useState<Record<string, string>>({});
|
||||
|
||||
const nodes = useFlowStore((state) => state.nodes);
|
||||
const setNode = useFlowStore((state) => state.setNode);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
|
||||
const handleNewValue = (value: string, key: string) => {
|
||||
let rawValue = value;
|
||||
const postTemplateValue = usePostTemplateValue({
|
||||
parameterId: name,
|
||||
nodeId: nodeId,
|
||||
node: nodeClass,
|
||||
});
|
||||
|
||||
if (typeof value === "object" && value) {
|
||||
rawValue = (value as { value: string }).value;
|
||||
}
|
||||
const { fields, functionality: submitButtonText } = dialogInputs || {};
|
||||
const dialogNodeData = fields?.data?.node;
|
||||
const dialogTemplate = dialogNodeData?.template || {};
|
||||
|
||||
const template = cloneDeep(dialogInputs?.fields?.data?.node?.template);
|
||||
template[key].value = value;
|
||||
const setNodeClass = (newNode: APIClassType) => {
|
||||
const targetNode = nodes.find((node) => node.id === nodeId);
|
||||
if (!targetNode) return;
|
||||
|
||||
const newNode = cloneDeep(nodes.find((node) => node.id === nodeId));
|
||||
if (newNode) {
|
||||
const template = newNode.data.node.template;
|
||||
const databaseFields = template.database_name.dialog_inputs.fields;
|
||||
const nodeTemplate = databaseFields.data.node.template;
|
||||
|
||||
nodeTemplate[key].value = rawValue;
|
||||
}
|
||||
setNode(nodeId, newNode!);
|
||||
targetNode.data.node = newNode;
|
||||
setNode(nodeId, targetNode);
|
||||
};
|
||||
|
||||
const handleErrorData = (newState: {
|
||||
title: string;
|
||||
list?: Array<string>;
|
||||
}) => {
|
||||
setErrorData(newState);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const updateFieldValue = (value: string | ValueObject, fieldKey: string) => {
|
||||
const newValue = typeof value === "object" ? value.value : value;
|
||||
const targetNode = nodes.find((node) => node.id === nodeId);
|
||||
if (!targetNode || !name) return;
|
||||
|
||||
targetNode.data.node.template[name].dialog_inputs.fields.data.node.template[
|
||||
fieldKey
|
||||
].value = newValue;
|
||||
setNode(nodeId, targetNode);
|
||||
setFieldValues((prev) => ({ ...prev, [fieldKey]: newValue }));
|
||||
|
||||
if (dialogTemplate[fieldKey].real_time_refresh) {
|
||||
mutateTemplate(
|
||||
{ [fieldKey]: newValue },
|
||||
nodeClass,
|
||||
setNodeClass,
|
||||
postTemplateValue,
|
||||
handleErrorData,
|
||||
name,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseDialog = () => {
|
||||
setFieldValues({});
|
||||
const targetNode = nodes.find((node) => node.id === nodeId);
|
||||
if (targetNode && name) {
|
||||
const nodeTemplate = targetNode.data.node.template;
|
||||
Object.keys(dialogTemplate).forEach((key) => {
|
||||
nodeTemplate[name].dialog_inputs.fields.data.node.template[key].value =
|
||||
"";
|
||||
});
|
||||
setNode(nodeId, targetNode);
|
||||
}
|
||||
setIsLoading(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleSubmitDialog = async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
await mutateTemplate(
|
||||
fieldValues,
|
||||
nodeClass,
|
||||
setNodeClass,
|
||||
postTemplateValue,
|
||||
handleErrorData,
|
||||
name,
|
||||
handleCloseDialog,
|
||||
nodeClass.tool_mode,
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
handleCloseDialog();
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
// Render
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<Dialog open={open} onOpenChange={handleCloseDialog}>
|
||||
<DialogContent className="max-w-[700px] gap-2 px-1 py-6">
|
||||
<DialogHeader className="px-5 pb-3">
|
||||
<DialogTitle>
|
||||
<div className="flex items-center">
|
||||
<span className="pb-2">
|
||||
{dialogInputs.fields?.data?.node?.display_name}
|
||||
</span>
|
||||
<span className="pb-2">{dialogNodeData?.display_name}</span>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div className="flex items-center gap-2">
|
||||
{dialogInputs.fields?.data?.node?.description}
|
||||
{dialogNodeData?.description}
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-5 overflow-y-auto px-5">
|
||||
{Object.entries(dialogInputs?.fields?.data?.node?.template ?? {}).map(
|
||||
([key, value]) => (
|
||||
<div key={key}>
|
||||
<div>
|
||||
{getCustomParameterTitle({
|
||||
title:
|
||||
dialogInputs?.fields?.data?.node?.template[key]
|
||||
.display_name ?? "",
|
||||
nodeId,
|
||||
isFlexView: false,
|
||||
})}
|
||||
</div>
|
||||
<ParameterRenderComponent
|
||||
handleOnNewValue={(value: string) =>
|
||||
handleNewValue(value, key)
|
||||
}
|
||||
name={key}
|
||||
nodeId={nodeId}
|
||||
templateData={value as Partial<InputFieldType>}
|
||||
templateValue={
|
||||
dialogInputs?.fields?.data?.node?.template[key].value
|
||||
}
|
||||
editNode={false}
|
||||
handleNodeClass={() => {}}
|
||||
nodeClass={dialogInputs.fields?.data?.node}
|
||||
disabled={false}
|
||||
placeholder=""
|
||||
isToolMode={false}
|
||||
/>
|
||||
{Object.entries(dialogTemplate).map(([fieldKey, fieldValue]) => (
|
||||
<div key={fieldKey}>
|
||||
<div>
|
||||
{getCustomParameterTitle({
|
||||
title:
|
||||
(fieldValue as { display_name: string })?.display_name ??
|
||||
"",
|
||||
nodeId,
|
||||
isFlexView: false,
|
||||
})}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
<ParameterRenderComponent
|
||||
handleOnNewValue={(value: string) =>
|
||||
updateFieldValue(value, fieldKey)
|
||||
}
|
||||
name={fieldKey}
|
||||
nodeId={nodeId}
|
||||
templateData={fieldValue as Partial<InputFieldType>}
|
||||
templateValue={fieldValues[fieldKey] || ""}
|
||||
editNode={false}
|
||||
handleNodeClass={() => {}}
|
||||
nodeClass={dialogNodeData}
|
||||
disabled={false}
|
||||
placeholder=""
|
||||
isToolMode={false}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="px-5 pt-3">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
<Button variant="secondary" onClick={handleCloseDialog}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button>{dialogInputs.functionality}</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={handleSubmitDialog}
|
||||
loading={isLoading}
|
||||
>
|
||||
{submitButtonText}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import ForwardedIconComponent from "../genericIconComponent";
|
||||
|
||||
const FetchIconComponent = ({
|
||||
source,
|
||||
name,
|
||||
}: {
|
||||
source: string;
|
||||
name: string;
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
{source ? (
|
||||
<img src={source} alt={name} />
|
||||
) : (
|
||||
<ForwardedIconComponent name="Unknown" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FetchIconComponent;
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
const LoadingTextComponent = ({ text }: { text: string }) => {
|
||||
const [dots, setDots] = useState(".");
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setDots((prevDots) => (prevDots === "..." ? "" : `${prevDots}.`));
|
||||
}, 300);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <span>{`${text}${dots}`}</span>;
|
||||
};
|
||||
|
||||
export default LoadingTextComponent;
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
import LoadingTextComponent from "@/components/common/loadingTextComponent";
|
||||
import { usePostTemplateValue } from "@/controllers/API/queries/nodes/use-post-template-value";
|
||||
import NodeDialog from "@/CustomNodes/GenericNode/components/NodeDialogComponent";
|
||||
import { mutateTemplate } from "@/CustomNodes/helpers/mutate-template";
|
||||
import useAlertStore from "@/stores/alertStore";
|
||||
import { getStatusColor } from "@/utils/stringManipulation";
|
||||
import { PopoverAnchor } from "@radix-ui/react-popover";
|
||||
import Fuse from "fuse.js";
|
||||
import { cloneDeep } from "lodash";
|
||||
|
|
@ -13,7 +15,6 @@ import ShadTooltip from "../../common/shadTooltipComponent";
|
|||
import { Button } from "../../ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
|
|
@ -42,25 +43,41 @@ export default function Dropdown({
|
|||
dialogInputs,
|
||||
...baseInputProps
|
||||
}: BaseInputProps & DropDownComponent): JSX.Element {
|
||||
const nodeId = baseInputProps?.nodeId;
|
||||
// Initialize state and refs
|
||||
const [open, setOpen] = useState(children ? true : false);
|
||||
const [openDialog, setOpenDialog] = useState(false);
|
||||
const [customValue, setCustomValue] = useState("");
|
||||
const [filteredOptions, setFilteredOptions] = useState(options);
|
||||
const [refreshOptions, setRefreshOptions] = useState(false);
|
||||
const refButton = useRef<HTMLButtonElement>(null);
|
||||
|
||||
// Initialize utilities and constants
|
||||
const placeholderName = name
|
||||
? formatPlaceholderName(name)
|
||||
: "Choose an option...";
|
||||
|
||||
const { firstWord } = formatName(name);
|
||||
const [open, setOpen] = useState(children ? true : false);
|
||||
const [openDialog, setOpenDialog] = useState(false);
|
||||
|
||||
const refButton = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const fuse = new Fuse(options, { keys: ["name", "value"] });
|
||||
const PopoverContentDropdown =
|
||||
children || editNode ? PopoverContent : PopoverContentWithoutPortal;
|
||||
const { nodeClass, nodeId, handleNodeClass, tooltip } = baseInputProps;
|
||||
|
||||
const [customValue, setCustomValue] = useState("");
|
||||
const [filteredOptions, setFilteredOptions] = useState(options);
|
||||
// API and store hooks
|
||||
const postTemplateValue = usePostTemplateValue({
|
||||
parameterId: name || "",
|
||||
nodeId: nodeId || "",
|
||||
node: nodeClass!,
|
||||
});
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
|
||||
const fuse = new Fuse(options, { keys: ["name", "value"] });
|
||||
// Utility functions
|
||||
const filterMetadataKeys = (
|
||||
metadata: Record<string, any> = {},
|
||||
excludeKeys: string[] = ["api_endpoint", "icon", "status"],
|
||||
) => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(metadata).filter(([key]) => !excludeKeys.includes(key)),
|
||||
);
|
||||
};
|
||||
|
||||
const searchRoleByTerm = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value;
|
||||
|
|
@ -71,28 +88,43 @@ export default function Dropdown({
|
|||
setCustomValue(value);
|
||||
};
|
||||
|
||||
const { nodeClass, handleNodeClass } = baseInputProps;
|
||||
const handleRefreshButtonPress = async () => {
|
||||
setRefreshOptions(true);
|
||||
setOpen(false);
|
||||
|
||||
const postTemplateValue = usePostTemplateValue({
|
||||
parameterId: name || "",
|
||||
nodeId: id,
|
||||
node: nodeClass!,
|
||||
});
|
||||
|
||||
const { isPending } = postTemplateValue;
|
||||
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
|
||||
const handleRefreshButtonPress = () => {
|
||||
mutateTemplate(
|
||||
await mutateTemplate(
|
||||
value,
|
||||
nodeClass!,
|
||||
handleNodeClass,
|
||||
postTemplateValue,
|
||||
setErrorData,
|
||||
);
|
||||
)?.then(() => {
|
||||
setTimeout(() => {
|
||||
setRefreshOptions(false);
|
||||
}, 2000);
|
||||
});
|
||||
};
|
||||
|
||||
const formatTooltipContent = (option: string, index: number) => {
|
||||
if (!optionsMetaData?.[index]) return option;
|
||||
|
||||
const metadata = optionsMetaData[index];
|
||||
const metadataEntries = Object.entries(metadata)
|
||||
.filter(([key, value]) => value !== null && key !== "icon")
|
||||
.map(([key, value]) => {
|
||||
const displayValue =
|
||||
typeof value === "string" && value.length > 20
|
||||
? `${value.substring(0, 30)}...`
|
||||
: String(value);
|
||||
return `${key}: ${displayValue}`;
|
||||
});
|
||||
|
||||
return metadataEntries.length > 0
|
||||
? `${firstWord}: ${option}\n${metadataEntries.join("\n")}`
|
||||
: option;
|
||||
};
|
||||
|
||||
// Effects
|
||||
useEffect(() => {
|
||||
if (disabled && value !== "") {
|
||||
onSelect("", undefined, true);
|
||||
|
|
@ -109,61 +141,71 @@ export default function Dropdown({
|
|||
}
|
||||
}, [open]);
|
||||
|
||||
// Render helper functions
|
||||
|
||||
const renderLoadingButton = () => (
|
||||
<Button
|
||||
className="dropdown-component-false-outline w-full justify-between py-2 font-normal"
|
||||
variant="primary"
|
||||
size="xs"
|
||||
>
|
||||
<LoadingTextComponent text="Loading options" />
|
||||
</Button>
|
||||
);
|
||||
|
||||
const renderTriggerButton = () => (
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
disabled={
|
||||
disabled ||
|
||||
(Object.keys(options).length === 0 &&
|
||||
!combobox &&
|
||||
!dialogInputs?.fields?.data?.node?.template)
|
||||
}
|
||||
variant="primary"
|
||||
size="xs"
|
||||
role="combobox"
|
||||
ref={refButton}
|
||||
aria-expanded={open}
|
||||
data-testid={id}
|
||||
className={cn(
|
||||
editNode
|
||||
? "dropdown-component-outline input-edit-node"
|
||||
: "dropdown-component-false-outline py-2",
|
||||
"w-full justify-between font-normal",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className="flex items-center gap-2 truncate"
|
||||
data-testid={`value-dropdown-${id}`}
|
||||
>
|
||||
{optionsMetaData?.[
|
||||
filteredOptions.findIndex((option) => option === value)
|
||||
]?.icon && (
|
||||
<ForwardedIconComponent
|
||||
name={
|
||||
optionsMetaData[
|
||||
filteredOptions.findIndex((option) => option === value)
|
||||
].icon
|
||||
}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
)}
|
||||
{value &&
|
||||
value !== "" &&
|
||||
filteredOptions.find((option) => option === value)
|
||||
? filteredOptions.find((option) => option === value)
|
||||
: placeholderName}
|
||||
</span>
|
||||
<ForwardedIconComponent
|
||||
name="ChevronsUpDown"
|
||||
<ShadTooltip content={!value ? (tooltip as string) : ""}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
disabled={
|
||||
disabled ||
|
||||
(Object.keys(options).length === 0 &&
|
||||
!combobox &&
|
||||
!dialogInputs?.fields?.data?.node?.template)
|
||||
}
|
||||
variant="primary"
|
||||
size="xs"
|
||||
role="combobox"
|
||||
ref={refButton}
|
||||
aria-expanded={open}
|
||||
data-testid={id}
|
||||
className={cn(
|
||||
"ml-2 h-4 w-4 shrink-0 text-foreground",
|
||||
disabled
|
||||
? "hover:text-placeholder-foreground"
|
||||
: "hover:text-foreground",
|
||||
editNode
|
||||
? "dropdown-component-outline input-edit-node"
|
||||
: "dropdown-component-false-outline py-2",
|
||||
"w-full justify-between font-normal",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
>
|
||||
<span
|
||||
className="flex items-center gap-2 truncate"
|
||||
data-testid={`value-dropdown-${id}`}
|
||||
>
|
||||
{optionsMetaData?.[
|
||||
filteredOptions.findIndex((option) => option === value)
|
||||
]?.icon && (
|
||||
<ForwardedIconComponent
|
||||
name={
|
||||
optionsMetaData?.[
|
||||
filteredOptions.findIndex((option) => option === value)
|
||||
]?.icon
|
||||
}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
)}
|
||||
{value && filteredOptions.includes(value) ? value : placeholderName}{" "}
|
||||
</span>
|
||||
<ForwardedIconComponent
|
||||
name="ChevronsUpDown"
|
||||
className={cn(
|
||||
"ml-2 h-4 w-4 shrink-0 text-foreground",
|
||||
disabled
|
||||
? "hover:text-placeholder-foreground"
|
||||
: "hover:text-foreground",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
</ShadTooltip>
|
||||
);
|
||||
|
||||
const renderSearchInput = () => (
|
||||
|
|
@ -211,10 +253,7 @@ export default function Dropdown({
|
|||
<div className="flex items-center gap-2 pl-1">
|
||||
<ForwardedIconComponent
|
||||
name="RefreshCcw"
|
||||
className={cn(
|
||||
"h-3 w-3 text-primary",
|
||||
isPending && "animate-spin",
|
||||
)}
|
||||
className={cn("refresh-icon h-3 w-3 text-primary")}
|
||||
/>
|
||||
Refresh list
|
||||
</div>
|
||||
|
|
@ -223,87 +262,118 @@ export default function Dropdown({
|
|||
<NodeDialog
|
||||
open={openDialog}
|
||||
dialogInputs={dialogInputs}
|
||||
onClose={() => setOpenDialog(false)}
|
||||
onClose={() => {
|
||||
setOpenDialog(false);
|
||||
setOpen(false);
|
||||
}}
|
||||
nodeId={nodeId!}
|
||||
name={name!}
|
||||
nodeClass={nodeClass!}
|
||||
/>
|
||||
</CommandGroup>
|
||||
);
|
||||
|
||||
const renderOptionsList = () => (
|
||||
<CommandList>
|
||||
<CommandEmpty>No values found.</CommandEmpty>
|
||||
<CommandGroup defaultChecked={false}>
|
||||
{filteredOptions?.map((option, index) => (
|
||||
<ShadTooltip key={index} delayDuration={700} content={option}>
|
||||
<div>
|
||||
<CommandItem
|
||||
value={option}
|
||||
onSelect={(currentValue) => {
|
||||
onSelect(currentValue);
|
||||
setOpen(false);
|
||||
}}
|
||||
className="items-center"
|
||||
data-testid={`${option}-${index}-option`}
|
||||
>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
{optionsMetaData?.[index]?.icon ? (
|
||||
<ForwardedIconComponent
|
||||
name={optionsMetaData?.[index]?.icon}
|
||||
className="h-4 w-4 shrink-0 text-primary"
|
||||
/>
|
||||
) : null}
|
||||
<div
|
||||
className={cn("flex truncate", {
|
||||
"flex-col":
|
||||
optionsMetaData && optionsMetaData?.length > 0,
|
||||
"w-full pl-2": !optionsMetaData?.[index]?.icon,
|
||||
})}
|
||||
>
|
||||
<div className="flex truncate">{option}</div>
|
||||
{optionsMetaData && optionsMetaData?.length > 0 ? (
|
||||
<div className="flex w-full items-center text-muted-foreground">
|
||||
{Object.entries(optionsMetaData?.[index] || {})
|
||||
.filter(
|
||||
([key, value]) => value !== null && key !== "icon",
|
||||
{filteredOptions?.length > 0 ? (
|
||||
filteredOptions?.map((option, index) => (
|
||||
<ShadTooltip
|
||||
key={index}
|
||||
delayDuration={700}
|
||||
styleClasses="whitespace-pre-wrap"
|
||||
content={formatTooltipContent(option, index)}
|
||||
>
|
||||
<div>
|
||||
<CommandItem
|
||||
value={option}
|
||||
onSelect={(currentValue) => {
|
||||
onSelect(currentValue);
|
||||
setOpen(false);
|
||||
}}
|
||||
className="items-center"
|
||||
data-testid={`${option}-${index}-option`}
|
||||
>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
{optionsMetaData && optionsMetaData.length > 0 && (
|
||||
<ForwardedIconComponent
|
||||
name={optionsMetaData?.[index]?.icon || "Unknown"}
|
||||
className="h-4 w-4 shrink-0 text-primary"
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={cn("flex truncate", {
|
||||
"flex-col":
|
||||
optionsMetaData && optionsMetaData?.length > 0,
|
||||
"w-full pl-2": !optionsMetaData?.[index]?.icon,
|
||||
})}
|
||||
>
|
||||
<div className="flex truncate">
|
||||
{option}{" "}
|
||||
<span
|
||||
className={`flex items-center pl-2 text-xs ${getStatusColor(
|
||||
optionsMetaData?.[index]?.status,
|
||||
)}`}
|
||||
>
|
||||
<LoadingTextComponent
|
||||
text={optionsMetaData?.[
|
||||
index
|
||||
]?.status?.toLowerCase()}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{optionsMetaData && optionsMetaData?.length > 0 ? (
|
||||
<div className="flex w-full items-center text-muted-foreground">
|
||||
{Object.entries(
|
||||
filterMetadataKeys(optionsMetaData?.[index] || {}),
|
||||
)
|
||||
.map(([key, value], i, arr) => (
|
||||
<div
|
||||
key={key}
|
||||
className={cn("flex items-center", {
|
||||
truncate: i === arr.length - 1,
|
||||
})}
|
||||
>
|
||||
{i > 0 && (
|
||||
<ForwardedIconComponent
|
||||
name="Circle"
|
||||
className="mx-1 h-1 w-1 overflow-visible fill-muted-foreground"
|
||||
/>
|
||||
)}
|
||||
.filter(
|
||||
([key, value]) =>
|
||||
value !== null && key !== "icon",
|
||||
)
|
||||
.map(([key, value], i, arr) => (
|
||||
<div
|
||||
className={cn("text-xs", {
|
||||
key={key}
|
||||
className={cn("flex items-center", {
|
||||
truncate: i === arr.length - 1,
|
||||
})}
|
||||
>{`${String(value)} ${key}`}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="ml-auto flex">
|
||||
<ForwardedIconComponent
|
||||
name="Check"
|
||||
className={cn(
|
||||
"h-4 w-4 shrink-0 text-primary",
|
||||
value === option ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{i > 0 && (
|
||||
<ForwardedIconComponent
|
||||
name="Circle"
|
||||
className="mx-1 h-1 w-1 overflow-visible fill-muted-foreground"
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={cn("text-xs", {
|
||||
truncate: i === arr.length - 1,
|
||||
})}
|
||||
>{`${String(value)} ${key}`}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="ml-auto flex">
|
||||
<ForwardedIconComponent
|
||||
name="Check"
|
||||
className={cn(
|
||||
"h-4 w-4 shrink-0 text-primary",
|
||||
value === option ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CommandItem>
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
))}
|
||||
</CommandItem>
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
))
|
||||
) : (
|
||||
<CommandItem disabled className="text-center text-sm">
|
||||
No options found
|
||||
</CommandItem>
|
||||
)}
|
||||
</CommandGroup>
|
||||
<CommandSeparator />
|
||||
{dialogInputs && dialogInputs?.fields && renderCustomOptionDialog()}
|
||||
|
|
@ -320,12 +390,13 @@ export default function Dropdown({
|
|||
}
|
||||
>
|
||||
<Command>
|
||||
{renderSearchInput()}
|
||||
{filteredOptions?.length > 0 && renderSearchInput()}
|
||||
{renderOptionsList()}
|
||||
</Command>
|
||||
</PopoverContentDropdown>
|
||||
);
|
||||
|
||||
// Loading state
|
||||
if (Object.keys(options).length === 0 && !combobox && isLoading) {
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -334,10 +405,13 @@ export default function Dropdown({
|
|||
);
|
||||
}
|
||||
|
||||
// Main render
|
||||
return (
|
||||
<Popover open={open} onOpenChange={children ? () => {} : setOpen}>
|
||||
{children ? (
|
||||
<PopoverAnchor>{children}</PopoverAnchor>
|
||||
) : refreshOptions || isLoading ? (
|
||||
renderLoadingButton()
|
||||
) : (
|
||||
renderTriggerButton()
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export type BaseInputProps<valueType = any> = {
|
|||
readonly?: boolean;
|
||||
placeholder?: string;
|
||||
isToolMode?: boolean;
|
||||
tooltip?: string;
|
||||
metadata?: any;
|
||||
nodeId?: string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -196,6 +196,8 @@
|
|||
--placeholder-foreground: 240 4% 46%; /* hsl(240, 4%, 46%) */
|
||||
--canvas: 0 0% 0%; /* hsl(0, 0%, 0%) */
|
||||
--canvas-dot: 240 5.3% 26.1%; /* hsl(240, 5.3%, 26.1%) */
|
||||
--accent-amber: 26 90% 37%; /* hsl(26, 90%, 37%) */
|
||||
--accent-amber-foreground: 26 90% 37%; /* hsl(26, 90%, 37%) */
|
||||
--accent-emerald: 164 86% 16%; /* hsl(164, 86%, 16%) */
|
||||
--accent-emerald-foreground: 158 64% 52%; /* hsl(158, 64%, 52%) */
|
||||
--accent-emerald-hover: 163.1 88.1% 19.8%; /* hsl(163.1, 88.1%, 19.8%) */
|
||||
|
|
|
|||
|
|
@ -113,3 +113,24 @@ export function parseString(
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
export const getStatusColor = (status: string): string => {
|
||||
const amberStatuses = [
|
||||
"initializing",
|
||||
"pending",
|
||||
"hibernating",
|
||||
"hiberated",
|
||||
"maintenance",
|
||||
"parked",
|
||||
];
|
||||
|
||||
if (amberStatuses.includes(status?.toLowerCase())) {
|
||||
return "text-accent-amber-foreground";
|
||||
}
|
||||
|
||||
if (status?.toLowerCase() === "terminating") {
|
||||
return "red-500";
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
|
|
|||
|
|
@ -741,7 +741,7 @@ export const formatName = (name) => {
|
|||
.join(" ");
|
||||
|
||||
const firstWord =
|
||||
formattedName.split(" ")[0].charAt(0).toUpperCase() +
|
||||
formattedName.split(" ")[0].charAt(0) +
|
||||
formattedName.split(" ")[0].slice(1);
|
||||
|
||||
return { formattedName, firstWord };
|
||||
|
|
|
|||
|
|
@ -175,6 +175,10 @@ const config = {
|
|||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
"accent-amber": {
|
||||
DEFAULT: "hsl(var(--accent-amber))",
|
||||
foreground: "hsl(var(--accent-amber-foreground))",
|
||||
},
|
||||
"accent-emerald": {
|
||||
DEFAULT: "hsl(var(--accent-emerald))",
|
||||
foreground: "hsl(var(--accent-emerald-foreground))",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue