diff --git a/graph/client/src/app/feature-projects/projects-sidebar.tsx b/graph/client/src/app/feature-projects/projects-sidebar.tsx index b3d9b181a0..8ffb660730 100644 --- a/graph/client/src/app/feature-projects/projects-sidebar.tsx +++ b/graph/client/src/app/feature-projects/projects-sidebar.tsx @@ -29,7 +29,7 @@ import { fetchProjectGraph, getProjectGraphDataService, useEnvironmentConfig, - useIntervalWhen, + usePoll, useRouteConstructor, } from '@nx/graph/shared'; import { @@ -295,24 +295,23 @@ export function ProjectsSidebar(): JSX.Element { } }, [searchParams]); - useIntervalWhen( - () => { - fetchProjectGraph( + usePoll( + async () => { + const response: ProjectGraphClientResponse = await fetchProjectGraph( projectGraphDataService, params, environmentConfig.appConfig - ).then((response: ProjectGraphClientResponse) => { - if (response.hash === lastHash) { - return; - } - projectGraphService.send({ - type: 'updateGraph', - projects: response.projects, - dependencies: response.dependencies, - fileMap: response.fileMap, - }); - setLastHash(response.hash); + ); + if (response.hash === lastHash) { + return; + } + projectGraphService.send({ + type: 'updateGraph', + projects: response.projects, + dependencies: response.dependencies, + fileMap: response.fileMap, }); + setLastHash(response.hash); }, 5000, environmentConfig.watch diff --git a/graph/client/src/app/shell.tsx b/graph/client/src/app/shell.tsx index 3905f0d983..aa18b7559b 100644 --- a/graph/client/src/app/shell.tsx +++ b/graph/client/src/app/shell.tsx @@ -16,7 +16,7 @@ import { fetchProjectGraph, getProjectGraphDataService, useEnvironmentConfig, - useIntervalWhen, + usePoll, } from '@nx/graph/shared'; import { Dropdown, Spinner } from '@nx/graph/ui-components'; import { getSystemTheme, Theme, ThemePanel } from '@nx/graph/ui-theme'; @@ -71,15 +71,15 @@ export function Shell(): JSX.Element { useLayoutEffect(() => { setErrors(routerErrors); }, [routerErrors]); - useIntervalWhen( - () => { - fetchProjectGraph( + + usePoll( + async () => { + const response: ProjectGraphClientResponse = await fetchProjectGraph( projectGraphDataService, params, environmentConfig.appConfig - ).then((response: ProjectGraphClientResponse) => { - setErrors(response.errors); - }); + ); + setErrors(response.errors); }, 1000, environmentConfig.watch diff --git a/graph/client/src/app/ui-components/error-boundary.tsx b/graph/client/src/app/ui-components/error-boundary.tsx index db3262ac1a..b9e0c38c59 100644 --- a/graph/client/src/app/ui-components/error-boundary.tsx +++ b/graph/client/src/app/ui-components/error-boundary.tsx @@ -3,7 +3,7 @@ import { fetchProjectGraph, getProjectGraphDataService, useEnvironmentConfig, - useIntervalWhen, + usePoll, } from '@nx/graph/shared'; import { ErrorRenderer } from '@nx/graph/ui-components'; import { @@ -23,20 +23,20 @@ export function ErrorBoundary() { const hasErrorData = isRouteErrorResponse(error) && error.data.errors?.length > 0; - useIntervalWhen( + usePoll( async () => { - fetchProjectGraph(projectGraphDataService, params, appConfig).then( - (data) => { - if ( - isRouteErrorResponse(error) && - error.data.id === 'project-not-found' && - data.projects.find((p) => p.name === error.data.projectName) - ) { - window.location.reload(); - } - return; - } + const data = await fetchProjectGraph( + projectGraphDataService, + params, + appConfig ); + if ( + isRouteErrorResponse(error) && + error.data.id === 'project-not-found' && + data.projects.find((p) => p.name === error.data.projectName) + ) { + window.location.reload(); + } }, 1000, watch diff --git a/graph/project-details/src/lib/project-details-page.tsx b/graph/project-details/src/lib/project-details-page.tsx index 48769ac4e7..9bcb7b7d41 100644 --- a/graph/project-details/src/lib/project-details-page.tsx +++ b/graph/project-details/src/lib/project-details-page.tsx @@ -16,7 +16,7 @@ import { fetchProjectGraph, getProjectGraphDataService, useEnvironmentConfig, - useIntervalWhen, + usePoll, } from '@nx/graph/shared'; import { ProjectDetailsHeader } from './project-details-header'; @@ -35,16 +35,16 @@ export function ProjectDetailsPage() { const projectGraphDataService = getProjectGraphDataService(); const params = useParams(); - useIntervalWhen( + usePoll( async () => { - fetchProjectGraph(projectGraphDataService, params, appConfig).then( - (data) => { - if (data?.hash !== hash) { - window.location.reload(); - } - return; - } + const data = await fetchProjectGraph( + projectGraphDataService, + params, + appConfig ); + if (data?.hash !== hash) { + window.location.reload(); + } }, 1000, watch diff --git a/graph/project-details/src/lib/project-details-wrapper.tsx b/graph/project-details/src/lib/project-details-wrapper.tsx index 56365c1c0e..3682765914 100644 --- a/graph/project-details/src/lib/project-details-wrapper.tsx +++ b/graph/project-details/src/lib/project-details-wrapper.tsx @@ -50,7 +50,8 @@ export function ProjectDetailsWrapper({ navigate( routeConstructor( `/projects/${encodeURIComponent(data.projectName)}`, - true + true, + ['expanded'] // omit expanded targets from search params ) ); } @@ -75,7 +76,8 @@ export function ProjectDetailsWrapper({ pathname: `/tasks/${encodeURIComponent(data.targetName)}`, search: `?projects=${encodeURIComponent(data.projectName)}`, }, - true + true, + ['expanded'] // omit expanded targets from search params ) ); } @@ -95,9 +97,9 @@ export function ProjectDetailsWrapper({ const updateSearchParams = ( params: URLSearchParams, - targetNames: string[] + targetNames?: string[] ) => { - if (targetNames.length === 0) { + if (!targetNames || targetNames.length === 0) { params.delete('expanded'); } else { params.set('expanded', targetNames.join(',')); @@ -118,13 +120,6 @@ export function ProjectDetailsWrapper({ if (collapseAllTargets) { collapseAllTargets(); } - setSearchParams( - (currentSearchParams) => { - currentSearchParams.delete('expanded'); - return currentSearchParams; - }, - { replace: true, preventScrollReset: true } - ); }; }, []); // only run on mount @@ -132,7 +127,7 @@ export function ProjectDetailsWrapper({ const expandedTargetsParams = searchParams.get('expanded')?.split(',') || []; - if (expandedTargetsParams.join(',') === expandedTargets.join(',')) { + if (expandedTargetsParams.join(',') === expandedTargets?.join(',')) { return; } diff --git a/graph/shared/src/index.ts b/graph/shared/src/index.ts index 224db3f90e..f7c0fee4f5 100644 --- a/graph/shared/src/index.ts +++ b/graph/shared/src/index.ts @@ -3,7 +3,7 @@ export * from './lib/external-api-service'; export * from './lib/use-environment-config'; export * from './lib/app-config'; export * from './lib/use-route-constructor'; -export * from './lib/use-interval-when'; +export * from './lib/use-poll'; export * from './lib/project-graph-data-service/get-project-graph-data-service'; export * from './lib/fetch-project-graph'; export * from './lib/error-toast'; diff --git a/graph/shared/src/lib/use-interval-when.ts b/graph/shared/src/lib/use-interval-when.ts deleted file mode 100644 index 3868db3ef8..0000000000 --- a/graph/shared/src/lib/use-interval-when.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useEffect, useRef } from 'react'; - -export const useIntervalWhen = ( - callback: () => void, - delay: number, - condition: boolean -) => { - const savedCallback = useRef(() => {}); - - useEffect(() => { - if (condition) { - savedCallback.current = callback; - } - }, [callback, condition]); - - useEffect(() => { - if (condition) { - const tick = () => { - savedCallback.current(); - }; - - if (delay !== null) { - let id = setInterval(tick, delay); - return () => clearInterval(id); - } - } - }, [delay, condition]); -}; diff --git a/graph/shared/src/lib/use-poll.ts b/graph/shared/src/lib/use-poll.ts new file mode 100644 index 0000000000..20d8e8db09 --- /dev/null +++ b/graph/shared/src/lib/use-poll.ts @@ -0,0 +1,37 @@ +import { useEffect, useRef } from 'react'; + +export const usePoll = ( + callback: () => Promise, + delay: number, + condition: boolean +) => { + const savedCallback = useRef(() => Promise.resolve()); + + useEffect(() => { + if (condition) { + savedCallback.current = callback; + } + }, [callback, condition]); + + useEffect(() => { + if (!condition) { + return; + } + let timeoutId: NodeJS.Timeout; + + async function callTickAfterDelay() { + await savedCallback.current(); + if (delay !== null) { + timeoutId = setTimeout(callTickAfterDelay, delay); + } + } + + callTickAfterDelay(); + + return () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + }; + }, [delay, condition]); +}; diff --git a/graph/shared/src/lib/use-route-constructor.ts b/graph/shared/src/lib/use-route-constructor.ts index 2bbcdb252c..be148bd3df 100644 --- a/graph/shared/src/lib/use-route-constructor.ts +++ b/graph/shared/src/lib/use-route-constructor.ts @@ -3,13 +3,23 @@ import { getEnvironmentConfig } from './use-environment-config'; export const useRouteConstructor = (): (( to: To, - retainSearchParams: boolean + retainSearchParams: boolean, + searchParamsKeysToOmit?: string[] ) => To) => { const { environment } = getEnvironmentConfig(); const { selectedWorkspaceId } = useParams(); const [searchParams] = useSearchParams(); - return (to: To, retainSearchParams: true) => { + return ( + to: To, + retainSearchParams: boolean = true, + searchParamsKeysToOmit: string[] = [] + ) => { + if (searchParamsKeysToOmit?.length) { + searchParamsKeysToOmit.forEach((key) => { + searchParams.delete(key); + }); + } let pathname = ''; if (typeof to === 'object') { diff --git a/graph/ui-graph/src/lib/tooltip-service.ts b/graph/ui-graph/src/lib/tooltip-service.ts index 796afd2975..eb32f49f6b 100644 --- a/graph/ui-graph/src/lib/tooltip-service.ts +++ b/graph/ui-graph/src/lib/tooltip-service.ts @@ -37,8 +37,8 @@ export class GraphTooltipService { if (graph.getTaskInputs) { graph.getTaskInputs(event.data.id).then((inputs) => { if ( - this.currentTooltip.type === 'taskNode' && - this.currentTooltip.props.id === event.data.id + this.currentTooltip?.type === 'taskNode' && + this.currentTooltip?.props.id === event.data.id ) { this.openTaskNodeTooltip(event.ref, { ...event.data, diff --git a/graph/ui-project-details/src/lib/target-configuration-details-header/target-configuration-details-header.tsx b/graph/ui-project-details/src/lib/target-configuration-details-header/target-configuration-details-header.tsx index ffd4f84719..9fe0174e60 100644 --- a/graph/ui-project-details/src/lib/target-configuration-details-header/target-configuration-details-header.tsx +++ b/graph/ui-project-details/src/lib/target-configuration-details-header/target-configuration-details-header.tsx @@ -129,14 +129,15 @@ export const TargetConfigurationDetailsHeader = ({ // TODO: fix tooltip overflow in collapsed state data-tooltip={isCollasped ? false : 'View in Task Graph'} data-tooltip-align-right + onClick={(e) => { + if (isCollasped) { + return; + } + e.stopPropagation(); + onViewInTaskGraph({ projectName, targetName }); + }} > - { - e.stopPropagation(); - onViewInTaskGraph({ projectName, targetName }); - }} - /> + )} diff --git a/package.json b/package.json index e4898adf97..10200d4e7c 100644 --- a/package.json +++ b/package.json @@ -249,7 +249,7 @@ "react-markdown": "^8.0.7", "react-redux": "8.0.5", "react-refresh": "^0.10.0", - "react-router-dom": "^6.21.2", + "react-router-dom": "^6.23.1", "react-textarea-autosize": "^8.5.3", "regenerator-runtime": "0.13.7", "resolve.exports": "1.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 46eb4262dc..c62ab79ec0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -822,8 +822,8 @@ devDependencies: specifier: ^0.10.0 version: 0.10.0 react-router-dom: - specifier: ^6.21.2 - version: 6.21.2(react-dom@18.3.1)(react@18.3.1) + specifier: ^6.23.1 + version: 6.23.1(react-dom@18.3.1)(react@18.3.1) react-textarea-autosize: specifier: ^8.5.3 version: 8.5.3(@types/react@18.3.1)(react@18.3.1) @@ -12051,11 +12051,6 @@ packages: typescript: 5.4.2 dev: true - /@remix-run/router@1.14.2: - resolution: {integrity: sha512-ACXpdMM9hmKZww21yEqWwiLws/UPLhNKvimN8RrYSqPSvB3ov7sLvAcfvaxePeLvccTQKGdkDIhLYApZVDFuKg==} - engines: {node: '>=14.0.0'} - dev: true - /@remix-run/router@1.15.3: resolution: {integrity: sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==} engines: {node: '>=14.0.0'} @@ -12066,6 +12061,11 @@ packages: engines: {node: '>=14.0.0'} dev: true + /@remix-run/router@1.16.1: + resolution: {integrity: sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==} + engines: {node: '>=14.0.0'} + dev: true + /@remix-run/server-runtime@2.8.1(typescript@5.4.2): resolution: {integrity: sha512-fh4SOEoONrN73Kvzc0gMDCmYpVRVbvoj9j3BUXHAcn0An8iX+HD/22gU7nTkIBzExM/F9xgEcwTewOnWqLw0Bg==} engines: {node: '>=18.0.0'} @@ -30502,26 +30502,26 @@ packages: use-sidecar: 1.1.2(@types/react@18.3.1)(react@18.3.1) dev: true - /react-router-dom@6.21.2(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-tE13UukgUOh2/sqYr6jPzZTzmzc70aGRP4pAjG2if0IP3aUT+sBtAKUJh0qMh0zylJHGLmzS+XWVaON4UklHeg==} + /react-router-dom@6.23.1(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==} engines: {node: '>=14.0.0'} peerDependencies: react: '>=16.8' react-dom: '>=16.8' dependencies: - '@remix-run/router': 1.14.2 + '@remix-run/router': 1.16.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-router: 6.21.2(react@18.3.1) + react-router: 6.23.1(react@18.3.1) dev: true - /react-router@6.21.2(react@18.3.1): - resolution: {integrity: sha512-jJcgiwDsnaHIeC+IN7atO0XiSRCrOsQAHHbChtJxmgqG2IaYQXSnhqGb5vk2CU/wBQA12Zt+TkbuJjIn65gzbA==} + /react-router@6.23.1(react@18.3.1): + resolution: {integrity: sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==} engines: {node: '>=14.0.0'} peerDependencies: react: '>=16.8' dependencies: - '@remix-run/router': 1.14.2 + '@remix-run/router': 1.16.1 react: 18.3.1 dev: true