From 717a560a545bb659f4bf8494edaa19c5f5155659 Mon Sep 17 00:00:00 2001 From: Philip Fulcher Date: Thu, 16 Dec 2021 16:30:04 -0700 Subject: [PATCH] chore(dep-graph): migrate dep-graph app to React (#8152) --- .gitignore | 2 +- .../{dep-graph-e2e => client-e2e}/.eslintrc | 0 .../cypress-watch-mode.json | 4 +- .../cypress.json | 4 +- dep-graph/client-e2e/project.json | 33 +++ .../src/fixtures/example.json | 0 .../src/integration/app.spec.ts | 80 ++++- .../src/plugins/index.js | 0 .../src/support/app.po.ts | 7 + .../src/support/commands.ts | 0 .../src/support/index.ts | 0 .../watch-mode-integration/watch-mode.spec.ts | 2 +- .../tsconfig.e2e.json | 0 .../tsconfig.json | 0 dep-graph/client/.babelrc | 11 + .../browserslist => client/.browserslistrc} | 5 +- dep-graph/client/.eslintrc.json | 21 ++ dep-graph/client/jest.config.js | 13 + .../{dep-graph => client}/postcss.config.js | 2 +- dep-graph/{dep-graph => client}/project.json | 38 +-- dep-graph/client/src/app/app.tsx | 12 + dep-graph/client/src/app/debugger-panel.tsx | 59 ++++ .../src/app/fetch-project-graph-service.ts | 4 +- .../client/src/app/hooks/use-debounce.ts | 22 ++ .../src/app/hooks/use-dep-graph-selector.ts | 11 + .../client/src/app/hooks/use-dep-graph.ts | 8 + .../src/app/hooks/use-environment-config.ts | 30 ++ .../client/src/app/hooks/use-interval-when.ts | 28 ++ .../hooks/use-project-graph-data-service.ts | 24 ++ .../src/app/interfaces.ts} | 8 +- .../src/app/local-project-graph-service.ts | 4 +- .../src/app/machines/custom-selected.state.ts | 0 .../src/app/machines/dep-graph.machine.ts | 15 +- .../src/app/machines/dep-graph.service.ts | 20 +- .../src/app/machines/dep-graph.spec.ts | 126 ++++---- .../src/app/machines/focused.state.ts | 1 - .../src/app/machines}/graph.service.ts | 4 +- .../app => client/src/app/machines}/graph.ts | 106 +++---- .../src/app/machines/interfaces.ts | 34 ++- .../client/src/app/machines/selectors.ts | 43 +++ .../src/app/machines/text-filtered.state.ts | 0 .../src/app/machines/unselected.state.ts | 1 - .../src/app/mock-project-graph-service.ts | 6 +- .../src/app/project-node-tooltip.ts | 14 +- dep-graph/client/src/app/shell.tsx | 190 ++++++++++++ .../src/app/sidebar/focused-project-panel.tsx | 59 ++++ .../src/app/sidebar/group-by-folder-panel.tsx | 39 +++ .../client/src/app/sidebar/project-list.tsx | 280 ++++++++++++++++++ .../client/src/app/sidebar/search-depth.tsx | 102 +++++++ .../src/app/sidebar/show-hide-projects.tsx | 100 +++++++ dep-graph/client/src/app/sidebar/sidebar.tsx | 181 +++++++++++ .../src/app/sidebar/text-filter-panel.tsx | 139 +++++++++ dep-graph/client/src/app/state.provider.tsx | 18 ++ .../src/app/styles-graph/edges.ts | 0 .../src/app/styles-graph/fonts.ts | 0 .../src/app/styles-graph/index.ts | 0 .../src/app/styles-graph/nodes.ts | 0 .../src/app/styles-graph/palette.ts | 0 .../src/app/tooltip-service.ts | 0 .../app/util-cytoscape/cytoscape.models.ts | 0 .../src/app/util-cytoscape/edge.ts | 0 .../src/app/util-cytoscape/index.ts | 0 .../src/app/util-cytoscape/parent-node.ts | 0 .../app/util-cytoscape/project-node.spec.ts | 0 .../src/app/util-cytoscape/project-node.ts | 0 .../src/app/util.spec.ts | 2 +- dep-graph/client/src/app/util.ts | 56 ++++ .../{dep-graph => client}/src/assets/.gitkeep | 0 .../src/assets/environment.dev.js | 0 .../src/assets/environment.watch.js | 0 .../src/assets/graphs/affected.json | 0 .../src/assets/graphs/focus-testing.json | 0 .../src/assets/graphs/nx-examples.json | 0 .../src/assets/graphs/nx.json | 0 .../src/assets/graphs/ocean.json | 0 .../src/assets/graphs/storybook.json | 0 .../src/assets/graphs/sub-apps.json | 0 .../src/environments/environment.prod.ts | 3 + .../client/src/environments/environment.ts | 6 + .../{dep-graph => client}/src/favicon.ico | Bin .../{dep-graph => client}/src/globals.d.ts | 4 +- dep-graph/client/src/index.html | 16 + dep-graph/client/src/main.tsx | 18 ++ .../{dep-graph => client}/src/polyfills.ts | 0 .../{dep-graph => client}/src/styles.scss | 36 +-- .../{dep-graph => client}/tailwind.config.js | 0 dep-graph/client/tsconfig.app.json | 23 ++ dep-graph/{dep-graph => client}/tsconfig.json | 4 + dep-graph/client/tsconfig.spec.json | 24 ++ dep-graph/dep-graph-e2e/project.json | 33 --- dep-graph/dep-graph/.babelrc | 3 - dep-graph/dep-graph/.eslintrc | 1 - dep-graph/dep-graph/jest.config.js | 15 - dep-graph/dep-graph/src/app/app.ts | 153 ---------- dep-graph/dep-graph/src/app/debugger-panel.ts | 59 ---- .../app/ui-sidebar/display-options-panel.ts | 176 ----------- .../app/ui-sidebar/focused-project-panel.ts | 67 ----- .../src/app/ui-sidebar/project-list.ts | 227 -------------- .../dep-graph/src/app/ui-sidebar/sidebar.ts | 37 --- .../src/app/ui-sidebar/text-filter-panel.ts | 109 ------- dep-graph/dep-graph/src/app/util.ts | 143 --------- dep-graph/dep-graph/src/index.html | 188 ------------ dep-graph/dep-graph/src/main.ts | 29 -- dep-graph/dep-graph/src/test-setup.ts | 0 dep-graph/dep-graph/tsconfig.app.json | 10 - dep-graph/dep-graph/tsconfig.spec.json | 11 - nx.json | 12 +- package.json | 6 +- packages/workspace/project.json | 2 +- scripts/copy-dep-graph-environment.ts | 4 +- workspace.json | 4 +- yarn.lock | 154 +++++++++- 112 files changed, 2042 insertions(+), 1503 deletions(-) rename dep-graph/{dep-graph-e2e => client-e2e}/.eslintrc (100%) rename dep-graph/{dep-graph-e2e => client-e2e}/cypress-watch-mode.json (65%) rename dep-graph/{dep-graph-e2e => client-e2e}/cypress.json (64%) create mode 100644 dep-graph/client-e2e/project.json rename dep-graph/{dep-graph-e2e => client-e2e}/src/fixtures/example.json (100%) rename dep-graph/{dep-graph-e2e => client-e2e}/src/integration/app.spec.ts (58%) rename dep-graph/{dep-graph-e2e => client-e2e}/src/plugins/index.js (100%) rename dep-graph/{dep-graph-e2e => client-e2e}/src/support/app.po.ts (77%) rename dep-graph/{dep-graph-e2e => client-e2e}/src/support/commands.ts (100%) rename dep-graph/{dep-graph-e2e => client-e2e}/src/support/index.ts (100%) rename dep-graph/{dep-graph-e2e => client-e2e}/src/watch-mode-integration/watch-mode.spec.ts (98%) rename dep-graph/{dep-graph-e2e => client-e2e}/tsconfig.e2e.json (100%) rename dep-graph/{dep-graph-e2e => client-e2e}/tsconfig.json (100%) create mode 100644 dep-graph/client/.babelrc rename dep-graph/{dep-graph/browserslist => client/.browserslistrc} (73%) create mode 100644 dep-graph/client/.eslintrc.json create mode 100644 dep-graph/client/jest.config.js rename dep-graph/{dep-graph => client}/postcss.config.js (60%) rename dep-graph/{dep-graph => client}/project.json (73%) create mode 100644 dep-graph/client/src/app/app.tsx create mode 100644 dep-graph/client/src/app/debugger-panel.tsx rename dep-graph/{dep-graph => client}/src/app/fetch-project-graph-service.ts (77%) create mode 100644 dep-graph/client/src/app/hooks/use-debounce.ts create mode 100644 dep-graph/client/src/app/hooks/use-dep-graph-selector.ts create mode 100644 dep-graph/client/src/app/hooks/use-dep-graph.ts create mode 100644 dep-graph/client/src/app/hooks/use-environment-config.ts create mode 100644 dep-graph/client/src/app/hooks/use-interval-when.ts create mode 100644 dep-graph/client/src/app/hooks/use-project-graph-data-service.ts rename dep-graph/{dep-graph/src/app/models.ts => client/src/app/interfaces.ts} (72%) rename dep-graph/{dep-graph => client}/src/app/local-project-graph-service.ts (71%) rename dep-graph/{dep-graph => client}/src/app/machines/custom-selected.state.ts (100%) rename dep-graph/{dep-graph => client}/src/app/machines/dep-graph.machine.ts (94%) rename dep-graph/{dep-graph => client}/src/app/machines/dep-graph.service.ts (51%) rename dep-graph/{dep-graph => client}/src/app/machines/dep-graph.spec.ts (88%) rename dep-graph/{dep-graph => client}/src/app/machines/focused.state.ts (95%) rename dep-graph/{dep-graph/src/app => client/src/app/machines}/graph.service.ts (67%) rename dep-graph/{dep-graph/src/app => client/src/app/machines}/graph.ts (85%) rename dep-graph/{dep-graph => client}/src/app/machines/interfaces.ts (85%) create mode 100644 dep-graph/client/src/app/machines/selectors.ts rename dep-graph/{dep-graph => client}/src/app/machines/text-filtered.state.ts (100%) rename dep-graph/{dep-graph => client}/src/app/machines/unselected.state.ts (96%) rename dep-graph/{dep-graph => client}/src/app/mock-project-graph-service.ts (90%) rename dep-graph/{dep-graph => client}/src/app/project-node-tooltip.ts (83%) create mode 100644 dep-graph/client/src/app/shell.tsx create mode 100644 dep-graph/client/src/app/sidebar/focused-project-panel.tsx create mode 100644 dep-graph/client/src/app/sidebar/group-by-folder-panel.tsx create mode 100644 dep-graph/client/src/app/sidebar/project-list.tsx create mode 100644 dep-graph/client/src/app/sidebar/search-depth.tsx create mode 100644 dep-graph/client/src/app/sidebar/show-hide-projects.tsx create mode 100644 dep-graph/client/src/app/sidebar/sidebar.tsx create mode 100644 dep-graph/client/src/app/sidebar/text-filter-panel.tsx create mode 100644 dep-graph/client/src/app/state.provider.tsx rename dep-graph/{dep-graph => client}/src/app/styles-graph/edges.ts (100%) rename dep-graph/{dep-graph => client}/src/app/styles-graph/fonts.ts (100%) rename dep-graph/{dep-graph => client}/src/app/styles-graph/index.ts (100%) rename dep-graph/{dep-graph => client}/src/app/styles-graph/nodes.ts (100%) rename dep-graph/{dep-graph => client}/src/app/styles-graph/palette.ts (100%) rename dep-graph/{dep-graph => client}/src/app/tooltip-service.ts (100%) rename dep-graph/{dep-graph => client}/src/app/util-cytoscape/cytoscape.models.ts (100%) rename dep-graph/{dep-graph => client}/src/app/util-cytoscape/edge.ts (100%) rename dep-graph/{dep-graph => client}/src/app/util-cytoscape/index.ts (100%) rename dep-graph/{dep-graph => client}/src/app/util-cytoscape/parent-node.ts (100%) rename dep-graph/{dep-graph => client}/src/app/util-cytoscape/project-node.spec.ts (100%) rename dep-graph/{dep-graph => client}/src/app/util-cytoscape/project-node.ts (100%) rename dep-graph/{dep-graph => client}/src/app/util.spec.ts (94%) create mode 100644 dep-graph/client/src/app/util.ts rename dep-graph/{dep-graph => client}/src/assets/.gitkeep (100%) rename dep-graph/{dep-graph => client}/src/assets/environment.dev.js (100%) rename dep-graph/{dep-graph => client}/src/assets/environment.watch.js (100%) rename dep-graph/{dep-graph => client}/src/assets/graphs/affected.json (100%) rename dep-graph/{dep-graph => client}/src/assets/graphs/focus-testing.json (100%) rename dep-graph/{dep-graph => client}/src/assets/graphs/nx-examples.json (100%) rename dep-graph/{dep-graph => client}/src/assets/graphs/nx.json (100%) rename dep-graph/{dep-graph => client}/src/assets/graphs/ocean.json (100%) rename dep-graph/{dep-graph => client}/src/assets/graphs/storybook.json (100%) rename dep-graph/{dep-graph => client}/src/assets/graphs/sub-apps.json (100%) create mode 100644 dep-graph/client/src/environments/environment.prod.ts create mode 100644 dep-graph/client/src/environments/environment.ts rename dep-graph/{dep-graph => client}/src/favicon.ico (100%) rename dep-graph/{dep-graph => client}/src/globals.d.ts (79%) create mode 100644 dep-graph/client/src/index.html create mode 100644 dep-graph/client/src/main.tsx rename dep-graph/{dep-graph => client}/src/polyfills.ts (100%) rename dep-graph/{dep-graph => client}/src/styles.scss (100%) rename dep-graph/{dep-graph => client}/tailwind.config.js (100%) create mode 100644 dep-graph/client/tsconfig.app.json rename dep-graph/{dep-graph => client}/tsconfig.json (67%) create mode 100644 dep-graph/client/tsconfig.spec.json delete mode 100644 dep-graph/dep-graph-e2e/project.json delete mode 100644 dep-graph/dep-graph/.babelrc delete mode 100644 dep-graph/dep-graph/.eslintrc delete mode 100644 dep-graph/dep-graph/jest.config.js delete mode 100644 dep-graph/dep-graph/src/app/app.ts delete mode 100644 dep-graph/dep-graph/src/app/debugger-panel.ts delete mode 100644 dep-graph/dep-graph/src/app/ui-sidebar/display-options-panel.ts delete mode 100644 dep-graph/dep-graph/src/app/ui-sidebar/focused-project-panel.ts delete mode 100644 dep-graph/dep-graph/src/app/ui-sidebar/project-list.ts delete mode 100644 dep-graph/dep-graph/src/app/ui-sidebar/sidebar.ts delete mode 100644 dep-graph/dep-graph/src/app/ui-sidebar/text-filter-panel.ts delete mode 100644 dep-graph/dep-graph/src/app/util.ts delete mode 100644 dep-graph/dep-graph/src/index.html delete mode 100644 dep-graph/dep-graph/src/main.ts delete mode 100644 dep-graph/dep-graph/src/test-setup.ts delete mode 100644 dep-graph/dep-graph/tsconfig.app.json delete mode 100644 dep-graph/dep-graph/tsconfig.spec.json diff --git a/.gitignore b/.gitignore index 9cee22106f..5cbf0b14d4 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,4 @@ tmp jest.debug.config.js .tool-versions /.verdaccio/build/local-registry -dep-graph/dep-graph/src/assets/environment.js \ No newline at end of file +dep-graph/client/src/assets/environment.js \ No newline at end of file diff --git a/dep-graph/dep-graph-e2e/.eslintrc b/dep-graph/client-e2e/.eslintrc similarity index 100% rename from dep-graph/dep-graph-e2e/.eslintrc rename to dep-graph/client-e2e/.eslintrc diff --git a/dep-graph/dep-graph-e2e/cypress-watch-mode.json b/dep-graph/client-e2e/cypress-watch-mode.json similarity index 65% rename from dep-graph/dep-graph-e2e/cypress-watch-mode.json rename to dep-graph/client-e2e/cypress-watch-mode.json index 6c3f664e0d..787a6239e1 100644 --- a/dep-graph/dep-graph-e2e/cypress-watch-mode.json +++ b/dep-graph/client-e2e/cypress-watch-mode.json @@ -6,7 +6,7 @@ "pluginsFile": "./src/plugins/index", "supportFile": "./src/support/index.ts", "video": true, - "videosFolder": "../../dist/cypress/dep-graph/dep-graph-e2e/videos", - "screenshotsFolder": "../../dist/cypress/dep-graph/dep-graph-e2e/screenshots", + "videosFolder": "../../dist/cypress/dep-graph/client-e2e/videos", + "screenshotsFolder": "../../dist/cypress/dep-graph/client-e2e/screenshots", "chromeWebSecurity": false } diff --git a/dep-graph/dep-graph-e2e/cypress.json b/dep-graph/client-e2e/cypress.json similarity index 64% rename from dep-graph/dep-graph-e2e/cypress.json rename to dep-graph/client-e2e/cypress.json index 9d3462ef17..198b5740e5 100644 --- a/dep-graph/dep-graph-e2e/cypress.json +++ b/dep-graph/client-e2e/cypress.json @@ -6,7 +6,7 @@ "pluginsFile": "./src/plugins/index", "supportFile": "./src/support/index.ts", "video": true, - "videosFolder": "../../dist/cypress/dep-graph/dep-graph-e2e/videos", - "screenshotsFolder": "../../dist/cypress/dep-graph/dep-graph-e2e/screenshots", + "videosFolder": "../../dist/cypress/dep-graph/client-e2e/videos", + "screenshotsFolder": "../../dist/cypress/dep-graph/client-e2e/screenshots", "chromeWebSecurity": false } diff --git a/dep-graph/client-e2e/project.json b/dep-graph/client-e2e/project.json new file mode 100644 index 0000000000..a5952dc686 --- /dev/null +++ b/dep-graph/client-e2e/project.json @@ -0,0 +1,33 @@ +{ + "root": "dep-graph/client-e2e", + "sourceRoot": "dep-graph/client-e2e/src", + "projectType": "application", + "targets": { + "e2e-disabled": { + "executor": "@nrwl/cypress:cypress", + "options": { + "cypressConfig": "dep-graph/client-e2e/cypress.json", + "tsConfig": "dep-graph/client-e2e/tsconfig.e2e.json", + "devServerTarget": "dep-graph-client:serve-for-e2e", + "baseUrl": "http://localhost:4200" + } + }, + "e2e-watch-disabled": { + "executor": "@nrwl/cypress:cypress", + "options": { + "cypressConfig": "dep-graph/client-e2e/cypress-watch-mode.json", + "tsConfig": "dep-graph/client-e2e/tsconfig.e2e.json", + "devServerTarget": "dep-graph-client:serve-for-e2e:watch", + "baseUrl": "http://localhost:4200" + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["dep-graph/client-e2e/**/*.ts"] + } + } + }, + "implicitDependencies": ["dep-graph-client"] +} diff --git a/dep-graph/dep-graph-e2e/src/fixtures/example.json b/dep-graph/client-e2e/src/fixtures/example.json similarity index 100% rename from dep-graph/dep-graph-e2e/src/fixtures/example.json rename to dep-graph/client-e2e/src/fixtures/example.json diff --git a/dep-graph/dep-graph-e2e/src/integration/app.spec.ts b/dep-graph/client-e2e/src/integration/app.spec.ts similarity index 58% rename from dep-graph/dep-graph-e2e/src/integration/app.spec.ts rename to dep-graph/client-e2e/src/integration/app.spec.ts index 75a8a7d685..03ea025c03 100644 --- a/dep-graph/dep-graph-e2e/src/integration/app.spec.ts +++ b/dep-graph/client-e2e/src/integration/app.spec.ts @@ -1,11 +1,14 @@ import { getCheckedProjectItems, getDeselectAllButton, + getImageDownloadButton, getIncludeProjectsInPathButton, getProjectItems, + getSelectAffectedButton, getSelectAllButton, getSelectProjectsMessage, getTextFilterInput, + getTextFilterReset, getUncheckedProjectItems, getUnfocusProjectButton, } from '../support/app.po'; @@ -20,15 +23,45 @@ describe('dep-graph-client', () => { cy.wait('@getGraph'); }); - it('should display message to select projects', () => { - getSelectProjectsMessage().should('be.visible'); + describe('select projects message', () => { + it('should display on load', () => { + getSelectProjectsMessage().should('be.visible'); + }); + + it('should hide when a project is selected', () => { + cy.contains('nx-dev').scrollIntoView().should('be.visible'); + cy.get('[data-project="nx-dev"]').should('be.visible'); + cy.get('[data-project="nx-dev"]').click({ force: true }); + getSelectProjectsMessage().should('not.exist'); + }); }); - it('should hide select projects message when a project is selected', () => { - cy.contains('nx-dev').scrollIntoView().should('be.visible'); - cy.get('[data-project="nx-dev"]').should('be.visible'); - cy.get('[data-project="nx-dev"]').click({ force: true }); - getSelectProjectsMessage().should('not.be.visible'); + describe('text filter', () => { + it('should hide clear button initially', () => { + getTextFilterReset().should('not.exist'); + }); + + it('should show clear button after typing', () => { + getTextFilterInput().type('nx-dev'); + getTextFilterReset().should('exist'); + }); + + it('should hide clear button after clicking', () => { + getTextFilterInput().type('nx-dev'); + getTextFilterReset().click().should('not.exist'); + }); + + it('should filter projects', () => { + getTextFilterInput().type('nx-dev'); + getCheckedProjectItems().should('have.length', 9); + }); + + it('should clear selection on reset', () => { + getTextFilterInput().type('nx-dev'); + getCheckedProjectItems().should('have.length', 9); + getTextFilterReset().click(); + getCheckedProjectItems().should('have.length', 0); + }); }); describe('selecting a different project', () => { @@ -54,6 +87,20 @@ describe('dep-graph-client', () => { }); }); + describe('show affected button', () => { + it('should be hidden initially', () => { + getSelectAffectedButton().should('not.exist'); + }); + + it('should check all affected project items', () => { + cy.get('[data-cy=project-select]').select('Affected', { force: true }); + cy.wait('@getGraph'); + getSelectAffectedButton().click(); + + getCheckedProjectItems().should('have.length', 5); + }); + }); + describe('selecting projects', () => { it('should select a project by clicking on the project name', () => { cy.get('[data-project="nx-dev"]').should('have.data', 'active', false); @@ -61,7 +108,7 @@ describe('dep-graph-client', () => { force: true, }); - cy.get('[data-project="nx-dev"]').should('have.data', 'active', true); + cy.get('[data-project="nx-dev"][data-active="true"]').should('exist'); }); it('should deselect a project by clicking on the project name again', () => { @@ -125,4 +172,21 @@ describe('dep-graph-client', () => { getCheckedProjectItems().should('have.length', 17); }); }); + + describe('image download button', () => { + it('should be hidden initally', () => { + getImageDownloadButton().should('have.class', 'opacity-0'); + }); + + it('should be shown when a project is selected', () => { + cy.get('[data-project="nx-dev"]').prev('button').click({ force: true }); + getImageDownloadButton().should('not.have.class', 'opacity-0'); + }); + + it('should be hidden when no more projects are selected', () => { + cy.get('[data-project="nx-dev"]').prev('button').click({ force: true }); + getDeselectAllButton().click(); + getImageDownloadButton().should('have.class', 'opacity-0'); + }); + }); }); diff --git a/dep-graph/dep-graph-e2e/src/plugins/index.js b/dep-graph/client-e2e/src/plugins/index.js similarity index 100% rename from dep-graph/dep-graph-e2e/src/plugins/index.js rename to dep-graph/client-e2e/src/plugins/index.js diff --git a/dep-graph/dep-graph-e2e/src/support/app.po.ts b/dep-graph/client-e2e/src/support/app.po.ts similarity index 77% rename from dep-graph/dep-graph-e2e/src/support/app.po.ts rename to dep-graph/client-e2e/src/support/app.po.ts index 3c523f451a..ef184837e7 100644 --- a/dep-graph/dep-graph-e2e/src/support/app.po.ts +++ b/dep-graph/client-e2e/src/support/app.po.ts @@ -2,6 +2,8 @@ export const getSelectProjectsMessage = () => cy.get('#no-projects-chosen'); export const getGraph = () => cy.get('#graph-container'); export const getSelectAllButton = () => cy.get('[data-cy=selectAllButton]'); export const getDeselectAllButton = () => cy.get('[data-cy=deselectAllButton]'); +export const getSelectAffectedButton = () => cy.get('[data-cy=affectedButton]'); + export const getUnfocusProjectButton = () => cy.get('[data-cy=unfocusButton]'); export const getProjectItems = () => cy.get('[data-project]'); @@ -13,5 +15,10 @@ export const getGroupByfolderItems = () => cy.get('input[name=displayOptions][value=groupByFolder]'); export const getTextFilterInput = () => cy.get('[data-cy=textFilterInput]'); +export const getTextFilterReset = () => cy.get('[data-cy=textFilterReset]'); + export const getIncludeProjectsInPathButton = () => cy.get('input[name=textFilterCheckbox]'); + +export const getImageDownloadButton = () => + cy.get('[data-cy=downloadImageButton]'); diff --git a/dep-graph/dep-graph-e2e/src/support/commands.ts b/dep-graph/client-e2e/src/support/commands.ts similarity index 100% rename from dep-graph/dep-graph-e2e/src/support/commands.ts rename to dep-graph/client-e2e/src/support/commands.ts diff --git a/dep-graph/dep-graph-e2e/src/support/index.ts b/dep-graph/client-e2e/src/support/index.ts similarity index 100% rename from dep-graph/dep-graph-e2e/src/support/index.ts rename to dep-graph/client-e2e/src/support/index.ts diff --git a/dep-graph/dep-graph-e2e/src/watch-mode-integration/watch-mode.spec.ts b/dep-graph/client-e2e/src/watch-mode-integration/watch-mode.spec.ts similarity index 98% rename from dep-graph/dep-graph-e2e/src/watch-mode-integration/watch-mode.spec.ts rename to dep-graph/client-e2e/src/watch-mode-integration/watch-mode.spec.ts index f9d56444ec..d7a27c3559 100644 --- a/dep-graph/dep-graph-e2e/src/watch-mode-integration/watch-mode.spec.ts +++ b/dep-graph/client-e2e/src/watch-mode-integration/watch-mode.spec.ts @@ -4,7 +4,7 @@ describe('dep-graph-client in watch mode', () => { beforeEach(() => { cy.clock(); cy.visit('/'); - cy.tick(1000); + cy.tick(2000); }); it('should auto-select new libs as they are created', () => { diff --git a/dep-graph/dep-graph-e2e/tsconfig.e2e.json b/dep-graph/client-e2e/tsconfig.e2e.json similarity index 100% rename from dep-graph/dep-graph-e2e/tsconfig.e2e.json rename to dep-graph/client-e2e/tsconfig.e2e.json diff --git a/dep-graph/dep-graph-e2e/tsconfig.json b/dep-graph/client-e2e/tsconfig.json similarity index 100% rename from dep-graph/dep-graph-e2e/tsconfig.json rename to dep-graph/client-e2e/tsconfig.json diff --git a/dep-graph/client/.babelrc b/dep-graph/client/.babelrc new file mode 100644 index 0000000000..61641ec8ac --- /dev/null +++ b/dep-graph/client/.babelrc @@ -0,0 +1,11 @@ +{ + "presets": [ + [ + "@nrwl/react/babel", + { + "runtime": "automatic" + } + ] + ], + "plugins": [] +} diff --git a/dep-graph/dep-graph/browserslist b/dep-graph/client/.browserslistrc similarity index 73% rename from dep-graph/dep-graph/browserslist rename to dep-graph/client/.browserslistrc index 8d6179367e..f1d12df4fa 100644 --- a/dep-graph/dep-graph/browserslist +++ b/dep-graph/client/.browserslistrc @@ -1,4 +1,7 @@ -# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers +# This file is used by: +# 1. autoprefixer to adjust CSS to support the below specified browsers +# 2. babel preset-env to adjust included polyfills +# # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # diff --git a/dep-graph/client/.eslintrc.json b/dep-graph/client/.eslintrc.json new file mode 100644 index 0000000000..927a2d63f0 --- /dev/null +++ b/dep-graph/client/.eslintrc.json @@ -0,0 +1,21 @@ +{ + "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "rules": { + "@typescript-eslint/no-implicit-any": "off" + }, + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/dep-graph/client/jest.config.js b/dep-graph/client/jest.config.js new file mode 100644 index 0000000000..313faa823d --- /dev/null +++ b/dep-graph/client/jest.config.js @@ -0,0 +1,13 @@ +// nx-ignore-next-line +const nxPreset = require('@nrwl/jest/preset'); + +module.exports = { + ...nxPreset, + displayName: 'dep-graph-client', + transform: { + '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nrwl/react/plugins/jest', + '^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nrwl/next/babel'] }], + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/nx-dev/nx-dev', +}; diff --git a/dep-graph/dep-graph/postcss.config.js b/dep-graph/client/postcss.config.js similarity index 60% rename from dep-graph/dep-graph/postcss.config.js rename to dep-graph/client/postcss.config.js index 421dcdbd76..30c0cf3777 100644 --- a/dep-graph/dep-graph/postcss.config.js +++ b/dep-graph/client/postcss.config.js @@ -1,7 +1,7 @@ module.exports = { plugins: { tailwindcss: { - config: './dep-graph/dep-graph/tailwind.config.js', + config: './dep-graph/client/tailwind.config.js', }, autoprefixer: {}, }, diff --git a/dep-graph/dep-graph/project.json b/dep-graph/client/project.json similarity index 73% rename from dep-graph/dep-graph/project.json rename to dep-graph/client/project.json index 012374c0ec..10fd5cea66 100644 --- a/dep-graph/dep-graph/project.json +++ b/dep-graph/client/project.json @@ -1,6 +1,6 @@ { - "root": "dep-graph/dep-graph", - "sourceRoot": "dep-graph/dep-graph/src", + "root": "dep-graph/client", + "sourceRoot": "dep-graph/client/src", "projectType": "application", "targets": { "build-base": { @@ -8,11 +8,11 @@ "options": { "maxWorkers": 8, "outputPath": "build/apps/dep-graph", - "index": "dep-graph/dep-graph/src/index.html", - "main": "dep-graph/dep-graph/src/main.ts", - "polyfills": "dep-graph/dep-graph/src/polyfills.ts", - "tsConfig": "dep-graph/dep-graph/tsconfig.app.json", - "styles": ["dep-graph/dep-graph/src/styles.scss"], + "index": "dep-graph/client/src/index.html", + "main": "dep-graph/client/src/main.tsx", + "polyfills": "dep-graph/client/src/polyfills.ts", + "tsConfig": "dep-graph/client/tsconfig.app.json", + "styles": ["dep-graph/client/src/styles.scss"], "scripts": [], "assets": [], "optimization": true, @@ -34,10 +34,10 @@ "dev": { "fileReplacements": [], "assets": [ - "dep-graph/dep-graph/src/favicon.ico", - "dep-graph/dep-graph/src/assets/graphs/", + "dep-graph/client/src/favicon.ico", + "dep-graph/client/src/assets/graphs/", { - "input": "dep-graph/dep-graph/src/assets", + "input": "dep-graph/client/src/assets", "output": "/", "glob": "environment.js" } @@ -63,21 +63,21 @@ "serve-base": { "executor": "@nrwl/web:dev-server", "options": { - "buildTarget": "dep-graph-dep-graph:build-base:dev" + "buildTarget": "dep-graph-client:build-base:dev" } }, "lint": { "executor": "@nrwl/linter:eslint", "outputs": ["{options.outputFile}"], "options": { - "lintFilePatterns": ["dep-graph/dep-graph/**/*.ts"] + "lintFilePatterns": ["dep-graph/client/**/*.{ts,tsx,js,jsx}"] } }, "test": { "executor": "@nrwl/jest:jest", - "outputs": ["coverage/dep-graph/dep-graph"], + "outputs": ["coverage/dep-graph/client"], "options": { - "jestConfig": "dep-graph/dep-graph/jest.config.js", + "jestConfig": "dep-graph/client/jest.config.js", "passWithNoTests": true } }, @@ -87,14 +87,14 @@ "options": { "commands": [ "npx ts-node -P ./scripts/tsconfig.scripts.json ./scripts/copy-dep-graph-environment.ts dev", - "nx serve-base dep-graph-dep-graph" + "nx serve-base dep-graph-client" ] }, "configurations": { "watch": { "commands": [ "npx ts-node -P ./scripts/tsconfig.scripts.json ./scripts/copy-dep-graph-environment.ts watch", - "nx serve-base dep-graph-dep-graph" + "nx serve-base dep-graph-client" ] } } @@ -105,7 +105,7 @@ "options": { "commands": [ "npx ts-node -P ./scripts/tsconfig.scripts.json ./scripts/copy-dep-graph-environment.ts dev", - "nx serve-base dep-graph-dep-graph" + "nx serve-base dep-graph-client" ], "readyWhen": "No issues found." }, @@ -113,12 +113,12 @@ "watch": { "commands": [ "npx ts-node -P ./scripts/tsconfig.scripts.json ./scripts/copy-dep-graph-environment.ts watch", - "nx serve-base dep-graph-dep-graph" + "nx serve-base dep-graph-client" ], "readyWhen": "No issues found." } } } }, - "tags": ["core"] + "tags": [] } diff --git a/dep-graph/client/src/app/app.tsx b/dep-graph/client/src/app/app.tsx new file mode 100644 index 0000000000..6ed3388031 --- /dev/null +++ b/dep-graph/client/src/app/app.tsx @@ -0,0 +1,12 @@ +import { Shell } from './shell'; +import { GlobalStateProvider } from './state.provider'; + +export function App() { + return ( + + + + ); +} + +export default App; diff --git a/dep-graph/client/src/app/debugger-panel.tsx b/dep-graph/client/src/app/debugger-panel.tsx new file mode 100644 index 0000000000..056509815d --- /dev/null +++ b/dep-graph/client/src/app/debugger-panel.tsx @@ -0,0 +1,59 @@ +import { ProjectGraphList } from './interfaces'; +import { GraphPerfReport } from './machines/interfaces'; +import { memo } from 'react'; + +export interface DebuggerPanelProps { + projectGraphs: ProjectGraphList[]; + selectedProjectGraph: string; + projectGraphChange: (projectName: string) => void; + lastPerfReport: GraphPerfReport; +} + +export const DebuggerPanel = memo(function ({ + projectGraphs, + selectedProjectGraph, + projectGraphChange, + lastPerfReport, +}: DebuggerPanelProps) { + return ( +
+

Debugger

+ +

+ Last render took {lastPerfReport.renderTime}ms:{' '} + {lastPerfReport.numNodes} nodes{' '} + |{' '} + {lastPerfReport.numEdges} edges + . +

+
+ ); +}); + +export default DebuggerPanel; diff --git a/dep-graph/dep-graph/src/app/fetch-project-graph-service.ts b/dep-graph/client/src/app/fetch-project-graph-service.ts similarity index 77% rename from dep-graph/dep-graph/src/app/fetch-project-graph-service.ts rename to dep-graph/client/src/app/fetch-project-graph-service.ts index dca3f79a40..75afe769b3 100644 --- a/dep-graph/dep-graph/src/app/fetch-project-graph-service.ts +++ b/dep-graph/client/src/app/fetch-project-graph-service.ts @@ -1,6 +1,6 @@ // nx-ignore-next-line -import { DepGraphClientResponse } from '@nrwl/workspace/src/command-line/dep-graph'; -import { ProjectGraphService } from './models'; +import type { DepGraphClientResponse } from '@nrwl/workspace/src/command-line/dep-graph'; +import { ProjectGraphService } from './interfaces'; export class FetchProjectGraphService implements ProjectGraphService { async getHash(): Promise { diff --git a/dep-graph/client/src/app/hooks/use-debounce.ts b/dep-graph/client/src/app/hooks/use-debounce.ts new file mode 100644 index 0000000000..2f62b1d205 --- /dev/null +++ b/dep-graph/client/src/app/hooks/use-debounce.ts @@ -0,0 +1,22 @@ +import { useEffect, useState } from 'react'; + +export function useDebounce(value: string, delay: number) { + // State and setters for debounced value + const [debouncedValue, setDebouncedValue] = useState(value); + useEffect( + () => { + // Update debounced value after delay + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + // Cancel the timeout if value changes (also on delay change or unmount) + // This is how we prevent debounced value from updating if value is changed ... + // .. within the delay period. Timeout gets cleared and restarted. + return () => { + clearTimeout(handler); + }; + }, + [value, delay] // Only re-call effect if value or delay changes + ); + return debouncedValue; +} diff --git a/dep-graph/client/src/app/hooks/use-dep-graph-selector.ts b/dep-graph/client/src/app/hooks/use-dep-graph-selector.ts new file mode 100644 index 0000000000..f28a114b94 --- /dev/null +++ b/dep-graph/client/src/app/hooks/use-dep-graph-selector.ts @@ -0,0 +1,11 @@ +import { useSelector } from '@xstate/react'; +import { DepGraphState } from '../machines/interfaces'; +import { useDepGraphService } from './use-dep-graph'; + +export type DepGraphSelector = (depGraphState: DepGraphState) => T; + +export function useDepGraphSelector(selectorFunc: DepGraphSelector): T { + const depGraphService = useDepGraphService(); + + return useSelector(depGraphService, selectorFunc); +} diff --git a/dep-graph/client/src/app/hooks/use-dep-graph.ts b/dep-graph/client/src/app/hooks/use-dep-graph.ts new file mode 100644 index 0000000000..01bfb77bae --- /dev/null +++ b/dep-graph/client/src/app/hooks/use-dep-graph.ts @@ -0,0 +1,8 @@ +import { useContext } from 'react'; +import { GlobalStateContext } from '../state.provider'; + +export function useDepGraphService() { + const globalState = useContext(GlobalStateContext); + + return globalState; +} diff --git a/dep-graph/client/src/app/hooks/use-environment-config.ts b/dep-graph/client/src/app/hooks/use-environment-config.ts new file mode 100644 index 0000000000..d398a2740e --- /dev/null +++ b/dep-graph/client/src/app/hooks/use-environment-config.ts @@ -0,0 +1,30 @@ +// nx-ignore-next-line +import type { DepGraphClientResponse } from '@nrwl/workspace/src/command-line/dep-graph'; +import { useRef } from 'react'; +import { AppConfig } from '../interfaces'; + +export function useEnvironmentConfig(): { + exclude: string[]; + focusedProject: string; + groupByFolder: boolean; + watch: boolean; + localMode: 'serve' | 'build'; + projectGraphResponse?: DepGraphClientResponse; + environment: 'dev' | 'watch' | 'release'; + appConfig: AppConfig; + useXstateInspect: boolean; +} { + const environmentConfig = useRef({ + exclude: window.exclude, + focusedProject: window.focusedProject, + groupByFolder: window.groupByFolder, + watch: window.watch, + localMode: window.localMode, + projectGraphResponse: window.projectGraphResponse, + environment: window.environment, + appConfig: window.appConfig, + useXstateInspect: window.useXstateInspect, + }); + + return environmentConfig.current; +} diff --git a/dep-graph/client/src/app/hooks/use-interval-when.ts b/dep-graph/client/src/app/hooks/use-interval-when.ts new file mode 100644 index 0000000000..2195b84023 --- /dev/null +++ b/dep-graph/client/src/app/hooks/use-interval-when.ts @@ -0,0 +1,28 @@ +import { useRef, useEffect } 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/dep-graph/client/src/app/hooks/use-project-graph-data-service.ts b/dep-graph/client/src/app/hooks/use-project-graph-data-service.ts new file mode 100644 index 0000000000..7848357dd1 --- /dev/null +++ b/dep-graph/client/src/app/hooks/use-project-graph-data-service.ts @@ -0,0 +1,24 @@ +import { FetchProjectGraphService } from '../fetch-project-graph-service'; +import { ProjectGraphService } from '../interfaces'; +import { LocalProjectGraphService } from '../local-project-graph-service'; +import { MockProjectGraphService } from '../mock-project-graph-service'; + +let projectGraphService: ProjectGraphService; + +export function useProjectGraphDataService() { + if (projectGraphService === undefined) { + if (window.environment === 'dev') { + projectGraphService = new FetchProjectGraphService(); + } else if (window.environment === 'watch') { + projectGraphService = new MockProjectGraphService(); + } else if (window.environment === 'release') { + if (window.localMode === 'build') { + projectGraphService = new LocalProjectGraphService(); + } else { + projectGraphService = new FetchProjectGraphService(); + } + } + } + + return projectGraphService; +} diff --git a/dep-graph/dep-graph/src/app/models.ts b/dep-graph/client/src/app/interfaces.ts similarity index 72% rename from dep-graph/dep-graph/src/app/models.ts rename to dep-graph/client/src/app/interfaces.ts index 15e9fe832d..c7215259d0 100644 --- a/dep-graph/dep-graph/src/app/models.ts +++ b/dep-graph/client/src/app/interfaces.ts @@ -1,5 +1,5 @@ // nx-ignore-next-line -import { DepGraphClientResponse } from '@nrwl/workspace/src/command-line/dep-graph'; +import type { DepGraphClientResponse } from '@nrwl/workspace/src/command-line/dep-graph'; export interface ProjectGraphList { id: string; @@ -25,9 +25,3 @@ export interface AppConfig { projectGraphs: ProjectGraphList[]; defaultProjectGraph: string; } - -export const DEFAULT_CONFIG: AppConfig = { - showDebugger: false, - projectGraphs: [], - defaultProjectGraph: null, -}; diff --git a/dep-graph/dep-graph/src/app/local-project-graph-service.ts b/dep-graph/client/src/app/local-project-graph-service.ts similarity index 71% rename from dep-graph/dep-graph/src/app/local-project-graph-service.ts rename to dep-graph/client/src/app/local-project-graph-service.ts index 291af27cd5..b899844b9b 100644 --- a/dep-graph/dep-graph/src/app/local-project-graph-service.ts +++ b/dep-graph/client/src/app/local-project-graph-service.ts @@ -1,6 +1,6 @@ // nx-ignore-next-line -import { DepGraphClientResponse } from '@nrwl/workspace/src/command-line/dep-graph'; -import { ProjectGraphService } from './models'; +import type { DepGraphClientResponse } from '@nrwl/workspace/src/command-line/dep-graph'; +import { ProjectGraphService } from './interfaces'; export class LocalProjectGraphService implements ProjectGraphService { async getHash(): Promise { diff --git a/dep-graph/dep-graph/src/app/machines/custom-selected.state.ts b/dep-graph/client/src/app/machines/custom-selected.state.ts similarity index 100% rename from dep-graph/dep-graph/src/app/machines/custom-selected.state.ts rename to dep-graph/client/src/app/machines/custom-selected.state.ts diff --git a/dep-graph/dep-graph/src/app/machines/dep-graph.machine.ts b/dep-graph/client/src/app/machines/dep-graph.machine.ts similarity index 94% rename from dep-graph/dep-graph/src/app/machines/dep-graph.machine.ts rename to dep-graph/client/src/app/machines/dep-graph.machine.ts index 885c92cbf0..0be090e0cc 100644 --- a/dep-graph/dep-graph/src/app/machines/dep-graph.machine.ts +++ b/dep-graph/client/src/app/machines/dep-graph.machine.ts @@ -1,6 +1,6 @@ import { assign } from '@xstate/immer'; import { Machine, send, spawn } from 'xstate'; -import { useGraphService } from '../graph.service'; +import { getGraphService } from './graph.service'; import { customSelectedStateConfig } from './custom-selected.state'; import { focusedStateConfig } from './focused.state'; import { @@ -27,16 +27,22 @@ export const initialContext: DepGraphContext = { appsDir: '', }, graph: null, + lastPerfReport: { + numEdges: 0, + numNodes: 0, + renderTime: 0, + }, }; const graphActor = (callback, receive) => { - const graphService = useGraphService(); + const graphService = getGraphService(); receive((e) => { - const selectedProjectNames = graphService.handleEvent(e); + const { selectedProjectNames, perfReport } = graphService.handleEvent(e); callback({ type: 'setSelectedProjectsFromGraph', selectedProjectNames, + perfReport, }); }); }; @@ -80,6 +86,7 @@ export const depGraphMachine = Machine< setSelectedProjectsFromGraph: { actions: assign((ctx, event) => { ctx.selectedProjects = event.selectedProjectNames; + ctx.lastPerfReport = event.perfReport; }), }, selectProject: { @@ -163,9 +170,11 @@ export const depGraphMachine = Machine< ctx.groupByFolder = event.groupByFolder; }), incrementSearchDepth: assign((ctx) => { + ctx.searchDepthEnabled = true; ctx.searchDepth = ctx.searchDepth + 1; }), decrementSearchDepth: assign((ctx) => { + ctx.searchDepthEnabled = true; ctx.searchDepth = ctx.searchDepth > 1 ? ctx.searchDepth - 1 : 1; }), setSearchDepthEnabled: assign((ctx, event) => { diff --git a/dep-graph/dep-graph/src/app/machines/dep-graph.service.ts b/dep-graph/client/src/app/machines/dep-graph.service.ts similarity index 51% rename from dep-graph/dep-graph/src/app/machines/dep-graph.service.ts rename to dep-graph/client/src/app/machines/dep-graph.service.ts index 1c242f6906..d91fc00280 100644 --- a/dep-graph/dep-graph/src/app/machines/dep-graph.service.ts +++ b/dep-graph/client/src/app/machines/dep-graph.service.ts @@ -1,13 +1,9 @@ -import { from } from 'rxjs'; -import { map, shareReplay } from 'rxjs/operators'; import { interpret, Interpreter, Typestate } from 'xstate'; import { depGraphMachine } from './dep-graph.machine'; import { DepGraphContext, - DepGraphUIEvents, - DepGraphSend, - DepGraphStateObservable, DepGraphSchema, + DepGraphUIEvents, } from './interfaces'; let depGraphService: Interpreter< @@ -17,23 +13,13 @@ let depGraphService: Interpreter< Typestate >; -let depGraphState$: DepGraphStateObservable; - -export function useDepGraphService(): [DepGraphStateObservable, DepGraphSend] { +export function getDepGraphService() { if (!depGraphService) { depGraphService = interpret(depGraphMachine, { devTools: !!window.useXstateInspect, }); depGraphService.start(); - - depGraphState$ = from(depGraphService).pipe( - map((state) => ({ - value: state.value, - context: state.context, - })), - shareReplay(1) - ); } - return [depGraphState$, depGraphService.send]; + return depGraphService; } diff --git a/dep-graph/dep-graph/src/app/machines/dep-graph.spec.ts b/dep-graph/client/src/app/machines/dep-graph.spec.ts similarity index 88% rename from dep-graph/dep-graph/src/app/machines/dep-graph.spec.ts rename to dep-graph/client/src/app/machines/dep-graph.spec.ts index 5f92c5c973..ae639c4760 100644 --- a/dep-graph/dep-graph/src/app/machines/dep-graph.spec.ts +++ b/dep-graph/client/src/app/machines/dep-graph.spec.ts @@ -1,4 +1,5 @@ -import { ProjectGraphDependency, ProjectGraphNode } from '@nrwl/devkit'; +// nx-ignore-next-line +import type { ProjectGraphDependency, ProjectGraphNode } from '@nrwl/devkit'; import { depGraphMachine } from './dep-graph.machine'; import { interpret } from 'xstate'; @@ -6,32 +7,44 @@ export const mockProjects: ProjectGraphNode[] = [ { name: 'app1', type: 'app', - data: {}, + data: { + root: 'apps/app1', + }, }, { name: 'app2', type: 'app', - data: {}, + data: { + root: 'apps/app2', + }, }, { name: 'ui-lib', type: 'lib', - data: {}, + data: { + root: 'libs/ui-lib', + }, }, { name: 'feature-lib1', type: 'lib', - data: {}, + data: { + root: 'libs/feature/lib1', + }, }, { name: 'feature-lib2', type: 'lib', - data: {}, + data: { + root: 'libs/feature/lib2', + }, }, { name: 'auth-lib', type: 'lib', - data: {}, + data: { + root: 'libs/auth-lib', + }, }, ]; @@ -282,70 +295,9 @@ describe('dep-graph machine', () => { expect(result.value).toEqual('unselected'); expect(result.context.selectedProjects).toEqual([]); }); - - it('should not decrement search depth below 1', () => { - let result = depGraphMachine.transition(depGraphMachine.initialState, { - type: 'initGraph', - projects: mockProjects, - dependencies: mockDependencies, - affectedProjects: [], - workspaceLayout: { appsDir: 'apps', libsDir: 'libs' }, - }); - - result = depGraphMachine.transition(result, { - type: 'focusProject', - projectName: 'app1', - }); - - expect(result.context.searchDepth).toEqual(1); - - result = depGraphMachine.transition(result, { - type: 'incrementSearchDepth', - }); - - expect(result.context.searchDepth).toEqual(2); - - result = depGraphMachine.transition(result, { - type: 'incrementSearchDepth', - }); - - result = depGraphMachine.transition(result, { - type: 'incrementSearchDepth', - }); - - expect(result.context.searchDepth).toEqual(4); - - result = depGraphMachine.transition(result, { - type: 'decrementSearchDepth', - }); - - result = depGraphMachine.transition(result, { - type: 'decrementSearchDepth', - }); - - expect(result.context.searchDepth).toEqual(2); - - result = depGraphMachine.transition(result, { - type: 'decrementSearchDepth', - }); - - expect(result.context.searchDepth).toEqual(1); - - result = depGraphMachine.transition(result, { - type: 'decrementSearchDepth', - }); - - expect(result.context.searchDepth).toEqual(1); - - result = depGraphMachine.transition(result, { - type: 'decrementSearchDepth', - }); - - expect(result.context.searchDepth).toEqual(1); - }); }); - describe('filtering projects by text', () => { + describe('search depth', () => { it('should not decrement search depth below 1', () => { let result = depGraphMachine.transition(depGraphMachine.initialState, { type: 'initGraph', @@ -406,5 +358,41 @@ describe('dep-graph machine', () => { expect(result.context.searchDepth).toEqual(1); }); + + it('should activate search depth if incremented or decremented', () => { + let result = depGraphMachine.transition(depGraphMachine.initialState, { + type: 'initGraph', + projects: mockProjects, + dependencies: mockDependencies, + affectedProjects: [], + workspaceLayout: { appsDir: 'apps', libsDir: 'libs' }, + }); + + result = depGraphMachine.transition(result, { + type: 'setSearchDepthEnabled', + searchDepthEnabled: false, + }); + + expect(result.context.searchDepthEnabled).toBe(false); + + result = depGraphMachine.transition(result, { + type: 'incrementSearchDepth', + }); + + expect(result.context.searchDepthEnabled).toBe(true); + + result = depGraphMachine.transition(result, { + type: 'setSearchDepthEnabled', + searchDepthEnabled: false, + }); + + expect(result.context.searchDepthEnabled).toBe(false); + + result = depGraphMachine.transition(result, { + type: 'decrementSearchDepth', + }); + + expect(result.context.searchDepthEnabled).toBe(true); + }); }); }); diff --git a/dep-graph/dep-graph/src/app/machines/focused.state.ts b/dep-graph/client/src/app/machines/focused.state.ts similarity index 95% rename from dep-graph/dep-graph/src/app/machines/focused.state.ts rename to dep-graph/client/src/app/machines/focused.state.ts index b4ebb518c2..0b5d9a3917 100644 --- a/dep-graph/dep-graph/src/app/machines/focused.state.ts +++ b/dep-graph/client/src/app/machines/focused.state.ts @@ -1,6 +1,5 @@ import { assign } from '@xstate/immer'; import { send } from 'xstate'; -import { selectProjectsForFocusedProject } from '../util'; import { DepGraphStateNodeConfig } from './interfaces'; export const focusedStateConfig: DepGraphStateNodeConfig = { diff --git a/dep-graph/dep-graph/src/app/graph.service.ts b/dep-graph/client/src/app/machines/graph.service.ts similarity index 67% rename from dep-graph/dep-graph/src/app/graph.service.ts rename to dep-graph/client/src/app/machines/graph.service.ts index 8a0334580c..457192c2f3 100644 --- a/dep-graph/dep-graph/src/app/graph.service.ts +++ b/dep-graph/client/src/app/machines/graph.service.ts @@ -1,9 +1,9 @@ import { GraphService } from './graph'; -import { GraphTooltipService } from './tooltip-service'; +import { GraphTooltipService } from '../tooltip-service'; let graphService: GraphService; -export function useGraphService(): GraphService { +export function getGraphService(): GraphService { if (!graphService) { graphService = new GraphService( new GraphTooltipService(), diff --git a/dep-graph/dep-graph/src/app/graph.ts b/dep-graph/client/src/app/machines/graph.ts similarity index 85% rename from dep-graph/dep-graph/src/app/graph.ts rename to dep-graph/client/src/app/machines/graph.ts index c594795575..aa4ea610db 100644 --- a/dep-graph/dep-graph/src/app/graph.ts +++ b/dep-graph/client/src/app/machines/graph.ts @@ -1,35 +1,27 @@ +// nx-ignore-next-line import type { ProjectGraphDependency, ProjectGraphNode } from '@nrwl/devkit'; import type { VirtualElement } from '@popperjs/core'; -import * as cy from 'cytoscape'; -import * as cytoscapeDagre from 'cytoscape-dagre'; -import * as popper from 'cytoscape-popper'; -import { Subject } from 'rxjs'; +import { default as cy } from 'cytoscape'; +import { default as cytoscapeDagre } from 'cytoscape-dagre'; +import { default as popper } from 'cytoscape-popper'; import type { Instance } from 'tippy.js'; -import { GraphRenderEvents } from './machines/interfaces'; -import { ProjectNodeToolTip } from './project-node-tooltip'; -import { edgeStyles, nodeStyles } from './styles-graph'; -import { GraphTooltipService } from './tooltip-service'; +import { ProjectNodeToolTip } from '../project-node-tooltip'; +import { edgeStyles, nodeStyles } from '../styles-graph'; +import { GraphTooltipService } from '../tooltip-service'; import { CytoscapeDagreConfig, ParentNode, ProjectEdge, ProjectNode, -} from './util-cytoscape'; +} from '../util-cytoscape'; +import { GraphRenderEvents, GraphPerfReport } from './interfaces'; -export interface GraphPerfReport { - renderTime: number; - numNodes: number; - numEdges: number; -} export class GraphService { private traversalGraph: cy.Core; private renderGraph: cy.Core; private openTooltip: Instance = null; - private renderTimesSubject = new Subject(); - renderTimes$ = this.renderTimesSubject.asObservable(); - constructor( private tooltipService: GraphTooltipService, private containerId: string @@ -38,17 +30,18 @@ export class GraphService { cy.use(popper); } - handleEvent(event: GraphRenderEvents): string[] { + handleEvent(event: GraphRenderEvents): { + selectedProjectNames: string[]; + perfReport: GraphPerfReport; + } { const time = Date.now(); - if ( - this.renderGraph && - event.type !== 'notifyGraphFocusProject' && - event.type !== 'notifyGraphUpdateGraph' - ) { + if (this.renderGraph && event.type !== 'notifyGraphUpdateGraph') { this.renderGraph.nodes('.focused').removeClass('focused'); } + this.tooltipService.hideAll(); + switch (event.type) { case 'notifyGraphInitGraph': this.initGraph( @@ -104,40 +97,43 @@ export class GraphService { break; } - let visibleProjects: string[] = []; + let selectedProjectNames: string[] = []; + let perfReport: GraphPerfReport = { + numEdges: 0, + numNodes: 0, + renderTime: 0, + }; if (this.renderGraph) { this.renderGraph .elements() .sort((a, b) => a.id().localeCompare(b.id())) - .layout({ + .layout({ name: 'dagre', nodeDimensionsIncludeLabels: true, rankSep: 75, rankDir: 'TB', edgeSep: 50, ranker: 'network-simplex', - }) + } as CytoscapeDagreConfig) .run(); this.renderGraph.fit().center().resize(); - visibleProjects = this.renderGraph + selectedProjectNames = this.renderGraph .nodes('[type!="dir"]') .map((node) => node.id()); const renderTime = Date.now() - time; - const report: GraphPerfReport = { + perfReport = { renderTime, numNodes: this.renderGraph.nodes().length, numEdges: this.renderGraph.edges().length, }; - - this.renderTimesSubject.next(report); } - return visibleProjects; + return { selectedProjectNames, perfReport }; } setShownProjects(selectedProjectNames: string[]) { @@ -178,9 +174,13 @@ export class GraphService { this.renderGraph?.nodes() ?? this.traversalGraph.collection(); const nodeToHide = this.renderGraph.$id(projectName); - const nodesToAdd = currentNodes.difference(nodeToHide); + const nodesToAdd = currentNodes + .difference(nodeToHide) + .difference(nodeToHide.ancestors()); const ancestorsToAdd = nodesToAdd.ancestors(); - const nodesToRender = nodesToAdd.union(ancestorsToAdd); + + let nodesToRender = nodesToAdd.union(ancestorsToAdd); + const edgesToRender = nodesToRender.edgesTo(nodesToRender); this.transferToRenderGraph(nodesToRender.union(edgesToRender)); @@ -229,22 +229,28 @@ export class GraphService { includePath: boolean, searchDepth: number = -1 ) { - const split = search.split(','); + if (search === '') { + this.transferToRenderGraph(this.traversalGraph.collection()); + } else { + const split = search.split(','); - let filteredProjects = this.traversalGraph.nodes().filter((node) => { - return split.findIndex((splitItem) => node.id().includes(splitItem)) > -1; - }); + let filteredProjects = this.traversalGraph.nodes().filter((node) => { + return ( + split.findIndex((splitItem) => node.id().includes(splitItem)) > -1 + ); + }); - if (includePath) { - filteredProjects = filteredProjects.union( - this.includeProjectsByDepth(filteredProjects, searchDepth) - ); + if (includePath) { + filteredProjects = filteredProjects.union( + this.includeProjectsByDepth(filteredProjects, searchDepth) + ); + } + + filteredProjects = filteredProjects.union(filteredProjects.ancestors()); + const edgesToRender = filteredProjects.edgesTo(filteredProjects); + + this.transferToRenderGraph(filteredProjects.union(edgesToRender)); } - - filteredProjects = filteredProjects.union(filteredProjects.ancestors()); - const edgesToRender = filteredProjects.edgesTo(filteredProjects); - - this.transferToRenderGraph(filteredProjects.union(edgesToRender)); } private transferToRenderGraph(elements: cy.Collection) { @@ -281,10 +287,6 @@ export class GraphService { this.listenForProjectNodeHovers(); } - getImage() { - return this.renderGraph.png({ bg: '#fff', full: true }); - } - private includeProjectsByDepth( projects: cy.NodeCollection | cy.NodeSingular, depth: number = -1 @@ -463,4 +465,8 @@ export class GraphService { .removeClass('highlight'); }); } + + getImage() { + return this.renderGraph.png({ bg: '#fff', full: true }); + } } diff --git a/dep-graph/dep-graph/src/app/machines/interfaces.ts b/dep-graph/client/src/app/machines/interfaces.ts similarity index 85% rename from dep-graph/dep-graph/src/app/machines/interfaces.ts rename to dep-graph/client/src/app/machines/interfaces.ts index a9ffa5c35f..39ddccfecc 100644 --- a/dep-graph/dep-graph/src/app/machines/interfaces.ts +++ b/dep-graph/client/src/app/machines/interfaces.ts @@ -1,7 +1,13 @@ -import { ProjectGraphDependency, ProjectGraphNode } from '@nrwl/devkit'; +// nx-ignore-next-line +import type { ProjectGraphDependency, ProjectGraphNode } from '@nrwl/devkit'; import { Observable } from 'rxjs'; -import { ActionObject, ActorRef, StateNodeConfig, StateValue } from 'xstate'; -import { GraphService } from '../graph'; +import { + ActionObject, + ActorRef, + State, + StateNodeConfig, + StateValue, +} from 'xstate'; // The hierarchical (recursive) schema for the states export interface DepGraphSchema { @@ -14,10 +20,19 @@ export interface DepGraphSchema { }; } +export interface GraphPerfReport { + renderTime: number; + numNodes: number; + numEdges: number; +} // The events that the machine handles export type DepGraphUIEvents = - | { type: 'setSelectedProjectsFromGraph'; selectedProjectNames: string[] } + | { + type: 'setSelectedProjectsFromGraph'; + selectedProjectNames: string[]; + perfReport: GraphPerfReport; + } | { type: 'selectProject'; projectName: string } | { type: 'deselectProject'; projectName: string } | { type: 'selectAll' } @@ -122,6 +137,7 @@ export interface DepGraphContext { appsDir: string; }; graph: ActorRef; + lastPerfReport: GraphPerfReport; } export type DepGraphStateNodeConfig = StateNodeConfig< @@ -138,3 +154,13 @@ export type DepGraphStateObservable = Observable<{ value: StateValue; context: DepGraphContext; }>; + +export type DepGraphState = State< + DepGraphContext, + DepGraphUIEvents, + any, + { + value: any; + context: DepGraphContext; + } +>; diff --git a/dep-graph/client/src/app/machines/selectors.ts b/dep-graph/client/src/app/machines/selectors.ts new file mode 100644 index 0000000000..14f2473611 --- /dev/null +++ b/dep-graph/client/src/app/machines/selectors.ts @@ -0,0 +1,43 @@ +import type { ProjectGraphNode } from '@nrwl/devkit'; +import { DepGraphSelector } from '../hooks/use-dep-graph-selector'; +import { WorkspaceLayout } from '../interfaces'; +import { GraphPerfReport } from './interfaces'; + +export const allProjectsSelector: DepGraphSelector = ( + state +) => state.context.projects; + +export const workspaceLayoutSelector: DepGraphSelector = ( + state +) => state.context.workspaceLayout; + +export const selectedProjectNamesSelector: DepGraphSelector = ( + state +) => state.context.selectedProjects; + +export const projectIsSelectedSelector: DepGraphSelector = (state) => + state.context.selectedProjects.length > 0; + +export const lastPerfReportSelector: DepGraphSelector = ( + state +) => state.context.lastPerfReport; + +export const focusedProjectNameSelector: DepGraphSelector = (state) => + state.context.focusedProject; + +export const searchDepthSelector: DepGraphSelector<{ + searchDepth: number; + searchDepthEnabled: boolean; +}> = (state) => ({ + searchDepth: state.context.searchDepth, + searchDepthEnabled: state.context.searchDepthEnabled, +}); + +export const includePathSelector: DepGraphSelector = (state) => + state.context.includePath; + +export const textFilterSelector: DepGraphSelector = (state) => + state.context.textFilter; + +export const hasAffectedProjectsSelector: DepGraphSelector = (state) => + state.context.affectedProjects.length > 0; diff --git a/dep-graph/dep-graph/src/app/machines/text-filtered.state.ts b/dep-graph/client/src/app/machines/text-filtered.state.ts similarity index 100% rename from dep-graph/dep-graph/src/app/machines/text-filtered.state.ts rename to dep-graph/client/src/app/machines/text-filtered.state.ts diff --git a/dep-graph/dep-graph/src/app/machines/unselected.state.ts b/dep-graph/client/src/app/machines/unselected.state.ts similarity index 96% rename from dep-graph/dep-graph/src/app/machines/unselected.state.ts rename to dep-graph/client/src/app/machines/unselected.state.ts index 2df2f04c02..8191e03e83 100644 --- a/dep-graph/dep-graph/src/app/machines/unselected.state.ts +++ b/dep-graph/client/src/app/machines/unselected.state.ts @@ -1,6 +1,5 @@ import { assign } from '@xstate/immer'; import { send } from 'xstate'; -import { useGraphService } from '../graph.service'; import { DepGraphStateNodeConfig } from './interfaces'; export const unselectedStateConfig: DepGraphStateNodeConfig = { diff --git a/dep-graph/dep-graph/src/app/mock-project-graph-service.ts b/dep-graph/client/src/app/mock-project-graph-service.ts similarity index 90% rename from dep-graph/dep-graph/src/app/mock-project-graph-service.ts rename to dep-graph/client/src/app/mock-project-graph-service.ts index df9951f6ca..7911fd6574 100644 --- a/dep-graph/dep-graph/src/app/mock-project-graph-service.ts +++ b/dep-graph/client/src/app/mock-project-graph-service.ts @@ -1,7 +1,7 @@ -import { ProjectGraphDependency, ProjectGraphNode } from '@nrwl/devkit'; +import type { ProjectGraphDependency, ProjectGraphNode } from '@nrwl/devkit'; // nx-ignore-next-line -import { DepGraphClientResponse } from '@nrwl/workspace/src/command-line/dep-graph'; -import { ProjectGraphService } from '../app/models'; +import type { DepGraphClientResponse } from '@nrwl/workspace/src/command-line/dep-graph'; +import { ProjectGraphService } from '../app/interfaces'; export class MockProjectGraphService implements ProjectGraphService { private response: DepGraphClientResponse = { diff --git a/dep-graph/dep-graph/src/app/project-node-tooltip.ts b/dep-graph/client/src/app/project-node-tooltip.ts similarity index 83% rename from dep-graph/dep-graph/src/app/project-node-tooltip.ts rename to dep-graph/client/src/app/project-node-tooltip.ts index 9c990e8b8b..cde4eea2e6 100644 --- a/dep-graph/dep-graph/src/app/project-node-tooltip.ts +++ b/dep-graph/client/src/app/project-node-tooltip.ts @@ -1,5 +1,5 @@ import * as cy from 'cytoscape'; -import { useDepGraphService } from './machines/dep-graph.service'; +import { getDepGraphService } from './machines/dep-graph.service'; export class ProjectNodeToolTip { constructor(private node: cy.NodeSingular) {} @@ -54,15 +54,21 @@ export class ProjectNodeToolTip { wrapper.classList.add('flex'); - const [_, send] = useDepGraphService(); + const depGraphService = getDepGraphService(); focusButton.addEventListener('click', () => - send({ type: 'focusProject', projectName: this.node.attr('id') }) + depGraphService.send({ + type: 'focusProject', + projectName: this.node.attr('id'), + }) ); focusButton.innerText = 'Focus'; excludeButton.addEventListener('click', () => { - send({ type: 'deselectProject', projectName: this.node.attr('id') }); + depGraphService.send({ + type: 'deselectProject', + projectName: this.node.attr('id'), + }); }); excludeButton.innerText = 'Exclude'; diff --git a/dep-graph/client/src/app/shell.tsx b/dep-graph/client/src/app/shell.tsx new file mode 100644 index 0000000000..3663cd4251 --- /dev/null +++ b/dep-graph/client/src/app/shell.tsx @@ -0,0 +1,190 @@ +// nx-ignore-next-line +import type { DepGraphClientResponse } from '@nrwl/workspace/src/command-line/dep-graph'; +import Tippy from '@tippyjs/react'; +import { useEffect, useState } from 'react'; +import DebuggerPanel from './debugger-panel'; +import { useDepGraphService } from './hooks/use-dep-graph'; +import { useDepGraphSelector } from './hooks/use-dep-graph-selector'; +import { useEnvironmentConfig } from './hooks/use-environment-config'; +import { useIntervalWhen } from './hooks/use-interval-when'; +import { useProjectGraphDataService } from './hooks/use-project-graph-data-service'; +import { getGraphService } from './machines/graph.service'; +import { + lastPerfReportSelector, + projectIsSelectedSelector, +} from './machines/selectors'; +import Sidebar from './sidebar/sidebar'; + +export function Shell() { + const depGraphService = useDepGraphService(); + + const projectGraphService = useProjectGraphDataService(); + const environment = useEnvironmentConfig(); + const lastPerfReport = useDepGraphSelector(lastPerfReportSelector); + const projectIsSelected = useDepGraphSelector(projectIsSelectedSelector); + + const [selectedProjectId, setSelectedProjectId] = useState( + environment.appConfig.defaultProjectGraph + ); + + function projectChange(projectGraphId: string) { + setSelectedProjectId(projectGraphId); + } + + useEffect(() => { + const { appConfig } = environment; + + const projectInfo = appConfig.projectGraphs.find( + (graph) => graph.id === selectedProjectId + ); + + const fetchProjectGraph = async () => { + const project: DepGraphClientResponse = + await projectGraphService.getProjectGraph(projectInfo.url); + + const workspaceLayout = project?.layout; + depGraphService.send({ + type: 'initGraph', + projects: project.projects, + dependencies: project.dependencies, + affectedProjects: project.affected, + workspaceLayout: workspaceLayout, + }); + + if (environment.focusedProject) { + depGraphService.send({ + type: 'focusProject', + projectName: environment.focusedProject, + }); + } + + if (environment.groupByFolder) { + depGraphService.send({ + type: 'setGroupByFolder', + groupByFolder: true, + }); + } + }; + fetchProjectGraph(); + }, [selectedProjectId, environment, depGraphService, projectGraphService]); + + useIntervalWhen( + () => { + const projectInfo = environment.appConfig.projectGraphs.find( + (graph) => graph.id === selectedProjectId + ); + + const fetchProjectGraph = async () => { + const project: DepGraphClientResponse = + await projectGraphService.getProjectGraph(projectInfo.url); + + depGraphService.send({ + type: 'updateGraph', + projects: project.projects, + dependencies: project.dependencies, + }); + }; + + fetchProjectGraph(); + }, + 5000, + environment.watch + ); + + function downloadImage() { + const graph = getGraphService(); + const data = graph.getImage(); + + let downloadLink = document.createElement('a'); + downloadLink.href = data; + downloadLink.download = 'graph.png'; + // this is necessary as link.click() does not work on the latest firefox + downloadLink.dispatchEvent( + new MouseEvent('click', { + bubbles: true, + cancelable: true, + view: window, + }) + ); + } + + return ( + <> + +
+ {environment.appConfig.showDebugger ? ( + + ) : null} + + {!projectIsSelected ? ( +
+ + + +

Please select projects in the sidebar.

+
+ ) : null} +
+
+ + + +
+
+ + ); +} diff --git a/dep-graph/client/src/app/sidebar/focused-project-panel.tsx b/dep-graph/client/src/app/sidebar/focused-project-panel.tsx new file mode 100644 index 0000000000..0803c59b77 --- /dev/null +++ b/dep-graph/client/src/app/sidebar/focused-project-panel.tsx @@ -0,0 +1,59 @@ +import { memo } from 'react'; + +export interface FocusedProjectPanelProps { + focusedProject: string; + resetFocus: () => void; +} + +export const FocusedProjectPanel = memo( + ({ focusedProject, resetFocus }: FocusedProjectPanelProps) => { + return ( +
+
resetFocus()} + > +

+ + + + Focused on {focusedProject} +

+
+ Reset + + + + + +
+
+
+ ); + } +); + +export default FocusedProjectPanel; diff --git a/dep-graph/client/src/app/sidebar/group-by-folder-panel.tsx b/dep-graph/client/src/app/sidebar/group-by-folder-panel.tsx new file mode 100644 index 0000000000..96914a873f --- /dev/null +++ b/dep-graph/client/src/app/sidebar/group-by-folder-panel.tsx @@ -0,0 +1,39 @@ +import { memo } from 'react'; + +export interface DisplayOptionsPanelProps { + groupByFolderChanged: (checked: boolean) => void; +} + +export const GroupByFolderPanel = memo( + ({ groupByFolderChanged }: DisplayOptionsPanelProps) => { + return ( +
+
+
+ groupByFolderChanged(event.target.checked)} + > +
+
+ +

+ Visually arrange libraries by folders with different colors. +

+
+
+
+ ); + } +); + +export default GroupByFolderPanel; diff --git a/dep-graph/client/src/app/sidebar/project-list.tsx b/dep-graph/client/src/app/sidebar/project-list.tsx new file mode 100644 index 0000000000..803074ec93 --- /dev/null +++ b/dep-graph/client/src/app/sidebar/project-list.tsx @@ -0,0 +1,280 @@ +import type { ProjectGraphNode } from '@nrwl/devkit'; +import { useDepGraphService } from '../hooks/use-dep-graph'; +import { useDepGraphSelector } from '../hooks/use-dep-graph-selector'; +import { + allProjectsSelector, + selectedProjectNamesSelector, + workspaceLayoutSelector, +} from '../machines/selectors'; +import { parseParentDirectoriesFromPilePath } from '../util'; + +function getProjectsByType(type: string, projects: ProjectGraphNode[]) { + return projects + .filter((project) => project.type === type) + .sort((a, b) => a.name.localeCompare(b.name)); +} + +interface SidebarProject { + projectGraphNode: ProjectGraphNode; + isSelected: boolean; +} + +type DirectoryProjectRecord = Record; + +function groupProjectsByDirectory( + projects: ProjectGraphNode[], + selectedProjects: string[], + workspaceLayout: { appsDir: string; libsDir: string } +): DirectoryProjectRecord { + let groups = {}; + + projects.forEach((project) => { + const workspaceRoot = + project.type === 'app' || project.type === 'e2e' + ? workspaceLayout.appsDir + : workspaceLayout.libsDir; + const directories = parseParentDirectoriesFromPilePath( + project.data.root, + workspaceRoot + ); + const directory = directories.join('/'); + + if (!groups.hasOwnProperty(directory)) { + groups[directory] = []; + } + groups[directory].push({ + projectGraphNode: project, + isSelected: selectedProjects.includes(project.name), + }); + }); + + return groups; +} + +function ProjectListItem({ + project, + toggleProject, + focusProject, +}: { + project: SidebarProject; + toggleProject: (projectId: string, currentlySelected: boolean) => void; + focusProject: (projectId: string) => void; +}) { + return ( +
  • +
    + + +
    + + {project.isSelected ? ( + + toggleProject(project.projectGraphNode.name, project.isSelected) + } + > + + + + + + ) : null} +
  • + ); +} + +function SubProjectList({ + headerText, + projects, + selectProject, + deselectProject, + focusProject, +}: { + headerText: string; + projects: SidebarProject[]; + selectProject: (projectName: string) => void; + deselectProject: (projectName: string) => void; + focusProject: (projectName: string) => void; +}) { + let sortedProjects = [...projects]; + sortedProjects.sort((a, b) => { + return a.projectGraphNode.name.localeCompare(b.projectGraphNode.name); + }); + + function toggleProject(projectName: string, currentlySelected: boolean) { + if (currentlySelected) { + deselectProject(projectName); + } else { + selectProject(projectName); + } + } + + return ( + <> +

    + {headerText} +

    +
      + {sortedProjects.map((project) => { + return ( + + ); + })} +
    + + ); +} + +export function ProjectList() { + const depGraphService = useDepGraphService(); + + function deselectProject(projectName: string) { + depGraphService.send({ type: 'deselectProject', projectName }); + } + + function selectProject(projectName: string) { + depGraphService.send({ type: 'selectProject', projectName }); + } + + function focusProject(projectName: string) { + depGraphService.send({ type: 'focusProject', projectName }); + } + + const projects = useDepGraphSelector(allProjectsSelector); + const workspaceLayout = useDepGraphSelector(workspaceLayoutSelector); + const selectedProjects = useDepGraphSelector(selectedProjectNamesSelector); + + const appProjects = getProjectsByType('app', projects); + const libProjects = getProjectsByType('lib', projects); + const e2eProjects = getProjectsByType('e2e', projects); + + const appDirectoryGroups = groupProjectsByDirectory( + appProjects, + selectedProjects, + workspaceLayout + ); + const libDirectoryGroups = groupProjectsByDirectory( + libProjects, + selectedProjects, + workspaceLayout + ); + const e2eDirectoryGroups = groupProjectsByDirectory( + e2eProjects, + selectedProjects, + workspaceLayout + ); + + const sortedAppDirectories = Object.keys(appDirectoryGroups).sort(); + const sortedLibDirectories = Object.keys(libDirectoryGroups).sort(); + const sortedE2EDirectories = Object.keys(e2eDirectoryGroups).sort(); + + return ( +
    +

    + app projects +

    + + {sortedAppDirectories.map((directoryName) => { + return ( + + ); + })} + +

    + e2e projects +

    + + {sortedE2EDirectories.map((directoryName) => { + return ( + + ); + })} + +

    + lib projects +

    + + {sortedLibDirectories.map((directoryName) => { + return ( + + ); + })} +
    + ); +} + +export default ProjectList; diff --git a/dep-graph/client/src/app/sidebar/search-depth.tsx b/dep-graph/client/src/app/sidebar/search-depth.tsx new file mode 100644 index 0000000000..e46a3b9fc8 --- /dev/null +++ b/dep-graph/client/src/app/sidebar/search-depth.tsx @@ -0,0 +1,102 @@ +import { memo } from 'react'; + +export interface SearchDepthProps { + searchDepth: number; + searchDepthEnabled: boolean; + searchDepthFilterEnabledChange: (checked: boolean) => void; + decrementDepthFilter: () => void; + incrementDepthFilter: () => void; +} + +export const SearchDepth = memo( + ({ + searchDepth, + searchDepthEnabled, + searchDepthFilterEnabledChange, + decrementDepthFilter, + incrementDepthFilter, + }: SearchDepthProps) => { + return ( +
    +
    +
    + + searchDepthFilterEnabledChange(event.target.checked) + } + > +
    +
    + +

    + Explore connected libraries step by step. +

    +
    +
    +
    +
    + + + {searchDepth} + + +
    +
    +
    + ); + } +); + +export default SearchDepth; diff --git a/dep-graph/client/src/app/sidebar/show-hide-projects.tsx b/dep-graph/client/src/app/sidebar/show-hide-projects.tsx new file mode 100644 index 0000000000..b2a27e3b1f --- /dev/null +++ b/dep-graph/client/src/app/sidebar/show-hide-projects.tsx @@ -0,0 +1,100 @@ +import { memo } from 'react'; + +export interface ShowHideAllProjectsProps { + showAllProjects: () => void; + hideAllProjects: () => void; + showAffectedProjects: () => void; + hasAffectedProjects: boolean; +} + +export const ShowHideAllProjects = memo( + ({ + showAllProjects, + hideAllProjects, + showAffectedProjects, + hasAffectedProjects: affectedProjects, + }: ShowHideAllProjectsProps) => { + return ( +
    + + + {affectedProjects ? ( + + ) : null} + + +
    + ); + } +); + +export default ShowHideAllProjects; diff --git a/dep-graph/client/src/app/sidebar/sidebar.tsx b/dep-graph/client/src/app/sidebar/sidebar.tsx new file mode 100644 index 0000000000..c94bf0a907 --- /dev/null +++ b/dep-graph/client/src/app/sidebar/sidebar.tsx @@ -0,0 +1,181 @@ +import { useCallback } from 'react'; +import { useDepGraphService } from '../hooks/use-dep-graph'; +import { useDepGraphSelector } from '../hooks/use-dep-graph-selector'; +import { + focusedProjectNameSelector, + hasAffectedProjectsSelector, + includePathSelector, + searchDepthSelector, + textFilterSelector, +} from '../machines/selectors'; +import FocusedProjectPanel from './focused-project-panel'; +import GroupByFolderPanel from './group-by-folder-panel'; +import ProjectList from './project-list'; +import SearchDepth from './search-depth'; +import ShowHideProjects from './show-hide-projects'; +import TextFilterPanel from './text-filter-panel'; + +export function Sidebar() { + const depGraphService = useDepGraphService(); + const focusedProject = useDepGraphSelector(focusedProjectNameSelector); + const searchDepthInfo = useDepGraphSelector(searchDepthSelector); + const includePath = useDepGraphSelector(includePathSelector); + const textFilter = useDepGraphSelector(textFilterSelector); + const hasAffectedProjects = useDepGraphSelector(hasAffectedProjectsSelector); + + function resetFocus() { + depGraphService.send({ type: 'unfocusProject' }); + } + + function showAllProjects() { + depGraphService.send({ type: 'selectAll' }); + } + + function hideAllProjects() { + depGraphService.send({ type: 'deselectAll' }); + } + + function showAffectedProjects() { + depGraphService.send({ type: 'selectAffected' }); + } + + function searchDepthFilterEnabledChange(checked: boolean) { + depGraphService.send({ + type: 'setSearchDepthEnabled', + searchDepthEnabled: checked, + }); + } + + function groupByFolderChanged(checked: boolean) { + depGraphService.send({ type: 'setGroupByFolder', groupByFolder: checked }); + } + + function incrementDepthFilter() { + depGraphService.send({ type: 'incrementSearchDepth' }); + } + + function decrementDepthFilter() { + depGraphService.send({ type: 'decrementSearchDepth' }); + } + + function resetTextFilter() { + depGraphService.send({ type: 'clearTextFilter' }); + } + + function includeLibsInPathChange() { + depGraphService.send({ + type: 'setIncludeProjectsByPath', + includeProjectsByPath: !includePath, + }); + } + + const updateTextFilter = useCallback( + (textFilter: string) => { + depGraphService.send({ type: 'filterByText', search: textFilter }); + }, + [depGraphService] + ); + + return ( + + ); +} + +export default Sidebar; diff --git a/dep-graph/client/src/app/sidebar/text-filter-panel.tsx b/dep-graph/client/src/app/sidebar/text-filter-panel.tsx new file mode 100644 index 0000000000..e008d9557b --- /dev/null +++ b/dep-graph/client/src/app/sidebar/text-filter-panel.tsx @@ -0,0 +1,139 @@ +import { useEffect, useState } from 'react'; +import { useDebounce } from '../hooks/use-debounce'; + +export interface TextFilterPanelProps { + textFilter: string; + resetTextFilter: () => void; + updateTextFilter: (textFilter: string) => void; + toggleIncludeLibsInPathChange: () => void; + includePath: boolean; +} + +export function TextFilterPanel({ + textFilter, + resetTextFilter, + updateTextFilter, + toggleIncludeLibsInPathChange, + includePath, +}: TextFilterPanelProps) { + const [currentTextFilter, setCurrentTextFilter] = useState(''); + + const debouncedTextFilter = useDebounce(currentTextFilter, 500); + + function onTextFilterKeyUp(event: React.KeyboardEvent) { + if (event.key === 'Enter') { + updateTextFilter(event.currentTarget.value); + } + } + + function onTextInputChange(change: string) { + if (change === '') { + setCurrentTextFilter(''); + resetTextFilter(); + } else { + setCurrentTextFilter(change); + } + } + + function resetClicked() { + setCurrentTextFilter(''); + resetTextFilter(); + } + + useEffect(() => { + if (debouncedTextFilter !== '') { + updateTextFilter(debouncedTextFilter); + } + }, [debouncedTextFilter, updateTextFilter]); + + return ( +
    +
    +
    event.preventDefault()} + > + + + + + + onTextInputChange(event.currentTarget.value)} + > + {currentTextFilter.length > 0 ? ( + + ) : null} +
    +
    + +
    +
    +
    + +
    +
    + +

    + Show libraries that are related to your search. +

    +
    +
    +
    +
    + ); +} + +export default TextFilterPanel; diff --git a/dep-graph/client/src/app/state.provider.tsx b/dep-graph/client/src/app/state.provider.tsx new file mode 100644 index 0000000000..d4f7a5773f --- /dev/null +++ b/dep-graph/client/src/app/state.provider.tsx @@ -0,0 +1,18 @@ +import { createContext } from 'react'; +import { InterpreterFrom } from 'xstate'; +import { depGraphMachine } from './machines/dep-graph.machine'; +import { getDepGraphService } from './machines/dep-graph.service'; + +export const GlobalStateContext = createContext< + InterpreterFrom +>({} as InterpreterFrom); + +export const GlobalStateProvider = (props) => { + const depGraphService = getDepGraphService(); + + return ( + + {props.children} + + ); +}; diff --git a/dep-graph/dep-graph/src/app/styles-graph/edges.ts b/dep-graph/client/src/app/styles-graph/edges.ts similarity index 100% rename from dep-graph/dep-graph/src/app/styles-graph/edges.ts rename to dep-graph/client/src/app/styles-graph/edges.ts diff --git a/dep-graph/dep-graph/src/app/styles-graph/fonts.ts b/dep-graph/client/src/app/styles-graph/fonts.ts similarity index 100% rename from dep-graph/dep-graph/src/app/styles-graph/fonts.ts rename to dep-graph/client/src/app/styles-graph/fonts.ts diff --git a/dep-graph/dep-graph/src/app/styles-graph/index.ts b/dep-graph/client/src/app/styles-graph/index.ts similarity index 100% rename from dep-graph/dep-graph/src/app/styles-graph/index.ts rename to dep-graph/client/src/app/styles-graph/index.ts diff --git a/dep-graph/dep-graph/src/app/styles-graph/nodes.ts b/dep-graph/client/src/app/styles-graph/nodes.ts similarity index 100% rename from dep-graph/dep-graph/src/app/styles-graph/nodes.ts rename to dep-graph/client/src/app/styles-graph/nodes.ts diff --git a/dep-graph/dep-graph/src/app/styles-graph/palette.ts b/dep-graph/client/src/app/styles-graph/palette.ts similarity index 100% rename from dep-graph/dep-graph/src/app/styles-graph/palette.ts rename to dep-graph/client/src/app/styles-graph/palette.ts diff --git a/dep-graph/dep-graph/src/app/tooltip-service.ts b/dep-graph/client/src/app/tooltip-service.ts similarity index 100% rename from dep-graph/dep-graph/src/app/tooltip-service.ts rename to dep-graph/client/src/app/tooltip-service.ts diff --git a/dep-graph/dep-graph/src/app/util-cytoscape/cytoscape.models.ts b/dep-graph/client/src/app/util-cytoscape/cytoscape.models.ts similarity index 100% rename from dep-graph/dep-graph/src/app/util-cytoscape/cytoscape.models.ts rename to dep-graph/client/src/app/util-cytoscape/cytoscape.models.ts diff --git a/dep-graph/dep-graph/src/app/util-cytoscape/edge.ts b/dep-graph/client/src/app/util-cytoscape/edge.ts similarity index 100% rename from dep-graph/dep-graph/src/app/util-cytoscape/edge.ts rename to dep-graph/client/src/app/util-cytoscape/edge.ts diff --git a/dep-graph/dep-graph/src/app/util-cytoscape/index.ts b/dep-graph/client/src/app/util-cytoscape/index.ts similarity index 100% rename from dep-graph/dep-graph/src/app/util-cytoscape/index.ts rename to dep-graph/client/src/app/util-cytoscape/index.ts diff --git a/dep-graph/dep-graph/src/app/util-cytoscape/parent-node.ts b/dep-graph/client/src/app/util-cytoscape/parent-node.ts similarity index 100% rename from dep-graph/dep-graph/src/app/util-cytoscape/parent-node.ts rename to dep-graph/client/src/app/util-cytoscape/parent-node.ts diff --git a/dep-graph/dep-graph/src/app/util-cytoscape/project-node.spec.ts b/dep-graph/client/src/app/util-cytoscape/project-node.spec.ts similarity index 100% rename from dep-graph/dep-graph/src/app/util-cytoscape/project-node.spec.ts rename to dep-graph/client/src/app/util-cytoscape/project-node.spec.ts diff --git a/dep-graph/dep-graph/src/app/util-cytoscape/project-node.ts b/dep-graph/client/src/app/util-cytoscape/project-node.ts similarity index 100% rename from dep-graph/dep-graph/src/app/util-cytoscape/project-node.ts rename to dep-graph/client/src/app/util-cytoscape/project-node.ts diff --git a/dep-graph/dep-graph/src/app/util.spec.ts b/dep-graph/client/src/app/util.spec.ts similarity index 94% rename from dep-graph/dep-graph/src/app/util.spec.ts rename to dep-graph/client/src/app/util.spec.ts index d4794b1963..cf142f5830 100644 --- a/dep-graph/dep-graph/src/app/util.spec.ts +++ b/dep-graph/client/src/app/util.spec.ts @@ -1,6 +1,6 @@ import { parseParentDirectoriesFromPilePath } from './util'; -describe('parseParentDirectoriesFromPilePath', () => { +describe('parseParentDirectoriesFromFilePath', () => { // path, workspaceRoot, output const cases: [string, string, string[]][] = [ ['apps/app1', 'apps', []], diff --git a/dep-graph/client/src/app/util.ts b/dep-graph/client/src/app/util.ts new file mode 100644 index 0000000000..fcbd84e7b8 --- /dev/null +++ b/dep-graph/client/src/app/util.ts @@ -0,0 +1,56 @@ +import { ProjectGraphDependency } from '@nrwl/devkit'; + +export function trimBackSlash(value: string): string { + return value.replace(/\/$/, ''); +} + +export function parseParentDirectoriesFromPilePath( + path: string, + workspaceRoot: string +) { + const root = trimBackSlash(path); + + // split the source root on directory separator + const split: string[] = root.split('/'); + + // check the first part for libs or apps, depending on workspaceLayout + if (split[0] === trimBackSlash(workspaceRoot)) { + split.shift(); + } + + // pop off the last element, which should be the lib name + split.pop(); + + return split; +} + +export function hasPath( + dependencies: Record, + target: string, + node: string, + visited: string[], + currentSearchDepth: number, + maxSearchDepth: number = -1 // -1 indicates unlimited search depth +) { + if (target === node) return true; + + if (maxSearchDepth === -1 || currentSearchDepth <= maxSearchDepth) { + for (let d of dependencies[node] || []) { + if (visited.indexOf(d.target) > -1) continue; + visited.push(d.target); + if ( + hasPath( + dependencies, + target, + d.target, + visited, + currentSearchDepth + 1, + maxSearchDepth + ) + ) + return true; + } + } + + return false; +} diff --git a/dep-graph/dep-graph/src/assets/.gitkeep b/dep-graph/client/src/assets/.gitkeep similarity index 100% rename from dep-graph/dep-graph/src/assets/.gitkeep rename to dep-graph/client/src/assets/.gitkeep diff --git a/dep-graph/dep-graph/src/assets/environment.dev.js b/dep-graph/client/src/assets/environment.dev.js similarity index 100% rename from dep-graph/dep-graph/src/assets/environment.dev.js rename to dep-graph/client/src/assets/environment.dev.js diff --git a/dep-graph/dep-graph/src/assets/environment.watch.js b/dep-graph/client/src/assets/environment.watch.js similarity index 100% rename from dep-graph/dep-graph/src/assets/environment.watch.js rename to dep-graph/client/src/assets/environment.watch.js diff --git a/dep-graph/dep-graph/src/assets/graphs/affected.json b/dep-graph/client/src/assets/graphs/affected.json similarity index 100% rename from dep-graph/dep-graph/src/assets/graphs/affected.json rename to dep-graph/client/src/assets/graphs/affected.json diff --git a/dep-graph/dep-graph/src/assets/graphs/focus-testing.json b/dep-graph/client/src/assets/graphs/focus-testing.json similarity index 100% rename from dep-graph/dep-graph/src/assets/graphs/focus-testing.json rename to dep-graph/client/src/assets/graphs/focus-testing.json diff --git a/dep-graph/dep-graph/src/assets/graphs/nx-examples.json b/dep-graph/client/src/assets/graphs/nx-examples.json similarity index 100% rename from dep-graph/dep-graph/src/assets/graphs/nx-examples.json rename to dep-graph/client/src/assets/graphs/nx-examples.json diff --git a/dep-graph/dep-graph/src/assets/graphs/nx.json b/dep-graph/client/src/assets/graphs/nx.json similarity index 100% rename from dep-graph/dep-graph/src/assets/graphs/nx.json rename to dep-graph/client/src/assets/graphs/nx.json diff --git a/dep-graph/dep-graph/src/assets/graphs/ocean.json b/dep-graph/client/src/assets/graphs/ocean.json similarity index 100% rename from dep-graph/dep-graph/src/assets/graphs/ocean.json rename to dep-graph/client/src/assets/graphs/ocean.json diff --git a/dep-graph/dep-graph/src/assets/graphs/storybook.json b/dep-graph/client/src/assets/graphs/storybook.json similarity index 100% rename from dep-graph/dep-graph/src/assets/graphs/storybook.json rename to dep-graph/client/src/assets/graphs/storybook.json diff --git a/dep-graph/dep-graph/src/assets/graphs/sub-apps.json b/dep-graph/client/src/assets/graphs/sub-apps.json similarity index 100% rename from dep-graph/dep-graph/src/assets/graphs/sub-apps.json rename to dep-graph/client/src/assets/graphs/sub-apps.json diff --git a/dep-graph/client/src/environments/environment.prod.ts b/dep-graph/client/src/environments/environment.prod.ts new file mode 100644 index 0000000000..c9669790be --- /dev/null +++ b/dep-graph/client/src/environments/environment.prod.ts @@ -0,0 +1,3 @@ +export const environment = { + production: true, +}; diff --git a/dep-graph/client/src/environments/environment.ts b/dep-graph/client/src/environments/environment.ts new file mode 100644 index 0000000000..7ed83767ff --- /dev/null +++ b/dep-graph/client/src/environments/environment.ts @@ -0,0 +1,6 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// When building for production, this file is replaced with `environment.prod.ts`. + +export const environment = { + production: false, +}; diff --git a/dep-graph/dep-graph/src/favicon.ico b/dep-graph/client/src/favicon.ico similarity index 100% rename from dep-graph/dep-graph/src/favicon.ico rename to dep-graph/client/src/favicon.ico diff --git a/dep-graph/dep-graph/src/globals.d.ts b/dep-graph/client/src/globals.d.ts similarity index 79% rename from dep-graph/dep-graph/src/globals.d.ts rename to dep-graph/client/src/globals.d.ts index d91999b287..2f74708f14 100644 --- a/dep-graph/dep-graph/src/globals.d.ts +++ b/dep-graph/client/src/globals.d.ts @@ -1,7 +1,5 @@ // nx-ignore-next-line -import { DepGraphClientResponse } from '@nrwl/workspace/src/command-line/dep-graph'; -import { ProjectGraph, ProjectGraphNode } from '@nrwl/devkit'; -import { ProjectGraphList } from './graphs'; +import type { DepGraphClientResponse } from '@nrwl/workspace/src/command-line/dep-graph'; import { AppConfig } from './app/models'; export declare global { diff --git a/dep-graph/client/src/index.html b/dep-graph/client/src/index.html new file mode 100644 index 0000000000..571eaa5db8 --- /dev/null +++ b/dep-graph/client/src/index.html @@ -0,0 +1,16 @@ + + + + + Nx Workspace Dependency Graph + + + + + + + + +
    + + diff --git a/dep-graph/client/src/main.tsx b/dep-graph/client/src/main.tsx new file mode 100644 index 0000000000..829b92689d --- /dev/null +++ b/dep-graph/client/src/main.tsx @@ -0,0 +1,18 @@ +import { StrictMode } from 'react'; +import * as ReactDOM from 'react-dom'; +import { inspect } from '@xstate/inspect'; +import App from './app/app'; + +if (window.useXstateInspect === true) { + inspect({ + url: 'https://stately.ai/viz?inspect', + iframe: false, // open in new window + }); +} + +ReactDOM.render( + + + , + document.getElementById('app') +); diff --git a/dep-graph/dep-graph/src/polyfills.ts b/dep-graph/client/src/polyfills.ts similarity index 100% rename from dep-graph/dep-graph/src/polyfills.ts rename to dep-graph/client/src/polyfills.ts diff --git a/dep-graph/dep-graph/src/styles.scss b/dep-graph/client/src/styles.scss similarity index 100% rename from dep-graph/dep-graph/src/styles.scss rename to dep-graph/client/src/styles.scss index 0a0d006916..22dea58976 100644 --- a/dep-graph/dep-graph/src/styles.scss +++ b/dep-graph/client/src/styles.scss @@ -20,6 +20,24 @@ html { display: flex; } +#no-projects-chosen { + display: flex; + width: 100%; + height: 100%; + align-items: center; + justify-content: center; +} + +#graph-container, +#cytoscape-graph { + width: 100%; + height: 100%; +} + +canvas { + cursor: pointer; +} + .tippy-box[data-theme~='nx'] { box-sizing: border-box; border-style: solid; @@ -90,21 +108,3 @@ html { background-color: rgba(243, 244, 246, 1); } } - -#no-projects-chosen { - display: flex; - width: 100%; - height: 100%; - align-items: center; - justify-content: center; -} - -#graph-container, -#cytoscape-graph { - width: 100%; - height: 100%; -} - -canvas { - cursor: pointer; -} diff --git a/dep-graph/dep-graph/tailwind.config.js b/dep-graph/client/tailwind.config.js similarity index 100% rename from dep-graph/dep-graph/tailwind.config.js rename to dep-graph/client/tailwind.config.js diff --git a/dep-graph/client/tsconfig.app.json b/dep-graph/client/tsconfig.app.json new file mode 100644 index 0000000000..7a1a35f04e --- /dev/null +++ b/dep-graph/client/tsconfig.app.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": ["node"], + "lib": ["DOM", "es2019"] + }, + "files": [ + "../../node_modules/@nrwl/react/typings/cssmodule.d.ts", + "../../node_modules/@nrwl/react/typings/image.d.ts" + ], + "exclude": [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx" + ], + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] +} diff --git a/dep-graph/dep-graph/tsconfig.json b/dep-graph/client/tsconfig.json similarity index 67% rename from dep-graph/dep-graph/tsconfig.json rename to dep-graph/client/tsconfig.json index 63dbe35fb2..ad56986cc1 100644 --- a/dep-graph/dep-graph/tsconfig.json +++ b/dep-graph/client/tsconfig.json @@ -1,5 +1,9 @@ { "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowSyntheticDefaultImports": true + }, "files": [], "include": [], "references": [ diff --git a/dep-graph/client/tsconfig.spec.json b/dep-graph/client/tsconfig.spec.json new file mode 100644 index 0000000000..65f3147400 --- /dev/null +++ b/dep-graph/client/tsconfig.spec.json @@ -0,0 +1,24 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"], + "lib": ["DOM"] + }, + "include": [ + "**/*.test.ts", + "**/*.spec.ts", + "**/*.test.tsx", + "**/*.spec.tsx", + "**/*.test.js", + "**/*.spec.js", + "**/*.test.jsx", + "**/*.spec.jsx", + "**/*.d.ts" + ], + "files": [ + "../../node_modules/@nrwl/react/typings/cssmodule.d.ts", + "../../node_modules/@nrwl/react/typings/image.d.ts" + ] +} diff --git a/dep-graph/dep-graph-e2e/project.json b/dep-graph/dep-graph-e2e/project.json deleted file mode 100644 index 46b324374f..0000000000 --- a/dep-graph/dep-graph-e2e/project.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "root": "dep-graph/dep-graph-e2e", - "sourceRoot": "dep-graph/dep-graph-e2e/src", - "projectType": "application", - "targets": { - "e2e-disabled": { - "executor": "@nrwl/cypress:cypress", - "options": { - "cypressConfig": "dep-graph/dep-graph-e2e/cypress.json", - "tsConfig": "dep-graph/dep-graph-e2e/tsconfig.e2e.json", - "devServerTarget": "dep-graph-dep-graph:serve-for-e2e", - "baseUrl": "http://localhost:4200" - } - }, - "e2e-watch-disabled": { - "executor": "@nrwl/cypress:cypress", - "options": { - "cypressConfig": "dep-graph/dep-graph-e2e/cypress-watch-mode.json", - "tsConfig": "dep-graph/dep-graph-e2e/tsconfig.e2e.json", - "devServerTarget": "dep-graph-dep-graph:serve-for-e2e:watch", - "baseUrl": "http://localhost:4200" - } - }, - "lint": { - "executor": "@nrwl/linter:eslint", - "outputs": ["{options.outputFile}"], - "options": { - "lintFilePatterns": ["dep-graph/dep-graph-e2e/**/*.ts"] - } - } - }, - "implicitDependencies": ["dep-graph-dep-graph"] -} diff --git a/dep-graph/dep-graph/.babelrc b/dep-graph/dep-graph/.babelrc deleted file mode 100644 index 0cae4a9a81..0000000000 --- a/dep-graph/dep-graph/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@nrwl/web/babel"] -} diff --git a/dep-graph/dep-graph/.eslintrc b/dep-graph/dep-graph/.eslintrc deleted file mode 100644 index ab8f38339c..0000000000 --- a/dep-graph/dep-graph/.eslintrc +++ /dev/null @@ -1 +0,0 @@ -{ "extends": "../../.eslintrc", "rules": {}, "ignorePatterns": ["!**/*"] } diff --git a/dep-graph/dep-graph/jest.config.js b/dep-graph/dep-graph/jest.config.js deleted file mode 100644 index 35d028a001..0000000000 --- a/dep-graph/dep-graph/jest.config.js +++ /dev/null @@ -1,15 +0,0 @@ -module.exports = { - name: 'dep-graph-dep-graph', - preset: '../../jest.preset.js', - setupFilesAfterEnv: ['/src/test-setup.ts'], - globals: { - 'ts-jest': { - tsconfig: '/tsconfig.spec.json', - }, - }, - transform: { - '^.+\\.[tj]s$': 'ts-jest', - }, - moduleFileExtensions: ['ts', 'js', 'html'], - coverageDirectory: '../../coverage/dep-graph/dep-graph', -}; diff --git a/dep-graph/dep-graph/src/app/app.ts b/dep-graph/dep-graph/src/app/app.ts deleted file mode 100644 index efa5380e9f..0000000000 --- a/dep-graph/dep-graph/src/app/app.ts +++ /dev/null @@ -1,153 +0,0 @@ -// nx-ignore-next-line -import type { DepGraphClientResponse } from '@nrwl/workspace/src/command-line/dep-graph'; -import { fromEvent } from 'rxjs'; -import { startWith } from 'rxjs/operators'; -import tippy from 'tippy.js'; -import { DebuggerPanel } from './debugger-panel'; -import { useGraphService } from './graph.service'; -import { useDepGraphService } from './machines/dep-graph.service'; -import { DepGraphSend } from './machines/interfaces'; -import { AppConfig, DEFAULT_CONFIG, ProjectGraphService } from './models'; -import { SidebarComponent } from './ui-sidebar/sidebar'; - -export class AppComponent { - private sidebar = new SidebarComponent(); - private graph = useGraphService(); - private debuggerPanel: DebuggerPanel; - - private windowResize$ = fromEvent(window, 'resize').pipe(startWith({})); - - private send: DepGraphSend; - - private downloadImageButton: HTMLButtonElement; - - constructor( - private config: AppConfig = DEFAULT_CONFIG, - private projectGraphService: ProjectGraphService - ) { - const [state$, send] = useDepGraphService(); - - state$.subscribe((state) => { - if (state.context.selectedProjects.length !== 0) { - document.getElementById('no-projects-chosen').style.display = 'none'; - if (this.downloadImageButton) { - this.downloadImageButton.classList.remove('opacity-0'); - } - } else { - document.getElementById('no-projects-chosen').style.display = 'flex'; - if (this.downloadImageButton) { - this.downloadImageButton.classList.add('opacity-0'); - } - } - }); - - this.send = send; - - this.loadProjectGraph(config.defaultProjectGraph); - this.render(); - - if (window.watch === true) { - setInterval( - () => this.updateProjectGraph(config.defaultProjectGraph), - 5000 - ); - } - - this.downloadImageButton = document.querySelector( - '[data-cy="downloadImageButton"]' - ); - - this.downloadImageButton.addEventListener('click', () => { - const graph = useGraphService(); - const data = graph.getImage(); - - var downloadLink = document.createElement('a'); - downloadLink.href = data; - downloadLink.download = 'graph.png'; - // this is necessary as link.click() does not work on the latest firefox - downloadLink.dispatchEvent( - new MouseEvent('click', { - bubbles: true, - cancelable: true, - view: window, - }) - ); - }); - - tippy(this.downloadImageButton, { - content: 'Download Graph as PNG', - placement: 'right', - theme: 'nx', - }); - } - - private async loadProjectGraph(projectGraphId: string) { - const projectInfo = this.config.projectGraphs.find( - (graph) => graph.id === projectGraphId - ); - - const project: DepGraphClientResponse = - await this.projectGraphService.getProjectGraph(projectInfo.url); - - const workspaceLayout = project?.layout; - this.send({ - type: 'initGraph', - projects: project.projects, - dependencies: project.dependencies, - affectedProjects: project.affected, - workspaceLayout: workspaceLayout, - }); - - if (!!window.focusedProject) { - this.send({ - type: 'focusProject', - projectName: window.focusedProject, - }); - } - - if (window.groupByFolder) { - this.send({ - type: 'setGroupByFolder', - groupByFolder: window.groupByFolder, - }); - } - } - - private async updateProjectGraph(projectGraphId: string) { - const projectInfo = this.config.projectGraphs.find( - (graph) => graph.id === projectGraphId - ); - - const project: DepGraphClientResponse = - await this.projectGraphService.getProjectGraph(projectInfo.url); - - this.send({ - type: 'updateGraph', - projects: project.projects, - dependencies: project.dependencies, - }); - } - - private render() { - const debuggerPanelContainer = document.getElementById('debugger-panel'); - - if (this.config.showDebugger) { - debuggerPanelContainer.hidden = false; - debuggerPanelContainer.style.display = 'flex'; - - this.debuggerPanel = new DebuggerPanel( - debuggerPanelContainer, - this.config.projectGraphs, - this.config.defaultProjectGraph - ); - - this.debuggerPanel.selectProject$.subscribe((id) => { - this.loadProjectGraph(id); - }); - - this.graph.renderTimes$.subscribe( - (renderTime) => (this.debuggerPanel.renderTime = renderTime) - ); - } - } -} diff --git a/dep-graph/dep-graph/src/app/debugger-panel.ts b/dep-graph/dep-graph/src/app/debugger-panel.ts deleted file mode 100644 index f44b5f1480..0000000000 --- a/dep-graph/dep-graph/src/app/debugger-panel.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Subject } from 'rxjs'; -import { GraphPerfReport } from './graph'; -import { ProjectGraphList } from './models'; -import { removeChildrenFromContainer } from './util'; - -export class DebuggerPanel { - set renderTime(renderTime: GraphPerfReport) { - this.renderReportElement.innerHTML = `Last render took ${renderTime.renderTime}ms: ${renderTime.numNodes} nodes | ${renderTime.numEdges} edges.`; - } - - private selectProjectSubject = new Subject(); - - selectProject$ = this.selectProjectSubject.asObservable(); - - private renderReportElement: HTMLElement; - - constructor( - private container: HTMLElement, - private projectGraphs: ProjectGraphList[], - private initialSelectedGraph: string - ) { - this.render(); - } - - private render() { - removeChildrenFromContainer(this.container); - - const header = document.createElement('h4'); - header.className = 'text-lg font-bold mr-4'; - header.innerText = `Debugger`; - - const select = document.createElement('select'); - select.className = - 'w-auto flex items-center px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white'; - - this.projectGraphs.forEach((projectGraph) => { - const option = document.createElement('option'); - option.value = projectGraph.id; - option.innerText = projectGraph.label; - - select.appendChild(option); - }); - - select.value = this.initialSelectedGraph; - select.dataset['cy'] = 'project-select'; - - select.onchange = (event) => - this.selectProjectSubject.next( - (event.currentTarget as HTMLSelectElement).value - ); - - this.renderReportElement = document.createElement('p'); - this.renderReportElement.className = 'text-sm'; - - this.container.appendChild(header); - this.container.appendChild(select); - this.container.appendChild(this.renderReportElement); - } -} diff --git a/dep-graph/dep-graph/src/app/ui-sidebar/display-options-panel.ts b/dep-graph/dep-graph/src/app/ui-sidebar/display-options-panel.ts deleted file mode 100644 index 3db236220d..0000000000 --- a/dep-graph/dep-graph/src/app/ui-sidebar/display-options-panel.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { useGraphService } from '../graph.service'; -import { useDepGraphService } from '../machines/dep-graph.service'; -import { DepGraphSend } from '../machines/interfaces'; -import { removeChildrenFromContainer } from '../util'; - -export class DisplayOptionsPanel { - searchDepthDisplay: HTMLSpanElement; - affectedButtonElement: HTMLElement; - groupByFolderCheckboxElement: HTMLInputElement; - - send: DepGraphSend; - - constructor(private container: HTMLElement) { - const [state$, send] = useDepGraphService(); - this.send = send; - this.render(); - - state$.subscribe((state) => { - if ( - state.context.affectedProjects.length > 0 && - this.affectedButtonElement.classList.contains('hidden') - ) { - this.affectedButtonElement.classList.remove('hidden'); - } else if ( - state.context.affectedProjects.length === 0 && - !this.affectedButtonElement.classList.contains('hidden') - ) { - this.affectedButtonElement.classList.add('hidden'); - } - - this.searchDepthDisplay.innerText = state.context.searchDepth.toString(); - - if ( - this.groupByFolderCheckboxElement.checked !== - state.context.groupByFolder - ) { - this.groupByFolderCheckboxElement.checked = state.context.groupByFolder; - } - }); - } - - private static renderHtmlTemplate(): HTMLElement { - const render = document.createElement('template'); - render.innerHTML = ` -
    -
    - - - -
    - -
    -
    -
    - -
    -
    - -

    Visually arrange libraries by folders with different colors.

    -
    -
    -
    - -
    -
    -
    - -
    -
    - -

    Explore connected libraries step by step.

    -
    -
    -
    -
    - - 1 - -
    -
    -
    -
    - `.trim(); - return render.content.firstChild as HTMLElement; - } - - private render() { - removeChildrenFromContainer(this.container); - - const element = DisplayOptionsPanel.renderHtmlTemplate(); - - this.affectedButtonElement = element.querySelector( - '[data-cy="affectedButton"]' - ); - - this.affectedButtonElement.addEventListener('click', () => - this.send({ type: 'selectAffected' }) - ); - - const selectAllButtonElement: HTMLElement = element.querySelector( - '[data-cy="selectAllButton"]' - ); - selectAllButtonElement.addEventListener('click', () => { - this.send({ type: 'selectAll' }); - }); - - const deselectAllButtonElement: HTMLElement = element.querySelector( - '[data-cy="deselectAllButton"]' - ); - deselectAllButtonElement.addEventListener('click', () => { - this.send({ type: 'deselectAll' }); - }); - - this.groupByFolderCheckboxElement = - element.querySelector('#displayOptions'); - - this.groupByFolderCheckboxElement.addEventListener( - 'change', - (event: InputEvent) => - this.send({ - type: 'setGroupByFolder', - groupByFolder: (event.target as HTMLInputElement).checked, - }) - ); - - this.searchDepthDisplay = element.querySelector('#depthFilterValue'); - const incrementButtonElement: HTMLInputElement = element.querySelector( - '#depthFilterIncrement' - ); - const decrementButtonElement: HTMLInputElement = element.querySelector( - '#depthFilterDecrement' - ); - const searchDepthEnabledElement: HTMLInputElement = - element.querySelector('#depthFilter'); - - incrementButtonElement.addEventListener('click', () => { - this.send({ type: 'incrementSearchDepth' }); - }); - decrementButtonElement.addEventListener('click', () => { - this.send({ type: 'decrementSearchDepth' }); - }); - - searchDepthEnabledElement.addEventListener('change', (event: InputEvent) => - this.send({ - type: 'setSearchDepthEnabled', - searchDepthEnabled: (event.target).checked, - }) - ); - - this.container.appendChild(element); - } -} diff --git a/dep-graph/dep-graph/src/app/ui-sidebar/focused-project-panel.ts b/dep-graph/dep-graph/src/app/ui-sidebar/focused-project-panel.ts deleted file mode 100644 index 93e6b8d689..0000000000 --- a/dep-graph/dep-graph/src/app/ui-sidebar/focused-project-panel.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { map } from 'rxjs/operators'; -import { useDepGraphService } from '../machines/dep-graph.service'; -import { DepGraphSend } from '../machines/interfaces'; -import { removeChildrenFromContainer } from '../util'; - -export class FocusedProjectPanel { - private send: DepGraphSend; - - constructor(private container: HTMLElement) { - const [state$, send] = useDepGraphService(); - this.send = send; - - state$ - .pipe(map(({ context }) => context.focusedProject)) - .subscribe((focusedProject) => this.render(focusedProject)); - } - - private static renderHtmlTemplate(): HTMLElement { - const render = document.createElement('template'); - render.innerHTML = ` -
    -
    -

    - - - - e2e-some-other-very-long-project-name -

    -
    - Reset - - - - - -
    -
    -
    - `.trim(); - return render.content.firstChild as HTMLElement; - } - - private render(projectName?: string) { - removeChildrenFromContainer(this.container); - - const element = FocusedProjectPanel.renderHtmlTemplate(); - const projectNameElement: HTMLElement = element.querySelector( - '#focused-project-name' - ); - const unfocusButtonElement = element.querySelector( - '[data-cy="unfocusButton"]' - ); - - if (projectName && projectName !== '') { - projectNameElement.innerText = `Focused on ${projectName}`; - this.container.hidden = false; - } else { - this.container.hidden = true; - } - - unfocusButtonElement.addEventListener('click', () => - this.send({ type: 'unfocusProject' }) - ); - - this.container.appendChild(element); - } -} diff --git a/dep-graph/dep-graph/src/app/ui-sidebar/project-list.ts b/dep-graph/dep-graph/src/app/ui-sidebar/project-list.ts deleted file mode 100644 index 3da59c0762..0000000000 --- a/dep-graph/dep-graph/src/app/ui-sidebar/project-list.ts +++ /dev/null @@ -1,227 +0,0 @@ -import type { ProjectGraphNode } from '@nrwl/devkit'; -import { useDepGraphService } from '../machines/dep-graph.service'; -import { DepGraphSend } from '../machines/interfaces'; -import { - parseParentDirectoriesFromPilePath, - removeChildrenFromContainer, -} from '../util'; - -export class ProjectList { - private projectItems: Record = {}; - - private send: DepGraphSend; - - constructor(private container: HTMLElement) { - const [state$, send] = useDepGraphService(); - this.send = send; - - state$.subscribe((state) => { - this.render(state.context.projects, state.context.workspaceLayout); - this.setSelectedProjects(state.context.selectedProjects); - }); - } - - private static renderHtmlItemTemplate(): HTMLElement { - const render = document.createElement('template'); - render.innerHTML = ` -
  • -
    - - -
    - - - - - - -
  • - `.trim(); - return render.content.firstChild as HTMLElement; - } - - setSelectedProjects(selectedProjects: string[]) { - Object.keys(this.projectItems).forEach((projectName) => { - this.projectItems[projectName].dataset['active'] = selectedProjects - .includes(projectName) - .toString(); - this.projectItems[projectName].dispatchEvent(new CustomEvent('change')); - }); - } - - checkAllProjects() { - this.send({ type: 'selectAll' }); - } - - uncheckAllProjects() { - this.send({ type: 'deselectAll' }); - } - - uncheckProject(projectName: string) { - this.send({ type: 'deselectProject', projectName }); - } - - private render( - projects: ProjectGraphNode[], - workspaceLayout: { appsDir: string; libsDir: string } - ) { - removeChildrenFromContainer(this.container); - - const appProjects = this.getProjectsByType('app', projects); - const libProjects = this.getProjectsByType('lib', projects); - const e2eProjects = this.getProjectsByType('e2e', projects); - - const appDirectoryGroups = this.groupProjectsByDirectory( - appProjects, - workspaceLayout - ); - const libDirectoryGroups = this.groupProjectsByDirectory( - libProjects, - workspaceLayout - ); - const e2eDirectoryGroups = this.groupProjectsByDirectory( - e2eProjects, - workspaceLayout - ); - - const sortedAppDirectories = Object.keys(appDirectoryGroups).sort(); - const sortedLibDirectories = Object.keys(libDirectoryGroups).sort(); - const sortedE2EDirectories = Object.keys(e2eDirectoryGroups).sort(); - - const appsHeader = document.createElement('h2'); - appsHeader.className = - 'mt-8 text-lg font-bold border-b border-gray-50 border-solid'; - appsHeader.textContent = 'App projects'; - this.container.append(appsHeader); - - sortedAppDirectories.forEach((directoryName) => { - this.createProjectList(directoryName, appDirectoryGroups[directoryName]); - }); - - const e2eHeader = document.createElement('h2'); - e2eHeader.className = - 'mt-8 text-lg font-bold border-b border-gray-50 border-solid'; - e2eHeader.textContent = 'E2E projects'; - this.container.append(e2eHeader); - - sortedE2EDirectories.forEach((directoryName) => { - this.createProjectList(directoryName, e2eDirectoryGroups[directoryName]); - }); - - const libHeader = document.createElement('h2'); - libHeader.className = - 'mt-8 text-lg font-bold border-b border-gray-50 border-solid'; - libHeader.textContent = 'Lib projects'; - this.container.append(libHeader); - - sortedLibDirectories.forEach((directoryName) => { - this.createProjectList(directoryName, libDirectoryGroups[directoryName]); - }); - } - - private getProjectsByType(type: string, projects: ProjectGraphNode[]) { - return projects - .filter((project) => project.type === type) - .sort((a, b) => a.name.localeCompare(b.name)); - } - - private groupProjectsByDirectory( - projects: ProjectGraphNode[], - workspaceLayout: { appsDir: string; libsDir: string } - ) { - let groups = {}; - - projects.forEach((project) => { - const workspaceRoot = - project.type === 'app' || project.type === 'e2e' - ? workspaceLayout.appsDir - : workspaceLayout.libsDir; - const directories = parseParentDirectoriesFromPilePath( - project.data.root, - workspaceRoot - ); - const directory = directories.join('/'); - - if (!groups.hasOwnProperty(directory)) { - groups[directory] = []; - } - groups[directory].push(project); - }); - - return groups; - } - - private createProjectList(headerText, projects) { - const header = document.createElement('h3'); - header.className = - 'mt-4 py-2 uppercase tracking-wide font-semibold text-sm lg:text-xs text-gray-900 cursor-text'; - header.textContent = headerText; - - const formGroup = document.createElement('ul'); - formGroup.className = 'mt-2 -ml-3'; - - let sortedProjects = [...projects]; - sortedProjects.sort((a, b) => { - return a.name.localeCompare(b.name); - }); - - projects.forEach((project) => { - const element = ProjectList.renderHtmlItemTemplate(); - const selectedIconElement: HTMLElement = element.querySelector( - 'span[role="selection-icon"]' - ); - const focusButtonElement: HTMLElement = element.querySelector('button'); - focusButtonElement.addEventListener('click', () => - this.send({ type: 'focusProject', projectName: project.name }) - ); - - const projectNameElement: HTMLElement = element.querySelector('label'); - projectNameElement.innerText = project.name; - projectNameElement.dataset['project'] = project.name; - projectNameElement.dataset['active'] = 'false'; - selectedIconElement.classList.add('hidden'); - - projectNameElement.addEventListener('click', (event) => { - const el = event.target as HTMLElement; - if (el.dataset['active'] === 'true') { - this.send({ - type: 'deselectProject', - projectName: el.dataset['project'], - }); - } else { - this.send({ - type: 'selectProject', - projectName: el.dataset['project'], - }); - } - }); - - projectNameElement.addEventListener('change', (event) => { - const el = event.target as HTMLElement; - if (el.dataset['active'] === 'false') { - selectedIconElement.classList.add('hidden'); - } else selectedIconElement.classList.remove('hidden'); - }); - - selectedIconElement.addEventListener('click', () => { - projectNameElement.dispatchEvent(new Event('click')); - }); - - this.projectItems[project.name] = projectNameElement; - - formGroup.append(element); - }); - - this.container.append(header); - this.container.append(formGroup); - } -} diff --git a/dep-graph/dep-graph/src/app/ui-sidebar/sidebar.ts b/dep-graph/dep-graph/src/app/ui-sidebar/sidebar.ts deleted file mode 100644 index c320dc4466..0000000000 --- a/dep-graph/dep-graph/src/app/ui-sidebar/sidebar.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { DisplayOptionsPanel } from './display-options-panel'; -import { FocusedProjectPanel } from './focused-project-panel'; -import { ProjectList } from './project-list'; -import { TextFilterPanel } from './text-filter-panel'; - -declare var ResizeObserver; - -export class SidebarComponent { - private displayOptionsPanel: DisplayOptionsPanel; - private focusedProjectPanel: FocusedProjectPanel; - private textFilterPanel: TextFilterPanel; - private projectList: ProjectList; - - constructor() { - const displayOptionsPanelContainer = document.getElementById( - 'display-options-panel' - ); - - this.displayOptionsPanel = new DisplayOptionsPanel( - displayOptionsPanelContainer - ); - - const focusedProjectPanelContainer = - document.getElementById('focused-project'); - - this.focusedProjectPanel = new FocusedProjectPanel( - focusedProjectPanelContainer - ); - - const textFilterPanelContainer = - document.getElementById('text-filter-panel'); - this.textFilterPanel = new TextFilterPanel(textFilterPanelContainer); - - const projectListContainer = document.getElementById('project-lists'); - this.projectList = new ProjectList(projectListContainer); - } -} diff --git a/dep-graph/dep-graph/src/app/ui-sidebar/text-filter-panel.ts b/dep-graph/dep-graph/src/app/ui-sidebar/text-filter-panel.ts deleted file mode 100644 index a9b026ecd9..0000000000 --- a/dep-graph/dep-graph/src/app/ui-sidebar/text-filter-panel.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { fromEvent, Subscription } from 'rxjs'; -import { debounceTime, filter, map } from 'rxjs/operators'; -import { useDepGraphService } from '../machines/dep-graph.service'; -import { DepGraphSend } from '../machines/interfaces'; -import { removeChildrenFromContainer } from '../util'; - -export interface TextFilterChangeEvent { - text: string; - includeInPath: boolean; -} - -export class TextFilterPanel { - private textInput: HTMLInputElement; - private includeInPathCheckbox: HTMLInputElement; - private send: DepGraphSend; - - constructor(private container: HTMLElement) { - const [_, send] = useDepGraphService(); - this.send = send; - this.render(); - } - - private static renderHtmlTemplate(): HTMLElement { - const render = document.createElement('template'); - render.innerHTML = ` -
    -
    -
    - - - - - - - -
    -
    -
    -
    -
    - -
    -
    - -

    Show libraries that are related to your search.

    -
    -
    -
    -
    - `.trim(); - return render.content.firstChild as HTMLElement; - } - - private render() { - removeChildrenFromContainer(this.container); - - const element = TextFilterPanel.renderHtmlTemplate(); - const resetInputElement: HTMLElement = - element.querySelector('#textFilterReset'); - resetInputElement.classList.add('hidden'); - - this.textInput = element.querySelector('input[type="text"]'); - this.textInput.addEventListener('keyup', (event) => { - if (event.key === 'Enter') { - this.send({ type: 'filterByText', search: this.textInput.value }); - } - - if (!!this.textInput.value.length) { - resetInputElement.classList.remove('hidden'); - this.includeInPathCheckbox.disabled = false; - } else { - resetInputElement.classList.add('hidden'); - this.includeInPathCheckbox.disabled = true; - } - }); - - fromEvent(this.textInput, 'keyup') - .pipe( - filter((event: KeyboardEvent) => event.key !== 'Enter'), - debounceTime(500), - map(() => - this.send({ type: 'filterByText', search: this.textInput.value }) - ) - ) - .subscribe(); - - this.includeInPathCheckbox = element.querySelector('#includeInPath'); - this.includeInPathCheckbox.addEventListener('change', () => - this.send({ - type: 'setIncludeProjectsByPath', - includeProjectsByPath: this.includeInPathCheckbox.checked, - }) - ); - - resetInputElement.addEventListener('click', () => { - this.textInput.value = ''; - this.includeInPathCheckbox.checked = false; - this.includeInPathCheckbox.disabled = true; - resetInputElement.classList.add('hidden'); - this.send([{ type: 'clearTextFilter' }]); - }); - - this.container.appendChild(element); - } -} diff --git a/dep-graph/dep-graph/src/app/util.ts b/dep-graph/dep-graph/src/app/util.ts deleted file mode 100644 index 0c0e44f750..0000000000 --- a/dep-graph/dep-graph/src/app/util.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { ProjectGraphDependency, ProjectGraphNode } from '@nrwl/devkit'; - -export function removeChildrenFromContainer(container: HTMLElement) { - Array.from(container.children).forEach((child) => - container.removeChild(child) - ); -} - -export function trimBackSlash(value: string): string { - return value.replace(/\/$/, ''); -} - -export function parseParentDirectoriesFromPilePath( - path: string, - workspaceRoot: string -) { - const root = trimBackSlash(path); - - // split the source root on directory separator - const split: string[] = root.split('/'); - - // check the first part for libs or apps, depending on workspaceLayout - if (split[0] === trimBackSlash(workspaceRoot)) { - split.shift(); - } - - // pop off the last element, which should be the lib name - split.pop(); - - return split; -} - -export function hasPath( - dependencies: Record, - target: string, - node: string, - visited: string[], - currentSearchDepth: number, - maxSearchDepth: number = -1 // -1 indicates unlimited search depth -) { - if (target === node) return true; - - if (maxSearchDepth === -1 || currentSearchDepth <= maxSearchDepth) { - for (let d of dependencies[node] || []) { - if (visited.indexOf(d.target) > -1) continue; - visited.push(d.target); - if ( - hasPath( - dependencies, - target, - d.target, - visited, - currentSearchDepth + 1, - maxSearchDepth - ) - ) - return true; - } - } - - return false; -} - -export function selectProjectsForFocusedProject( - projects: ProjectGraphNode[], - dependencies: Record, - focusedProjectName: string, - searchDepth: number -) { - return projects - .map((project) => project.name) - .filter( - (projectName) => - hasPath( - dependencies, - focusedProjectName, - projectName, - [], - 1, - searchDepth - ) || - hasPath( - dependencies, - projectName, - focusedProjectName, - [], - 1, - searchDepth - ) - ); -} - -export function filterProjectsByText( - text: string, - includeInPath: boolean, - searchDepth: number, - projects: ProjectGraphNode[], - dependencies: Record -) { - const split = text.split(',').map((splitItem) => splitItem.trim()); - - const selectedProjects = new Set(); - - projects - .map((project) => project.name) - .forEach((project) => { - const projectMatch = - split.findIndex((splitItem) => project.includes(splitItem)) > -1; - - if (projectMatch) { - selectedProjects.add(project); - - if (includeInPath) { - projects - .map((project) => project.name) - .forEach((projectInPath) => { - if ( - hasPath( - dependencies, - project, - projectInPath, - [], - 1, - searchDepth - ) || - hasPath( - dependencies, - projectInPath, - project, - [], - 1, - searchDepth - ) - ) { - selectedProjects.add(projectInPath); - } - }); - } - } - }); - - return Array.from(selectedProjects); -} diff --git a/dep-graph/dep-graph/src/index.html b/dep-graph/dep-graph/src/index.html deleted file mode 100644 index a53bfeb3af..0000000000 --- a/dep-graph/dep-graph/src/index.html +++ /dev/null @@ -1,188 +0,0 @@ - - - - - Nx Workspace Dependency Graph - - - - - - - - - - - - - - - - - - - - -
    - - -
    - -
    - - - -

    Please select projects in the sidebar.

    -
    -
    -
    - -
    -
    -
    - diff --git a/dep-graph/dep-graph/src/main.ts b/dep-graph/dep-graph/src/main.ts deleted file mode 100644 index 5f66ff45a9..0000000000 --- a/dep-graph/dep-graph/src/main.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { AppComponent } from './app/app'; -import { LocalProjectGraphService } from './app/local-project-graph-service'; -import { inspect } from '@xstate/inspect'; -import { ProjectGraphService } from './app/models'; -import { MockProjectGraphService } from './app/mock-project-graph-service'; -import { FetchProjectGraphService } from './app/fetch-project-graph-service'; - -if (window.useXstateInspect === true) { - inspect({ - url: 'https://stately.ai/viz?inspect', - iframe: false, // open in new window - }); -} - -let projectGraphService: ProjectGraphService; - -if (window.environment === 'dev') { - projectGraphService = new FetchProjectGraphService(); -} else if (window.environment === 'watch') { - projectGraphService = new MockProjectGraphService(); -} else if (window.environment === 'release') { - if (window.localMode === 'build') { - projectGraphService = new LocalProjectGraphService(); - } else { - projectGraphService = new FetchProjectGraphService(); - } -} - -setTimeout(() => new AppComponent(window.appConfig, projectGraphService)); diff --git a/dep-graph/dep-graph/src/test-setup.ts b/dep-graph/dep-graph/src/test-setup.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dep-graph/dep-graph/tsconfig.app.json b/dep-graph/dep-graph/tsconfig.app.json deleted file mode 100644 index 4206675676..0000000000 --- a/dep-graph/dep-graph/tsconfig.app.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../dist/out-tsc", - "types": ["node"], - "lib": ["DOM", "es2019"] - }, - "exclude": ["**/*.spec.ts", "**/*.test.ts"], - "include": ["**/*.ts"] -} diff --git a/dep-graph/dep-graph/tsconfig.spec.json b/dep-graph/dep-graph/tsconfig.spec.json deleted file mode 100644 index 1b59ddf639..0000000000 --- a/dep-graph/dep-graph/tsconfig.spec.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../dist/out-tsc", - "module": "commonjs", - "types": ["jest", "node"], - "lib": ["DOM"] - }, - "files": ["src/test-setup.ts"], - "include": ["**/*.spec.ts", "**/*.test.ts", "**/*.d.ts"] -} diff --git a/nx.json b/nx.json index 52ab9aed76..de7dcd1156 100644 --- a/nx.json +++ b/nx.json @@ -56,8 +56,18 @@ "generators": { "@nrwl/react": { "application": { + "style": "css", + "linter": "eslint", "babel": true + }, + "component": { + "style": "css" + }, + "library": { + "style": "css", + "linter": "eslint" } } - } + }, + "defaultProject": "dep-graph-client" } diff --git a/package.json b/package.json index 055897ee44..ef4fb03dfd 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "@tailwindcss/typography": "^0.4.1", "@testing-library/react": "11.2.6", "@testing-library/react-hooks": "7.0.1", + "@tippyjs/react": "^4.2.6", "@types/css-minimizer-webpack-plugin": "^3.0.2", "@types/cytoscape": "^3.18.2", "@types/eslint": "^8.2.0", @@ -119,6 +120,7 @@ "@typescript-eslint/parser": "~5.3.0", "@xstate/immer": "^0.2.0", "@xstate/inspect": "^0.5.1", + "@xstate/react": "^1.6.3", "angular": "1.8.0", "autoprefixer": "^10.2.5", "babel-jest": "27.2.3", @@ -147,7 +149,7 @@ "eslint-plugin-import": "2.25.2", "eslint-plugin-jsx-a11y": "6.4.1", "eslint-plugin-react": "7.23.1", - "eslint-plugin-react-hooks": "4.2.0", + "eslint-plugin-react-hooks": "4.3.0", "express": "4.17.1", "file-loader": "^6.2.0", "file-type": "^16.2.0", @@ -209,6 +211,7 @@ "react-redux": "7.2.3", "react-refresh": "^0.10.0", "react-router-dom": "5.1.2", + "react-test-renderer": "17.0.2", "regenerator-runtime": "0.13.7", "release-it": "^14.11.3", "rollup": "^2.56.2", @@ -233,7 +236,6 @@ "tcp-port-used": "^1.0.2", "terser": "4.3.8", "terser-webpack-plugin": "^5.1.1", - "tippy.js": "^6.3.1", "tmp": "~0.2.1", "tree-kill": "1.2.2", "ts-jest": "27.0.5", diff --git a/packages/workspace/project.json b/packages/workspace/project.json index 99ef71332a..fd01730956 100644 --- a/packages/workspace/project.json +++ b/packages/workspace/project.json @@ -2,7 +2,7 @@ "root": "packages/workspace", "sourceRoot": "packages/workspace/src", "projectType": "library", - "implicitDependencies": ["dep-graph-dep-graph"], + "implicitDependencies": ["dep-graph-client"], "targets": { "test": { "executor": "@nrwl/jest:jest", diff --git a/scripts/copy-dep-graph-environment.ts b/scripts/copy-dep-graph-environment.ts index b7722e208c..c457038f31 100644 --- a/scripts/copy-dep-graph-environment.ts +++ b/scripts/copy-dep-graph-environment.ts @@ -7,6 +7,6 @@ const mode = argv._[0]; console.log(`Setting up dep-graph for ${mode}`); copyFileSync( - `dep-graph/dep-graph/src/assets/environment.${mode}.js`, - `dep-graph/dep-graph/src/assets/environment.js` + `dep-graph/client/src/assets/environment.${mode}.js`, + `dep-graph/client/src/assets/environment.js` ); diff --git a/workspace.json b/workspace.json index 8ddeed969e..4321172434 100644 --- a/workspace.json +++ b/workspace.json @@ -6,8 +6,8 @@ "create-nx-plugin": "packages/create-nx-plugin", "create-nx-workspace": "packages/create-nx-workspace", "cypress": "packages/cypress", - "dep-graph-dep-graph": "dep-graph/dep-graph", - "dep-graph-dep-graph-e2e": "dep-graph/dep-graph-e2e", + "dep-graph-client": "/dep-graph/client", + "dep-graph-client-e2e": "dep-graph/client-e2e", "detox": "packages/detox", "devkit": "packages/devkit", "docs": "docs", diff --git a/yarn.lock b/yarn.lock index bde7c80643..14de427618 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3125,11 +3125,16 @@ schema-utils "^3.0.0" source-map "^0.7.3" -"@popperjs/core@^2.0.0", "@popperjs/core@^2.5.4", "@popperjs/core@^2.6.0", "@popperjs/core@^2.9.0", "@popperjs/core@^2.9.2": +"@popperjs/core@^2.0.0", "@popperjs/core@^2.5.4", "@popperjs/core@^2.6.0", "@popperjs/core@^2.9.2": version "2.10.2" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.2.tgz#0798c03351f0dea1a5a4cabddf26a55a7cbee590" integrity sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ== +"@popperjs/core@^2.9.0": + version "2.11.0" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.0.tgz#6734f8ebc106a0860dff7f92bf90df193f0935d7" + integrity sha512-zrsUxjLOKAzdewIDRWy9nsV1GQsKBCWaGwsZQlCgr6/q+vjyZhFgqedLfFBuI9anTPEUT4APq9Mu0SZBTzIcGQ== + "@reduxjs/toolkit@1.6.1": version "1.6.1" resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.6.1.tgz#7bc83b47352a663bf28db01e79d17ba54b98ade9" @@ -4298,6 +4303,13 @@ "@babel/runtime" "^7.12.5" "@testing-library/dom" "^7.28.1" +"@tippyjs/react@^4.2.6": + version "4.2.6" + resolved "https://registry.yarnpkg.com/@tippyjs/react/-/react-4.2.6.tgz#971677a599bf663f20bb1c60a62b9555b749cc71" + integrity sha512-91RicDR+H7oDSyPycI13q3b7o4O60wa2oRbjlz2fyRLmHImc4vyDwuUP8NtZaN0VARJY5hybvDYrFzhY9+Lbyw== + dependencies: + tippy.js "^6.3.1" + "@tokenizer/token@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276" @@ -4936,6 +4948,11 @@ dependencies: "@types/node" "*" +"@types/yoga-layout@1.9.2": + version "1.9.2" + resolved "https://registry.yarnpkg.com/@types/yoga-layout/-/yoga-layout-1.9.2.tgz#efaf9e991a7390dc081a0b679185979a83a9639a" + integrity sha512-S9q47ByT2pPvD65IvrWp7qppVMpk9WGMbVq9wbWZOHg6tnXSD4vyhao6nOSBwwfDdV2p3Kx9evA9vI+XWTfDvw== + "@typescript-eslint/eslint-plugin@~5.3.0": version "5.3.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.3.1.tgz#d8ff412f10f54f6364e7fd7c1e70eb6767f434c3" @@ -5524,6 +5541,14 @@ dependencies: fast-safe-stringify "^2.0.7" +"@xstate/react@^1.6.3": + version "1.6.3" + resolved "https://registry.yarnpkg.com/@xstate/react/-/react-1.6.3.tgz#706f3beb7bc5879a78088985c8fd43b9dab7f725" + integrity sha512-NCUReRHPGvvCvj2yLZUTfR0qVp6+apc8G83oXSjN4rl89ZjyujiKrTff55bze/HrsvCsP/sUJASf2n0nzMF1KQ== + dependencies: + use-isomorphic-layout-effect "^1.0.0" + use-subscription "^1.3.0" + "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" @@ -6256,6 +6281,11 @@ atomic-sleep@^1.0.0: resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== +auto-bind@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/auto-bind/-/auto-bind-4.0.0.tgz#e3589fc6c2da8f7ca43ba9f84fa52a744fc997fb" + integrity sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ== + autolinker@~0.15.0: version "0.15.3" resolved "https://registry.yarnpkg.com/autolinker/-/autolinker-0.15.3.tgz#342417d8f2f3461b14cf09088d5edf8791dc9832" @@ -7493,7 +7523,7 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== -cli-boxes@^2.2.1: +cli-boxes@^2.2.0, cli-boxes@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== @@ -7517,7 +7547,7 @@ cli-spinners@^1.0.1: resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-1.3.1.tgz#002c1990912d0d59580c93bd36c056de99e4259a" integrity sha512-1QL4544moEsDVH9T/l6Cemov/37iv1RtoKf7NJ04A60+4MREXNfx/QvavbH6QoGdsD4N4Mwy49cmaINR/o2mdg== -cli-spinners@^2.5.0: +cli-spinners@^2.3.0, cli-spinners@^2.5.0: version "2.6.1" resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d" integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g== @@ -7620,6 +7650,13 @@ coa@^2.0.2: chalk "^2.4.1" q "^1.1.2" +code-excerpt@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/code-excerpt/-/code-excerpt-3.0.0.tgz#fcfb6748c03dba8431c19f5474747fad3f250f10" + integrity sha512-VHNTVhd7KsLGOqfX3SyeO8RyYPMp1GJOg194VITk04WMYCv4plV68YWe6TJZxd9MhobjtpMRnVky01gqZsalaw== + dependencies: + convert-to-spaces "^1.0.1" + code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" @@ -8128,6 +8165,11 @@ convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.5.1, dependencies: safe-buffer "~5.1.1" +convert-to-spaces@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/convert-to-spaces/-/convert-to-spaces-1.0.2.tgz#7e3e48bbe6d997b1417ddca2868204b4d3d85715" + integrity sha1-fj5Iu+bZl7FBfdyihoIEtNPYVxU= + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -10425,12 +10467,7 @@ eslint-plugin-jsx-a11y@^6.4.1, eslint-plugin-jsx-a11y@^6.5.1: language-tags "^1.0.5" minimatch "^3.0.4" -eslint-plugin-react-hooks@4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz#8c229c268d468956334c943bb45fc860280f5556" - integrity sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ== - -eslint-plugin-react-hooks@^4.2.0, eslint-plugin-react-hooks@^4.3.0: +eslint-plugin-react-hooks@4.3.0, eslint-plugin-react-hooks@^4.2.0, eslint-plugin-react-hooks@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz#318dbf312e06fab1c835a4abef00121751ac1172" integrity sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA== @@ -12852,6 +12889,42 @@ injection-js@^2.4.0: dependencies: tslib "^2.0.0" +ink-spinner@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/ink-spinner/-/ink-spinner-4.0.3.tgz#0d0f4a787ae1a4270928e063d9c52527cb264feb" + integrity sha512-uJ4nbH00MM9fjTJ5xdw0zzvtXMkeGb0WV6dzSWvFv2/+ks6FIhpkt+Ge/eLdh0Ah6Vjw5pLMyNfoHQpRDRVFbQ== + dependencies: + cli-spinners "^2.3.0" + +ink@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ink/-/ink-3.2.0.tgz#434793630dc57d611c8fe8fffa1db6b56f1a16bb" + integrity sha512-firNp1q3xxTzoItj/eOOSZQnYSlyrWks5llCTVX37nJ59K3eXbQ8PtzCguqo8YI19EELo5QxaKnJd4VxzhU8tg== + dependencies: + ansi-escapes "^4.2.1" + auto-bind "4.0.0" + chalk "^4.1.0" + cli-boxes "^2.2.0" + cli-cursor "^3.1.0" + cli-truncate "^2.1.0" + code-excerpt "^3.0.0" + indent-string "^4.0.0" + is-ci "^2.0.0" + lodash "^4.17.20" + patch-console "^1.0.0" + react-devtools-core "^4.19.1" + react-reconciler "^0.26.2" + scheduler "^0.20.2" + signal-exit "^3.0.2" + slice-ansi "^3.0.0" + stack-utils "^2.0.2" + string-width "^4.2.2" + type-fest "^0.12.0" + widest-line "^3.1.0" + wrap-ansi "^6.2.0" + ws "^7.5.5" + yoga-layout-prebuilt "^1.9.6" + inline-style-parser@0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" @@ -17312,6 +17385,11 @@ pascalcase@^0.1.1: resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= +patch-console@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/patch-console/-/patch-console-1.0.0.tgz#19b9f028713feb8a3c023702a8cc8cb9f7466f9d" + integrity sha512-nxl9nrnLQmh64iTzMfyylSlRozL7kAXIaxw1fVcLYdyhNkJCRUzirRZTikXGJsg+hc4fqpneTK6iU2H1Q8THSA== + path-browserify@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" @@ -18820,6 +18898,14 @@ react-dev-utils@^11.0.4: strip-ansi "6.0.0" text-table "0.2.0" +react-devtools-core@^4.19.1: + version "4.22.1" + resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.22.1.tgz#b276d42f860bedc373c9b3c0f5f96734318dd453" + integrity sha512-pvpNDHE7p0FtcCmIWGazoY8LLVfBI9sw0Kf10kdHhPI9Tzt3OG/qEt16GrAbE0keuna5WzX3r1qPKVjqOqsuUg== + dependencies: + shell-quote "^1.6.1" + ws "^7" + react-docgen-typescript@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/react-docgen-typescript/-/react-docgen-typescript-2.1.1.tgz#c9f9ccb1fa67e0f4caf3b12f2a07512a201c2dcf" @@ -18926,7 +19012,7 @@ react-intersection-observer@^8.32.2: resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-8.32.2.tgz#527eeecf569309d64ed96330636d90aac336c957" integrity sha512-QTcea+n28AvOHbTku+jErfQqknbc4Nuh7EUNik8p/JMN56W2Jhjs+qcYZzQhAoyLX8pZD0QXpYX0lW87faackQ== -react-is@17.0.2, react-is@^17.0.0, react-is@^17.0.1, react-is@^17.0.2: +react-is@17.0.2, "react-is@^16.12.0 || ^17.0.0", react-is@^17.0.0, react-is@^17.0.1, react-is@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== @@ -18977,6 +19063,15 @@ react-popper@^2.2.4: react-fast-compare "^3.0.1" warning "^4.0.2" +react-reconciler@^0.26.2: + version "0.26.2" + resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.26.2.tgz#bbad0e2d1309423f76cf3c3309ac6c96e05e9d91" + integrity sha512-nK6kgY28HwrMNwDnMui3dvm3rCFjZrcGiuwLc5COUipBK5hWHLOxMJhSnSomirqWwjPBJKV1QcbkI0VJr7Gl1Q== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + scheduler "^0.20.2" + react-redux@7.2.3: version "7.2.3" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.3.tgz#4c084618600bb199012687da9e42123cca3f0be9" @@ -19057,6 +19152,14 @@ react-select@^3.2.0: react-input-autosize "^3.0.0" react-transition-group "^4.3.0" +react-shallow-renderer@^16.13.1: + version "16.14.1" + resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz#bf0d02df8a519a558fd9b8215442efa5c840e124" + integrity sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg== + dependencies: + object-assign "^4.1.1" + react-is "^16.12.0 || ^17.0.0" + react-sizeme@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-3.0.2.tgz#4a2f167905ba8f8b8d932a9e35164e459f9020e4" @@ -19089,6 +19192,16 @@ react-syntax-highlighter@^15.4.3: prismjs "^1.25.0" refractor "^3.2.0" +react-test-renderer@17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-17.0.2.tgz#4cd4ae5ef1ad5670fc0ef776e8cc7e1231d9866c" + integrity sha512-yaQ9cB89c17PUb0x6UfWRs7kQCorVdHlutU1boVPEsB8IDZH6n9tHxMacc3y0JoXOJUsZb/t/Mb8FUWMKaM7iQ== + dependencies: + object-assign "^4.1.1" + react-is "^17.0.2" + react-shallow-renderer "^16.13.1" + scheduler "^0.20.2" + react-textarea-autosize@^8.3.0: version "8.3.3" resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.3.3.tgz#f70913945369da453fd554c168f6baacd1fa04d8" @@ -20830,7 +20943,7 @@ stable@^0.1.8: resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== -stack-utils@^2.0.3: +stack-utils@^2.0.2, stack-utils@^2.0.3: version "2.0.5" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5" integrity sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA== @@ -22050,6 +22163,11 @@ type-detect@4.0.8: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== +type-fest@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.12.0.tgz#f57a27ab81c68d136a51fd71467eff94157fa1ee" + integrity sha512-53RyidyjvkGpnWPMF9bQgFtWp+Sl8O2Rp13VavmJgfAP9WWG6q6TkrKU8iyJdnwnfgHI6k2hTlgqH4aSdjoTbg== + type-fest@^0.18.0: version "0.18.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f" @@ -22506,7 +22624,7 @@ use-latest@^1.0.0: dependencies: use-isomorphic-layout-effect "^1.0.0" -use-subscription@1.5.1: +use-subscription@1.5.1, use-subscription@^1.3.0: version "1.5.1" resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.5.1.tgz#73501107f02fad84c6dd57965beb0b75c68c42d1" integrity sha512-Xv2a1P/yReAjAbhylMfFplFKj9GssgTwN7RlcTxBujFQcloStWNDQdc4g4NRWH9xS4i/FDk04vQBptAXoF3VcA== @@ -23350,6 +23468,11 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" +ws@^7, ws@^7.5.5: + version "7.5.6" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b" + integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA== + ws@^7.0.0, ws@^7.4.6: version "7.5.5" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881" @@ -23533,6 +23656,13 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +yoga-layout-prebuilt@^1.9.6: + version "1.10.0" + resolved "https://registry.yarnpkg.com/yoga-layout-prebuilt/-/yoga-layout-prebuilt-1.10.0.tgz#2936fbaf4b3628ee0b3e3b1df44936d6c146faa6" + integrity sha512-YnOmtSbv4MTf7RGJMK0FvZ+KD8OEe/J5BNnR0GHhD8J/XcG/Qvxgszm0Un6FTHWW4uHlTgP0IztiXQnGyIR45g== + dependencies: + "@types/yoga-layout" "1.9.2" + zone.js@~0.11.4: version "0.11.4" resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.11.4.tgz#0f70dcf6aba80f698af5735cbb257969396e8025"