diff --git a/graph/client-e2e/src/support/app.po.ts b/graph/client-e2e/src/support/app.po.ts index 33c5a80199d6a..9230cdc3fb7f2 100644 --- a/graph/client-e2e/src/support/app.po.ts +++ b/graph/client-e2e/src/support/app.po.ts @@ -1,4 +1,5 @@ -export const getSelectProjectsMessage = () => cy.get('#no-projects-chosen'); +export const getSelectProjectsMessage = () => + cy.get('[data-cy=no-projects-selected]'); export const getGraph = () => cy.get('#graph-container'); export const getSelectAllButton = () => cy.get('[data-cy=selectAllButton]'); export const getDeselectAllButton = () => cy.get('[data-cy=deselectAllButton]'); diff --git a/graph/client/src/app/app.tsx b/graph/client/src/app/app.tsx index c5bc5b1005cf3..de2f2d4ebfcc8 100644 --- a/graph/client/src/app/app.tsx +++ b/graph/client/src/app/app.tsx @@ -1,10 +1,9 @@ -import { GlobalStateProvider } from './state.provider'; import { themeInit } from './theme-resolver'; import { rankDirInit } from './rankdir-resolver'; import { createBrowserRouter, - RouterProvider, createHashRouter, + RouterProvider, } from 'react-router-dom'; import { routes } from './routes'; import { getEnvironmentConfig } from './hooks/use-environment-config'; @@ -22,11 +21,7 @@ if (environmentConfig.localMode === 'build') { const router = routerCreate(routes); export function App() { - return ( - - - - ); + return ; } export default App; diff --git a/graph/client/src/app/external-api.ts b/graph/client/src/app/external-api.ts new file mode 100644 index 0000000000000..3b15d90a68855 --- /dev/null +++ b/graph/client/src/app/external-api.ts @@ -0,0 +1,13 @@ +import { getProjectGraphService } from './machines/get-services'; + +export class ExternalApi { + projectGraphService = getProjectGraphService(); + + focusProject(projectName: string) { + this.projectGraphService.send({ type: 'focusProject', projectName }); + } + + enableExperimentalFeatures() { + window.appConfig.showExperimentalFeatures = true; + } +} diff --git a/graph/client/src/app/feature-projects/hooks/use-project-graph-selector.ts b/graph/client/src/app/feature-projects/hooks/use-project-graph-selector.ts new file mode 100644 index 0000000000000..50a36d35d7b76 --- /dev/null +++ b/graph/client/src/app/feature-projects/hooks/use-project-graph-selector.ts @@ -0,0 +1,18 @@ +import { useSelector } from '@xstate/react'; +import { ProjectGraphState } from '../machines/interfaces'; +import { getProjectGraphService } from '../../machines/get-services'; + +export type ProjectGraphSelector = ( + projectGraphState: ProjectGraphState +) => T; + +export function useProjectGraphSelector( + selectorFunc: ProjectGraphSelector +): T { + const projectGraphService = getProjectGraphService(); + + return useSelector( + projectGraphService, + selectorFunc + ); +} diff --git a/graph/client/src/app/machines/custom-selected.state.ts b/graph/client/src/app/feature-projects/machines/custom-selected.state.ts similarity index 90% rename from graph/client/src/app/machines/custom-selected.state.ts rename to graph/client/src/app/feature-projects/machines/custom-selected.state.ts index 2bed077f526e9..08c4d083953af 100644 --- a/graph/client/src/app/machines/custom-selected.state.ts +++ b/graph/client/src/app/feature-projects/machines/custom-selected.state.ts @@ -1,8 +1,8 @@ import { assign } from '@xstate/immer'; import { actions, send } from 'xstate'; -import { DepGraphStateNodeConfig } from './interfaces'; +import { ProjectGraphStateNodeConfig } from './interfaces'; -export const customSelectedStateConfig: DepGraphStateNodeConfig = { +export const customSelectedStateConfig: ProjectGraphStateNodeConfig = { entry: actions.choose([ { cond: 'selectActionCannotBePersistedToRoute', diff --git a/graph/client/src/app/machines/focused.state.ts b/graph/client/src/app/feature-projects/machines/focused.state.ts similarity index 94% rename from graph/client/src/app/machines/focused.state.ts rename to graph/client/src/app/feature-projects/machines/focused.state.ts index 79a83b983a66e..722a98a64285f 100644 --- a/graph/client/src/app/machines/focused.state.ts +++ b/graph/client/src/app/feature-projects/machines/focused.state.ts @@ -1,8 +1,8 @@ import { assign } from '@xstate/immer'; import { send } from 'xstate'; -import { DepGraphStateNodeConfig } from './interfaces'; +import { ProjectGraphStateNodeConfig } from './interfaces'; -export const focusedStateConfig: DepGraphStateNodeConfig = { +export const focusedStateConfig: ProjectGraphStateNodeConfig = { entry: [ assign((ctx, event) => { if (event.type !== 'focusProject') return; diff --git a/graph/client/src/app/machines/graph.actor.ts b/graph/client/src/app/feature-projects/machines/graph.actor.ts similarity index 60% rename from graph/client/src/app/machines/graph.actor.ts rename to graph/client/src/app/feature-projects/machines/graph.actor.ts index d97961c125df3..89261ff2b1ec9 100644 --- a/graph/client/src/app/machines/graph.actor.ts +++ b/graph/client/src/app/feature-projects/machines/graph.actor.ts @@ -1,10 +1,11 @@ -import { getGraphService } from './graph.service'; +import { getGraphService } from '../../machines/graph.service'; export const graphActor = (callback, receive) => { const graphService = getGraphService(); receive((e) => { - const { selectedProjectNames, perfReport } = graphService.handleEvent(e); + const { selectedProjectNames, perfReport } = + graphService.handleProjectEvent(e); callback({ type: 'setSelectedProjectsFromGraph', selectedProjectNames, diff --git a/graph/client/src/app/feature-projects/machines/interfaces.ts b/graph/client/src/app/feature-projects/machines/interfaces.ts new file mode 100644 index 0000000000000..e919c4f5b6ec1 --- /dev/null +++ b/graph/client/src/app/feature-projects/machines/interfaces.ts @@ -0,0 +1,113 @@ +import { GraphPerfReport } from '../../interfaces'; +// nx-ignore-next-line +import { + ProjectGraphDependency, + ProjectGraphProjectNode, +} from 'nx/src/config/project-graph'; +import { ActionObject, ActorRef, State, StateNodeConfig } from 'xstate'; +import { GraphRenderEvents, RouteEvents } from '../../machines/interfaces'; + +// The hierarchical schema for the states +export interface ProjectGraphSchema { + states: { + idle: {}; + unselected: {}; + focused: {}; + textFiltered: {}; + customSelected: {}; + tracing: {}; + }; +} + +export type TracingAlgorithmType = 'shortest' | 'all'; + +// The events that the machine handles +export type ProjectGraphMachineEvents = + | { + type: 'setSelectedProjectsFromGraph'; + selectedProjectNames: string[]; + perfReport: GraphPerfReport; + } + | { type: 'selectProject'; projectName: string } + | { type: 'deselectProject'; projectName: string } + | { type: 'selectProjects'; projectNames: string[] } + | { type: 'deselectProjects'; projectNames: string[] } + | { type: 'selectAll' } + | { type: 'deselectAll' } + | { type: 'selectAffected' } + | { type: 'setGroupByFolder'; groupByFolder: boolean } + | { type: 'setTracingStart'; projectName: string } + | { type: 'setTracingEnd'; projectName: string } + | { type: 'clearTraceStart' } + | { type: 'clearTraceEnd' } + | { type: 'setTracingAlgorithm'; algorithm: TracingAlgorithmType } + | { type: 'setCollapseEdges'; collapseEdges: boolean } + | { type: 'setIncludeProjectsByPath'; includeProjectsByPath: boolean } + | { type: 'incrementSearchDepth' } + | { type: 'decrementSearchDepth' } + | { type: 'setSearchDepthEnabled'; searchDepthEnabled: boolean } + | { type: 'setSearchDepth'; searchDepth: number } + | { type: 'focusProject'; projectName: string } + | { type: 'unfocusProject' } + | { type: 'filterByText'; search: string } + | { type: 'clearTextFilter' } + | { + type: 'notifyProjectGraphSetProjects'; + projects: ProjectGraphProjectNode[]; + dependencies: Record; + affectedProjects: string[]; + workspaceLayout: { + libsDir: string; + appsDir: string; + }; + } + | { + type: 'updateGraph'; + projects: ProjectGraphProjectNode[]; + dependencies: Record; + }; + +// The context (extended state) of the machine +export interface ProjectGraphContext { + projects: ProjectGraphProjectNode[]; + dependencies: Record; + affectedProjects: string[]; + selectedProjects: string[]; + focusedProject: string | null; + textFilter: string; + includePath: boolean; + searchDepth: number; + searchDepthEnabled: boolean; + groupByFolder: boolean; + collapseEdges: boolean; + workspaceLayout: { + libsDir: string; + appsDir: string; + }; + graphActor: ActorRef; + routeSetterActor: ActorRef; + routeListenerActor: ActorRef; + lastPerfReport: GraphPerfReport; + tracing: { + start: string; + end: string; + algorithm: TracingAlgorithmType; + }; +} + +export type ProjectGraphStateNodeConfig = StateNodeConfig< + ProjectGraphContext, + {}, + ProjectGraphMachineEvents, + ActionObject +>; + +export type ProjectGraphState = State< + ProjectGraphContext, + ProjectGraphMachineEvents, + any, + { + value: any; + context: ProjectGraphContext; + } +>; diff --git a/graph/client/src/app/machines/project-graph.machine.ts b/graph/client/src/app/feature-projects/machines/project-graph.machine.ts similarity index 97% rename from graph/client/src/app/machines/project-graph.machine.ts rename to graph/client/src/app/feature-projects/machines/project-graph.machine.ts index 6b5bf0fa006ed..b0d9a9e17ec17 100644 --- a/graph/client/src/app/machines/project-graph.machine.ts +++ b/graph/client/src/app/feature-projects/machines/project-graph.machine.ts @@ -1,17 +1,13 @@ import { assign } from '@xstate/immer'; -import { createMachine, Machine, send, spawn } from 'xstate'; +import { createMachine, send, spawn } from 'xstate'; import { customSelectedStateConfig } from './custom-selected.state'; import { focusedStateConfig } from './focused.state'; import { graphActor } from './graph.actor'; -import { - ProjectGraphContext, - ProjectGraphSchema, - ProjectGraphEvents, -} from './interfaces'; -import { createRouteMachine } from './route-setter.machine'; +import { createRouteMachine } from '../../machines/route-setter.machine'; import { textFilteredStateConfig } from './text-filtered.state'; import { tracingStateConfig } from './tracing.state'; import { unselectedStateConfig } from './unselected.state'; +import { ProjectGraphContext, ProjectGraphMachineEvents } from './interfaces'; export const initialContext: ProjectGraphContext = { projects: [], @@ -46,11 +42,11 @@ export const initialContext: ProjectGraphContext = { export const projectGraphMachine = createMachine< ProjectGraphContext, - ProjectGraphEvents + ProjectGraphMachineEvents >( { predictableActionArguments: true, - id: 'DepGraph', + id: 'project-graph', initial: 'idle', context: initialContext, states: { diff --git a/graph/client/src/app/machines/dep-graph.spec.ts b/graph/client/src/app/feature-projects/machines/project-graph.spec.ts similarity index 100% rename from graph/client/src/app/machines/dep-graph.spec.ts rename to graph/client/src/app/feature-projects/machines/project-graph.spec.ts diff --git a/graph/client/src/app/feature-projects/machines/selectors.ts b/graph/client/src/app/feature-projects/machines/selectors.ts new file mode 100644 index 0000000000000..fd281197203eb --- /dev/null +++ b/graph/client/src/app/feature-projects/machines/selectors.ts @@ -0,0 +1,59 @@ +// nx-ignore-next-line +import type { ProjectGraphProjectNode } from '@nrwl/devkit'; +import { ProjectGraphSelector } from '../hooks/use-project-graph-selector'; +import { GraphPerfReport, WorkspaceLayout } from '../../interfaces'; +import { TracingAlgorithmType } from './interfaces'; + +export const allProjectsSelector: ProjectGraphSelector< + ProjectGraphProjectNode[] +> = (state) => state.context.projects; + +export const workspaceLayoutSelector: ProjectGraphSelector = ( + state +) => state.context.workspaceLayout; + +export const selectedProjectNamesSelector: ProjectGraphSelector = ( + state +) => state.context.selectedProjects; + +export const projectIsSelectedSelector: ProjectGraphSelector = ( + state +) => state.context.selectedProjects.length > 0; + +export const lastPerfReportSelector: ProjectGraphSelector = ( + state +) => state.context.lastPerfReport; + +export const focusedProjectNameSelector: ProjectGraphSelector = ( + state +) => state.context.focusedProject; + +export const searchDepthSelector: ProjectGraphSelector<{ + searchDepth: number; + searchDepthEnabled: boolean; +}> = (state) => ({ + searchDepth: state.context.searchDepth, + searchDepthEnabled: state.context.searchDepthEnabled, +}); + +export const includePathSelector: ProjectGraphSelector = (state) => + state.context.includePath; + +export const groupByFolderSelector: ProjectGraphSelector = (state) => + state.context.groupByFolder; + +export const collapseEdgesSelector: ProjectGraphSelector = (state) => + state.context.collapseEdges; + +export const textFilterSelector: ProjectGraphSelector = (state) => + state.context.textFilter; + +export const hasAffectedProjectsSelector: ProjectGraphSelector = ( + state +) => state.context.affectedProjects.length > 0; + +export const getTracingInfo: ProjectGraphSelector<{ + start: string; + end: string; + algorithm: TracingAlgorithmType; +}> = (state) => state.context.tracing; diff --git a/graph/client/src/app/machines/text-filtered.state.ts b/graph/client/src/app/feature-projects/machines/text-filtered.state.ts similarity index 91% rename from graph/client/src/app/machines/text-filtered.state.ts rename to graph/client/src/app/feature-projects/machines/text-filtered.state.ts index 4240a97be66db..75aa3d2f60331 100644 --- a/graph/client/src/app/machines/text-filtered.state.ts +++ b/graph/client/src/app/feature-projects/machines/text-filtered.state.ts @@ -1,8 +1,8 @@ import { assign } from '@xstate/immer'; import { send } from 'xstate'; -import { DepGraphStateNodeConfig } from './interfaces'; +import { ProjectGraphStateNodeConfig } from './interfaces'; -export const textFilteredStateConfig: DepGraphStateNodeConfig = { +export const textFilteredStateConfig: ProjectGraphStateNodeConfig = { entry: [ assign((ctx, event) => { if (event.type !== 'filterByText') return; diff --git a/graph/client/src/app/machines/tracing.state.ts b/graph/client/src/app/feature-projects/machines/tracing.state.ts similarity index 88% rename from graph/client/src/app/machines/tracing.state.ts rename to graph/client/src/app/feature-projects/machines/tracing.state.ts index 5eaba6b3e697a..d227c6c1cbaa8 100644 --- a/graph/client/src/app/machines/tracing.state.ts +++ b/graph/client/src/app/feature-projects/machines/tracing.state.ts @@ -1,7 +1,7 @@ import { assign } from '@xstate/immer'; -import { DepGraphStateNodeConfig } from './interfaces'; +import { ProjectGraphStateNodeConfig } from './interfaces'; -export const tracingStateConfig: DepGraphStateNodeConfig = { +export const tracingStateConfig: ProjectGraphStateNodeConfig = { entry: [ assign((ctx, event) => { if (event.type === 'setTracingStart') { diff --git a/graph/client/src/app/machines/unselected.state.ts b/graph/client/src/app/feature-projects/machines/unselected.state.ts similarity index 87% rename from graph/client/src/app/machines/unselected.state.ts rename to graph/client/src/app/feature-projects/machines/unselected.state.ts index 2be0e66cb9a76..64234231e0e5a 100644 --- a/graph/client/src/app/machines/unselected.state.ts +++ b/graph/client/src/app/feature-projects/machines/unselected.state.ts @@ -1,9 +1,9 @@ import { assign } from '@xstate/immer'; import { send, spawn } from 'xstate'; -import { DepGraphStateNodeConfig } from './interfaces'; -import { routeListener } from './route-listener.actor'; +import { routeListener } from '../../machines/route-listener.actor'; +import { ProjectGraphStateNodeConfig } from './interfaces'; -export const unselectedStateConfig: DepGraphStateNodeConfig = { +export const unselectedStateConfig: ProjectGraphStateNodeConfig = { entry: [ 'notifyGraphHideAllProjects', assign((ctx, event) => { diff --git a/graph/client/src/app/feature-projects/panels/collapse-edges-panel.stories.tsx b/graph/client/src/app/feature-projects/panels/collapse-edges-panel.stories.tsx index 2134a7402c2f1..4215a20561c46 100644 --- a/graph/client/src/app/feature-projects/panels/collapse-edges-panel.stories.tsx +++ b/graph/client/src/app/feature-projects/panels/collapse-edges-panel.stories.tsx @@ -1,8 +1,5 @@ -import { ComponentStory, ComponentMeta } from '@storybook/react'; -import { - CollapseEdgesPanel, - CollapseEdgesPanelProps, -} from './collapse-edges-panel'; +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { CollapseEdgesPanel } from './collapse-edges-panel'; export default { component: CollapseEdgesPanel, diff --git a/graph/client/src/app/feature-projects/panels/focused-project-panel.stories.tsx b/graph/client/src/app/feature-projects/panels/focused-project-panel.stories.tsx index 18b2534e74943..f1d84d89fe2ea 100644 --- a/graph/client/src/app/feature-projects/panels/focused-project-panel.stories.tsx +++ b/graph/client/src/app/feature-projects/panels/focused-project-panel.stories.tsx @@ -1,8 +1,5 @@ -import { ComponentStory, ComponentMeta } from '@storybook/react'; -import { - FocusedProjectPanel, - FocusedProjectPanelProps, -} from './focused-project-panel'; +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { FocusedProjectPanel } from './focused-project-panel'; export default { component: FocusedProjectPanel, diff --git a/graph/client/src/app/feature-projects/panels/group-by-folder-panel.stories.tsx b/graph/client/src/app/feature-projects/panels/group-by-folder-panel.stories.tsx index ca6aab3a0cb98..c50f38549f8a3 100644 --- a/graph/client/src/app/feature-projects/panels/group-by-folder-panel.stories.tsx +++ b/graph/client/src/app/feature-projects/panels/group-by-folder-panel.stories.tsx @@ -1,8 +1,5 @@ -import { ComponentStory, ComponentMeta } from '@storybook/react'; -import { - DisplayOptionsPanelProps, - GroupByFolderPanel, -} from './group-by-folder-panel'; +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { GroupByFolderPanel } from './group-by-folder-panel'; export default { component: GroupByFolderPanel, diff --git a/graph/client/src/app/feature-projects/panels/rankdir-panel.stories.tsx b/graph/client/src/app/feature-projects/panels/rankdir-panel.stories.tsx index d682047915d03..a643b5f71e1f4 100644 --- a/graph/client/src/app/feature-projects/panels/rankdir-panel.stories.tsx +++ b/graph/client/src/app/feature-projects/panels/rankdir-panel.stories.tsx @@ -1,4 +1,4 @@ -import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { ComponentMeta, ComponentStory } from '@storybook/react'; import RankDirPanel from './rankdir-panel'; export default { diff --git a/graph/client/src/app/feature-projects/panels/rankdir-panel.tsx b/graph/client/src/app/feature-projects/panels/rankdir-panel.tsx index 00c266db430f2..3d0a1c68b1627 100644 --- a/graph/client/src/app/feature-projects/panels/rankdir-panel.tsx +++ b/graph/client/src/app/feature-projects/panels/rankdir-panel.tsx @@ -1,7 +1,6 @@ -import { Menu } from '@headlessui/react'; import { - ArrowsUpDownIcon, ArrowsRightLeftIcon, + ArrowsUpDownIcon, } from '@heroicons/react/24/outline'; import { useEffect, useState } from 'react'; import { diff --git a/graph/client/src/app/feature-projects/panels/search-depth.stories.tsx b/graph/client/src/app/feature-projects/panels/search-depth.stories.tsx index bd84c0a755254..7c4dd825a35d7 100644 --- a/graph/client/src/app/feature-projects/panels/search-depth.stories.tsx +++ b/graph/client/src/app/feature-projects/panels/search-depth.stories.tsx @@ -1,5 +1,5 @@ -import { ComponentStory, ComponentMeta } from '@storybook/react'; -import { SearchDepth, SearchDepthProps } from './search-depth'; +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { SearchDepth } from './search-depth'; export default { component: SearchDepth, diff --git a/graph/client/src/app/feature-projects/panels/show-hide-projects.stories.tsx b/graph/client/src/app/feature-projects/panels/show-hide-projects.stories.tsx index d0e3c5ed5d09a..b08fe2ec2e644 100644 --- a/graph/client/src/app/feature-projects/panels/show-hide-projects.stories.tsx +++ b/graph/client/src/app/feature-projects/panels/show-hide-projects.stories.tsx @@ -1,8 +1,5 @@ -import { ComponentStory, ComponentMeta } from '@storybook/react'; -import { - ShowHideAllProjects, - ShowHideAllProjectsProps, -} from './show-hide-projects'; +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { ShowHideAllProjects } from './show-hide-projects'; export default { component: ShowHideAllProjects, diff --git a/graph/client/src/app/feature-projects/panels/text-filter-panel.stories.tsx b/graph/client/src/app/feature-projects/panels/text-filter-panel.stories.tsx index f236451f9eb82..df81c136a0db7 100644 --- a/graph/client/src/app/feature-projects/panels/text-filter-panel.stories.tsx +++ b/graph/client/src/app/feature-projects/panels/text-filter-panel.stories.tsx @@ -1,5 +1,5 @@ -import { ComponentStory, ComponentMeta } from '@storybook/react'; -import { TextFilterPanel, TextFilterPanelProps } from './text-filter-panel'; +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { TextFilterPanel } from './text-filter-panel'; export default { component: TextFilterPanel, diff --git a/graph/client/src/app/feature-projects/panels/text-filter-panel.tsx b/graph/client/src/app/feature-projects/panels/text-filter-panel.tsx index d6e58dd4c056b..02ef7c0adbba3 100644 --- a/graph/client/src/app/feature-projects/panels/text-filter-panel.tsx +++ b/graph/client/src/app/feature-projects/panels/text-filter-panel.tsx @@ -1,7 +1,3 @@ -import { useEffect, useState } from 'react'; -import type { KeyboardEvent } from 'react'; -import { useDebounce } from '../../hooks/use-debounce'; -import { BackspaceIcon, FunnelIcon } from '@heroicons/react/24/outline'; import DebouncedTextInput from '../../ui-components/debounced-text-input'; export interface TextFilterPanelProps { diff --git a/graph/client/src/app/feature-projects/panels/theme-panel.stories.tsx b/graph/client/src/app/feature-projects/panels/theme-panel.stories.tsx index ecfa19b148a1a..c8d5e0b2e8e15 100644 --- a/graph/client/src/app/feature-projects/panels/theme-panel.stories.tsx +++ b/graph/client/src/app/feature-projects/panels/theme-panel.stories.tsx @@ -1,4 +1,4 @@ -import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { ComponentMeta, ComponentStory } from '@storybook/react'; import ThemePanel from './theme-panel'; export default { diff --git a/graph/client/src/app/feature-projects/panels/tracing-panel.stories.tsx b/graph/client/src/app/feature-projects/panels/tracing-panel.stories.tsx index 673d42f357b0b..caa7682c9c5fd 100644 --- a/graph/client/src/app/feature-projects/panels/tracing-panel.stories.tsx +++ b/graph/client/src/app/feature-projects/panels/tracing-panel.stories.tsx @@ -1,5 +1,5 @@ -import { ComponentStory, ComponentMeta } from '@storybook/react'; -import { TracingPanel, TracingPanelProps } from './tracing-panel'; +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { TracingPanel } from './tracing-panel'; export default { component: TracingPanel, diff --git a/graph/client/src/app/feature-projects/panels/tracing-panel.tsx b/graph/client/src/app/feature-projects/panels/tracing-panel.tsx index 0610b973f8576..88a13833dc0ac 100644 --- a/graph/client/src/app/feature-projects/panels/tracing-panel.tsx +++ b/graph/client/src/app/feature-projects/panels/tracing-panel.tsx @@ -5,7 +5,7 @@ import { XCircleIcon, } from '@heroicons/react/24/outline'; import { memo } from 'react'; -import { TracingAlgorithmType } from '../../machines/interfaces'; +import { TracingAlgorithmType } from '../machines/interfaces'; export interface TracingPanelProps { start: string; diff --git a/graph/client/src/app/feature-projects/project-list.tsx b/graph/client/src/app/feature-projects/project-list.tsx index 6b6fe5d861f13..9e1cc6cd84054 100644 --- a/graph/client/src/app/feature-projects/project-list.tsx +++ b/graph/client/src/app/feature-projects/project-list.tsx @@ -6,17 +6,17 @@ import { } from '@heroicons/react/24/outline'; // nx-ignore-next-line import type { ProjectGraphNode } from '@nrwl/devkit'; -import { useDepGraphService } from '../hooks/use-dep-graph'; -import { useDepGraphSelector } from '../hooks/use-dep-graph-selector'; +import { useProjectGraphSelector } from './hooks/use-project-graph-selector'; import { allProjectsSelector, getTracingInfo, selectedProjectNamesSelector, workspaceLayoutSelector, -} from '../machines/selectors'; +} from './machines/selectors'; import { parseParentDirectoriesFromFilePath } from '../util'; -import { TracingAlgorithmType } from '../machines/interfaces'; -import ExperimentalFeature from '../experimental-feature'; +import ExperimentalFeature from '../ui-components/experimental-feature'; +import { TracingAlgorithmType } from './machines/interfaces'; +import { getProjectGraphService } from '../machines/get-services'; function getProjectsByType(type: string, projects: ProjectGraphNode[]) { return projects @@ -75,26 +75,26 @@ function ProjectListItem({ project: SidebarProject; tracingInfo: TracingInfo; }) { - const depGraphService = useDepGraphService(); + const projectGraphService = getProjectGraphService(); function startTrace(projectName: string) { - depGraphService.send({ type: 'setTracingStart', projectName }); + projectGraphService.send({ type: 'setTracingStart', projectName }); } function endTrace(projectName: string) { - depGraphService.send({ type: 'setTracingEnd', projectName }); + projectGraphService.send({ type: 'setTracingEnd', projectName }); } function toggleProject(projectName: string, currentlySelected: boolean) { if (currentlySelected) { - depGraphService.send({ type: 'deselectProject', projectName }); + projectGraphService.send({ type: 'deselectProject', projectName }); } else { - depGraphService.send({ type: 'selectProject', projectName }); + projectGraphService.send({ type: 'selectProject', projectName }); } } function focusProject(projectName: string) { - depGraphService.send({ type: 'focusProject', projectName }); + projectGraphService.send({ type: 'focusProject', projectName }); } return ( @@ -178,7 +178,7 @@ function SubProjectList({ projects: SidebarProject[]; tracingInfo: TracingInfo; }) { - const depGraphService = useDepGraphService(); + const projectGraphService = getProjectGraphService(); let sortedProjects = [...projects]; sortedProjects.sort((a, b) => { @@ -190,9 +190,9 @@ function SubProjectList({ (project) => project.projectGraphNode.name ); if (currentlySelected) { - depGraphService.send({ type: 'deselectProjects', projectNames }); + projectGraphService.send({ type: 'deselectProjects', projectNames }); } else { - depGraphService.send({ type: 'selectProjects', projectNames }); + projectGraphService.send({ type: 'selectProjects', projectNames }); } } @@ -236,11 +236,13 @@ function SubProjectList({ } export function ProjectList() { - const tracingInfo = useDepGraphSelector(getTracingInfo); + const tracingInfo = useProjectGraphSelector(getTracingInfo); - const projects = useDepGraphSelector(allProjectsSelector); - const workspaceLayout = useDepGraphSelector(workspaceLayoutSelector); - const selectedProjects = useDepGraphSelector(selectedProjectNamesSelector); + const projects = useProjectGraphSelector(allProjectsSelector); + const workspaceLayout = useProjectGraphSelector(workspaceLayoutSelector); + const selectedProjects = useProjectGraphSelector( + selectedProjectNamesSelector + ); const appProjects = getProjectsByType('app', projects); const libProjects = getProjectsByType('lib', projects); diff --git a/graph/client/src/app/feature-projects/projects-sidebar.tsx b/graph/client/src/app/feature-projects/projects-sidebar.tsx index 4b6fafbf11bc6..a8298795b93f1 100644 --- a/graph/client/src/app/feature-projects/projects-sidebar.tsx +++ b/graph/client/src/app/feature-projects/projects-sidebar.tsx @@ -1,8 +1,6 @@ -import { InformationCircleIcon } from '@heroicons/react/24/outline'; import { useCallback } from 'react'; -import ExperimentalFeature from '../experimental-feature'; -import { useDepGraphService } from '../hooks/use-dep-graph'; -import { useDepGraphSelector } from '../hooks/use-dep-graph-selector'; +import ExperimentalFeature from '../ui-components/experimental-feature'; +import { useProjectGraphSelector } from './hooks/use-project-graph-selector'; import { collapseEdgesSelector, focusedProjectNameSelector, @@ -12,7 +10,7 @@ import { includePathSelector, searchDepthSelector, textFilterSelector, -} from '../machines/selectors'; +} from './machines/selectors'; import CollapseEdgesPanel from './panels/collapse-edges-panel'; import FocusedProjectPanel from './panels/focused-project-panel'; import GroupByFolderPanel from './panels/group-by-folder-panel'; @@ -21,92 +19,102 @@ import SearchDepth from './panels/search-depth'; import ShowHideProjects from './panels/show-hide-projects'; import TextFilterPanel from './panels/text-filter-panel'; import TracingPanel from './panels/tracing-panel'; -import { TracingAlgorithmType } from '../machines/interfaces'; import { useEnvironmentConfig } from '../hooks/use-environment-config'; +import { TracingAlgorithmType } from './machines/interfaces'; +import { getProjectGraphService } from '../machines/get-services'; export function ProjectsSidebar(): JSX.Element { const environmentConfig = useEnvironmentConfig(); - const depGraphService = useDepGraphService(); - const focusedProject = useDepGraphSelector(focusedProjectNameSelector); - const searchDepthInfo = useDepGraphSelector(searchDepthSelector); - const includePath = useDepGraphSelector(includePathSelector); - const textFilter = useDepGraphSelector(textFilterSelector); - const hasAffectedProjects = useDepGraphSelector(hasAffectedProjectsSelector); - const groupByFolder = useDepGraphSelector(groupByFolderSelector); - const collapseEdges = useDepGraphSelector(collapseEdgesSelector); - - const isTracing = depGraphService.state.matches('tracing'); + const projectGraphService = getProjectGraphService(); + const focusedProject = useProjectGraphSelector(focusedProjectNameSelector); + const searchDepthInfo = useProjectGraphSelector(searchDepthSelector); + const includePath = useProjectGraphSelector(includePathSelector); + const textFilter = useProjectGraphSelector(textFilterSelector); + const hasAffectedProjects = useProjectGraphSelector( + hasAffectedProjectsSelector + ); + const groupByFolder = useProjectGraphSelector(groupByFolderSelector); + const collapseEdges = useProjectGraphSelector(collapseEdgesSelector); - // const isTracing = depGraphService.state.matches('tracing'); - const tracingInfo = useDepGraphSelector(getTracingInfo); + const isTracing = projectGraphService.state.matches('tracing'); + const tracingInfo = useProjectGraphSelector(getTracingInfo); function resetFocus() { - depGraphService.send({ type: 'unfocusProject' }); + projectGraphService.send({ type: 'unfocusProject' }); } function showAllProjects() { - depGraphService.send({ type: 'selectAll' }); + projectGraphService.send({ type: 'selectAll' }); } function hideAllProjects() { - depGraphService.send({ type: 'deselectAll' }); + projectGraphService.send({ type: 'deselectAll' }); } function showAffectedProjects() { - depGraphService.send({ type: 'selectAffected' }); + projectGraphService.send({ type: 'selectAffected' }); } function searchDepthFilterEnabledChange(checked: boolean) { - depGraphService.send({ + projectGraphService.send({ type: 'setSearchDepthEnabled', searchDepthEnabled: checked, }); } function groupByFolderChanged(checked: boolean) { - depGraphService.send({ type: 'setGroupByFolder', groupByFolder: checked }); + projectGraphService.send({ + type: 'setGroupByFolder', + groupByFolder: checked, + }); } function collapseEdgesChanged(checked: boolean) { - depGraphService.send({ type: 'setCollapseEdges', collapseEdges: checked }); + projectGraphService.send({ + type: 'setCollapseEdges', + collapseEdges: checked, + }); } function incrementDepthFilter() { - depGraphService.send({ type: 'incrementSearchDepth' }); + projectGraphService.send({ type: 'incrementSearchDepth' }); } function decrementDepthFilter() { - depGraphService.send({ type: 'decrementSearchDepth' }); + projectGraphService.send({ type: 'decrementSearchDepth' }); } function resetTextFilter() { - depGraphService.send({ type: 'clearTextFilter' }); + projectGraphService.send({ type: 'clearTextFilter' }); } function includeLibsInPathChange() { - depGraphService.send({ + projectGraphService.send({ type: 'setIncludeProjectsByPath', includeProjectsByPath: !includePath, }); } function resetTraceStart() { - depGraphService.send({ type: 'clearTraceStart' }); + projectGraphService.send({ type: 'clearTraceStart' }); } function resetTraceEnd() { - depGraphService.send({ type: 'clearTraceEnd' }); + projectGraphService.send({ type: 'clearTraceEnd' }); } function setAlgorithm(algorithm: TracingAlgorithmType) { - depGraphService.send({ type: 'setTracingAlgorithm', algorithm: algorithm }); + projectGraphService.send({ + type: 'setTracingAlgorithm', + algorithm: algorithm, + }); } const updateTextFilter = useCallback( (textFilter: string) => { - depGraphService.send({ type: 'filterByText', search: textFilter }); + projectGraphService.send({ type: 'filterByText', search: textFilter }); }, - [depGraphService] + [projectGraphService] ); return ( diff --git a/graph/client/src/app/hooks/use-task-graph-selector.ts b/graph/client/src/app/feature-tasks/hooks/use-task-graph-selector.ts similarity index 51% rename from graph/client/src/app/hooks/use-task-graph-selector.ts rename to graph/client/src/app/feature-tasks/hooks/use-task-graph-selector.ts index 29e65a32ff144..47f6b17f20578 100644 --- a/graph/client/src/app/hooks/use-task-graph-selector.ts +++ b/graph/client/src/app/feature-tasks/hooks/use-task-graph-selector.ts @@ -1,9 +1,8 @@ import { useSelector } from '@xstate/react'; -import { DepGraphState, TaskGraphState } from '../machines/interfaces'; -import { useDepGraphService } from './use-dep-graph'; -import { getTaskGraphService } from '../machines/get-services'; +import { getTaskGraphService } from '../../machines/get-services'; +import { TaskGraphState } from '../machines/interfaces'; -export type TaskGraphSelector = (depGraphState: TaskGraphState) => T; +export type TaskGraphSelector = (taskGraphState: TaskGraphState) => T; export function useTaskGraphSelector(selectorFunc: TaskGraphSelector): T { const taskGraphMachine = getTaskGraphService(); diff --git a/graph/client/src/app/feature-tasks/machines/interfaces.ts b/graph/client/src/app/feature-tasks/machines/interfaces.ts new file mode 100644 index 0000000000000..33217fa6a4dd2 --- /dev/null +++ b/graph/client/src/app/feature-tasks/machines/interfaces.ts @@ -0,0 +1,12 @@ +import { State } from 'xstate'; +import { TaskGraphContext, TaskGraphEvents } from './task-graph.machine'; + +export type TaskGraphState = State< + TaskGraphContext, + TaskGraphEvents, + any, + { + value: any; + context: TaskGraphContext; + } +>; diff --git a/graph/client/src/app/feature-tasks/machines/task-graph-render.actor.ts b/graph/client/src/app/feature-tasks/machines/task-graph-render.actor.ts new file mode 100644 index 0000000000000..3c503ccb9a5a1 --- /dev/null +++ b/graph/client/src/app/feature-tasks/machines/task-graph-render.actor.ts @@ -0,0 +1,9 @@ +import { getGraphService } from '../../machines/graph.service'; + +export const taskGraphRenderActor = (callback, receive) => { + const graphService = getGraphService(); + + receive((e) => { + graphService.handleTaskEvent(e); + }); +}; diff --git a/graph/client/src/app/machines/task-graph.machine.ts b/graph/client/src/app/feature-tasks/machines/task-graph.machine.ts similarity index 52% rename from graph/client/src/app/machines/task-graph.machine.ts rename to graph/client/src/app/feature-tasks/machines/task-graph.machine.ts index c2a4063ea85ec..91ac8dc8c9182 100644 --- a/graph/client/src/app/machines/task-graph.machine.ts +++ b/graph/client/src/app/feature-tasks/machines/task-graph.machine.ts @@ -1,23 +1,26 @@ -import { createMachine } from 'xstate'; +import { ActorRef, createMachine, send, spawn } from 'xstate'; // nx-ignore-next-line import { ProjectGraphProjectNode } from 'nx/src/config/project-graph'; import { assign } from '@xstate/immer'; +import { GraphRenderEvents } from '../../machines/interfaces'; +import { taskGraphRenderActor } from './task-graph-render.actor'; +// nx-ignore-next-line +import { TaskGraph } from '@nrwl/devkit'; -export type TaskGraphRecord = Record< - string, - Record> ->; +export type TaskGraphRecord = Record; export interface TaskGraphContext { selectedTaskId: string; projects: ProjectGraphProjectNode[]; taskGraphs: TaskGraphRecord; + graphActor: ActorRef; } const initialContext: TaskGraphContext = { selectedTaskId: null, projects: [], taskGraphs: {}, + graphActor: null, }; export type TaskGraphEvents = @@ -31,6 +34,13 @@ export type TaskGraphEvents = taskId: string; }; +export interface TaskGraphSchema { + states: { + idle: {}; + initialized: {}; + }; +} + export const taskGraphMachine = createMachine< TaskGraphContext, TaskGraphEvents @@ -43,7 +53,19 @@ export const taskGraphMachine = createMachine< idle: { on: { notifyTaskGraphSetProjects: { - actions: ['setProjects'], + actions: [ + 'setProjects', + send( + (ctx, event) => ({ + type: 'notifyTaskGraphSetProjects', + projects: ctx.projects, + taskGraphs: ctx.taskGraphs, + }), + { + to: (context) => context.graphActor, + } + ), + ], target: 'initialized', }, }, @@ -51,7 +73,18 @@ export const taskGraphMachine = createMachine< initialized: { on: { selectTask: { - actions: ['selectTask'], + actions: [ + 'selectTask', + send( + (ctx, event) => ({ + type: 'notifyTaskGraphTaskSelected', + taskId: ctx.selectedTaskId, + }), + { + to: (context) => context.graphActor, + } + ), + ], }, }, }, @@ -61,6 +94,7 @@ export const taskGraphMachine = createMachine< actions: { setProjects: assign((ctx, event) => { if (event.type !== 'notifyTaskGraphSetProjects') return; + ctx.graphActor = spawn(taskGraphRenderActor, 'taskGraphRenderActor'); ctx.projects = event.projects; ctx.taskGraphs = event.taskGraphs; diff --git a/graph/client/src/app/feature-tasks/task-list.stories.tsx b/graph/client/src/app/feature-tasks/task-list.stories.tsx index 4182d63b6bf02..5428cca3ea69e 100644 --- a/graph/client/src/app/feature-tasks/task-list.stories.tsx +++ b/graph/client/src/app/feature-tasks/task-list.stories.tsx @@ -1,4 +1,4 @@ -import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { ComponentMeta, ComponentStory } from '@storybook/react'; import { TaskList, TaskListProps } from './task-list'; const Story: ComponentMeta = { diff --git a/graph/client/src/app/feature-tasks/task-list.tsx b/graph/client/src/app/feature-tasks/task-list.tsx index 0bef024d8e5d8..c0deeacb35ef0 100644 --- a/graph/client/src/app/feature-tasks/task-list.tsx +++ b/graph/client/src/app/feature-tasks/task-list.tsx @@ -58,11 +58,7 @@ function ProjectListItem({ selectedTaskId, }: { project: SidebarProjectWithTargets; - selectTask: ( - projectName: string, - targetName: string, - configurationName: string - ) => void; + selectTask: (taskId: string) => void; selectedTaskId: string; }) { return ( @@ -71,24 +67,40 @@ function ProjectListItem({
{project.targets.map((target) => ( <> - {target.targetName} -
+
+ + + +
{target.configurations.map((configuration) => (
- {selectedTaskId === - `${project.projectGraphNode.name}:${target.targetName}:${configuration.name}` ? ( - selected - ) : null}