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)
The `NotionUserList` component enables you to:
The `NotionUserList` component enables you to:
- Retrieve user data from Notion
- Access user information such as ID, type, name, and avatar URL

View file

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

View file

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

View file

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

View file

@ -164,3 +164,13 @@ body {
.ag-body-vertical-scroll-viewport::-webkit-scrollbar-thumb:hover {
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 PopoverContentDropdown = children
? PopoverContent
: PopoverContentWithoutPortal;
const PopoverContentDropdown =
children || editNode ? PopoverContent : PopoverContentWithoutPortal;
return (
<>

View file

@ -181,18 +181,6 @@ export default function Header(): JSX.Element {
/>
</div>
</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" />

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,11 +1,11 @@
import { CustomCellRendererProps } from "ag-grid-react";
import { cn, isTimeStampString } from "../../utils/utils";
import ArrayReader from "../arrayReaderComponent";
import DateReader from "../dateReaderComponent";
import NumberReader from "../numberReader";
import ObjectRender from "../objectRender";
import StringReader from "../stringReaderComponent";
import { Badge } from "../ui/badge";
import { cn, isTimeStampString } from "../../../../utils/utils";
import ArrayReader from "../../../arrayReaderComponent";
import DateReader from "../../../dateReaderComponent";
import NumberReader from "../../../numberReader";
import ObjectRender from "../../../objectRender";
import StringReader from "../../../stringReaderComponent";
import { Badge } from "../../../ui/badge";
export default function TableAutoCellRender({
value,
@ -43,7 +43,6 @@ export default function TableAutoCellRender({
} else {
return <StringReader string={value} />;
}
break;
case "number":
return <NumberReader number={value} />;
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}
ref={realRef}
getRowId={(params) => {
return params.data.id;
}}
pagination={true}
onGridReady={onGridReady}
onColumnMoved={onColumnMoved}

View file

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

View file

@ -31,11 +31,7 @@ function RefreshButton({
// icon class name should take into account the disabled state and the loading state
const disabledIconTextClass = disabled ? "text-muted-foreground" : "";
const iconClassName = cn(
"h-4 w-4",
isLoading ? "animate-spin" : "animate-wiggle",
disabledIconTextClass
);
const iconClassName = cn("h-4 w-4 animate-wiggle", disabledIconTextClass);
return (
<Button
@ -44,10 +40,11 @@ function RefreshButton({
className={classNames}
onClick={handleClick}
id={id}
loading={isLoading}
>
{button_text && <span className="mr-1">{button_text}</span>}
<IconComponent
name={isLoading ? "Loader2" : "RefreshCcw"}
name={"RefreshCcw"}
className={iconClassName}
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 SIGNIN_ERROR_ALERT = "Error signing in";
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 WRONG_FILE_ERROR_ALERT = "Invalid file type";
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_ADD_SUCCESS_ALERT = "Success! New user added!";
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 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 API_PAGE_PARAGRAPH_1 =
"Your secret API keys are listed below. Please note that we do not display your secret API keys again after you generate them.";
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_PARAGRAPH =
"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_USER_KEYS =
"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 refHtml = useRef<HTMLDivElement & ReactNode>(null);
const infoHtml = useRef<HTMLDivElement & ReactNode>(null);
const setErrorData = useAlertStore((state) => state.setErrorData);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const nodes = useFlowStore((state) => state.nodes);
const edges = useFlowStore((state) => state.edges);
@ -392,7 +391,7 @@ export default function ParameterComponent({
});
}}
name={name}
data={data}
data={data.node?.template[name]}
/>
</div>
{data.node?.template[name]?.refresh_button && (
@ -448,8 +447,8 @@ export default function ParameterComponent({
data.node?.template[name]?.real_time_refresh)
}
>
<div className="mt-2 flex w-full items-center">
<div className="w-5/6 flex-grow">
<div className="mt-2 flex w-full items-center gap-2">
<div className="flex-1">
<Dropdown
disabled={disabled}
isLoading={isLoading}
@ -467,7 +466,6 @@ export default function ParameterComponent({
name={name}
data={data}
button_text={data.node?.template[name]?.refresh_button_text}
className="extra-side-bar-buttons ml-2 mt-1"
handleUpdateValues={handleRefreshButtonPress}
id={"refresh-button-" + name}
/>

View file

@ -1,4 +1,5 @@
import { cloneDeep } from "lodash";
import emojiRegex from "emoji-regex";
import { useCallback, useEffect, useMemo, useState } from "react";
import { NodeToolbar, useUpdateNodeInternals } from "reactflow";
import IconComponent from "../../components/genericIconComponent";
@ -220,8 +221,7 @@ export default function GenericNode({
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 iconElement = data?.node?.icon;

View file

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

View file

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

View file

@ -20,6 +20,7 @@ import { Button } from "../../components/ui/button";
import { modalHeaderType } from "../../types/components";
import { cn } from "../../utils/utils";
import { switchCaseModalSize } from "./helpers/switch-case-size";
import * as Form from "@radix-ui/react-form";
type ContentProps = { children: ReactNode };
type HeaderProps = { children: ReactNode; description: string };
@ -52,10 +53,10 @@ const Trigger: React.FC<TriggerProps> = ({
);
};
const Header: React.FC<{ children: ReactNode; description: string | null }> = ({
children,
description,
}: modalHeaderType): JSX.Element => {
const Header: React.FC<{
children: ReactNode;
description: string | JSX.Element | null;
}> = ({ children, description }: modalHeaderType): JSX.Element => {
return (
<DialogHeader>
<DialogTitle className="flex items-center">{children}</DialogTitle>
@ -111,6 +112,7 @@ interface BaseModalProps {
| "smaller"
| "small"
| "medium"
| "medium-tall"
| "large"
| "three-cards"
| "large-thin"
@ -179,31 +181,33 @@ function BaseModal({
) : (
<Dialog open={open} onOpenChange={setOpen}>
{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">
{headerChild}
</div>
{onSubmit ? (
<form
<Form.Root
onSubmit={(event) => {
event.preventDefault();
onSubmit();
}}
className="flex flex-col gap-6"
className="flex flex-1 flex-col gap-6"
>
<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}
</div>
{ContentFooter && (
<div className="flex flex-row-reverse">{ContentFooter}</div>
)}
</form>
</Form.Root>
) : (
<>
<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}
</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 { forwardRef, useEffect, useState } from "react";
import CodeAreaComponent from "../../components/codeAreaComponent";
import DictComponent from "../../components/dictComponent";
import Dropdown from "../../components/dropdownComponent";
import FloatComponent from "../../components/floatComponent";
import { ColDef, GridApi } from "ag-grid-community";
import { forwardRef, useEffect, useRef, useState } from "react";
import IconComponent from "../../components/genericIconComponent";
import InputFileComponent from "../../components/inputFileComponent";
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 TableComponent from "../../components/tableComponent";
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 { NodeDataType } from "../../types/flow";
import {
convertObjToArray,
convertValuesToNumbers,
hasDuplicateKeys,
scapedJSONStringfy,
} from "../../utils/reactflowUtils";
import { classNames } from "../../utils/utils";
import BaseModal from "../baseModal";
import useColumnDefs from "./hooks/use-column-defs";
import useRowData from "./hooks/use-row-data";
const EditNodeModal = forwardRef(
(
@ -52,61 +22,51 @@ const EditNodeModal = forwardRef(
setOpen: (open: boolean) => void;
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);
function changeAdvanced(n) {
setMyData((old) => {
let newData = cloneDeep(old);
newData.node!.template[n].advanced =
!newData.node!.template[n].advanced;
return newData;
});
myData.current.node!.template[n].advanced =
!myData.current.node!.template[n]?.advanced;
}
const handleOnNewValue = (newValue: any, name) => {
setMyData((old) => {
let newData = cloneDeep(old);
newData.node!.template[name].value = newValue;
return newData;
});
myData.current.node!.template[name].value = newValue;
};
const rowData = useRowData(data, open);
const columnDefs: ColDef[] = useColumnDefs(
data,
handleOnNewValue,
changeAdvanced,
open
);
const [gridApi, setGridApi] = useState<GridApi | null>(null);
useEffect(() => {
if (open) {
setMyData(data); // reset data to what it is on node when opening modal
if (gridApi && open) {
myData.current = data;
gridApi.refreshCells();
}
}, [open]);
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
const type = (templateParam) => {
return myData.node?.template[templateParam].type;
};
}, [gridApi, open]);
return (
<BaseModal
key={data.id}
size="large-h-full"
size="medium-tall"
open={open}
setOpen={setOpen}
onChangeOpenModal={(open) => {
setMyData(data);
}}
onSubmit={() => {
setNode(data.id, (old) => ({
...old,
data: {
...old.data,
node: myData.node,
node: myData.current.node,
},
}));
setOpen(false);
@ -115,526 +75,30 @@ const EditNodeModal = forwardRef(
<BaseModal.Trigger>
<></>
</BaseModal.Trigger>
<BaseModal.Header description={myData.node?.description!}>
<span className="pr-2">{myData.type}</span>
<Badge variant="secondary">ID: {myData.id}</Badge>
<BaseModal.Header description={data.node?.description!}>
<span className="pr-2">{data.type}</span>
<Badge variant="secondary">ID: {data.id}</Badge>
</BaseModal.Header>
<BaseModal.Content>
<div className="flex pb-2">
<IconComponent
name="Variable"
className="edit-node-modal-variable "
/>
<span className="edit-node-modal-span">Parameters</span>
</div>
<div className="flex h-full flex-col">
<div className="flex pb-2">
<IconComponent
name="Variable"
className="edit-node-modal-variable "
/>
<span className="edit-node-modal-span">Parameters</span>
</div>
<div className="edit-node-modal-arrangement">
<div
className={classNames(
"edit-node-modal-box",
nodeLength > limitScrollFieldsModal
? "overflow-scroll overflow-x-hidden custom-scroll"
: "",
)}
>
<div className="h-full">
{nodeLength > 0 && (
<div className="edit-node-modal-table">
<Table className="table-fixed bg-muted outline-1">
<TableHeader className="edit-node-modal-table-header">
<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,
);
}}
/>
) : 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>
<TableComponent
onGridReady={(params) => {
setGridApi(params.api);
}}
tooltipShowDelay={0.5}
columnDefs={columnDefs}
rowData={rowData}
/>
)}
</div>
</div>
@ -643,7 +107,7 @@ const EditNodeModal = forwardRef(
<BaseModal.Footer submit={{ label: "Save Changes" }} />
</BaseModal>
);
},
}
);
export default EditNodeModal;

View file

@ -11,15 +11,10 @@ import { nodeIconsLucide } from "../../utils/styleUtils";
import BaseModal from "../baseModal";
export default function SecretKeyModal({
title,
cancelText,
confirmationText,
children,
icon,
data,
onCloseModal,
}: ApiKeyType) {
const Icon: any = nodeIconsLucide[icon];
const [open, setOpen] = useState(false);
const [apiKeyName, setApiKeyName] = useState(data?.apikeyname ?? "");
const [apiKeyValue, setApiKeyValue] = useState("");
@ -66,118 +61,91 @@ export default function SecretKeyModal({
.catch((err) => {});
}
function handleSubmitForm() {
if (!renderKey) {
setRenderKey(true);
handleAddNewKey();
} else {
setOpen(false);
}
}
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.Header description={""}>
<span className="pr-2">{title}</span>
<Icon
name="icon"
className="h-6 w-6 pl-1 text-foreground"
aria-hidden="true"
/>
</BaseModal.Header>
<BaseModal.Content>
{renderKey === true && (
<>
<span className="text-xs">
<BaseModal.Header
description={
renderKey ? (
<>
{" "}
Please save this secret key somewhere safe and accessible. For
security reasons,{" "}
<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
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">
<Input ref={inputRef} readOnly={true} value={apiKeyValue} />
</div>
<div>
<Button
className="ml-3"
onClick={() => {
handleCopyClick();
}}
>
{textCopied ? (
<IconComponent name="Copy" className="h-4 w-4" />
) : (
<IconComponent name="Check" className="h-4 w-4" />
)}
</Button>
</div>
<Button
onClick={() => {
handleCopyClick();
}}
variant="none"
size="none"
>
{textCopied ? (
<IconComponent name="Copy" className="h-4 w-4" />
) : (
<IconComponent name="Check" className="h-4 w-4" />
)}
</Button>
</div>
</>
)}
<Form.Root
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.Field name="apikey">
<div className="flex items-center justify-between gap-2">
<Form.Control asChild>
<Input
//fake api key
id="primary-input"
value={apiKeyName}
ref={inputRef}
onChange={({ target: { value } }) => {
setApiKeyName(value);
}}
>
<Form.Label className="data-[invalid]:label-invalid">
Name (optional){" "}
</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>
placeholder="Insert a name for your API Key"
/>
</Form.Control>
</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>
</Form.Field>
)}
</BaseModal.Content>
<BaseModal.Footer
submit={{ label: renderKey ? "Done" : "Create Secret Key" }}
/>
</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",
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",
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 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 FlowPage = lazy(() => import("./pages/FlowPage"));
const LoginPage = lazy(() => import("./pages/LoginPage"));
@ -77,6 +79,7 @@ const Router = () => {
>
<Route index element={<Navigate replace to={"general"} />} />
<Route path="global-variables" element={<GlobalVariablesPage />} />
<Route path="api-keys" element={<ApiKeysPage />} />
<Route path="general/:scrollId?" element={<GeneralPage />} />
<Route path="shortcuts" element={<ShortcutsPage />} />
<Route path="messages" element={<MessagesPage />} />
@ -189,14 +192,6 @@ const Router = () => {
</ProtectedRoute>
}
></Route>
<Route
path="api-keys"
element={
<ProtectedRoute>
<ApiKeysPage />
</ProtectedRoute>
}
></Route>
</Route>
</Routes>
</Suspense>

View file

@ -100,3 +100,13 @@ select:-webkit-autofill:focus {
.json-view-dark {
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 = {
enabled: boolean;
setEnabled: (state: boolean) => void;
disabled: boolean | undefined;
disabled?: boolean | undefined;
size: "small" | "medium" | "large";
id?: string;
editNode?: boolean;
@ -393,11 +393,7 @@ export type UserInputType = {
};
export type ApiKeyType = {
title: string;
cancelText: string;
confirmationText: string;
children: ReactElement;
icon: string;
data?: any;
onCloseModal: () => void;
};
@ -547,7 +543,7 @@ export type iconsType = {
export type modalHeaderType = {
children: ReactNode;
description: string | null;
description: string | JSX.Element | null;
};
export type codeAreaModalPropsType = {

View file

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

View file

@ -62,6 +62,8 @@ test("dropDownComponent", async ({ page }) => {
expect(false).toBeTruthy();
}
await page.waitForTimeout(1000);
await page.getByTestId("more-options-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("Open Playground", { 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();
});