chore(dep-graph): migrate dep-graph app to React (#8152)
This commit is contained in:
parent
a78d43189a
commit
717a560a54
2
.gitignore
vendored
2
.gitignore
vendored
@ -11,4 +11,4 @@ tmp
|
||||
jest.debug.config.js
|
||||
.tool-versions
|
||||
/.verdaccio/build/local-registry
|
||||
dep-graph/dep-graph/src/assets/environment.js
|
||||
dep-graph/client/src/assets/environment.js
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
33
dep-graph/client-e2e/project.json
Normal file
33
dep-graph/client-e2e/project.json
Normal file
@ -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"]
|
||||
}
|
||||
@ -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', () => {
|
||||
describe('select projects message', () => {
|
||||
it('should display on load', () => {
|
||||
getSelectProjectsMessage().should('be.visible');
|
||||
});
|
||||
|
||||
it('should hide select projects message when a project is selected', () => {
|
||||
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.be.visible');
|
||||
getSelectProjectsMessage().should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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]');
|
||||
@ -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', () => {
|
||||
11
dep-graph/client/.babelrc
Normal file
11
dep-graph/client/.babelrc
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@nrwl/react/babel",
|
||||
{
|
||||
"runtime": "automatic"
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": []
|
||||
}
|
||||
@ -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
|
||||
#
|
||||
21
dep-graph/client/.eslintrc.json
Normal file
21
dep-graph/client/.eslintrc.json
Normal file
@ -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": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
13
dep-graph/client/jest.config.js
Normal file
13
dep-graph/client/jest.config.js
Normal file
@ -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',
|
||||
};
|
||||
@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {
|
||||
config: './dep-graph/dep-graph/tailwind.config.js',
|
||||
config: './dep-graph/client/tailwind.config.js',
|
||||
},
|
||||
autoprefixer: {},
|
||||
},
|
||||
@ -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": []
|
||||
}
|
||||
12
dep-graph/client/src/app/app.tsx
Normal file
12
dep-graph/client/src/app/app.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { Shell } from './shell';
|
||||
import { GlobalStateProvider } from './state.provider';
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<GlobalStateProvider>
|
||||
<Shell></Shell>
|
||||
</GlobalStateProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
59
dep-graph/client/src/app/debugger-panel.tsx
Normal file
59
dep-graph/client/src/app/debugger-panel.tsx
Normal file
@ -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 (
|
||||
<div
|
||||
id="debugger-panel"
|
||||
className="
|
||||
w-auto
|
||||
text-gray-700
|
||||
bg-gray-50
|
||||
border-b border-gray-200
|
||||
p-4
|
||||
flex flex-column
|
||||
items-center
|
||||
justify-items-center
|
||||
gap-4
|
||||
"
|
||||
>
|
||||
<h4 className="text-lg font-bold mr-4">Debugger</h4>
|
||||
<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"
|
||||
data-cy="project-select"
|
||||
onChange={(event) => projectGraphChange(event.target.value)}
|
||||
value={selectedProjectGraph}
|
||||
>
|
||||
{projectGraphs.map((projectGraph) => {
|
||||
return (
|
||||
<option key={projectGraph.id} value={projectGraph.id}>
|
||||
{projectGraph.label}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<p className="text-sm">
|
||||
Last render took {lastPerfReport.renderTime}ms:{' '}
|
||||
<b className="font-mono text-medium">{lastPerfReport.numNodes} nodes</b>{' '}
|
||||
|{' '}
|
||||
<b className="font-mono text-medium">{lastPerfReport.numEdges} edges</b>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default DebuggerPanel;
|
||||
@ -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<string> {
|
||||
22
dep-graph/client/src/app/hooks/use-debounce.ts
Normal file
22
dep-graph/client/src/app/hooks/use-debounce.ts
Normal file
@ -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;
|
||||
}
|
||||
11
dep-graph/client/src/app/hooks/use-dep-graph-selector.ts
Normal file
11
dep-graph/client/src/app/hooks/use-dep-graph-selector.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { useSelector } from '@xstate/react';
|
||||
import { DepGraphState } from '../machines/interfaces';
|
||||
import { useDepGraphService } from './use-dep-graph';
|
||||
|
||||
export type DepGraphSelector<T> = (depGraphState: DepGraphState) => T;
|
||||
|
||||
export function useDepGraphSelector<T>(selectorFunc: DepGraphSelector<T>): T {
|
||||
const depGraphService = useDepGraphService();
|
||||
|
||||
return useSelector<typeof depGraphService, T>(depGraphService, selectorFunc);
|
||||
}
|
||||
8
dep-graph/client/src/app/hooks/use-dep-graph.ts
Normal file
8
dep-graph/client/src/app/hooks/use-dep-graph.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { useContext } from 'react';
|
||||
import { GlobalStateContext } from '../state.provider';
|
||||
|
||||
export function useDepGraphService() {
|
||||
const globalState = useContext(GlobalStateContext);
|
||||
|
||||
return globalState;
|
||||
}
|
||||
30
dep-graph/client/src/app/hooks/use-environment-config.ts
Normal file
30
dep-graph/client/src/app/hooks/use-environment-config.ts
Normal file
@ -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;
|
||||
}
|
||||
28
dep-graph/client/src/app/hooks/use-interval-when.ts
Normal file
28
dep-graph/client/src/app/hooks/use-interval-when.ts
Normal file
@ -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]);
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
@ -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<string> {
|
||||
@ -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) => {
|
||||
@ -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<DepGraphContext>
|
||||
>;
|
||||
|
||||
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;
|
||||
}
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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 = {
|
||||
@ -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(),
|
||||
@ -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<GraphPerfReport>();
|
||||
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(<CytoscapeDagreConfig>{
|
||||
.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,10 +229,15 @@ export class GraphService {
|
||||
includePath: boolean,
|
||||
searchDepth: number = -1
|
||||
) {
|
||||
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;
|
||||
return (
|
||||
split.findIndex((splitItem) => node.id().includes(splitItem)) > -1
|
||||
);
|
||||
});
|
||||
|
||||
if (includePath) {
|
||||
@ -246,6 +251,7 @@ export class GraphService {
|
||||
|
||||
this.transferToRenderGraph(filteredProjects.union(edgesToRender));
|
||||
}
|
||||
}
|
||||
|
||||
private transferToRenderGraph(elements: cy.Collection) {
|
||||
let currentFocusedProjectName;
|
||||
@ -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 });
|
||||
}
|
||||
}
|
||||
@ -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<GraphRenderEvents>;
|
||||
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;
|
||||
}
|
||||
>;
|
||||
43
dep-graph/client/src/app/machines/selectors.ts
Normal file
43
dep-graph/client/src/app/machines/selectors.ts
Normal file
@ -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<ProjectGraphNode[]> = (
|
||||
state
|
||||
) => state.context.projects;
|
||||
|
||||
export const workspaceLayoutSelector: DepGraphSelector<WorkspaceLayout> = (
|
||||
state
|
||||
) => state.context.workspaceLayout;
|
||||
|
||||
export const selectedProjectNamesSelector: DepGraphSelector<string[]> = (
|
||||
state
|
||||
) => state.context.selectedProjects;
|
||||
|
||||
export const projectIsSelectedSelector: DepGraphSelector<boolean> = (state) =>
|
||||
state.context.selectedProjects.length > 0;
|
||||
|
||||
export const lastPerfReportSelector: DepGraphSelector<GraphPerfReport> = (
|
||||
state
|
||||
) => state.context.lastPerfReport;
|
||||
|
||||
export const focusedProjectNameSelector: DepGraphSelector<string> = (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<boolean> = (state) =>
|
||||
state.context.includePath;
|
||||
|
||||
export const textFilterSelector: DepGraphSelector<string> = (state) =>
|
||||
state.context.textFilter;
|
||||
|
||||
export const hasAffectedProjectsSelector: DepGraphSelector<boolean> = (state) =>
|
||||
state.context.affectedProjects.length > 0;
|
||||
@ -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 = {
|
||||
@ -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 = {
|
||||
@ -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';
|
||||
190
dep-graph/client/src/app/shell.tsx
Normal file
190
dep-graph/client/src/app/shell.tsx
Normal file
@ -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<string>(
|
||||
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 (
|
||||
<>
|
||||
<Sidebar></Sidebar>
|
||||
<div id="main-content" className="flex-grow overflow-hidden">
|
||||
{environment.appConfig.showDebugger ? (
|
||||
<DebuggerPanel
|
||||
projectGraphs={environment.appConfig.projectGraphs}
|
||||
selectedProjectGraph={selectedProjectId}
|
||||
lastPerfReport={lastPerfReport}
|
||||
projectGraphChange={projectChange}
|
||||
></DebuggerPanel>
|
||||
) : null}
|
||||
|
||||
{!projectIsSelected ? (
|
||||
<div id="no-projects-chosen" className="flex text-gray-700">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-6 w-6 mr-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M11 15l-3-3m0 0l3-3m-3 3h8M3 12a9 9 0 1118 0 9 9 0 01-18 0z"
|
||||
/>
|
||||
</svg>
|
||||
<h4>Please select projects in the sidebar.</h4>
|
||||
</div>
|
||||
) : null}
|
||||
<div id="graph-container">
|
||||
<div id="cytoscape-graph"></div>
|
||||
<Tippy content="Download Graph as PNG" placement="right" theme="nx">
|
||||
<button
|
||||
type="button"
|
||||
className={`
|
||||
fixed
|
||||
z-50
|
||||
bottom-4
|
||||
right-4
|
||||
w-16
|
||||
h-16
|
||||
rounded-full
|
||||
bg-green-nx-base
|
||||
shadow-sm
|
||||
text-white
|
||||
block
|
||||
transition
|
||||
duration-300
|
||||
transform
|
||||
${!projectIsSelected ? 'opacity-0' : ''}
|
||||
`}
|
||||
data-cy="downloadImageButton"
|
||||
onClick={downloadImage}
|
||||
>
|
||||
<svg
|
||||
height="24"
|
||||
width="24"
|
||||
className="absolute top-1/2 left-1/2 -mt-3 -ml-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</Tippy>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
59
dep-graph/client/src/app/sidebar/focused-project-panel.tsx
Normal file
59
dep-graph/client/src/app/sidebar/focused-project-panel.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
export interface FocusedProjectPanelProps {
|
||||
focusedProject: string;
|
||||
resetFocus: () => void;
|
||||
}
|
||||
|
||||
export const FocusedProjectPanel = memo(
|
||||
({ focusedProject, resetFocus }: FocusedProjectPanelProps) => {
|
||||
return (
|
||||
<div className="mt-10 px-4">
|
||||
<div
|
||||
className="p-2 shadow-sm bg-green-nx-base text-gray-50 border border-gray-200 rounded-md flex items-center group relative cursor-pointer overflow-hidden"
|
||||
data-cy="unfocusButton"
|
||||
onClick={() => resetFocus()}
|
||||
>
|
||||
<p className="truncate transition duration-200 ease-in-out group-hover:opacity-60">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-6 w-6 inline -mt-1 mr-1"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span id="focused-project-name">Focused on {focusedProject}</span>
|
||||
</p>
|
||||
<div className="absolute right-2 flex transition-all translate-x-32 transition duration-200 ease-in-out group-hover:translate-x-0 pl-2 rounded-md text-gray-700 items-center text-sm font-medium bg-white shadow-sm ring-1 ring-gray-500">
|
||||
Reset
|
||||
<span className="p-1 rounded-md">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default FocusedProjectPanel;
|
||||
39
dep-graph/client/src/app/sidebar/group-by-folder-panel.tsx
Normal file
39
dep-graph/client/src/app/sidebar/group-by-folder-panel.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
export interface DisplayOptionsPanelProps {
|
||||
groupByFolderChanged: (checked: boolean) => void;
|
||||
}
|
||||
|
||||
export const GroupByFolderPanel = memo(
|
||||
({ groupByFolderChanged }: DisplayOptionsPanelProps) => {
|
||||
return (
|
||||
<div className="mt-8 px-4">
|
||||
<div className="flex items-start">
|
||||
<div className="flex items-center h-5">
|
||||
<input
|
||||
id="displayOptions"
|
||||
name="displayOptions"
|
||||
value="groupByFolder"
|
||||
type="checkbox"
|
||||
className="h-4 w-4 border-gray-300 rounded"
|
||||
onChange={(event) => groupByFolderChanged(event.target.checked)}
|
||||
></input>
|
||||
</div>
|
||||
<div className="ml-3 text-sm">
|
||||
<label
|
||||
htmlFor="displayOptions"
|
||||
className="cursor-pointer font-medium text-gray-700"
|
||||
>
|
||||
Group by folder
|
||||
</label>
|
||||
<p className="text-gray-500">
|
||||
Visually arrange libraries by folders with different colors.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default GroupByFolderPanel;
|
||||
280
dep-graph/client/src/app/sidebar/project-list.tsx
Normal file
280
dep-graph/client/src/app/sidebar/project-list.tsx
Normal file
@ -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<string, SidebarProject[]>;
|
||||
|
||||
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 (
|
||||
<li className="text-xs text-gray-600 block cursor-default select-none relative py-1 pl-3 pr-9">
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
className="flex rounded-md"
|
||||
title="Focus on this library"
|
||||
onClick={() => focusProject(project.projectGraphNode.name)}
|
||||
>
|
||||
<span className="p-1 rounded-md flex items-center font-medium bg-white transition hover:bg-gray-50 shadow-sm ring-1 ring-gray-200">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2h-1.528A6 6 0 004 9.528V4z" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M8 10a4 4 0 00-3.446 6.032l-1.261 1.26a1 1 0 101.414 1.415l1.261-1.261A4 4 0 108 10zm-2 4a2 2 0 114 0 2 2 0 01-4 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
<label
|
||||
className="font-mono font-normal ml-3 p-2 transition hover:bg-gray-50 cursor-pointer block rounded-md truncate w-full"
|
||||
data-project={project.projectGraphNode.name}
|
||||
data-active={project.isSelected}
|
||||
onClick={() =>
|
||||
toggleProject(project.projectGraphNode.name, project.isSelected)
|
||||
}
|
||||
>
|
||||
{project.projectGraphNode.name}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{project.isSelected ? (
|
||||
<span
|
||||
title="This library is visible"
|
||||
className="text-green-nx-base absolute inset-y-0 right-0 flex items-center cursor-pointer"
|
||||
onClick={() =>
|
||||
toggleProject(project.projectGraphNode.name, project.isSelected)
|
||||
}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
) : null}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<h3 className="mt-4 py-2 uppercase tracking-wide font-semibold text-sm lg:text-xs text-gray-900 cursor-text">
|
||||
{headerText}
|
||||
</h3>
|
||||
<ul className="mt-2 -ml-3">
|
||||
{sortedProjects.map((project) => {
|
||||
return (
|
||||
<ProjectListItem
|
||||
key={project.projectGraphNode.name}
|
||||
project={project}
|
||||
toggleProject={toggleProject}
|
||||
focusProject={focusProject}
|
||||
></ProjectListItem>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div id="project-lists" className="mt-8 px-4 border-t border-gray-200">
|
||||
<h2 className="mt-8 text-lg font-bold border-b border-gray-50 border-solid">
|
||||
app projects
|
||||
</h2>
|
||||
|
||||
{sortedAppDirectories.map((directoryName) => {
|
||||
return (
|
||||
<SubProjectList
|
||||
key={'app-' + directoryName}
|
||||
headerText={directoryName}
|
||||
projects={appDirectoryGroups[directoryName]}
|
||||
deselectProject={deselectProject}
|
||||
selectProject={selectProject}
|
||||
focusProject={focusProject}
|
||||
></SubProjectList>
|
||||
);
|
||||
})}
|
||||
|
||||
<h2 className="mt-8 text-lg font-bold border-b border-gray-50 border-solid">
|
||||
e2e projects
|
||||
</h2>
|
||||
|
||||
{sortedE2EDirectories.map((directoryName) => {
|
||||
return (
|
||||
<SubProjectList
|
||||
key={'e2e-' + directoryName}
|
||||
headerText={directoryName}
|
||||
projects={e2eDirectoryGroups[directoryName]}
|
||||
deselectProject={deselectProject}
|
||||
selectProject={selectProject}
|
||||
focusProject={focusProject}
|
||||
></SubProjectList>
|
||||
);
|
||||
})}
|
||||
|
||||
<h2 className="mt-8 text-lg font-bold border-b border-gray-50 border-solid">
|
||||
lib projects
|
||||
</h2>
|
||||
|
||||
{sortedLibDirectories.map((directoryName) => {
|
||||
return (
|
||||
<SubProjectList
|
||||
key={'lib-' + directoryName}
|
||||
headerText={directoryName}
|
||||
projects={libDirectoryGroups[directoryName]}
|
||||
deselectProject={deselectProject}
|
||||
selectProject={selectProject}
|
||||
focusProject={focusProject}
|
||||
></SubProjectList>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProjectList;
|
||||
102
dep-graph/client/src/app/sidebar/search-depth.tsx
Normal file
102
dep-graph/client/src/app/sidebar/search-depth.tsx
Normal file
@ -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 (
|
||||
<div className="mt-4 px-4">
|
||||
<div className="mt-4 flex items-start">
|
||||
<div className="flex items-center h-5">
|
||||
<input
|
||||
id="depthFilter"
|
||||
name="depthFilter"
|
||||
value="depthFilterActivated"
|
||||
type="checkbox"
|
||||
className="h-4 w-4 border-gray-300 rounded"
|
||||
checked={searchDepthEnabled}
|
||||
onChange={(event) =>
|
||||
searchDepthFilterEnabledChange(event.target.checked)
|
||||
}
|
||||
></input>
|
||||
</div>
|
||||
<div className="ml-3 text-sm">
|
||||
<label
|
||||
htmlFor="depthFilter"
|
||||
className="cursor-pointer font-medium text-gray-700"
|
||||
>
|
||||
Activate proximity
|
||||
</label>
|
||||
<p className="text-gray-500">
|
||||
Explore connected libraries step by step.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 px-10">
|
||||
<div className="flex rounded-md shadow-sm text-gray-500">
|
||||
<button
|
||||
title="Remove ancestor level"
|
||||
className="inline-flex items-center py-2 px-4 rounded-l-md border border-gray-300 bg-gray-50 text-gray-500 hover:bg-gray-100"
|
||||
onClick={decrementDepthFilter}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M20 12H4"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<span
|
||||
id="depthFilterValue"
|
||||
className="p-1.5 bg-white flex-1 block w-full rounded-none border-t border-b border-gray-300 text-center font-mono"
|
||||
>
|
||||
{searchDepth}
|
||||
</span>
|
||||
<button
|
||||
title="Add ancestor level"
|
||||
className="inline-flex items-center py-2 px-4 rounded-r-md border border-gray-300 bg-gray-50 text-gray-500 hover:bg-gray-100"
|
||||
onClick={incrementDepthFilter}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default SearchDepth;
|
||||
100
dep-graph/client/src/app/sidebar/show-hide-projects.tsx
Normal file
100
dep-graph/client/src/app/sidebar/show-hide-projects.tsx
Normal file
@ -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 (
|
||||
<div className="mt-8 px-4">
|
||||
<button
|
||||
onClick={showAllProjects}
|
||||
type="button"
|
||||
className="w-full flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
|
||||
data-cy="selectAllButton"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="-ml-1 mr-2 h-5 w-5 text-gray-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
/>
|
||||
</svg>
|
||||
Show all projects
|
||||
</button>
|
||||
|
||||
{affectedProjects ? (
|
||||
<button
|
||||
onClick={showAffectedProjects}
|
||||
type="button"
|
||||
className="mt-3 w-full flex items-center px-4 py-2 border border-red-300 rounded-md shadow-sm text-sm font-medium text-red-500 bg-white hover:bg-red-50"
|
||||
data-cy="affectedButton"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="-ml-1 mr-2 h-5 w-5 text-red-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||
/>
|
||||
</svg>
|
||||
Show affected projects
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
onClick={hideAllProjects}
|
||||
type="button"
|
||||
className="mt-3 w-full flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
|
||||
data-cy="deselectAllButton"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="-ml-1 mr-2 h-5 w-5 text-gray-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
|
||||
/>
|
||||
</svg>
|
||||
Hide all projects
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default ShowHideAllProjects;
|
||||
181
dep-graph/client/src/app/sidebar/sidebar.tsx
Normal file
181
dep-graph/client/src/app/sidebar/sidebar.tsx
Normal file
@ -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 (
|
||||
<div
|
||||
className="
|
||||
flex flex-col
|
||||
h-full
|
||||
overflow-scroll
|
||||
w-72
|
||||
pb-10
|
||||
shadow-lg
|
||||
ring-1 ring-gray-400 ring-opacity-10
|
||||
relative
|
||||
"
|
||||
id="sidebar"
|
||||
>
|
||||
<div className="bg-blue-nx-base">
|
||||
<div className="flex items-center justify-start mx-4 my-5 text-white">
|
||||
<svg
|
||||
className="h-10 w-auto"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>Nx</title>
|
||||
<path d="M11.987 14.138l-3.132 4.923-5.193-8.427-.012 8.822H0V4.544h3.691l5.247 8.833.005-3.998 3.044 4.759zm.601-5.761c.024-.048 0-3.784.008-3.833h-3.65c.002.059-.005 3.776-.003 3.833h3.645zm5.634 4.134a2.061 2.061 0 0 0-1.969 1.336 1.963 1.963 0 0 1 2.343-.739c.396.161.917.422 1.33.283a2.1 2.1 0 0 0-1.704-.88zm3.39 1.061c-.375-.13-.8-.277-1.109-.681-.06-.08-.116-.17-.176-.265a2.143 2.143 0 0 0-.533-.642c-.294-.216-.68-.322-1.18-.322a2.482 2.482 0 0 0-2.294 1.536 2.325 2.325 0 0 1 4.002.388.75.75 0 0 0 .836.334c.493-.105.46.36 1.203.518v-.133c-.003-.446-.246-.55-.75-.733zm2.024 1.266a.723.723 0 0 0 .347-.638c-.01-2.957-2.41-5.487-5.37-5.487a5.364 5.364 0 0 0-4.487 2.418c-.01-.026-1.522-2.39-1.538-2.418H8.943l3.463 5.423-3.379 5.32h3.54l1.54-2.366 1.568 2.366h3.541l-3.21-5.052a.7.7 0 0 1-.084-.32 2.69 2.69 0 0 1 2.69-2.691h.001c1.488 0 1.736.89 2.057 1.308.634.826 1.9.464 1.9 1.541a.707.707 0 0 0 1.066.596zm.35.133c-.173.372-.56.338-.755.639-.176.271.114.412.114.412s.337.156.538-.311c.104-.231.14-.488.103-.74z" />
|
||||
</svg>
|
||||
<span className="ml-4 text-xl font-medium"> Dependency Graph </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
id="help"
|
||||
className="
|
||||
mt-3
|
||||
px-4
|
||||
text-xs text-gray-500
|
||||
flex
|
||||
items-center
|
||||
cursor-pointer
|
||||
hover:underline
|
||||
"
|
||||
href="https://nx.dev/structure/dependency-graph"
|
||||
rel="nofollow noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4 mr-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
Analyse and visualize your workspace.
|
||||
</a>
|
||||
|
||||
{focusedProject ? (
|
||||
<FocusedProjectPanel
|
||||
focusedProject={focusedProject}
|
||||
resetFocus={resetFocus}
|
||||
></FocusedProjectPanel>
|
||||
) : null}
|
||||
|
||||
<TextFilterPanel
|
||||
includePath={includePath}
|
||||
resetTextFilter={resetTextFilter}
|
||||
textFilter={textFilter}
|
||||
toggleIncludeLibsInPathChange={includeLibsInPathChange}
|
||||
updateTextFilter={updateTextFilter}
|
||||
></TextFilterPanel>
|
||||
|
||||
<div>
|
||||
<ShowHideProjects
|
||||
hideAllProjects={hideAllProjects}
|
||||
showAllProjects={showAllProjects}
|
||||
showAffectedProjects={showAffectedProjects}
|
||||
hasAffectedProjects={hasAffectedProjects}
|
||||
></ShowHideProjects>
|
||||
|
||||
<GroupByFolderPanel
|
||||
groupByFolderChanged={groupByFolderChanged}
|
||||
></GroupByFolderPanel>
|
||||
|
||||
<SearchDepth
|
||||
searchDepth={searchDepthInfo.searchDepth}
|
||||
searchDepthEnabled={searchDepthInfo.searchDepthEnabled}
|
||||
searchDepthFilterEnabledChange={searchDepthFilterEnabledChange}
|
||||
incrementDepthFilter={incrementDepthFilter}
|
||||
decrementDepthFilter={decrementDepthFilter}
|
||||
></SearchDepth>
|
||||
</div>
|
||||
|
||||
<ProjectList></ProjectList>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Sidebar;
|
||||
139
dep-graph/client/src/app/sidebar/text-filter-panel.tsx
Normal file
139
dep-graph/client/src/app/sidebar/text-filter-panel.tsx
Normal file
@ -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<HTMLInputElement>) {
|
||||
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 (
|
||||
<div>
|
||||
<div className="mt-10 px-4">
|
||||
<form
|
||||
className="flex rounded-md shadow-sm relative"
|
||||
onSubmit={(event) => event.preventDefault()}
|
||||
>
|
||||
<span className="inline-flex items-center p-2 rounded-l-md border border-r-0 border-gray-300 bg-gray-50 text-gray-500">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
className="p-1.5 bg-white text-gray-600 flex-1 block w-full rounded-none rounded-r-md border border-gray-300"
|
||||
placeholder="lib name, other lib name"
|
||||
data-cy="textFilterInput"
|
||||
name="filter"
|
||||
value={currentTextFilter}
|
||||
onKeyUp={onTextFilterKeyUp}
|
||||
onChange={(event) => onTextInputChange(event.currentTarget.value)}
|
||||
></input>
|
||||
{currentTextFilter.length > 0 ? (
|
||||
<button
|
||||
data-cy="textFilterReset"
|
||||
type="reset"
|
||||
onClick={resetClicked}
|
||||
className="p-1 top-1 right-1 absolute bg-white inline-block rounded-md text-gray-500"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M12 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2M3 12l6.414 6.414a2 2 0 001.414.586H19a2 2 0 002-2V7a2 2 0 00-2-2h-8.172a2 2 0 00-1.414.586L3 12z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
) : null}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 px-4">
|
||||
<div className="flex items-start">
|
||||
<div className="flex items-center h-5">
|
||||
<input
|
||||
disabled={textFilter.length === 0}
|
||||
id="includeInPath"
|
||||
name="textFilterCheckbox"
|
||||
type="checkbox"
|
||||
value="includeInPath"
|
||||
className="h-4 w-4 border-gray-300 rounded"
|
||||
checked={includePath}
|
||||
onChange={toggleIncludeLibsInPathChange}
|
||||
></input>
|
||||
</div>
|
||||
<div className="ml-3 text-sm">
|
||||
<label
|
||||
htmlFor="includeInPath"
|
||||
className="font-medium text-gray-700 cursor-pointer"
|
||||
>
|
||||
Include related libraries
|
||||
</label>
|
||||
<p className="text-gray-500">
|
||||
Show libraries that are related to your search.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TextFilterPanel;
|
||||
18
dep-graph/client/src/app/state.provider.tsx
Normal file
18
dep-graph/client/src/app/state.provider.tsx
Normal file
@ -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<typeof depGraphMachine>
|
||||
>({} as InterpreterFrom<typeof depGraphMachine>);
|
||||
|
||||
export const GlobalStateProvider = (props) => {
|
||||
const depGraphService = getDepGraphService();
|
||||
|
||||
return (
|
||||
<GlobalStateContext.Provider value={depGraphService}>
|
||||
{props.children}
|
||||
</GlobalStateContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -1,6 +1,6 @@
|
||||
import { parseParentDirectoriesFromPilePath } from './util';
|
||||
|
||||
describe('parseParentDirectoriesFromPilePath', () => {
|
||||
describe('parseParentDirectoriesFromFilePath', () => {
|
||||
// path, workspaceRoot, output
|
||||
const cases: [string, string, string[]][] = [
|
||||
['apps/app1', 'apps', []],
|
||||
56
dep-graph/client/src/app/util.ts
Normal file
56
dep-graph/client/src/app/util.ts
Normal file
@ -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<string, ProjectGraphDependency[]>,
|
||||
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;
|
||||
}
|
||||
3
dep-graph/client/src/environments/environment.prod.ts
Normal file
3
dep-graph/client/src/environments/environment.prod.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const environment = {
|
||||
production: true,
|
||||
};
|
||||
6
dep-graph/client/src/environments/environment.ts
Normal file
6
dep-graph/client/src/environments/environment.ts
Normal file
@ -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,
|
||||
};
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@ -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 {
|
||||
16
dep-graph/client/src/index.html
Normal file
16
dep-graph/client/src/index.html
Normal file
@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Nx Workspace Dependency Graph</title>
|
||||
<base href="/" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
|
||||
<script id="environment" src="environment.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
18
dep-graph/client/src/main.tsx
Normal file
18
dep-graph/client/src/main.tsx
Normal file
@ -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(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
document.getElementById('app')
|
||||
);
|
||||
@ -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;
|
||||
}
|
||||
23
dep-graph/client/tsconfig.app.json
Normal file
23
dep-graph/client/tsconfig.app.json
Normal file
@ -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"]
|
||||
}
|
||||
@ -1,5 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
24
dep-graph/client/tsconfig.spec.json
Normal file
24
dep-graph/client/tsconfig.spec.json
Normal file
@ -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"
|
||||
]
|
||||
}
|
||||
@ -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"]
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
{
|
||||
"presets": ["@nrwl/web/babel"]
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
{ "extends": "../../.eslintrc", "rules": {}, "ignorePatterns": ["!**/*"] }
|
||||
@ -1,15 +0,0 @@
|
||||
module.exports = {
|
||||
name: 'dep-graph-dep-graph',
|
||||
preset: '../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||
},
|
||||
},
|
||||
transform: {
|
||||
'^.+\\.[tj]s$': 'ts-jest',
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'js', 'html'],
|
||||
coverageDirectory: '../../coverage/dep-graph/dep-graph',
|
||||
};
|
||||
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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: <b class="font-mono text-medium">${renderTime.numNodes} nodes</b> | <b class="font-mono text-medium">${renderTime.numEdges} edges</b>.`;
|
||||
}
|
||||
|
||||
private selectProjectSubject = new Subject<string>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -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 = `
|
||||
<div>
|
||||
<div class="mt-8 px-4">
|
||||
<button type="button" class="w-full flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50" data-cy="selectAllButton">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="-ml-1 mr-2 h-5 w-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
Show all projects
|
||||
</button>
|
||||
<button type="button" class="mt-3 w-full flex items-center px-4 py-2 border border-red-300 rounded-md shadow-sm text-sm font-medium text-red-500 bg-white hover:bg-red-50 hidden" data-cy="affectedButton">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="-ml-1 mr-2 h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
Show affected projects
|
||||
</button>
|
||||
<button type="button" class="mt-3 w-full flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50" data-cy="deselectAllButton">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="-ml-1 mr-2 h-5 w-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
|
||||
</svg>
|
||||
Hide all projects
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 px-4">
|
||||
<div class="flex items-start">
|
||||
<div class="flex items-center h-5">
|
||||
<input id="displayOptions" name="displayOptions" value="groupByFolder" type="checkbox" class="h-4 w-4 border-gray-300 rounded">
|
||||
</div>
|
||||
<div class="ml-3 text-sm">
|
||||
<label for="displayOptions" class="cursor-pointer font-medium text-gray-700">Group by folder</label>
|
||||
<p class="text-gray-500">Visually arrange libraries by folders with different colors.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 px-4">
|
||||
<div class="mt-4 flex items-start">
|
||||
<div class="flex items-center h-5">
|
||||
<input id="depthFilter" name="depthFilter" value="groupByFolder" type="checkbox" class="h-4 w-4 border-gray-300 rounded">
|
||||
</div>
|
||||
<div class="ml-3 text-sm">
|
||||
<label for="depthFilter" class="cursor-pointer font-medium text-gray-700">Activate proximity</label>
|
||||
<p class="text-gray-500">Explore connected libraries step by step.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 px-10">
|
||||
<div class="flex rounded-md shadow-sm text-gray-500">
|
||||
<button id="depthFilterDecrement" title="Remove ancestor level" class="inline-flex items-center py-2 px-4 rounded-l-md border border-gray-300 bg-gray-50 text-gray-500 hover:bg-gray-100">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4" />
|
||||
</svg>
|
||||
</button>
|
||||
<span id="depthFilterValue" class="p-1.5 bg-white flex-1 block w-full rounded-none border-t border-b border-gray-300 text-center font-mono">1</span>
|
||||
<button id="depthFilterIncrement" title="Add ancestor level" class="inline-flex items-center py-2 px-4 rounded-r-md border border-gray-300 bg-gray-50 text-gray-500 hover:bg-gray-100">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`.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: (<HTMLInputElement>event.target).checked,
|
||||
})
|
||||
);
|
||||
|
||||
this.container.appendChild(element);
|
||||
}
|
||||
}
|
||||
@ -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 = `
|
||||
<div class="mt-10 px-4">
|
||||
<div class="p-2 shadow-sm bg-green-nx-base text-gray-50 border border-gray-200 rounded-md flex items-center group relative cursor-pointer overflow-hidden" data-cy="unfocusButton">
|
||||
<p class="truncate transition duration-200 ease-in-out group-hover:opacity-60">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline -mt-1 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span id="focused-project-name">e2e-some-other-very-long-project-name</span>
|
||||
</p>
|
||||
<div class="absolute right-2 flex transition-all translate-x-32 transition duration-200 ease-in-out group-hover:translate-x-0 pl-2 rounded-md text-gray-700 items-center text-sm font-medium bg-white shadow-sm ring-1 ring-gray-500">
|
||||
Reset
|
||||
<span class="p-1 rounded-md">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`.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);
|
||||
}
|
||||
}
|
||||
@ -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<string, HTMLElement> = {};
|
||||
|
||||
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 = `
|
||||
<li class="text-xs text-gray-600 block cursor-default select-none relative py-1 pl-3 pr-9">
|
||||
<div class="flex items-center">
|
||||
<button type="button" class="flex rounded-md" title="Focus on this library">
|
||||
<span class="p-1 rounded-md flex items-center font-medium bg-white transition hover:bg-gray-50 shadow-sm ring-1 ring-gray-200">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2h-1.528A6 6 0 004 9.528V4z" />
|
||||
<path fill-rule="evenodd" d="M8 10a4 4 0 00-3.446 6.032l-1.261 1.26a1 1 0 101.414 1.415l1.261-1.261A4 4 0 108 10zm-2 4a2 2 0 114 0 2 2 0 01-4 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
<label class="font-mono font-normal ml-3 p-2 transition hover:bg-gray-50 cursor-pointer block rounded-md truncate w-full" data-project="project-name" data-active="false">
|
||||
project-name
|
||||
</label>
|
||||
</div>
|
||||
<span role="selection-icon" title="This library is visible" class="text-green-nx-base absolute inset-y-0 right-0 flex items-center cursor-pointer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
`.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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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 = `
|
||||
<div>
|
||||
<div class="mt-10 px-4">
|
||||
<form class="flex rounded-md shadow-sm relative" onSubmit="return false">
|
||||
<span class="inline-flex items-center p-2 rounded-l-md border border-r-0 border-gray-300 bg-gray-50 text-gray-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||||
</svg>
|
||||
</span>
|
||||
<input type="text" class="p-1.5 bg-white text-gray-600 flex-1 block w-full rounded-none rounded-r-md border border-gray-300" placeholder="lib name, other lib name" data-cy="textFilterInput" name="filter">
|
||||
<button id="textFilterReset" type="reset" class="p-1 top-1 right-1 absolute bg-white inline-block rounded-md text-gray-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2M3 12l6.414 6.414a2 2 0 001.414.586H19a2 2 0 002-2V7a2 2 0 00-2-2h-8.172a2 2 0 00-1.414.586L3 12z" />
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="mt-4 px-4">
|
||||
<div class="flex items-start">
|
||||
<div class="flex items-center h-5">
|
||||
<input disabled id="includeInPath" name="textFilterCheckbox" type="checkbox" value="includeInPath" class="h-4 w-4 border-gray-300 rounded">
|
||||
</div>
|
||||
<div class="ml-3 text-sm">
|
||||
<label for="includeInPath" class="font-medium text-gray-700 cursor-pointer">Include related libraries</label>
|
||||
<p class="text-gray-500">Show libraries that are related to your search.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`.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);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user