Added API Keys into Settings, and made changes into BaseModal (#2067)

This pull request encompasses a series of updates and bug fixes aimed at
improving the overall functionality, user experience, and UI consistency
of the application. The changes include enhancements to modals, buttons,
toolbars, and various other components. Below is a summary of the key
changes made:

- Used TableComponent as default to show API Keys
- Changed Backend to show Created At
- Transferred info from API Keys page to API Keys section of the
Settings page
- Refactored API Keys adding modal
- Refactored BaseModal
This commit is contained in:
Lucas Oliveira 2024-06-06 18:31:09 +02:00 • committed by GitHub
commit 0631226cd8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 1069 additions and 1100 deletions

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,7 +29,6 @@ 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}
@ -43,6 +42,5 @@ export default function ToggleShadComponent({
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(
( (
@ -54,59 +24,49 @@ const EditNodeModal = forwardRef(
}, },
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,11 +75,12 @@ 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 h-full flex-col">
<div className="flex pb-2"> <div className="flex pb-2">
<IconComponent <IconComponent
name="Variable" name="Variable"
@ -128,513 +89,16 @@ const EditNodeModal = forwardRef(
<span className="edit-node-modal-span">Parameters</span> <span className="edit-node-modal-span">Parameters</span>
</div> </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>
<TableHead className="h-7 text-center">DESC</TableHead>
<TableHead className="h-7 p-0 text-center">
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
);
}} }}
tooltipShowDelay={0.5}
columnDefs={columnDefs}
rowData={rowData}
/> />
) : 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>

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,38 +61,60 @@ 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
className="ml-3"
onClick={() => { onClick={() => {
handleCopyClick(); handleCopyClick();
}} }}
variant="none"
size="none"
> >
{textCopied ? ( {textCopied ? (
<IconComponent name="Copy" className="h-4 w-4" /> <IconComponent name="Copy" className="h-4 w-4" />
@ -106,78 +123,29 @@ export default function SecretKeyModal({
)} )}
</Button> </Button>
</div> </div>
</div>
</> </>
)} ) : (
<Form.Field name="apikey">
<Form.Root <div className="flex items-center justify-between gap-2">
onSubmit={(event) => {
setRenderKey(true);
handleAddNewKey();
event.preventDefault();
}}
>
{renderKey === false && (
<div className="grid gap-5">
<Form.Field name="username">
<div
style={{
display: "flex",
alignItems: "baseline",
justifyContent: "space-between",
}}
>
<Form.Label className="data-[invalid]:label-invalid">
Name (optional){" "}
</Form.Label>
</div>
<Form.Control asChild> <Form.Control asChild>
<input <Input
//fake api key
id="primary-input"
value={apiKeyName}
ref={inputRef}
onChange={({ target: { value } }) => { onChange={({ target: { value } }) => {
setApiKeyName(value); setApiKeyName(value);
}} }}
value={apiKeyName} placeholder="Insert a name for your API Key"
className="primary-input"
placeholder="My key name"
/> />
</Form.Control> </Form.Control>
</div>
</Form.Field> </Form.Field>
</div>
)} )}
{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();
}); });