diff --git a/docs/docs/integrations/notion/list-users.md b/docs/docs/integrations/notion/list-users.md index a85642214..0eb8236f5 100644 --- a/docs/docs/integrations/notion/list-users.md +++ b/docs/docs/integrations/notion/list-users.md @@ -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 diff --git a/src/backend/base/langflow/services/database/models/api_key/model.py b/src/backend/base/langflow/services/database/models/api_key/model.py index cb216d9ae..157b08b32 100644 --- a/src/backend/base/langflow/services/database/models/api_key/model.py +++ b/src/backend/base/langflow/services/database/models/api_key/model.py @@ -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 diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index ef33cb95d..42eec6f76 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -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", diff --git a/src/frontend/package.json b/src/frontend/package.json index 7a56d080e..a33d73e08 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -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", diff --git a/src/frontend/src/App.css b/src/frontend/src/App.css index a4ff01961..809959757 100644 --- a/src/frontend/src/App.css +++ b/src/frontend/src/App.css @@ -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; +} diff --git a/src/frontend/src/components/dropdownComponent/index.tsx b/src/frontend/src/components/dropdownComponent/index.tsx index bff138681..0fd981603 100644 --- a/src/frontend/src/components/dropdownComponent/index.tsx +++ b/src/frontend/src/components/dropdownComponent/index.tsx @@ -33,9 +33,8 @@ export default function Dropdown({ const refButton = useRef(null); - const PopoverContentDropdown = children - ? PopoverContent - : PopoverContentWithoutPortal; + const PopoverContentDropdown = + children || editNode ? PopoverContent : PopoverContentWithoutPortal; return ( <> diff --git a/src/frontend/src/components/headerComponent/index.tsx b/src/frontend/src/components/headerComponent/index.tsx index 209445b17..50a0c8cf8 100644 --- a/src/frontend/src/components/headerComponent/index.tsx +++ b/src/frontend/src/components/headerComponent/index.tsx @@ -181,18 +181,6 @@ export default function Header(): JSX.Element { /> - {autoLogin && ( - - )} <> diff --git a/src/frontend/src/components/inputComponent/components/popover/index.tsx b/src/frontend/src/components/inputComponent/components/popover/index.tsx index 45c326c2c..06be89d91 100644 --- a/src/frontend/src/components/inputComponent/components/popover/index.tsx +++ b/src/frontend/src/components/inputComponent/components/popover/index.tsx @@ -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} /> - - + ); }; diff --git a/src/frontend/src/components/inputComponent/components/popoverObject/index.tsx b/src/frontend/src/components/inputComponent/components/popoverObject/index.tsx index d7c9e83a7..d43022845 100644 --- a/src/frontend/src/components/inputComponent/components/popoverObject/index.tsx +++ b/src/frontend/src/components/inputComponent/components/popoverObject/index.tsx @@ -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} /> - - + ); }; diff --git a/src/frontend/src/components/inputComponent/index.tsx b/src/frontend/src/components/inputComponent/index.tsx index 331f5c089..b361f48ab 100644 --- a/src/frontend/src/components/inputComponent/index.tsx +++ b/src/frontend/src/components/inputComponent/index.tsx @@ -108,6 +108,7 @@ export default function InputComponent({ setSelectedOptions={setSelectedOptions} options={objectOptions} value={value} + editNode={editNode} autoFocus={autoFocus} disabled={disabled} setShowOptions={setShowOptions} diff --git a/src/frontend/src/components/inputGlobalComponent/index.tsx b/src/frontend/src/components/inputGlobalComponent/index.tsx index 318c57518..21c93e6bf 100644 --- a/src/frontend/src/components/inputGlobalComponent/index.tsx +++ b/src/frontend/src/components/inputGlobalComponent/index.tsx @@ -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({ )} 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) => { diff --git a/src/frontend/src/components/shadTooltipComponent/index.tsx b/src/frontend/src/components/shadTooltipComponent/index.tsx index 6bfad61ee..51254cf68 100644 --- a/src/frontend/src/components/shadTooltipComponent/index.tsx +++ b/src/frontend/src/components/shadTooltipComponent/index.tsx @@ -11,7 +11,7 @@ export default function ShadTooltip({ delayDuration = 500, }: ShadToolTipType): JSX.Element { return ( - + {children} ; } - break; case "number": return ; default: diff --git a/src/frontend/src/components/tableComponent/components/tableNodeCellRender/index.tsx b/src/frontend/src/components/tableComponent/components/tableNodeCellRender/index.tsx new file mode 100644 index 000000000..37e19339b --- /dev/null +++ b/src/frontend/src/components/tableComponent/components/tableNodeCellRender/index.tsx @@ -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 ? ( + { + handleOnNewValue(value, templateData.key); + }} + /> + ) : templateData.multiline ? ( + { + handleOnNewValue(value, templateData.key); + }} + /> + ) : ( + handleOnNewValue(value, templateData.key)} + setDb={(value) => { + handleOnChangeDb(value, templateData.key); + }} + name={templateData.key} + data={templateData} + /> + ); + } else { + return ( + handleOnNewValue(value, templateData.key)} + value={templateValue ?? "Choose an option"} + id={"dropdown-edit-" + templateData.name} + /> + ); + } + + case "NestedDict": + return ( + { + handleOnNewValue(newValue, templateData.key); + }} + id="editnode-div-dict-input" + /> + ); + + case "dict": + return ( +
1 ? "my-3" : "", + )} + > + { + const valueToNumbers = convertValuesToNumbers(newValue); + setErrorDuplicateKey(hasDuplicateKeys(valueToNumbers)); + handleOnNewValue(valueToNumbers, templateData.key); + }} + isList={templateData.list ?? false} + /> +
+ ); + + case "bool": + return ( + { + handleOnNewValue(isEnabled, templateData.key); + }} + size="small" + editNode={true} + /> + ); + + case "float": + return ( + { + handleOnNewValue(value, templateData.key); + }} + /> + ); + case "int": + return ( + { + handleOnNewValue(value, templateData.key); + }} + /> + ); + + case "file": + return ( + { + handleOnNewValue(value, templateData.key); + }} + fileTypes={templateData.fileTypes} + onFileChange={(filePath: string) => { + templateData.file_path = filePath; + }} + /> + ); + + case "prompt": + return ( + { + 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 ( + { + 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 ( +
+ {getCellType()} +
+ ); +} diff --git a/src/frontend/src/components/tableComponent/components/tableToggleCellRender/index.tsx b/src/frontend/src/components/tableComponent/components/tableToggleCellRender/index.tsx new file mode 100644 index 000000000..5fcf1b875 --- /dev/null +++ b/src/frontend/src/components/tableComponent/components/tableToggleCellRender/index.tsx @@ -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 ( +
+ { + setValue(e); + setEnabled(e); + }} + size="small" + editNode={true} + /> +
+ ); +} diff --git a/src/frontend/src/components/tableComponent/components/tableTooltipRender/index.tsx b/src/frontend/src/components/tableComponent/components/tableTooltipRender/index.tsx new file mode 100644 index 000000000..d1644d623 --- /dev/null +++ b/src/frontend/src/components/tableComponent/components/tableTooltipRender/index.tsx @@ -0,0 +1,9 @@ +import { CustomTooltipProps } from "ag-grid-react"; + +export default function TableTooltipRender({ value }: CustomTooltipProps) { + return ( +
+ {value} +
+ ); +} diff --git a/src/frontend/src/components/tableComponent/index.tsx b/src/frontend/src/components/tableComponent/index.tsx index 3a44d8b0f..a4e4f4d6a 100644 --- a/src/frontend/src/components/tableComponent/index.tsx +++ b/src/frontend/src/components/tableComponent/index.tsx @@ -125,6 +125,9 @@ const TableComponent = forwardRef< }} columnDefs={colDef} ref={realRef} + getRowId={(params) => { + return params.data.id; + }} pagination={true} onGridReady={onGridReady} onColumnMoved={onColumnMoved} diff --git a/src/frontend/src/components/toggleShadComponent/index.tsx b/src/frontend/src/components/toggleShadComponent/index.tsx index ef1dc0d33..e76518f03 100644 --- a/src/frontend/src/components/toggleShadComponent/index.tsx +++ b/src/frontend/src/components/toggleShadComponent/index.tsx @@ -29,20 +29,18 @@ export default function ToggleShadComponent({ } return ( -
- { - setEnabled(isEnabled); - }} - > -
+ { + setEnabled(isEnabled); + }} + > ); } diff --git a/src/frontend/src/components/ui/refreshButton.tsx b/src/frontend/src/components/ui/refreshButton.tsx index b039772d8..a1bdc18e6 100644 --- a/src/frontend/src/components/ui/refreshButton.tsx +++ b/src/frontend/src/components/ui/refreshButton.tsx @@ -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 ( - + - )} - - { - setRenderKey(true); - handleAddNewKey(); - event.preventDefault(); - }} - > - {renderKey === false && ( -
- -
+
+ + { + setApiKeyName(value); }} - > - - Name (optional){" "} - -
- - { - setApiKeyName(value); - }} - value={apiKeyName} - className="primary-input" - placeholder="My key name" - /> - - + placeholder="Insert a name for your API Key" + /> +
- )} - {renderKey === false && ( -
- - - - - -
- )} - - {renderKey === true && ( -
- -
- )} - +
+ )} + ); } diff --git a/src/frontend/src/pages/ApiKeysPage/index.tsx b/src/frontend/src/pages/ApiKeysPage/index.tsx deleted file mode 100644 index 49f38715e..000000000 --- a/src/frontend/src/pages/ApiKeysPage/index.tsx +++ /dev/null @@ -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 ( -
- - {LAST_USED_SPAN_1} -

{LAST_USED_SPAN_2} -
-
- ); - } - - return ( - <> -
- {userData && ( -
-
-
-
-
-
-

- API keys -

-

- {API_PAGE_PARAGRAPH_1} -
- {API_PAGE_PARAGRAPH_2} -

-
-
-
- - {keysList.current && - keysList.current.length === 0 && - !loadingKeys && ( - <> -
-

{API_PAGE_USER_KEYS}

-
- - )} - <> - {loadingKeys && ( -
- Loading... -
- )} -
- {keysList.current && - keysList.current.length > 0 && - !loadingKeys && ( - - - - Name - Key - Created - - Last Used - -
- -
-
-
- Total Uses - -
-
- {!loadingKeys && ( - - {keysList.current.map( - (api_keys: ApiKey, index: number) => ( - - - - - {api_keys.name ? api_keys.name : "-"} - - - - - - {api_keys.api_key} - - - - -
- {moment(api_keys.created_at).format( - "YYYY-MM-DD HH:mm" - )} -
-
-
- - -
- {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")} -
-
-
- - {api_keys.total_uses} - - -
- { - handleDeleteKey(keys); - }} - > - - - Are you sure you want to delete - this key? This action cannot be - undone. - - - - - - -
-
-
- ) - )} -
- )} -
- )} -
- -
-
- - - -
-
- -
-
-
-
- )} - - ); -} diff --git a/src/frontend/src/pages/SettingsPage/index.tsx b/src/frontend/src/pages/SettingsPage/index.tsx index b5169d9af..d956f20f4 100644 --- a/src/frontend/src/pages/SettingsPage/index.tsx +++ b/src/frontend/src/pages/SettingsPage/index.tsx @@ -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: ( + + ), + }, { title: "Shortcuts", href: "/settings/shortcuts", diff --git a/src/frontend/src/pages/SettingsPage/pages/ApiKeysPage/components/ApiKeyHeader/index.tsx b/src/frontend/src/pages/SettingsPage/pages/ApiKeysPage/components/ApiKeyHeader/index.tsx new file mode 100644 index 000000000..cdd7c7c27 --- /dev/null +++ b/src/frontend/src/pages/SettingsPage/pages/ApiKeysPage/components/ApiKeyHeader/index.tsx @@ -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 ( + <> +
+
+

+ API Keys + +

+

{API_PAGE_PARAGRAPH}

+
+
+ + + + +
+
+ + ); +}; +export default ApiKeyHeaderComponent; diff --git a/src/frontend/src/pages/SettingsPage/pages/ApiKeysPage/helpers/column-defs.ts b/src/frontend/src/pages/SettingsPage/pages/ApiKeysPage/helpers/column-defs.ts new file mode 100644 index 000000000..9ea7af469 --- /dev/null +++ b/src/frontend/src/pages/SettingsPage/pages/ApiKeysPage/helpers/column-defs.ts @@ -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, + }, + ]; +}; diff --git a/src/frontend/src/pages/SettingsPage/pages/ApiKeysPage/hooks/use-api-keys.tsx b/src/frontend/src/pages/SettingsPage/pages/ApiKeysPage/hooks/use-api-keys.tsx new file mode 100644 index 000000000..8af9b4a0b --- /dev/null +++ b/src/frontend/src/pages/SettingsPage/pages/ApiKeysPage/hooks/use-api-keys.tsx @@ -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; diff --git a/src/frontend/src/pages/SettingsPage/pages/ApiKeysPage/hooks/use-handle-delete-key.tsx b/src/frontend/src/pages/SettingsPage/pages/ApiKeysPage/hooks/use-handle-delete-key.tsx new file mode 100644 index 000000000..74d5dae99 --- /dev/null +++ b/src/frontend/src/pages/SettingsPage/pages/ApiKeysPage/hooks/use-handle-delete-key.tsx @@ -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; diff --git a/src/frontend/src/pages/SettingsPage/pages/ApiKeysPage/index.tsx b/src/frontend/src/pages/SettingsPage/pages/ApiKeysPage/index.tsx new file mode 100644 index 000000000..8efa2e3f9 --- /dev/null +++ b/src/frontend/src/pages/SettingsPage/pages/ApiKeysPage/index.tsx @@ -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([]); + 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 ( +
+ + +
+ + + { + setSelectedRows( + event.api.getSelectedRows().map((row) => row.id), + ); + }} + rowSelection="multiple" + suppressRowClickSelection={true} + pagination={true} + columnDefs={columnDefs} + rowData={keysList.current} + /> + + +
+
+ ); +} diff --git a/src/frontend/src/routes.tsx b/src/frontend/src/routes.tsx index d14762c08..cf7fcdc7a 100644 --- a/src/frontend/src/routes.tsx +++ b/src/frontend/src/routes.tsx @@ -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 = () => { > } /> } /> + } /> } /> } /> } /> @@ -189,14 +192,6 @@ const Router = () => { } > - - - - } - > diff --git a/src/frontend/src/style/classes.css b/src/frontend/src/style/classes.css index 29140e7fa..08226b832 100644 --- a/src/frontend/src/style/classes.css +++ b/src/frontend/src/style/classes.css @@ -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; +} diff --git a/src/frontend/src/types/components/index.ts b/src/frontend/src/types/components/index.ts index 49d3728ed..ff0c941e3 100644 --- a/src/frontend/src/types/components/index.ts +++ b/src/frontend/src/types/components/index.ts @@ -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 = { diff --git a/src/frontend/src/utils/utils.ts b/src/frontend/src/utils/utils.ts index 64d35713f..52ae9310c 100644 --- a/src/frontend/src/utils/utils.ts +++ b/src/frontend/src/utils/utils.ts @@ -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( diff --git a/src/frontend/tests/end-to-end/dropdownComponent.spec.ts b/src/frontend/tests/end-to-end/dropdownComponent.spec.ts index 0846e5575..6c0a7dd9e 100644 --- a/src/frontend/tests/end-to-end/dropdownComponent.spec.ts +++ b/src/frontend/tests/end-to-end/dropdownComponent.spec.ts @@ -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(); diff --git a/src/frontend/tests/end-to-end/generalBugs.spec.ts b/src/frontend/tests/end-to-end/generalBugs.spec.ts new file mode 100644 index 000000000..49ac0af65 --- /dev/null +++ b/src/frontend/tests/end-to-end/generalBugs.spec.ts @@ -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(); +}); diff --git a/src/frontend/tests/end-to-end/userSettings.spec.ts b/src/frontend/tests/end-to-end/userSettings.spec.ts index 4516d3898..1228fffb4 100644 --- a/src/frontend/tests/end-to-end/userSettings.spec.ts +++ b/src/frontend/tests/end-to-end/userSettings.spec.ts @@ -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(); });