merge fix

This commit is contained in:
cristhianzl 2024-06-06 13:34:01 -03:00
commit 3a6cd8822c
44 changed files with 1071 additions and 1102 deletions

View file

@ -9,7 +9,7 @@ The `NotionUserList` component retrieves users from Notion. It provides a conven
[Notion Reference](https://developers.notion.com/reference/get-users) [Notion Reference](https://developers.notion.com/reference/get-users)
The `NotionUserList` component enables you to: The `NotionUserList` component enables you to:
- Retrieve user data from Notion - Retrieve user data from Notion
- Access user information such as ID, type, name, and avatar URL - Access user information such as ID, type, name, and avatar URL

View file

@ -55,6 +55,7 @@ class ApiKeyRead(ApiKeyBase):
id: UUID id: UUID
api_key: str = Field(schema_extra={"validate_default": True}) api_key: str = Field(schema_extra={"validate_default": True})
user_id: UUID = Field() user_id: UUID = Field()
created_at: datetime = Field()
@field_validator("api_key") @field_validator("api_key")
@classmethod @classmethod

View file

@ -44,6 +44,7 @@
"debounce-promise": "^3.1.2", "debounce-promise": "^3.1.2",
"dompurify": "^3.0.5", "dompurify": "^3.0.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"emoji-regex": "^10.3.0",
"esbuild": "^0.17.19", "esbuild": "^0.17.19",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"framer-motion": "^11.0.6", "framer-motion": "^11.0.6",
@ -479,6 +480,7 @@
}, },
"node_modules/@clack/prompts/node_modules/is-unicode-supported": { "node_modules/@clack/prompts/node_modules/is-unicode-supported": {
"version": "1.3.0", "version": "1.3.0",
"extraneous": true,
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -5997,9 +5999,9 @@
"integrity": "sha512-C6q/xcUJf/2yODRxAVCfIk4j3y3LMsD0ehiE2RQNV2cxc8XU62gR6vvYh3+etSUzlgTfil+qDHI1vubpdf0TOA==" "integrity": "sha512-C6q/xcUJf/2yODRxAVCfIk4j3y3LMsD0ehiE2RQNV2cxc8XU62gR6vvYh3+etSUzlgTfil+qDHI1vubpdf0TOA=="
}, },
"node_modules/emoji-regex": { "node_modules/emoji-regex": {
"version": "8.0.0", "version": "10.3.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw=="
}, },
"node_modules/end-of-stream": { "node_modules/end-of-stream": {
"version": "1.4.4", "version": "1.4.4",
@ -12204,6 +12206,16 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/string-width-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/string-width/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/strip-ansi": { "node_modules/strip-ansi": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",

View file

@ -39,6 +39,7 @@
"debounce-promise": "^3.1.2", "debounce-promise": "^3.1.2",
"dompurify": "^3.0.5", "dompurify": "^3.0.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"emoji-regex": "^10.3.0",
"esbuild": "^0.17.19", "esbuild": "^0.17.19",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"framer-motion": "^11.0.6", "framer-motion": "^11.0.6",

View file

@ -164,3 +164,13 @@ body {
.ag-body-vertical-scroll-viewport::-webkit-scrollbar-thumb:hover { .ag-body-vertical-scroll-viewport::-webkit-scrollbar-thumb:hover {
background-color: #bbb; background-color: #bbb;
} }
/* This CSS is to not apply the border for the column having 'no-border' class */
.no-border.ag-cell:focus {
border: none !important;
outline: none;
}
.no-border.ag-cell {
border: none !important;
outline: none;
}

View file

@ -33,9 +33,8 @@ export default function Dropdown({
const refButton = useRef<HTMLButtonElement>(null); const refButton = useRef<HTMLButtonElement>(null);
const PopoverContentDropdown = children const PopoverContentDropdown =
? PopoverContent children || editNode ? PopoverContent : PopoverContentWithoutPortal;
: PopoverContentWithoutPortal;
return ( return (
<> <>

View file

@ -181,18 +181,6 @@ export default function Header(): JSX.Element {
/> />
</div> </div>
</AlertDropdown> </AlertDropdown>
{autoLogin && (
<button
onClick={() => {
navigate("/account/api-keys");
}}
>
<IconComponent
name="Key"
className="side-bar-button-size text-muted-foreground hover:text-accent-foreground"
/>
</button>
)}
<> <>
<Separator orientation="vertical" /> <Separator orientation="vertical" />

View file

@ -10,7 +10,11 @@ import {
CommandList, CommandList,
} from "../../../ui/command"; } from "../../../ui/command";
import { Input } from "../../../ui/input"; import { Input } from "../../../ui/input";
import { Popover, PopoverContentWithoutPortal } from "../../../ui/popover"; import {
Popover,
PopoverContent,
PopoverContentWithoutPortal,
} from "../../../ui/popover";
const CustomInputPopover = ({ const CustomInputPopover = ({
id, id,
refInput, refInput,
@ -39,6 +43,9 @@ const CustomInputPopover = ({
showOptions, showOptions,
}) => { }) => {
const setErrorData = useAlertStore.getState().setErrorData; const setErrorData = useAlertStore.getState().setErrorData;
const PopoverContentInput = editNode
? PopoverContent
: PopoverContentWithoutPortal;
const handleInputChange = (e) => { const handleInputChange = (e) => {
if (password) { if (password) {
@ -107,7 +114,7 @@ const CustomInputPopover = ({
data-testid={editNode ? id + "-edit" : id} data-testid={editNode ? id + "-edit" : id}
/> />
</PopoverAnchor> </PopoverAnchor>
<PopoverContentWithoutPortal <PopoverContentInput
className="nocopy nowheel nopan nodelete nodrag noundo p-0" className="nocopy nowheel nopan nodelete nodrag noundo p-0"
style={{ minWidth: refInput?.current?.clientWidth ?? "200px" }} style={{ minWidth: refInput?.current?.clientWidth ?? "200px" }}
side="bottom" side="bottom"
@ -184,7 +191,7 @@ const CustomInputPopover = ({
</CommandGroup> </CommandGroup>
</CommandList> </CommandList>
</Command> </Command>
</PopoverContentWithoutPortal> </PopoverContentInput>
</Popover> </Popover>
); );
}; };

View file

@ -9,7 +9,11 @@ import {
CommandList, CommandList,
} from "../../../ui/command"; } from "../../../ui/command";
import { Input } from "../../../ui/input"; import { Input } from "../../../ui/input";
import { Popover, PopoverContentWithoutPortal } from "../../../ui/popover"; import {
Popover,
PopoverContent,
PopoverContentWithoutPortal,
} from "../../../ui/popover";
const CustomInputPopoverObject = ({ const CustomInputPopoverObject = ({
id, id,
refInput, refInput,
@ -23,6 +27,7 @@ const CustomInputPopoverObject = ({
disabled, disabled,
setShowOptions, setShowOptions,
required, required,
editNode,
className, className,
placeholder, placeholder,
onChange, onChange,
@ -34,6 +39,10 @@ const CustomInputPopoverObject = ({
handleKeyDown, handleKeyDown,
showOptions, showOptions,
}) => { }) => {
const PopoverContentInput = editNode
? PopoverContent
: PopoverContentWithoutPortal;
const handleInputChange = (e) => { const handleInputChange = (e) => {
onChange && onChange(e.target.value); onChange && onChange(e.target.value);
}; };
@ -79,7 +88,7 @@ const CustomInputPopoverObject = ({
data-testid={id} data-testid={id}
/> />
</PopoverAnchor> </PopoverAnchor>
<PopoverContentWithoutPortal <PopoverContentInput
className="nocopy nowheel nopan nodelete nodrag noundo p-0" className="nocopy nowheel nopan nodelete nodrag noundo p-0"
style={{ minWidth: refInput?.current?.clientWidth ?? "200px" }} style={{ minWidth: refInput?.current?.clientWidth ?? "200px" }}
side="bottom" side="bottom"
@ -159,7 +168,7 @@ const CustomInputPopoverObject = ({
</CommandGroup> </CommandGroup>
</CommandList> </CommandList>
</Command> </Command>
</PopoverContentWithoutPortal> </PopoverContentInput>
</Popover> </Popover>
); );
}; };

View file

@ -108,6 +108,7 @@ export default function InputComponent({
setSelectedOptions={setSelectedOptions} setSelectedOptions={setSelectedOptions}
options={objectOptions} options={objectOptions}
value={value} value={value}
editNode={editNode}
autoFocus={autoFocus} autoFocus={autoFocus}
disabled={disabled} disabled={disabled}
setShowOptions={setShowOptions} setShowOptions={setShowOptions}

View file

@ -32,11 +32,11 @@ export default function InputGlobalComponent({
const setErrorData = useAlertStore((state) => state.setErrorData); const setErrorData = useAlertStore((state) => state.setErrorData);
useEffect(() => { useEffect(() => {
if (data.node?.template[name]) if (data)
if ( if (
globalVariablesEntries && globalVariablesEntries &&
!globalVariablesEntries.includes(data.node?.template[name].value) && !globalVariablesEntries.includes(data.value) &&
data.node?.template[name].load_from_db data.load_from_db
) { ) {
setTimeout(() => { setTimeout(() => {
onChange("", true); onChange("", true);
@ -46,17 +46,11 @@ export default function InputGlobalComponent({
}, [globalVariablesEntries]); }, [globalVariablesEntries]);
useEffect(() => { useEffect(() => {
if ( if (!data.value && data.display_name) {
!data.node?.template[name].value && if (unavaliableFields[data.display_name!] && !disabled) {
data.node?.template[name].display_name
) {
if (
unavaliableFields[data.node?.template[name].display_name!] &&
!disabled
) {
setTimeout(() => { setTimeout(() => {
setDb(true); setDb(true);
onChange(unavaliableFields[data.node?.template[name].display_name!]); onChange(unavaliableFields[data.display_name!]);
}, 100); }, 100);
} }
} }
@ -68,10 +62,7 @@ export default function InputGlobalComponent({
await deleteGlobalVariable(id) await deleteGlobalVariable(id)
.then(() => { .then(() => {
removeGlobalVariable(key); removeGlobalVariable(key);
if ( if (data?.value === key && data?.load_from_db) {
data?.node?.template[name].value === key &&
data?.node?.template[name].load_from_db
) {
onChange(""); onChange("");
setDb(false); setDb(false);
} }
@ -94,8 +85,8 @@ export default function InputGlobalComponent({
id={"input-" + name} id={"input-" + name}
editNode={editNode} editNode={editNode}
disabled={disabled} disabled={disabled}
password={data.node?.template[name].password ?? false} password={data.password ?? false}
value={data.node?.template[name].value ?? ""} value={data.value ?? ""}
options={globalVariablesEntries} options={globalVariablesEntries}
optionsPlaceholder={"Global Variables"} optionsPlaceholder={"Global Variables"}
optionsIcon="Globe" optionsIcon="Globe"
@ -138,10 +129,10 @@ export default function InputGlobalComponent({
</DeleteConfirmationModal> </DeleteConfirmationModal>
)} )}
selectedOption={ selectedOption={
data?.node?.template[name].load_from_db && data?.load_from_db &&
globalVariablesEntries && globalVariablesEntries &&
globalVariablesEntries.includes(data?.node?.template[name].value ?? "") globalVariablesEntries.includes(data?.value ?? "")
? data?.node?.template[name].value ? data?.value
: "" : ""
} }
setSelectedOption={(value) => { setSelectedOption={(value) => {

View file

@ -11,7 +11,7 @@ export default function ShadTooltip({
delayDuration = 500, delayDuration = 500,
}: ShadToolTipType): JSX.Element { }: ShadToolTipType): JSX.Element {
return ( return (
<Tooltip delayDuration={delayDuration}> <Tooltip defaultOpen={!children} delayDuration={delayDuration}>
<TooltipTrigger asChild={asChild}>{children}</TooltipTrigger> <TooltipTrigger asChild={asChild}>{children}</TooltipTrigger>
<TooltipContent <TooltipContent
className={cn(styleClasses, "max-w-96")} className={cn(styleClasses, "max-w-96")}

View file

@ -1,11 +1,11 @@
import { CustomCellRendererProps } from "ag-grid-react"; import { CustomCellRendererProps } from "ag-grid-react";
import { cn, isTimeStampString } from "../../utils/utils"; import { cn, isTimeStampString } from "../../../../utils/utils";
import ArrayReader from "../arrayReaderComponent"; import ArrayReader from "../../../arrayReaderComponent";
import DateReader from "../dateReaderComponent"; import DateReader from "../../../dateReaderComponent";
import NumberReader from "../numberReader"; import NumberReader from "../../../numberReader";
import ObjectRender from "../objectRender"; import ObjectRender from "../../../objectRender";
import StringReader from "../stringReaderComponent"; import StringReader from "../../../stringReaderComponent";
import { Badge } from "../ui/badge"; import { Badge } from "../../../ui/badge";
export default function TableAutoCellRender({ export default function TableAutoCellRender({
value, value,
@ -43,7 +43,6 @@ export default function TableAutoCellRender({
} else { } else {
return <StringReader string={value} />; return <StringReader string={value} />;
} }
break;
case "number": case "number":
return <NumberReader number={value} />; return <NumberReader number={value} />;
default: default:

View file

@ -0,0 +1,266 @@
import { CustomCellRendererProps } from "ag-grid-react";
import { cloneDeep } from "lodash";
import { useState } from "react";
import CodeAreaComponent from "../../../codeAreaComponent";
import DictComponent from "../../../dictComponent";
import Dropdown from "../../../dropdownComponent";
import FloatComponent from "../../../floatComponent";
import InputFileComponent from "../../../inputFileComponent";
import InputGlobalComponent from "../../../inputGlobalComponent";
import InputListComponent from "../../../inputListComponent";
import IntComponent from "../../../intComponent";
import KeypairListComponent from "../../../keypairListComponent";
import PromptAreaComponent from "../../../promptComponent";
import TextAreaComponent from "../../../textAreaComponent";
import ToggleShadComponent from "../../../toggleShadComponent";
import useFlowStore from "../../../../stores/flowStore";
import {
convertObjToArray,
convertValuesToNumbers,
hasDuplicateKeys,
scapedJSONStringfy,
} from "../../../../utils/reactflowUtils";
import { classNames } from "../../../../utils/utils";
export default function TableNodeCellRender({
node: { data },
value: {
value,
nodeClass,
handleOnNewValue: handleOnNewValueNode,
handleOnChangeDb,
},
}: CustomCellRendererProps) {
const handleOnNewValue = (newValue: any, name: string) => {
handleOnNewValueNode(newValue, name);
setTemplateData((old) => {
let newData = cloneDeep(old);
newData.value = newValue;
return newData;
});
setTemplateValue(newValue);
};
const [templateValue, setTemplateValue] = useState(value);
const [templateData, setTemplateData] = useState(data);
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
const edges = useFlowStore((state) => state.edges);
const id = {
inputTypes: templateData.input_types,
type: templateData.type,
id: nodeClass.id,
fieldName: templateData.key,
};
const disabled =
edges.some(
(edge) =>
edge.targetHandle ===
scapedJSONStringfy(
templateData.proxy
? {
...id,
proxy: templateData.proxy,
}
: id,
),
) ?? false;
function getCellType() {
switch (templateData.type) {
case "str":
if (!templateData.options) {
return templateData?.list ? (
<InputListComponent
componentName={templateData.key ?? undefined}
editNode={true}
disabled={disabled}
value={
!templateValue || templateValue === "" ? [""] : templateValue
}
onChange={(value: string[]) => {
handleOnNewValue(value, templateData.key);
}}
/>
) : templateData.multiline ? (
<TextAreaComponent
id={"textarea-edit-" + templateData.name}
data-testid={"textarea-edit-" + templateData.name}
disabled={disabled}
editNode={true}
value={templateValue ?? ""}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateData.key);
}}
/>
) : (
<InputGlobalComponent
disabled={disabled}
editNode={true}
onChange={(value) => handleOnNewValue(value, templateData.key)}
setDb={(value) => {
handleOnChangeDb(value, templateData.key);
}}
name={templateData.key}
data={templateData}
/>
);
} else {
return (
<Dropdown
editNode={true}
options={templateData.options}
onSelect={(value) => handleOnNewValue(value, templateData.key)}
value={templateValue ?? "Choose an option"}
id={"dropdown-edit-" + templateData.name}
/>
);
}
case "NestedDict":
return (
<DictComponent
disabled={disabled}
editNode={true}
value={templateValue.toString() === "{}" ? {} : templateValue}
onChange={(newValue) => {
handleOnNewValue(newValue, templateData.key);
}}
id="editnode-div-dict-input"
/>
);
case "dict":
return (
<div
className={classNames(
"max-h-48 w-full overflow-auto custom-scroll",
templateValue?.length > 1 ? "my-3" : "",
)}
>
<KeypairListComponent
disabled={disabled}
editNode={true}
value={
templateValue?.length === 0 || !templateValue
? [{ "": "" }]
: convertObjToArray(templateValue, templateData.type)
}
duplicateKey={errorDuplicateKey}
onChange={(newValue) => {
const valueToNumbers = convertValuesToNumbers(newValue);
setErrorDuplicateKey(hasDuplicateKeys(valueToNumbers));
handleOnNewValue(valueToNumbers, templateData.key);
}}
isList={templateData.list ?? false}
/>
</div>
);
case "bool":
return (
<ToggleShadComponent
id={"toggle-edit-" + templateData.name}
disabled={disabled}
enabled={templateValue}
setEnabled={(isEnabled) => {
handleOnNewValue(isEnabled, templateData.key);
}}
size="small"
editNode={true}
/>
);
case "float":
return (
<FloatComponent
disabled={disabled}
editNode={true}
rangeSpec={templateData.rangeSpec}
value={templateValue ?? ""}
onChange={(value) => {
handleOnNewValue(value, templateData.key);
}}
/>
);
case "int":
return (
<IntComponent
rangeSpec={templateData.rangeSpec}
id={"edit-int-input-" + templateData.name}
disabled={disabled}
editNode={true}
value={templateValue ?? ""}
onChange={(value) => {
handleOnNewValue(value, templateData.key);
}}
/>
);
case "file":
return (
<InputFileComponent
editNode={true}
disabled={disabled}
value={templateValue ?? ""}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateData.key);
}}
fileTypes={templateData.fileTypes}
onFileChange={(filePath: string) => {
templateData.file_path = filePath;
}}
/>
);
case "prompt":
return (
<PromptAreaComponent
readonly={nodeClass.flow ? true : false}
field_name={templateData.key}
editNode={true}
disabled={disabled}
nodeClass={nodeClass}
setNodeClass={(value) => {
nodeClass = value;
}}
value={templateValue ?? ""}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateData.key);
}}
id={"prompt-area-edit-" + templateData.name}
data-testid={"modal-prompt-input-" + templateData.name}
/>
);
case "code":
return (
<CodeAreaComponent
readonly={nodeClass.flow && templateData.dynamic ? true : false}
dynamic={templateData.dynamic ?? false}
setNodeClass={(value) => {
nodeClass = value;
}}
nodeClass={nodeClass}
disabled={disabled}
editNode={true}
value={templateValue ?? ""}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateData.key);
}}
id={"code-area-edit" + templateData.name}
/>
);
case "Any":
return <>-</>;
default:
return String(templateValue);
}
}
return (
<div className="group flex h-full w-[300px] items-center justify-center py-2.5">
{getCellType()}
</div>
);
}

View file

@ -0,0 +1,24 @@
import { CustomCellRendererProps } from "ag-grid-react";
import { useState } from "react";
import ToggleShadComponent from "../../../toggleShadComponent";
export default function TableToggleCellRender({
value: { name, enabled, setEnabled },
}: CustomCellRendererProps) {
const [value, setValue] = useState(enabled);
return (
<div className="flex h-full items-center">
<ToggleShadComponent
id={"show" + name}
enabled={value}
setEnabled={(e) => {
setValue(e);
setEnabled(e);
}}
size="small"
editNode={true}
/>
</div>
);
}

View file

@ -0,0 +1,9 @@
import { CustomTooltipProps } from "ag-grid-react";
export default function TableTooltipRender({ value }: CustomTooltipProps) {
return (
<div className="z-45 overflow-y-auto rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1">
{value}
</div>
);
}

View file

@ -125,6 +125,9 @@ const TableComponent = forwardRef<
}} }}
columnDefs={colDef} columnDefs={colDef}
ref={realRef} ref={realRef}
getRowId={(params) => {
return params.data.id;
}}
pagination={true} pagination={true}
onGridReady={onGridReady} onGridReady={onGridReady}
onColumnMoved={onColumnMoved} onColumnMoved={onColumnMoved}

View file

@ -29,20 +29,18 @@ export default function ToggleShadComponent({
} }
return ( return (
<div className={disabled ? "pointer-events-none cursor-not-allowed " : ""}> <Switch
<Switch id={id}
id={id} data-testid={id}
data-testid={id} style={{
style={{ transform: `scaleX(${scaleX}) scaleY(${scaleY})`,
transform: `scaleX(${scaleX}) scaleY(${scaleY})`, }}
}} disabled={disabled}
disabled={disabled} className=""
className="" checked={enabled}
checked={enabled} onCheckedChange={(isEnabled: boolean) => {
onCheckedChange={(isEnabled: boolean) => { setEnabled(isEnabled);
setEnabled(isEnabled); }}
}} ></Switch>
></Switch>
</div>
); );
} }

View file

@ -31,11 +31,7 @@ function RefreshButton({
// icon class name should take into account the disabled state and the loading state // icon class name should take into account the disabled state and the loading state
const disabledIconTextClass = disabled ? "text-muted-foreground" : ""; const disabledIconTextClass = disabled ? "text-muted-foreground" : "";
const iconClassName = cn( const iconClassName = cn("h-4 w-4 animate-wiggle", disabledIconTextClass);
"h-4 w-4",
isLoading ? "animate-spin" : "animate-wiggle",
disabledIconTextClass
);
return ( return (
<Button <Button
@ -44,10 +40,11 @@ function RefreshButton({
className={classNames} className={classNames}
onClick={handleClick} onClick={handleClick}
id={id} id={id}
loading={isLoading}
> >
{button_text && <span className="mr-1">{button_text}</span>} {button_text && <span className="mr-1">{button_text}</span>}
<IconComponent <IconComponent
name={isLoading ? "Loader2" : "RefreshCcw"} name={"RefreshCcw"}
className={iconClassName} className={iconClassName}
id={id + "-icon"} id={id + "-icon"}
/> />

View file

@ -23,6 +23,7 @@ export const USER_EDIT_ERROR_ALERT = "Error on edit user";
export const USER_ADD_ERROR_ALERT = "Error when adding new user"; export const USER_ADD_ERROR_ALERT = "Error when adding new user";
export const SIGNIN_ERROR_ALERT = "Error signing in"; export const SIGNIN_ERROR_ALERT = "Error signing in";
export const DEL_KEY_ERROR_ALERT = "Error on delete key"; export const DEL_KEY_ERROR_ALERT = "Error on delete key";
export const DEL_KEY_ERROR_ALERT_PLURAL = "Error on delete keys";
export const UPLOAD_ERROR_ALERT = "Error uploading file"; export const UPLOAD_ERROR_ALERT = "Error uploading file";
export const WRONG_FILE_ERROR_ALERT = "Invalid file type"; export const WRONG_FILE_ERROR_ALERT = "Invalid file type";
export const UPLOAD_ALERT_LIST = "Please upload a JSON file"; export const UPLOAD_ALERT_LIST = "Please upload a JSON file";
@ -54,6 +55,7 @@ export const USER_DEL_SUCCESS_ALERT = "Success! User deleted!";
export const USER_EDIT_SUCCESS_ALERT = "Success! User edited!"; export const USER_EDIT_SUCCESS_ALERT = "Success! User edited!";
export const USER_ADD_SUCCESS_ALERT = "Success! New user added!"; export const USER_ADD_SUCCESS_ALERT = "Success! New user added!";
export const DEL_KEY_SUCCESS_ALERT = "Success! Key deleted!"; export const DEL_KEY_SUCCESS_ALERT = "Success! Key deleted!";
export const DEL_KEY_SUCCESS_ALERT_PLURAL = "Success! Keys deleted!";
export const FLOW_BUILD_SUCCESS_ALERT = `Flow built successfully`; export const FLOW_BUILD_SUCCESS_ALERT = `Flow built successfully`;
export const SAVE_SUCCESS_ALERT = "Changes saved successfully!"; export const SAVE_SUCCESS_ALERT = "Changes saved successfully!";

View file

@ -613,11 +613,8 @@ export const FETCH_ERROR_DESCRIPION =
export const SIGN_UP_SUCCESS = "Account created! Await admin activation. "; export const SIGN_UP_SUCCESS = "Account created! Await admin activation. ";
export const API_PAGE_PARAGRAPH_1 = export const API_PAGE_PARAGRAPH =
"Your secret API keys are listed below. Please note that we do not display your secret API keys again after you generate them."; "Your secret API keys are listed below. Do not share your API key with others, or expose it in the browser or other client-side code.";
export const API_PAGE_PARAGRAPH_2 =
"Do not share your API key with others, or expose it in the browser or other client-side code.";
export const API_PAGE_USER_KEYS = export const API_PAGE_USER_KEYS =
"This user does not have any keys assigned at the moment."; "This user does not have any keys assigned at the moment.";

View file

@ -67,7 +67,6 @@ export default function ParameterComponent({
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const refHtml = useRef<HTMLDivElement & ReactNode>(null); const refHtml = useRef<HTMLDivElement & ReactNode>(null);
const infoHtml = useRef<HTMLDivElement & ReactNode>(null); const infoHtml = useRef<HTMLDivElement & ReactNode>(null);
const setErrorData = useAlertStore((state) => state.setErrorData);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow); const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const nodes = useFlowStore((state) => state.nodes); const nodes = useFlowStore((state) => state.nodes);
const edges = useFlowStore((state) => state.edges); const edges = useFlowStore((state) => state.edges);
@ -392,7 +391,7 @@ export default function ParameterComponent({
}); });
}} }}
name={name} name={name}
data={data} data={data.node?.template[name]}
/> />
</div> </div>
{data.node?.template[name]?.refresh_button && ( {data.node?.template[name]?.refresh_button && (
@ -448,8 +447,8 @@ export default function ParameterComponent({
data.node?.template[name]?.real_time_refresh) data.node?.template[name]?.real_time_refresh)
} }
> >
<div className="mt-2 flex w-full items-center"> <div className="mt-2 flex w-full items-center gap-2">
<div className="w-5/6 flex-grow"> <div className="flex-1">
<Dropdown <Dropdown
disabled={disabled} disabled={disabled}
isLoading={isLoading} isLoading={isLoading}
@ -467,7 +466,6 @@ export default function ParameterComponent({
name={name} name={name}
data={data} data={data}
button_text={data.node?.template[name]?.refresh_button_text} button_text={data.node?.template[name]?.refresh_button_text}
className="extra-side-bar-buttons ml-2 mt-1"
handleUpdateValues={handleRefreshButtonPress} handleUpdateValues={handleRefreshButtonPress}
id={"refresh-button-" + name} id={"refresh-button-" + name}
/> />

View file

@ -1,4 +1,5 @@
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import emojiRegex from "emoji-regex";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { NodeToolbar, useUpdateNodeInternals } from "reactflow"; import { NodeToolbar, useUpdateNodeInternals } from "reactflow";
import IconComponent from "../../components/genericIconComponent"; import IconComponent from "../../components/genericIconComponent";
@ -220,8 +221,7 @@ export default function GenericNode({
const nameEditable = true; const nameEditable = true;
const emojiRegex = /\p{Emoji}/u; const isEmoji = emojiRegex().test(data?.node?.icon!);
const isEmoji = emojiRegex.test(data?.node?.icon!);
const iconNodeRender = useCallback(() => { const iconNodeRender = useCallback(() => {
const iconElement = data?.node?.icon; const iconElement = data?.node?.icon;

View file

@ -65,7 +65,6 @@ const ApiModal = forwardRef(
); );
const pythonCode = getPythonCode(flow?.name, tweak); const pythonCode = getPythonCode(flow?.name, tweak);
const widgetCode = getWidgetCode(flow?.id, flow?.name, autoLogin); const widgetCode = getWidgetCode(flow?.id, flow?.name, autoLogin);
console.log("flow", flow);
const includeWebhook = flow.webhook; const includeWebhook = flow.webhook;
const tweaksCode = buildTweaks(flow); const tweaksCode = buildTweaks(flow);
const codesArray = [ const codesArray = [

View file

@ -4,7 +4,7 @@ export const switchCaseModalSize = (size: string) => {
switch (size) { switch (size) {
case "x-small": case "x-small":
minWidth = "min-w-[20vw]"; minWidth = "min-w-[20vw]";
height = "h-full"; height = "";
break; break;
case "smaller": case "smaller":
minWidth = "min-w-[40vw]"; minWidth = "min-w-[40vw]";
@ -12,7 +12,7 @@ export const switchCaseModalSize = (size: string) => {
break; break;
case "smaller-h-full": case "smaller-h-full":
minWidth = "min-w-[40vw]"; minWidth = "min-w-[40vw]";
height = "h-full"; height = "";
break; break;
case "small": case "small":
minWidth = "min-w-[40vw]"; minWidth = "min-w-[40vw]";
@ -20,16 +20,19 @@ export const switchCaseModalSize = (size: string) => {
break; break;
case "small-h-full": case "small-h-full":
minWidth = "min-w-[40vw]"; minWidth = "min-w-[40vw]";
height = "h-full"; height = "";
break; break;
case "medium": case "medium":
minWidth = "min-w-[60vw]"; minWidth = "min-w-[60vw]";
height = "h-[60vh]"; height = "h-[60vh]";
break; break;
case "medium-tall":
minWidth = "min-w-[60vw]";
height = "h-[90vh]";
break;
case "medium-h-full": case "medium-h-full":
minWidth = "min-w-[60vw]"; minWidth = "min-w-[60vw]";
height = "h-full"; height = "";
break; break;
case "large": case "large":
minWidth = "min-w-[85vw]"; minWidth = "min-w-[85vw]";
@ -56,11 +59,11 @@ export const switchCaseModalSize = (size: string) => {
case "large-h-full": case "large-h-full":
minWidth = "min-w-[80vw]"; minWidth = "min-w-[80vw]";
height = "h-full"; height = "";
break; break;
default: default:
minWidth = "min-w-[80vw]"; minWidth = "min-w-[80vw]";
height = "h-[80vh]"; height = "h-[90vh]";
break; break;
} }
return { minWidth, height }; return { minWidth, height };

View file

@ -20,6 +20,7 @@ import { Button } from "../../components/ui/button";
import { modalHeaderType } from "../../types/components"; import { modalHeaderType } from "../../types/components";
import { cn } from "../../utils/utils"; import { cn } from "../../utils/utils";
import { switchCaseModalSize } from "./helpers/switch-case-size"; import { switchCaseModalSize } from "./helpers/switch-case-size";
import * as Form from "@radix-ui/react-form";
type ContentProps = { children: ReactNode }; type ContentProps = { children: ReactNode };
type HeaderProps = { children: ReactNode; description: string }; type HeaderProps = { children: ReactNode; description: string };
@ -52,10 +53,10 @@ const Trigger: React.FC<TriggerProps> = ({
); );
}; };
const Header: React.FC<{ children: ReactNode; description: string | null }> = ({ const Header: React.FC<{
children, children: ReactNode;
description, description: string | JSX.Element | null;
}: modalHeaderType): JSX.Element => { }> = ({ children, description }: modalHeaderType): JSX.Element => {
return ( return (
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center">{children}</DialogTitle> <DialogTitle className="flex items-center">{children}</DialogTitle>
@ -111,6 +112,7 @@ interface BaseModalProps {
| "smaller" | "smaller"
| "small" | "small"
| "medium" | "medium"
| "medium-tall"
| "large" | "large"
| "three-cards" | "three-cards"
| "large-thin" | "large-thin"
@ -179,31 +181,33 @@ function BaseModal({
) : ( ) : (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
{triggerChild} {triggerChild}
<DialogContent className={cn(minWidth, "duration-300")}> <DialogContent
className={cn(minWidth, height, "flex flex-col duration-300")}
>
<div className="truncate-doubleline word-break-break-word"> <div className="truncate-doubleline word-break-break-word">
{headerChild} {headerChild}
</div> </div>
{onSubmit ? ( {onSubmit ? (
<form <Form.Root
onSubmit={(event) => { onSubmit={(event) => {
event.preventDefault(); event.preventDefault();
onSubmit(); onSubmit();
}} }}
className="flex flex-col gap-6" className="flex flex-1 flex-col gap-6"
> >
<div <div
className={`flex flex-col ${height} w-full transition-all duration-300`} className={`flex w-full flex-1 flex-col transition-all duration-300`}
> >
{ContentChild} {ContentChild}
</div> </div>
{ContentFooter && ( {ContentFooter && (
<div className="flex flex-row-reverse">{ContentFooter}</div> <div className="flex flex-row-reverse">{ContentFooter}</div>
)} )}
</form> </Form.Root>
) : ( ) : (
<> <>
<div <div
className={`flex flex-col ${height} w-full transition-all duration-300`} className={`flex w-full flex-1 flex-col transition-all duration-300`}
> >
{ContentChild} {ContentChild}
</div> </div>

View file

@ -0,0 +1,91 @@
import { ColDef, ValueGetterParams } from "ag-grid-community";
import { useMemo } from "react";
import TableAutoCellRender from "../../../components/tableComponent/components/tableAutoCellRender";
import TableNodeCellRender from "../../../components/tableComponent/components/tableNodeCellRender";
import TableToggleCellRender from "../../../components/tableComponent/components/tableToggleCellRender";
import TableTooltipRender from "../../../components/tableComponent/components/tableTooltipRender";
const useColumnDefs = (
myData: any,
handleOnNewValue: (newValue: any, name: string) => void,
changeAdvanced: (n: string) => void,
open: boolean,
) => {
const columnDefs: ColDef[] = useMemo(
() => [
{
headerName: "Name",
field: "display_name",
valueGetter: (params) => {
const templateParam = params.data;
return (
(templateParam.display_name
? templateParam.display_name
: templateParam.name) ?? params.data.key
);
},
tooltipField: "display_name",
tooltipComponent: TableTooltipRender,
wrapText: true,
autoHeight: true,
flex: 1,
resizable: false,
cellClass: "no-border",
},
{
headerName: "Description",
field: "info",
tooltipField: "info",
tooltipComponent: TableTooltipRender,
wrapText: true,
autoHeight: true,
flex: 2,
resizable: false,
cellClass: "no-border",
},
{
headerName: "Value",
field: "value",
cellRenderer: TableNodeCellRender,
valueGetter: (params: ValueGetterParams) => {
return {
value: params.data.value,
nodeClass: myData.node,
handleOnNewValue: handleOnNewValue,
handleOnChangeDb: (value, key) => {
myData.node!.template[key].load_from_db = value;
},
};
},
minWidth: 330,
autoHeight: true,
flex: 1,
resizable: false,
cellClass: "no-border",
},
{
headerName: "Show",
field: "advanced",
cellRenderer: TableToggleCellRender,
valueGetter: (params: ValueGetterParams) => {
return {
name: params.data.name,
enabled: !params.data.advanced,
setEnabled: () => {
changeAdvanced(params.data.key);
},
};
},
editable: false,
maxWidth: 80,
resizable: false,
cellClass: "no-border",
},
],
[open, myData],
);
return columnDefs;
};
export default useColumnDefs;

View file

@ -0,0 +1,37 @@
import { useMemo } from "react";
import { LANGFLOW_SUPPORTED_TYPES } from "../../../constants/constants";
import { TemplateVariableType } from "../../../types/api";
const useRowData = (myData, open) => {
const rowData = useMemo(() => {
return Object.keys(myData.node!.template)
.filter((key: string) => {
const templateParam = myData.node!.template[
key
] as TemplateVariableType;
return (
key.charAt(0) !== "_" &&
templateParam.show &&
LANGFLOW_SUPPORTED_TYPES.has(templateParam.type) &&
!(
(key === "code" && templateParam.type === "code") ||
(key.includes("code") && templateParam.proxy)
)
);
})
.map((key: string) => {
const templateParam = myData.node!.template[
key
] as TemplateVariableType;
return {
...templateParam,
key: key,
id: key,
};
});
}, [open, myData]);
return rowData;
};
export default useRowData;

View file

@ -1,43 +1,13 @@
import { cloneDeep } from "lodash"; import { ColDef, GridApi } from "ag-grid-community";
import { forwardRef, useEffect, useState } from "react"; import { forwardRef, useEffect, useRef, useState } from "react";
import CodeAreaComponent from "../../components/codeAreaComponent";
import DictComponent from "../../components/dictComponent";
import Dropdown from "../../components/dropdownComponent";
import FloatComponent from "../../components/floatComponent";
import IconComponent from "../../components/genericIconComponent"; import IconComponent from "../../components/genericIconComponent";
import InputFileComponent from "../../components/inputFileComponent"; import TableComponent from "../../components/tableComponent";
import InputGlobalComponent from "../../components/inputGlobalComponent";
import InputListComponent from "../../components/inputListComponent";
import IntComponent from "../../components/intComponent";
import KeypairListComponent from "../../components/keypairListComponent";
import PromptAreaComponent from "../../components/promptComponent";
import ShadTooltip from "../../components/shadTooltipComponent";
import TextAreaComponent from "../../components/textAreaComponent";
import ToggleShadComponent from "../../components/toggleShadComponent";
import { Badge } from "../../components/ui/badge"; import { Badge } from "../../components/ui/badge";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "../../components/ui/table";
import {
LANGFLOW_SUPPORTED_TYPES,
limitScrollFieldsModal,
} from "../../constants/constants";
import { Case } from "../../shared/components/caseComponent";
import useFlowStore from "../../stores/flowStore"; import useFlowStore from "../../stores/flowStore";
import { NodeDataType } from "../../types/flow"; import { NodeDataType } from "../../types/flow";
import {
convertObjToArray,
convertValuesToNumbers,
hasDuplicateKeys,
scapedJSONStringfy,
} from "../../utils/reactflowUtils";
import { classNames } from "../../utils/utils";
import BaseModal from "../baseModal"; import BaseModal from "../baseModal";
import useColumnDefs from "./hooks/use-column-defs";
import useRowData from "./hooks/use-row-data";
const EditNodeModal = forwardRef( const EditNodeModal = forwardRef(
( (
@ -52,61 +22,51 @@ const EditNodeModal = forwardRef(
setOpen: (open: boolean) => void; setOpen: (open: boolean) => void;
data: NodeDataType; data: NodeDataType;
}, },
ref, ref
) => { ) => {
const nodes = useFlowStore((state) => state.nodes); const myData = useRef(data);
const dataFromStore = nodes.find((node) => node.id === node.id)?.data;
const [myData, setMyData] = useState(dataFromStore ?? data);
const edges = useFlowStore((state) => state.edges);
const setNode = useFlowStore((state) => state.setNode); const setNode = useFlowStore((state) => state.setNode);
function changeAdvanced(n) { function changeAdvanced(n) {
setMyData((old) => { myData.current.node!.template[n].advanced =
let newData = cloneDeep(old); !myData.current.node!.template[n]?.advanced;
newData.node!.template[n].advanced =
!newData.node!.template[n].advanced;
return newData;
});
} }
const handleOnNewValue = (newValue: any, name) => { const handleOnNewValue = (newValue: any, name) => {
setMyData((old) => { myData.current.node!.template[name].value = newValue;
let newData = cloneDeep(old);
newData.node!.template[name].value = newValue;
return newData;
});
}; };
const rowData = useRowData(data, open);
const columnDefs: ColDef[] = useColumnDefs(
data,
handleOnNewValue,
changeAdvanced,
open
);
const [gridApi, setGridApi] = useState<GridApi | null>(null);
useEffect(() => { useEffect(() => {
if (open) { if (gridApi && open) {
setMyData(data); // reset data to what it is on node when opening modal myData.current = data;
gridApi.refreshCells();
} }
}, [open]); }, [gridApi, open]);
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
const type = (templateParam) => {
return myData.node?.template[templateParam].type;
};
return ( return (
<BaseModal <BaseModal
key={data.id} key={data.id}
size="large-h-full" size="medium-tall"
open={open} open={open}
setOpen={setOpen} setOpen={setOpen}
onChangeOpenModal={(open) => {
setMyData(data);
}}
onSubmit={() => { onSubmit={() => {
setNode(data.id, (old) => ({ setNode(data.id, (old) => ({
...old, ...old,
data: { data: {
...old.data, ...old.data,
node: myData.node, node: myData.current.node,
}, },
})); }));
setOpen(false); setOpen(false);
@ -115,526 +75,30 @@ const EditNodeModal = forwardRef(
<BaseModal.Trigger> <BaseModal.Trigger>
<></> <></>
</BaseModal.Trigger> </BaseModal.Trigger>
<BaseModal.Header description={myData.node?.description!}> <BaseModal.Header description={data.node?.description!}>
<span className="pr-2">{myData.type}</span> <span className="pr-2">{data.type}</span>
<Badge variant="secondary">ID: {myData.id}</Badge> <Badge variant="secondary">ID: {data.id}</Badge>
</BaseModal.Header> </BaseModal.Header>
<BaseModal.Content> <BaseModal.Content>
<div className="flex pb-2"> <div className="flex h-full flex-col">
<IconComponent <div className="flex pb-2">
name="Variable" <IconComponent
className="edit-node-modal-variable " name="Variable"
/> className="edit-node-modal-variable "
<span className="edit-node-modal-span">Parameters</span> />
</div> <span className="edit-node-modal-span">Parameters</span>
</div>
<div className="edit-node-modal-arrangement"> <div className="h-full">
<div
className={classNames(
"edit-node-modal-box",
nodeLength > limitScrollFieldsModal
? "overflow-scroll overflow-x-hidden custom-scroll"
: "",
)}
>
{nodeLength > 0 && ( {nodeLength > 0 && (
<div className="edit-node-modal-table"> <TableComponent
<Table className="table-fixed bg-muted outline-1"> onGridReady={(params) => {
<TableHeader className="edit-node-modal-table-header"> setGridApi(params.api);
<TableRow className=""> }}
<TableHead className="h-7 text-center">PARAM</TableHead> tooltipShowDelay={0.5}
<TableHead className="h-7 text-center">DESC</TableHead> columnDefs={columnDefs}
<TableHead className="h-7 p-0 text-center"> rowData={rowData}
VALUE />
</TableHead>
<TableHead className="h-7 text-center">SHOW</TableHead>
</TableRow>
</TableHeader>
<TableBody className="p-0">
{Object.keys(myData.node!.template)
.filter(
(templateParam) =>
templateParam.charAt(0) !== "_" &&
myData.node?.template[templateParam].show &&
LANGFLOW_SUPPORTED_TYPES.has(
myData.node!.template[templateParam].type,
),
)
.map((templateParam, index) => {
let id = {
inputTypes:
myData.node!.template[templateParam].input_types,
type: myData.node!.template[templateParam].type,
id: myData.id,
fieldName: templateParam,
};
let disabled =
edges.some(
(edge) =>
edge.targetHandle ===
scapedJSONStringfy(
myData.node!.template[templateParam].proxy
? {
...id,
proxy:
myData.node?.template[templateParam]
.proxy,
}
: id,
),
) ?? false;
return (
<TableRow
key={index}
className={
"h-10 " +
((templateParam === "code" &&
type(templateParam) === "code") ||
(templateParam.includes("code") &&
myData.node?.template[templateParam].proxy)
? " hidden "
: "")
}
>
<TableCell className="truncate p-0 text-center text-sm text-foreground sm:px-3">
<ShadTooltip
styleClasses="z-50"
content={
myData.node?.template[templateParam].proxy
? myData.node?.template[templateParam]
.proxy?.id
: myData.node?.template[templateParam]
.display_name
? myData.node!.template[templateParam]
.display_name
: myData.node?.template[templateParam]
.name
}
>
<span>
{myData.node?.template[templateParam]
.display_name
? myData.node!.template[templateParam]
.display_name
: myData.node?.template[templateParam]
.name}
</span>
</ShadTooltip>
</TableCell>
<TableCell className="truncate p-0 text-center text-sm text-foreground sm:px-3">
<ShadTooltip
styleClasses="z-50"
content={
data.node?.template[templateParam]?.info ??
null
}
>
<span>
{data.node?.template[templateParam]?.info ??
""}
</span>
</ShadTooltip>
</TableCell>
<TableCell className="w-[300px] p-0 text-center text-xs text-foreground ">
<Case
condition={
type(templateParam) === "str" &&
!myData.node!.template[templateParam]
.options
}
>
<div className="mx-auto">
{myData.node!.template[templateParam]
?.list ? (
<InputListComponent
componentName={templateParam}
editNode={true}
disabled={disabled}
value={
!myData.node!.template[templateParam]
.value ||
myData.node!.template[templateParam]
.value === ""
? [""]
: myData.node!.template[
templateParam
].value
}
onChange={(value: string[]) => {
handleOnNewValue(
value,
templateParam,
);
}}
/>
) : myData.node!.template[templateParam]
.multiline ? (
<TextAreaComponent
id={
"textarea-edit-" +
myData.node!.template[templateParam]
.name
}
data-testid={
"textarea-edit-" +
myData.node!.template[templateParam]
.name
}
disabled={disabled}
editNode={true}
value={
myData.node!.template[templateParam]
.value ?? ""
}
onChange={(
value: string | string[],
) => {
handleOnNewValue(
value,
templateParam,
);
}}
/>
) : (
<InputGlobalComponent
disabled={disabled}
editNode={true}
onChange={(value) =>
handleOnNewValue(value, templateParam)
}
setDb={(value) => {
setMyData((oldData) => {
let newData = cloneDeep(oldData);
newData.node!.template[
templateParam
].load_from_db = value;
return newData;
});
}}
name={templateParam}
data={myData}
/>
)}
</div>
</Case>
<Case
condition={
type(templateParam) === "NestedDict"
}
>
<div className=" w-full">
<DictComponent
disabled={disabled}
editNode={true}
value={
myData.node!.template[
templateParam
]?.value?.toString() === "{}"
? {}
: myData.node!.template[templateParam]
.value
}
onChange={(newValue) => {
myData.node!.template[
templateParam
].value = newValue;
handleOnNewValue(
newValue,
templateParam,
);
}}
id="editnode-div-dict-input"
/>
</div>
</Case>
<Case
condition={type(templateParam) === "dict"}
>
<div
className={classNames(
"max-h-48 w-full overflow-auto custom-scroll",
myData.node!.template[templateParam].value
?.length > 1
? "my-3"
: "",
)}
>
<KeypairListComponent
disabled={disabled}
editNode={true}
value={
myData.node!.template[templateParam]
.value?.length === 0 ||
!myData.node!.template[templateParam]
.value
? [{ "": "" }]
: convertObjToArray(
myData.node!.template[
templateParam
].value,
type(templateParam)!,
)
}
duplicateKey={errorDuplicateKey}
onChange={(newValue) => {
const valueToNumbers =
convertValuesToNumbers(newValue);
myData.node!.template[
templateParam
].value = valueToNumbers;
setErrorDuplicateKey(
hasDuplicateKeys(valueToNumbers),
);
handleOnNewValue(
valueToNumbers,
templateParam,
);
}}
isList={
data.node?.template[templateParam]
?.list ?? false
}
/>
</div>
</Case>
<Case
condition={type(templateParam) === "bool"}
>
<div className="ml-auto">
{" "}
<ToggleShadComponent
id={
"toggle-edit-" +
myData.node!.template[templateParam]
.name
}
disabled={disabled}
enabled={
myData.node!.template[templateParam]
.value
}
setEnabled={(isEnabled) => {
handleOnNewValue(
isEnabled,
templateParam,
);
}}
size="small"
editNode={true}
/>
</div>
</Case>
<Case
condition={type(templateParam) === "float"}
>
<div className="mx-auto">
<FloatComponent
disabled={disabled}
editNode={true}
rangeSpec={
myData.node!.template[templateParam]
.rangeSpec
}
value={
myData.node!.template[templateParam]
.value ?? ""
}
onChange={(value) => {
handleOnNewValue(value, templateParam);
}}
/>
</div>
</Case>
<Case
condition={
type(templateParam) === "str" &&
myData.node!.template[templateParam].options
}
>
<div className="mx-auto">
<Dropdown
editNode={true}
options={
myData.node!.template[templateParam]
.options
}
onSelect={(value) =>
handleOnNewValue(value, templateParam)
}
value={
myData.node!.template[templateParam]
.value ?? "Choose an option"
}
id={
"dropdown-edit-" +
myData.node!.template[templateParam]
.name
}
></Dropdown>
</div>
</Case>
<Case condition={type(templateParam) === "int"}>
<div className="mx-auto">
<IntComponent
rangeSpec={
data.node?.template[templateParam]
?.rangeSpec
}
id={
"edit-int-input-" +
myData.node!.template[templateParam]
.name
}
disabled={disabled}
editNode={true}
value={
myData.node!.template[templateParam]
.value ?? ""
}
onChange={(value) => {
handleOnNewValue(value, templateParam);
}}
/>
</div>
</Case>
<Case
condition={type(templateParam) === "file"}
>
<div className="mx-auto">
<InputFileComponent
editNode={true}
disabled={disabled}
value={
myData.node!.template[templateParam]
.value ?? ""
}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam);
}}
fileTypes={
myData.node!.template[templateParam]
.fileTypes
}
onFileChange={(filePath: string) => {
data.node!.template[
templateParam
].file_path = filePath;
}}
></InputFileComponent>
</div>
</Case>
<Case
condition={type(templateParam) === "prompt"}
>
<div className="mx-auto">
<PromptAreaComponent
readonly={
myData.node?.flow ? true : false
}
field_name={templateParam}
editNode={true}
disabled={disabled}
nodeClass={myData.node}
setNodeClass={(nodeClass) => {
myData.node = nodeClass;
}}
value={
myData.node!.template[templateParam]
.value ?? ""
}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam);
}}
id={
"prompt-area-edit-" +
myData.node!.template[templateParam]
.name
}
data-testid={
"modal-prompt-input-" +
myData.node!.template[templateParam]
.name
}
/>
</div>
</Case>
<Case
condition={type(templateParam) === "code"}
>
<div className="mx-auto">
<CodeAreaComponent
readonly={
myData.node?.flow &&
myData.node!.template[templateParam]
.dynamic
? true
: false
}
dynamic={
data.node!.template[templateParam]
?.dynamic ?? false
}
setNodeClass={(nodeClass) => {
data.node = nodeClass;
}}
nodeClass={data.node}
disabled={disabled}
editNode={true}
value={
myData.node!.template[templateParam]
.value ?? ""
}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam);
}}
id={
"code-area-edit" +
myData.node!.template[templateParam]
.name
}
/>
</div>
</Case>
<Case condition={type(templateParam) === "Any"}>
<>-</>
</Case>
</TableCell>
<TableCell className="p-0 text-right">
<div className="items-center text-center">
<ToggleShadComponent
id={
"show" +
myData.node?.template[templateParam].name
}
enabled={
!myData.node?.template[templateParam]
.advanced
}
setEnabled={(e) => {
changeAdvanced(templateParam);
}}
disabled={disabled}
size="small"
editNode={true}
/>
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
)} )}
</div> </div>
</div> </div>
@ -643,7 +107,7 @@ const EditNodeModal = forwardRef(
<BaseModal.Footer submit={{ label: "Save Changes" }} /> <BaseModal.Footer submit={{ label: "Save Changes" }} />
</BaseModal> </BaseModal>
); );
}, }
); );
export default EditNodeModal; export default EditNodeModal;

View file

@ -11,15 +11,10 @@ import { nodeIconsLucide } from "../../utils/styleUtils";
import BaseModal from "../baseModal"; import BaseModal from "../baseModal";
export default function SecretKeyModal({ export default function SecretKeyModal({
title,
cancelText,
confirmationText,
children, children,
icon,
data, data,
onCloseModal, onCloseModal,
}: ApiKeyType) { }: ApiKeyType) {
const Icon: any = nodeIconsLucide[icon];
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [apiKeyName, setApiKeyName] = useState(data?.apikeyname ?? ""); const [apiKeyName, setApiKeyName] = useState(data?.apikeyname ?? "");
const [apiKeyValue, setApiKeyValue] = useState(""); const [apiKeyValue, setApiKeyValue] = useState("");
@ -66,118 +61,91 @@ export default function SecretKeyModal({
.catch((err) => {}); .catch((err) => {});
} }
function handleSubmitForm() {
if (!renderKey) {
setRenderKey(true);
handleAddNewKey();
} else {
setOpen(false);
}
}
return ( return (
<BaseModal size="small-h-full" open={open} setOpen={setOpen}> <BaseModal
onSubmit={handleSubmitForm}
size="small-h-full"
open={open}
setOpen={setOpen}
>
<BaseModal.Trigger>{children}</BaseModal.Trigger> <BaseModal.Trigger>{children}</BaseModal.Trigger>
<BaseModal.Header description={""}> <BaseModal.Header
<span className="pr-2">{title}</span> description={
<Icon renderKey ? (
name="icon" <>
className="h-6 w-6 pl-1 text-foreground" {" "}
aria-hidden="true"
/>
</BaseModal.Header>
<BaseModal.Content>
{renderKey === true && (
<>
<span className="text-xs">
Please save this secret key somewhere safe and accessible. For Please save this secret key somewhere safe and accessible. For
security reasons,{" "} security reasons,{" "}
<strong>you won't be able to view it again</strong> through your <strong>you won't be able to view it again</strong> through your
account. If you lose this secret key, you'll need to generate a account. If you lose this secret key, you'll need to generate a
new one. new one.
</span> </>
<div className="flex pt-3"> ) : (
<>Create a secret API Key to use Langflow API.</>
)
}
>
<span className="pr-2">Create API Key</span>
<IconComponent
name="Key"
className="h-6 w-6 pl-1 text-foreground"
aria-hidden="true"
/>
</BaseModal.Header>
<BaseModal.Content>
{renderKey ? (
<>
<div className="flex items-center gap-3">
<div className="w-full"> <div className="w-full">
<Input ref={inputRef} readOnly={true} value={apiKeyValue} /> <Input ref={inputRef} readOnly={true} value={apiKeyValue} />
</div> </div>
<div> <Button
<Button onClick={() => {
className="ml-3" handleCopyClick();
onClick={() => { }}
handleCopyClick(); variant="none"
}} size="none"
> >
{textCopied ? ( {textCopied ? (
<IconComponent name="Copy" className="h-4 w-4" /> <IconComponent name="Copy" className="h-4 w-4" />
) : ( ) : (
<IconComponent name="Check" className="h-4 w-4" /> <IconComponent name="Check" className="h-4 w-4" />
)} )}
</Button> </Button>
</div>
</div> </div>
</> </>
)} ) : (
<Form.Field name="apikey">
<Form.Root <div className="flex items-center justify-between gap-2">
onSubmit={(event) => { <Form.Control asChild>
setRenderKey(true); <Input
handleAddNewKey(); //fake api key
event.preventDefault(); id="primary-input"
}} value={apiKeyName}
> ref={inputRef}
{renderKey === false && ( onChange={({ target: { value } }) => {
<div className="grid gap-5"> setApiKeyName(value);
<Form.Field name="username">
<div
style={{
display: "flex",
alignItems: "baseline",
justifyContent: "space-between",
}} }}
> placeholder="Insert a name for your API Key"
<Form.Label className="data-[invalid]:label-invalid"> />
Name (optional){" "} </Form.Control>
</Form.Label>
</div>
<Form.Control asChild>
<input
onChange={({ target: { value } }) => {
setApiKeyName(value);
}}
value={apiKeyName}
className="primary-input"
placeholder="My key name"
/>
</Form.Control>
</Form.Field>
</div> </div>
)} </Form.Field>
{renderKey === false && ( )}
<div className="float-right">
<Button
type="button"
className="mr-3"
variant="outline"
onClick={() => {
setOpen(false);
}}
>
{cancelText}
</Button>
<Form.Submit asChild>
<Button className="mt-8">{confirmationText}</Button>
</Form.Submit>
</div>
)}
{renderKey === true && (
<div className="float-right">
<Button
onClick={() => {
setOpen(false);
setRenderKey(false);
}}
className="mt-8"
>
Done
</Button>
</div>
)}
</Form.Root>
</BaseModal.Content> </BaseModal.Content>
<BaseModal.Footer
submit={{ label: renderKey ? "Done" : "Create Secret Key" }}
/>
</BaseModal> </BaseModal>
); );
} }

View file

@ -1,291 +0,0 @@
import { useContext, useEffect, useRef, useState } from "react";
import IconComponent from "../../components/genericIconComponent";
import ShadTooltip from "../../components/shadTooltipComponent";
import { Button } from "../../components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "../../components/ui/table";
import { AuthContext } from "../../contexts/authContext";
import { deleteApiKey, getApiKey } from "../../controllers/API";
import ConfirmationModal from "../../modals/confirmationModal";
import SecretKeyModal from "../../modals/secretKeyModal";
import moment from "moment";
import Header from "../../components/headerComponent";
import {
DEL_KEY_ERROR_ALERT,
DEL_KEY_SUCCESS_ALERT,
} from "../../constants/alerts_constants";
import {
API_PAGE_PARAGRAPH_1,
API_PAGE_PARAGRAPH_2,
API_PAGE_USER_KEYS,
LAST_USED_SPAN_1,
LAST_USED_SPAN_2,
} from "../../constants/constants";
import useAlertStore from "../../stores/alertStore";
import { ApiKey } from "../../types/components";
export default function ApiKeysPage() {
const [loadingKeys, setLoadingKeys] = useState(true);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData);
const { userData } = useContext(AuthContext);
const [userId, setUserId] = useState("");
const keysList = useRef([]);
useEffect(() => {
getKeys();
}, [userData]);
function getKeys() {
setLoadingKeys(true);
if (userData) {
getApiKey()
.then((keys: [ApiKey]) => {
keysList.current = keys["api_keys"];
setUserId(keys["user_id"]);
setLoadingKeys(false);
})
.catch((error) => {
setLoadingKeys(false);
});
}
}
function resetFilter() {
getKeys();
}
function handleDeleteKey(keys) {
deleteApiKey(keys)
.then((res) => {
resetFilter();
setSuccessData({
title: DEL_KEY_SUCCESS_ALERT,
});
})
.catch((error) => {
setErrorData({
title: DEL_KEY_ERROR_ALERT,
list: [error["response"]["data"]["detail"]],
});
});
}
function lastUsedMessage() {
return (
<div className="text-xs">
<span>
{LAST_USED_SPAN_1}
<br></br> {LAST_USED_SPAN_2}
</span>
</div>
);
}
return (
<>
<Header></Header>
{userData && (
<div className="main-page-panel">
<div className="m-auto flex h-full flex-row justify-center">
<div className="basis-5/6">
<div className="m-auto flex h-full flex-col space-y-8 p-8 ">
<div className="flex items-center justify-between space-y-2">
<div>
<h2 className="text-2xl font-bold tracking-tight">
API keys
</h2>
<p className="text-muted-foreground">
{API_PAGE_PARAGRAPH_1}
<br />
{API_PAGE_PARAGRAPH_2}
</p>
</div>
<div className="flex items-center space-x-2"></div>
</div>
{keysList.current &&
keysList.current.length === 0 &&
!loadingKeys && (
<>
<div className="flex items-center justify-between">
<h2>{API_PAGE_USER_KEYS}</h2>
</div>
</>
)}
<>
{loadingKeys && (
<div>
<strong>Loading...</strong>
</div>
)}
<div
className={
"max-h-[15rem] overflow-scroll overflow-x-hidden rounded-md border-2 bg-muted custom-scroll" +
(loadingKeys ? " border-0" : "")
}
>
{keysList.current &&
keysList.current.length > 0 &&
!loadingKeys && (
<Table className={"table-fixed bg-muted outline-1"}>
<TableHeader
className={
loadingKeys
? "hidden"
: "table-fixed bg-muted outline-1"
}
>
<TableRow>
<TableHead className="h-10">Name</TableHead>
<TableHead className="h-10">Key</TableHead>
<TableHead className="h-10">Created</TableHead>
<TableHead className="flex h-10 items-center">
Last Used
<ShadTooltip
side="top"
content={lastUsedMessage()}
>
<div>
<IconComponent
name="Info"
className="ml-1 h-3 w-3"
/>
</div>
</ShadTooltip>
</TableHead>
<TableHead className="h-10">Total Uses</TableHead>
<TableHead className="h-10 w-[100px] text-right"></TableHead>
</TableRow>
</TableHeader>
{!loadingKeys && (
<TableBody>
{keysList.current.map(
(api_keys: ApiKey, index: number) => (
<TableRow key={index}>
<TableCell className="truncate py-2">
<ShadTooltip content={api_keys.name}>
<span className="cursor-default">
{api_keys.name ? api_keys.name : "-"}
</span>
</ShadTooltip>
</TableCell>
<TableCell className="truncate py-2">
<span className="cursor-default">
{api_keys.api_key}
</span>
</TableCell>
<TableCell className="truncate py-2 ">
<ShadTooltip
side="top"
content={moment(
api_keys.created_at
).format("YYYY-MM-DD HH:mm")}
>
<div>
{moment(api_keys.created_at).format(
"YYYY-MM-DD HH:mm"
)}
</div>
</ShadTooltip>
</TableCell>
<TableCell className="truncate py-2">
<ShadTooltip
side="top"
content={
moment(api_keys.last_used_at).format(
"YYYY-MM-DD HH:mm"
) === "Invalid date"
? "Never"
: moment(
api_keys.last_used_at
).format("YYYY-MM-DD HH:mm")
}
>
<div>
{moment(api_keys.last_used_at).format(
"YYYY-MM-DD HH:mm"
) === "Invalid date"
? "Never"
: moment(
api_keys.last_used_at
).format("YYYY-MM-DD HH:mm")}
</div>
</ShadTooltip>
</TableCell>
<TableCell className="truncate py-2">
{api_keys.total_uses}
</TableCell>
<TableCell className="flex w-[100px] py-2 text-right">
<div className="flex">
<ConfirmationModal
title="Delete"
titleHeader="Delete User"
modalContentTitle="Attention!"
cancelText="Cancel"
confirmationText="Delete"
icon={"UserMinus2"}
data={api_keys.id}
index={index}
onConfirm={(index, keys) => {
handleDeleteKey(keys);
}}
>
<ConfirmationModal.Content>
<span>
Are you sure you want to delete
this key? This action cannot be
undone.
</span>
</ConfirmationModal.Content>
<ConfirmationModal.Trigger>
<IconComponent
name="Trash2"
className="ml-2 h-4 w-4 cursor-pointer"
/>
</ConfirmationModal.Trigger>
</ConfirmationModal>
</div>
</TableCell>
</TableRow>
)
)}
</TableBody>
)}
</Table>
)}
</div>
<div className="flex items-center justify-between">
<div>
<SecretKeyModal
title="Create new secret key"
cancelText="Cancel"
confirmationText="Create secret key"
icon={"Key"}
data={userId}
onCloseModal={getKeys}
>
<Button>
<IconComponent name="Plus" className="mr-1 h-5 w-5" />
Create new secret key
</Button>
</SecretKeyModal>
</div>
</div>
</>
</div>
</div>
</div>
</div>
)}
</>
);
}

View file

@ -25,7 +25,6 @@ export default function SettingsPage(): JSX.Element {
/> />
), ),
}, },
{ {
title: "Global Variables", title: "Global Variables",
href: "/settings/global-variables", href: "/settings/global-variables",
@ -36,6 +35,16 @@ export default function SettingsPage(): JSX.Element {
/> />
), ),
}, },
{
title: "API Keys",
href: "/settings/api-keys",
icon: (
<ForwardedIconComponent
name="Key"
className="w-4 flex-shrink-0 justify-start stroke-[1.5]"
/>
),
},
{ {
title: "Shortcuts", title: "Shortcuts",
href: "/settings/shortcuts", href: "/settings/shortcuts",

View file

@ -0,0 +1,58 @@
import ForwardedIconComponent from "../../../../../../components/genericIconComponent";
import { Button } from "../../../../../../components/ui/button";
import { API_PAGE_PARAGRAPH } from "../../../../../../constants/constants";
import SecretKeyModal from "../../../../../../modals/secretKeyModal";
import { cn } from "../../../../../../utils/utils";
type ApiKeyHeaderComponentProps = {
selectedRows: string[];
handleDeleteKey: () => void;
fetchApiKeys: () => void;
userId: string;
};
const ApiKeyHeaderComponent = ({
selectedRows,
handleDeleteKey,
fetchApiKeys,
userId,
}: ApiKeyHeaderComponentProps) => {
return (
<>
<div className="flex w-full items-center justify-between gap-4 space-y-0.5">
<div className="flex w-full flex-col">
<h2 className="flex items-center text-lg font-semibold tracking-tight">
API Keys
<ForwardedIconComponent
name="Key"
className="ml-2 h-5 w-5 text-primary"
/>
</h2>
<p className="text-sm text-muted-foreground">{API_PAGE_PARAGRAPH}</p>
</div>
<div className="flex flex-shrink-0 items-center gap-2">
<Button
data-testid="api-key-button-store"
variant="primary"
className="group px-2"
disabled={selectedRows.length === 0}
onClick={handleDeleteKey}
>
<ForwardedIconComponent
name="Trash2"
className={cn(
"h-5 w-5 text-destructive group-disabled:text-primary",
)}
/>
</Button>
<SecretKeyModal data={userId} onCloseModal={fetchApiKeys}>
<Button data-testid="api-key-button-store" variant="primary">
<ForwardedIconComponent name="Plus" className="mr-2 w-4" />
Add New
</Button>
</SecretKeyModal>
</div>
</div>
</>
);
};
export default ApiKeyHeaderComponent;

View file

@ -0,0 +1,40 @@
import TableAutoCellRender from "../../../../../components/tableComponent/components/tableAutoCellRender";
export const getColumnDefs = () => {
return [
{
headerCheckboxSelection: true,
checkboxSelection: true,
showDisabledCheckboxes: true,
headerName: "Name",
field: "name",
cellRenderer: TableAutoCellRender,
flex: 2,
},
{
headerName: "Key",
field: "api_key",
cellRenderer: TableAutoCellRender,
flex: 1,
},
{
headerName: "Created",
field: "created_at",
cellRenderer: TableAutoCellRender,
flex: 1,
},
{
headerName: "Last Used",
field: "last_used_at",
cellRenderer: TableAutoCellRender,
flex: 1,
},
{
headerName: "Total Uses",
field: "total_uses",
cellRenderer: TableAutoCellRender,
flex: 1,
resizable: false,
},
];
};

View file

@ -0,0 +1,26 @@
import { getApiKey } from "../../../../../controllers/API";
const useApiKeys = (userData, setLoadingKeys, keysList, setUserId) => {
const fetchApiKeys = () => {
setLoadingKeys(true);
getApiKey()
.then((keys) => {
keysList.current = keys["api_keys"].map((apikey) => ({
...apikey,
name: apikey.name && apikey.name !== "" ? apikey.name : "Untitled",
last_used_at: apikey.last_used_at ?? "Never",
}));
setUserId(keys["user_id"]);
setLoadingKeys(false);
})
.catch((error) => {
setLoadingKeys(false);
});
};
return {
fetchApiKeys,
};
};
export default useApiKeys;

View file

@ -0,0 +1,42 @@
import {
DEL_KEY_ERROR_ALERT,
DEL_KEY_ERROR_ALERT_PLURAL,
DEL_KEY_SUCCESS_ALERT,
DEL_KEY_SUCCESS_ALERT_PLURAL,
} from "../../../../../constants/alerts_constants";
import { deleteApiKey } from "../../../../../controllers/API";
const useDeleteApiKeys = (
selectedRows,
resetFilter,
setSuccessData,
setErrorData,
) => {
const handleDeleteKey = () => {
Promise.all(selectedRows.map((selectedRow) => deleteApiKey(selectedRow)))
.then(() => {
resetFilter();
setSuccessData({
title:
selectedRows.length === 1
? DEL_KEY_SUCCESS_ALERT
: DEL_KEY_SUCCESS_ALERT_PLURAL,
});
})
.catch((error) => {
setErrorData({
title:
selectedRows.length === 1
? DEL_KEY_ERROR_ALERT
: DEL_KEY_ERROR_ALERT_PLURAL,
list: [error?.response?.data?.detail],
});
});
};
return {
handleDeleteKey,
};
};
export default useDeleteApiKeys;

View file

@ -0,0 +1,75 @@
import { SelectionChangedEvent } from "ag-grid-community";
import { useContext, useEffect, useRef, useState } from "react";
import TableComponent from "../../../../components/tableComponent";
import { Card, CardContent } from "../../../../components/ui/card";
import { AuthContext } from "../../../../contexts/authContext";
import useAlertStore from "../../../../stores/alertStore";
import ApiKeyHeaderComponent from "./components/ApiKeyHeader";
import { getColumnDefs } from "./helpers/column-defs";
import useApiKeys from "./hooks/use-api-keys";
import useDeleteApiKeys from "./hooks/use-handle-delete-key";
export default function ApiKeysPage() {
const [loadingKeys, setLoadingKeys] = useState(true);
const [selectedRows, setSelectedRows] = useState<string[]>([]);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData);
const { userData } = useContext(AuthContext);
const [userId, setUserId] = useState("");
const keysList = useRef([]);
useEffect(() => {
fetchApiKeys();
}, [userData]);
const { fetchApiKeys } = useApiKeys(
userData,
setLoadingKeys,
keysList,
setUserId,
);
function resetFilter() {
fetchApiKeys();
}
const { handleDeleteKey } = useDeleteApiKeys(
selectedRows,
resetFilter,
setSuccessData,
setErrorData,
);
const columnDefs = getColumnDefs();
return (
<div className="flex h-full w-full flex-col justify-between gap-6">
<ApiKeyHeaderComponent
selectedRows={selectedRows}
handleDeleteKey={handleDeleteKey}
fetchApiKeys={fetchApiKeys}
userId={userId}
/>
<div className="flex h-full w-full flex-col justify-between">
<Card x-chunk="dashboard-04-chunk-2" className="h-full pt-4">
<CardContent className="h-full">
<TableComponent
overlayNoRowsTemplate="No data available"
onSelectionChanged={(event: SelectionChangedEvent) => {
setSelectedRows(
event.api.getSelectedRows().map((row) => row.id),
);
}}
rowSelection="multiple"
suppressRowClickSelection={true}
pagination={true}
columnDefs={columnDefs}
rowData={keysList.current}
/>
</CardContent>
</Card>
</div>
</div>
);
}

View file

@ -10,7 +10,9 @@ import MessagesPage from "./pages/SettingsPage/pages/messagesPage";
const AdminPage = lazy(() => import("./pages/AdminPage")); const AdminPage = lazy(() => import("./pages/AdminPage"));
const LoginAdminPage = lazy(() => import("./pages/AdminPage/LoginPage")); const LoginAdminPage = lazy(() => import("./pages/AdminPage/LoginPage"));
const ApiKeysPage = lazy(() => import("./pages/ApiKeysPage")); const ApiKeysPage = lazy(
() => import("./pages/SettingsPage/pages/ApiKeysPage"),
);
const DeleteAccountPage = lazy(() => import("./pages/DeleteAccountPage")); const DeleteAccountPage = lazy(() => import("./pages/DeleteAccountPage"));
const FlowPage = lazy(() => import("./pages/FlowPage")); const FlowPage = lazy(() => import("./pages/FlowPage"));
const LoginPage = lazy(() => import("./pages/LoginPage")); const LoginPage = lazy(() => import("./pages/LoginPage"));
@ -77,6 +79,7 @@ const Router = () => {
> >
<Route index element={<Navigate replace to={"general"} />} /> <Route index element={<Navigate replace to={"general"} />} />
<Route path="global-variables" element={<GlobalVariablesPage />} /> <Route path="global-variables" element={<GlobalVariablesPage />} />
<Route path="api-keys" element={<ApiKeysPage />} />
<Route path="general/:scrollId?" element={<GeneralPage />} /> <Route path="general/:scrollId?" element={<GeneralPage />} />
<Route path="shortcuts" element={<ShortcutsPage />} /> <Route path="shortcuts" element={<ShortcutsPage />} />
<Route path="messages" element={<MessagesPage />} /> <Route path="messages" element={<MessagesPage />} />
@ -189,14 +192,6 @@ const Router = () => {
</ProtectedRoute> </ProtectedRoute>
} }
></Route> ></Route>
<Route
path="api-keys"
element={
<ProtectedRoute>
<ApiKeysPage />
</ProtectedRoute>
}
></Route>
</Route> </Route>
</Routes> </Routes>
</Suspense> </Suspense>

View file

@ -100,3 +100,13 @@ select:-webkit-autofill:focus {
.json-view-dark { .json-view-dark {
background-color: #141924 !important; background-color: #141924 !important;
} }
.ag-row .ag-cell {
display: flex;
align-items: center;
}
.ag-cell {
line-height: 1.25rem;
padding-top: 0.675rem;
padding-bottom: 0.675rem;
}

View file

@ -38,7 +38,7 @@ export type InputComponentType = {
export type ToggleComponentType = { export type ToggleComponentType = {
enabled: boolean; enabled: boolean;
setEnabled: (state: boolean) => void; setEnabled: (state: boolean) => void;
disabled: boolean | undefined; disabled?: boolean | undefined;
size: "small" | "medium" | "large"; size: "small" | "medium" | "large";
id?: string; id?: string;
editNode?: boolean; editNode?: boolean;
@ -393,11 +393,7 @@ export type UserInputType = {
}; };
export type ApiKeyType = { export type ApiKeyType = {
title: string;
cancelText: string;
confirmationText: string;
children: ReactElement; children: ReactElement;
icon: string;
data?: any; data?: any;
onCloseModal: () => void; onCloseModal: () => void;
}; };
@ -547,7 +543,7 @@ export type iconsType = {
export type modalHeaderType = { export type modalHeaderType = {
children: ReactNode; children: ReactNode;
description: string | null; description: string | JSX.Element | null;
}; };
export type codeAreaModalPropsType = { export type codeAreaModalPropsType = {

View file

@ -1,7 +1,7 @@
import { ColDef, ColGroupDef } from "ag-grid-community"; import { ColDef, ColGroupDef } from "ag-grid-community";
import clsx, { ClassValue } from "clsx"; import clsx, { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import TableAutoCellRender from "../components/tableAutoCellRender"; import TableAutoCellRender from "../components/tableComponent/components/tableAutoCellRender";
import { APIDataType, TemplateVariableType } from "../types/api"; import { APIDataType, TemplateVariableType } from "../types/api";
import { import {
groupedObjType, groupedObjType,
@ -345,8 +345,10 @@ export function freezeObject(obj: any) {
return JSON.parse(JSON.stringify(obj)); return JSON.parse(JSON.stringify(obj));
} }
export function isTimeStampString(str: string): boolean { export function isTimeStampString(str: string): boolean {
const timestampRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3}Z)?$/; const timestampRegexA = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3}Z)?$/;
return timestampRegex.test(str); const timestampRegexB = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{6})?$/;
return timestampRegexA.test(str) || timestampRegexB.test(str);
} }
export function extractColumnsFromRows( export function extractColumnsFromRows(

View file

@ -62,6 +62,8 @@ test("dropDownComponent", async ({ page }) => {
expect(false).toBeTruthy(); expect(false).toBeTruthy();
} }
await page.waitForTimeout(1000);
await page.getByTestId("more-options-modal").click(); await page.getByTestId("more-options-modal").click();
await page.getByTestId("edit-button-modal").click(); await page.getByTestId("edit-button-modal").click();

View file

@ -0,0 +1,99 @@
import { expect, test } from "@playwright/test";
test("should interact with api request", async ({ page }) => {
await page.goto("/");
await page.waitForTimeout(2000);
let modalCount = 0;
try {
const modalTitleElement = await page?.getByTestId("modal-title");
if (modalTitleElement) {
modalCount = await modalTitleElement.count();
}
} catch (error) {
modalCount = 0;
}
while (modalCount === 0) {
await page.getByText("New Project", { exact: true }).click();
await page.waitForTimeout(5000);
modalCount = await page.getByTestId("modal-title")?.count();
}
await page.getByTestId("blank-flow").click();
await page.waitForTimeout(1000);
await page.getByTestId("extended-disclosure").click();
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("api request");
await page.waitForTimeout(1000);
await page
.getByTestId("dataAPI Request")
.dragTo(page.locator('//*[@id="react-flow-id"]'));
await page.mouse.up();
await page.mouse.down();
await page.getByTitle("fit view").click();
await page.getByTitle("zoom out").click();
await page.getByTitle("zoom out").click();
await page.getByTitle("zoom out").click();
});
test("erase button should clear the chat messages", async ({ page }) => {
if (!process.env.CI) {
dotenv.config();
dotenv.config({ path: path.resolve(__dirname, "../../.env") });
}
await page.goto("/");
await page.waitForTimeout(1000);
let modalCount = 0;
try {
const modalTitleElement = await page?.getByTestId("modal-title");
if (modalTitleElement) {
modalCount = await modalTitleElement.count();
}
} catch (error) {
modalCount = 0;
}
while (modalCount === 0) {
await page.getByText("New Project", { exact: true }).click();
await page.waitForTimeout(5000);
modalCount = await page.getByTestId("modal-title")?.count();
}
await page.getByRole("heading", { name: "Basic Prompting" }).click();
await page.waitForTimeout(1000);
await page.getByTitle("fit view").click();
await page.getByTitle("zoom out").click();
await page.getByTitle("zoom out").click();
await page.getByTitle("zoom out").click();
if (!process.env.OPENAI_API_KEY) {
//You must set the OPENAI_API_KEY on .env file to run this test
expect(false).toBe(true);
}
await page
.getByTestId("popover-anchor-input-openai_api_key")
.fill(process.env.OPENAI_API_KEY ?? "");
await page.getByText("Playground", { exact: true }).click();
await page.getByPlaceholder("Send a message...").fill("Hello, how are you?");
await page.getByTestId("icon-LucideSend").click();
let valueUser = await page.getByTestId("sender_name_user").textContent();
let valueAI = await page.getByTestId("sender_name_ai").textContent();
expect(valueUser).toBe("User");
expect(valueAI).toBe("AI");
await page.getByTestId("icon-Eraser").last().click();
await page.getByText("Hello, how are you?").isHidden();
await page.getByText("AI", { exact: true }).last().isHidden();
await page.getByText("User", { exact: true }).last().isHidden();
await page.getByText("Start a conversation").isVisible();
await page.getByText("Langflow Chat").isVisible();
});

View file

@ -95,5 +95,33 @@ test("should see shortcuts", async ({ page }) => {
await page.getByText("Delete Component", { exact: true }).isVisible(); await page.getByText("Delete Component", { exact: true }).isVisible();
await page.getByText("Open Playground", { exact: true }).isVisible(); await page.getByText("Open Playground", { exact: true }).isVisible();
await page.getByText("Undo", { exact: true }).isVisible(); await page.getByText("Undo", { exact: true }).isVisible();
await page.getByText("Redo", { exact: true }).isVisible();
await page.mouse.wheel(0, 10000);
await page.getByText("Redo", { exact: true }).last().isVisible();
await page.getByText("Reset Columns").last().isVisible();
});
test("should interact with API Keys", async ({ page }) => {
await page.goto("/");
await page.waitForTimeout(2000);
await page.getByTestId("user-profile-settings").click();
await page.getByText("Settings").click();
await page.getByText("API Keys").click();
await page.getByText("API Keys", { exact: true }).nth(1).isVisible();
await page.getByText("Add New").click();
await page.getByPlaceholder("Insert a name for your API Key").isVisible();
const randomName = Math.random().toString(36).substring(2);
await page
.getByPlaceholder("Insert a name for your API Key")
.fill(randomName);
await page.getByText("Create Secret Key", { exact: true }).click();
await page.getByText("Please save").isVisible();
await page.getByTestId("icon-Copy").click();
await page.waitForTimeout(1000);
await page.getByText("Api Key Copied!").isVisible();
await page.getByText(randomName).isVisible();
}); });