diff --git a/eslint.config.js b/eslint.config.js index 784b80ac3..cf59f25b6 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,5 +1,6 @@ import pluginJs from "@eslint/js"; import pluginReact from "eslint-plugin-react"; +import reactHooks from "eslint-plugin-react-hooks"; import simpleImportSort from "eslint-plugin-simple-import-sort"; import globals from "globals"; import tseslint from "typescript-eslint"; @@ -11,8 +12,20 @@ export default [ pluginJs.configs.recommended, ...tseslint.configs.recommended, pluginReact.configs.flat.recommended, + reactHooks.configs.flat.recommended, { rules: { + "react-hooks/exhaustive-deps": [ + "warn", + // I left this commented out because it causes infinite loops in the codebase, + // but may useful for mass-refactoring. + // { + // enableDangerousAutofixThisMayCauseInfiniteLoops: true, + // }, + ], + "react-hooks/set-state-in-effect": "warn", + "react-hooks/refs": "warn", + "react-hooks/immutability": "warn", "@typescript-eslint/no-explicit-any": "off", "react/react-in-jsx-scope": "off", "@typescript-eslint/no-empty-object-type": "off", diff --git a/package-lock.json b/package-lock.json index bcd8779bb..fbd0c8e2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "oasis-app", + "name": "tangleml/tangle-ui", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "oasis-app", + "name": "tangleml/tangle-ui", "version": "0.1.0", "license": "Apache-2.0", "dependencies": { @@ -75,6 +75,7 @@ "@vitest/coverage-v8": "^3.2.4", "eslint": "^9.39.1", "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.0.1", "globals": "^16.5.0", "jsdom": "^27.2.0", "knip": "^5.63.1", @@ -6301,6 +6302,26 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, "node_modules/eslint-plugin-react/node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -7181,6 +7202,23 @@ "node": ">= 0.4" } }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", diff --git a/package.json b/package.json index a84f2830c..b97ad2fc5 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui", "test:e2e:headed": "playwright test --headed", - "lint": "eslint src --config eslint.config.js", + "lint": "eslint --quiet src --config eslint.config.js", + "lint:all": "eslint src --config eslint.config.js", "format": "prettier --write .", "typecheck": "tsc --noEmit", "gh-pages": "npm run build:ghpages && gh-pages -d dist", @@ -115,6 +116,7 @@ "@vitest/coverage-v8": "^3.2.4", "eslint": "^9.39.1", "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.0.1", "globals": "^16.5.0", "jsdom": "^27.2.0", "knip": "^5.63.1", diff --git a/src/components/Editor/IOEditor/InputValueEditor/InputValueEditor.tsx b/src/components/Editor/IOEditor/InputValueEditor/InputValueEditor.tsx index 066e3ed74..f4584c008 100644 --- a/src/components/Editor/IOEditor/InputValueEditor/InputValueEditor.tsx +++ b/src/components/Editor/IOEditor/InputValueEditor/InputValueEditor.tsx @@ -141,7 +141,7 @@ export const InputValueEditor = ({ setValidationError(null); }, - [input.name, currentSubgraphSpec], + [input.name, componentSpec], ); const hasChanges = useCallback(() => { @@ -203,7 +203,7 @@ export const InputValueEditor = ({ void navigator.clipboard.writeText(inputValue.trim()); notify("Input value copied to clipboard", "success"); } - }, [inputValue]); + }, [inputValue, notify]); const deleteNode = useCallback(async () => { if (!currentSubgraphSpec.inputs) return; @@ -258,17 +258,21 @@ export const InputValueEditor = ({ }, []); useEffect(() => { - setInputValue(initialInputValue); - setInputName(input.name); - setInputType(input.type?.toString() ?? "any"); - setInputOptional(initialIsOptional); - setValidationError(null); + queueMicrotask(() => { + setInputValue(initialInputValue); + setInputName(input.name); + setInputType(input.type?.toString() ?? "any"); + setInputOptional(initialIsOptional); + setValidationError(null); + }); }, [input, initialInputValue, initialIsOptional]); useEffect(() => { if (triggerSave) { - saveChanges(); - setTriggerSave(false); + queueMicrotask(() => { + saveChanges(); + setTriggerSave(false); + }); } }, [triggerSave, saveChanges]); diff --git a/src/components/Editor/IOEditor/OutputNameEditor/OutputNameEditor.tsx b/src/components/Editor/IOEditor/OutputNameEditor/OutputNameEditor.tsx index da4258339..91eb476c0 100644 --- a/src/components/Editor/IOEditor/OutputNameEditor/OutputNameEditor.tsx +++ b/src/components/Editor/IOEditor/OutputNameEditor/OutputNameEditor.tsx @@ -122,7 +122,7 @@ export const OutputNameEditor = ({ setValidationError(null); }, - [currentSubgraphSpec, output.name], + [componentSpec, output.name], ); const deleteNode = useCallback(async () => { @@ -161,7 +161,9 @@ export const OutputNameEditor = ({ ]); useEffect(() => { - setOutputName(output.name); + queueMicrotask(() => { + setOutputName(output.name); + }); }, [output.name]); return ( diff --git a/src/components/Home/PipelineSection/BulkActionsBar.tsx b/src/components/Home/PipelineSection/BulkActionsBar.tsx index 34cd4fc0f..da07281be 100644 --- a/src/components/Home/PipelineSection/BulkActionsBar.tsx +++ b/src/components/Home/PipelineSection/BulkActionsBar.tsx @@ -37,7 +37,7 @@ const BulkActionsBar = ({ const errorMessage = getErrorMessage(error); notify("Failed to delete some pipelines: " + errorMessage, "error"); } - }, [selectedPipelines, onDeleteSuccess]); + }, [selectedPipelines, onDeleteSuccess, notify]); return (
diff --git a/src/components/Home/PipelineSection/PipelineRow.tsx b/src/components/Home/PipelineSection/PipelineRow.tsx index ae71d66e2..47f943c10 100644 --- a/src/components/Home/PipelineSection/PipelineRow.tsx +++ b/src/components/Home/PipelineSection/PipelineRow.tsx @@ -74,7 +74,7 @@ const PipelineRow = ({ }; await deletePipeline(name, deleteCallback); - }, [name]); + }, [name, onDelete]); const handleClick = useCallback((e: MouseEvent) => { // Prevent row click when clicking on the checkbox diff --git a/src/components/Home/RunSection/RunRow.tsx b/src/components/Home/RunSection/RunRow.tsx index da817b76a..80c71df1f 100644 --- a/src/components/Home/RunSection/RunRow.tsx +++ b/src/components/Home/RunSection/RunRow.tsx @@ -41,7 +41,7 @@ const RunRow = ({ run }: { run: PipelineRunResponse }) => { navigator.clipboard.writeText(createdBy); notify(`"${createdBy}" copied to clipboard`, "success"); }, - [createdBy], + [createdBy, notify], ); const statusCounts = convertExecutionStatsToStatusCounts( diff --git a/src/components/PipelineRun/components/CancelPipelineRunButton.tsx b/src/components/PipelineRun/components/CancelPipelineRunButton.tsx index 4be968e43..cb22f51e6 100644 --- a/src/components/PipelineRun/components/CancelPipelineRunButton.tsx +++ b/src/components/PipelineRun/components/CancelPipelineRunButton.tsx @@ -53,7 +53,7 @@ export const CancelPipelineRunButton = ({ } catch (error) { notify(`Error cancelling run: ${error}`, "error"); } - }, [runId, available]); + }, [runId, available, notify, cancelPipeline]); const onClick = useCallback(() => { setIsOpen(true); diff --git a/src/components/PipelineRun/components/RerunPipelineButton.tsx b/src/components/PipelineRun/components/RerunPipelineButton.tsx index 27e01e039..ab8766539 100644 --- a/src/components/PipelineRun/components/RerunPipelineButton.tsx +++ b/src/components/PipelineRun/components/RerunPipelineButton.tsx @@ -28,9 +28,12 @@ export const RerunPipelineButton = ({ const { awaitAuthorization, isAuthorized } = useAwaitAuthorization(); const { getToken } = useAuthLocalStorage(); - const onSuccess = useCallback((response: PipelineRun) => { - navigate({ to: `${APP_ROUTES.RUNS}/${response.id}` }); - }, []); + const onSuccess = useCallback( + (response: PipelineRun) => { + navigate({ to: `${APP_ROUTES.RUNS}/${response.id}` }); + }, + [navigate], + ); const onError = useCallback( (error: Error | string) => { diff --git a/src/components/shared/Authentication/AuthorizedUserProfile.tsx b/src/components/shared/Authentication/AuthorizedUserProfile.tsx index 04060e28f..37db7d70a 100644 --- a/src/components/shared/Authentication/AuthorizedUserProfile.tsx +++ b/src/components/shared/Authentication/AuthorizedUserProfile.tsx @@ -1,6 +1,6 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { LogOutIcon } from "lucide-react"; -import { useEffectEvent, useSyncExternalStore } from "react"; +import { useCallback, useSyncExternalStore } from "react"; import { Icon } from "@/components/ui/icon"; import { Spinner } from "@/components/ui/spinner"; @@ -52,10 +52,11 @@ export function AuthorizedUserProfile() { ); const profile = localTokenStorage.getJWT(); - const onLogoutSuccess = useEffectEvent(() => { + const onLogoutSuccess = useCallback(() => { queryClient.invalidateQueries({ queryKey: ["user"] }); localTokenStorage.clear(); - }); + }, [queryClient, localTokenStorage]); + const { mutate: logout, isPending } = useLogout({ onSuccess: onLogoutSuccess, }); diff --git a/src/components/shared/ComponentEditor/components/PythonComponentEditor.tsx b/src/components/shared/ComponentEditor/components/PythonComponentEditor.tsx index 1229944d3..a085d1657 100644 --- a/src/components/shared/ComponentEditor/components/PythonComponentEditor.tsx +++ b/src/components/shared/ComponentEditor/components/PythonComponentEditor.tsx @@ -77,7 +77,12 @@ export const PythonComponentEditor = withSuspenseWrapper( setValidationErrors(errors); } }, - [yamlGenerator, onComponentTextChange, yamlGeneratorOptions], + [ + yamlGenerator, + yamlGeneratorOptions, + onComponentTextChange, + onErrorsChange, + ], ); useEffect(() => { diff --git a/src/components/shared/ComponentEditor/components/YamlComponentEditor.tsx b/src/components/shared/ComponentEditor/components/YamlComponentEditor.tsx index b3142a4db..b8ce8f55c 100644 --- a/src/components/shared/ComponentEditor/components/YamlComponentEditor.tsx +++ b/src/components/shared/ComponentEditor/components/YamlComponentEditor.tsx @@ -39,7 +39,7 @@ export const YamlComponentEditor = withSuspenseWrapper( setValidationErrors([]); onErrorsChange([]); }, - [onComponentTextChange, validateComponentSpec], + [onComponentTextChange, onErrorsChange, validateComponentSpec], ); return ( diff --git a/src/components/shared/Dialogs/BackendConfigurationDialog.tsx b/src/components/shared/Dialogs/BackendConfigurationDialog.tsx index 0d517df2d..77ff3405d 100644 --- a/src/components/shared/Dialogs/BackendConfigurationDialog.tsx +++ b/src/components/shared/Dialogs/BackendConfigurationDialog.tsx @@ -109,14 +109,18 @@ const BackendConfigurationDialog = ({ }, [isConfiguredFromEnv, isConfiguredFromRelativePath, setOpen]); useEffect(() => { - setIsEnvConfig(isConfiguredFromEnv); - setIsRelativePathConfig(isConfiguredFromRelativePath); + queueMicrotask(() => { + setIsEnvConfig(isConfiguredFromEnv); + setIsRelativePathConfig(isConfiguredFromRelativePath); + }); }, [isConfiguredFromEnv, isConfiguredFromRelativePath]); useEffect(() => { - setInputBackendUrl( - isConfiguredFromEnv || isConfiguredFromRelativePath ? "" : backendUrl, - ); + queueMicrotask(() => { + setInputBackendUrl( + isConfiguredFromEnv || isConfiguredFromRelativePath ? "" : backendUrl, + ); + }); }, [isConfiguredFromEnv, isConfiguredFromRelativePath, backendUrl]); const hasBackendConfigured = diff --git a/src/components/shared/Dialogs/ComponentDetailsDialog.tsx b/src/components/shared/Dialogs/ComponentDetailsDialog.tsx index e6879bccb..1abafc53b 100644 --- a/src/components/shared/Dialogs/ComponentDetailsDialog.tsx +++ b/src/components/shared/Dialogs/ComponentDetailsDialog.tsx @@ -201,12 +201,15 @@ const ComponentDetails = ({ setIsEditDialogOpen(false); }, []); - const onOpenChange = useCallback((open: boolean) => { - setOpen(open); - if (!open) { - onClose?.(); - } - }, []); + const onOpenChange = useCallback( + (open: boolean) => { + setOpen(open); + if (!open) { + onClose?.(); + } + }, + [onClose], + ); const handleEditComponent = useCallback(() => { setIsEditDialogOpen(true); @@ -227,7 +230,7 @@ const ComponentDetails = ({ ); return [...actions, EditButton]; - }, [actions, hasEnabledInAppEditor, handleEditComponent]); + }, [hasEnabledInAppEditor, actions, handleEditComponent, displayName]); return ( <> diff --git a/src/components/shared/Dialogs/ComponentDuplicateDialog.tsx b/src/components/shared/Dialogs/ComponentDuplicateDialog.tsx index 164c4f8a8..3ccaf2dec 100644 --- a/src/components/shared/Dialogs/ComponentDuplicateDialog.tsx +++ b/src/components/shared/Dialogs/ComponentDuplicateDialog.tsx @@ -60,7 +60,7 @@ const ComponentDuplicateDialog = ({ ); setNewDigest(digest); } - }, [newComponent, newName]); + }, [newComponent, newComponentDigest, newName]); const handleOnOpenChange = useCallback( (open: boolean) => { @@ -82,7 +82,7 @@ const ComponentDuplicateDialog = ({ setClose(); }, - [handleImportComponent, setClose], + [handleImportComponent, newComponent, setClose], ); const handleReplaceAndImport = useCallback(async () => { @@ -94,7 +94,7 @@ const ComponentDuplicateDialog = ({ handleImportComponent(yamlString); setClose(); - }, [handleImportComponent, setClose]); + }, [existingComponent?.name, handleImportComponent, newComponent, setClose]); const handleCancel = useCallback(() => { setClose(); @@ -118,7 +118,7 @@ const ComponentDuplicateDialog = ({ } generateNewDigest(); - }, [existingComponent, newComponent]); + }, [existingComponent, newComponent, newComponentDigest]); return ( diff --git a/src/components/shared/Dialogs/InputDialog.tsx b/src/components/shared/Dialogs/InputDialog.tsx index 2bf92c3a9..3c26de305 100644 --- a/src/components/shared/Dialogs/InputDialog.tsx +++ b/src/components/shared/Dialogs/InputDialog.tsx @@ -68,15 +68,19 @@ export function InputDialog({ useEffect(() => { if (isOpen) { - setValue(defaultValue); - setError(null); + queueMicrotask(() => { + setValue(defaultValue); + setError(null); + }); } }, [isOpen, defaultValue]); useEffect(() => { if (isOpen && defaultValue && validate) { const validationError = validate(defaultValue.trim()); - setError(validationError); + queueMicrotask(() => { + setError(validationError); + }); } }, [isOpen, defaultValue, validate]); diff --git a/src/components/shared/FavoriteComponentToggle.tsx b/src/components/shared/FavoriteComponentToggle.tsx index 9bc190d7e..23c1e9fac 100644 --- a/src/components/shared/FavoriteComponentToggle.tsx +++ b/src/components/shared/FavoriteComponentToggle.tsx @@ -122,12 +122,12 @@ export const ComponentFavoriteToggle = ({ const onFavorite = useCallback(() => { setComponentFavorite(component, !isFavorited); - }, [isFavorited, setComponentFavorite]); + }, [component, isFavorited, setComponentFavorite]); // Delete User Components const handleDelete = useCallback(async () => { removeFromComponentLibrary(component); - }, [removeFromComponentLibrary]); + }, [component, removeFromComponentLibrary]); /* Confirmation Dialog handlers */ const openConfirmationDialog = useCallback(() => { diff --git a/src/components/shared/FullscreenElement/FullscreenElement.tsx b/src/components/shared/FullscreenElement/FullscreenElement.tsx index 8bacca63b..aad519ded 100644 --- a/src/components/shared/FullscreenElement/FullscreenElement.tsx +++ b/src/components/shared/FullscreenElement/FullscreenElement.tsx @@ -19,7 +19,7 @@ function FullscreenElementPortal({ fullscreen, defaultMountElement, }: FullscreenElementProps) { - const id = useRef(Math.random().toString(15).substring(2, 15)); + const id = useRef(crypto.randomUUID().toString()); const containerElementRef = useRef( document.createElement("div"), ); diff --git a/src/components/shared/GitHubAuth/useGitHubAuthPopup.ts b/src/components/shared/GitHubAuth/useGitHubAuthPopup.ts index b1b3c4b2c..d1ba60d44 100644 --- a/src/components/shared/GitHubAuth/useGitHubAuthPopup.ts +++ b/src/components/shared/GitHubAuth/useGitHubAuthPopup.ts @@ -70,6 +70,19 @@ export function useGitHubAuthPopup({ const popupRef = useRef(null); const intervalRef = useRef(null); + const closePopup = useCallback(() => { + setIsLoading(false); + + if (popupRef.current) { + popupRef.current.close(); + } + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + setIsPopupOpen(false); + onClose?.(); + }, [onClose]); + const openPopup = useCallback(() => { if (popupRef.current && !popupRef.current.closed) { popupRef.current.focus(); @@ -137,20 +150,7 @@ export function useGitHubAuthPopup({ // We'll continue monitoring until popup closes or returns to our domain } }, 1000); - }, [onError, onSuccess, onClose]); - - const closePopup = useCallback(() => { - setIsLoading(false); - - if (popupRef.current) { - popupRef.current.close(); - } - if (intervalRef.current) { - clearInterval(intervalRef.current); - } - setIsPopupOpen(false); - onClose?.(); - }, [onClose]); + }, [onError, closePopup, onSuccess]); const bringPopupToFront = useCallback(() => { if (popupRef.current && !popupRef.current.closed) { diff --git a/src/components/shared/HuggingFaceAuth/HuggingFaceAuthButton.tsx b/src/components/shared/HuggingFaceAuth/HuggingFaceAuthButton.tsx index 03177f4f6..75ebfb38c 100644 --- a/src/components/shared/HuggingFaceAuth/HuggingFaceAuthButton.tsx +++ b/src/components/shared/HuggingFaceAuth/HuggingFaceAuthButton.tsx @@ -34,7 +34,7 @@ function useSyncAuthStorageWithUserDetails() { } else { authStorage.clear(); } - }, [user]); + }, [authStorage, user]); } const HuggingFaceAuthButtonComponent = withSuspenseWrapper(() => { diff --git a/src/components/shared/HuggingFaceAuth/useHuggingFaceAuthPopup.ts b/src/components/shared/HuggingFaceAuth/useHuggingFaceAuthPopup.ts index 11ac046fc..3ae2500a4 100644 --- a/src/components/shared/HuggingFaceAuth/useHuggingFaceAuthPopup.ts +++ b/src/components/shared/HuggingFaceAuth/useHuggingFaceAuthPopup.ts @@ -1,12 +1,5 @@ import { useQueryClient } from "@tanstack/react-query"; -import { - useCallback, - useEffect, - useEffectEvent, - useMemo, - useRef, - useState, -} from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { GetUserResponse } from "@/api/types.gen"; import type { @@ -58,12 +51,12 @@ export function useHuggingFaceAuthPopup({ typeof setInterval > | null>(null); - const cleanup = useEffectEvent(() => { + const cleanup = useCallback(() => { if (pollAuthorizationInfoIntervalRef.current) { clearInterval(pollAuthorizationInfoIntervalRef.current); pollAuthorizationInfoIntervalRef.current = null; } - }); + }, []); const closePopup = useCallback(() => { setIsLoading(false); @@ -72,14 +65,17 @@ export function useHuggingFaceAuthPopup({ setIsPopupOpen(false); onClose?.(); - }, [onClose]); - - const onErrorStateHandler = useEffectEvent((error: string) => { - onError(error); - closePopup(); - }); + }, [cleanup, onClose]); + + const onErrorStateHandler = useCallback( + (error: string) => { + onError(error); + closePopup(); + }, + [onError, closePopup], + ); - const pollAuthorizationInfo = useEffectEvent(async () => { + const pollAuthorizationInfo = useCallback(async () => { return queryClient .fetchQuery({ queryKey: ["user"], queryFn: getUserDetails, staleTime: 0 }) .then((user) => { @@ -102,7 +98,7 @@ export function useHuggingFaceAuthPopup({ .finally(() => { queryClient.invalidateQueries({ queryKey: ["user"] }); }); - }); + }, [queryClient, onErrorStateHandler, onSuccess, closePopup]); /** * In Hugging Face auth flow, the App is embedded in an iframe, rendering it as 3rd party origin. @@ -136,7 +132,7 @@ export function useHuggingFaceAuthPopup({ useEffect(() => { return () => cleanup(); - }, []); + }, [cleanup]); const bringPopupToFront = useCallback(() => { // no-op diff --git a/src/components/shared/ReactFlow/FlowCanvas/IONode/IONode.tsx b/src/components/shared/ReactFlow/FlowCanvas/IONode/IONode.tsx index 6e1d39d3c..81881fe75 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/IONode/IONode.tsx +++ b/src/components/shared/ReactFlow/FlowCanvas/IONode/IONode.tsx @@ -88,7 +88,17 @@ const IONode = ({ type, data, selected = false }: IONodeProps) => { clearContent(); } }; - }, [input, output, selected, readOnly]); + }, [ + input, + output, + selected, + readOnly, + isInput, + isOutput, + setContent, + currentGraphSpec, + clearContent, + ]); const connectedOutput = getOutputConnectedDetails( currentGraphSpec, diff --git a/src/components/shared/ReactFlow/FlowCanvas/SubgraphBreadcrumbs/SubgraphBreadcrumbs.tsx b/src/components/shared/ReactFlow/FlowCanvas/SubgraphBreadcrumbs/SubgraphBreadcrumbs.tsx index 670b16bcb..a2a066639 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/SubgraphBreadcrumbs/SubgraphBreadcrumbs.tsx +++ b/src/components/shared/ReactFlow/FlowCanvas/SubgraphBreadcrumbs/SubgraphBreadcrumbs.tsx @@ -15,13 +15,14 @@ import { InlineStack } from "@/components/ui/layout"; import { buildExecutionUrl } from "@/hooks/useSubgraphBreadcrumbs"; import { useComponentSpec } from "@/providers/ComponentSpecProvider"; import { useExecutionDataOptional } from "@/providers/ExecutionDataProvider"; +import { EMPTY } from "@/utils/constants"; export const SubgraphBreadcrumbs = () => { const navigate = useNavigate(); const { currentSubgraphPath, navigateToPath } = useComponentSpec(); const executionData = useExecutionDataOptional(); const rootExecutionId = executionData?.rootExecutionId; - const segments = executionData?.segments || []; + const segments = executionData?.segments || EMPTY.Array; const getExecutionIdForIndex = useCallback( (targetIndex: number): string | undefined => { diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/AnnotationsEditor/AnnotationsInput.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/AnnotationsEditor/AnnotationsInput.tsx index 7c2aabdaa..ce1a81e51 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/AnnotationsEditor/AnnotationsInput.tsx +++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/AnnotationsEditor/AnnotationsInput.tsx @@ -69,25 +69,28 @@ export const AnnotationsInput = ({ setIsDialogOpen(false); }, []); - const validateChange = useCallback((e: ChangeEvent) => { - const newValue = e.target.value; - - setInputValue(newValue); + const validateChange = useCallback( + (e: ChangeEvent) => { + const newValue = e.target.value; - if (newValue === "") { - setIsInvalid(false); - return; - } + setInputValue(newValue); - if (config?.type === "json") { - try { - JSON.parse(newValue); + if (newValue === "") { setIsInvalid(false); - } catch { - setIsInvalid(true); + return; } - } - }, []); + + if (config?.type === "json") { + try { + JSON.parse(newValue); + setIsInvalid(false); + } catch { + setIsInvalid(true); + } + } + }, + [config?.type], + ); const handleQuantityKeyInputChange = useCallback( (e: ChangeEvent) => { diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/AnnotationsEditor/AnnotationsSection.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/AnnotationsEditor/AnnotationsSection.tsx index 80c856a4f..17cd6b86b 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/AnnotationsEditor/AnnotationsSection.tsx +++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/AnnotationsEditor/AnnotationsSection.tsx @@ -5,6 +5,7 @@ import { Separator } from "@/components/ui/separator"; import useToastNotification from "@/hooks/useToastNotification"; import type { Annotations } from "@/types/annotations"; import type { TaskSpec } from "@/utils/componentSpec"; +import { EMPTY } from "@/utils/constants"; import { AnnotationsEditor } from "./AnnotationsEditor"; import { ComputeResourcesEditor } from "./ComputeResourcesEditor"; @@ -21,7 +22,7 @@ export const AnnotationsSection = ({ }: AnnotationsSectionProps) => { const notify = useToastNotification(); - const rawAnnotations = (taskSpec.annotations || {}) as Annotations; + const rawAnnotations = (taskSpec.annotations || EMPTY.Obj) as Annotations; const [annotations, setAnnotations] = useState({ ...rawAnnotations, diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentInputField.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentInputField.tsx index a7266591d..078d8b7cf 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentInputField.tsx +++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/ArgumentInputField.tsx @@ -60,7 +60,7 @@ export const ArgumentInputField = ({ const [isTextareaDialogOpen, setIsTextareaDialogOpen] = useState(false); - const undoValue = useMemo(() => argument, []); + const undoValue = useMemo(() => argument, [argument]); const hint = argument.inputSpec.annotations?.hint as string | undefined; const handleInputChange = (e: ChangeEvent) => { @@ -86,7 +86,7 @@ export const ArgumentInputField = ({ onSave(updatedArgument); setLastSubmittedValue(value); }, - [inputValue, lastSubmittedValue, argument, onSave], + [lastSubmittedValue, argument, onSave], ); const handleRemove = () => { @@ -174,7 +174,13 @@ export const ArgumentInputField = ({ "error", ); }); - }, [inputValue, disabled, argument]); + }, [ + disabled, + argument.isRemoved, + argument.inputSpec.name, + inputValue, + notify, + ]); const canUndo = useMemo( () => !equal(argument, undoValue), @@ -201,10 +207,12 @@ export const ArgumentInputField = ({ useEffect(() => { const value = getInputValue(argument); if (value !== undefined && value !== inputValue) { - setInputValue(value); - setLastSubmittedValue(value); + queueMicrotask(() => { + setInputValue(value); + setLastSubmittedValue(value); + }); } - }, [argument]); + }, [argument, inputValue]); const disabledCopy = useMemo( () => disabled || argument.isRemoved || inputValue === "", diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/Handles.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/Handles.tsx index 2d12ad9f7..2a9c58b64 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/Handles.tsx +++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/Handles.tsx @@ -73,9 +73,13 @@ export const InputHandle = ({ (fromHandle === handleId && fromNode === nodeId) || (toHandle === handleId && toNode === nodeId) ) { - setActive(true); + queueMicrotask(() => { + setActive(true); + }); } else { - setActive(false); + queueMicrotask(() => { + setActive(false); + }); } }, [fromHandle, fromNode, toHandle, toNode, handleId, nodeId]); @@ -256,9 +260,13 @@ export const OutputHandle = ({ (fromHandle === handleId && fromNode === nodeId) || (toHandle === handleId && toNode === nodeId) ) { - setActive(true); + queueMicrotask(() => { + setActive(true); + }); } else { - setActive(false); + queueMicrotask(() => { + setActive(false); + }); } }, [fromHandle, fromNode, toHandle, toNode, handleId, nodeId]); diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/TaskNodeCard.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/TaskNodeCard.tsx index 0e49b0535..27134f62f 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/TaskNodeCard.tsx +++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/TaskNodeCard.tsx @@ -181,14 +181,16 @@ const TaskNodeCard = () => { return ; }, [ - nodeId, readOnly, + isSubgraphNode, + taskId, + isSubgraphNavigationEnabled, + isInAppEditorEnabled, + taskNode, + nodeId, callbacks.onDuplicate, callbacks.onUpgrade, - isInAppEditorEnabled, isCustomComponent, - isSubgraphNode, - taskId, subgraphDescription, navigateToSubgraph, handleEditComponent, @@ -231,9 +233,11 @@ const TaskNodeCard = () => { }, []); useEffect(() => { - if (contentRef.current && scrollHeight > 0 && dimensions.h) { - setCondensed(scrollHeight > dimensions.h); - } + queueMicrotask(() => { + if (contentRef.current && scrollHeight > 0 && dimensions.h) { + setCondensed(scrollHeight > dimensions.h); + } + }); }, [scrollHeight, dimensions.h]); useEffect(() => { @@ -246,7 +250,7 @@ const TaskNodeCard = () => { clearContent(); } }; - }, [selected, taskConfigMarkup, setContent, clearContent]); + }, [selected, taskConfigMarkup, setContent, clearContent, isDragging]); if (!taskSpec) { return null; diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/IOCell.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/IOCell.tsx index 64152b1be..80208866c 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/IOCell.tsx +++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/IOCell.tsx @@ -65,7 +65,7 @@ const IOCell = ({ io, artifactData }: IOCellProps) => { if (!artifactData?.value) return; handleCopy(artifactData.value, "Value"); - }, [artifactData?.value, handleCopy]); + }, [artifactData, handleCopy]); useEffect(() => { return () => { diff --git a/src/components/shared/ReactFlow/FlowSidebar/components/ComponentItem.tsx b/src/components/shared/ReactFlow/FlowSidebar/components/ComponentItem.tsx index 1c7575748..591a497e3 100644 --- a/src/components/shared/ReactFlow/FlowSidebar/components/ComponentItem.tsx +++ b/src/components/shared/ReactFlow/FlowSidebar/components/ComponentItem.tsx @@ -109,7 +109,12 @@ const ComponentMarkup = ({ type: "highlight", }); }); - }, [digest, isHighlightTasksOnComponentHoverEnabled]); + }, [ + digest, + getNodeIdsByDigest, + isHighlightTasksOnComponentHoverEnabled, + notifyNode, + ]); const onMouseLeave = useCallback(() => { if (!isHighlightTasksOnComponentHoverEnabled) return; @@ -122,7 +127,12 @@ const ComponentMarkup = ({ type: "clear", }); }); - }, [digest, isHighlightTasksOnComponentHoverEnabled]); + }, [ + digest, + getNodeIdsByDigest, + isHighlightTasksOnComponentHoverEnabled, + notifyNode, + ]); const onMouseClick = useCallback(() => { if (!isHighlightTasksOnComponentHoverEnabled) return; @@ -215,10 +225,10 @@ const ComponentMarkup = ({ }; const ComponentItemFromUrl = ({ url }: ComponentItemFromUrlProps) => { - if (!url) return null; - const { isLoading, error, componentRef } = useComponentFromUrl(url); + if (!url) return null; + if (!componentRef.spec) { componentRef.spec = EMPTY_GRAPH_COMPONENT_SPEC; } diff --git a/src/components/shared/ReactFlow/FlowSidebar/components/ImportComponent.tsx b/src/components/shared/ReactFlow/FlowSidebar/components/ImportComponent.tsx index 73ad35760..52d1c1aed 100644 --- a/src/components/shared/ReactFlow/FlowSidebar/components/ImportComponent.tsx +++ b/src/components/shared/ReactFlow/FlowSidebar/components/ImportComponent.tsx @@ -121,7 +121,7 @@ const ImportComponent = ({ } else if (tab === TabType.File && selectedFile) { onImportFromFile(selectedFile as string); } - }, [tab, url, selectedFile]); + }, [tab, selectedFile, onImportFromUrl, url, onImportFromFile]); const handleUrlChange = useCallback( (event: ChangeEvent) => { diff --git a/src/components/shared/ReactFlow/FlowSidebar/components/PublishedComponentsSearch.tsx b/src/components/shared/ReactFlow/FlowSidebar/components/PublishedComponentsSearch.tsx index bf32615ef..8eaabc1c9 100644 --- a/src/components/shared/ReactFlow/FlowSidebar/components/PublishedComponentsSearch.tsx +++ b/src/components/shared/ReactFlow/FlowSidebar/components/PublishedComponentsSearch.tsx @@ -179,21 +179,14 @@ const SearchRequestInput = ({ value, onChange }: SearchRequestProps) => { [], ); - const debouncedOnChange = useCallback( - debounce((searchRequest: LibraryFilterRequest) => { - onChange(searchRequest); - }, DEBOUNCE_TIME_MS), - [onChange], - ); - const [searchRequest, dispatch] = useReducer(searchRequestReducer, { searchTerm: value, filters: DEFAULT_ACTIVE_FILTERS, }); useEffect(() => { - debouncedOnChange(searchRequest); - }, [searchRequest, debouncedOnChange]); + debounce(onChange, DEBOUNCE_TIME_MS)(searchRequest); + }, [onChange, searchRequest]); const onFiltersChange = useCallback((filters: string[]) => { dispatch({ type: "SET_FILTERS", payload: filters }); diff --git a/src/components/shared/ReactFlow/FlowSidebar/sections/FileActions.tsx b/src/components/shared/ReactFlow/FlowSidebar/sections/FileActions.tsx index 7a5a8e640..3ca733505 100644 --- a/src/components/shared/ReactFlow/FlowSidebar/sections/FileActions.tsx +++ b/src/components/shared/ReactFlow/FlowSidebar/sections/FileActions.tsx @@ -88,7 +88,7 @@ const FileActions = ({ isOpen }: { isOpen: boolean }) => { return componentSpec?.name ? `${componentSpec.name} (Copy)` : `Untitled Pipeline ${new Date().toLocaleTimeString()}`; - }, [componentSpec?.name]); + }, [componentSpec]); useEffect(() => { const fetchLastSaved = async () => { diff --git a/src/components/shared/ReactFlow/FlowSidebar/sections/GraphComponents.tsx b/src/components/shared/ReactFlow/FlowSidebar/sections/GraphComponents.tsx index 422c03cc7..efb237538 100644 --- a/src/components/shared/ReactFlow/FlowSidebar/sections/GraphComponents.tsx +++ b/src/components/shared/ReactFlow/FlowSidebar/sections/GraphComponents.tsx @@ -147,14 +147,15 @@ const GraphComponents = ({ isOpen }: { isOpen: boolean }) => { ); }, [ - componentLibrary, - usedComponentsFolder, - userComponentsFolder, - favoritesFolder, isLoading, error, - searchResult, + componentLibrary, remoteComponentLibrarySearchEnabled, + searchResult, + usedComponentsFolder, + favoritesFolder, + userComponentsFolder, + handleFiltersChange, ]); if (!isOpen) { diff --git a/src/components/shared/Settings/useBetaFlagReducer.ts b/src/components/shared/Settings/useBetaFlagReducer.ts index 836729cfa..8917092b6 100644 --- a/src/components/shared/Settings/useBetaFlagReducer.ts +++ b/src/components/shared/Settings/useBetaFlagReducer.ts @@ -27,20 +27,23 @@ export function useBetaFlagsReducer(betaFlags: BetaFlags) { .forEach((flag) => removeFlag(flag)); }, [betaFlags, getFlags, removeFlag]); - const reducer = useCallback((state: State, action: Action) => { - switch (action.type) { - case "setFlag": - setFlag(action.payload.key, action.payload.enabled); - - return state.map((flag) => - flag.key === action.payload.key - ? { ...flag, enabled: action.payload.enabled } - : flag, - ); - default: - return state; - } - }, []); + const reducer = useCallback( + (state: State, action: Action) => { + switch (action.type) { + case "setFlag": + setFlag(action.payload.key, action.payload.enabled); + + return state.map((flag) => + flag.key === action.payload.key + ? { ...flag, enabled: action.payload.enabled } + : flag, + ); + default: + return state; + } + }, + [setFlag], + ); return useReducer( reducer, diff --git a/src/components/shared/Submitters/GoogleCloud/GoogleCloudSubmitter.tsx b/src/components/shared/Submitters/GoogleCloud/GoogleCloudSubmitter.tsx index cb41da8e2..b6e20b452 100644 --- a/src/components/shared/Submitters/GoogleCloud/GoogleCloudSubmitter.tsx +++ b/src/components/shared/Submitters/GoogleCloud/GoogleCloudSubmitter.tsx @@ -31,14 +31,14 @@ const GoogleCloudSubmitter = ({ componentSpec }: GoogleCloudSubmitterProps) => { (e: ChangeEvent) => { updateConfig({ googleCloudOAuthClientId: e.target.value }); }, - [], + [updateConfig], ); const handleDirectoryInputChange = useCallback( (e: ChangeEvent) => { updateConfig({ gcsOutputDirectory: e.target.value }); }, - [], + [updateConfig], ); return ( diff --git a/src/components/shared/Submitters/GoogleCloud/RegionInput.tsx b/src/components/shared/Submitters/GoogleCloud/RegionInput.tsx index c5d147701..ced5c65f4 100644 --- a/src/components/shared/Submitters/GoogleCloud/RegionInput.tsx +++ b/src/components/shared/Submitters/GoogleCloud/RegionInput.tsx @@ -29,9 +29,12 @@ interface RegionInputProps { } export const RegionInput = ({ config, onChange }: RegionInputProps) => { - const handleSelectChange = useCallback((value: string) => { - onChange({ region: value }); - }, []); + const handleSelectChange = useCallback( + (value: string) => { + onChange({ region: value }); + }, + [onChange], + ); return (
diff --git a/src/components/ui/resize-handle.tsx b/src/components/ui/resize-handle.tsx index 78ae15dad..48eb6f9b2 100644 --- a/src/components/ui/resize-handle.tsx +++ b/src/components/ui/resize-handle.tsx @@ -47,7 +47,6 @@ export const VerticalResizeHandle = ({ resizingRef.current = null; document.removeEventListener("mousemove", handleMouseMove); - document.removeEventListener("mouseup", handleMouseUp); }, [handleMouseMove]); const handleMouseDown = useCallback( @@ -63,7 +62,7 @@ export const VerticalResizeHandle = ({ }; document.addEventListener("mousemove", handleMouseMove); - document.addEventListener("mouseup", handleMouseUp); + document.addEventListener("mouseup", handleMouseUp, { once: true }); }, [handleMouseMove, handleMouseUp], ); diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx index 3dfb69c26..cd1c3389e 100644 --- a/src/components/ui/sidebar.tsx +++ b/src/components/ui/sidebar.tsx @@ -607,6 +607,8 @@ function SidebarMenuSkeleton({ }) { // Random width between 50 to 90%. const width = React.useMemo(() => { + // todo: fix this, we dont really need random width, we can just use a fixed width + // eslint-disable-next-line react-hooks/purity return `${Math.floor(Math.random() * 40) + 50}%`; }, []); diff --git a/src/hooks/useComponentFromUrl.ts b/src/hooks/useComponentFromUrl.ts index 1569d52ac..f1b0ca42d 100644 --- a/src/hooks/useComponentFromUrl.ts +++ b/src/hooks/useComponentFromUrl.ts @@ -90,7 +90,7 @@ const useComponentFromUrl = (url?: string) => { text: componentText, url: url, }), - [componentSpec, componentDigest, componentText], + [componentSpec, componentDigest, componentText, url], ); return { isLoading, error, componentRef }; diff --git a/src/hooks/useComponentSpecToEdges.ts b/src/hooks/useComponentSpecToEdges.ts index 48c9c3190..c83ed34bd 100644 --- a/src/hooks/useComponentSpecToEdges.ts +++ b/src/hooks/useComponentSpecToEdges.ts @@ -33,7 +33,7 @@ const useComponentSpecToEdges = ( useEffect(() => { const newEdges = getEdges(componentSpec); setFlowEdges(newEdges); - }, [componentSpec]); + }, [componentSpec, setFlowEdges]); return { edges: flowEdges, diff --git a/src/hooks/useDebouncedState.ts b/src/hooks/useDebouncedState.ts index 83b2e8e7b..c318c293c 100644 --- a/src/hooks/useDebouncedState.ts +++ b/src/hooks/useDebouncedState.ts @@ -49,7 +49,13 @@ export function useDebouncedState( }, debounceMs); return clearDebounce; - }, [currentState, debounceMs, onStateChange, shouldIgnoreChange]); + }, [ + clearDebounce, + currentState, + debounceMs, + onStateChange, + shouldIgnoreChange, + ]); return { clearDebounce, updatePreviousState }; } diff --git a/src/hooks/useGhostNode.ts b/src/hooks/useGhostNode.ts index 0ab08544e..1ee1ac532 100644 --- a/src/hooks/useGhostNode.ts +++ b/src/hooks/useGhostNode.ts @@ -101,8 +101,11 @@ export const useGhostNode = () => { useEffect(() => { if (!connectionInProgress) { - setTabCycleIndex(-1); - setHighlightedComponentDigest(null); + // todo: one of the suggested workarounds to fix "react-hooks/set-state-in-effect" + queueMicrotask(() => { + setTabCycleIndex(-1); + setHighlightedComponentDigest(null); + }); } }, [connectionInProgress, setHighlightedComponentDigest]); diff --git a/src/hooks/useGoogleCloudSubmitter.ts b/src/hooks/useGoogleCloudSubmitter.ts index 791dae909..7ad790d70 100644 --- a/src/hooks/useGoogleCloudSubmitter.ts +++ b/src/hooks/useGoogleCloudSubmitter.ts @@ -124,7 +124,7 @@ export const useGoogleCloudSubmitter = ({ result: "failed", }); } - }, [config.googleCloudOAuthClientId]); + }, [config.googleCloudOAuthClientId, notify]); const submit = useCallback(async () => { if (vertexPipelineJob === undefined) { @@ -157,6 +157,9 @@ export const useGoogleCloudSubmitter = ({ .toLowerCase() .replace(/[^-a-z0-9]/g, "-") .replace(/^-+/, ""); // No leading dashes + + // todo: fix this? + // eslint-disable-next-line react-hooks/immutability vertexPipelineJob.displayName = displayName; const result = await aiplatformCreatePipelineJob( config.projectId, @@ -179,7 +182,15 @@ export const useGoogleCloudSubmitter = ({ result: "failed", }); } - }, [vertexPipelineJob, config, componentSpec]); + }, [ + vertexPipelineJob, + componentSpec?.name, + config.projectId, + config.region, + config.googleCloudOAuthClientId, + config.gcsOutputDirectory, + notify, + ]); useEffect(() => { if (componentSpec !== undefined) { @@ -220,7 +231,14 @@ export const useGoogleCloudSubmitter = ({ URL.revokeObjectURL(jsonBlobUrl); } }; - }, [componentSpec, pipelineArguments, config.gcsOutputDirectory]); + }, [ + componentSpec, + pipelineArguments, + config.gcsOutputDirectory, + vertexPipelineJob, + notify, + jsonBlobUrl, + ]); return { config, diff --git a/src/hooks/useHintNode.ts b/src/hooks/useHintNode.ts index 7dfab100d..5bd39d550 100644 --- a/src/hooks/useHintNode.ts +++ b/src/hooks/useHintNode.ts @@ -73,9 +73,8 @@ export const useHintNode = ({ key, hint }: { key: string; hint: string }) => { }, [ isRemoteComponentLibraryEnabled, shouldShowHint, - connectionTo?.x, - connectionTo?.y, - connectionFromHandle, + connectionTo, + connectionFromHandle?.id, key, hint, ]); diff --git a/src/hooks/useHistoryManager.ts b/src/hooks/useHistoryManager.ts index 925cbe4d6..506de1cc1 100644 --- a/src/hooks/useHistoryManager.ts +++ b/src/hooks/useHistoryManager.ts @@ -63,10 +63,7 @@ export function useHistoryManager( setCurrentIndex(-1); }, []); - const canUndo = useMemo( - () => currentIndex > 0, - [currentIndex, history.length], - ); + const canUndo = useMemo(() => currentIndex > 0, [currentIndex]); const canRedo = useMemo( () => currentIndex < history.length - 1, diff --git a/src/hooks/useLoadPipelineRuns.ts b/src/hooks/useLoadPipelineRuns.ts index 023ac8673..6dadc15fd 100644 --- a/src/hooks/useLoadPipelineRuns.ts +++ b/src/hooks/useLoadPipelineRuns.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { fetchExecutionStatus } from "@/services/executionService"; import { fetchPipelineRuns } from "@/services/pipelineRunService"; @@ -8,7 +8,7 @@ const useLoadPipelineRuns = (pipelineName: string, backendUrl: string) => { const [pipelineRuns, setPipelineRuns] = useState([]); const [latestRun, setLatestRun] = useState(null); - const fetchData = async () => { + const fetchData = useCallback(async () => { if (!pipelineName) return; const res = await fetchPipelineRuns(pipelineName); @@ -27,11 +27,12 @@ const useLoadPipelineRuns = (pipelineName: string, backendUrl: string) => { } setPipelineRuns(res.runs); - }; + }, [pipelineName, backendUrl]); + // todo: replace with useQuery useEffect(() => { fetchData(); - }, [pipelineName]); + }, [fetchData, pipelineName]); return { pipelineRuns, latestRun, refetch: fetchData }; }; diff --git a/src/hooks/useNodeCallbacks.ts b/src/hooks/useNodeCallbacks.ts index 590c1d9c5..ebe40b9c2 100644 --- a/src/hooks/useNodeCallbacks.ts +++ b/src/hooks/useNodeCallbacks.ts @@ -104,7 +104,7 @@ export const useNodeCallbacks = ({ onElementsRemove(params); } }, - [triggerConfirmation, onElementsRemove, getNodeById], + [getNodeById, reactFlowInstance, triggerConfirmation, onElementsRemove], ); const setArguments = useCallback( diff --git a/src/hooks/useSubgraphBreadcrumbs.ts b/src/hooks/useSubgraphBreadcrumbs.ts index 30135625a..5e5b7e919 100644 --- a/src/hooks/useSubgraphBreadcrumbs.ts +++ b/src/hooks/useSubgraphBreadcrumbs.ts @@ -4,7 +4,7 @@ import { useMemo } from "react"; import { useBackend } from "@/providers/BackendProvider"; import { fetchExecutionDetails } from "@/services/executionService"; import { isGraphImplementationOutput } from "@/utils/componentSpec"; -import { ONE_MINUTE_IN_MS } from "@/utils/constants"; +import { EMPTY, ONE_MINUTE_IN_MS } from "@/utils/constants"; export interface BreadcrumbSegment { taskId: string; @@ -100,7 +100,7 @@ export const useSubgraphBreadcrumbs = ( retry: 1, }); - const segments = data?.segments || []; + const segments = data?.segments || EMPTY.Array; const path = useMemo(() => { return ["root", ...segments.map((seg) => seg.taskId)]; }, [segments]); diff --git a/src/providers/BackendProvider.tsx b/src/providers/BackendProvider.tsx index deee1b5ec..037762725 100644 --- a/src/providers/BackendProvider.tsx +++ b/src/providers/BackendProvider.tsx @@ -124,14 +124,14 @@ export const BackendProvider = ({ children }: { children: ReactNode }) => { return false; }); }, - [backendUrl], + [backendUrl, notify], ); useEffect(() => { if (settingsLoaded) { ping({ notifyResult: false }); } - }, [backendUrl, settingsLoaded]); + }, [backendUrl, ping, settingsLoaded]); useEffect(() => { const getSettings = async () => { @@ -147,7 +147,7 @@ export const BackendProvider = ({ children }: { children: ReactNode }) => { setSettingsLoaded(true); }; getSettings(); - }, []); + }, [backendUrlFromEnv]); const contextValue = useMemo( () => ({ diff --git a/src/providers/ComponentLibraryProvider/ComponentLibraryProvider.tsx b/src/providers/ComponentLibraryProvider/ComponentLibraryProvider.tsx index 1b016cdc1..1ab8dbc24 100644 --- a/src/providers/ComponentLibraryProvider/ComponentLibraryProvider.tsx +++ b/src/providers/ComponentLibraryProvider/ComponentLibraryProvider.tsx @@ -378,12 +378,7 @@ export const ComponentLibraryProvider = ({ console.error("Error importing component:", error); }); }, - [ - refreshUserComponents, - refreshComponentLibrary, - importComponent, - newComponent, - ], + [refreshUserComponents, refreshComponentLibrary, newComponent], ); const addToComponentLibrary = useCallback( @@ -412,12 +407,7 @@ export const ComponentLibraryProvider = ({ console.error("Error adding component to library:", error); }); }, - [ - userComponentsFolder, - refreshComponentLibrary, - refreshUserComponents, - importComponent, - ], + [userComponentsFolder, refreshComponentLibrary, refreshUserComponents], ); const removeFromComponentLibrary = useCallback( @@ -481,7 +471,7 @@ export const ComponentLibraryProvider = ({ (libraryName: AvailableComponentLibraries) => { return getComponentLibraryObject(libraryName); }, - [], + [getComponentLibraryObject], ); const isLoading = isLibraryLoading || isUserComponentsLoading; diff --git a/src/providers/ContextPanelProvider.tsx b/src/providers/ContextPanelProvider.tsx index c08aa0c86..440181021 100644 --- a/src/providers/ContextPanelProvider.tsx +++ b/src/providers/ContextPanelProvider.tsx @@ -61,7 +61,7 @@ export const ContextPanelProvider = ({ return () => { document.removeEventListener("keydown", handleKeyDown); }; - }, [defaultContent]); + }, [clearContent, defaultContent, setNodes]); const value = useMemo( () => ({ content, setContent, clearContent }), diff --git a/src/providers/PipelineRunsProvider.tsx b/src/providers/PipelineRunsProvider.tsx index f583fcdc9..d57173952 100644 --- a/src/providers/PipelineRunsProvider.tsx +++ b/src/providers/PipelineRunsProvider.tsx @@ -166,13 +166,7 @@ export const PipelineRunsProvider = ({ }, }); }, - [ - backendUrl, - refetch, - isAuthorized, - awaitAuthorization, - isAuthorizationRequired, - ], + [backendUrl, refetch, isAuthorized, awaitAuthorization], ); useEffect(() => { @@ -190,7 +184,7 @@ export const PipelineRunsProvider = ({ submit, setRecentRunsCount, }), - [runs, recentRuns, isLoading, error, refetch, submit, setRecentRunsCount], + [runs, recentRuns, isLoading, isSubmitting, error, refetch, submit], ); return ( diff --git a/src/providers/TaskNodeProvider.tsx b/src/providers/TaskNodeProvider.tsx index 10d1a85b4..e08e899ce 100644 --- a/src/providers/TaskNodeProvider.tsx +++ b/src/providers/TaskNodeProvider.tsx @@ -10,10 +10,12 @@ import type { RunStatus } from "@/types/pipelineRun"; import type { TaskNodeData, TaskNodeDimensions } from "@/types/taskNode"; import type { ArgumentType, + ComponentReference, InputSpec, OutputSpec, TaskSpec, } from "@/utils/componentSpec"; +import { EMPTY } from "@/utils/constants"; import { getComponentName } from "@/utils/getComponentName"; import { taskIdToNodeId } from "@/utils/nodes/nodeIdUtils"; @@ -77,9 +79,9 @@ export const TaskNodeProvider = ({ const taskId = data.taskId; const nodeId = taskId ? taskIdToNodeId(taskId) : ""; - const componentRef = taskSpec?.componentRef || {}; - const inputs = componentRef.spec?.inputs || []; - const outputs = componentRef.spec?.outputs || []; + const componentRef: ComponentReference = taskSpec?.componentRef || EMPTY.Obj; + const inputs = componentRef.spec?.inputs || EMPTY.Array; + const outputs = componentRef.spec?.outputs || EMPTY.Array; const name = getComponentName(componentRef); @@ -152,9 +154,10 @@ export const TaskNodeProvider = ({ }), [ selected, + data.isGhost, data.highlighted, data.readOnly, - data.isGhost, + data.connectable, status, isCustomComponent, dimensions, @@ -173,6 +176,7 @@ export const TaskNodeProvider = ({ [ handleSetArguments, handleSetAnnotations, + handleSetCacheStaleness, handleDeleteTaskNode, handleDuplicateTaskNode, handleUpgradeTaskNode, diff --git a/src/utils/constants.ts b/src/utils/constants.ts index dc6919dc6..7aada6dd3 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -77,3 +77,8 @@ export const TWENTY_FOUR_HOURS_IN_MS = 24 * 60 * 60 * 1000; export const ROOT_TASK_ID = "root"; export const ISO8601_DURATION_ZERO_DAYS = "P0D"; + +export const EMPTY = Object.freeze({ + Array: [], // todo freeze this + Obj: Object.freeze({}), +});